API Gateway の API Key を調べてみた

API Gateway の API Keyと使用量プランを使ってクォータ、スロットリングでAPI制御する方法を調べてみました
2019.12.10

西田@大阪です

API Gateway の API Keyと使用量プランを使ってクォータ、スロットリングでAPI制御する方法を調べてみました

何に使えるの?

ユーザーが一定期間内に何回APIを利用したかを計測し、計測した値をもとにクォータやスロットリングなどの制限を設定することができます

ただしAPI Keyを唯一の認証にはつかってはいけません。API Key は API に対してではなく、利用量プランに紐づくため、利用量プランが別のAPIに紐付けられると、同じAPI Keyを使って利用可能となってしまいます

そのため、思わぬユーザーにAPIをアクセスされる可能性があります

検証方法

API Keyを検証する方法は以下の2種類あります

  • Custom Authorizer
  • HTTP HEADER

使用量プランの制限

利用料プランを使ってユーザーのAPI利用を制限するには「クォータ」と「スロットリング」を使用します

クォータ

ユーザーがAPIを呼び出しできる「最大回数」を設定できます APIの呼び出し回数がリミットに達すると、「ピリオド」の期間中、APIの呼び出しが失敗し 401(Unauthorized) が返されるようになります

設定できるパラメーターは以下の通りです

名前 説明
Limit 呼び出しを行える最大回数です。API利用者はこれを超えて「Period」期間内にAPIの呼び出しを行うことができません。
Offset API Keyの利用開始時点の初期値を設定します。月や週の途中から契約が始まるようなケースで呼び出し回数を調整するために利用できます
Period リミットが適用される期間です。執筆時点ではDAY, WEEK, MONTHが指定できます

スロットリング

ユーザーがAPIを呼び出しできる「流量」を設定できます

1秒間あたりのAPIの呼び出しが、設定された「レート」を超えないように調整されます

「レート」を超えたAPI呼び出しは失敗し429(Too Many Requests)が返されます

ただし「レート」を超えたAPI呼び出しが直ちに429になるわけでなく、「バースト」に設定された値分バースト(レートを超えた呼び出し)が許容されます

詳しい情報については以下をご参考ください

API リクエストを調整してスループットを向上させる - Amazon API Gateway

設定できるパラメーターは以下の通りです

名前 説明
RateLimit 1秒あたりのリクエスト数の平均
BurstLimit バーストとして許容されるリクエスト数

設定

今回は Severless Framework(以下 sls) を利用して設定していきます

参考: https://serverless.com/framework/docs/providers/aws/events/apigateway#setting-api-keys-for-your-rest-api

設定する使用量プランは以下です

プラン リミット ピリオド レート バースト
free 100 DAY 1 2
paid 200 DAY 2 4

これを sls で設定するにはserveless.yml に以下のように設定します

provider:
  name: aws
  runtime: python3.8
  apiKeys:
    - free: # 使用量プラン
      - free-plan-key # 名前だけを指定するとKeyが自動生成されdeploy時に標準出力されます
      - name: free-plan-key2 # 名前とKeyの値を指定することもできます
        value: a123456789012345678901234567890 # 30-128文字の英数字
    - paid:
        - paid-plan-key
  usagePlan:
    - free:
        quota:
          limit: 100
          period: DAY
        throttle:
          burstLimit: 2
          rateLimit: 1
    - paid:
        quota:
          limit: 200
          period: DAY
        throttle:
          burstLimit: 4
          rateLimit: 2

functions:
  api:
    handler: handler.sample_api
    events:
      - http:
          path: sample
          method: get
          private: true # 使用量プラン有効にするエンドポイントにtrueを指定します

※ CFnを使用している関係でAPI Keyを更新するのにAPI Key名を変更する必要がある点に注意してください

確認

お手軽にベンチマークをかけれる vegeta を使って1秒間で2リクエストを5秒間アクセスし続けて確認してみます

