AWSのLambdaでasync / awaitを使う

# LambdaのNode.js v8.10対応

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


> [AWS Lambda が Node.js v8.10 をサポート](https://aws.amazon.com/jp/about-aws/whats-new/2018/04/aws-lambda-supports-nodejs/)

特にLambdaに関連した変更点としてうれしいのは、`async / await`が使えるようになったという点だと思います。
（パフォーマンスも向上する[^1]らしいですが未検証です）

[^1]: 参考リンク[AWS Lambda Node.js 8 support: what it changes for serverless developers](https://serverless.com/blog/aws-lambda-node-8-support-what-changes-serverless-developers/)


# async / await を使うメリット

Node.jsは非同期処理を特徴としている為、コールバック地獄が待ち受けていました。（しかもLambdaの場合は、この非同期処理の恩恵があまり無いのがまた悲しい話で。。。）

これを回避する一つの方法として`Promise`を使う方法がありました。ただ、`Promise`もチェーンが多くなったり分岐があったり例外処理が入ってくると、コールバック地獄よりはかなりマシとはいえ、読みにくくなってしまいます。

そこで`async / await`を使うと、非同期処理部分を同期処理の様に記述することができるので、ソースがとても読みやすくなるというメリットがあります。

# コード例

では、実際に`async / await`が威力を発揮するLambda特有のコードの例を3つ取り上げます。

## KMS

KMSはDBのパスワードなどの機密情報を暗号化しておけるものです。
この暗号化した環境変数を復号する際のテンプレートコードを`async / await`を使って書き直すと以下のコードになります。

```javascript
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にファイルを並列で同時にアップロードする場合のコード例です。

```javascript
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件の書き込みが終わってから次の書き込みを行います。

```javascript
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を選択するのが良さそうです。