S3へのレプリケーションをトリガーにCloud Frontのキャッシュ削除を実行する

## はじめに
フロントエンドのBuild成果物がDeploy環境にレプリケートされた事をトリガーに、Cloud Frontのキャッシュ削除を行うCDの構築を行ったので、それに関するTipsのようなものを書き残しておこうと思う。

## 構成イメージ
何らかのトリガーでCode BuildがStartすると、S3にBuild成果物が保存され、それをトリガーにレプリケートが走る。
レプリケーション先の環境でレプリケート後に自動でDeploy（Cloud Frontのキャッシュ削除）を実行させる。

図で示すと以下のようになる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/de9eeda5-a28d-a653-0c41-99b1ed228c1a.png)

※上記のCICDを構築したシステム側の構成は、よくあるS3×Cloud Frontで静的ホスティングしているもの。

## Cloud Frontのキャッシュ削除
今回のDeployは、S3へのレプリケーションでindex.html・JavaScript・CSSなどがPUTされたら、Cloud Frontのファイルの無効化を行い、次にユーザがサイトにアクセスしたら最新のS3のファイルを見に行くようにさせる方式で行った。
Deploy自体は、レプリケーションでs3のBucketに新規でオブジェクトが作成されるのでそれをトリガーにLambda関数を実行し、以下のAWS CLIコマンドで行っている事と同じ事をAWS SDK（今回はJavaScript）で実行させる、という感じで行う。

```bash
aws cloudfront create-invalidation \
--distribution-id {distribution id} Paths={Quantity=1,Items=['/*']},CallerReference=UUID
```

※ _Paths={Quantity=1,Items=['/*']},CallerReference=UUID_ の部分は複雑になるとJSONで書いた方がいい場合もあるが、その時は以下のようにJSONファイルからパラメータを読み込むようにすればいい。

```bash
aws cloudfront create-invalidation --distribution-id {distribution id} --paths file://paths.json
```
```json:paths.json
{
  "Paths": {
    "Quantity": 1,
    "Items": ["/*"]
  },
  "CallerReference": "UUID"
}
```

・参考：[aws.cloudfront.create-invalidation](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/cloudfront/create-invalidation.html)
・参考：[ファイルから AWS CLI パラメータをロードする](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-usage-parameters-file.html)

## IAM
Lambda関数を作成したり、トリガーを設定するために必要な権限としては以下が必要になる。

　・ iam:系：実行ロールの作成・付与をするのに必要
　・ lambda:系：lambda関数を作成したり、トリガーを設定したりするのに必要
　・ s3:系：s3のBucketにオブジェクトが追加されたら…というトリガーの設定をするのに必要

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "iam:PassRole",
                "iam:ListAttachedRolePolicies",
                "iam:PutRolePolicy",
                "iam:ListRolePolicies",
                "iam:GetRolePolicy"
            ],
            "Resource": "arn:aws:iam::xxxxxxxxxxxx:role/replication-deploy-lambda"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": [
                "iam:ListPolicies",
                "iam:ListRoles"
            ],
            "Resource": "*"
        }
    ]
}
```
```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "lambda:CreateFunction",
                "lambda:UpdateFunctionCode",
                "lambda:AddPermission",
                "lambda:GetFunction",
                "lambda:GetPolicy"
            ],
            "Resource": "arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:replication-deploy-lambda"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": [
                "lambda:ListFunctions",
                "lambda:ListEventSourceMappings",
                "lambda:GetAccountSettings"
            ],
            "Resource": "*"
        }
    ]
}
```
```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "s3:PutBucketNotification",
                "s3:GetBucketNotification",
                "s3:GetBucketLocation"
            ],
            "Resource": "arn:aws:s3:::replication-deploy-lambda"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": "s3:ListAllMyBuckets",
            "Resource": "*"
        }
    ]
}
```

## Lambda関数のトリガーを設定する
[Amazon S3 イベント通知](https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/NotificationHowTo.html)という仕組みを使い、S3にオブジェクトが追加されたタイミングでLambda関数をトリガーするように設定をする。
s3のバケットに対するイベントのPUTを設定し、場合によってはプレフィックス・サフィックスを指定する事で、特定のオブジェクトが追加された時のみLambdaがトリガーされるように設定する事もできる。

以下ではManagement Consoleでの設定と、AWS CLIコマンドでの設定の2パターンを見てみる。

### Management Consoleでの設定のパターン
＋トリガーを追加 から以下の画面が開くのでそこからトリガーにS3を設定し、イベントタイプなどを選択する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/614709fb-8443-2754-7a50-79161d770c84.png)

※Management Consoleから設定する場合、 _s3:GetBucketLocation_ （AmazonS3バケットが存在するリージョンを返す権限）がないと以下のようなエラーになり作成できないので注意（トリガーの一覧にS3のイベントが設定できているように見えるがエラーになる）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/b7378e64-f009-879b-23ec-f3ed0ac7feb2.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/39e27de0-088a-68a7-ffbc-25e4f29f1854.png)

### AWS CLIコマンドでの設定のパターン
AWS CLIコマンドで行う場合は2Stepになる（ _aws lambda add-permission_ について何をしているのか？は[Lambda関数の権限で注意点](#lambda関数の権限で注意点)を参照）。

```bash
aws lambda add-permission \
    --function-name replication-deploy-lambda \
    --principal s3.amazonaws.com \
    --statement-id replication-deploy-lambda-permission \
    --action "lambda:InvokeFunction" \
    --source-arn arn:aws:s3:::replication-deploy-lambda \
    --source-account xxxxxxxxxxxx

