部署示例人员批准项目 - Amazon Step Functions
Amazon Web Services 文档中描述的 Amazon Web Services 服务或功能可能因区域而异。要查看适用于中国区域的差异,请参阅中国的 Amazon Web Services 服务入门

本文属于机器翻译版本。若本译文内容与英语原文存在差异,则一律以英文原文为准。

部署示例人员批准项目

本教程介绍如何部署允许 Amazon Step Functions 执行在任务期间暂停并等待用户对电子邮件做出响应的人员批准项目。一旦用户批准任务继续,工作流程就会进入下一个状态。

部署本教程中包含的 Amazon CloudFormation 堆栈将创建所有必需的资源,包括:

  • Amazon API Gateway

  • Amazon Lambda 函数

  • Amazon Step Functions 状态机

  • Amazon Simple Notification S

  • 相关 Amazon Identity and Access Management 角色和权限

注意

您需要提供创建 Amazon CloudFormation 堆栈时有权访问的有效电子邮件地址。

有关更多信息,请参阅 。使用 CloudFormation 模板AWS::StepFunctions::StateMachine中的资源Amazon CloudFormation用户指南.

第 1 步:创建Amazon CloudFormation模板

  1. Amazon CloudFormation 模板源代码 部分复制示例代码。

  2. 将 Amazon CloudFormation 模板的源粘贴到本地计算机的文件中。

    在此示例中,文件名为 human-approval.yaml

第 2 步:创建堆栈

  1. 登录 Amazon CloudFormation 控制台

  2. 选择创建堆栈,然后选择。使用新资源(使用新资源(标准)).

  3. 在存储库的创建堆栈页面上,请执行以下操作:

    1. 前提条件-准备模板部分,确保模板准备就绪已选择。

    2. 指定模板部分,选择。上传模板文件然后选择。选择文件上传到human-approval.yaml您之前创建的文件包含模板源代码.

  4. 选择 Next(下一步)。

  5. Specify stack details(指定堆栈详细信息)页面中,请执行以下操作:

    1. 适用于堆栈名称,输入您的堆栈的名称。

    2. UNDER参数,输入有效的电子邮件地址。您将使用此电子邮件地址订阅 Amazon SNS 主题。

  6. 选择下一步,然后选择。下一步

  7. 在存储库的审核页面上,选择。我承认这一点Amazon CloudFormation可能会创建 IAM 资源然后选择。Create.

    Amazon CloudFormation 开始创建您的堆栈,并显示 CREATE_IN_PROGRESS 状态。在此过程完成后,Amazon CloudFormation 将显示 CREATE_COMPLETE 状态。

  8. (可选) 要显示您的堆栈中的资源,请选择堆栈,然后选择 Resources 选项卡。

    
            显示资源

第 3 步:批准Amazon SNS 订阅

Amazon SNS 主题创建后,您将收到一封电子邮件,要求您确认订阅。

  1. 打开创建 Amazon CloudFormation 堆栈时提供的电子邮件账户。

  2. 打开消息Amazon通知-订阅确认no-reply@sns.amazonaws.com

    该电子邮件将列出Amazon SNS 主题的Amazon SNS 主题的Amazon SNS 资源名称以及确认链接。

  3. 选择 confirm subscription (确认订阅) 链接。

    
            订阅已确认!

第 4 步:运行执行

  1. 登录到Step Functions 控制台.

  2. 在存储库的状态机页面上,选择。HumanApprovalLambdaStateMachine.

  3. 选择 Start execution (开始执行)

  4. 为执行输入一个名称,例如 ApprovalTest

  5. (可选)为执行输入一些内容。

    { "Comment": "Testing the human approval tutorial." }
  6. 选择 Start execution (开始执行)

    这些区域有:ApprovalTest状态机执行开始,并在Lambda 回调task。

    
            执行等待回调
  7. 在您之前用于 Amazon SNS 主题的电子邮件账户中,打开主题为需要来自的批准Amazon Step Functions.

    此消息包括用于批准拒绝的独立 URL。

  8. 选择批准 URL。

    工作流程根据您的选择继续。

    
            执行等待回调

Amazon CloudFormation 模板源代码

使用此 Amazon CloudFormation 模板部署人员审批流程工作流程的示例。

