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"