API GatewayとStep FunctionsをTerraformでサーバーレスAPIを構築する
はじめに
AWS Summit 2025へ参加し、Step FunctionだけでAPIの処理を行う... というセッションを見て感化されたのでこの記事を執筆しようと思いました。
今回は、AWS上でサーバーレスなAPIを構築する際に、多くの場合で採用されるLambdaをあえて利用せずに、API GatewayとStep Functionsを直接連携させる方法について解説します。
通常、API GatewayのバックエンドにはLambdaを配置してビジネスロジックを実装することが多いですが、リクエストの内容に応じて処理を振り分けたり、単純なAWSサービス連携を実行したりするだけであれば、Step Functionsで十分なケースがあります。
この構成のメリットは以下の通りです。
- Lambdaのコールドスタートを考慮しなくて良い
- 状態管理やリトライ処理をStep Functionsのワークフローで視覚的に定義できる
- Terraformですべてのリソースをコード化しやすく、管理が容易になる
今回は、簡単なToDo管理APIを例に、Terraformを使ってこのアーキテクチャを構築する手順をご紹介します。
アーキテクチャ概要
今回構築するシステムの全体像は以下の通りです。
クライアント
│
│ HTTPリクエスト (POST /create, GET /get)
↓
┌───────────────────┐
│ API Gateway │ (HTTP API)
└───────────────────┘
│
│ AWS_PROXY統合
↓
┌───────────────────┐
│ Step Functions │ (State Machine)
└───────────────────┘
│
├─ (operation: "create") → DynamoDB:PutItem
│
└─ (operation: "getTodo") → DynamoDB:GetItem
API Gatewayが受け取ったリクエストを直接Step Functionsに渡し、Step Functionsがリクエスト内容(operation
)に応じてDynamoDBの操作を分岐させます。
実装解説
それでは、Terraformのコードを見ていきましょう。
1. DynamoDBテーブル
まずは、ToDoアイテムを保存するためのDynamoDBテーブルを定義します。
resource "aws_dynamodb_table" "todo_table" {
name = "${var.project_name}-table"
billing_mode = "PAY_PER_REQUEST"
hash_key = "id"
range_key = "userid"
attribute {
name = "id"
type = "S"
}
attribute {
name = "userid"
type = "S"
}
tags = var.tags
}
id
をパーティションキー、userid
をソートキーとするシンプルなテーブルです。
2. Step Functions ステートマシン
次に、API Gatewayからのリクエストを処理するステートマシンを定義します。
ステートマシンの定義 (statemachine.json)
ワークフローの定義はJSONで行います。Choice
ステートを使って、operation
の値によって処理を分岐させているのがポイントです。
{
"Comment": "A state machine that handles both creating and getting items from DynamoDB.",
"StartAt": "CheckOperation",
"States": {
"CheckOperation": {
"Type": "Choice",
"Choices": [
{
"Variable": "$.operation",
"StringEquals": "create",
"Next": "CreateItem"
},
{
"Variable": "$.operation",
"StringEquals": "getTodo",
"Next": "GetItem"
}
],
"Default": "FailState"
},
"CreateItem": {
"Type": "Task",
"Resource": "arn:aws:states:::dynamodb:putItem",
"Parameters": {
"TableName": "${DYNAMODB_TABLE_NAME}",
"Item": {
"id": {
"S.$": "States.UUID()"
},
"userId": {
"S.$": "$.payload.userId"
},
"task": {
"S.$": "$.payload.task"
},
"timestamp": {
"S.$": "$$.State.EnteredTime"
}
}
},
"End": true
},
"GetItem": {
"Type": "Task",
"Resource": "arn:aws:states:::dynamodb:getItem",
"Parameters": {
"TableName": "${DYNAMODB_TABLE_NAME}",
"Key": {
"userId": {
"S.$": "$.payload.userId"
}
}
},
"End": true
},
"FailState": {
"Type": "Fail",
"Error": "InvalidOperation",
"Cause": "The specified operation is not supported."
}
}
}
CreateItem
とGetItem
タスクでは、Resource
にarn:aws:states:::dynamodb:putItem
やarn:aws:states:::dynamodb:getItem
といったAWS SDK統合を指定することで、Lambdaを介さずに直接DynamoDBを操作しています。
Terraformリソース (stepfunctions.tf)
terraform-aws-modules/step-functions/aws
モジュールを使い、先ほどのJSONファイルを読み込んでステートマシンを作成します。
module "todo_sfn" {
source = "terraform-aws-modules/step-functions/aws"
name = "${var.project_name}-state-machine"
definition = templatefile("${path.module}/statemachine.json", {
DYNAMODB_TABLE_NAME = aws_dynamodb_table.todo_table.name
})
service_integrations = {
dynamodb = {
dynamodb = [aws_dynamodb_table.todo_table.arn]
}
}
tags = var.tags
}
templatefile
関数を使って、JSON内の変数 ${DYNAMODB_TABLE_NAME}
に実際のテーブル名を埋め込んでいます。
3. API Gateway
最後に、API Gatewayを定義します。ここが今回のアーキテクチャの核心部分です。
module "todo_api_gateway" {
source = "terraform-aws-modules/apigateway-v2/aws"
name = "${var.project_name}-api"
protocol_type = "HTTP"
description = "API for managing a ToDo list"
cors_configuration = {
allow_methods = ["*"]
allow_origins = ["*"]
allow_headers = ["content-type", "x-amz-date", "authorization", "x-api-key", "x-amz-security-token", "x-amz-user-agent"]
}
routes = {
"POST /create" = {
integration = {
type = "AWS_PROXY"
subtype = "StepFunctions-StartExecution" # ここが重要!
credentials_arn = aws_iam_role.api_gateway_role.arn
request_parameters = {
StateMachineArn = module.todo_sfn.state_machine_arn
Input = jsonencode({
operation = "create",
payload = "$request.body" # リクエストボディ全体をpayloadに
})
}
payload_format_version = "1.0"
timeout_milliseconds = 12000
}
}
"GET /get" = {
integration = {
type = "AWS_PROXY"
subtype = "StepFunctions-StartExecution" # ここが重要!
credentials_arn = aws_iam_role.api_gateway_role.arn
request_parameters = {
StateMachineArn = module.todo_sfn.state_machine_arn
Input = jsonencode({
operation = "getTodo",
payload = "$request.body" # リクエストボディ全体をpayloadに
})
}
payload_format_version = "1.0"
timeout_milliseconds = 12000
}
}
}
stage_name = var.stage_name
tags = var.tags
}
# API GatewayがStep Functionsを呼び出すためのIAMロール
resource "aws_iam_role" "api_gateway_role" {
name = "api-gateway-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "apigateway.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
tags = var.tags
}
resource "aws_iam_role_policy" "api_gateway_policy" {
name = "api-gateway-policy"
role = aws_iam_role.api_gateway_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "states:StartExecution"
Resource = [module.todo_sfn.state_machine_arn]
}]
})
}
terraform-aws-modules/apigateway-v2/aws
モジュールを利用しています。
ルート定義の中にあるintegration
設定が最も重要です。
-
type
:AWS_PROXY
を指定します。 -
subtype
:StepFunctions-StartExecution
を指定することで、連携先がStep Functionsの実行であることが示されます。 -
request_parameters
:-
StateMachineArn
: 呼び出すステートマシンのARNを指定します。 -
Input
: ステートマシンに渡す入力(JSON)を定義します。ここでoperation
フィールドを追加し、リクエストボディの内容をpayload
に入れることで、後続のステートマシンが処理を分岐できるようにしています。
-
また、API GatewayがStep FunctionsのStartExecution
アクションを呼び出すためのIAMロールとポリシーも忘れずに作成します。
まとめ
今回は、Lambdaを使わずにAPI GatewayとStep Functionsを直接連携させる構成をTerraformで構築する方法を紹介しました。
単純なロジックやAWSサービスへのルーティングであれば、この構成は非常にシンプルかつ強力です。Lambdaの管理から解放され、ワークフローに集中できるメリットは大きいと感じます。
Discussion