Implantar um projeto de aprovação humana de exemplo - AWS Step Functions

As traduções são geradas por tradução automática. Em caso de conflito entre o conteúdo da tradução e da versão original em inglês, a versão em inglês prevalecerá.

Implantar um projeto de aprovação humana de exemplo

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 AWS CloudFormation pilha incluída neste tutorial criará todos os recursos necessários, incluindo:

  • Recursos do Amazon API Gateway

  • E AWS Lambda funções

  • Uma máquina de AWS Step Functions estado

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

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

nota

Você precisará fornecer um endereço de e-mail válido ao qual tenha acesso ao criar a AWS CloudFormation pilha.

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

Etapa 1: criar um AWS CloudFormation modelo

  1. Copie o código de exemplo na seção AWS CloudFormation Código-fonte do modelo.

  2. Cole a fonte do AWS CloudFormation modelo 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 AWS 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 de revisão, escolha Eu reconheço que isso AWS CloudFormation pode criar recursos do IAM e, em seguida, escolha Criar.

    AWS CloudFormation começa a criar sua pilha e exibe o status CREATE_IN_PROGRESS. Quando o processo estiver concluído, AWS CloudFormation exibirá o status CREATE_COMPLETE.

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

    Display resources (Exibir recursos)

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. Abra a conta de e-mail que você forneceu ao criar a AWS CloudFormation pilha.

  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).

    Assinatura confirmada!

Etapa 4: Executar a máquina de estado

  1. Na HumanApprovalLambdaStateMachinepágina, escolha 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) Para identificar a execução, insira um nome para ela no campo Nome. Por padrão, o Step Functions cria automaticamente um nome de execução exclusivo.

      nota

      Step Functions permite criar nomes para máquinas de estado, execuções e atividades, além de rótulos que contenham caracteres não ASCII. Esses nomes não ASCII não funcionam com a Amazon. CloudWatch Para garantir que você possa acompanhar CloudWatch as métricas, escolha um nome que use somente caracteres ASCII.

    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 ApprovalTestestado é iniciada e pausada na tarefa Lambda Callback.

    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 Página de Detalhes da execução — Visão geral da interface.

      Execução aguardando chamada de retorno
  3. Na conta de e-mail que você usou para o tópico do Amazon SNS anteriormente, abra a mensagem com o assunto Aprovação obrigató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

AWS CloudFormation Código-fonte do modelo

Use esse AWS CloudFormation modelo para implantar um exemplo de fluxo de trabalho do 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 += '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 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