API GatewayをCloudFormationで構築したら、マネコンとの項目の関連がわからなすぎたので、二度と調べなくていいようにまとめました

API GatewayをCloudFormationで構築しようとしたら、マネジメントコンソールの構造とCloudFormationの項目の関連がわからなすぎて無駄に時間がかかったので、二度と調べなくていいようにまとめました。CloudFormationのサンプルテンプレートつき。
2024.05.02

CloudFormation、使ってますか?

同じ環境を何度も構築する時、マネジメントコンソールで手動構築するのはだるいので、よくCloudFormationを使って構築しています。

今回、API GatewayをCloudFormationで構築しようとしたのですが、マネジメントコンソールでメソッドリクエストや統合レスポンスまわりの入力とCloudFormationの項目の関連がわからなすぎて無駄に時間がかかったので、二度と調べなくていいようにまとめました。

非プロキシ統合のLambdaをリクエスト先にしたAPI GatewayのCloudFormationのテンプレートを、ブログの最後に載せました。 テンプレートだけ欲しい人は、最後だけ見てください。

想定する読者

CloudFormationの基本的な使い方や、記法については理解している前提です。

また、API Gatewayの全体的な構成や、リソースパスの設定、メソッドリクエスト(Method request)、統合リクエスト(Integration request)、統合レスポンス(Integration response)、メソッドレスポンス(Method response)にどういった値を入力するかは理解している前提で進めます。

このブログでは、API Gatewayのマネジメントコンソールで入力しているあの値、CloudFormationのどの項目に対応するんだっけなー。ってとこを知りたい方向けにまとめています。

API Gatewayの主なリソースタイプ

大きなくくりで言うと、API Gatewayのマネジメントコンソールのこの部分は、CloudFormationのこのリソースタイプに対応しています。

リソースタイプ 説明
AWS::ApiGateway::Resource API Gatewayのリソースパスにあたる部分を設定
AWS::ApiGateway::Method API GatewayのGETやPOSTのメソッドタイプにあわせて、それに対応するAWSサービスや前処理、後処理を設定

この、 AWS::ApiGateway::Resource の話と、 AWS::ApiGateway::Method のリソースタイプが結構ややこしいので、細かく見ていきます。

リソース(Resources)

リソースは、主に AWS::ApiGateway::Resource で設定します。

リソースの詳細(Resource details)

リソースのパスを作成する画面とCloudFormationを比較します。

  ApiResource:
    Type: AWS::ApiGateway::Resource
    Properties:
      RestApiId: !Ref Api
      ## ↓ リソースを作成 - リソースの詳細
      ParentId: !GetAtt Api.RootResourceId # リソースパス
      # ParentId: !Ref ApiResource
      PathPart: '{path-method-req}' #リソース名
      ## ↑ リソースを作成 - リソースの詳細

マネジメントコンソールの リソースパス に当たる部分が、 ParentId です。 マネジメントコンソールでは選択式ですが、CloudFormationではリソースIDを指定します。

ルートパス(/)の子リソースにしたい場合は、ApiAWS::ApiGateway::RestApi の論理IDとすると、 !GetAtt Api.RootResourceId で指定できます。

その他のリソースの子リソースにしたい場合は、 !Ref ApiResource みたいな形で指定できます。

リソース名 に当たる部分が、 PathPart です。

固有のパスではなく、パス変数を利用したい場合は、 {path-method-req} のように {}(中括弧) でパス変数名を囲みます。 ここに指定したパス変数を、メソッドリクエストのリクエストパラメータとして利用できます。

また、{path-method-req+} のように + をつけると、そのリソースより深いパス全てが対象になります。

API Gatewayでは、慣習的に {proxy+} というパス変数名を利用することが多いです。 詳しくはこちらのドキュメントを御覧ください。

CORS(クロスオリジンリソース共有) のチェックボックスは、ちょっと特殊です。 CORS対応のためのOPTIONSメソッドを追加作成するかどうかを指定します。

このチェックボックスをチェックしておくと、OPTIONSメソッドが追加で作成されます。

CORS対応のためにどういったOPTIONSメソッドが作成されるかは、こちらのブログがわかりやすいのでご参照ください。

このチェックを入れてるときに、実際に作成されるOPTIONSメソッドをCloudFormationで表現したものが、こちらです。

  ## ↓ リソースを作成 - CORS(クロスオリジンリソース共有)
  ApiMethodOptions:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref Api
      ResourceId: !Ref ApiResource
      HttpMethod: OPTIONS
      AuthorizationType: NONE
      AuthorizationScopes: []
      ApiKeyRequired: False
      Integration:
        Type: MOCK
        PassthroughBehavior: WHEN_NO_MATCH
        RequestTemplates:
          application/json: '{"statusCode": 200}'
        IntegrationResponses:
          - StatusCode: '200'
            ResponseParameters:
              method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
              method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'"
              method.response.header.Access-Control-Allow-Origin: "'*'"
      MethodResponses:
        - StatusCode: '200'
          ResponseParameters:
            method.response.header.Access-Control-Allow-Headers: true
            method.response.header.Access-Control-Allow-Methods: true
            method.response.header.Access-Control-Allow-Origin: true
          ResponseModels:
            application/json: Empty
  ## ↑ リソースを作成 - CORS(クロスオリジンリソース共有)

メソッドリクエスト(Method request)

