CloudFrontのキャッシュクリア(invalidation)を自動化してみた – 仕組みやCLIコマンドを解説

こんにちは。てぃろです。

CloudFrontのキャッシュ(chache)をうまく使えてますか?

CloudFrontではWebアプリ配信のレスポンスをよくするためにキャッシュを使えます。しかし、アプリを新たにデプロイする時にはキャッシュをクリア(削除)しないと、古いアプリのまま使われてしまう恐れがあります。

そこで、アプリをデプロイするたびにCloudFrontのキャッシュを自動的にクリアできるようにしたいと思います。

CloudFrontのキャッシュ詳しい仕様は公式ドキュメントをご覧ください。

今回も自作のアプリフレームワークを題材に解説していきます。以下がフレームワークの概要説明の資料です。

CloudFrontのキャッシュクリアの操作をマネジメントコンソールで確認する

まず、CloudFrontのキャッシュクリアの操作がどのようなものか見ていきます。

突然ですが、公式ドキュメントやマネジメントコンソールを見てもわかりますが、CloudFrontではキャッシュをクリア(削除)するという表現は使いません

CloudFrontのキャッシュクリアすることはファイルの無効化と言います。無効化は英語でinvalidationといい、随所にこの単語が現れてきます。

コメント 2020-05-04 061826
invalidationというタブがいわゆるキャッシュクリアの画面

ファイル無効化の操作は”無効化リクエストを作成する”ことと等価です。

その操作は簡単です。上記のマネジメントコンソールの画面から”Create Invalidation”をクリックして出てくる以下の画面でキャッシュの中からクリアしたいファイルパスを指定するだけです。

Inkedコメント 2020-05-04 061513_LI

改行で複数指定も可能です。ファイルパスを工夫すれば、ピンポイントで対象のファイルのみをクリアするといったことも可能です。

