

Terjemahan disediakan oleh mesin penerjemah. Jika konten terjemahan yang diberikan bertentangan dengan versi bahasa Inggris aslinya, utamakan versi bahasa Inggris.

# Menerapkan alur kerja yang menunggu persetujuan manusia di Step Functions
<a name="tutorial-human-approval"></a>

Tutorial ini menunjukkan cara men-deploy proyek persetujuan manusia yang memungkinkan eksekusi AWS Step Functions untuk dijeda selama tugas, dan menunggu pengguna untuk merespons email. Alur kerja berlanjut ke status berikutnya setelah pengguna menyetujui tugas untuk melanjutkan. 

Menyebarkan CloudFormation tumpukan yang disertakan dalam tutorial ini akan membuat semua sumber daya yang diperlukan, termasuk:
+ Sumber daya Amazon API Gateway
+ Sebuah AWS Lambda fungsi
+ Mesin AWS Step Functions negara
+ Topik email Amazon Simple Notification Service
+  AWS Identity and Access Management Peran dan izin terkait

**catatan**  
Anda harus memberikan alamat email yang valid yang dapat Anda akses saat membuat CloudFormation tumpukan.

Untuk informasi selengkapnya, lihat [Bekerja dengan CloudFormation Template](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-guide.html) dan `[AWS::StepFunctions::StateMachine](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-stepfunctions-statemachine.html)` sumber daya di *Panduan AWS CloudFormation Pengguna*.

## Langkah 1: Buat CloudFormation template
<a name="human-approval-deploy"></a>

1. Salin kode contoh dari bagian [CloudFormation Kode Sumber Template](#human-approval-yaml).

1. Tempelkan sumber CloudFormation template ke dalam file di mesin lokal Anda.

   Untuk contoh ini file disebut `human-approval.yaml`.

## Langkah 2: Buat tumpukan
<a name="human-approval-create-stack"></a>

1. Masuk ke [Konsol CloudFormation](https://console.aws.amazon.com/cloudformation/home).

1. Pilih **Buat Tumpukan**, lalu pilih **Dengan sumber daya baru (standar)**.

1. Pada halaman **Buat tumpukan**, lakukan hal berikut:

   1. Di bagian **Prasyarat - Siapkan templat**, pastikan **Template sudah siap dipilih**.

   1. Di bagian **Tentukan templat**, pilih **Unggah file templat** lalu pilih **Pilih file** untuk mengunggah `human-approval.yaml` file yang Anda buat sebelumnya yang menyertakan [kode sumber templat](#human-approval-yaml).

1. Pilih **Berikutnya**.

1. Pada halaman **Tentukan detail tumpukan**, lakukan hal berikut:

   1. Untuk **nama Stack**, masukkan nama untuk tumpukan Anda.

   1. Di bawah **Parameter**, masukkan alamat email yang valid. Anda akan menggunakan alamat email ini untuk berlangganan topik Amazon SNS.

1. Pilih **Berikutnya**, lalu pilih **Berikutnya** lagi.

1. **Pada halaman **Tinjauan**, pilih **Saya mengakui yang CloudFormation mungkin membuat sumber daya IAM** dan kemudian pilih Buat.**

   CloudFormation mulai membuat tumpukan Anda dan menampilkan status **CREATE\$1IN\$1PROGRESS**. Ketika proses selesai, CloudFormation menampilkan status **CREATE\$1COMPLETE**.

1. (Opsional) Untuk menampilkan sumber daya di tumpukan Anda, pilih tumpukan dan pilih tab **Sumber Daya**.

## Langkah 3: Menyetujui langganan Amazon SNS
<a name="human-approval-approve-sub"></a>

Setelah topik Amazon SNS dibuat, Anda akan menerima email yang meminta Anda mengonfirmasi langganan.

1. Buka akun email yang Anda berikan saat Anda membuat CloudFormation tumpukan.

1. Buka pesan **Notifikasi AWS - Konfirmasi Langganan** dari **no-reply@sns.amazonaws.com**

   Email akan mencantumkan Amazon Resource Name untuk topik Amazon SNS, dan tautan konfirmasi.

1. Pilih tautan **konfirmasikan langganan**.  
![\[Tangkapan layar ilustratif dari email konfirmasi berlangganan.\]](http://docs.aws.amazon.com/id_id/step-functions/latest/dg/images/tutorial-human-approval-sub-conf.png)

## Langkah 4: Jalankan mesin negara
<a name="human-approval-run"></a>

1. Pada **HumanApprovalLambdaStateMachine**halaman, pilih **Mulai eksekusi**.

   Kotak dialog **Mulai eksekusi** ditampilkan.

1. Dalam kotak dialog **Mulai eksekusi**, lakukan hal berikut:

   1. (Opsional) Masukkan nama eksekusi khusus untuk mengganti default yang dihasilkan.
**Nama dan pencatatan non-ASCII**  
Step Functions menerima nama untuk mesin negara, eksekusi, aktivitas, dan label yang berisi karakter non-ASCII. Karena karakter seperti itu akan CloudWatch mencegah Amazon mencatat data, sebaiknya gunakan hanya karakter ASCII sehingga Anda dapat melacak metrik Step Functions.

   1. Dalam kotak **Input, masukkan input** JSON berikut untuk menjalankan alur kerja Anda.

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

   1. Pilih **Mulai Eksekusi**.

      Eksekusi mesin **ApprovalTest**status dimulai, dan berhenti pada tugas **Lambda** Callback.

   1. Konsol Step Functions mengarahkan Anda ke halaman yang berjudul dengan ID eksekusi Anda. Halaman ini dikenal sebagai halaman *Detail Eksekusi*. Di halaman ini, Anda dapat meninjau hasil eksekusi saat eksekusi berlangsung atau setelah selesai.

      Untuk meninjau hasil eksekusi, pilih status individual pada **tampilan Grafik**, lalu pilih tab individual di [Detail langkah](concepts-view-execution-details.md#exec-details-intf-step-details) panel untuk melihat detail setiap status termasuk input, output, dan definisi masing-masing. Untuk detail tentang informasi eksekusi yang dapat Anda lihat di halaman *Rincian Eksekusi*, lihat[Ikhtisar detail eksekusi](concepts-view-execution-details.md#exec-details-interface-overview).  
![\[Eksekusi menunggu callback\]](http://docs.aws.amazon.com/id_id/step-functions/latest/dg/images/tutorial-human-approval-pause.png)

1. Di akun email yang Anda gunakan untuk topik Amazon SNS sebelumnya, buka pesan dengan subjek **Persetujuan yang diperlukan** dari. AWS Step Functions

   Pesan termasuk terpisah URLs untuk **Menyetujui** dan **Menolak**.

1. Pilih **Setujui** URL.

   Alur kerja berlanjut berdasarkan pilihan Anda.  
![\[Eksekusi menunggu callback\]](http://docs.aws.amazon.com/id_id/step-functions/latest/dg/images/tutorial-human-approval-continue.png)

## CloudFormation Kode Sumber Template
<a name="human-approval-yaml"></a>

Gunakan AWS CloudFormation template ini untuk menyebarkan contoh alur kerja proses persetujuan manusia.

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