🚀

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テーブルを定義します。

dynamodb.tf
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の値によって処理を分岐させているのがポイントです。

statemachine.json
{
  "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."
    }
  }
}

CreateItemGetItemタスクでは、Resourcearn:aws:states:::dynamodb:putItemarn:aws:states:::dynamodb:getItemといったAWS SDK統合を指定することで、Lambdaを介さずに直接DynamoDBを操作しています。

Terraformリソース (stepfunctions.tf)

terraform-aws-modules/step-functions/awsモジュールを使い、先ほどのJSONファイルを読み込んでステートマシンを作成します。

stepfunctions.tf
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を定義します。ここが今回のアーキテクチャの核心部分です。

apigateway.tf
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の管理から解放され、ワークフローに集中できるメリットは大きいと感じます。

GitHubで編集を提案

Discussion