LambdaのNode.js v8.10対応
2018/4/2からAWSのLambdaがNode8に対応しました。
(東京リージョンも対応しています)
特にLambdaに関連した変更点としてうれしいのは、async / await
が使えるようになったという点だと思います。
(パフォーマンスも向上する1らしいですが未検証です)
async / await を使うメリット
Node.jsは非同期処理を特徴としている為、コールバック地獄が待ち受けていました。(しかもLambdaの場合は、この非同期処理の恩恵があまり無いのがまた悲しい話で。。。)
これを回避する一つの方法としてPromise
を使う方法がありました。ただ、Promise
もチェーンが多くなったり分岐があったり例外処理が入ってくると、コールバック地獄よりはかなりマシとはいえ、読みにくくなってしまいます。
そこでasync / await
を使うと、非同期処理部分を同期処理の様に記述することができるので、ソースがとても読みやすくなるというメリットがあります。
コード例
では、実際にasync / await
が威力を発揮するLambda特有のコードの例を3つ取り上げます。
KMS
KMSはDBのパスワードなどの機密情報を暗号化しておけるものです。
この暗号化した環境変数を復号する際のテンプレートコードをasync / await
を使って書き直すと以下のコードになります。
const AWS = require('aws-sdk');
const encrypted = process.env['PASSWORD'];
let decrypted;
function processEvent(event, context, callback) {
// TODO handle the event here
}
exports.handler = async (event, context, callback) => {
if (decrypted) {
processEvent(event, context, callback);
} else {
// Decrypt code should run once and variables stored outside of the function
// handler so that these are decrypted once per container
const kms = new AWS.KMS();
try {
const params = { CiphertextBlob: new Buffer(encrypted, 'base64') };
const data = await kms.decrypt(params).promise();
decrypted = data.Plaintext.toString('ascii');
processEvent(event, context, callback);
} catch (err) {
console.log('Decrypt error:', err);
callback(err);
}
}
};
変更点
元のコード(コンソール上で「コード」を選択した時に出てくるテンプレート)からの変更箇所は以下の点です。
-
exports.handler
にセットするメソッドにasync
が付いている -
kms.decrypt
にawait
が付いている -
kms.decrypt
にコールバック関数を設定せずにPromise
を返している- 成功時は返された
Promise
からデータを取得している - 失敗時は
try ~ catch
でエラー内容を取得している
- 成功時は返された
特にコールバック関数部分が無くなった為、処理が同期的に書かれていて流れが追いやすくなった上に、かなりすっきりとしたコードに変わっています。
S3
次はS3にファイルを並列で同時にアップロードする場合のコード例です。
const AWS = require('aws-sdk');
const s3 = new AWS.S3({region: 'ap-northeast-1'});
try {
// 同時実行できるように先にPromiseの配列を作成
// (メモリ不足になるほどの容量・ファイル数はこない想定)
const writePromises = Object.keys(event).map(function(key) {
// jsonのキーがファイル名、値をファイルの内容のjson形式にする
const params = {
Bucket: 'test-bucket',
Key: key + '.json',
Body: JSON.stringify(event[key]),
ContentType: 'application/json',
ACL: 'public-read' // 読み取り可で公開
};
// S3へアップロードするPromiseを返す
return s3.upload(params).promise();
});
// 並列でPromiseを全て実行する
await Promise.all(writePromises);
} catch (err) {
// S3への書き込みに失敗した場合
console.log('S3 upload error:', err);
}
まずはjsonの中身を走査してS3へのアップロードを行うPromise
の配列を作成します。その後、その配列をPromise.all
に渡して、並列で同時実行させています。途中でどれか一つでも失敗した場合は、catch
部分が呼び出されます。
この例で、並列で書き込んでいるのは少しでも実行時間を短くする為です。
(外部のAPIやリソースの取得のように時間がかかる場合は効果的です)
DynamoDB
最後は、DynamoDBへシリアルでデータを書き込む場合のコード例です。
シリアルの場合、1件の書き込みが終わってから次の書き込みを行います。
const AWS = require('aws-sdk');
const dynamo = new AWS.DynamoDB.DocumentClient({region: "ap-northeast-1"});
for (let key in event) {
// jsonのキーがid(プライマリキー)、値をcontentにする
const params = {
TableName: 'test',
Key: {
'id': key
},
UpdateExpression: 'set content = :val',
ExpressionAttributeValues: {
':val': event[key]
}
};
try {
// DBを更新する
await dynamo.update(params).promise();
} catch (err) {
// DBの更新に失敗した場合
console.log('DynamoDB update error:', err);
}
}
今回は、シリアルなのでjsonの中身を走査してそのまま1件ずつDBに更新をかけています。
通常、コールバックでこういった処理を書こうとすると、再帰的なコードが出てきてしまいますが、async / await
を使うと自然なループだけで処理を書くことができます。
DynamoDBでは、一つ前のS3と違って並列で同時に書き込むと書き込みキャパシティを余分に消費してしまうので、逐次的に書き込む方法を選んでいます。
まとめ
async / await
を使うとコールバック地獄から解放されます。
すでにPromise
を使っている場合でもソースが読みやすくなります。
特にNode8にしたからといって弊害もなさそうなので、今後新しいAPIを作る時はNode8を選択するのが良さそうです。