メソッドリクエストは、主に AWS::ApiGateway::Method で設定します。

メソッドリクエストの設定(Method request settings)

メソッドリクエストを編集する画面とCloudFormationを比較します。

API Gatewayのメソッドリクエストの 認可 にあたる部分が、 AuthorizationType です。

以下の設定値を必要に応じて指定します。

認可タイプ 設定値 備考
認可なし NONE
IAM認可 AWS_IAM
カスタム認可 CUSTOM 追加で AuthorizerIdAWS::ApiGateway::Authorizer で構築したID(e.g. !Ref Authorizer)を指定
Cognito認可 COGNITO_USER_POOLS 追加で AuthorizerIdAWS::ApiGateway::Authorizer で構築したリソースのID(e.g. !Ref Authorizer)を指定。
AuthorizationScopes に必要に応じてscopeのリストを指定。

リクエストバリデーター は、マネジメントコンソールとCloudFormationの設定が異なり、ちょっとややこしいです。

基本、マネジメントコンソールでは次の図のようにリクエストバリデーターは選択式になっています。

実は、選択して保存した際に、リクエストバリデーターが自動生成されています。

そのため、CloudFormationでリクエストバリデーターを設定する場合は、リクエストバリデーターのリソースを明示的に作成する必要があります。

CloudFormationでは、次のように AWS::ApiGateway::RequestValidator でリクエストバリデータを作成して、 RequestValidatorId にそのリソースのID(e.g. !Ref ParamsValidator)を指定します。

  ## ↓ メソッドリクエスト - リクエストバリデーター
  ParamsValidator: # クエリ文字列パラメータおよびヘッダーを検証
    Type: AWS::ApiGateway::RequestValidator
    Properties:
      Name: params-only
      RestApiId: !Ref Api
      ValidateRequestBody: false
      ValidateRequestParameters: true
  BodyAndParamsValidator: # 本文、クエリ文字列パラメータ、およびヘッダーを検証
    Type: AWS::ApiGateway::RequestValidator
    Properties:
      Name: body-and-params
      RestApiId: !Ref Api
      ValidateRequestBody: true
      ValidateRequestParameters: true
  BodyValidator: # 本文を検証
    Type: AWS::ApiGateway::RequestValidator
    Properties:
      Name: body-only
      RestApiId: !Ref Api
      ValidateRequestBody: true
      ValidateRequestParameters: false
  ## ↑ メソッドリクエスト - リクエストバリデーター

APIキーは必須です のチェックボックスは、 ApiKeyRequired で、 true / false で指定します。

オペレーション名 は、 OperationName で、任意の文字列で指定します。

リクエストパス、URLクエリ文字列パラメータ、HTTPリクエストヘッダー(Request paths, URL query string parameters, HTTP request headers)

リクエストパスURLクエリ文字列パラメータHTTPリクエストヘッダー はちょっとややこしい構造になっています。

これらの必須かどうかは、 RequestParameters で文字列オブジェクトとBool値で指定して、 キャッシュするかどうかは、 IntegrationCacheKeyParameters で文字列リストで指定します。

具体的にCloudFormation化すると、こんな感じです。

  ApiMethodAny:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref Api
      ResourceId: !Ref ApiResource
      HttpMethod: ANY
      ...
      RequestParameters:
        ## ↓ メソッドリクエスト - リクエストパス(パス変数と同じ変数)
        method.request.path.path-method-req: true
        ## ↑ メソッドリクエスト - リクエストパス(パス変数と同じ変数)
        ## ↓ メソッドリクエスト - URLクエリ文字列パラメータ
        method.request.querystring.query-method-req: true
        ## ↑ メソッドリクエスト - URLクエリ文字列パラメータ
        ## ↓ メソッドリクエスト - HTTP リクエストヘッダー
        method.request.header.header-method-req: true
        ## ↑ メソッドリクエスト - HTTP リクエストヘッダー
      ...
      Integration:
        ...
        CacheKeyParameters:
          ## ↓ メソッドリクエスト - リクエストパス(キャッシュ)
          - method.request.path.path-method-req
          ## ↑ メソッドリクエスト - リクエストパス(キャッシュ)
          ## ↓ メソッドリクエスト - URLクエリ文字列パラメータ(キャッシュ)
          - method.request.querystring.query-method-req
          ## ↑ メソッドリクエスト - URLクエリ文字列パラメータ(キャッシュ)
          ## ↓ メソッドリクエスト - HTTP リクエストヘッダー(キャッシュ)
          - method.request.header.header-method-req
          ## ↑ メソッドリクエスト - HTTP リクエストヘッダー(キャッシュ)
        ...

もう少し詳しく解説します。 リクエストパラメーターの設定について、CloudFormationのドキュメントを見るとこんな風に書いてあります。

RequestParameters

A key-value map defining required or optional method request parameters that can be accepted by API Gateway. A key is a method request parameter name matching the pattern of method.request.{location}.{name}, where location is querystring, path, or header and name is a valid and unique parameter name. The value associated with the key is a Boolean flag indicating whether the parameter is required (true) or optional (false). The method request parameter names defined here are available in Integration to be mapped to integration request parameters or templates.

つまり、 RequestParameters でまとめて文字列オブジェクトとBool値で以下のように設定ができます。

