AWS

AWS Step Functionsとは?IaCで作るメール承認フロー構築手順

NEW 2026年8月26日

目次Category


1. AWS Step Functions とは

AWS Step Functions

AWS Step Functions

AWS Step Functions は、「コードやGUIで処理の流れ(状態=ステート)を定義し、
実行時の状態遷移を視覚的に管理・監視する」ためのオーケストレーション
(ワークフロー)サービスです。

ノード形式の見た目からWeb画面で完結するツールに見えますが、実態は
「コード(IaC)で定義し、実行状況をビジュアルで監視する」
タイプのサービスです。

定義の実態は JSON / YAML(コード管理)

画面上でドラッグ&ドロップして作れるGUI(Workflow Studio)も用意されていますが
実態は ASL(Amazon States Language) というJSON/YAML形式の定義ファイルです。

そのため、CloudFormation・AWS CDK・Serverless Framework・Terraform などを通して
コード(IaC)として管理・デプロイするのが実践的な運用の標準です。
今回の承認フローは template.yaml(CloudFormation)1ファイルで、ステートマシン定義
に加えて必要なリソース一式(SNS・Lambda・API Gateway・IAMロール)をまとめて
管理しています。

  • 作るとき: Web画面(Workflow Studio)で視覚的に組むことも、CloudFormationなどの
    コードで定義することも両方可能(最終的にはIaC管理が推奨)
  • 動かすとき: 実行状況がビジュアル(ノード形式)で可視化され、どこで止まったか
    何が渡されたかが一目瞭然になる

2. 本ハンズオンの構成図

今回構築する「承認フロー」のアーキテクチャ構成図です。
Step Functionsが承認プロセスを一時停止し、Amazon SNS経由でメールを送信します。
ユーザーがメール内のリンクをクリックすると、Amazon API GatewayとAWS Lambdaを
経由して承認結果を処理し、ワークフローを再開する構成です。

「承認フロー」のアーキテクチャ構成図

構成図の処理フロー

  1. ユーザーがStep Functionsのワークフローを開始します。
  2. Step Functionsが承認処理を開始します。
  3. Amazon SNSから承認者へメールを送信します。
  4. 承認者がメール内の承認または却下リンクをクリックします。
  5. Amazon API GatewayがHTTPリクエストを受け取ります。
  6. AWS Lambdaが承認結果を処理します。
  7. Step Functionsのワークフローを再開し、承認結果に応じた処理を実行します。

3. ハンズオン:CloudFormationを用いたデプロイ手順

ここからは、AWSマネジメントコンソールを使用して「承認フロー」のワークフロー一式
をデプロイする手順を解説します。
今回の構成では、template.yaml(CloudFormationテンプレート)1ファイルのみで、
関連するAWSリソースをまとめて構築します。

AWS CloudFormation
手順3-1: CloudFormationでのスタック作成

CloudFormationテンプレートをアップロードして、承認フローに必要な
AWSリソースを自動構築します。

3-1. CloudFormationテンプレートを準備する

以下の template.yaml を使用します。 このテンプレートには、Amazon SNS、
AWS Lambda、Amazon API Gateway、AWS Step Functions、IAMロールなど、
承認フローに必要なリソースが定義されています。

template.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: >
  Step Functions human approval flow.
  Approve/Reject links in an SNS email call API Gateway -> Lambda (SendTaskSuccess),
  which resumes the waitForTaskToken callback and branches via a Choice state.

Parameters:
  ApproverEmail:
    Type: String
    Description: Email address that receives approval request emails
    AllowedPattern: "[^@]+@[^@]+"
  ApprovalTimeoutSeconds:
    Type: Number
    Description: Timeout in seconds while waiting for approval (default 1 hour)
    Default: 30

