Help us understand the problem. What is going on with this article?

AWSのLambdaでasync / awaitを使う

More than 1 year has passed since last update.

LambdaのNode.js v8.10対応

2018/4/2からAWSのLambdaがNode8に対応しました。
(東京リージョンも対応しています)

AWS Lambda が Node.js v8.10 をサポート

特に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.decryptawaitが付いている
  • 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を選択するのが良さそうです。

m__ike_
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした