Techfirm Cloud Architect Blog

テックファーム株式会社クラウドインフラグループのブログ

Terraformで作ったREST APIにServerless Frameworkからデプロイしてみた

以前、IaCでREST APIを設定するための要素やデプロイ方法について書きました。

記事内にも記載していますが、実際はTerraformやCloudFormationでAPI Gatewayをすべて構築することは少ないのではないかと思います。
今回は、Terraformで構築したREST APIに対してServerless Frameworkからデプロイを行いたいと思います。

誰が何を使ってAPI Gatewayを管理するか

API Gatewayをもっとも簡単に構築する方法はServerless Frameworkにすべて任せてしまうことだと思います。
しかし、以下のような理由からログ出力等の煩雑な設定を実施する人がいない場合があります。

  • 開発担当者は機能実装に忙しいし、何を設定すればいいのか分からない
  • インフラ担当者はServerless Frameworkについて知識がない

また、デプロイされるまでapi-idが分からないため、カスタムドメインやAWS WAFとの連携が設定できません。
これらもServerless Frameworkで実装可能ですが、上のように実施する人をさらに選ぶようになると思います。

そこで、今回はなるべくTerraform側に設定を寄せていきます。

作成するリソースの境界

API GatewayをTerraformで作るときでも、デプロイはServerless Frameworkで実施するのが適切です。その上で、ログ出力の設定等はTerraformで実施したいと思います。
前の記事で説明したとおり、ログ出力の設定はステージ(aws_api_gateway_stage)にあります。 しかし、aws_api_gateway_stageを作るためにはデプロイメント(aws_api_gateway_deployment)が必要です。

そこで、今回紹介するのは以下の構成です。

  1. TerraformでREST APIと最初のデプロイメントを作成する
    • ダミーのAPIを定義
    • aws_api_gateway_stagedeployment_idは変更を検知しないようにする
  2. Serverless Frameworkで上記のREST APIを指定してデプロイする
    • Serverless FrameworkはREST APIを作成しなくなる
    • デプロイでStageNameの指定されたAWS::ApiGateway::Deploymentが作成される
    • ステージの参照先デプロイメントも更新される

次から実際の設定を見ていきます。

Terraformの設定

まずTerraformのリソースを作成します。
実際に動作するメソッドがないとデプロイでエラーになるため、/staticcheckというリソースにGETメソッドを作成しています。

Terraform作成構成

Serverless Frameworkのデプロイ先はルート(/)でもいいのですが、今回はTerraformで用意した/api以下にデプロイします。

まず始めに、REST APIの作成とデプロイ先のリソースを作成するコードです。

# REST API
resource "aws_api_gateway_rest_api" "api" {
  name        = "test-api"
}

# /apiリソース
resource "aws_api_gateway_resource" "api" {
  path_part   = "api"
  rest_api_id = aws_api_gateway_rest_api.api.id
  parent_id   = aws_api_gateway_rest_api.api.root_resource_id
}

次に、/staticcheckを作成するコードです。
最後のaws_api_gateway_integration_responseリソースに応答内容が定義されています。

# ダミー用statickcheck API
resource "aws_api_gateway_resource" "staticcheck" {
  path_part   = "staticcheck"
  rest_api_id = aws_api_gateway_rest_api.api.id
  parent_id   = aws_api_gateway_rest_api.api.root_resource_id
}
resource "aws_api_gateway_method" "staticcheck_get" {
  rest_api_id   = aws_api_gateway_resource.staticcheck.rest_api_id
  resource_id   = aws_api_gateway_resource.staticcheck.id
  http_method   = "GET"
  authorization = "NONE"
}
resource "aws_api_gateway_method_response" "staticcheck_get_200" {
  rest_api_id   = aws_api_gateway_method.staticcheck_get.rest_api_id
  resource_id   = aws_api_gateway_method.staticcheck_get.resource_id
  http_method   = aws_api_gateway_method.staticcheck_get.http_method
  status_code = "200"
}
resource "aws_api_gateway_integration" "staticcheck_get" {
  rest_api_id   = aws_api_gateway_method.staticcheck_get.rest_api_id
  resource_id   = aws_api_gateway_method.staticcheck_get.resource_id
  http_method   = aws_api_gateway_method.staticcheck_get.http_method
  type                 = "MOCK"
  passthrough_behavior = "WHEN_NO_TEMPLATES"
  request_templates = {
    "application/json" = jsonencode({
      statusCode = 200
    })
  }
}
resource "aws_api_gateway_integration_response" "staticcheck_get_200" {
  rest_api_id   = aws_api_gateway_method.staticcheck_get.rest_api_id
  resource_id   = aws_api_gateway_method.staticcheck_get.resource_id
  http_method   = aws_api_gateway_method.staticcheck_get.http_method
  status_code = aws_api_gateway_method_response.staticcheck_get_200.status_code
  response_templates = {
    "application/json" = jsonencode({
        deployed = true
    })
  }
}

そして、これらをデプロイしてステージを設定するためのコードです。