aws s3api put-bucket-notification-configuration \
    --bucket replication-deploy-lambda \
    --notification-configuration file://notification.json
```
```json:notification.json
{
    "LambdaFunctionConfigurations": [
        {
            "LambdaFunctionArn": "arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:replication-deploy-lambda",
            "Events": [
                "s3:ObjectCreated:Put"
            ]
        }
    ]
}
```

※ _s3:ObjectCreated:Put_ の部分は自分がトリガーにしたいイベントを書くが、今回はPUTになる（アクション一覧は[サポートされるイベントタイプ](https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/notification-how-to-event-types-and-destinations.html#supported-notification-event-types)を参照）。

※AWS CLIのリファレンスではSNSの設定が例になっているので [_TopicConfiguration_](https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/API/API_TopicConfiguration.html) であるが、今回はLambdaをトリガーする設定なので [_LambdaFunctionConfiguration_](https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/API/API_LambdaFunctionConfiguration.html) で設定する事になる。

※今回は _aws s3api put-bucket-notification-configuration_ というAPIでLambdaのトリガーを設定しているが、他にも [_aws lambda create-event-source-mapping_](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/create-event-source-mapping.html) というAPIもあり、これも同じようにイベントトリガーを設定するためのAPIだが、これはS3を設定できる仕様になっていないので今回は _aws s3api put-bucket-notification-configuration_ を使っている。

※上記の「Management Consoleでの設定」では、 _s3:GetBucketLocation_ がないとエラーになりトリガーの設定ができなかったが、AWS CLIの場合はなくても**作成はできる**。ただし、Management Consoleから設定されたトリガーを見るには _s3:GetBucketLocatio_ の権限がないと以下のようになるので必要と思われる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/8363652b-a84a-29f5-50af-14302dc9be93.png)

AWS CLIのリファレンスは以下

　・ [aws.lambdaadd-permission](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/add-permission.html)
　・ [aws.s3api.put-bucket-notification-configuration](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/s3api/put-bucket-notification-configuration.html)

・参考：[【Tips】AWS-CLIでAWS LambdaのイベントソースにS3を設定する](https://dev.classmethod.jp/articles/tips-lambda-eventsource-s3/)
・参考：[PutBucketNotificationConfiguration　Request Body](https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/API/API_PutBucketNotificationConfiguration.html#API_PutBucketNotificationConfiguration_RequestBody)

## Lambda関数の権限で注意点
Lambdaには2つの権限設定があり、

・ 実行ロール<br>　Lambda関数がどのAWSサービスへアクセスできるのか？を制御するための権限
・ リソースベースのポリシー<br>　どのAWSサービスならLambda関数へアクセスできるのか？を制御するための権限

この2つの設定をしないといけない。今回だとイベントのトリガーはs3なので以下のように設定する必要がある。

| アクセス権限 > リソースベースのポリシー | アクセス権限を編集 |
|:-:|:-:|
| ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/a11c7a8f-c4b9-15b7-9ab8-252f574908d2.png)  | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/869323df-713e-dae9-1ee7-5130efa18872.png)  |

以下ではLambda関数のリソースベースのポリシーを設定する方法を、Management Consoleから行う場合と、AWS CLIコマンドで行う場合とを見ていく。

### Management Consoleでの設定のパターン
Management Consoleからやる分には、「トリガーを追加」でS3を指定すると勝手に「リソースベースのポリシー」は作成される。

### AWS CLIコマンドでの設定のパターン
```bash
aws lambda add-permission \
    --function-name {関数名} \
    --principal s3.amazonaws.com \
    --statement-id {statement id(一意になれば何でもOK)} \
    --action "lambda:InvokeFunction" \
    --source-arn arn:aws:s3:::{バケット名} \
    --source-account {バケットがあるAWSアカウントID12桁}
