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
で、アップロードする。
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をクリック。
Customを選択して適当なJSONをセットしてInvokeをクリック
下の方のExecution resultsにログが出るので確認。
日本語が???とかに文字化けてますが、CloudWatch側のログにはちゃんと残ってます。
CloudWatchで見る場合はLogsから対象のFunctionを選択して掘り下げていくとログが見れます。
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 cls = new Lambda();
cls.invokeAsync('Chatter', new Map<String, Object>{
'parent_id'=>'005A0000001cd4c',
'content'=>'Chatter'
},
'');
で、Chatterを開くと…
Good!!
今回のサンプルだとApex→Lambda→Chatterというフローになっていて
Lambda経由のChatter投稿という意味なさげな感じになっていますが
実際にSalesforceで連携するときのユースケースとしては
S3にアップロードされたらChatterFeed作ってあげたり、他のSFDCのAPIを叩いたり
ApexトリガでDML走る度にLambda function動かすとか、そういった使い方になると思います。
一日しか触ってないですが、サーバ立てたりオートスケールの設定無しで
スケールするファンクションを簡単に作れるのは、非常に強力だなーと思いました。