2014-12-14

ApexからAmazon Lambda叩いてみる。

Amazon Lambdaというすんばらしいサービスがプレビュー提供されたということでApexから触ってみた。

 

1. Lambda側のfunctionを書く

今回はChatter投稿するようなスクリプトを書いてみた。
var jsforce = require('jsforce');
var username = "test@example.com";
var password = "hogefuga";
var loginUrl = "https://login.salesforce.com";

exports.handler = function(event, context) {
    var conn = new jsforce.Connection({
    loginUrl : loginUrl
    });

    var chatter_body = {
        body: {
            messageSegments: [{
                type: 'Text',
                text: "hello " + event.content + "!!"
            }]
        }
    };
    conn.login(username, password, function(err, res) {
        conn.chatter
            .resource("/feeds/record/" + event.parent_id + "/feed-items")
            .create(chatter_body, function(err, result) {
                console.log(result);
                console.log(err);
                context.done(null, 'chatter de lambda');
            });
    });
};

上記スクリプトをindex.jsとして保存する。

必要なパッケージもローカルインストールしておく。今回はjsforceのみ。

npm install jsforce

2. 作成したLambda functionをアップロードする

node_modulesと対象のスクリプトファイルをまとめてzipファイルにする。
$ zip -r lambda.zip index.js node_modules

で、アップロードする。

AWS Lambda

Function Name, Description→適当に

Function Code→生成したZipファイルをアップロード。File nameとHandler nameはそのまま。

Role name→Functionの実行コンテキストになるロールを指定する。Create/Select Roleで作成・指定すればOK。

Advanced Settings→一回あたりのFunction実行の利用可能なメモリやタイムアウト時間を適当にセット。

3. 動作確認

作成したLambda Functionが正しく動作するかを確認する。

まずは確認対象のLambda Functionを選択してEdit/Testをクリック。

lambda_select

Customを選択して適当なJSONをセットしてInvokeをクリック

AWS Lambda_Test

下の方のExecution resultsにログが出るので確認。

lambda_test

日本語が???とかに文字化けてますが、CloudWatch側のログにはちゃんと残ってます。

CloudWatchで見る場合はLogsから対象のFunctionを選択して掘り下げていくとログが見れます。

lambda_log_list

lambda_log

4. Lambdaを呼び出すApexクラスを作成

現時点ではLambdaのドキュメントにはREST APIの仕様があまり書かれていないっぽくて

Signature Versionがどれに対応しているかとかよくわからなかったんですが、

JavascriptのSDK見たらSignatureのVersion指定のパラメータで

v2,v3,v4が指定できる的なことが書いてあったのでv4で実装してみたら、普通に呼び出せました!

Signature Version4での生成方法はこちら

 

で、いつも通り汚い感じですが、Apexで書くとこんな感じ↓