設定項目 設定値 備考
リクエストパス method.request.path.{名前} リソース作成時に設定したパス変数名と同じ名前で true を設定。マネジメントコンソールの場合、パス変数があれば自動生成される。
URLクエリ文字列パラメータ method.request.querystring.{名前} 必須かどうかで true / false を設定
HTTP リクエストヘッダー method.request.header.{名前} 必須かどうかで true / false を設定

一方、キャッシュを利用するかどうかは別の項目で指定します。 後々他のところでも出てきますが、キャッシュ関連は IntegrationCacheKeyParameters で全部まとめて指定することになります。

こちらもCloudFormationのドキュメントを見るとこんな風に書いてあります。

CacheKeyParameters

A list of request parameters whose values API Gateway caches. To be valid values for cacheKeyParameters, these parameters must also be specified for Method requestParameters

正直、このドキュメントだけだとよくわからなかったんですよね…。 なので、実際にマネジメントコンソールから設定して、AWS CLIで現状の設定値を取得したりして調べました。

つまり、キャッシュしたい項目を下のような設定値で、 CacheKeyParametres で文字列リストにして指定すればキャッシュが有効になります。

キャッシュしたい項目 設定値
リクエストパス method.request.path.{名前}
URLクエリ文字列パラメータ method.request.querystring.{名前}
HTTP リクエストヘッダー method.request.header.{名前}

リクエスト本文(Request body)

リクエスト本文 に当たる部分は、 RequestModels で指定します。

モデル自体はマネジメントコンソールと同様、別途作成が必要です。

CloudFormationでは、次のように AWS::ApiGateway::Model でモデルを作成して、 RequestModels にコンテンツタイプとあわせて、そのリソースのID(e.g. !Ref RequestBodyModel)を指定します。

  RequestBodyModel:
    Type: AWS::ApiGateway::Model
    Properties:
      Name: RequestBodyModel
      RestApiId: !Ref Api
      ContentType: application/json
      Description: Request body model sample
      Schema:
        $schema: http://json-schema.org/draft-04/schema#
        title: RequestBodyModel
        type: object
        required:
          - body-method-req
        properties:
          body:
            type: string

統合リクエスト(Integration request)

統合リクエストは、統合タイプによって設定が異なります。

今回は、 Lambda非プロキシ統合 を例に説明します。

統合リクエストの設定は、主に AWS::ApiGateway::MethodIntegration で設定します。

Method details

統合リクエストを編集する画面とCloudFormationを比較します。

統合タイプ 周りの設定は、マネジメントコンソールとCloudFormationで結構違います。

マネジメントコンソールの方はラジオボタンで統合タイプを選択すると、自動で設定される部分が結構あります。 CloudFormationはその自動で設定されている部分を、明示的に設定する必要があります。

まず、 統合タイプに当たる部分は、 Type なのですが、マネジメントコンソールとCloudFormationで選べる値が異なります。対応は次の通りです。

Type マネジメントコンソール
AWS Lambda関数(非プロキシ統合)、AWSのサービス
AWS_PROXY Lambda関数(プロキシ統合)
HTTP HTTP(非プロキシ統合)、VPCリンク
HTTP_PROXY HTTP(プロキシ統合)
MOCK Mock

今回は、非プロキシ統合のLambda関数に限って、設定値を説明します。

Lambda関数 にあたる部分は、 Uri で設定します。

マネジメントコンソールではリストから選択できますが、CloudFormationでは次のドキュメントに沿って、設定値を作る必要があります。

Uri

Specifies Uniform Resource Identifier (URI) of the integration endpoint.

For HTTP or HTTP_PROXY integrations, the URI must be a fully formed, encoded HTTP(S) URL according to the RFC-3986 specification for standard integrations. If connectionType is VPC_LINK specify the Network Load Balancer DNS name. For AWS or AWS_PROXY integrations, the URI is of the form arn:aws:apigateway:{region}:{subdomain.service|service}:path|action/{service_api}. Here, {Region} is the API Gateway region (e.g., us-east-1); {service} is the name of the integrated AWS service (e.g., s3); and {subdomain} is a designated subdomain supported by certain AWS service for fast host-name lookup. action can be used for an AWS service action-based API, using an Action={name}&{p1}={v1}&p2={v2}... query string. The ensuing {service_api} refers to a supported action {name} plus any required input parameters. Alternatively, path can be used for an AWS service path-based API. The ensuing service_api refers to the path to an AWS service resource, including the region of the integrated AWS service, if applicable. For example, for integration with the S3 API of GetObject, the uri can be either arn:aws:apigateway:us-west-2:s3:action/GetObject&Bucket={bucket}&Key={key} or arn:aws:apigateway:us-west-2:s3:path/{bucket}/{key}

ざっくり言うと、API Gatewayから実行させたい AWS API を指定しろということです。

API GatewayからLambdaを実行したいので、LambdaをInvokeするAPIを参照します。

リクエストの構文はこうですね。

POST /2015-03-31/functions/FunctionName/invocations?Qualifier=Qualifier HTTP/1.1 X-Amz-Invocation-Type: InvocationType X-Amz-Log-Type: LogType X-Amz-Client-Context: ClientContext

Payload

ここでいう FunctionName は、実行したいLambdaのARNを指定します。

