API GatewayのOpenAPIインポートとTerraformのテンプレートでいい感じにAPI管理する

API GatewayのOpenAPIインポートとTerraformのテンプレートでいい感じにAPI管理する
この記事をシェアする

本記事では、Terraform のテンプレート機能を使って、API Gateway の OpenAPI インポート機能で、動的な値(Lambda の ARN など)を注入する方法を紹介します。

1. この記事の要約

  • API Gateway の OpenAPI インポート機能を使って REST API を定義します
  • OpenAPI ファイルをテンプレート化し、Terraform で動的な値(Lambda の ARN など)を注入することで、宣言的かつ DRY に API を管理できます。

2. API Gateway の OpenAPI インポートとは?課題は?

API Gateway の OpenAPI のインポートとは、OpenAPI 仕様(Swagger 仕様)で記述した API 定義ファイルを、そのまま API Gateway の設定として取り込める機能です。この機能を使うことで、API のエンドポイントやリクエスト/レスポンスの定義、認証方式などを一括で管理できます。

  • OpenAPI ファイルをインポートすることで、API Gateway のリソースやメソッド、統合設定などを自動生成できます。
  • AWS 固有の拡張(x-amazon-apigateway-xxx)を使うことで、統合先や認証方式なども OpenAPI ファイル内で指定できます。
  • Terraform でも、OpenAPI インポートを使うことができます。

課題

API Gateway の OpenAPI インポート機能だけでは、Lambda の ARN など動的な値を直接注入できません。ただし、OpenAPI ファイル内に Lambda 名称などをハードコードすることは、保守性の観点から、なるべく避けたいです。

3. 解決策:OpenAPI インポート + Terraform テンプレート の組み合わせ

  • OpenAPI ファイルをテンプレート化し、Terraform の `templatefile` 関数で動的な値(例:Lambda の ARN)を注入します。
  • 動的な値を埋め込んだ後の OpenAPI ファイルをインポートします

サンプルコード

今回は、統合先を Lambda プロキシ統合とし、認証に Cognito オーソライザーを設定する例を提示します。

まずは、OpenAPI テンプレートです。

# OpenAPI テンプレート例
openapi: 3.0.3
info:
  title: Demo - Terraform x API Gateway x OpenAPI
  version: 1.0.0
servers:
  - url: https://example.com/
components:
  securitySchemes:  # 認証設定は`securitySchemes`に定義。今回はCognito認証の例を記載。
    CognitoAuthorizer:
      type: apiKey
      name: Authorization
      in: header
      x-amazon-apigateway-authtype: cognito_user_pools
      x-amazon-apigateway-authorizer:
        type: cognito_user_pools
        providerARNs:
          - ${cognito_user_pool_arn}  # CognitoユーザプールのARNをプレースホルダーにして、Terraformで動的に注入。