# ※API_KEYにfreeのAPI Keyが設定されてる前提です
echo "GET https://sample-api-gw.example.com" | vegeta attack -header "X-API-KEY: $API_KEY" -rate=2 -duration=5s | vegeta report
Requests      [total, rate, throughput]  10, 2.22, 1.31
Duration      [total, attack, wait]      4.5744649s, 4.500001989s, 74.462911ms
Latencies     [mean, 50, 95, 99, max]    78.439416ms, 68.357618ms, 214.098603ms, 214.098603ms, 214.098603ms
Bytes In      [total, mean]              8788, 878.80
Bytes Out     [total, mean]              0, 0.00
Success       [ratio]                    60.00%
Status Codes  [code:count]               200:6  429:4

何回か試したところばらつきがありましたが、いくつかのリクエストが429が返却され失敗していることが確認できました

# ※API_KEYにpaidのAPI Keyが設定されてる前提です
echo "GET https://sample-api-gw.example.com" | vegeta attack -header "X-API-KEY: $API_KEY" -rate=2 -duration=5s | vegeta report
Requests      [total, rate, throughput]  10, 2.22, 2.17
Duration      [total, attack, wait]      4.60279174s, 4.499963195s, 102.828545ms
Latencies     [mean, 50, 95, 99, max]    88.608431ms, 78.065841ms, 214.345343ms, 214.345343ms, 214.345343ms
Bytes In      [total, mean]              14564, 1456.40
Bytes Out     [total, mean]              0, 0.00
Success       [ratio]                    100.00%
Status Codes  [code:count]               200:10

API Keyを paid に変更したところ、429が返却されなくりました

今度は1秒間で4リクエストを5秒間アクセスし続けます

# ※API_KEYにpaidのAPI Keyが設定されてる前提です
echo "GET https://sample-api-gw.example.com" | vegeta attack -header "X-API-KEY: $API_KEY" -rate=4 -duration=5s | vegeta report
Requests      [total, rate, throughput]  20, 4.21, 3.96
Duration      [total, attack, wait]      4.802387722s, 4.745446072s, 56.94165ms
Latencies     [mean, 50, 95, 99, max]    74.479068ms, 70.526317ms, 150.019519ms, 211.864973ms, 211.864973ms
Bytes In      [total, mean]              27693, 1384.65
Bytes Out     [total, mean]              0, 0.00
Success       [ratio]                    95.00%
Status Codes  [code:count]               200:19  429:1

こちらもばらつきがありましたが、429が返されるようになりました

プログラムから操作する

boto3をつかってpythonで使用量プラン、API Keyを操作する方法をご紹介します

使用量を取得

import boto3

apigw = boto3.client("apigateway")

res = apigw.get_usage(
    usagePlanId="${使用量プランID}",
    startDate="2019-12-01",
    endDate="2019-12-16",
)
print(res["items"])
{'${プランID}': [[0, 1000], [0, 1000], [0, 1000], [0, 1000], [0, 1000], [0, 1000], [0, 1000], [0, 1000], [0, 1000], [0, 1000], [0, 1000], [0, 1000], [0, 1000], [0, 1000], [215, 785]]}

新たなAPI Keyを作成し、使用量プランに紐付ける

import boto3
import uuid

apikey = apigw.create_api_key(
    name="${API Key名}",
    value=str("API Key"),
)

res = apigw.create_usage_plan_key(
    usagePlanId="${使用量プランID}",
    keyId=apikey["id"],
    keyType="API_KEY",
)

最後に

APIの使用量など永続可するのも難しく、課金に直結し失敗が許されない機能となりがちで、実装するのが難しい機能と思われます。 この部分にAWSで使われている仕組みを使えるのは助かる場面は多いのではないでしょうか

補足

スロットリングの制御に使われているトークンバケットについて補足しておきます トークンバケットは、ネットワークの流量などを調整するアルゴリズムで単純にレートを超える流量を禁止するのではなく、ある程度のバーストを許容するのが特徴です。詳しくは以下をご参考ください トークンバケット - Wikipedia

使用量プランのスロットリング機能は以下のAPI Gatewayのスロットリング機能を個別に適用できるようにしているものと予想されます API リクエストを調整してスループットを向上させる - Amazon API Gateway

そのため RateLimit と BurstLimit の以下の図のように BurstLimit の設定された値分 RateLimit の余剰分が溜まっていき、その分バーストのリクエストが許されるものと思われます

参考にした記事: 自分用メモ:Amazon API Gateway に学ぶトークンバケットアルゴリズム - Qiita