CDKのAPIGateway+Lambda構成をOpenAPIの定義から作成する


## どんな内容か

CDKにてAPIGateway+Lambdaの構成を作る際に、[RestApi](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.RestApi.html)ではなくOpenAPI定義からAPIGatewayを作ることができる[SpecRestApi](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.SpecRestApi.html)を使ってみたという内容になります。

## OpenAPIとは

OpenAPIとは、RESTful APIを記述するための標準化されたフォーマットのことで、OpenAPI Specification(OAS)やOpenAPI Generatorなど取り巻く技術の総称としても呼ばれています。

もともとswaggerとして有名だったのですが、OASとして標準化されてからはOpenAPIと呼ばれることが多く、swaggerはOpenAPIのツール的な位置付けになっています。

例えば、[https://jsonplaceholder.typicode.com/](https://jsonplaceholder.typicode.com/) というRESTfulAPIを公開しているサービスがあるのですが、その中の /posts , /posts/{id} をOpenAPIで定義すると以下のようになります。

```yaml:api/docs/openapi.yaml
openapi: 3.0.0
info:
  version: 1.0.0
  title: JSON Placeholder API
  description: See https://jsonplaceholder.typicode.com/
paths:
  /posts:
    get:
      description: Returns all posts
      tags: ["Posts"]
      operationId: "getPosts"
      responses:
        "200":
          description: Successful response
          content:
            "application/json":
              schema:
                $ref: "#/components/schemas/PostsList"

  /posts/{id}:
    get:
      description: Returns a post by id
      tags: ["Posts"]
      operationId: "getPost"
      parameters:
        - name: id
          in: path
          required: true
          description: The user id.
          schema:
            type: integer
            format: int64
      responses:
        "200":
          description: Successful response
          content:
            "application/json":
              schema:
                $ref: "#/components/schemas/Post"
        "404":
          description: Post not found

components:
  schemas:
    PostsList:
      "type": "array"
      "items":
        $ref: "#/components/schemas/Post"
    Post:
      "type": "object"
      "required":
        - "id"
        - "userId"
        - "title"
        - "body"
      "properties":
        id:
          type: "integer"
        userId:
          type: "integer"
        title:
          type: "string"
        body:
          type: "string"
```

今回はこちらのOpenAPI仕様のyamlファイルを使ってAPIGatewayを作っていきたいと思います。

## CDKを使ったAPIGateway+Lambda構成の一般的な作成方法

APIGateway+Lambdaを構成するやり方を色々調べてみると、[RestApi](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.RestApi.html) を利用するケースがよくみられており、前述のOpenAPI通りにCDKで記述すると以下のようになります。（CDKはTypeScript、v2.38.0です）

```ts
// Lambda関数の作成
const fn = new lambda.Function(this, 'handler', {
  // Lambda関数の設定
});

// RestApiを使ったAPIGatewayの作成
const apigw = new apigateway.RestApi(this, "RestApi", {
  restApiName: "restApi",
});

const integration = new apigateway.LambdaIntegration(fn);

// /posts
const postsResource = apigw.root.addResource("posts");
postsResource.addMethod("GET", integration);

// /posts/{id}
const postIdResource = postsResource.addResource("{id}");
postIdResource.addMethod("GET", integration);
```

RestApiの良い点としては、リソース毎に定義するため、AuthorizerやRequestValidatorなどの設定を柔軟に定義できることですが、反面、OpenAPI定義ファイルに合わせてCDKのコードを合わせて作成しないといけないため、リソースが増えると記述量が比例して増えるので辛かったり、仕様変更時にOpenAPI定義とCDKの二重メンテが発生してしまうことがあります。

CDKを正として事後でOpenAPI定義ファイルを生成したい場合は、APIGatewayからOpenAPIをエクスポートすることもできるのでそれでもよいと思います。マネジメントコンソールでは以下のメニューからエクスポートできます。

![cdkapi1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2277523/436481d6-c8ac-3424-cdb1-3a404d338332.png)


## SpecRestApiを使ったAPIGateway+Lambda作成方法

OpenAPIを利用したAPIGateway作成方法として、公式CDKにて[SpecRestApi](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.SpecRestApi.html)が提供されています。こちらのCDKコード例は以下のようになります。

```ts
const apigw = new apigateway.SpecRestApi(this, "RestApi", {
  apiDefinition: apigateway.ApiDefinition.fromAsset("./api/docs/openapi.yaml"),
  restApiName: "restApi",
})
```

ただ、このままだとLambdaと連携しないApigatewayができるだけなので、RestApiでいうintegration的な処理が必要になります。色々なやり方があるかもしれませんが、私はOpenAPI拡張の [x-amazon-apigateway-integrationLambda](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-swagger-extensions-integration.html)を使って以下のように書きました。権限設定も合わせて必要だったので追加しています。

```ts:lib/cdk-openapi-express-stack.ts
// SpecRestApiを使ったAPIGatewayの作成
const swaggerYaml = yaml.parse(
  fs.readFileSync("./api/docs/openapi.yaml").toString()
);

for (const path in swaggerYaml.paths) {
  for (const method in swaggerYaml.paths[path]) {
    swaggerYaml.paths[path][method]["x-amazon-apigateway-integration"] = {
      uri: `arn:${cdk.Aws.PARTITION}:apigateway:${cdk.Aws.REGION}:lambda:path/2015-03-31/functions/${fn.functionArn}/invocations`,
      passthroughBehavior: "when_no_match",
      httpMethod: "POST",
      type: "aws_proxy",
    };
  }
}

const apigw = new apigateway.SpecRestApi(this, "RestApi", {
  apiDefinition: apigateway.ApiDefinition.fromInline(swaggerYaml),
});

fn.addPermission("LambdaPermisson", {
  principal: new iam.ServicePrincipal("apigateway.amazonaws.com"),
  action: "lambda:InvokeFunction",
  sourceArn: apigw.arnForExecuteApi(),
});
```

この内容で `cdk deploy` するとこのようにApiGatewayのリソースが定義されて、統合タイプとしてはLambda関数に設定されます。

![cdkapi2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2277523/c46e62d3-3b34-6bc7-85b7-df7e1ca0d189.png)

## まとめ

CDKのSpecRestAPIを使うことでOpenAPI定義とAPIGatewayの乖離を防ぐことができました。定義ファイルを正とできることで、クライアントアプリ側の連携も安定しそうです。また、今回は記載していませんがLambda統合だけでなく、Authorizerなどの設定も追加することが可能です。欠点としては、私の方法ではリソース全体に設定しているため、リソース個別で設定や呼び出すLambdaが異なるような場合には大変になりそうな気がしました。