そして、ドキュメントに記載の通り値を組み合わせてUriを作っていくと、こんな形になります。

        Uri: !Sub
          - arn:aws:apigateway:${Region}:${Service}:${ActionType}/${ServiceApi}
          - Region: !Sub ${AWS::Region} # AWSリージョン
            Service: lambda # AWS のサービス
            ActionType: path # アクションタイプ
            ServiceApi: !Sub 2015-03-31/functions/arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${Lambda}/invocations # アクション名/パスオーバーライド

なお、LambdaのInvoke APIは POST で実行するので、 IntegrationHttpMethodPOST を指定しておきます。

基本、Lambdaはバイナリデータを受け取らないので、 ContentHandlingCONVERT_TO_TEXT を指定しておきます。実際、マネジメントコンソールでLambdaを選択したときは自動で設定されています。

これで、マネジメントコンソールで統合タイプにLambda関数を選択したときに自動で設定される部分を、CloudFormationで設定できます。

実行ロール に当たる部分は、 Credentials にIAM RoleのARN文字列(e.g. !GetAtt ApiGatewayRole.Arn)で指定します。

認証情報キャッシュ に当たる部分は、 CacheKeyParameters で指定します。 先ほどメソッドリクエストでも出てきたやつですね。

このリストに、 caller.aws.account を追加すると、 マネジメントコンソールで 発信者のアカウンドIDをキャッシュに追加する を選択したときと同じ挙動になります。 caller.aws.principal を追加すると、 発信者のプリンシパルをキャッシュに追加する を選択したときと同じ挙動になります。 どちらも追加しない場合は、 発信者の認証情報をキャッシュキーに追加しない を選択したときと同じ挙動になります。

CloudFormationで具体的に設定すると、こんな感じです。

        CacheKeyParameters:
          ## ↓ 統合リクエスト - Method details(認証情報キャッシュ - 発信者のアカウンドIDをキャッシュに追加する)
          - caller.aws.account
          ## ↑ 統合リクエスト - Method details(認証情報キャッシュ - 発信者のアカウンドIDをキャッシュに追加する)
          ## ↓ 統合リクエスト - Method details(認証情報キャッシュ - 発信者のプリンシパルをキャッシュに追加する)
#          - caller.aws.principal
          ## ↑ 統合リクエスト - Method details(認証情報キャッシュ - 発信者のプリンシパルをキャッシュに追加する)

カスタムタイムアウト は、 TimeoutInMillis でミリ秒単位で指定します。 設定していない場合は、デフォルト値(29秒)が適用されます。

リクエスト本文のパススルー は、 PassthroughBehavior で指定します。 設定値とマネジメントコンソールの表示の対応は次の通りです。

PassthroughBehavior マネジメントコンソールの表示
WHEN_NO_TEMPLATES テンプレートが定義されていない場合 (推奨)
WHEN_NO_MATCH リクエストの content-type ヘッダーに一致するテンプレートがない場合
NEVER 不可

URLパスパラメータ、URLクエリ文字列パラメータ、HTTPリクエストヘッダーのパラメータ(URL path parameters, URL query string parameters, URL request headers parameters)

これらのパラメータもメソッドリクエストと同様に、マッピング元の指定と、キャッシュの有無の指定を設定する項目が分かれています。

IntegrationRequestParameters で指定し、 CacheKeyParameters でキャッシュの有無を指定します。

具体的にCloudFormation化すると、こんな感じです。

  ApiMethodAny:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref Api
      ResourceId: !Ref ApiResource
      HttpMethod: ANY
      ...
      Integration:
        RequestParameters:
          ## ↓ 統合リクエスト - URL パスパラメータ
          integration.request.path.path-integ-req: method.request.path.path-method-req
          ## ↑ 統合リクエスト - URL パスパラメータ
          ## ↓ 統合リクエスト - URL クエリ文字列パラメータ
          integration.request.querystring.query-integ-req: method.request.querystring.query-method-req
          ## ↑ 統合リクエスト - URL クエリ文字列パラメータ
          ## ↓ 統合リクエスト - HTTP ヘッダー
          integration.request.header.header-integ-req: method.request.header.header-method-req
          ## ↑ 統合リクエスト - HTTP ヘッダー
        CacheKeyParameters:
          ## ↓ 統合リクエスト - URL パスパラメータ(キャッシュ)
          - integration.request.path.path-integ-req
          ## ↑ 統合リクエスト - URL パスパラメータ(キャッシュ)
          ## ↓ 統合リクエスト - URL クエリ文字列パラメータ(キャッシュ)
          - integration.request.querystring.query-integ-req
          ## ↑ 統合リクエスト - URL クエリ文字列パラメータ(キャッシュ)
          ## ↓ 統合リクエスト - HTTP ヘッダー(キャッシュ)
          - integration.request.header.header-integ-req
          ## ↑ 統合リクエスト - HTTP ヘッダー(キャッシュ)
        ...

リクエストパラメーターの設定について、CloudFormationのドキュメントを見るとこんな風に書いてあります。

RequestParameters

A key-value map specifying request parameters that are passed from the method request to the back end. The key is an integration request parameter name and the associated value is a method request parameter value or static value that must be enclosed within single quotes and pre-encoded as required by the back end. The method request parameter value must match the pattern of method.request.{location}.{name}, where location is querystring, path, or header and name must be a valid and unique method request parameter name.

正直、このドキュメントだけだとよくわからなかったんですよね…。 なので、実際にマネジメントコンソールから設定して、AWS CLIで現状の設定値を取得したりして調べました。

