

Las traducciones son generadas a través de traducción automática. En caso de conflicto entre la traducción y la version original de inglés, prevalecerá la version en inglés.

# Implementación de un flujo de trabajo que espera la aprobación humana en Step Functions
<a name="tutorial-human-approval"></a>

Este tutorial le muestra cómo implementar un proyecto de aprobación humana que le permita a una ejecución de AWS Step Functions detenerse durante una tarea y esperar a que un usuario responda a un correo electrónico. El flujo de trabajo avanza al siguiente estado una vez que el usuario ha aprobado que la tarea continúe. 

La implementación de la pila de CloudFormation incluida en este tutorial creará todos los recursos necesarios, incluidos:
+ Recursos de Amazon API Gateway
+ Funciones de AWS Lambda
+ Una máquina de estado de AWS Step Functions
+ Un tema de correo electrónico de Amazon Simple Notification Service
+ Permisos y roles de AWS Identity and Access Management relacionados

**nota**  
Tendrá que proporcionar una dirección de correo electrónico a la que tenga acceso cuando cree la pila de CloudFormation.

Para obtener más información, consulte [Trabajo con plantillas de CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-guide.html) y el recurso de `[AWS::StepFunctions::StateMachine](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-stepfunctions-statemachine.html)` en la *Guía del usuario de AWS CloudFormation*.

## Paso 1: Creación de una plantilla de CloudFormation
<a name="human-approval-deploy"></a>

1. Copie el código de ejemplo de la sección [CloudFormationCódigo de origen de la plantilla de](#human-approval-yaml).

1. Peque el origen de la plantilla de CloudFormation en un archivo en su máquina local.

   En este ejemplo, el archivo se llama `human-approval.yaml`.

## Paso 2: Crear una pila
<a name="human-approval-create-stack"></a>

1. Inicie sesión en la [consola de CloudFormation](https://console.aws.amazon.com/cloudformation/home).

1. Elija **Crear pila**, y, a continuación, elija **Con nuevos recursos (estándar)**.

1. En la página **Crear pila**, proceda del modo siguiente:

   1. En la sección **Requisitos previos: Preparar plantilla**, asegúrese de que esté seleccionada la opción **La plantilla está lista**.

   1. En la sección **Especificar plantilla**, elija **Cargar un archivo de plantilla** y, a continuación, elija **Elegir archivo** para cargar el archivo `human-approval.yaml` creado anteriormente y que incluye el [código fuente de la plantilla](#human-approval-yaml).

1. Elija **Siguiente**.

1. En la página **Especificar detalles de pila**, haga lo siguiente:

   1. En **Nombre de pila**, escriba un nombre para la pila.

   1. En **Parámetros** introduzca una dirección de correo electrónico válida. Utilizará esta dirección de correo electrónico para suscribirse al tema de Amazon SNS.

1. Elija **Siguiente** y después elija **Siguiente** otra vez.

1. En la página **Revisar**, seleccione **Confirmo que CloudFormation puede crear recursos de IAM.** y, a continuación, elija **Crear**.

   CloudFormation comienza a crear la pila y muestra el estado **CREATE\$1IN\$1PROGRESS**. Cuando el proceso se haya completado, CloudFormation mostrará el estado **CREATE\$1COMPLETE**.

1. (Opcional) Para mostrar los recursos de la pila, seleccione la pila y elija la pestaña **Recursos**.

## Paso 3: Aprobar la suscripción de Amazon SNS
<a name="human-approval-approve-sub"></a>

Una vez que se haya creado el tema de Amazon SNS, recibirá un correo electrónico solicitando que confirme la suscripción.

1. Abra la cuenta de correo electrónico proporcionada cuando creó la pila de CloudFormation.

1. Abra el mensaje **Notificación de AWS: confirmación de suscripción)** de parte de **no-reply@sns.amazonaws.com**

   El correo electrónico incluirá el nombre del recurso de Amazon para el tema de Amazon SNS y un enlace de confirmación.

1. Elija el enlace **Confirmar suscripción**.  
![\[Captura de pantalla ilustrativa de un correo electrónico de confirmación de suscripción.\]](http://docs.aws.amazon.com/es_es/step-functions/latest/dg/images/tutorial-human-approval-sub-conf.png)

## Paso 4: Ejecutar la máquina de estado
<a name="human-approval-run"></a>

1. En la página **HumanApprovalLambdaStateMachine**, seleccione **Iniciar ejecución**.

   Aparece el cuadro de diálogo **Iniciar ejecución**.

1. En el cuadro de diálogo **Iniciar ejecución**, haga lo siguiente:

   1. (Opcional) Introduzca un nombre de ejecución personalizado para anular el valor predeterminado generado.
**Nombres y registros con caracteres no ASCII**  
Step Functions acepta nombres para máquinas de estado, ejecuciones, actividades y etiquetas que contengan caracteres no ASCII. Dado que estos caracteres impedirán que Amazon CloudWatch registre datos, le recomendamos que utilice únicamente caracteres ASCII para poder realizar un seguimiento de las métricas en Step Functions.

   1. En el cuadro **Entrada**, introduzca la siguiente entrada de JSON para ejecutar el flujo de trabajo.

      ```
      {
          "Comment": "Testing the human approval tutorial."
      }
      ```

   1. Seleccione **Iniciar ejecución**.

      La ejecución de la máquina de estado **ApprovalTest** se inicia y se detiene en la tarea **Devolución de llamada de Lambda**.

   1. La consola de Step Functions le dirige a una página cuyo título es su ID de ejecución. Esta página se conoce como *Detalles de la ejecución*. En esta página, puede revisar los resultados de la ejecución a medida que avanza la ejecución o una vez finalizada.

      Para revisar los resultados de la ejecución, elija los estados individuales en la **Vista de gráfico** y, a continuación, elija las pestañas individuales del panel [Detalles del paso](concepts-view-execution-details.md#exec-details-intf-step-details) para ver los detalles de cada estado, incluidas la entrada, la salida y la definición, respectivamente. Para obtener más información sobre la ejecución que puede ver en la página *Detalles de la ejecución*, consulte [Información general sobre los detalles de ejecución](concepts-view-execution-details.md#exec-details-interface-overview).  
![\[Ejecución a la espera de la devolución de llamada\]](http://docs.aws.amazon.com/es_es/step-functions/latest/dg/images/tutorial-human-approval-pause.png)

1. En la cuenta de correo electrónico que utilizó para el tema de Amazon SNS anteriormente, abra el mensaje con el asunto **Aprobación necesaria de AWS Step Functions)**. 

   El mensaje incluye URL separadas para **Aprobar** y **Rechazar**.

1. Elija la URL **Aprobar**.

   El flujo de trabajo continúa basado en su elección.  
![\[Ejecución a la espera de la devolución de llamada\]](http://docs.aws.amazon.com/es_es/step-functions/latest/dg/images/tutorial-human-approval-continue.png)

## CloudFormationCódigo de origen de la plantilla de
<a name="human-approval-yaml"></a>

Utilice esta plantilla de AWS CloudFormation para implementar un ejemplo de un flujo de trabajo de proceso de aprobación humano.

```
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
```