Implantar um fluxo de trabalho que aguarde aprovação humana no Step Functions - AWS Step Functions

Implantar um fluxo de trabalho que aguarde aprovação humana no Step Functions

Este tutorial mostra como implantar um projeto de aprovação humana que permite que uma execução do AWS Step Functions pause durante uma tarefa e aguarde a resposta de um usuário a um e-mail. O fluxo de trabalho segue para o próximo estado depois que o usuário aprova a tarefa para prosseguir.

A implantação da pilha do CloudFormation incluída neste tutorial criará todos os recursos necessários, incluindo:

  • Recursos do Amazon API Gateway

  • Uma função do AWS Lambda

  • Uma máquina de estado do AWS Step Functions

  • Um tópico do e-mail do Amazon Simple Notification Service

  • Funções e permissões relacionadas do AWS Identity and Access Management

nota

Você precisará fornecer um endereço de e-mail válido ao qual você tem acesso quando criar uma pilha do CloudFormation.

Para obter mais informações, consulte Como trabalhar com modelos do CloudFormation e o recurso AWS::StepFunctions::StateMachine no Guia do usuário do AWS CloudFormation.

Etapa 1: Criar um modelo do CloudFormation

  1. Copie o código de exemplo na seção CloudFormationModelo de código-fonte do .

  2. Cole a origem do modelo do CloudFormation em um arquivo na sua máquina local.

    Neste exemplo, o arquivo é chamado de human-approval.yaml.

Etapa 2: Criar uma pilha

  1. Faça login no console do CloudFormation.

  2. Selecione Criar pilha e Com novos recursos (padrão).

  3. Na página Create a stack (Criar uma pilha), faça o seguinte:

    1. Na seção Pré-requisito – Preparar modelo, verifique se a opção O modelo está pronto está selecionada.

    2. Na seção Especificar modelo, escolha Fazer upload de um arquivo de modelo e escolha Escolher arquivo para carregar o human-approval.yaml arquivo que você criou anteriormente e que inclui o código-fonte do modelo.

  4. Escolha Próximo.

  5. Na página Specify stack details (Especificar detalhes da pilha), faça o seguinte:

    1. Em Nome da pilha, informe um nome para sua pilha.

    2. Em Parâmetros, insira um endereço de e-mail válido. Você vai usar esse endereço de e-mail para assinar o tópico do Amazon SNS.

  6. Selecione Próximo e Próximo novamente.

  7. Na página Revisão, escolha Eu reconheço que o CloudFormation pode criar recursos do IAM. e escolha Criar.

    O CloudFormation começa a criar sua pilha e exibe o status CREATE_IN_PROGRESS. Quando o processo é concluído, o CloudFormation exibe o status CREATE_COMPLETE.

  8. (Opcional) Para exibir os recursos em sua pilha, selecione a pilha e escolha a guia Resources.

Etapa 3: Aprovar a assinatura do Amazon SNS

Quando o tópico do Amazon SNS for criado, você receberá um e-mail solicitando a confirmação da assinatura.

  1. Acesse a conta de e-mail que você forneceu quando criou a pilha do CloudFormation.

  2. Abra a mensagem AWS Notification - Subscription Confirmation (Notificação da AWS – confirmação da assinatura) recebida de no-reply@sns.amazonaws.com

    O e-mail listará o Nome do recurso da Amazon para o tópico do Amazon SNS e um link de confirmação.

  3. Clique no link confirm subscription (confirmar assinatura).

    Captura de tela ilustrativa de um e-mail de confirmação de assinatura.

Etapa 4: Executar a máquina de estado

  1. Na página HumanApprovalLambdaStateMachine, selecione Iniciar execução.

    A caixa de diálogo Iniciar execução é exibida.

  2. Na caixa de diálogo Iniciar execução, faça o seguinte:

    1. (Opcional) Insira um nome de execução personalizado para substituir o padrão gerado.

      Nomes e registro em log não ASCII

      O Step Functions aceita nomes de máquina de estado, execuções, atividades e rótulos que contenham caracteres não ASCII. Como esses caracteres não permitem que o Amazon CloudWatch registre dados em log, recomendamos usar somente caracteres ASCII para monitorar as métricas do Step Functions.

    2. Na caixa Entrada, insira a seguinte entrada JSON para executar seu fluxo de trabalho.

      { "Comment": "Testing the human approval tutorial." }
    3. Selecione Iniciar execução.

      A execução da máquina de estado ApprovalTest iniciará e pausará a tarefa Retorno de chamada do Lambda.

    4. O console do Step Functions direciona você para uma página em que o título é o ID da execução. Essa página é conhecida como página de Detalhes da execução. Nesta página, você pode revisar os resultados da execução à medida que a execução avança ou após a conclusão.

      Para revisar os resultados da execução, escolha estados individuais na Exibição em gráfico e, em seguida, escolha as guias individuais no painel Detalhes da etapa para visualizar os detalhes de cada estado, incluindo entrada, saída e definição, respectivamente. Para obter detalhes sobre as informações de execução que você pode visualizar na página Detalhes da execução, consulte Visão geral dos detalhes da execução.

      Execução aguardando chamada de retorno
  3. Na conta de e-mail que você usou anteriormente para o tópico do Amazon SNS, abra a mensagem com o assunto Aprovação necessária de AWS Step Functions.

    A mensagem inclui URLs separados para Approve (Aprovar) e Reject (Rejeitar).

  4. Selecione o URL para Approve (Aprovar).

    O fluxo de trabalho seguirá com base na sua escolha.

    Execução aguardando chamada de retorno

CloudFormationModelo de código-fonte do

Use esse modelo do AWS CloudFormation para implantar um exemplo de um fluxo de trabalho de processo de aprovação humana.

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 { SFN: StepFunctions } = require("@aws-sdk/client-sfn"); 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 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 }) .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: nodejs18.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: "nodejs18.x" Timeout: "25" Code: ZipFile: Fn::Sub: | console.log('Loading function'); const { SNS } = require("@aws-sdk/client-sns"); 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 += '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 SNS(); var params = { Message: emailMessage, Subject: "Required approval from AWS Step Functions", TopicArn: emailSnsTopic }; sns.publish(params) .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