```

・参考：[AWS のサービスへのアクセス権を関数に付与する（翻訳が良くないので意訳すると、「AWSサービスからLambda関数を呼び出すアクセス権を付与する」）](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/access-control-resource-based.html#permissions-resource-serviceinvoke)_The simplest resource-based policy statement allows a service to invoke a function. (最も単純なリソースベースのポリシーステートメントにより、サービスは関数を呼び出すことができます。)と書かれているように、他のAWSサービスがLambda関数を呼び出すための設定が「リソースベースのポリシー」_
・参考：[リソースベースのポリシーの仕組み](https://dev.classmethod.jp/articles/tsnote-lambda-s3-event-trigger/#toc-3)

## Lambda関数を実装する
イベントトリガーの場合には、Lambda関数にEventオブジェクトが渡ってくるが、まずはS3へのオブジェクト追加（Amazon S3 イベント通知）時にどんなEventオブジェクトが渡ってくるのか？を見てみる。
その後、そのEventオブジェクトの中身からDeploy（CloudFront CreateInvalidation）を行うのに必要な情報を抜き出し実際にそれを実行するコードを考える。

### Lambdaに渡ってくるEventオブジェクトの中身
以下のようなオブジェクトが渡ってくる。
※実際に実装してみて後から分かった事として、Recordsと配列になっているが実際には配列の要素は1つであった（複数になる場合があればご指摘下さい）。

この中にどのBucketにPUTされたのか？という事が分かる情報（以下のJSONのキーの情報）があるので、これを使って次の項で実際にLambda関数を実装していく。

```json
{
  "Records": [
    {
      "eventVersion": "2.1",
      "eventSource": "aws:s3",
      "awsRegion": "us-east-2",
      "eventTime": "2019-09-03T19:37:27.192Z",
      "eventName": "ObjectCreated:Put",
      "userIdentity": {
        "principalId": "AWS:AIDAINPONIXQXHT3IKHL2"
      },
      "requestParameters": {
        "sourceIPAddress": "205.255.255.255"
      },
      "responseElements": {
        "x-amz-request-id": "D82B88E5F771F645",
        "x-amz-id-2": "vlR7PnpV2Ce81l0PRw6jlUpck7Jo5ZsQjryTjKlc5aLWGVHPZLj5NeC6qMa0emYBDXOo6QBU0Wo="
      },
      "s3": {
        "s3SchemaVersion": "1.0",
        "configurationId": "828aa6fc-f7b5-4305-8584-487c791949c1",
        "bucket": {
          "name": "lambda-artifacts-deafc19498e3f2df",
          "ownerIdentity": {
            "principalId": "A3I5XTEXAMAI3E"
          },
          "arn": "arn:aws:s3:::lambda-artifacts-deafc19498e3f2df"
        },
        "object": {
          "key": "b21b84d653bb07b05b1e6b33684dc11b",
          "size": 1305107,
          "eTag": "b21b84d653bb07b05b1e6b33684dc11b",
          "sequencer": "0C0F6F405D6ED209E1"
        }
      }
    }
  ]
}
```

・参考：[AWS Lambda を Amazon S3 に使用する](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/with-s3.html)

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

```js:index.js
const { CloudFrontClient, CreateInvalidationCommand } = require("@aws-sdk/client-cloudfront");
const config = require("./config")
const client = new CloudFrontClient({ 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;
        const link = config.linking.filter(el => el.bucket === bucket)[0];

        console.log("object", object);
        console.log("link", link);

        if (!object.includes("index.html") || !link) {
            console.log("Not create invalidation because not applicable.", object);
            return "";
        }

        const input = {
            DistributionId: link.distributionId,
            InvalidationBatch: {
                CallerReference: String(Date.now()),
                Paths: {
                    Items: ["/*"],
                    Quantity: 1
                }
            }
        }
        const command = new CreateInvalidationCommand(input);
        const response = await client.send(command);

        console.log("status", response.$metadata.httpStatusCode);
        console.log("invalidation status", response.Invalidation.Status);

        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 "";
}
```
```js:config.js
exports.linking = [
    {
        bucket: "replication-deploy-lambda",
        distributionId: "xxxxxxxxxxxxxxxxx"
    }
]
```

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

・_event.Records[0].s3.bucket.name_ ・ _event.Records[0].s3.object.key_<br>　ここでLambda関数に渡ってくるEventオブジェクトの中身から必要な情報を抜き出している（Eventオブジェクトの中身については[Lambdaに渡ってくるEventオブジェクトの中身](#lambdaに渡ってくるeventオブジェクトの中身)を項を参照）。
・_config.linking.filter(・・・)_<br>　Eventオブジェクトの中身を見れば分かるが、Cloud FrontのディストリビューションIDは当然ながらS3のPUTイベントには含まれていない。そこでどのBucketとどのディストリビューションが紐づいているか？を何らかの形で特定できるようにする必要があり、今回は`config/index.js`にその情報を持たせている。
・_if (objName.includes("index.html") && link)_<br>　Lambdaのトリガー設定時に「プレフィックス・サフィックス」を設定できるので、ここで敢えて条件分岐は不要かもしれないが念のために実装している。
・_console.log()_<br>　今回はS3がLambda関数のトリガーでありreturnされた結果を人が見る事ができないので、何が起きているか分かるように敢えてconsole.log()でlogに出力させるようにした。
・_return "";_<br>　これも今回S3がトリガーでreturnされた結果を人が見れないので`""`（空文字）を返すようにした（何かJSONを返しても意味ないと判断した）。
・_config.js_<br>　これはjsonでもいい（jsだとコメントアウトができたりテストしやすかったのでjsにしたが、テスト側で書き換えられるので今思えばjsonでいい気がしている）。
・_ if (error.$metadata)_<br>　AWS SDKのエラーでresponseがある場合、 _$metadata_ の中に _httpStatusCode_ の情報が入っているので、それがある時はそれを参照するようにしている。

・参考：[CloudFront Client - AWS SDK for JavaScript v3](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-cloudfront/index.html)
・参考：[Class CreateInvalidationCommand](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-cloudfront/classes/createinvalidationcommand.html)

## Lambda関数の実行ロールの設定
[Lambda関数の権限で注意点](#lambda関数の権限で注意点)で触れたが、Lambda関数の権限には2つあり、Lambda関数が他のAWSサービスに対して何らかの操作を行うには、実行ロールでその権限を設定する必要がある。
Management Consoleから関数を作成すると、以下のように勝手に実行ロールが作成されるが、その実行ロールに対し管理ポリシーやインラインポリシーで権限を付与する。

今回はCloud Frontの Create Invalidationが実行できればいいので以下のようなインラインポリシーを追加でアタッチするればOK。

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "cloudfront:CreateInvalidation",
            "Resource": [
                 "arn:aws:cloudfront::xxxxxxxxxxxx:distribution/xxxxxxxxxx",
                 "arn:aws:cloudfront::xxxxxxxxxxxx:distribution/xxxxxxxxxx"
            ]
        }
    ]
}
```

## まとめとして
2020年12月から[レプリケーションがより使いやすくなった](https://aws.amazon.com/jp/blogs/news/new-amazon-s3-replication-adds-support-for-multiple-destination-buckets)ので今回扱ったような構成でのDeployもよくあるものになると思う。

## 補足 「Lambda関数のトリガーを設定する」について
上記の[Lambda関数のトリガーを設定する](#lambda関数のトリガーを設定する)では、**[Amazon S3 イベント通知](https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/NotificationHowTo.html)**の仕組みを使ってLambdaを実行するように設定したが、Cloud Trailのデータイベントを使ってCloud Watch EventでCloud Trailのイベント(AWS API Call via CloudTrail)を補足してLambdaを実行させる事も技術的には可能。
ただし、Cloud Trailのデータイベント記録は**有料**なのでそこだけ注意。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/cafbb1f0-271e-e9a2-de72-868185334181.png)
・参考：[AWS CloudTrail の料金](https://aws.amazon.com/jp/cloudtrail/pricing/)