RareJob Tech Blog

レアジョブのエンジニア・デザイナーによる技術ブログです

CloudFront + S3 でオリジンが更新されたら自動でキャッシュ削除する仕組みを作る

在宅勤務で引きこもりの才能に目覚めた DevOps チームの Shino です。

美味しいラーメンを食べたい衝動が時々訪れるのが悩みです。 おすすめの取り寄せラーメンがありましたら教えて頂きたく。(ジャンル問わず)

今回は CloudFront + S3 の構成で S3 上のコンテンツが更新されたら自動で CloudFront 上のキャッシュを削除する仕組みを作ったので簡単にまとめます。

概要

状況

CloudFront + S3 の構成で静的 Web ページなどの配信を行っている。

CloudFront が S3 上のコンテンツをキャッシュしてくれるので配信自体のパフォーマンスは悪くない。

課題

S3 上のコンテンツを更新するたびに、手動で CloudFront 上のキャッシュを削除しているため手間がかかる。

∵ キャッシュされたままの古いコンテンツが配信されてしまうのを防ぐため

解決策

Lambda を用いて S3 上のコンテンツが更新されたら自動で CloudFront 上のキャッシュを削除する。

f:id:shino8383:20200507001849p:plain

やったこと

全体の流れ

  1. S3 のオブジェクトが更新・作成されると Lambda にバケット名・オブジェクトのパスの情報を含んだイベントが渡される
  2. Lambda 上で、渡された S3 バケットをオリジンとする CloudFront ディストリビューションを取得する
  3. 取得した CloudFront ディストリビューションに対して、渡されたオブジェクトのパスを元にキャッシュ削除を実行する

実装内容

■ S3 - Lambda 間連携

1.S3 のオブジェクトが更新・作成されると Lambda にバケット名・オブジェクトのパスの情報を含んだイベントが渡される

これは Lambda のトリガーに S3 を設定すれば自動でイベント通知してくれます。 すべての更新イベントに対して設定しておいて問題ないかと思います。

f:id:shino8383:20200507024817p:plain

■ Lambda の実装

叩く API は実装する言語を問わず共通のはずなので、API リファレンスを記載しながら説明します。

2.Lambda 上で、渡された S3 バケットをオリジンとする CloudFront ディストリビューションを取得する

これは CloudFront API

ListDistributions - Amazon CloudFront

を使ってディストリビューションの一覧を取得します。

ただ、ListDistributions にはフィルターの機能はないので目的のオリジンを持つディストリビューションかどうかの判定は自前で条件分岐を作るしかないかと思います。

次に、

3.取得した CloudFront ディストリビューションに対して、渡されたオブジェクトのパスを元にキャッシュ削除を実行する

これは CloudFront API

CreateInvalidation - Amazon CloudFront

を用いてキャッシュ削除を実行します。 引数でディストリビューションの指定とオブジェクトのパスの指定を行います。

実装上の注意点

今回、実装するにあたって注意した点が3つあります。

  • Lambda 関数の冪等性
  • API のリクエスト数制限
  • Lambda の利用料

■Lambda 関数の冪等性

Lambda 関数の冪等性については調べれば色々情報が出てくると思いますので詳しくは触れません。 簡単に説明しますと

"実行した回数に関わらず同じ結果が得られる性質"

のことを冪等性といいます。

そして、

Lambda は複数回実行される可能性がある

という仕様があるので冪等性を考慮しています。 これは今回のケースで言えば S3 上のオブジェクトを 1 回更新しても、Lambda は 1 回以上実行される可能性があるということです。

今回実装したキャッシュ自動削除では、

オブジェクトの更新 1 回に対して、キャッシュ削除が 1 回だけ実行される

という結果が欲しかったので、キャッシュ削除の API を叩く前に 実行中のキャッシュ削除がないかを判定するロジックを設けました。 この判定がなければ、同じオブジェクトに対する複数のキャッシュ削除が重複することになります。

キャッシュ削除の一覧は

ListInvalidations - Amazon CloudFront

で取得しました。

f:id:shino8383:20200507041107p:plain

API のリクエスト数制限

AWSAPI には一定時間当たりのリクエスト上限があります。 一定時間内にリクエスト上限を超えるとエラーが返却されます。

例えば SNSAPI の一つ、 Subscribe は 1 秒あたり 100 回の制限があります。

Subscribe - Amazon Simple Notification Service

This action is throttled at 100 transactions per second (TPS).

CloudFront の API リファレンスには回数の記載がないですが、制限自体はあるはずです。

実装の際にリクエスト回数が増えすぎないかテストしていましたが、ThrottlingException が何度か返ってきていた記憶があります。

Common Errors - Amazon CloudFront

つまり今回のケースでは、オブジェクト更新が一度に大量に行われる場合などを考慮して必要以上に API をコールしないような実装が必要です。

冪等性の件でキャッシュ削除を行わない分岐を設けたのは、影響は軽微ですがリクエスト数の観点からも良いと言えるのではないかと思います。

■Lambda の利用料

最後にお金の話です。

最低でも更新されたオブジェクトの数だけ Lambda が実行されるので Lambda の利用料にも気をつかうことにします。

Lambda の利用料は

利用料 = 実行回数 * 割り当てたメモリの量

で決まります。

Lambda 上のロジックでは実行回数(=更新するオブジェクトの数)のコントロールはできないので、割り当てるメモリの量が小さくなるように配慮します。

具体的に言えば、一度に大量にデータを保持する処理は避けることにします。

今回の実装の中で、データを大量に取得しやすいのが

ListInvalidations - Amazon CloudFront

だと思ったのでこちらの処理に対して手を加えることにします。引数で何も指定しなければ指定したディストリビューションについて過去のキャッシュ削除履歴を相当な件数引っ張ってくることになるかと思います。 (15000 件近く返却されるところまでは確認しました)

今回は、弊社の CloudFront + S3 の利用状況も考慮した上で

  • 取得するキャッシュ削除履歴の数を引数で指定
  • キャッシュ削除の履歴を新しいものから 10 件取得する。
  • 実行中の履歴があればもう 10 件取得する。
  • これを最大で直近 30 分以内の履歴について行う。

という実装をしました。

この実装はリクエスト数を増やしている側面もあるので、リクエスト数の増加とデータ取得量最適化の天秤になるかと思います。

最後に

CloudFront + S3 の構成はよくある構成だと思いますので、実装の際に考えていたことを改めて言語化してみました。

この Lambda (とその中で叩かれる API )は大量に実行される可能性があるので、 キャッシュ自動削除の対象にする S3 バケットやオブジェクトのパスはちゃんと考察するべきかと思います。

ちなみに今回は Slack にキャッシュ削除の開始通知を飛ばすところまで実装しました。

f:id:shino8383:20200514192553p:plain

これからもすきあらば自動化等で日々の業務のコストを下げていけたらいいなと思っています。

では今回はこのへんで。

DevOps チームは仲間を募集中です👏

👏採用/求人情報 | アピール | 未来の教育を作る人のマガジン インフラエンジニア(AWS)