ですが、今回はアプリをリリースする話なので、キャッシュすべてをクリアするために上記画像のように”/*”と設定します。

これで画面右下の”Invalidate”ボタンを押せば、ファイル無効化(=キャッシュクリア)が行われます。

これでファイル無効化の操作のイメージを掴んでいただけたかと思います。

では次にこれをCI/CDで自動化していきましょう。そのためにはこの操作をコマンドで実行できるようにしていきます。そのために以下の通りに進めます。

  • AWS CLIを使ったファイル無効化のコマンドを作る
  • コマンドをCI/CDパイプラインに組み込む

AWS CLIを使ったファイル無効化のコマンドを作る

CI/CDパイプラインで動作させるために、ファイル無効化リクエストをAWS CLIで送れるようそのコマンドを作成します。

結論から書くと、以下のようなコマンドになりました。

json=`aws cloudfront list-distributions --query DistributionList.Items[]`
Id=`echo $json | jq -r '.[] | select(.Comment=="vueslsapp") | .Id'`
if [ -n "$Id" ] ;then aws cloudfront create-invalidation --distribution-id $Id --paths "/*";else echo "Not found the Distribution."; fi

人によっては、1行目と2行目を合わせてワンライナーにすると思います。ワンライナーってかっこいいのですが、私はワンライナーの可読性の低さがあまり好きではないので意図的に分けています。

では、ここからこのコマンドを作る過程を順を追って説明をしていきます。

AWS CLIのファイル無効化コマンドの仕様を確認する

ファイル無効化のコマンドは公式ドキュメントによると、このような仕様です。

 create-invalidation
--distribution-id <value>
[--invalidation-batch <value>]
[--paths <value>]
[--cli-input-json <value>]
[--generate-cli-skeleton <value>]

つまり、このコマンドを使うにはDistribution ID(CloudFrontを識別するID)が必要になります。またここではオプション扱いされてますが、実は”–paths”(クリア対象のファイルパス)も必須です。

Distribution IDを設定値として持つのは面倒なので、これもAWS CLIで事前に取得するようにします。

しかし、Distribution IDを取得しようとしたとき、このアプリの構成上初回のデプロイではCloudFrontが存在しないのでDistribution IDが取得できません。(CloudFrontは後付けでデプロイする形で作っている)

そうでなくても、初回デプロイはキャッシュもないのでファイルの無効化は必要ないです。そのため初回デプロイはファイル無効化をしないような例外処理も入れます。

まとめると、以下の要件を満たすコマンドであると言えます。

  • AWS CLIコマンドの”create-invalidation”を使用してキャッシュクリアする
  • キャッシュクリア範囲は、すべてのパス(/*)
  • Distribution IDもAWS CLIコマンドを使用して取得する
  • 初回デプロイ時にはDistribution IDが取得できなくてもCI/CDパイプラインを失敗にしない(かつ、取得できなかったことがわかるメッセージを出す)

次に、Distribution IDの取得方法についてみていきます。

対象のDistribution IDをDistributionListから抽出する

AWS CLIには、CloudFrontのDistribution(CloudFrontの単位)のリストを取得するコマンドがあります。

公式ドキュメントをよく見ると、ec2のコマンドにあるような”–filter”オプションがありませんので、”–query”オプションを使って可能な限り絞り込みをします。この辺りは以下の記事が参考になりました。

これをもとに作ったDistributionをリストするコマンドはこちら。

aws cloudfront list-distributions --query DistributionList.Items[]

実行結果は以下のようになります(長いです)。

thiroyoshi@thiroyoshi-W10:~$ aws cloudfront list-distributions --query DistributionList.Items[]
[
   {
       "Status": "Deployed",
       "Comment": "vueslsapp",
       "ViewerCertificate": {
           "SSLSupportMethod": "sni-only",
           "Certificate": "arn:aws:acm:us-east-1:XXXXXXXX:certificate/XXXXXX",
           "CertificateSource": "acm",
           "MinimumProtocolVersion": "TLSv1",
           "ACMCertificateArn": "arn:aws:acm:us-east-1:XXXXXX:certificate/XXXXXX"
       },
       "Origins": {
           "Quantity": 1,
           "Items": [
               {
                   "Id": "dev-vueslsapp.thiroyoshi.com",
                   "S3OriginConfig": {
                       "OriginAccessIdentity": "origin-access-identity/cloudfront/XXXXXXXXXXX"
                   },
                   "DomainName": "dev-vueslsapp.thiroyoshi.com.s3.amazonaws.com",
                   "CustomHeaders": {
                       "Quantity": 0
                   },
                   "OriginPath": ""
               }
           ]
       },
       "Enabled": true,
       "WebACLId": "",
       "DefaultCacheBehavior": {
           "MaxTTL": 86400,
           "TrustedSigners": {
               "Enabled": false,
               "Quantity": 0
           },
           "ViewerProtocolPolicy": "redirect-to-https",
           "SmoothStreaming": false,
           "DefaultTTL": 3600,
           "ForwardedValues": {
               "QueryString": true,
               "Cookies": {
                   "Forward": "none"
               },
               "QueryStringCacheKeys": {
                   "Quantity": 0
               },
               "Headers": {
                   "Quantity": 0
               }
           },
           "MinTTL": 0,
           "TargetOriginId": "dev-vueslsapp.thiroyoshi.com",
           "AllowedMethods": {
               "CachedMethods": {
                   "Quantity": 2,
                   "Items": [
                       "HEAD",
                       "GET"
                   ]
               },
               "Quantity": 2,
               "Items": [
                   "HEAD",
                   "GET"
               ]
           },
           "Compress": false
       },
       "CustomErrorResponses": {
           "Quantity": 2,
           "Items": [
               {
                   "ErrorCachingMinTTL": 0,
                   "ResponseCode": "200",
                   "ErrorCode": 403,
                   "ResponsePagePath": "/index.html"
               },
               {
                   "ErrorCachingMinTTL": 0,
                   "ResponseCode": "200",
                   "ErrorCode": 404,
                   "ResponsePagePath": "/index.html"
               }
           ]
       },
       "Aliases": {
           "Quantity": 1,
           "Items": [
               "vueslsapp.thiroyoshi.com"
           ]
       },
       "CacheBehaviors": {
           "Quantity": 0
       },
       "Id": "AAAAAAAAAAA",
       "ARN": "arn:aws:cloudfront::XXXXXXXXXXX:distribution/AAAAAAAAAAA",
       "DomainName": "dzk869wnnytdm.cloudfront.net",
       "IsIPV6Enabled": true,
       "PriceClass": "PriceClass_200",
       "Restrictions": {
           "GeoRestriction": {
               "RestrictionType": "none",
               "Quantity": 0
           }
       },
       "HttpVersion": "HTTP2",
       "LastModifiedTime": "2020-04-25T21:35:03.593Z"
   }
]

やりたいのは、上記の結果のなかの “Id”: “AAAAAAAAAAA”の部分を抽出することです。

上記の結果は要素数が一つの配列です。このときはDistributionが一つだからなのですが、複数になると要素数が増えるので目当てのDistributionのみを抽出する必要があります。

“–filter”オプションが使えないので、この結果をもう一つコマンドを作ってそれでフィルターしていきます。

そのために、jsonという変数に結果を代入していきます。結果、コマンドは以下のようになります。

json=`aws cloudfront list-distributions --query DistributionList.Items[]`

ここで得られたjsonはJSONの形式になっていますので、jqコマンドで要素を操作することができます。

https://stedolan.github.io/jq/

jqコマンドにはselectクエリがあり、条件で指定の配列の要素を抽出できますので、これを活用して以下のようなコマンドとしました。

echo $json | jq -r '.[] | select(.Comment=="vueslsapp") | .Id

selectに使う条件はなんでもいいと思います。私の場合Distributionのコメントとしてアプリ名を入れていたので、今回はそれを抽出条件にしました。

もっとスマートにやるなら、Tagsとか使えるといいかもしれませんね。

ちなみに、jqコマンドについてお世話になったのはコチラの記事です。

最後に、ファイル無効化のコマンドに抽出できたDistribution IDを渡す必要があるので、変数に代入しておく形にしておきます。

Id=`echo $json | jq -r '.[] | select(.Comment=="vueslsapp") | .Id'`

ここまででキャッシュクリア対象のDistribution IDが抽出できました。

次に、キャッシュクリアを実行していきます。

キャッシュクリアをリクエストする(Distribution IDがなければメッセージ出力)

AWS CLIでキャッシュクリアを実行していきます。

ここで”初回デプロイ時にはDistribution IDが取得できなくてもCI/CDパイプラインを失敗にしない(かつ、取得できなかったことがわかるメッセージを出す)”ということを思い出しましょう。

CloudFrontがデプロイされていない状態の場合には、先のコマンドの結果として、変数$Idが空文字になります

つまり、初回のCloudFrontがない場合の対処方法は、ファイル無効化コマンド実行前の変数$Idの空文字判定と言えます。

ところで、bashの空文字判定は以下のようにしてできます。

if [ -n "$Id" ] ;then <do something>;else <do something>; fi

“-n”は対象が空文字でなければTrueを返します。つまり、thenのあとにAWS CLIコマンドを、elseのあとにメッセージ出力を入れればOKです。

結果、ファイル無効化コマンドは以下のようになります。

if [ -n "$Id" ] ;then aws cloudfront create-invalidation --distribution-id $Id --paths "/*";else echo "Not found the Distribution."; fi

これでファイル無効化のためのコマンドのすべてが完成しました!

もう一度できたものをまとめると、以下の通りです。

json=`aws cloudfront list-distributions --query DistributionList.Items[]`
Id=`echo $json | jq -r '.[] | select(.Comment=="vueslsapp") | .Id'`
if [ -n "$Id" ] ;then aws cloudfront create-invalidation --distribution-id $Id --paths "/*";else echo "Not found the Distribution."; fi

あとは、これをCI/CDパイプラインで実行できるように、ジョブの中に組み込んで下さい。

アプリのデプロイが終わった後に最後に実行されるように入れるのがオススメです。

まとめ

今回はCloudFrontのキャッシュクリアを自動化してみました。

コマンドを作ってCI/CDパイプラインにのせられる形にすることが今回の肝です。記事内でも書いていましたが、Distribution IDを抽出する条件はcomment以外でも可能だと思います。ご自分のプロジェクト特性に合わせて修正してください。

CloudFrontのコマンドに”–filter”があればよかったのですが、なぜなかったのか…。それでもそれなりにスッキリしたコマンドになってよかったなと思っています。

それでは、よいCloudFrontライフを!