次のように、integration.request.{設定項目}.{名前} で各項目の設定ができるようです。 マッピング元は、メソッドリクエストのパラメータ名(method.request.{path, querystring or header}.{名前})を指定するか、一重引用符で静的な値(e.g. 'fixed-parameter')を指定できます。

設定項目 設定値
URLパスパラメータ integration.request.path.{名前}
URLクエリ文字列パラメータ integration.request.querystring.{名前}
URLリクエストヘッダーのパラメータ integration.request.header.{名前}

今回の例ではメソッドリクエストで設定した各変数名を、次のように統合リクエストで変数名を変換するようにしています。

設定項目 メソッドリクエストでの変数名 統合リクエストでの変数名
URLパスパラメータ path-method-req path-integ-req
URLクエリ文字列パラメータ query-method-req query-integ-req
URLリクエストヘッダーのパラメータ header-method-req header-integ-req

マッピングテンプレート

マッピングテンプレート は、 RequestTemplates でコンテンツタイプと合わせて本文を指定します。

マッピングテンプレートの話は、1つのブログにできそうなくらいボリュームがあるので、本ブログではマッピングテンプレートの詳細には立ち入りません。マッピングテンプレートの内容については、理解している前提で進めます。

マッピングテンプレートの内容については、次のブログ等を参考にしてください。

今回の例では、次のドキュメントを参考に、全てのリクエストパラメーターをJSON形式で本文に埋め込むようなマッピングテンプレートを設定します。

        RequestTemplates:
          application/json: |-
            #set($allParams = $input.params())
            {
              "params" : {
                #foreach($type in $allParams.keySet())
                #set($params = $allParams.get($type))
                "$type" : {
                  #foreach($paramName in $params.keySet())
                  "$paramName" : "$util.escapeJavaScript($params.get($paramName))"
                    #if($foreach.hasNext),#end
                    #end
                }
                  #if($foreach.hasNext),#end
                  #end
              },
            "body": $input.body
            }

ここまでの設定がうまくいっていると、このAPI Gatewayをデプロイして、次のようなcurlコマンドを実行してパラメーターを渡した場合、マッピングテンプレートで変換されて、Lambda(AWS APIのLambda Invoke)のpayloadには、その次のようなJSON形式のリクエストが渡されるようになります。

$ export APIGATEWAY_URL=<<デプロイしたAPI GatewayのURL>>
$ curl -X POST \
  -d '{"body-method-req": "mybody"}' \
  -H 'header-method-req: myheader' \
  -H 'Content-Type: application/json' \
  "${APIGATEWAY_URL}/mypath?query-method-req=myquery"

Response body

{
  "params": {
    "path": {
      "path-method-req": "mypath"
    },
    "querystring": {
      "query-method-req": "myquery"
    },
    "header": {
      "header-method-req": "myheader"
    }
  },
  "body": {
    "body-method-req": "mybody"
  }
}

ちなみに、実際に渡されているJSONを見てみると、メソッドリクエストで設定したパラメーターが、マッピングテンプレートで変換されていることがわかります。

じゃあ、統合リクエストで変換したパラメーターはどこに行ったのかというと、Lambda(AWS APIのLambda Invoke)へクエリ文字列として渡されています。 今回で言えば、 query-integ-req=myquery がクエリ文字列としてLambdaに渡されていますが、受け取らずに無視されます。

この辺の話は、こちらのブログが詳しいので、参考にしてください。

メソッドレスポンス(Method response)

メソッドレスポンスの設定は、主に AWS::ApiGateway::MethodMethodResponses で設定します。

レスポンスの詳細(Responses details)

メソッドレスポンスを作成する画面とCloudFormationを比較します。

HTTPステータスコードStatusCode で文字列で指定します。

ヘッダー名 は、 ResponseParameters で method.response.header.{ヘッダー名} の形式で、必須かどうかで true / false とあわせて列挙します。

マネジメントコンソールでは必須かどうかのチェック項目がメソッドレスポンスに存在しませんが、後述の統合レスポンスのヘッダーのマッピングが設定されているかどうかで、内部的に必須かどうかの設定がされているようです。

レスポンス本文 に当たる部分は、 ResponseModels で指定します。

モデル自体は、マネジメントコンソールと同様に、別途作成が必要です。

CloudFormationでは、次のように AWS::ApiGateway::Model でモデルを作成して、 ResponseModels にコンテンツタイプ(e.g. application/json)とあわせて、そのリソースのID(e.g. !Ref ResponseBodyModel)を指定します。

  ResponseBodyModel:
    Type: AWS::ApiGateway::Model
    Properties:
      Name: ResponseBodyModel
      RestApiId: !Ref Api
      ContentType: application/json
      Description: Response body model sample
      Schema:
        $schema: http://json-schema.org/draft-04/schema#
        title: ResponseBodyModel
        type: object
        required:
          - body
        properties:
          body:
            type: object
            required:
              - body-method-res
            properties:
              body-method-res:
                type: string

統合レスポンス(Integration response)

統合レスポンスの設定は、主に AWS::ApiGateway::MethodIntegrationIntegrationResponses で設定します。

AWS::ApiGateway::Method Integration IntegrationResponses - AWS CloudFormation

レスポンスの詳細(Responses details)

統合レスポンスを編集する画面とCloudFormationを比較します。

