echo("備忘録");

IT技術やプログラミング関連など、技術系の事を備忘録的にまとめています。

【Serverless Framework】APIキーの作成方法と注意点(後編:注意点と対策)

本日のお題

Serverless Framework(以下「SFW」)において、API GatewayAPIキーを作成する方法、及びその際の注意点について。

なお前回記載した通り、今回のお題は前編と後編の2回に分けており、この記事はその後編になります。

  • 前編:作成方法(これは前回
  • 後編:注意点と対策(今回はココ)

参考

TL; DR

  • provider.apiGateway.apiKeys 配列の末尾以外のAPIKeyを削除すると、予期しない挙動をするケースがある
  • 対策としては、enabled:false で無効化するか、resources.Resources に生CFnテンプレートを書く
    • もちろん、削除しないのが一番いい
  • APIキーの値を設定する可能性がある場合、APIキー名は設定しない方がよい。(必ずではない)

Serverless FrameworkでAPIキーを作成する際の注意点

先に結論から言うと、「apiKeys 配列で上にある(=末尾ではない)APIキーを削除する場合は注意が必要」となります。

そしてその際の挙動は「他のAPIキーに名前(=name)を設定しているものがあるかどうか」によって変わります。

ケース1:名前(=name)を設定していない場合

まず、現在のAPIキーに名前(=name)を設定しているものがない状態だったとします。

provider:
  apiGateway:
    apiKeys:
      - description: 削除するAPIキー
      - description: 削除しないAPIキー1
      - description: 削除しないAPIキー2

この状態で、一番上の「description: 削除するAPIキー」を削除してデプロイすると、最新のAPIキーは下記の状態となります。(1個ずつ上にスライドする感じ)

  • エラーは発生しない(=デプロイ自体は成功する)
  • 「description: 削除しないAPIキー1」のキーの値(=value)が、デプロイ前の「description: 削除するAPIキー」の値になっている
  • 同様に「description: 削除しないAPIキー2」のキーの値(=value)が、デプロイ前の「description: 削除しないAPIキー1」の値になっている
ケース2:名前(=name)を設定している場合

次に、現在のAPIキーに名前(=name)を設定しているものがある状態だったとします。

provider:
  apiGateway:
    apiKeys:
      - description: 削除するAPIキー
      - name: noDeletedKey1
        description: 削除しないAPIキー1
      - description: 削除しないAPIキー2

この状態で、一番上の「description: 削除するAPIキー」を削除してデプロイすると、下記の状態となります。

  • エラーが発生する(=デプロイ自体が失敗する)
  • 「ApiKey:noDeletedKey1 already exists」というエラーが表示される。(=名前の重複エラー)

対策

上記の現象は、簡潔に言うと「CloudFormation(以下CFn)の論理IDのズレ」が原因で起こります。
これの回避策としては、下記2点となります。

  1. enabled:false を設定して対応する
  2. resources.Resource セクションに生CFnテンプレートでAPIキーを定義する

なおこの「CFnの論理IDのズレ」については、この下の「参考:論理IDのズレが起こる理由」で説明しているので、興味があれば一度読んでみてください。(単にAPIキーを作るだけなら、特に知らなくても問題ありません。)

対策1:enabled:false を設定して対応する

これが一番手っ取り早いです。
enabled:false を設定すれば、そのAPI Keyは無効になりますし、API Key自体が削除されるわけではないので、先述の「論理IDのズレ」も発生しません。

ただし「使わなくなったAPIキーがずっと残り続ける」という問題があります。(許容できるなら問題なし)

対策2:resources.Resources セクションに生CFnテンプレートでAPIキーを定義する

生CFnテンプレートなら、論理IDを自分で設定でき、使わなくなったAPIキーを削除しても論理IDが変更されることはないので、これも「論理IDのズレ」は起こりません。

例えばこんな感じです。

resources:
  Resources:
    # APIキーの設定
    CfnApiGatewayApiKey1:
      Type: 'AWS::ApiGateway::ApiKey'
      Properties:
        Description: APIキー1
        StageKeys:
          # 「ApiGatewayRestApi」は、Serverless FrameworkがAPI Gatewayに対して付与する固定の論理ID
          - RestApiId: !Ref ApiGatewayRestApi
          - StageName: dev
    CfnApiGatewayApiKey2:
      Type: 'AWS::ApiGateway::ApiKey'
      Properties:
        Description: APIキー2
        StageKeys:
          - RestApiId: !Ref ApiGatewayRestApi
          - StageName: dev
  
    # 使用料プランを作成した場合、使用料プランとAPIキーの紐づけ設定
    CfnApiGatewayUsagePlanKey1:
      Type: AWS::ApiGateway::UsagePlanKey'
      Properties:
        KeyId: !Ref CfnApiGatewayApiKey1
        KeyType: 'API_KEY'
        # 「ApiGatewayUsagePlan」は、Serverless Frameworkが使用料プランに対して付与する固定の論理ID
        UsagePlanId: !Ref ApiGatewayUsagePlan
    CfnApiGatewayUsagePlanKey2:
      Type: 'AWS::ApiGateway::ApiKey'
      Properties:
        KeyId: !Ref CfnApiGatewayApiKey2
        KeyType: 'API_KEY'
        UsagePlanId: !Ref ApiGatewayUsagePlan

ただし、この方法を実施する場合、以下の問題があります。

  • APIキー(必要に応じて使用料プランとの紐づけ設定も)を自分で1から生CFnテンプレートを書く必要がある
    • 使用料プラン自体はServerless Frameworkの usagePlan で定義すればOK
  • 初回デプロイ時のみ、必ず「ApiKeyのデプロイステージが不正(=存在しない)」というエラーが出る
    • ステージ情報(AWS::ApiGateway::Deployment で設定)より先にApiKeyを作成してしまうため
    • Api Gatewayや使用量プランと異なり、ステージ情報の論理IDは毎回ランダムな文字列が設定されるため、DependsOn 設定ができない
    • 対策としては下記
      • 初回デプロイ時は、resources.Resources セクションのAPIキー、及び使用料プランとの紐づけ設定をコメントアウトしてデプロイする。(=ステージだけ先にデプロイする)
      • 2回目以降は、上記のコメント状態を解除してデプロイする(=ステージはすでに存在するので、デプロイ可能)

参考:論理IDのズレが起こる理由

Serverless Frameworkでは、serverless.ymlをCloudFormation(以下CFn)テンプレートに変換する際、apiKeys の配列に対して、上から順に「ApiGatewayApiKey1」「ApiGatewayApiKey2」「ApiGatewayApiKey3」...のように、「ApiGatewayApiKey+連番」という論理IDを付けます。

なので最初の状態は、CFnテンプレートは以下のようになります。

ApiGatewayApiKey1:
  Type: 'AWS::ApiGateway::ApiKey'
  Properties:
    Description: 削除するAPIキー
    StageKeys:
      # 「ApiGatewayRestApi」は、Serverless FrameworkがApi Gatewayに対して付与する固定の論理ID
      - RestApiId: !Ref ApiGatewayRestApi
      - StageName: dev
ApiGatewayApiKey2:
  Type: 'AWS::ApiGateway::ApiKey'
  Properties:
    Description: 削除しないAPIキー1
    StageKeys:
      - RestApiId: !Ref ApiGatewayRestApi
      - StageName: dev
ApiGatewayApiKey3:
  Type: 'AWS::ApiGateway::ApiKey'
  Properties:
    Description: 削除しないAPIキー2
    StageKeys:
      - RestApiId: !Ref ApiGatewayRestApi
      - StageName: dev

この状態で一番上の「description: 削除するAPIキー」を削除すると、最新のCFnテンプレートは以下のようになります。

ApiGatewayApiKey1:
  Type: 'AWS::ApiGateway::ApiKey'
  Properties:
    Description: 削除しないAPIキー1
    StageKeys:
      - RestApiId: !Ref ApiGatewayRestApi
      - StageName: dev
ApiGatewayApiKey2:
  Type: 'AWS::ApiGateway::ApiKey'
  Properties:
    Description: 削除しないAPIキー2
    StageKeys:
      - RestApiId: !Ref ApiGatewayRestApi
      - StageName: dev

つまりCFnテンプレート的には「ApiGatewayApiKey1&2のDescriptionだけが変更された」という扱いになってしまうので、「ケース1:名前(=name)を設定していない場合 」で説明した現象が発生します。(内部的には「削除するAPIキー」自体、削除されていない)

また「ケース2:名前(=name)を設定している場合」に関してですが、一番上の「description: 削除するAPIキー」の削除前後の最新のCFnテンプレートは以下のようになります。

# 削除前
ApiGatewayApiKey1:
  Type: 'AWS::ApiGateway::ApiKey'
  Properties:
    Description: 削除するAPIキー
    StageKeys:
      - RestApiId: !Ref ApiGatewayRestApi
      - StageName: dev
ApiGatewayApiKey2:
  Type: 'AWS::ApiGateway::ApiKey'
  Properties:
    Description: 削除しないAPIキー1
    Name:  noDeletedKey1
    StageKeys:
      - RestApiId: !Ref ApiGatewayRestApi
      - StageName: dev
ApiGatewayApiKey3:
  Type: 'AWS::ApiGateway::ApiKey'
  Properties:
    Description: 削除しないAPIキー2
    StageKeys:
      - RestApiId: !Ref ApiGatewayRestApi
      - StageName: dev
  
  
#削除後
ApiGatewayApiKey1:
  Type: 'AWS::ApiGateway::ApiKey'
  Properties:
    Description: 削除しないAPIキー1
    Name:  noDeletedKey1
    StageKeys:
      - RestApiId: !Ref ApiGatewayRestApi
      - StageName: dev
ApiGatewayApiKey2:
  Type: 'AWS::ApiGateway::ApiKey'
  Properties:
    Description: 削除しないAPIキー2
    StageKeys:
      - RestApiId: !Ref ApiGatewayRestApi
      - StageName: dev

この状態でデプロイしようとすると、CFnは下記の挙動を取ります。

  • 先頭の定義から順に処理を実施する
  • 「ApiGatewayApiKey1」にNameが新規に設定されたから、既存のAPIキー(=「description: 削除するAPIキー」のAPIキー) の削除→再生成(=置き換え)を実行しようとする
  • でも前回の「ApiGatewayApiKey2」で作成した「Name: noDeletedKey1」のキーがまだ存在している
  • Nameが重複しているのでエラー

結果、名前の重複エラーとなります。

ちなみに、名前を指定した際にAPIキーの削除→再生成が実施されるのは、CFnの仕様のようです。

  • 名前を指定すると、置き換えを必要とする更新を実施できない
  • 置き換えを実施する場合、名前を変更する必要がある
    • 今回は新たに名前を指定したので「置き換えを実施する必要がある」と判断された

If you specify a name, you cannot perform updates that require replacement of this resource. You can perform updates that require no or some interruption. If you must replace the resource, specify a new name.

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-apikey.html#aws-resource-apigateway-apikey-properties

前回の記事の中で「valueを設定する場合、name は設定しないことが推奨されている」と記載しましたが、この制約があるからだと思います。

告知

今週の2023/6/24(土) (AWS Dev Day 2023の翌日)に開催される「Reject Day 2023」(主催:JAWS-UG&AWS Startup Community)において「Mobageの監視環境をAWSで構築する話」という内容でお話しさせて頂くことになりました。

connpass.com

内容ですが、YAPC::Kyoto 2023でお話しした内容をベースに、もう少し開発者よりな話をしたいと思いますので、皆様よろしくお願いします。

それでは、今回はこの辺で。