Amazon API Gateway の「キーごとのキャッシュの無効化」機能を使って特定リソースパスのみキャッシュ更新をしてみた

2023.02.20

いわさです。

先日以下の記事にて API Gateway のキャッシュ機能と使用量プランの関係を整理しました。

キャッシュを有効化した際、設定項目に以下の「キーごとのキャッシュの無効化」という項目がありましたがその時はデフォルト設定でスルーしていました。

CloudFront でキャッシュをクリアする際にはパスパターンを指定してキャッシュクリアが可能ですが、API Gateway のキャッシュクリアは操作としてはステージごとに一括クリアしか出来ません。

ただし通常のキャッシュクリア操作以外にも、実は HTTP リクエストの内容次第でキャッシュ有効期限前に強制的にキャッシュをリロードすることが出来ます。それがこの「キーごとのキャッシュの無効化」です。

ここでいう「キー」はキャッシュキーを指しており、「無効化」というのは古いキャッシュをクリアして新しいキャッシュを取得することを指しています。

本日はこの機能を使って指定したクライアントから特定パスのキャッシュクリアを行ってみました。
また、キャッシュを誰がクリア出来るのか、クリア出来ないようにした場合の挙動がどうなるのかについても整理したので紹介します。

そもそものキャッシュクリア機能は一括のみ

前回の記事で以下のようにキャッシュを有効化した API を作成しました。

% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/222"   
{"datetime":"2/19/2023 9:40:07 PM"}
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/222"
{"datetime":"2/19/2023 9:40:07 PM"}

まずおさらいですが API Gateway のキャッシュクリアは以下のボタンからクリアすることが出来ます。ここではクリアするリソースパスなどは指定せずに一括でのクリアとなっています。

クリアに少し時間を要する場合があるという注意内容が表示されていましたが、私が検証したミニマムな環境で数秒でクリアされました。

クリア後は初回のリクエストにて次のように最新のレスポンスがキャッシュされてました。

% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/222"
{"datetime":"2/19/2023 9:41:15 PM"}
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/222"
{"datetime":"2/19/2023 9:41:15 PM"}

「キーごとのキャッシュの無効化」を使って古いキャッシュを無効化する

ではここから機能の解説をして実際に使ってみます。
API Gateway では先程のキャッシュクリア操作以外にCache-Control: max-age=0ヘッダーを付与してリクエストによって API Gateway 上のキャッシュを強制的に更新することが出来ます。

ちなみに、「再読み込み」時のブラウザ仕様は一般的に以下のようになっているようです。

最近のブラウザーは、後方互換性のために max-age=0 を「再読み込み」の際に使用し、no-cache を使用して「強制再読み込み」を行うようになっています。

Cache-Control - HTTP | MDN より引用

API Gateway の以下の設定ではこのリクエストを許可する範囲や挙動を設定することが出来ます。

設定項目としてはまず許可が必要かどうかを設定します。
このチェックボックスを OFF にした場合は許可が不要となる、全てのクライアントからの上記リクエストが処理されます。

許可が必要になった場合は、キャッシュクリアアクションが許可された IAM による SigV4署名が必要になります。
そして署名されていないリクエストの場合はキャッシュクリアされないのですが、その際にどういうエラー処理動作となるのかを以下の3つから選択出来ます。

  • キャッシュコントロールヘッダーを無視し、レスポンスヘッダーで警告を追加
  • キャッシュコントロールヘッダーを無視
  • 403 ステータスコードでリクエストは失敗

実際に試すとよりイメージがしやすいです。試してみましょう。

許可不要。誰でもキャッシュクリア可能

先程の UI で「許可が必要」のチェックを OFF にしました。TTL は 3600 秒です。
ちなみにこの設定を変更した際のステージのデプロイは不要です。保存して少しすると設定が反映されました。(タイムラグがちょっとある)

前提として次のようにパスパラメータ(222 や 111)をキャッシュキーとしています。
通常リクエストすると次のようにパラメータごとにキャッシュされていることが確認出来ます。

% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/222/111"
{"datetime":"2/19/2023 10:02:37 PM"}
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/222/111"
{"datetime":"2/19/2023 10:02:37 PM"}
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/222/222"
{"datetime":"2/19/2023 10:02:42 PM"}
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/222/222"
{"datetime":"2/19/2023 10:02:42 PM"}