paths:
  /hello:
    get:  # 1つ目のエンドポイントを定義。
      operationId: hello
      summary: Hello endpoint
      security:
        - CognitoAuthorizer: []  # securitySchemesで定義した認証設定を参照すると、API Gatewayの認証設定が適用されます。
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
          headers:
            Access-Control-Allow-Origin:
              schema:
                type: string
      x-amazon-apigateway-integration:  # API Gatewayの統合設定を定義。
        uri: ${lambda_hello_invoke_arn}  # LambdaのARNをプレースホルダーにして、Terraformで動的に注入。
        httpMethod: POST
        type: aws_proxy
        payloadFormatVersion: 2
    options:  # 2つ目のエンドポイントを定義。
      operationId: helloOptions
      responses:
        "200":
          description: OK
          headers:
            Access-Control-Allow-Headers:
              schema:
                type: string
            Access-Control-Allow-Methods:
              schema:
                type: string
            Access-Control-Allow-Origin:
              schema:
                type: string
      x-amazon-apigateway-integration:  # API Gatewayの統合設定を定義。
        type: mock  # モック統合を定義(CORSプリフライトリクエストを処理するため)
        responses:  # モック統合のレスポンスを定義。
          default:
            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: "'GET,OPTIONS'"
              method.response.header.Access-Control-Allow-Origin: "'*'"
        requestTemplates:  # モック統合のリクエストテンプレートを定義。
          application/json: '{"statusCode": 200}'
        passthroughBehavior: "never"
  /uppercase:  # 以降も、上記と同様に定義。
    post:
      operationId: uppercase
      summary: Convert text to uppercase
      security:
        - CognitoAuthorizer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                text:
                  type: string
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  result:
                    type: string
          headers:
            Access-Control-Allow-Origin:
              schema:
                type: string
      x-amazon-apigateway-integration:
        uri: ${lambda_uppercase_invoke_arn}
        httpMethod: POST
        type: aws_proxy
        payloadFormatVersion: 2
    options:
      operationId: uppercaseOptions
      responses:
        "200":
          description: OK
          headers:
            Access-Control-Allow-Headers:
              schema:
                type: string
            Access-Control-Allow-Methods:
              schema:
                type: string
            Access-Control-Allow-Origin:
              schema:
                type: string
      x-amazon-apigateway-integration:
        responses:
          default:
            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: "'POST,OPTIONS'"
              method.response.header.Access-Control-Allow-Origin: "'*'"
        requestTemplates:
          application/json: '{"statusCode": 200}'
        passthroughBehavior: "never"
        type: mock

Swagger UI での表示イメージです。(普通に表示できていますね)

次に、Terraform のコードです。

# `template_file`を利用して、OpenAPIファイル内のプレースホルダーを埋めて、dataとして取り込む。
data "template_file" "openapi" {
  template = file("${path.module}/openapi.yaml")
  vars = {
    lambda_hello_invoke_arn     = aws_lambda_function.hello.invoke_arn
    lambda_uppercase_invoke_arn = aws_lambda_function.uppercase.invoke_arn
    cognito_user_pool_arn       = aws_cognito_user_pool.main.arn
  }
}

# テンプレート化したOpenAPIファイルをAPI Gatewayにインポート。
resource "aws_api_gateway_rest_api" "api" {
  name = local.project_prefix
  body = data.template_file.openapi.rendered
}
# このほか、`aws_api_gateway_deployment`や`aws_api_gateway_stage`などのリソースも必要。(省略)

# OpenAPIから参照するCognitoユーザプールの作成が必要
resource "aws_cognito_user_pool" "main" {
  name = local.project_prefix
}
resource "aws_cognito_user_pool_client" "main" {
  name         = local.project_prefix
  user_pool_id = aws_cognito_user_pool.main.id

  # ALLOW_USER_PASSWORD_AUTHよりも、よりセキュアな認証フローを使用した方が良い
  explicit_auth_flows = [
    "ALLOW_USER_PASSWORD_AUTH",
    "ALLOW_REFRESH_TOKEN_AUTH"
  ]
}

# OpenAPIから参照するLambda関数の作成が必要
resource "aws_lambda_function" "hello" {
    # 省略
}
resource "aws_lambda_function" "uppercase" {
    # 省略
}
# そのほかLambda関数に関するリソースの作成も必要(省略)
#   - Lambdaロール
#   - Lambdaパーミッション

コードの全量はGitHub 上のデモプロジェクトにあります。

CodeSpaces で動作します。(GitHubアカウントとAWS のアカウントが必要です)

さいごに

REST API のリソース定義を Terraform にベタ書きするのは、コードがかなり増えてしまうので嫌いでした。そこで、OpenAPI インポートを使いたくなるのですが、今度は Lambda の ARN を OpenAPI に書かなくてはいけないという問題が浮上しました。

Terraform のテンプレート機能を挟むことで、上記の問題をまるっと解決できるので、今回はその方法を紹介しました。

ちなみに、CDK でも同じことができます。Node.js で前処理できるので、当然ながら断然自由度が高いです。openapixのような、サードパーティのコンストラクトもありますね。

この記事をシェアする
著者:酒井亮太郎
シナモロール