Amazon API Gateway のキャッシュを有効化した時に API キーの使用量がどのように算出されるか検証してみた

2023.02.19

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

いわさです。

Amazon API Gateway ではオプションでキャッシュ機能を使うことが出来ます。
API Gateway のキャッシュ機能を使うと、対象キャッシュキーのキャッシュデータが存在する場合は統合されたバックエンドは呼び出されずに API Gateway がレスポンスを生成してくれます。

一方で API Gateway には API キーと使用量プランを使って、API キーあたりの API 使用量を把握したり、あるいはレート制限を設定することが出来ます。
今回キャッシュを有効化した API で API キーの使用量がどのように計測されるのかを検証してみましたのでご紹介します。

流れとして API Gateway の作成からキャッシュの有効化、使用量の作成と評価までを行います。
一通り検証手順は残すのでちょっと冗長な感じです。必要なところだけ見てください。
先にまとめを以下に記しておきます。

先にまとめ

  • キャッシュが使われる呼び出しの場合でも API キーごとの使用量は変わらず計測される
  • レート制限も有効。レート制限に達していればキャッシュが存在していてもブロックされる
  • API キーはキャッシュキーに含まれないので異なる API キー間で同じキャッシュが共有される

検証用の API を用意

今回は SAM CLI の Hello World Example テンプレートを使って API Gateway + Lambda の API を用意します。
以下のコマンドでプロジェクトを作成した後に API Gateway の設定と Lambda のコードを少し修正してデプロイしましょう。

% sam --version
SAM CLI, version 1.72.0
% sam init --runtime dotnet6 
Which template source would you like to use?
        1 - AWS Quick Start Templates
        2 - Custom Template Location
Choice: 1

Choose an AWS Quick Start application template
        1 - Hello World Example
        2 - Data processing
        3 - Multi-step workflow
        4 - Scheduled task
        5 - Standalone function
        6 - Serverless API
Template: 1

:

Commands you can use next
=========================
[*] Create pipeline: cd hoge0219sam && sam pipeline init --bootstrap
[*] Validate SAM template: cd hoge0219sam && sam validate
[*] Test Function in the Cloud: cd hoge0219sam && sam sync --stack-name {stack-name} --watch

テンプレートでは以下の API 部分を修正し、パスパラメータを設定しました。
後ほどキャッシュキーに含めて、キャッシュキーが異なる場合の動きを確認するためです。

template.yml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  Sample SAM Template for hoge0219sam

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 10
    MemorySize: 128

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: ./src/HelloWorld/
      Handler: HelloWorld::HelloWorld.Function::FunctionHandler
      Runtime: dotnet6
      Architectures:
        - x86_64
      MemorySize: 256
      Environment: # More info about Env Vars: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#environment-object
        Variables:
          PARAM1: VALUE
      Events:
        HelloWorld:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /hello/{hoge1}/{hoge2}
            Method: get
:

Lambda コードは次のようにデフォルトのコードからさらに削りました。
リクエストパラメータは確認せずに、タイムスタンプを設定したレスポンス用のオブジェクトを作成します。

Function.cs

using System.Collections.Generic;
using System.Threading.Tasks;
using System.Text.Json;
using System;
using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.Core;

// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace HelloWorld
{

    public class Function
    {
        public async Task<APIGatewayProxyResponse> FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context)
        {
            var body = new Dictionary<string, string>
            {
                { "datetime", DateTime.Now.ToString() },
            };

            return new APIGatewayProxyResponse
            {
                Body = JsonSerializer.Serialize(body),
                StatusCode = 200,
                Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } }
            };
        }
    }
}

上記の準備が出来たらビルド&デプロイします。

% sam build
Building codeuri: /Users/iwasa.takahito/work/hoge0219apigwcache/hoge0219sam/src/HelloWorld runtime: dotnet6 metadata: {} architecture: x86_64 functions: HelloWorldFunction
Running DotnetCliPackageBuilder:GlobalToolInstall

:

% sam deploy --guided

:

Successfully created/updated stack - hoge0219sam-dotnet-apigw in ap-northeast-1

キャッシュも何もまだ設定していないですが、この時点での API の挙動を先に確認しておきましょう。

% curl https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/111/
{"datetime":"2/19/2023 12:11:02 AM"}
% curl https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/222/
{"datetime":"2/19/2023 12:11:20 AM"}
% curl https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/333/
{"datetime":"2/19/2023 12:11:22 AM"}

タイムスタンプが取得出来ていますね。

API Gateway のキャッシュを有効化する