ここで特定のパスパラメータのみCache-Control: max-age=0ヘッダーを追加してみましょう。

% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/222/222"
{"datetime":"2/19/2023 10:02:42 PM"}
% curl -H "Cache-Control: max-age=0" "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/222/222"
{"datetime":"2/19/2023 10:03:22 PM"}
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/222/111"                              
{"datetime":"2/19/2023 10:02:37 PM"}
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/222/222"                              
{"datetime":"2/19/2023 10:03:22 PM"}

上記ハイライト箇所でヘッダーを追加しています。
タイムスタンプが更新されているのでキャッシュが使われずに統合バックエンドで処理されていますね。
そして以降のキャッシュはそこで取得された最新のものが使われていることもわかります。

また、上記更新リクエストは対象のキャッシュキーのみで、別のキャッシュキーのレスポンスは古いキャッシュのままですね。

当然ながらヘッダーが付与された際はキャッシュが使用されないので誰でも利用出来るようにした場合は次の懸念があり、注意事項として公式ドキュメントでも言及されています。

すべてのクライアントが API キャッシュを無効にすると、API のレイテンシが大幅に増加する可能性があります。

許可が必要

まず、許可が必要を ON にすると先ほどのように誰でもクリアができなくなります。
後述しますが、IAM 認可ポリシーで許可されているトークンを使ったリクエストでのみ処理が可能です。

そして許可されていないキャッシュクリアリクエストをどのように処理するかを選択することが出来ます。

キャッシュコントロールヘッダーを無視、レスポンスヘッダーで警告を追加

このオプションを選択した場合はキャッシュコントロールヘッダーを付与しても無視されます。
ここでいう無視というのはキャッシュクリアせずに、ヘッダー指定されなかった場合と同様に引き続き古いキャッシュが使われるということを指します。

このオプションではさらにレスポンスヘッダーに警告が追加されます。

% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/222/222"
{"datetime":"2/19/2023 10:03:22 PM"}
% curl -i -H "Cache-Control: max-age=0" "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/222/222"
HTTP/2 200 
content-type: application/json
content-length: 36
date: Sun, 19 Feb 2023 22:05:19 GMT
x-amzn-requestid: 2ae946b9-a0d5-4c92-8fa1-dcf42b44437d
x-amz-apigw-id: Am189HU_NjMF26A=
x-amzn-trace-id: Root=1-63f29d1f-45b713093bdea8f0148e1314
warning: 199 Cache-control headers were ignored because the caller was unauthorized.
x-cache: Miss from cloudfront
via: 1.1 ba1ce9c69a66256a857451734e2da0ae.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT57-P2
x-amz-cf-id: 70KEPYXU4pWODskvD-NvGeY-WNa6dDsdfGrcKFqALcebUOKXtyyTmA==

{"datetime":"2/19/2023 10:03:22 PM"}

キャッシュコントロールヘッダーを無視

このオプションは先程とほぼ同じですが、レスポンスヘッダーでの警告がされません。
単純に、ただただ無視されます。

% curl -i -H "Cache-Control: max-age=0" "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/222/222"
HTTP/2 200 
content-type: application/json
content-length: 36
date: Sun, 19 Feb 2023 22:06:31 GMT
x-amzn-requestid: d51faa9b-b2d2-413d-a459-6fba29fda6be
x-amz-apigw-id: Am2IJGY9NjMFslw=
x-amzn-trace-id: Root=1-63f29d67-444ca4ec06eb5feb0f193696
x-cache: Miss from cloudfront
via: 1.1 bb1254d529a36c3ccadc99ae5b0b3ffa.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT57-P2
x-amz-cf-id: W8KtfZ_dv4oyk5Oom9pmTK3WEZSJzYrcBAzOXDZNs5XnjLXZOZeD4Q==

{"datetime":"2/19/2023 10:03:22 PM"}

403 ステータスコードでリクエストは失敗

このオプションは先程の2つと異なり、403 エラーとなります。古いキャッシュすら取得されません。

