AWS S3にデプロイしたら自動的にCloudFrontのキャッシュを無効化(invalidation)するNode.js  Lambda

# 概要
静的コンテンツをAWS S3に置いて、CloudFrontを前段に立てる構成になっている状態で、S3を更新してもCloudFrontのエッジサーバーのキャッシュは自動で更新されずキャッシュの有効期限中は反映されない
そのキャッシュを手動で無効化する運用フローになっているのを自動化する手順のメモ
ロールを作るところからnode.jsのLambdaでやる構成での記事がなかったように思うので、残しておく

# 構成
最初はデプロイスクリプトから [CloudFrontのAPI](https://docs.aws.amazon.com/cloudfront/latest/APIReference/API_CreateInvalidation.html)を直に叩いて実現しようかと思った

しかしLambdaがS3の追加削除イベントをトリガーにできるようなので、タイミングとしてそちらのほうが適切であり、更に[AWS SDK](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CloudFront.html#createInvalidation-property)を使った方が楽だと分かったのでそのように構成した
スクリプトはフロントエンド エンジニアが管理しやすいようにNode.js javascriptにした

# 手順
## IAM ロールを作る
LambdaがCloudFrontのCreateInvalidationを叩けるように適当な名前でIAM Roleを作る

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": "cloudfront:CreateInvalidation",
            "Resource": "*"
        }
    ]
}
```
## Lambdaのロールに適用する
上で作ったロールを適用

<img width="644" alt="スクリーンショット 2020-02-24 16.11.15.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50442/6176ca9b-21ba-574d-cb9e-1990007d3803.png">

## Lambdaのトリガーを追加する
反応させたいS3 Bucketごとに、 `すべてのオブジェクト作成イベント` と `オブジェクトの削除（すべて）` の2つのトリガーを作る

<img width="826" alt="スクリーンショット 2020-02-24 16.20.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50442/06d91a7f-7086-39e2-d0e4-d9aef0e5e344.png">
<img width="830" alt="スクリーンショット 2020-02-24 16.20.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50442/6ef1b750-0010-ccac-cc61-ba9d540107d7.png">


## Lambdaのコードを書く
インライン編集、Node.jsを選ぶ
<img width="1267" alt="スクリーンショット 2020-02-24 15.49.30.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50442/76d7ae20-59b4-a2ee-9985-bf3d603f47b6.png">

```javascript

const AWS = require("aws-sdk");
const cloudfront = new AWS.CloudFront({ apiVersion: "2019-03-26" });

// @see https://kotororo.net/2018/07/%E9%9D%99%E7%9A%84%E3%82%B3%E3%83%B3%E3%83%86%E3%83%B3%E3%83%84%E3%82%92%E6%9B%B4%E6%96%B0%E3%81%97%E3%81%9F%E3%81%A8%E3%81%8D%E3%81%AB%E5%8D%B3%E6%99%82%E5%8F%8D%E6%98%A0%E3%81%95%E3%82%8C%E3%82%8B%E3%82%88%E3%81%86%E3%81%ABcloudfront%E3%81%AEinvalidation%E3%82%92%E8%B5%B0%E3%82%89%E3%81%9B%E3%82%8Blambda%E9%96%A2%E6%95%B0/
exports.handler = async event => {
  const paths = getPathsFromEvent(event);
  const s3BucketToCloudFrontDistributionId = {
    "S3のバケット名": "CloudFront Distribution ID",
  };
  const distributionId = s3BucketToCloudFrontDistributionId[getS3BucketFromEvent(event)];
  const params = {
    DistributionId: distributionId,
    InvalidationBatch: {
      CallerReference: new Date().getTime().toString(),
      Paths: paths
    }
  };

  // @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CloudFront.html#createInvalidation-property
  await cloudfront
    .createInvalidation(params, function(err, data) {
      if (err) {
        console.log(err, err.stack);
      } else {
        console.log(data);
      }
    })
    .promise();
  console.log("invalidate: " + paths.Items[0]);
};

function getPathsFromEvent(event) {
  console.log(JSON.stringify(event));
  return {
    Quantity: 1,
    Items: ["/" + event.Records[0].s3.object.key]
  };
}

function getS3BucketFromEvent(event) {
  return event.Records[0].s3.bucket.name;
}
```
※ 複数の開発環境に反応させるために、 `s3BucketToCloudFrontDistributionId` というマッピングにしてあるので、自分の環境の内容を入れる

## 動作確認
S3に追加削除して、そのパスがInvalidationされていたら成功


# いくつまで一度に無効化できるのか？
もともと `*` でinvalidationしていたのだが、今回の変更でS3のイベントが各々のオブジェクトの追加削除ごとに発生し、invalidationも1パスずつリクエストされるようになった
もし大量のイベントが発生したときに問題なく動くのか気になり、いくつまで一度にinvaidationできるのか調べた

[FAQ](https://aws.amazon.com/jp/cloudfront/faqs/) によると以下の通りなので、通常の使用であれば問題なさそうだった

> Q.無効リクエスト数には制限がありますか？

> オブジェクトを個別に無効にする場合は、進行中のディストリビューションごとに最大 3,000 個のオブジェクトまで、一度に無効化リクエストを作成できます。これは、最大 3,000 個のオブジェクトに対する 1 つの無効化リクエスト、1 つのオブジェクトに対する最大 3,000 個のリクエスト、または 3,000 個のオブジェクトを超えないその他の任意の組み合わせとすることができます。

> `*` ワイルドカードを使用している場合、最大 15 個の無効化パスのリクエストを一度に作成できます。また、進行中のディストリビューションごとに最大 3,000 個の個別のオブジェクトを同時に作成することができます。ワイルドカードの無効化リクエストの制限は、オブジェクトの個別の無効化の制限とは無関係です。この限度を超過すると、以前のリクエストが終了するまで、余分な無効リクエストはエラーを返します。

# 困ったこと
* handlerでもらえるEventの型の定義がどう調べても分からず、console.logで見てみるしかなかった
  * [「イベント構造はサービスによって異なります」という記述](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/nodejs-prog-model-handler.html)しか見当たらなかった

# 保留したこと
* Lambdaのインラインコードをgit・S3管理すること
* 請求への注意・請求アラームをつけること

静的コンテンツ自体をそんなに頻繁に更新しないので保留した


# 参考
- https://blog.miguelangelnieto.net/posts/Automatic_Cloudfront_invalidation_with_Amazon_Lambda.html
- https://qiita.com/kskinaba/items/dcf9693dd034517e114a
- https://kotororo.net/2018/07/%E9%9D%99%E7%9A%84%E3%82%B3%E3%83%B3%E3%83%86%E3%83%B3%E3%83%84%E3%82%92%E6%9B%B4%E6%96%B0%E3%81%97%E3%81%9F%E3%81%A8%E3%81%8D%E3%81%AB%E5%8D%B3%E6%99%82%E5%8F%8D%E6%98%A0%E3%81%95%E3%82%8C%E3%82%8B%E3%82%88%E3%81%86%E3%81%ABcloudfront%E3%81%AEinvalidation%E3%82%92%E8%B5%B0%E3%82%89%E3%81%9B%E3%82%8Blambda%E9%96%A2%E6%95%B0/