AWSTemplateFormatVersion: "2010-09-09" Description: "AWS Step Functions Human based task example. It sends an email with an HTTP URL for approval." Parameters: Email: Type: String AllowedPattern: "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$" ConstraintDescription: Must be a valid email address. Resources: # Begin API Gateway Resources ExecutionApi: Type: "AWS::ApiGateway::RestApi" Properties: Name: "Human approval endpoint" Description: "HTTP Endpoint backed by API Gateway and Lambda" FailOnWarnings: true ExecutionResource: Type: 'AWS::ApiGateway::Resource' Properties: RestApiId: !Ref ExecutionApi ParentId: !GetAtt "ExecutionApi.RootResourceId" PathPart: execution ExecutionMethod: Type: "AWS::ApiGateway::Method" Properties: AuthorizationType: NONE HttpMethod: GET Integration: Type: AWS IntegrationHttpMethod: POST Uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LambdaApprovalFunction.Arn}/invocations" IntegrationResponses: - StatusCode: 302 ResponseParameters: method.response.header.Location: "integration.response.body.headers.Location" RequestTemplates: application/json: | { "body" : $input.json('$'), "headers": { #foreach($header in $input.params().header.keySet()) "$header": "$util.escapeJavaScript($input.params().header.get($header))" #if($foreach.hasNext),#end #end }, "method": "$context.httpMethod", "params": { #foreach($param in $input.params().path.keySet()) "$param": "$util.escapeJavaScript($input.params().path.get($param))" #if($foreach.hasNext),#end #end }, "query": { #foreach($queryParam in $input.params().querystring.keySet()) "$queryParam": "$util.escapeJavaScript($input.params().querystring.get($queryParam))" #if($foreach.hasNext),#end #end } } ResourceId: !Ref ExecutionResource RestApiId: !Ref ExecutionApi MethodResponses: - StatusCode: 302 ResponseParameters: method.response.header.Location: true ApiGatewayAccount: Type: 'AWS::ApiGateway::Account' Properties: CloudWatchRoleArn: !GetAtt "ApiGatewayCloudWatchLogsRole.Arn" ApiGatewayCloudWatchLogsRole: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: - apigateway.amazonaws.com Action: - 'sts:AssumeRole' Policies: - PolicyName: ApiGatewayLogsPolicy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - "logs:*" Resource: !Sub "arn:${AWS::Partition}:logs:*:*:*" ExecutionApiStage: DependsOn: - ApiGatewayAccount Type: 'AWS::ApiGateway::Stage' Properties: DeploymentId: !Ref ApiDeployment MethodSettings: - DataTraceEnabled: true HttpMethod: '*' LoggingLevel: INFO ResourcePath: /* RestApiId: !Ref ExecutionApi StageName: states ApiDeployment: Type: "AWS::ApiGateway::Deployment" DependsOn: - ExecutionMethod Properties: RestApiId: !Ref ExecutionApi StageName: DummyStage # End API Gateway Resources # Begin # Lambda that will be invoked by API Gateway LambdaApprovalFunction: Type: 'AWS::Lambda::Function' Properties: Code: ZipFile: Fn::Sub: | const AWS = require('aws-sdk'); var redirectToStepFunctions = function(lambdaArn, statemachineName, executionName, callback) { const lambdaArnTokens = lambdaArn.split(":"); const partition = lambdaArnTokens[1]; const region = lambdaArnTokens[3]; const accountId = lambdaArnTokens[4]; console.log("partition=" + partition); console.log("region=" + region); console.log("accountId=" + accountId); const executionArn = "arn:" + partition + ":states:" + region + ":" + accountId + ":execution:" + statemachineName + ":" + executionName; console.log("executionArn=" + executionArn); const url = "https://console.aws.amazon.com/states/home?region=" + region + "#/executions/details/" + executionArn; callback(null, { statusCode: 302, headers: { Location: url } }); }; exports.handler = (event, context, callback) => { console.log('Event= ' + JSON.stringify(event)); const action = event.query.action; const taskToken = event.query.taskToken; const statemachineName = event.query.sm; const executionName = event.query.ex; const stepfunctions = new AWS.StepFunctions(); var message = ""; if (action === "approve") { message = { "Status": "Approved! Task approved by ${Email}" }; } else if (action === "reject") { message = { "Status": "Rejected! Task rejected by ${Email}" }; } else { console.error("Unrecognized action. Expected: approve, reject."); callback({"Status": "Failed to process the request. Unrecognized Action."}); } stepfunctions.sendTaskSuccess({ output: JSON.stringify(message), taskToken: event.query.taskToken }) .promise() .then(function(data) { redirectToStepFunctions(context.invokedFunctionArn, statemachineName, executionName, callback); }).catch(function(err) { console.error(err, err.stack); callback(err); }); } Description: Lambda function that callback to AWS Step Functions FunctionName: LambdaApprovalFunction Handler: index.handler Role: !GetAtt "LambdaApiGatewayIAMRole.Arn" Runtime: nodejs12.x LambdaApiGatewayInvoke: Type: "AWS::Lambda::Permission" Properties: Action: "lambda:InvokeFunction" FunctionName: !GetAtt "LambdaApprovalFunction.Arn" Principal: "apigateway.amazonaws.com" SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ExecutionApi}/*" LambdaApiGatewayIAMRole: Type: "AWS::IAM::Role" Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Action: - "sts:AssumeRole" Effect: "Allow" Principal: Service: - "lambda.amazonaws.com" Policies: - PolicyName: CloudWatchLogsPolicy PolicyDocument: Statement: - Effect: Allow Action: - "logs:*" Resource: !Sub "arn:${AWS::Partition}:logs:*:*:*" - PolicyName: StepFunctionsPolicy PolicyDocument: Statement: - Effect: Allow Action: - "states:SendTaskFailure" - "states:SendTaskSuccess" Resource: "*" # End Lambda that will be invoked by API Gateway # Begin state machine that publishes to Lambda and sends an email with the link for approval HumanApprovalLambdaStateMachine: Type: AWS::StepFunctions::StateMachine Properties: RoleArn: !GetAtt LambdaStateMachineExecutionRole.Arn DefinitionString: Fn::Sub: | { "StartAt": "Lambda Callback", "TimeoutSeconds": 3600, "States": { "Lambda Callback": { "Type": "Task", "Resource": "arn:${AWS::Partition}:states:::lambda:invoke.waitForTaskToken", "Parameters": { "FunctionName": "${LambdaHumanApprovalSendEmailFunction.Arn}", "Payload": { "ExecutionContext.$": "$$", "APIGatewayEndpoint": "https://${ExecutionApi}.execute-api.${AWS::Region}.amazonaws.com/states" } }, "Next": "ManualApprovalChoiceState" }, "ManualApprovalChoiceState": { "Type": "Choice", "Choices": [ { "Variable": "$.Status", "StringEquals": "Approved! Task approved by ${Email}", "Next": "ApprovedPassState" }, { "Variable": "$.Status", "StringEquals": "Rejected! Task rejected by ${Email}", "Next": "RejectedPassState" } ] }, "ApprovedPassState": { "Type": "Pass", "End": true }, "RejectedPassState": { "Type": "Pass", "End": true } } } SNSHumanApprovalEmailTopic: Type: AWS::SNS::Topic Properties: Subscription: - Endpoint: !Sub ${Email} Protocol: email LambdaHumanApprovalSendEmailFunction: Type: "AWS::Lambda::Function" Properties: Handler: "index.lambda_handler" Role: !GetAtt LambdaSendEmailExecutionRole.Arn Runtime: "nodejs12.x" Timeout: "25" Code: ZipFile: Fn::Sub: | console.log('Loading function'); const AWS = require('aws-sdk'); exports.lambda_handler = (event, context, callback) => { console.log('event= ' + JSON.stringify(event)); console.log('context= ' + JSON.stringify(context)); const executionContext = event.ExecutionContext; console.log('executionContext= ' + executionContext); const executionName = executionContext.Execution.Name; console.log('executionName= ' + executionName); const statemachineName = executionContext.StateMachine.Name; console.log('statemachineName= ' + statemachineName); const taskToken = executionContext.Task.Token; console.log('taskToken= ' + taskToken); const apigwEndpint = event.APIGatewayEndpoint; console.log('apigwEndpint = ' + apigwEndpint) const approveEndpoint = apigwEndpint + "/execution?action=approve&ex=" + executionName + "&sm=" + statemachineName + "&taskToken=" + encodeURIComponent(taskToken); console.log('approveEndpoint= ' + approveEndpoint); const rejectEndpoint = apigwEndpint + "/execution?action=reject&ex=" + executionName + "&sm=" + statemachineName + "&taskToken=" + encodeURIComponent(taskToken); console.log('rejectEndpoint= ' + rejectEndpoint); const emailSnsTopic = "${SNSHumanApprovalEmailTopic}"; console.log('emailSnsTopic= ' + emailSnsTopic); var emailMessage = 'Welcome! \n\n'; emailMessage += 'This is an email requiring an approval for a step functions execution. \n\n' emailMessage += 'Please check the following information and click "Approve" link if you want to approve. \n\n' emailMessage += 'Execution Name -> ' + executionName + '\n\n' emailMessage += 'Approve ' + approveEndpoint + '\n\n' emailMessage += 'Reject ' + rejectEndpoint + '\n\n' emailMessage += 'Thanks for using Step functions!' const sns = new AWS.SNS(); var params = { Message: emailMessage, Subject: "Required approval from AWS Step Functions", TopicArn: emailSnsTopic }; sns.publish(params) .promise() .then(function(data) { console.log("MessageID is " + data.MessageId); callback(null); }).catch( function(err) { console.error(err, err.stack); callback(err); }); } LambdaStateMachineExecutionRole: Type: "AWS::IAM::Role" Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: states.amazonaws.com Action: "sts:AssumeRole" Policies: - PolicyName: InvokeCallbackLambda PolicyDocument: Statement: - Effect: Allow Action: - "lambda:InvokeFunction" Resource: - !Sub "${LambdaHumanApprovalSendEmailFunction.Arn}" LambdaSendEmailExecutionRole: Type: "AWS::IAM::Role" Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: "sts:AssumeRole" Policies: - PolicyName: CloudWatchLogsPolicy PolicyDocument: Statement: - Effect: Allow Action: - "logs:CreateLogGroup" - "logs:CreateLogStream" - "logs:PutLogEvents" Resource: !Sub "arn:${AWS::Partition}:logs:*:*:*" - PolicyName: SNSSendEmailPolicy PolicyDocument: Statement: - Effect: Allow Action: - "SNS:Publish" Resource: - !Sub "${SNSHumanApprovalEmailTopic}" # End state machine that publishes to Lambda and sends an email with the link for approval Outputs: ApiGatewayInvokeURL: Value: !Sub "https://${ExecutionApi}.execute-api.${AWS::Region}.amazonaws.com/states" StateMachineHumanApprovalArn: Value: !Ref HumanApprovalLambdaStateMachine