HTTPステータスの正規表現 は、 SelectionPattern で指定します。 設定しなかった場合は、デフォルトレスポンスとなります。

正規表現の設定方法については、次のドキュメントを参考にしてください。

メソッドレスポンスのステータスコード は、 StatusCode で文字列で指定します。

このステータスコードは、メソッドレスポンスで作成したステータスコードと一致している必要があります。

コンテンツの処理 は、 ContentHandling で指定します。

CloudFormationで設定する値と、マネジメントコンソールの表示の対応は次の通りです。

ContentHandling マネジメントコンソールの表示
CONVERT_TO_TEXT テキストに変換
CONVERT_TO_BINARY バイナリに変換
設定値無し パススルー

ヘッダーのマッピング(Header mapping)

ヘッダーのマッピング は、 ResponseParameters で method.response.header.{ヘッダー名} の形式で、マッピングしたい値とあわせて列挙します。

マッピングしたい値は、Lambda(AWS APIのLambda Invoke)からリターンされたHTTPリクエストのヘッダー(e.g. integration.response.header.{名前})や、リクエストボディからJSON式(e.g. integration.response.body.{JSON-expression})で設定するか、一重引用符で静的な値(e.g. 'fixed-parameter')を指定できます。

今回の例では、LambdaからリターンされたHTTPリクエストのボディから、params.header.header-method-req (リクエスト時にheader-method-req ヘッダーに設定していて、統合リクエストのマッピングテンプレートでボディにマッピングされた値)を取得して、ヘッダー名 header-method-res に再マッピングしています。

            ResponseParameters:
              method.response.header.header-method-res: integration.response.body.params.header.header-method-req

マッピングテンプレート(Mapping template)

マッピングテンプレート は、 ResponseTemplates でコンテンツタイプと合わせて本文を指定します。

今回の例では、次のドキュメントを参考に、リクエスト本文に含まれていた body-method-req パラメータ名を、 body-method-res に変換するマッピングテンプレートにしています。

            ResponseTemplates:
              application/json: |-
                {
                  "params": $input.json('$.params'),
                  "body": {"body-method-res": $input.json('$.body.body-method-req')}
                }

ここまでの設定がうまくいっていると、このAPI Gatewayをデプロイして、次のようなcurlコマンドを実行してパラメーターを渡した場合、マッピングテンプレートで変換されて、Lambdaに渡り、Lambdaは受け取ったpayloadをそのままリターンするようにしておきます。 そして、レスポンスデータもマッピングテンプレートで変換されて、その次のようなJSON形式の結果が返ってきます。

$ export APIGATEWAY_URL=<<デプロイしたAPI GatewayのURL>>
$ curl -i -X POST \
  -d '{"body-method-req": "mybody"}' \
  -H 'header-method-req: myheader' \
  -H 'Content-Type: application/json' \
  "${APIGATEWAY_URL}/mypath?query-method-req=myquery"

Response header

HTTP/2 200
date: Wed, 01 May 2024 08:27:24 GMT
content-type: application/json
content-length: 463
x-amzn-requestid: 4948b355-a8b4-4719-ad07-46b162187abe
x-amz-apigw-id: XFR04GwqNjMEf3Q=
header-method-res: myheader
x-amzn-trace-id: Root=1-6631fceb-45a39f755e635be24dc7b2ee;Parent=19b0bf74599fa6ba;Sampled=0;lineage=c0b71b4a:0

Response body

{
  "params": {
    "path": {
      "path-method-req": "mypath"
    },
    "querystring": {
      "query-method-req": "myquery"
    },
    "header": {
      "accept": "*/*",
      "content-type": "application/json",
      "header-method-req": "myheader",
      "Host": "xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com",
      "User-Agent": "curl/8.4.0",
      "X-Amzn-Trace-Id": "Root=1-6631cf77-323a12f90d7abdd762c86dc3",
      "X-Forwarded-For": "xx.xx.xx.xx",
      "X-Forwarded-Port": "443",
      "X-Forwarded-Proto": "https"
    }
  },
  "body": {
    "body-method-res": "mybody"
  }
}

返却されたヘッダー header-method-res には、リクエストの header-method-req ヘッダーに設定した値が入っており、ちゃんとマッピングされて返ってきてるなーということがわかります。

おわりに

API Gatewayのメソッド周りについてCloudFormationで設定する方法をまとめました。

マネジメントコンソールと構造が結構違って、いろいろなドキュメントを往復したのでもう二度と調べたくないです。そういう気持ちで書きました。 次やる時は、このブログを見返してやります。

このブログが、他の方の参考になれば幸いです。

おまけ(CloudFormationコード全体)

AWSTemplateFormatVersion: "2010-09-09"

Parameters:
  StageName:
    Description: Name of API stage.
    Type: String
    Default: prod

