

翻訳は機械翻訳により提供されています。提供された翻訳内容と英語版の間で齟齬、不一致または矛盾がある場合、英語版が優先します。

# Step Functions で人間の承認を待つワークフローのデプロイ
<a name="tutorial-human-approval"></a>

このチュートリアルでは、AWS Step Functions がタスク中に実行を停止し、ユーザーが E メールに返答するまで待機することができる人間による承諾プロジェクトをデプロイする方法を説明します。ユーザーによってタスクの進行が承認されてから、ワークフローは次の状態に進みます。

このチュートリアルに含まれている CloudFormation スタックをデプロイすると、以下を含めてすべての必要なリソースが作成されます。
+ Amazon API Gateway リソース
+ AWS Lambda 関数
+ AWS Step Functions ステートマシン
+ Amazon Simple Notification Service eメールトピック
+ 関連する AWS Identity and Access Management ロールとアクセス権限

**注記**  
CloudFormation スタックを作成するときにアクセスできる有効な E メールアドレスを提供する必要があります。

詳細については、[CloudFormation テンプレートの使用](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-guide.html)と *AWS CloudFormation ユーザーガイド*の `[AWS::StepFunctions::StateMachine](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-stepfunctions-statemachine.html)` リソースを参照してください。

## ステップ 1: CloudFormation テンプレートを作成する
<a name="human-approval-deploy"></a>

1. [CloudFormation テンプレートソースコード](#human-approval-yaml) セクションからサンプルコードをコピーします。

1. CloudFormation テンプレートのソースをローカルマシン上のファイルに貼り付けます。

   この例では、ファイルは `human-approval.yaml` という名前です。

## ステップ 2: スタックを作成する
<a name="human-approval-create-stack"></a>

1. [CloudFormation コンソール](https://console.aws.amazon.com/cloudformation/home) にログインします。

1. **[スタックの作成]** を選択し、**[新しいリソースを使用 (標準)]** を選択します。

1. **[Create stack]** (スタックの作成) ページで、次の手順を実行します。

   1. **[前提条件 - テンプレートの準備]** セクションで、**[テンプレートの準備完了]** が選択されていることを確認します。

   1. **[テンプレートを指定]** セクションで **[テンプレートファイルをアップロード]** を選択し、次に **[ファイルを選択]** を選択し、先ほど作成した、[テンプレートソースコード](#human-approval-yaml)を含む `human-approval.yaml` ファイルをアップロードします。

1. [**次へ**] を選択します。

1. **[スタックの詳細を指定]** ページで、以下を実行します。

   1. **[スタック名]** に、スタックの名前を入力します。

   1. **[パラメータ]** に、有効な E メールアドレスを入力します。この E メールアドレスを使用して、Amazon SNS トピックをサブスクライブします。

1. **[次へ]** を選択し、もう一度 **[次へ]** を選択します。

1. **[Review]** (確認) ページで、**CloudFormation による IAM リソース作成の可能性があることを認める** を選択し、**[Create]** (作成) を選択します。

   CloudFormation はスタックの作成をスタートし、**CREATE\$1IN\$1PROGRESS** のステータスが表示されます。プロセスが完了すると、CloudFormation に **CREATE\$1COMPLETE** ステータスが表示されます。

1. (省略可能) スタックのリソースを表示するには、スタックを選択して **[Resources]** (リソース) タブを選択します。

## ステップ 3: Amazon SNS サブスクリプションを承認する
<a name="human-approval-approve-sub"></a>

Amazon SNS トピックが作成されると、サブスクリプションの確認を求める E メールが届きます。

1. CloudFormation スタックを作成した際に指定した E メールアカウントを開きます。

1. **no-reply@sns.amazonaws.com** からの **AWS 通知を開く - サブスクリプションの確認**メッセージを開きます

   E メールは Amazon SNS トピックの Amazon リソースネームと確認用のリンクを表示します。

1. **[confirm subscription]** (サブスクリプションの確認) リンクを選択します。  
![\[サブスクリプションの確認 E メールのスクリーンショットの例。\]](http://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/images/tutorial-human-approval-sub-conf.png)

## ステップ 4: ステートマシンを実行する
<a name="human-approval-run"></a>

1. **[HumanApprovalLambdaStateMachine]** ページで **[実行を開始]** を選択します。

   **[実行を開始]** ダイアログが表示されます。

1. **[実行を開始]** ダイアログボックスで、以下の操作を行います。

   1. (オプション) 生成されたデフォルトを上書きするカスタム実行名を入力します。
**非 ASCII 名とログ記録**  
Step Functions では、ステートマシン、実行、アクティビティ、ラベルに、ASCII 以外の文字を含む名前を使用できます。このような文字を使用すると Amazon CloudWatch がデータを記録できなくなるため、Step Functions のメトリクスを追跡できるように ASCII 文字のみを使用することをお勧めします。

   1. **[入力]** ボックスに、次の JSON 入力を入力してワークフローを実行します。

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

   1. **[実行のスタート]** を選択します。

      **[ApprovalTest]** ステートマシンの実行を開始し、**[Lambda コールバック]** タスクで一時停止します。

   1. Step Functions コンソールから実行 ID のタイトルが付いたページが表示されます。このページは、*[実行の詳細]* ページと呼ばれます。このページでは、実行の進行中または完了後に実行結果を確認できます。

      実行結果を確認するには、**[グラフビュー]** で個々の状態を選択し、[ステップの詳細](concepts-view-execution-details.md#exec-details-intf-step-details) ペインの個々のタブを選択すると、入力、出力、定義などの各状態の詳細がそれぞれ表示されます。*[実行の詳細]* ページに表示できる実行情報の詳細については、「[実行の詳細の概要](concepts-view-execution-details.md#exec-details-interface-overview)」を参照してください。  
![\[コールバックを待機する実行\]](http://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/images/tutorial-human-approval-pause.png)

1. 以前に Amazon SNS トピックに使用した E メールアカウントで、**[Required approval from AWS Step Functions]** ( からの承認要請) という件名のメッセージを開きます。

   このメッセージには、**[Approve]** (承認) 用と **[Reject]** (拒否) 用に別々の URL が含まれています。

1. **[Approve]** (承認) URL を選択します。

   選択に基づいてワークフローが続行されます。  
![\[コールバックを待機する実行\]](http://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/images/tutorial-human-approval-continue.png)

## CloudFormation テンプレートソースコード
<a name="human-approval-yaml"></a>

この AWS 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 { 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
```