public with sharing class Lambda {
    private final String SERVICE = 'lambda';
    private final String DEFAULT_REGION = 'us-east-1';
    
    /**
     * Access Key Id
     */
    private String AWSAccessKeyId = 'AKI******************';
    
    /**
     * Access Key Secret
     */
    private String AWSAccessKeySecret = '*************************';
    
    /**
     * Function呼び出し
     */
    public String invokeAsync(
        String functionName,
        Map<String, Object> payLoad,
        String region
    ) {
        if (String.isBlank(region)) {
            region = DEFAULT_REGION;
        }
        Map<String, String> params = new Map<String, String>{};
        
        List<String> signedHeaders = new List<String>{
            'host', 'x-amz-date'
        };
        
        DateTime dt = DateTime.now();
        String credentialScope = dt.formatGmt('YYYYMMdd') + '/' + 
                                 region + '/' + SERVICE + '/aws4_request';
        
        String jsonBody = Json.serialize(payLoad);
        String resource_path = '/2014-11-13/functions/' + functionName + '/invoke-async/';
        String hostname = 'lambda.' + region + '.amazonaws.com';
        String stringToSign = this.createStringToSign(
            'AWS4-HMAC-SHA256',
            dt, 
            credentialScope, 
            this.createHashedCanonicalRequest(
                'SHA256',
                'POST',
                resource_path,
                params,
                new Map<String, String>{
                    'Host' => hostname,
                    'x-amz-date' => dt.formatGmt('YYYYMMdd') + 'T' + dt.formatGmt('HHmmss') + 'Z'
                },
                signedHeaders,
                jsonBody
            )
        );
        
        String signature = this.createSignature(
            'hmacSHA256',
            dt,
            region,
            SERVICE,
            Blob.valueOf(stringToSign)
        );
        
        return this.callLambda(
            dt,
            hostname,
            credentialScope,
            signedHeaders,
            signature,
            resource_path,
            jsonBody,
            'POST'
        );
    }
    
    /**
     * LambdaのAPIコールの共通メソッド
     */
    private String callLambda(
        DateTime dt,
        String hostname,
        String credentialScope,
        List<String> signedHeaders,
        String signature,
        String resource,
        String jsonBody,
        String method
    ) {
        HttpRequest req = new HttpRequest();
        req.setHeader('Host', hostname);
        req.setHeader(
            'x-amz-date', 
            dt.formatGmt('YYYYMMdd') + 'T' + dt.formatGmt('HHmmss') + 'Z'
        );
        
        req.setHeader('Content-Type', 'application/json; charset=UTF-8');
        req.setHeader('Content-Length', String.valueOf(jsonBody.length()));
        
        req.setHeader(
            'Authorization',
            'AWS4-HMAC-SHA256 Credential=' + this.AWSAccessKeyId + '/' + credentialScope + ',' +
            'SignedHeaders=' + String.join(Utility.getLowerCaseSortedList(signedHeaders), ';') + ',' + 
            'Signature=' + signature
        );
        req.setEndpoint('https://' + hostname + resource);
        req.setMethod(method);
        
        req.setBody(jsonBody);
        
        Http http = new Http();
        HTTPResponse res = http.send(req);
        return res.getBody();
    }
    
    /**
     * HashedCanonicalRequestを作成
     */
    private String createHashedCanonicalRequest(
        String algorithm,
        String method,
        String url,
        Map<String, String> params,
        Map<String, String> headers,
        List<String> signedHeaders,
        String payload
    ) {     
        String signature = '';
        signature += method + Constants.LF;
        signature += url + Constants.LF;
        signature += Utility.getSortedParam(params) + Constants.LF;
        signature += Utility.createCanonicalHeaders(headers) + Constants.LF;
        signature += String.join(Utility.getLowerCaseSortedList(signedHeaders), ';').toLowerCase() + Constants.LF;
        signature += EncodingUtil.convertToHex(
                        Crypto.generateDigest(
                            algorithm,
                            Blob.valueOf(payload)
                        )
                     );
        return EncodingUtil.convertToHex(
            Crypto.generateDigest(
                algorithm, 
                Blob.valueOf(signature)
            )
        ).toLowerCase();
    }
    /**
     * StringToSignを作成
     */
    private String createStringToSign(
        String algorithm,
        DateTime requestDateTime,
        String credentialScope,
        String hashedCanonicalRequest
    ) {
        String stringToSign = algorithm + Constants.LF;
        stringToSign += 
           requestDateTime.formatGmt('YYYYMMdd') + 'T' + 
           requestDateTime.formatGmt('HHmmss') + 'Z' + Constants.LF;
        stringToSign += credentialScope + Constants.LF;
        stringToSign += hashedCanonicalRequest;
        return stringToSign;        
    }
    
    private String createSignature(
        String algorithm,
        DateTime requestDateTime,
        String region,
        String service,
        Blob stringToSign
    ) {
        system.debug(stringToSign.toString());
        Blob kDate = Crypto.generateMac(
            algorithm,
            Blob.valueOf(requestDateTime.formatGmt('YYYYMMdd')),
            Blob.valueOf('AWS4' + this.AWSAccessKeySecret)
        );
        Blob kRegion = Crypto.generateMac(
            algorithm,
            Blob.valueOf(region),
            kDate
        );
        Blob kService = Crypto.generateMac(
            algorithm,
            Blob.valueOf(service),
            kRegion
        );
        
        Blob kSigning = Crypto.generateMac(
            algorithm,
            Blob.valueOf('aws4_request'),
            kService
        );
        
        return EncodingUtil.convertToHex(
            Crypto.generateMac(
                algorithm,
                stringToSign,
                kSigning
            )
        );
    }
}

ApexでのSignature Version4の生成方法は過去の記事を参照ください!

リモートサイトの設定も忘れずに。

lambda_remotesite

あとは普通に呼び出すだけ!

Lambda cls = new Lambda();
cls.invokeAsync('Chatter', new Map<String, Object>{
    'parent_id'=>'005A0000001cd4c',
    'content'=>'Chatter'
},
'');

で、Chatterを開くと…

lambda_chatter

Good!!

 

今回のサンプルだとApex→Lambda→Chatterというフローになっていて

Lambda経由のChatter投稿という意味なさげな感じになっていますが

実際にSalesforceで連携するときのユースケースとしては

S3にアップロードされたらChatterFeed作ってあげたり、他のSFDCのAPIを叩いたり

ApexトリガでDML走る度にLambda function動かすとか、そういった使い方になると思います。

 

一日しか触ってないですが、サーバ立てたりオートスケールの設定無しで

スケールするファンクションを簡単に作れるのは、非常に強力だなーと思いました。

このエントリーをはてなブックマークに追加