% curl -i -H "Cache-Control: max-age=0" "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/222/222"
HTTP/2 403 
content-type: application/json
content-length: 42
date: Sun, 19 Feb 2023 22:08:19 GMT
x-amzn-requestid: 5bede526-7c73-4b43-b3ec-858a8f76fffa
x-amzn-errortype: MissingAuthenticationTokenException
x-amz-apigw-id: Am2ZEFQDtjMFnhg=
x-cache: Error from cloudfront
via: 1.1 24763e4640ebb0bb6627bbd182fff826.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT57-P2
x-amz-cf-id: qX48WVHtPNnjEAYJ0oYLZYIppu-3evzhB0TzhXRQlJmT2EfOwsZJOA==

{"message":"Missing Authentication Token"}

ただし、キャッシュコントロールヘッダーがなければ期待どおり古いキャッシュが取得されます。

% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/222/222"                             
{"datetime":"2/19/2023 10:03:22 PM"}

許可されたリクエスト

SigV4 署名付きリクエストを行ってみましょう。
以下のツールを使うと簡単に送信することが出来ますので今回使っています。

ここではhogeプロファイルは管理者権限を持っていますのでキャッシュ無効化アクションも許可されています。

# キャッシュクリア指定しない通常のリクエスト
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/222/222"
{"datetime":"2/19/2023 10:31:17 PM"}
# 署名せずにキャッシュクリア
% curl -H "Cache-Control: max-age=0" "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/222/222"
{"message":"Missing Authentication Token"}
# 署名してキャッシュクリア
% awscurl -H "Cache-Control: max-age=0" --region ap-northeast-1 --profile hoge "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/222/222"
{"datetime":"2/19/2023 10:32:00 PM"}
# キャッシュクリア後の通常リクエスト
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/222/222"                              
{"datetime":"2/19/2023 10:32:00 PM"}

署名するとキャッシュを更新することが出来ましたね。
ポイントとしてはこれを利用するために IAM 認可の設定を行う必要はないという点です。
API 自体の認可自体は引き続きオーソライザーを使ったり匿名だったりが可能です。あくまでもリクエストに署名を含めてアクションとして許可してやれば良いという感じです。

ここで IAM ポリシーで対象プロファイルのキャッシュクリアを拒否してみましょう。
アクションはexecute-api:InvalidateCacheです。

次の IAM ポリシーをアタッチします。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Deny",
            "Action": "execute-api:InvalidateCache",
            "Resource": "*"
        }
    ]
}

この場合は次のようにリクエストに失敗します。

% awscurl -H "Cache-Control: max-age=0" --region ap-northeast-1 --profile hoge "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/222/222"
{"Message":"User: arn:aws:iam::123456789012:user/hoge is not authorized to perform: execute-api:InvalidateCache on resource: arn:aws:execute-api:ap-northeast-1:********9012:gxtxguf8q8/Prod/GET/hello/222/222 with an explicit deny"}
Traceback (most recent call last):
  File "/Users/iwasa.takahito/.pyenv/versions/3.9.8/bin/awscurl", line 8, in <module>
    sys.exit(main())
  File "/Users/iwasa.takahito/.pyenv/versions/3.9.8/lib/python3.9/site-packages/awscurl/awscurl.py", line 521, in main
    inner_main(sys.argv[1:])
  File "/Users/iwasa.takahito/.pyenv/versions/3.9.8/lib/python3.9/site-packages/awscurl/awscurl.py", line 515, in inner_main
    response.raise_for_status()
  File "/Users/iwasa.takahito/.pyenv/versions/3.9.8/lib/python3.9/site-packages/requests/models.py", line 1021, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 403 Client Error: Forbidden for url: https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/222/222

さいごに

本日は Amazon API Gateway の「キーごとのキャッシュの無効化」機能を使って特定リソースパスのみキャッシュ更新をしてみました。

API Gateway のキャッシュはそもそも最大 3600 秒なので、CloudFront でたまにやる「長期間設定した上で必要に応じてクリアする」ような運用は適していない気がします。
さらに今回の機能はキャッシュキーごとにひとつづつクリアする必要があり特定範囲を一括でクリア出来るものではないので、そのあたりを考えると冒頭のブラウザの再読み込みに対応する必要がある場合だったり、あるいはケースによってはピンポイントでキャッシュをクリアしたいなどに利用出来る感じでしょうか。

おそらく初めてキャッシュを有効化する際に「この設定なんだろ?」となると思うので覚えておくと良いですね。