🪶

【CloudFront】S3で更新したファイルを自動ですぐに反映する

2023/05/31に公開

AWS を利用して静的な Web コンテンツを配信する方法として CloudFront + S3 の構成をよくとります。

構成方法は開発ガイド「安全な静的ウェブサイトの使用開始」や 巷の記事 が参考になります。

CloudFront のポテンシャルを十分に引き出すために、S3オリジンのキャッシュを存分に利かせて「爆速だー!ヒャッハー!」しようとしたら・・・

S3 に置いたファイルを上書きしても直ぐに反映してくれない。

マジデスカ…

トラップカードの存在に気づきます。

とりあえず反映する方法

手作業で済む範囲で、すぐにキャッシュを更新したい場合は大まかに2通り。

キャッシュを無効にする

CloudFront のビヘイビア設定にあるキャッシュポリシーを無効(Managed-CachingDisabled)に変更する方法です。まごうことなき急しのぎな回避策です。

応答パフォーマンスは確実に落ちますし、オリジンの負担も増える事に注意したい。

コンテンツ更新の都度「キャッシュ削除」を発行する

CloudFront のディストリビューション内にある「キャッシュ削除」タブから更新したファイルパスを指定するか、全パス指定 /* をして全エッジサーバーからキャッシュクリアを指示します。

更新の都度、手作業になる点に注意したい。あまり更新しないなら、この対策も有り。

自動化はロマン

自動化こそロマン! もとい、運用手間の軽減と作業ミスのリスクを減らして、人的・利用料的コストを下げて爆速ヒャッハーするには極力自動化です。

構成

S3 バケットのプロパティ設定にある「イベント通知」を使います。

Lambda の下ごしらえ

コードの作成

Lambda 関数をランタイム「Node.js 18.x」で作成します。Lambda 名は任意の指定ができます。「cloudfront-cache-clear」として進めます。

ファイル名 index.mjs で以下のコードを貼り付けて保存し、デプロイ(Deploy)します。

index.mjs
import { CloudFront } from "@aws-sdk/client-cloudfront";

export const handler = async (event) => {
  const s3keys = event.Records.map((r) => encodeURI(`/${r.s3.object.key}`));

  const client = new CloudFront();
  await client.createInvalidation({
    DistributionId: process.env.DISTRIBUTION_ID,
    InvalidationBatch: {
      CallerReference: new Date().toISOString(),
      Paths: {
        Quantity: s3keys.length,
        Items: s3keys
      }
    }
  });
};

環境変数の設定

環境変数 DISTRIBUTION_ID に CloudFront のディストリビューション ID を設定します。

設定>環境変数

実行ロールの修正

自動生成される実行ロールでは createInvalidation() を実行できないのでアクセス権限を追加します。

設定>アクセス権限>実行ロールにある、ロールのリンクをクリックして IAM を開きます。

許可ポリシーの「許可を追加>インラインポリシーを作成」を選択します。

「JSON」タブに切り替えて、以下の内容を貼り付けます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "cloudfront:CreateInvalidation",
            "Resource": "arn:aws:cloudfront::123456789012:distribution/E1234567890ABC"
        }
    ]
}

123456789012はアカウントID、E1234567890ABCはCloudFrontのディストリビューションIDです。適宜変えます。

ポリシーの確認をし、インラインポリシー名を「CreateInvalidation」と付けて保存します。

S3 バケットに紐付ける

CloudFront のオリジンターゲットに設定している、S3 バケットのプロパティを開きます。

「イベント通知を作成」をクリックします。

イベント名を適宜付けます。

イベントタイプの一覧から「すべてのオブジェクトと作成イベント」のみチェックを付けます。

一番下の送信先で「Lambda 関数」を選択、下ごしらえした Lambda を選択したら「変更の保存」をクリックします。

保存が成功したら以下のように追加されているはずです。

補足

同時に Lambda のアクセス権限>リソースベースのポリシーステートメントに S3 から lambda:InvokeFunction を実行できるポリシーが自動的に追加されます。

動作チェック

あとは、S3 バケットにファイルを上書きアップロードして、CloudFront のドメインにアクセスして更新確認します。

チェックポイント

更新されていないようであれば、以下の2点を確認します。

  • CloudFront の「キャッシュ削除」に発行された記録があるか。
    • 詳細を開いてオブジェクトパスが目的のS3バケットのファイルから期待するものか。
    • 日本語ファイル名であれば URI エンコードされているか。
  • Lambda のモニタリング>ログを見て実行されていて、成功しているか。
    • 必要なら CloudWatch ログ側で確認します。

考慮するところ

1更新=1トリガー?

AWS コンソールで試した限りですが、S3 のオブジェクト作成イベントは複数同時にアップロードしても1ファイルごとに1個のイベントとなって、都度 Lambda が実行されました。

イベントの仕様上 event.Records と複数レコード=複数ファイル分を1イベントとして受け取りそうな構造になっていますが、、。100 ファイル更新なら Lambda トリガーも 100 回呼び出されるのかなと思いました。

キャッシュ削除は従量課金

見落とされがちですが、キャッシュ削除をしたパス数に応じて従量課金があります。

執筆時点で 1,000 パス分を超えると 1 パスごとに追加料金が発生します。定常的・頻繁にファイルの上書き更新が想定される場合は考慮するところです。

【公式】料金>その他の機能>無効リクエスト
https://aws.amazon.com/jp/cloudfront/pricing/

新規ファイル

今回のコードは S3 への「新規アップロード」か「既存の上書きアップロード」かと判別していません。

新規時はそもそもキャッシュ削除する必要性が無いですが、イベントだけで判別できる方法が分からずでした。S3 にアクセスして存在チェックするしか無いのでしょうか…(´・ω・`)
ご存じの方コメントしてもらえると嬉しいです。

大量更新への対策

SQS キューを使う?

S3 のイベント通知先には Lambda の他に SQS キューも指定できます。

S3 イベントトリガー → SQS → Lambda(パスまとめ処理) → CloudFront

のような構成にして、ロングポーリングタイムアウトやバッチ処理するメッセージ数を組み合わせて対象パス数を削減するよう、ワイルドカード化するまとめ処理を組み込むようにする?

ただ、これだけの工数を使うならば上書きしなくても良いユニークなパス設計や、クエリ文字列を併用したファイル更新の伝達手段も検討したいところです。

特定のファイルだけに限定する

イベント通知のプレフィックス・サフィックスを利用して index.html など特定のファイルだけ自動キャッシュ削除の対策をする割り切りも考えたいですね。

おわりに

爆速ヒャッハー(コンテンツ配信したい)のために、キャッシュ更新のトラップを解除するコードを載せて終わるつもりが、運用想定すると更にトリガー回数の伏兵が待ち伏せてました。

奥が深い・・・

それでは良きキャッシュライフを!

コラボスタイル Developers

Discussion