次に API Gateway のキャッシュを有効化します。
ステージごとにキャッシュの有効化を設定します。

これによってキャッシュが有効な間はバックエンドの Lambda が実行されずに、API Gateway がキャッシュからレスポンスを生成してくれるようになります。
ただし、API Gateway のキャッシュを使用するにあたっていくつか制限がありますので利用前に確認しておきましょう。
特に、API Gateway が呼び出し回数に応じた料金になっているのに対してキャッシュ機能はプロビジョニングされている期間に応じた料金となっています。API のリクエスト数が全くなくても料金が発生します。

  • キャッシュを有効化している期間に応じた追加の料金が発生する
  • キャッシュ出来るレスポンスサイズに上限(1,048,576 バイト)あり

キャッシュを有効化すると、キャッシュインスタンスがプロビジョニングされます。
完了まで数分かかります。

以下の状態でキャッシュが利用可能になりました。

今回はリソースのパスからパラメータを取得するようになっていて、パラメータをキャッシュキーとしたいと思います。
メソッドリクエストでリクエストパスのキャッシュオプションを ON にします。
設定後にステージへデプロイしましょう。

リクエストしてみると、次のようにタイムスタンプが更新されていないことが確認出来ました。 Lambda が実行されて新たなレスポンスが生成されずに、API Gateway のキャッシュが使用されていますね。

% curl https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/222/
{"datetime":"2/19/2023 12:20:21 AM"}
% curl https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/222/
{"datetime":"2/19/2023 12:20:21 AM"}
% curl https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/333/
{"datetime":"2/19/2023 12:20:28 AM"}
% curl https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/333/
{"datetime":"2/19/2023 12:20:28 AM"}
% curl https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/222/
{"datetime":"2/19/2023 12:20:21 AM"}

API キーと使用量プランを使う

続いて、作成した API で API キーと使用量プランを使ってみたいと思います。
キャッシュを有効化した場合の挙動をしっかり把握したいと思います。

使用量プランと API キーはセットで使用する必要があります。
まずは使用量プランを作成し、キャッシュを有効化した API ステージを関連付けします。

続いて API キーを生成し、使用量プランにキーを関連付けします。
ここでは複数のキーをインポートしました。

最後に対象のリソースのメソッドリクエストで「API キーの必要性」を true に変更します。

ステージを再度デプロイ後に、cURL でリクエストを送信してみましょう。
まずは API キーなしの場合ですが、403 Firbidden となりました。

% curl https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/222/
{"message":"Forbidden"}

続いて API キーを設定しましょう。
デフォルトではリクエストの x-api-key ヘッダーにキーを設定します。

% curl -H "x-api-key: key1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/222/
{"datetime":"2/19/2023 12:56:07 AM"}
% curl -H "x-api-key: key2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/222/
{"datetime":"2/19/2023 12:56:07 AM"}
% curl -H "x-api-key: key3xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/222/
{"datetime":"2/19/2023 12:56:07 AM"}

それぞれのキーを変えて同じリソースにアクセスしたところ、全て同じ結果となりました。
このように API キーが別のものでも同じキャッシュが利用出来ることが確認出来ました。

カスタムオーソライザーによる API キー設定でも問題なし

先程は x-api-key ヘッダーを使用しましたが、以前に次のようにカスタムオーソライザーを使って様々な形で API キーを利用する方法を紹介しました。
問題なさそうな気がしますが念の為このオーソライザーを使った場合でもキャッシュが共有されるのか確認してみましょう。

% curl -H "x-api-key: key1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"  https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/333/
{"datetime":"2/19/2023 1:13:10 AM"}
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/333?hogeapikey=key1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"     
{"datetime":"2/19/2023 1:13:10 AM"}

問題なく同じキャッシュが使われていますね。

キャッシュヒットした場合でも使用量としてカウントされる

では適当な API キーを用意してキャッシュヒットさせてみたいと思います。
次のように複数のキーとリソースでキャッシュが有効な間に何度かリクエストを送信してみます。

  1. Key1 でリソースA へ 10 回
  2. Key2 でリソースB へ 5 回
  3. Key1 でリソースB へ 5 回
  4. Key2 でリソースA へ 5 回

以下の実行結果を見て頂くとわかりますが、キャッシュされたレスポンスが取得されており、Lambda の実行回数自体は 2~3 回だと思います。