Resources:
  # ---------------------------------------------------------------
  # SNS: 承認依頼メールの配信
  # ---------------------------------------------------------------
  ApprovalTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: !Sub "${AWS::StackName}-approval-topic"

  ApprovalSubscription:
    Type: AWS::SNS::Subscription
    Properties:
      TopicArn: !Ref ApprovalTopic
      Protocol: email
      Endpoint: !Ref ApproverEmail

  # ---------------------------------------------------------------
  # Lambda 1: approval-request(承認依頼メールを送る)
  # ---------------------------------------------------------------
  ApprovalRequestFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: PublishToApprovalTopic
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action: sns:Publish
                Resource: !Ref ApprovalTopic

  ApprovalRequestFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub "${AWS::StackName}-approval-request"
      Runtime: python3.12
      Handler: index.handler
      Timeout: 30
      Role: !GetAtt ApprovalRequestFunctionRole.Arn
      Environment:
        Variables:
          TOPIC_ARN: !Ref ApprovalTopic
          API_URL: !Sub "https://${ApprovalApi}.execute-api.${AWS::Region}.amazonaws.com/prod/respond"
      Code:
        ZipFile: |
          import json
          import os
          import urllib.parse

          import boto3

          sns = boto3.client("sns")


          def handler(event, context):
              task_token = event["taskToken"]
              execution_name = event.get("executionName", "-")
              original_input = event.get("input", {})

              encoded_token = urllib.parse.quote(task_token, safe="")
              base_url = os.environ["API_URL"]
              approve_url = f"{base_url}?action=approve&taskToken={encoded_token}"
              reject_url = f"{base_url}?action=reject&taskToken={encoded_token}"

              message = "\n".join([
                  "承認依頼が届いています。",
                  "",
                  f"実行名: {execution_name}",
                  f"入力: {json.dumps(original_input, ensure_ascii=False)}",
                  "",
                  "以下のリンクをクリックして回答してください。",
                  "",
                  f"【承認する】 {approve_url}",
                  "",
                  f"【却下する】 {reject_url}",
              ])

              sns.publish(
                  TopicArn=os.environ["TOPIC_ARN"],
                  Subject="[Approval Request] Step Functions approval needed",
                  Message=message,
              )
              return {"status": "notified"}

  # ---------------------------------------------------------------
  # Lambda 2: approval-callback(リンククリックを受けてトークンを返す)
  # ---------------------------------------------------------------
  ApprovalCallbackFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: SendTaskResult
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - states:SendTaskSuccess
                Resource: "*"

  ApprovalCallbackFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub "${AWS::StackName}-approval-callback"
      Runtime: python3.12
      Handler: index.handler
      Timeout: 30
      Role: !GetAtt ApprovalCallbackFunctionRole.Arn
      Code:
        ZipFile: |
          import json

          import boto3

          sfn = boto3.client("stepfunctions")

          PAGE = """<!DOCTYPE html>
          <html lang="ja"><head><meta charset="utf-8"><title>{title}</title></head>
          <body style="font-family: sans-serif; text-align: center; padding-top: 4rem;">
          <h1>{title}</h1><p>{detail}</p></body></html>"""


          def respond(status, title, detail):
              return {
                  "statusCode": status,
                  "headers": {"Content-Type": "text/html; charset=utf-8"},
                  "body": PAGE.format(title=title, detail=detail),
              }


          def handler(event, context):
              params = event.get("queryStringParameters") or {}
              task_token = params.get("taskToken")
              action = params.get("action")

              if not task_token or action not in ("approve", "reject"):
                  return respond(
                      400,
                      "リクエストが不正です",
                      "taskToken と action (approve/reject) が必要です。"
                  )

              try:
                  sfn.send_task_success(
                      taskToken=task_token,
                      output=json.dumps({"action": action}),
                  )
              except sfn.exceptions.TaskTimedOut:
                  return respond(
                      410,
                      "期限切れです",
                      "この承認依頼はタイムアウト済み、または回答済みです。"
                  )
              except sfn.exceptions.InvalidToken:
                  return respond(
                      400,
                      "トークンが不正です",
                      "リンクが壊れているか、既に使用済みです。"
                  )

              if action == "approve":
                  return respond(
                      200,
                      "承認しました",
                      "ワークフローの処理を続行します。"
                  )

              return respond(
                  200,
                  "却下しました",
                  "ワークフローは却下として処理されます。"
              )

  # ---------------------------------------------------------------
  # API Gateway (REST API): GET /respond
  # ---------------------------------------------------------------
  ApprovalApi:
    Type: AWS::ApiGateway::RestApi
    Properties:
      Name: !Sub "${AWS::StackName}-approval-api"
      EndpointConfiguration:
        Types:
          - REGIONAL

  RespondResource:
    Type: AWS::ApiGateway::Resource
    Properties:
      RestApiId: !Ref ApprovalApi
      ParentId: !GetAtt ApprovalApi.RootResourceId
      PathPart: respond

  RespondMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref ApprovalApi
      ResourceId: !Ref RespondResource
      HttpMethod: GET
      AuthorizationType: NONE
      Integration:
        Type: AWS_PROXY
        IntegrationHttpMethod: POST
        Uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ApprovalCallbackFunction.Arn}/invocations"

  ApiDeployment:
    Type: AWS::ApiGateway::Deployment
    DependsOn: RespondMethod
    Properties:
      RestApiId: !Ref ApprovalApi

  ApiStage:
    Type: AWS::ApiGateway::Stage
    Properties:
      RestApiId: !Ref ApprovalApi
      DeploymentId: !Ref ApiDeployment
      StageName: prod

  CallbackFunctionPermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref ApprovalCallbackFunction
      Action: lambda:InvokeFunction
      Principal: apigateway.amazonaws.com
      SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ApprovalApi}/*/GET/respond"

  # ---------------------------------------------------------------
  # Step Functions ステートマシン
  # ---------------------------------------------------------------
  StateMachineRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: states.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: InvokeApprovalRequestFunction
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action: lambda:InvokeFunction
                Resource: !GetAtt ApprovalRequestFunction.Arn

  ApprovalStateMachine:
    Type: AWS::StepFunctions::StateMachine
    Properties:
      StateMachineName: !Sub "${AWS::StackName}-approval-flow"
      RoleArn: !GetAtt StateMachineRole.Arn
      DefinitionString: !Sub
        - |
          {
            "Comment": "人間の承認・非承認で分岐する承認フロー(SNSメールのリンクで承認/却下)",
            "StartAt": "RequestApproval",
            "States": {
              "RequestApproval": {
                "Type": "Task",
                "Resource": "arn:aws:states:::lambda:invoke.waitForTaskToken",
                "Parameters": {
                  "FunctionName": "${ApprovalRequestFunctionArn}",
                  "Payload": {
                    "taskToken.$": "$$.Task.Token",
                    "executionName.$": "$$.Execution.Name",
                    "startTime.$": "$$.Execution.StartTime",
                    "input.$": "$"
                  }
                },
                "TimeoutSeconds": ${TimeoutSeconds},
                "ResultPath": "$.approval",
                "Catch": [
                  {
                    "ErrorEquals": ["States.Timeout"],
                    "ResultPath": "$.error",
                    "Next": "TimedOut"
                  }
                ],
                "Next": "IsApproved"
              },
              "IsApproved": {
                "Type": "Choice",
                "Choices": [
                  {
                    "Variable": "$.approval.action",
                    "StringEquals": "approve",
                    "Next": "Approved"
                  }
                ],
                "Default": "Rejected"
              },
              "Approved": {
                "Comment": "承認後の実処理をここに差し替える(例: Lambda呼び出し、ECSタスク起動など)",
                "Type": "Pass",
                "Result": {
                  "message": "承認されました。後続処理を実行します。"
                },
                "ResultPath": "$.result",
                "End": true
              },
              "Rejected": {
                "Comment": "却下時の処理をここに差し替える(例: 通知、クリーンアップなど)",
                "Type": "Pass",
                "Result": {
                  "message": "却下されました。"
                },
                "ResultPath": "$.result",
                "Next": "RejectedFail"
              },
              "RejectedFail": {
                "Type": "Fail",
                "Error": "ApprovalRejected",
                "Cause": "承認者によって却下されました"
              },
              "TimedOut": {
                "Type": "Fail",
                "Error": "ApprovalTimeout",
                "Cause": "承認期限内に応答がありませんでした"
              }
            }
          }
        - ApprovalRequestFunctionArn: !GetAtt ApprovalRequestFunction.Arn
          TimeoutSeconds: !Ref ApprovalTimeoutSeconds

Outputs:
  StateMachineArn:
    Description: ARN of the approval flow state machine
    Value: !Ref ApprovalStateMachine
  ApiEndpoint:
    Description: API endpoint for the approve/reject callback
    Value: !Sub "https://${ApprovalApi}.execute-api.${AWS::Region}.amazonaws.com/prod/respond"

上記の内容をそのまま template.yaml として保存してください。 CloudFormationの
スタック作成時に、このファイルをテンプレートとしてアップロードします。

3-2. CloudFormationでスタックを作成する

  1. AWSマネジメントコンソールへログインします。
  2. CloudFormation を開きます。
  3. 「スタックの作成」から「既存のテンプレートを選択」を選択します。
  4. 「テンプレートファイルのアップロード」を選択します。
  5. 上記の内容を保存した template.yaml をアップロードします。
  6. スタックの詳細を入力します。
    • スタック名: 例 ojt-stack-[name]-01(アカウント内で一意にする)
    • ApproverEmail: 承認依頼メールを受け取るメールアドレス
    • ApprovalTimeoutSeconds: 承認待ち時間のタイムアウト秒数(例:90)
  7. 「次へ」で進み、最後の確認画面の一番下にある「AWS CloudFormation によって IAM
    リソースが作成される場合があることを承認します」にチェック
    を入れます。
    (※このチェックは必須です)
  8. 「送信」 をクリックし、ステータスが CREATE_COMPLETE になるまで数分待ちます。
⚠️ 重要:SNSメール購読(Subscription Confirmation)の確認

CloudFormationのスタック作成が完了すると、 ApproverEmail に設定した
メールアドレス宛へ 「AWS Notification – Subscription Confirmation」という件名の
SNSメール購読確認メールが送信されます。
メール本文内の「Confirm subscription」リンクを必ずクリックしてください。

※ メールのリンクをクリックしないと、テスト実行しても承認依頼メールは
一切届きません(実行は承認待ちのまま止まり、タイムアウトで失敗します)。

確認メールが見当たらない場合は次をチェックしてください:

  • 迷惑メールフォルダに振り分けられていないか
  • コンソールの SNS → サブスクリプション で、該当のメールアドレスの
    ステータスが 保留中の確認(PendingConfirmation)のままになっていないか
    (保留中のままなら、サブスクリプションを選択して
    「確認をリクエスト」で再送できます)

4. 動作確認とテスト実行

  1. マネジメントコンソールで Step Functions を開き、
    ステートマシン ojt-stack-[name]-01 を選択します。
  2. 「新しい実行」 をクリックし、入力に以下のような
    テスト用のJSONを入れて実行します。
    {
      "requestId": "demo-001",
      "detail": "テスト承認依頼"
    }
  3. ApproverEmail 宛に件名
    「[Approval Request] Step Functions approval needed」
    のメールが届くので、本文の 【承認する】 または 【却下する】 リンクをクリックします。
    (ブラウザに「承認しました / 却下しました」のページが表示されます)
  4. Step Functions の実行画面(グラフビュー)で結果を確認します。
    操作実行結果
    【承認する】SUCCEEDEDApproved ステートを通過)
    【却下する】FAILED(エラー: ApprovalRejected
    何もせず放置ApprovalTimeoutSeconds 経過後に
    FAILED(エラー: ApprovalTimeout

Step Functionsの実行結果画面

5. 後片付け(リソースの削除)

検証が完了して不要になった場合は、CloudFormationのスタック一覧画面から
対象のスタックを選択し、「削除」を実行します。
CloudFormationでは、テンプレートから作成したリソースをスタック単位で
管理できるため、SNS・Lambda・API Gateway・IAM ロールなど、
スタックが作ったリソースは一括でまとめて削除されます。

6. まとめ

AWS Step Functionsは、分散アプリケーションのコンポーネントを一連のステップとして
調整し、ワークフローとして視覚的に管理できるサービスです。

また、Amazon States Language(ASL)を利用してステートマシンを定義できるため、
ワークフローをコードとして管理できます。

CloudFormationなどでテンプレート化することで、承認フローなどの複雑な処理を
再現可能な形で管理できます。

7. 参考記事・URL

この記事を書いた人

村守俊一

shunichi_muramori

26卒 ITソリューション部ビジネスソリューションセクション