Resources:
  LambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Path: /
  ApiGatewayRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - apigateway.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AWSLambda_FullAccess
      Path: /

  Lambda:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          exports.handler = async function(event) {
            return event
          }
      FunctionName: api-lambda
      MemorySize: 128
      Runtime: nodejs20.x
      Handler: index.handler
      Role: !GetAtt LambdaRole.Arn

  Api:
    Type: AWS::ApiGateway::RestApi
    Properties:
      Name: SampleApi
      EndpointConfiguration:
        Types:
          - REGIONAL

  RequestBodyModel:
    Type: AWS::ApiGateway::Model
    Properties:
      Name: RequestBodyModel
      RestApiId: !Ref Api
      ContentType: application/json
      Description: Request body model sample
      Schema:
        $schema: http://json-schema.org/draft-04/schema#
        title: RequestBodyModel
        type: object
        required:
          - body-method-req
        properties:
          body-method-req:
            type: string

  ResponseBodyModel:
    Type: AWS::ApiGateway::Model
    Properties:
      Name: ResponseBodyModel
      RestApiId: !Ref Api
      ContentType: application/json
      Description: Response body model sample
      Schema:
        $schema: http://json-schema.org/draft-04/schema#
        title: ResponseBodyModel
        type: object
        required:
          - body
        properties:
          body:
            type: object
            required:
              - body-method-res
            properties:
              body-method-res:
                type: string

  ## ↓ メソッドリクエスト - リクエストバリデーター
  ParamsValidator: # クエリ文字列パラメータおよびヘッダーを検証
    Type: AWS::ApiGateway::RequestValidator
    Properties:
      Name: params-only
      RestApiId: !Ref Api
      ValidateRequestBody: false
      ValidateRequestParameters: true
  BodyAndParamsValidator: # 本文、クエリ文字列パラメータ、およびヘッダーを検証
    Type: AWS::ApiGateway::RequestValidator
    Properties:
      Name: body-and-params
      RestApiId: !Ref Api
      ValidateRequestBody: true
      ValidateRequestParameters: true
  BodyValidator: # 本文を検証
    Type: AWS::ApiGateway::RequestValidator
    Properties:
      Name: body-only
      RestApiId: !Ref Api
      ValidateRequestBody: true
      ValidateRequestParameters: false
  ## ↑ メソッドリクエスト - リクエストバリデーター

  ApiResource:
    Type: AWS::ApiGateway::Resource
    Properties:
      RestApiId: !Ref Api
      ## ↓ リソースを作成 - リソースの詳細
      ParentId: !GetAtt Api.RootResourceId # リソースパス
      # ParentId: !Ref ApiResource
      PathPart: '{path-method-req}' #リソース名
      ## ↑ リソースを作成 - リソースの詳細

  ApiMethodAny:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref Api
      ResourceId: !Ref ApiResource
      HttpMethod: ANY
      ## ↓ メソッドリクエスト - メソッドリクエストの設定
      AuthorizationType: NONE # 認可
      AuthorizerId: !Ref AWS::NoValue
      AuthorizationScopes: [] # Authorization Scopes
      RequestValidatorId: !Ref BodyAndParamsValidator # リクエストバリデーター
      ApiKeyRequired: false # API キーは必須です
      OperationName: AnyMethodOperation # オペレーション名 - オプション
      ## ↑ メソッドリクエスト - メソッドリクエストの設定
      RequestParameters:
        ## ↓ メソッドリクエスト - リクエストパス(パス変数と同じ変数)
        method.request.path.path-method-req: true
        ## ↑ メソッドリクエスト - リクエストパス(パス変数と同じ変数)
        ## ↓ メソッドリクエスト - URLクエリ文字列パラメータ
        method.request.querystring.query-method-req: true
        ## ↑ メソッドリクエスト - URLクエリ文字列パラメータ
        ## ↓ メソッドリクエスト - HTTP リクエストヘッダー
        method.request.header.header-method-req: true
        ## ↑ メソッドリクエスト - HTTP リクエストヘッダー
      ## ↓ メソッドリクエスト - リクエスト本文
      RequestModels:
        application/json: !Ref RequestBodyModel
      ## ↑ メソッドリクエスト - リクエスト本文
      Integration:
        ## ↓ 統合リクエスト - Method details
        Type: AWS # 統合タイプ
        Uri: !Sub
          - arn:aws:apigateway:${Region}:${Service}:${ActionType}/${ServiceApi}
          - Region: !Sub ${AWS::Region} # AWSリージョン
            Service: lambda # AWS のサービス
            ActionType: path # アクションタイプ
            ServiceApi: !Sub 2015-03-31/functions/arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${Lambda}/invocations # アクション名/パスオーバーライド
        IntegrationHttpMethod: POST # HTTPメソッド
        ContentHandling: CONVERT_TO_TEXT # コンテンツの処理
        Credentials: !GetAtt ApiGatewayRole.Arn # 実行ロール
        TimeoutInMillis: 28000 # カスタムタイムアウト
        PassthroughBehavior: WHEN_NO_TEMPLATES # リクエスト本文のパススルー
        ## ↑ 統合リクエスト - Method details
        RequestParameters:
          ## ↓ 統合リクエスト - URL パスパラメータ
          integration.request.path.path-integ-req: method.request.path.path-method-req
          ## ↑ 統合リクエスト - URL パスパラメータ
          ## ↓ 統合リクエスト - URL クエリ文字列パラメータ
          integration.request.querystring.query-integ-req: method.request.querystring.query-method-req
          ## ↑ 統合リクエスト - URL クエリ文字列パラメータ
          ## ↓ 統合リクエスト - HTTP ヘッダー
          integration.request.header.header-integ-req: method.request.header.header-method-req
          ## ↑ 統合リクエスト - HTTP ヘッダー
        CacheKeyParameters:
          ## ↓ メソッドリクエスト - リクエストパス(キャッシュ)
          - method.request.path.path-method-req
          ## ↑ メソッドリクエスト - リクエストパス(キャッシュ)
          ## ↓ メソッドリクエスト - URLクエリ文字列パラメータ(キャッシュ)
          - method.request.querystring.query-method-req
          ## ↑ メソッドリクエスト - URLクエリ文字列パラメータ(キャッシュ)
          ## ↓ メソッドリクエスト - HTTP リクエストヘッダー(キャッシュ)
          - method.request.header.header-method-req
          ## ↑ メソッドリクエスト - HTTP リクエストヘッダー(キャッシュ)
          ## ↓ 統合リクエスト - Method details(認証情報キャッシュ - 発信者のアカウンドIDをキャッシュに追加する)
          - caller.aws.account
          ## ↑ 統合リクエスト - Method details(認証情報キャッシュ - 発信者のアカウンドIDをキャッシュに追加する)
          ## ↓ 統合リクエスト - Method details(認証情報キャッシュ - 発信者のプリンシパルをキャッシュに追加する)
          # - caller.aws.principal
          ## ↑ 統合リクエスト - Method details(認証情報キャッシュ - 発信者のプリンシパルをキャッシュに追加する)
          ## ↓ 統合リクエスト - URL パスパラメータ(キャッシュ)
          - integration.request.path.path-integ-req
          ## ↑ 統合リクエスト - URL パスパラメータ(キャッシュ)
          ## ↓ 統合リクエスト - URL クエリ文字列パラメータ(キャッシュ)
          - integration.request.querystring.query-integ-req
          ## ↑ 統合リクエスト - URL クエリ文字列パラメータ(キャッシュ)
          ## ↓ 統合リクエスト - HTTP ヘッダー(キャッシュ)
          - integration.request.header.header-integ-req
          ## ↑ 統合リクエスト - HTTP ヘッダー(キャッシュ)
        ## ↓ 統合リクエスト - マッピングテンプレート
        RequestTemplates:
          application/json: |-
            #set($allParams = $input.params())
            {
              "params" : {
                #foreach($type in $allParams.keySet())
                #set($params = $allParams.get($type))
                "$type" : {
                  #foreach($paramName in $params.keySet())
                  "$paramName" : "$util.escapeJavaScript($params.get($paramName))"
                    #if($foreach.hasNext),#end
                    #end
                }
                  #if($foreach.hasNext),#end
                  #end
              },
            "body": $input.body
            }
        ## ↑ 統合リクエスト - マッピングテンプレート
        IntegrationResponses:
            ## ↓ 統合レスポンス - レスポンスの詳細
          - SelectionPattern: .* # Lambda エラーの正規表現
            StatusCode: '200' # メソッドレスポンスのステータスコード
            ContentHandling: CONVERT_TO_TEXT # コンテンツの処理
            ## ↑ 統合レスポンス - レスポンスの詳細
            ## ↓ 統合レスポンス - ヘッダーのマッピング
            ResponseParameters:
              method.response.header.header-method-res: integration.response.body.params.header.header-method-req
            ## ↑ 統合レスポンス - ヘッダーのマッピング
            ## ↓ 統合レスポンス - マッピングテンプレート
            ResponseTemplates:
              application/json: |-
                {
                  "params": $input.json('$.params'),
                  "body": {"body-method-res": $input.json('$.body.body-method-req')}
                }
            ## ↑ 統合レスポンス - マッピングテンプレート
      MethodResponses:
        ## ↓ メソッドレスポンス - レスポンスの詳細
        - StatusCode: '200' # HTTP ステータスコード
          ResponseParameters: # ヘッダー名
            method.response.header.header-method-res: true
          ResponseModels: # レスポンス本文
            application/json: !Ref ResponseBodyModel
        ## ↑ メソッドレスポンス - レスポンスの詳細
  ## ↓ リソースを作成 - CORS(クロスオリジンリソース共有)
  ApiMethodOptions:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref Api
      ResourceId: !Ref ApiResource
      HttpMethod: OPTIONS
      AuthorizationType: NONE
      AuthorizationScopes: []
      ApiKeyRequired: False
      Integration:
        Type: MOCK
        PassthroughBehavior: WHEN_NO_MATCH
        RequestTemplates:
          application/json: '{"statusCode": 200}'
        IntegrationResponses:
          - StatusCode: '200'
            ResponseParameters:
              method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
              method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'"
              method.response.header.Access-Control-Allow-Origin: "'*'"
      MethodResponses:
        - StatusCode: '200'
          ResponseParameters:
            method.response.header.Access-Control-Allow-Headers: true
            method.response.header.Access-Control-Allow-Methods: true
            method.response.header.Access-Control-Allow-Origin: true
          ResponseModels:
            application/json: Empty
  ## ↑ リソースを作成 - CORS(クロスオリジンリソース共有)

  ApiDeployment:
    Type: AWS::ApiGateway::Deployment
    DependsOn:
      - ApiMethodAny
      - ApiMethodOptions
      - RequestBodyModel
      - ResponseBodyModel
    Properties:
      RestApiId: !Ref Api
      StageName: !Sub ${StageName}

Outputs:
  ApiRootUrl:
    Description: Root Url of the API
    Value: !Sub https://${Api}.execute-api.${AWS::Region}.amazonaws.com/${StageName}