# 初期デプロイ+ステージ
resource "aws_api_gateway_deployment" "first" {
  rest_api_id = aws_api_gateway_rest_api.api.id
  description = "infra empty deployment"
  depends_on = [
    aws_api_gateway_integration.staticcheck_get,
    aws_api_gateway_integration_response.staticcheck_get_200
  ]
}
resource "aws_api_gateway_stage" "stage" {
  deployment_id = aws_api_gateway_deployment.first.id
  rest_api_id   = aws_api_gateway_rest_api.api.id
  stage_name    = var.env
  description   = "${var.env} created by Terraform."
  lifecycle {
    ignore_changes = [
      deployment_id,
    ]
  }
}

上記ではステージの設定は入れていないの状態ですが、必要に応じてログ出力等の設定を入れていきます。
ここでポイントは、以下の2点です。

  • aws_api_gateway_deploymentdepends_onを定義する
  • aws_api_gateway_stageignore_changesdeployment_idを指定する

最初のdepends_onの定義は、リソースの作成が完了する前にデプロイメント作成が走るのを防ぐためです。
デプロイメントはリソースやメソッドと依存関係を持たないので、実行順序を明示しています。
(デプロイはServerless Frameworkでコントロールしたいため、triggersは定義していません)

次のignore_changesの指定は、deployment_idの変更を無視するように設定しています。 Serverless Frameworkのデプロイでステージの参照先デプロイメント(deployment_id)が更新されるため、Terraformがそれを変更として検知しないようにします。

リソースの作成はここまでですが、最後にServerless Frameworkに渡す必要のある値をoutputします。

output "apigw_api_id" {
  value = aws_api_gateway_rest_api.api.id
}
output "apigw_api_resource_id" {
  value = aws_api_gateway_resource.api.id
}

REST APIのID(上記apigw_api_id)とデプロイ先リソースのID(上記apigw_api_resource_id)になります。
serverless.yml等に直接書かずCodeBuildの変数として渡す場合は、outputではなく直接CodeBuildの変数等に設定してください。

また、ルートリソース(/)にデプロイする場合は、apigw_api_resource_idaws_api_gateway_rest_api.api.root_resource_idになります。

serverless.ymlの設定

serverless.ymlの設定に、上で作成したAPI Gatewayを設定します。
特別に必要な設定は以下の箇所のみです。

provider:
  apiGateway:
    restApiId: "<<REST APIのID(Terraformでoutputとしたapigw_api_id)>>"
    restApiRootResourceId: "<<リソースのID(Terraformでoutputとしたapigw_api_resource_id)>>"

restApiIdrestApiRootResourceIdに、それぞれTerraformでoutputした内容を設定するだけです。 (ルート(/)にデプロイする場合、restApiRootResourceIdは省略可能です)
あとは、REST APIのログ設定等を入れずに通常通りserverless.ymlの設定をおこなってください。
たとえば、以下のようになります。

service: apigwtest
frameworkVersion: '3'
provider:
  name: aws
  runtime: python3.9
  stage: ${opt:stage, self:custom.defaultStage}
  region: ap-northeast-1
  apiGateway:
    restApiId: xxxxxxxxxx
    restApiRootResourceId: yyyyyy
package:
  individually: true
  excludeDevDependencies: true
  patterns:
    - '!**'
custom:
  defaultStage: dev
functions:
  test1:
    handler: functions/test/test.handler
    role: arn:aws:iam::123456789012:role/${self:provider.stage}-api-lambda-test
    disableLogs: true
    package:
      patterns:
        - functions/test/**
    events:
      - http:
          path: /test
          method: GET

これをデプロイすると、以下のようになります。赤いところがデプロイにより追加・変更されたところです。

slsデプロイ後API構成

restApiRootResourceId/apiのリソースIDを指定しているため、その下にリソースが作成されます。

デプロイの裏ではCloudFormationのAWS::ApiGateway::Deploymentリソースが作成されます。
このリソースにはStageNameが指定されているため、デプロイメントを作成するとともにステージの参照先デプロイを更新します。
これにより、Terraformで作成したステージの向き先デプロイメントが書き換えられ、Serverless Frameworkで作成したAPIもアクセス可能な状態になります。 (restApiRootResourceIdにルート以外のリソースIDを指定しているときはコマンド結果にあるURLのパスはずれてしまうようなのでご注意ください)

Lambda関数の定義

今回のテーマであるAPI Gatewayから少し外れますが、Lambda関数デプロイの話しも少し書いておきます。
先のserverless.ymlの例では以下のようにroledisableLogsを指定していました。

functions:
  test1:
    handler: functions/test/test.handler
    role: arn:aws:iam::123456789012:role/${self:provider.stage}-api-lambda-test
    disableLogs: true

roleはLambda関数が使うIAMロールで、disableLogsはCloudWatch LogsのLogGroupを作らないようにする設定です。
いずれも、以下のような理由からTerraformで集中管理しています。

  • 開発者にはなるべく機能の実装に集中してもらいたい
  • セキュリティに関する設定はなるべく少人数で管理したい(開発者は人員の増減が多い)
  • ログ保存期間等、運用に関する設定をなるべく近いところにまとめたい

API Gatewayと同じように、他のリソースもTerraformに寄せられる部分があるという例でした。

最後に

今回はTerraformで構築したAPI GatewayにServerless Frameworkからデプロイする方法について紹介しました。
どのリソースを誰が何を使って管理するか、その時の状況によって変わってくるかと思います。
Terraformに寄せて管理することはメリットも多いと思いますので、役割分担を決めるときには考えていいただければと思います。