【小ネタ】AWS CDKでAPI Gateway + Swaggerの環境を構築する

おはようございます。CX事業本部@札幌の佐藤です。

はじめに

AWS CDKではAPI Gatewayを作成する方法として、以下の3種類の方法があります。

  • @aws-cdk/aws-apigatewayRestAPI を使う(基本はこれ)
  • @aws-cdk/aws-apigatewayCfnXXXX を使う(冗長な記述になる)
  • @aws-cdk/aws-samCfnApi を使う(AWS SAMのAWS::Serverless::Apiと同じ)

概要

AWS CDKを使ってサーバーレスなRestAPIのインフラを構築することになりました。AWS SAMだと、AWS::Serverless::Apiを使えば、Swaggerに対応しているんですが、AWS CDKでは以下のIssueにある通り、現状API GatewayのSwaggerが対応されていません。(対応予定ではあります)

https://github.com/aws/aws-cdk/issues/723

RestApiでSwaggerが使えないとなると、CfnRestApiを使うしかないんですが、せっかくCDKを使っているのに低レベルなConstructを使ってしまうと、CDKの抽象化のメリットが薄れてしまうため、なるべくRestApiなどの高レベルなConstructを使いたいところです。どうにかして高レベルConstructを使いつつSwagger対応できないかを調査しました。

ConstructNodeを使って解決

CDKには Core API として、ConstructNode というものがあり、これは CDKによって作成される構成ツリー(CloudFormationの実体)と対話するためのAPIとなっています。これを使うことで、高レベルConstruct から CloudFormation のリソースを抽出して、動的にプロパティを変更して、Swaggerに対応させるということができました。以下は実際のコードです。

CDKのインフラコード

@aws-cdk/aws-apigatewayRestApiを使いつつ、Swaggerに対応させます。api.node.findChild('Resource')でCloudFormationのAWS::APIGateway::RestApiのリソースを取得し、bodyプロパティにSwaggerのJSONを代入する形にしています。

import * as cdk from '@aws-cdk/core';
import * as lambda from '@aws-cdk/aws-lambda';
import * as apig from '@aws-cdk/aws-apigateway';
import { Swagger } from './interfaces/swagger';
import { CfnRestApi } from '@aws-cdk/aws-apigateway';

export class ApiGatewaySwaggerStack extends cdk.Stack {

  private myRegion: string = cdk.Stack.of(this).region;

  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
		
    // Lambdaの作成
    this.testFunction = new lambda.Function(this, 'testFunction', {
      code: lambda.Code.fromAsset('src/lambda/handlers/api-gw'),
      runtime: lambda.Runtime.NODEJS_10_X,
      handler: 'test-function.handler',
      description: 'Api GatewayからトリガされるFunction',
      functionName: 'test-function',
      memorySize: 256,
    });
		
    // API Gatewayの作成(RestApiを使う)
    const api = new apig.RestApi(this, 'RestApi', {
      restApiName: "test",
    });
    
    // nodeの子要素からAWS::ApiGateway::RestApiを取得し、bodyプロパティにswaggerのJSONを追記
    const apiNode = api.node.findChild('Resource') as CfnRestApi;
    apiNode.body = this.createSwaggerJson();
    
    // 現状、公式でSwaggerが対応されていなく、「The REST API doesn't contain any methods」エラーになってしまうため、ダミーでメソッドを追加する
    api.root.addMethod('ANY', new apig.MockIntegration());
  }

  private createSwaggerJson(): Swagger {
    return {
      openapi: '3.0.0',
      info: {
        description: 'test',
        title: 'test',
        version: "0.1"
      },
      paths: {
        '/invoke':{
          post: {
            summary: 'LambdaをInvokeする',
            parameters: [
              {
                in: 'path',
                name: 'hoge',
                required: true,
                schema: {
                  type: 'string'
                }
              }
            ],
            requestBody: {
              content: {
                'application/json': {
                  schema: {
                    properties: {
                      foo: {
                        type: 'string',
                      },
                      bar: {
                        type: 'integer'
                      }
                    }
                  }
                }
              }
            },
            responses: {
              200: {
                description: 'OK',
                content: {
                  'application/json': {
                    schema: {
                      properties: {
                        id: {
                          type: 'integer',
                        },
                        name: {
                          type: 'string'
                        }
                      }
                    }
                  }
                }
              }
            },
            'x-amazon-apigateway-integration': {
              uri: `arn:aws:apigateway:${this.myRegion}:lambda:path/2015-03-31/functions/${this.testFunction.functionArn}/invocations`,
              responses: {
                default: {
                  statusCode: "200"
                }
              },
              passthroughBehavior: "when_no_match",
              httpMethod: "POST",
              contentHandling: "CONVERT_TO_TEXT",
              type: "aws_proxy"
            }
          }
        }
      }
    }
  }
}

cdk synthでCloudFormationテンプレートを出力すると、以下のようにBodyプロパティに、Swaggerの定義が出力されています。

RestApi0C43BF4B:
    Type: AWS::ApiGateway::RestApi
    Properties:
      Body:
        openapi: 3.0.0
        info:
          description: test
          title: test
          version: "0.1"
        paths:
          /invoke:
            post:
              summary: LambdaをInvokeする
              parameters:
                - in: path
                  name: hoge
                  required: true
                  schema:
                    type: string
              requestBody:
                content:
                  application/json:
                    schema:
                      properties:
                        foo:
                          type: string
                        bar:
                          type: integer
              responses:
                "200":
                  description: OK
                  content:
                    application/json:
                      schema:
                        properties:
                          id:
                            type: integer
                          name:
                            type: string
              x-amazon-apigateway-integration:
                uri:
                  Fn::Join:
                    - ""
                    - - "arn:aws:apigateway:"
                      - Ref: AWS::Region
                      - :lambda:path/2015-03-31/functions/
                      - Fn::GetAtt:
                          - TestFunctionC517E46D
                          - Arn
                      - /invocations
                responses:
                  default:
                    statusCode: "200"
                passthroughBehavior: when_no_match
                httpMethod: POST
                contentHandling: CONVERT_TO_TEXT
                type: aws_proxy
      Name: test

まとめ

このように、現状高レベルで対応されていないリソースに関しても、ConstructNodeを使うことで、直接CloudFormationテンプレートに対して値を追記、設定をオーバーライドすることができるため、とても便利な機能でした。