実行結果
# Ax10
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/222?hogeapikey=key1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 
{"datetime":"2/19/2023 1:24:49 AM"}
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/222?hogeapikey=key1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{"datetime":"2/19/2023 1:24:49 AM"}
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/222?hogeapikey=key1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{"datetime":"2/19/2023 1:24:49 AM"}
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/222?hogeapikey=key1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{"datetime":"2/19/2023 1:24:49 AM"}
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/222?hogeapikey=key1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{"datetime":"2/19/2023 1:24:49 AM"}
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/222?hogeapikey=key1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{"datetime":"2/19/2023 1:24:49 AM"}
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/222?hogeapikey=key1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{"datetime":"2/19/2023 1:24:49 AM"}
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/222?hogeapikey=key1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{"datetime":"2/19/2023 1:24:49 AM"}
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/222?hogeapikey=key1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{"datetime":"2/19/2023 1:24:49 AM"}
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/222?hogeapikey=key1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{"datetime":"2/19/2023 1:24:49 AM"}

# Bx5
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/333?hogeapikey=key2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{"datetime":"2/19/2023 1:24:18 AM"}
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/333?hogeapikey=key2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{"datetime":"2/19/2023 1:24:18 AM"}
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/333?hogeapikey=key2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{"datetime":"2/19/2023 1:24:18 AM"}
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/333?hogeapikey=key2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{"datetime":"2/19/2023 1:24:18 AM"}
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/333?hogeapikey=key2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{"datetime":"2/19/2023 1:24:18 AM"}

# Cx5
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/333?hogeapikey=key1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{"datetime":"2/19/2023 1:25:19 AM"}
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/333?hogeapikey=key1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{"datetime":"2/19/2023 1:25:19 AM"}
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/333?hogeapikey=key1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{"datetime":"2/19/2023 1:25:19 AM"}
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/333?hogeapikey=key1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{"datetime":"2/19/2023 1:25:19 AM"}
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/333?hogeapikey=key1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{"datetime":"2/19/2023 1:25:19 AM"}

# Dx5
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/222?hogeapikey=key2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{"datetime":"2/19/2023 1:24:49 AM"}
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/222?hogeapikey=key2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{"datetime":"2/19/2023 1:24:49 AM"}
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/222?hogeapikey=key2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{"datetime":"2/19/2023 1:24:49 AM"}
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/222?hogeapikey=key2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{"datetime":"2/19/2023 1:24:49 AM"}
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/222?hogeapikey=key2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{"datetime":"2/19/2023 1:24:49 AM"}

使用量の確認が出来るまで少しタイムラグがあります。
この時は 4 時間後くらいに確認しました。

Key1 の使用量は 17 回でした。

Key2 の使用量は 12 回でした。

それぞれ実際試した回数よりも 2 回ほど多い気がしますが、キャッシュされたレスポンスが取得される場合でも API 使用回数に含まれていることが確認出来ました。

クォーターも効く

API キーが処理されてそのあと、キャッシュあるいは統合バックエンドに流れるような仕組みであることが想像出来ましたのでクォーター制限なども効きそうですね。
試してみましょう。

1 日に 10 リクエストまでというクォーターを使用量プランに設定しました。
キャッシュが有効な API に対して実行した場合どうなるでしょうか。

# 1 回目
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/222?hogeapikey=key3xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{"datetime":"2/19/2023 6:16:49 AM"}
# 2 回目
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/222?hogeapikey=key3xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{"datetime":"2/19/2023 6:16:49 AM"}

:

# 10 回目
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/222?hogeapikey=key3xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{"datetime":"2/19/2023 6:16:49 AM"}
# 11 回目
% curl "https://gxtxguf8q8.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/111/222?hogeapikey=key3xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{"message":"Limit Exceeded"}

キャッシュを使った場合でも正しくカウントされてクォーターに反映されていますね。

さいごに

本日は API Gateway のキャッシュを有効化した時に API キーの使用量がどのように算出されるか検証してみました。

API Gateway の前段に CloudFront を配置する場合もありますが、そこでキャッシュしようとすると API Gateway までリクエストが送信されなくなるので API キーや使用量プランを正しく使うことが出来なくなってしまいます。
その場合でも統合先バックエンドの負荷を下げたり API のパフォーマンスを向上したい場合は API Gateway のキャッシュを活用することが出来ます。
イメージとしては API Gateway と統合先バックエンドの間にキャッシュレイヤーが入る感じになるので、API Gateway の動的な制御部分は動作してくれる良い感じのキャッシュです。

API キーや使用量プランの機能を損なわずにキャッシュを導入することが出来るためユースケースが適当であればかなり有効だと思いました。
プロビジョニングタイプの料金プランになるのでそこだけ注意ですね。