S3へのレプリケーションをトリガーにLambdaのコード更新・新規バージョン発行を実行する

## はじめに
バックエンドのBuild成果物（jar）がDeploy環境にレプリケートされた事をトリガーに、Lambdaのコード更新・新規バージョン発行を行うCDの構築を行ったので、それに関するTipsのようなものを書き残しておこうと思う。
（前々回はCloud Frontのキャッシュ削除についての記事、前回はECSのサービス更新についての記事、だったが、今回でレプリケーション後のDeployシリーズ3つ目となる、Lambdaのコード更新・新規バージョン発行についての記事。）

## 構成イメージ
何らかのトリガーでCode BuildがStartすると、S3にBuild成果物であるjarがpushされ、それをトリガーにレプリケートが走る。レプリケーション先の環境でレプリケート後に自動でDeploy（Lambdaのコード更新・新規バージョン発行）を実行させる。

図で示すと以下のようになる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/92e2467e-82ad-6ae5-f1ee-f34ae7f7954e.png)

## Lambdaのコード更新・新規バージョン発行
今回のDeployは、S3へのレプリケーションでjarがPUTされたら、Lambdaのソースコード更新・新規バージョン発行を行うというもの。
Deploy自体は、Cloud Frontのキャッシュ削除の時と同様に、レプリケーションでS3のBucketに新規でオブジェクトが作成されるのでそれをトリガーにLambda関数を実行し、以下のAWS CLIコマンドで行っている事と同じ事をAWS SDK（今回はJavaScript）で実行させる、という感じで行う。

```bash
aws lambda update-function-code --function {function名} --s3-bucket {jarのあるS3 Bucket名} --s3-key {jarのファイル名}
aws lambda publish-version --function-name {function名}
```

※_{jarのファイル名}_ について、Bucketの中でフォルダ階層を作っているならルートディレクトリからのパスで書く必要がある
　ex) _--s3-key artifact/lambda/hoge.jar_

・参考：[aws.lambda.update-function-code](https://docs.aws.amazon.com/cli/latest/reference/lambda/update-function-code.html)
・参考：[aws.lambda.publish-version](https://docs.aws.amazon.com/cli/latest/reference/lambda/publish-version.html)

## IAM
S3へのレプリケーションをトリガーにCloud Frontのキャッシュ削除を実行する#iam と全く同じになるのでそれを参照。

## Lambda関数のトリガーを設定する
S3へのレプリケーションをトリガーにCloud Frontのキャッシュ削除を実行する#lambda関数のトリガーを設定する と全く同じになるのでそれを参照。

## Lambda関数を実装する
ここも基本的には、S3へのレプリケーションをトリガーにCloud Frontのキャッシュ削除を実行する#lambda関数を実装する とほぼ同じだが、Eventオブジェクトの中身からDeploy（Lambda UpdateFunctionCode, PublishVersion）を行うのに必要な情報を抜き出す部分が少し異なってくる。

### Lambdaに渡ってくるEventオブジェクトの中身
S3へのレプリケーションをトリガーにCloud Frontのキャッシュ削除を実行する#lambdaに渡ってくるeventオブジェクトの中身 と全く同じになるのでそれを参照。

### Lambda関数（index.handler）の実装
今回はNode.jsのLambda関数でDeployを実行させるので、実装としては以下のようした。

```js
const { LambdaClient, UpdateFunctionCodeCommand, PublishVersionCommand } = require("@aws-sdk/client-lambda");
const client = new LambdaClient({ region: process.env.REGION });

exports.handler = async (event) => {
    try {
        const bucket = event.Records[0].s3.bucket.name;
        const object = event.Records[0].s3.object.key;

        if (!object.includes(process.env.FILE_NAME)) {
            console.log("not applicable. object is ", object);
            return "";
        }

        const updateInput = {
            FunctionName: process.env.FUNCTION_NAME,
            S3Bucket: bucket,
            S3Key: object
        };

        const updateCommand = new UpdateFunctionCodeCommand(updateInput);
        const updateResponse = await client.send(updateCommand);

        console.log("UpdateFunctionCodeCommand status", updateResponse.$metadata.httpStatusCode);
        console.log("UpdateFunctionCodeCommand state", updateResponse.State);

        if (process.env.PUBLISH) {
            const publishInput = {
                FunctionName: process.env.FUNCTION_NAME
            };

            const publishCommand = new PublishVersionCommand(publishInput);
            const publishResponse = await client.send(publishCommand);

            console.log("PublishVersionCommand status", publishResponse.$metadata.httpStatusCode);
            console.log("PublishVersionCommand state", publishResponse.State);

            return "";
        } else {
            return "";
        }
    } catch (error) {
        return errorHandler(error);
    }
}

const errorHandler = (error) => {
    const obj = {};
    obj["status"] = 500;
    obj["message"] = error.message;
    obj["stack"] = error.stack;
    obj["result"] = "ng";
    if (error.$metadata) {
        obj["status"] = error.$metadata.httpStatusCode;
    }

    console.log("errorHandler", obj);
    return "";
}
```

※実装について何点か補足すると、

・_if (process.env.PUBLISH)_
　PubishVersionを行わないパターンも想定し、環境変数にPUBLISH（値は何でもいい）がある時のみPublishVersionを行うようにしている

・_if (object.includes("hoge.jar"))_
　Lambdaのトリガー設定時に「プレフィックス・サフィックス」を設定できるので、ここで敢えて条件分岐は不要かもしれないが念のために実装してた

・_console.log()_
　今回はCloud Watch Events（EventBridge）がLambda関数のトリガーであり、returnされた結果を人が見る事ができないので、何が起きているか分かるように敢えてconsole.log()でlogに出力させるようにした

・_return "";_
　これも今回S3がトリガーでreturnされた結果を人が見れないので""（空文字）を返すようにした（何かJSONを返しても意味ないと判断）。

・参考：[Lambda Client - AWS SDK for JavaScript v3](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-lambda/index.html)
・参考：[Class UpdateFunctionCodeCommand](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-lambda/classes/updatefunctioncodecommand.html)
・参考：[Class PublishVersionCommand](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-lambda/classes/publishversioncommand.html)

## Lambda関数の実行ロールの設定
Lambda関数が他のAWSサービスに対して何らかの操作を行うには、実行ロールでその権限を設定する必要がある。
今回はLambdaのUpdateFunctionCode・PublishVersionが実行できればいいので以下のようなインラインポリシーを実行ロールに追加でアタッチするればOK。

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                 "lambda:UpdateFunctionCode",
                 "lambda:PublishVersion"
            ],
            "Resource": "arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:{lambda関数名}"
        }
    ]
}
```

## まとめとして
前々回はCloud Frontのキャッシュ削除について、前回はECSのサービス更新について、過去2回で扱ったが、Lambdaについても同じような考え方でレプリケーション後にそれをトリガーにしてDeployを実行させる事ができた。