NRIネットコム Blog

NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

AWS Step Function承認フローをAWS Step Functionsのワークフローから呼び出して多段階承認フローを作成する方法(AWS Systems Manager Automation編)

小西秀和です。
以前書いた次の記事でAWS Systems Manager Automationの承認アクションを使用してAWS Step Functionsのワークフローへ承認フローを追加する方法を試してみました。

今回はこのAWS Step Functionsの承認フローをコンポーネント化し、別のAWS Step Functionsのワークフローから呼び出して多段階承認フローを作成する方法を試してみたいと思います。

※本記事および当執筆者のその他の記事で掲載されているソースコードは自主研究活動の一貫として作成したものであり、動作を保証するものではありません。使用する場合は自己責任でお願い致します。また、予告なく修正することもありますのでご了承ください。

今回の記事の内容は次のような構成になっています。

本記事で試す構成図

今回試す構成は次のようにコンポーネント化したAWS Step Functions承認フローを3回使用する3段階の承認フローとなります。

AWS Step FunctionsへAWS LambdaとAWS Systems Manager Automationで多段階承認フローを作成する構成例
AWS Step FunctionsへAWS LambdaとAWS Systems Manager Automationで多段階承認フローを作成する構成例

コンポーネント化したAWS Step Functions承認フロー

コンポーネント化したAWS Step Functions承認フローは次の記事で紹介したものです。

参考: AWS Step Functionsのワークフローへ承認フローを追加する方法(AWS Systems Manager Automation編)

上記の記事でも紹介したように、AWS CloudFormationテンプレートを更新する場合の考慮事項として、親SSM Document、子SSM Documentの定義を更新する場合にAWS CloudFormationの仕様上、SSM Document名を変更して再作成する必要があることが挙げられます

AWS Step FunctionsへAWS LambdaとAWS Systems Manager Automationで承認フローを追加する構成例
AWS Step FunctionsへAWS LambdaとAWS Systems Manager Automationで承認フローを追加する構成例

AWS CloudFormationテンプレートとパラメータの例

AWS CloudFormationテンプレート(AWS Step FunctionsへのAWS LambdaとAWS Systems Manager Automationによる多段階承認フローの追加)

入力パラメータ例

Level1EmailForNotification: sample1@h-o2k.com #承認リクエストを送信するメールアドレス(1段階目承認用)
Level1SsmApprovers: arn:aws:iam::XXXXXXXXXXXX:role/ho2k.com #承認リクエストに対して承認、拒否を決定するIAMロールまたはIAMユーザー(1段階目承認用)
Level1SsmMinRequiredApprovals: 1 #承認に必要な人数。ここに記載の人数が承認して初めて処理として承認される。(1段階目承認用)
Level2EmailForNotification: sample2@h-o2k.com #承認リクエストを送信するメールアドレス(2段階目承認用)
Level2SsmApprovers: arn:aws:iam::XXXXXXXXXXXX:role/ho2k.com #承認リクエストに対して承認、拒否を決定するIAMロールまたはIAMユーザー(2段階目承認用)
Level2SsmMinRequiredApprovals: 1 #承認に必要な人数。ここに記載の人数が承認して初めて処理として承認される。(2段階目承認用)
Level3EmailForNotification: sample3@h-o2k.com #承認リクエストを送信するメールアドレス(3段階目承認用)
Level3SsmApprovers: arn:aws:iam::XXXXXXXXXXXX:role/ho2k.com #承認リクエストに対して承認、拒否を決定するIAMロールまたはIAMユーザー(3段階目承認用)
Level3SsmMinRequiredApprovals: 1 #承認に必要な人数。ここに記載の人数が承認して初めて処理として承認される。(3段階目承認用)  
SsmAutomationAssumeRoleName: SsmAutomationAssumeRole #作成するAutomationAssumeRoleの名称(SSM Automationの実行にこのロールを使用する)
SsmChildDocumentForApprovalActionName: SsmChildDocumentForApprovalAction #親SSM Documentの名称
SsmChildDocumentForApprovalActionVersionName: 1  #親SSM Documentのバージョン名 
SsmParentDocumentApprovalActionName: SsmParentDocumentApprovalAction #子SSM Documentの名称
SsmParentDocumentApprovalActionVersionName: 1 #子SSM Documentのバージョン名

テンプレート本体

ファイル名:SfnApprovalCFnSfnWithMultiLevelSsmApproval.yml

実装で注意するべき点としてはコンポーネント化したAWS Step Functionsを呼び出すAWS Step FunctionsのIAMロールのポリシー権限が挙げられます。
AWS Step FunctionsからAWS Step Functionsを呼び出す場合のResource指定には次の3つがあります。

  • arn:aws:states:::states:startExecution:非同期
  • arn:aws:states:::states:startExecution.sync:同期(Outputが文字列)
  • arn:aws:states:::states:startExecution.sync:2:同期(OutputがJSON)

これらの非同期と同期では必要なIAMロールのポリシー権限が異なるため、その点に注意が必要です。
今回は承認フローを同期的に処理し、OutputをJSON形式で受け取って処理するのでstartExecution.sync:2を使用して必要なIAMロールのポリシー権限を設定しています。

また、AWS Step FunctionsからAWS Step Functionsを呼び出す場合のInputには次のパラメータを指定します。
"AWS_STEP_FUNCTIONS_STARTED_BY_EXECUTION_ID.$": "$$.Execution.Id"

<参考>
Manage AWS Step Functions Executions as an Integrated Service - AWS Step Functions
IAM Policies for integrated services:AWS Step Functions - AWS Step Functions

AWSTemplateFormatVersion: '2010-09-09'
Description: 'Add AWS Systems Manager Automation Approval Action to AWS Step Functions.'
Parameters:
  SsmAutomationAssumeRoleName:
    Type: String
    Default: "SsmAutomationAssumeRole"
  SsmParentDocumentApprovalActionName:
    Type: String
    Default: "SsmParentDocumentApprovalAction"
  SsmChildDocumentForApprovalActionName:
    Type: String
    Default: "SsmChildDocumentForApprovalAction"
  SsmParentDocumentApprovalActionVersionName:
    Type: String
    Default: "1"
  SsmChildDocumentForApprovalActionVersionName:
    Type: String
    Default: "1"
  Level1SsmApprovers:
    Type: String
    Default: "arn:aws:iam::XXXXXXXXXXXX:role/ho2k.com"
  Level1SsmMinRequiredApprovals:
    Type: String
    Default: "1"
  Level1EmailForNotification: 
    Type: String
    Default: "sample1@h-o2k.com"
  Level2SsmApprovers:
    Type: String
    Default: "arn:aws:iam::XXXXXXXXXXXX:role/ho2k.com"
  Level2SsmMinRequiredApprovals:
    Type: String
    Default: "1"
  Level2EmailForNotification: 
    Type: String
    Default: "sample2@h-o2k.com"
  Level3SsmApprovers:
    Type: String
    Default: "arn:aws:iam::XXXXXXXXXXXX:role/ho2k.com"
  Level3SsmMinRequiredApprovals:
    Type: String
    Default: "1"
  Level3EmailForNotification: 
    Type: String
    Default: "sample3@h-o2k.com"
Resources:
  SsmAutomationAssumeRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub '${SsmAutomationAssumeRoleName}-${AWS::Region}'
      Path: /
      MaxSessionDuration: 43200
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ssm.amazonaws.com
                - lambda.amazonaws.com
                - edgelambda.amazonaws.com
                - events.amazonaws.com
                - scheduler.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: !Sub 'IAMPolicy-AdditionalPolicyForAutomationRole-${AWS::Region}'
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
            - Effect: Allow
              Action:
                - iam:PassRole
              Resource:
                - !Sub 'arn:aws:iam::${AWS::AccountId}:role/*'
            - Effect: Allow
              Action:
                - logs:CreateLogGroup
              Resource:
                - 'arn:aws:logs:*:*:*'
            - Effect: Allow
              Action:
                - logs:CreateLogStream
                - logs:PutLogEvents
              Resource:
                - !Sub 'arn:aws:logs:*:*:log-group:/aws/*/*:*'
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/service-role/AmazonSSMAutomationRole'

  LambdaForSsmStartAutomationExecution:
    Type: AWS::Lambda::Function
    DependsOn: 
      - SsmAutomationAssumeRole
    Properties:
      FunctionName: LambdaForSsmStartAutomationExecution
      Description : 'LambdaForSsmStartAutomationExecution'
      Runtime: python3.9
      MemorySize: 10240
      Timeout: 900
      Role: !GetAtt SsmAutomationAssumeRole.Arn
      Handler: index.lambda_handler
      Code:
        ZipFile: |
          import botocore
          import boto3
          import json
          import os
          import sys

          region = os.environ.get('AWS_REGION')
          sts_client = boto3.client("sts", region_name=region)
          account_id = sts_client.get_caller_identity()["Account"]

          ssm_client = boto3.client('ssm', region_name=region)

          def lambda_handler(event, context):
              print(("Received event: " + json.dumps(event, indent=2)))

              try:
                  ssm_auto_resp = ssm_client.start_automation_execution(
                      DocumentName=event['ssm_parent_doc_name'],
                      Parameters={
                          'DocumentName': [event['ssm_child_doc_name']],
                          'AutomationAssumeRole': [event['ssm_automation_assume_role']],
                          'Description': [event['ssm_description']],
                          'Message': [event['ssm_message']],
                          'NotificationArn': [event['ssm_notification_arn']],
                          'Approvers': [event['ssm_approvers']],
                          'MinRequiredApprovals': [event['ssm_min_required_approvals']],
                          'LambdaNameForApproved': [event['ssm_lambda_for_approved']],
                          'LambdaParametersForApproved': [json.dumps({
                              'result': event['ssm_lambda_parameters_for_approved'],
                              'token': event['token']
                          })],
                          'LambdaNameForRejected': [event['ssm_lambda_for_reject']],
                          'LambdaParametersForRejected': [json.dumps({
                              'result': event['ssm_lambda_parameters_for_reject'],
                              'token': event['token']
                          })]
                      },
                      Mode='Auto'
                  )
              except Exception as ex:
                  print(f'Exception:{ex}')
                  tb = sys.exc_info()[2]
                  print(f'ssm_client start_automation_execution FAIL. Exception:{str(ex.with_traceback(tb))}')
                  raise

              result = {}
              result['params'] = event.copy()
              return result

  LambdaForReceivingAutomationResult:
    Type: AWS::Lambda::Function
    DependsOn: 
      - AutomationResultReceivedByLambdaRole
    Properties:
      FunctionName: AutomationResultReceivedByLambda
      Description : 'LambdaForReceivingAutomationResult'
      Runtime: python3.9
      MemorySize: 10240
      Timeout: 900
      Role: !GetAtt AutomationResultReceivedByLambdaRole.Arn
      Handler: index.lambda_handler
      Code:
        ZipFile: |
          import botocore
          import boto3
          import json
          import os
          import sys

          region = os.environ.get('AWS_REGION')
          sts_client = boto3.client("sts", region_name=region)
          account_id = sts_client.get_caller_identity()["Account"]

          sns_client = boto3.client('sns', region_name=region)
          sfn_client = boto3.client('stepfunctions', region_name=region)

          def lambda_handler(event, context):
              print(("Received event: " + json.dumps(event, indent=2)))

              if event.get('result','') == 'Approved': 
                  is_approved = True
              else:
                  is_approved = False

              try:
                  #コールバックしたトークンでSFN側にタスクの成功を送信する。
                  sfn_res = sfn_client.send_task_success(
                      taskToken=event['token'],
                      output=json.dumps({'is_approved':is_approved})
                  )
              except Exception as ex:
                  print(f'Exception:{ex}')
                  tb = sys.exc_info()[2]
                  print(f'sfn_client send_task_success FAIL. Exception:{str(ex.with_traceback(tb))}')
                  raise

              return {'is_approved':is_approved}

  AutomationResultReceivedByLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub 'IAMRole-LambdaForReceivingAutomationResult-${AWS::Region}'
      Path: /
      MaxSessionDuration: 43200
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - edgelambda.amazonaws.com
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: !Sub 'IAMPolicy-LambdaForReceivingAutomationResult-${AWS::Region}'
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
            - Effect: Allow
              Action:
                - logs:CreateLogGroup
              Resource:
                - 'arn:aws:logs:*:*:*'
            - Effect: Allow
              Action:
                - logs:CreateLogStream
                - logs:PutLogEvents
              Resource:
                - !Sub 'arn:aws:logs:*:*:log-group:/aws/lambda/AutomationResultReceivedByLambda:*'
            - Effect: Allow
              Action:
                - lambda:InvokeFunction
              Resource:
                - !Sub 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:Automation*'
            - Effect: Allow
              Action:
                - states:ListActivities
                - states:ListExecutions
                - states:ListStateMachines
                - states:DescribeActivity
                - states:DescribeExecution
                - states:DescribeStateMachine
                - states:DescribeStateMachineForExecution
                - states:GetExecutionHistory
                - states:SendTaskSuccess
              Resource:
                - '*'

  SsmParentDocumentApprovalAction:
    Type: AWS::SSM::Document
    Properties: 
      Name: !Ref SsmParentDocumentApprovalActionName
      DocumentType: Automation
      VersionName: !Ref SsmParentDocumentApprovalActionVersionName
      DocumentFormat: YAML
      Content: 
        description: 'SsmParentDocumentApprovalAction'
        schemaVersion: '0.3'
        assumeRole: "{{ AutomationAssumeRole }}"
        parameters:
          AutomationAssumeRole:
            type: String
            description: "(Optional) The ARN of the role that allows Automation to perform the actions on your behalf."
            default: ''
          DocumentName:
            description: 'Document Name'
            type: String
            default: SsmChildDocumentForApprovalAction
          Description:
            description: 'Operation Description'
            type: String
            default: SsmChildDocumentForApprovalAction
          Message:
            description: Message
            type: String
            default: 'Please Approve after Confirmation.'
          NotificationArn:
            description: 'Amazon SNS Topic ARN for Approval Notification.'
            type: String
            default: 'arn:aws:sns:ap-northeast-1:000000000000:AutomationApprovalNotification'
          LambdaNameForApproved:
            description: 'Lambda Function Name to Invoke if Approved.'
            type: String
            default: AutomationResultReceivedByLambda
          LambdaParametersForApproved:
            description: 'Lambda Function Parameters to Invoke if Approved.'
            type: String
            default: "{\"result\":\"Approved\",\"token\":\"XXXXXXXXXXXX\"}"
          LambdaNameForRejected:
            description: 'Lambda Function Name to Invoke if Rejected.'
            type: String
            default: AutomationResultReceivedByLambda
          LambdaParametersForRejected:
            description: 'Lambda Function Parameters to Invoke if Rejected.'
            type: String
            default: "{\"result\":\"Rejected\",\"token\":\"XXXXXXXXXXXX\"}"
          Approvers:
            description: 'The IAM User or IAM Role of the Approver.'
            type: StringList
          MinRequiredApprovals:
            description: MinRequiredApprovals
            type: Integer
            default: 1
        mainSteps:
          - name: ParentExecuteAutomation
            action: 'aws:executeAutomation'
            timeoutSeconds: 43200
            onFailure: 'step:LambdaNameForRejected'
            inputs:
              DocumentName: '{{DocumentName}}'
              RuntimeParameters:
                AutomationAssumeRole: '{{AutomationAssumeRole}}'
                Description: '{{Description}}'
                Message: '{{Message}}'
                NotificationArn: '{{NotificationArn}}'
                LambdaNameForApproved: '{{LambdaNameForApproved}}'
                LambdaParametersForApproved: '{{LambdaParametersForApproved}}'
                LambdaNameForRejected: '{{LambdaNameForRejected}}'
                LambdaParametersForRejected: '{{LambdaParametersForRejected}}'
                Approvers: '{{Approvers}}'
                MinRequiredApprovals: '{{MinRequiredApprovals}}'
            isEnd: true
          - name: LambdaNameForRejected
            action: 'aws:invokeLambdaFunction'
            maxAttempts: 3
            timeoutSeconds: 120
            onFailure: Abort
            inputs:
              FunctionName: '{{LambdaNameForRejected}}'
              Payload: '{{LambdaParametersForRejected}}'
            isEnd: true

  SsmChildDocumentForApprovalAction:
    Type: AWS::SSM::Document
    Properties: 
      Name: !Ref SsmChildDocumentForApprovalActionName
      DocumentType: Automation
      VersionName: !Ref SsmChildDocumentForApprovalActionVersionName
      DocumentFormat: YAML
      Content: 
        description: 'SsmChildDocumentForApprovalAction'
        schemaVersion: '0.3'
        assumeRole: "{{ AutomationAssumeRole }}"
        parameters:
          AutomationAssumeRole:
            type: String
            description: "(Optional) The ARN of the role that allows Automation to perform the actions on your behalf."
            default: ''
          Description:
            description: 'Operation Description'
            type: String
            default: 'SsmChildDocumentForApprovalAction'
          Message:
            description: Message
            type: String
            default: 'Please Approve after Confirmation.'
          NotificationArn:
            description: 'Amazon SNS Topic ARN for Approval Notification.'
            type: String
            default: 'arn:aws:sns:ap-northeast-1:000000000000:AutomationApprovalNotification'
          LambdaNameForApproved:
            description: 'Lambda Function Name to Invoke if Approved.'
            type: String
            default: AutomationResultReceivedByLambda
          LambdaParametersForApproved:
            description: 'Lambda Function Parameters to Invoke if Approved.'
            type: String
            default: "{\"result\":\"Approved\",\"token\":\"XXXXXXXXXXXX\"}"
          LambdaNameForRejected:
            description: 'Lambda Function Name to Invoke if Rejected.'
            type: String
            default: AutomationResultReceivedByLambda
          LambdaParametersForRejected:
            description: 'Lambda Function Parameters to Invoke if Rejected.'
            type: String
            default: "{\"result\":\"Rejected\",\"token\":\"XXXXXXXXXXXX\"}"
          Approvers:
            description: 'The IAM User or IAM Role of the Approver.'
            type: StringList
          MinRequiredApprovals:
            description: MinRequiredApprovals
            type: Integer
            default: 1
        mainSteps:
          - name: ApprovalAction
            action: 'aws:approve'
            timeoutSeconds: 43200
            onFailure: 'step:LambdaNameForRejected'
            inputs:
              Message: '{{Message}}'
              NotificationArn: '{{NotificationArn}}'
              Approvers: '{{Approvers}}'
              MinRequiredApprovals: '{{MinRequiredApprovals}}'
          - name: LambdaNameForApproved
            action: 'aws:invokeLambdaFunction'
            maxAttempts: 3
            timeoutSeconds: 120
            onFailure: Abort
            inputs:
              FunctionName: '{{LambdaNameForApproved}}'
              Payload: '{{LambdaParametersForApproved}}'
            isEnd: true
          - name: LambdaNameForRejected
            action: 'aws:invokeLambdaFunction'
            maxAttempts: 3
            timeoutSeconds: 120
            onFailure: Abort
            inputs:
              FunctionName: '{{LambdaNameForRejected}}'
              Payload: '{{LambdaParametersForRejected}}'
            isEnd: true

  SnsAutomationApprovalNotificationLevel1:
    Type: AWS::SNS::Topic
    Properties: 
      TopicName: AutomationApprovalNotificationLevel1
      DisplayName: AutomationApprovalNotificationLevel1
      FifoTopic: False
      Subscription: 
        - Endpoint: !Ref Level1EmailForNotification
          Protocol: email
  SnsAutomationApprovalNotificationLevel2:
    Type: AWS::SNS::Topic
    Properties: 
      TopicName: AutomationApprovalNotificationLevel2
      DisplayName: AutomationApprovalNotificationLevel2
      FifoTopic: False
      Subscription: 
        - Endpoint: !Ref Level2EmailForNotification
          Protocol: email
  SnsAutomationApprovalNotificationLevel3:
    Type: AWS::SNS::Topic
    Properties: 
      TopicName: AutomationApprovalNotificationLevel3
      DisplayName: AutomationApprovalNotificationLevel3
      FifoTopic: False
      Subscription: 
        - Endpoint: !Ref Level3EmailForNotification
          Protocol: email

  StepFunctionsWithSsmAutomationApproval: 
    Type: AWS::StepFunctions::StateMachine
    DependsOn: 
      - LambdaForSsmStartAutomationExecution
      - LambdaForReceivingAutomationResult
      - StepFunctionsWithSsmAutomationApprovalRole
      - StepFunctionsWithSsmAutomationApprovalLogGroup
    Properties: 
      StateMachineName: StepFunctionsWithSsmAutomationApproval
      StateMachineType: STANDARD
      RoleArn: !GetAtt StepFunctionsWithSsmAutomationApprovalRole.Arn
      LoggingConfiguration: 
        Level: ALL
        IncludeExecutionData: true
        Destinations: 
          - CloudWatchLogsLogGroup:
              LogGroupArn: !GetAtt StepFunctionsWithSsmAutomationApprovalLogGroup.Arn
      DefinitionString: !Sub |-
        {
            "Comment": "Sample of adding an Approval flow to AWS Step Functions.",
            "TimeoutSeconds": 43200, 
            "StartAt": "InvokeLambdaForSsmStartAutomationExecution",
            "States": {
              "InvokeLambdaForSsmStartAutomationExecution": {
                "Type": "Task",
                "Resource": "arn:aws:states:::lambda:invoke.waitForTaskToken",
                "Parameters": {
                  "FunctionName": "${LambdaForSsmStartAutomationExecution.Arn}:$LATEST",
                  "Payload": {
                    "step.$": "$$.State.Name",
                    "token.$": "$$.Task.Token",
                    "ssm_parent_doc_name.$": "$$.Execution.Input.ssm_parent_doc_name",
                    "ssm_automation_assume_role.$": "$$.Execution.Input.ssm_automation_assume_role", 
                    "ssm_child_doc_name.$": "$$.Execution.Input.ssm_child_doc_name",
                    "ssm_description.$": "$$.Execution.Input.ssm_description",
                    "ssm_lambda_for_approved.$": "$$.Execution.Input.ssm_lambda_for_approved",
                    "ssm_lambda_parameters_for_approved.$": "$$.Execution.Input.ssm_lambda_parameters_for_approved",
                    "ssm_lambda_for_reject.$": "$$.Execution.Input.ssm_lambda_for_reject",
                    "ssm_lambda_parameters_for_reject.$": "$$.Execution.Input.ssm_lambda_parameters_for_reject",
                    "ssm_notification_arn.$": "$$.Execution.Input.ssm_notification_arn",
                    "ssm_approvers.$": "$$.Execution.Input.ssm_approvers",
                    "ssm_min_required_approvals.$": "$$.Execution.Input.ssm_min_required_approvals",
                    "ssm_message.$": "States.Format('Approval request has been received. Please review file {} at the following URL to decide whether to approve or deny. URL: {}', $$.Execution.Input.confirmation_file, $$.Execution.Input.confirmation_url)"
                  }
                },
                "Retry": [
                  {
                    "ErrorEquals": [
                      "Lambda.ServiceException",
                      "Lambda.AWSLambdaException",
                      "Lambda.SdkClientException",
                      "Lambda.TooManyRequestsException"
                    ],
                    "IntervalSeconds": 2,
                    "MaxAttempts": 6,
                    "BackoffRate": 2
                  }
                ],
                "Catch": [
                    {
                      "ErrorEquals": [
                        "States.ALL"
                      ],
                      "Next": "Fail"
                    }
                ],
                "Next": "ApprovalResult"
              },
              "ApprovalResult": {
                "Type": "Choice",
                "Choices": [
                  {
                    "Variable": "$.is_approved",
                    "BooleanEquals": true,
                    "Next": "Approved"
                  },
                  {
                    "Variable": "$.is_approved",
                    "BooleanEquals": false,
                    "Next": "Rejected"
                  }
                ],
                "Default": "Rejected"
              },
              "Approved": {
                "Type": "Succeed"
              },
              "Rejected": {
                "Type": "Succeed"
              },
              "Fail": {
                "Type": "Fail"
              }
            }
          }

  StepFunctionsWithSsmAutomationApprovalRole:
    Type: AWS::IAM::Role
    DependsOn: 
      - LambdaForSsmStartAutomationExecution
      - LambdaForReceivingAutomationResult
    Properties:
      RoleName: !Sub 'IAMRole-StepFunctionsWithSsmAutomationApproval-${AWS::Region}'
      Path: /
      MaxSessionDuration: 43200
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - states.amazonaws.com
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
      - PolicyName: !Sub 'IAMPolicy-StepFunctionsWithSsmAutomationApproval-${AWS::Region}'
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
              - lambda:InvokeFunction
            Resource:
              - !Sub '${LambdaForSsmStartAutomationExecution.Arn}:*'
              - !Sub '${LambdaForReceivingAutomationResult.Arn}:*'
          - Effect: Allow
            Action:
              - lambda:InvokeFunction
            Resource:
              - !Sub '${LambdaForSsmStartAutomationExecution.Arn}'
              - !Sub '${LambdaForReceivingAutomationResult.Arn}'
      - PolicyName: CloudWatchLogsDeliveryFullAccessPolicy
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
              - logs:DescribeResourcePolicies
              - logs:DescribeLogGroups
              - logs:GetLogDelivery
              - logs:CreateLogDelivery
              - logs:DeleteLogDelivery
              - logs:UpdateLogDelivery
              - logs:ListLogDeliveries
              - logs:PutResourcePolicy
            Resource:
              - '*'
      - PolicyName: XRayAccessPolicy
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
              - xray:PutTraceSegments
              - xray:PutTelemetryRecords
              - xray:GetSamplingRules
              - xray:GetSamplingTargets
            Resource:
              - '*'
  StepFunctionsWithSsmAutomationApprovalLogGroup:
    Type: AWS::Logs::LogGroup
    Properties: 
      LogGroupName: /aws/vendedlogs/states/Logs-StepFunctionsWithSsmAutomationApproval

  StepFunctionsCallerForApprovalFlow: 
    Type: AWS::StepFunctions::StateMachine
    DependsOn: 
      - StepFunctionsWithSsmAutomationApproval
      - StepFunctionsCallerForApprovalFlowRole
      - StepFunctionsCallerForApprovalFlowLogGroup
    Properties: 
      StateMachineName: StepFunctionsCallerForApprovalFlow
      StateMachineType: STANDARD
      RoleArn: !GetAtt StepFunctionsCallerForApprovalFlowRole.Arn
      LoggingConfiguration: 
        Level: ALL
        IncludeExecutionData: true
        Destinations: 
          - CloudWatchLogsLogGroup:
              LogGroupArn: !GetAtt StepFunctionsCallerForApprovalFlowLogGroup.Arn
      DefinitionString: !Sub |-
        {
            "Comment": "Sample of Upper-Level Caller for Approval Flow.",
            "TimeoutSeconds": 129600, 
            "StartAt": "Level1ExecutionStepFunctionsWithSsmAutomationApproval",
            "States": {
              "Level1ExecutionStepFunctionsWithSsmAutomationApproval": {
                "Type": "Task",
                "Resource": "arn:aws:states:::states:startExecution.sync:2",
                "OutputPath": "$.Output",
                "Parameters": {
                  "StateMachineArn": "${StepFunctionsWithSsmAutomationApproval.Arn}", 
                  "Input": {
                    "AWS_STEP_FUNCTIONS_STARTED_BY_EXECUTION_ID.$": "$$.Execution.Id",
                    "region.$": "$$.Execution.Input.region",
                    "ssm_parent_doc_name.$": "$$.Execution.Input.ssm_parent_doc_name",
                    "ssm_automation_assume_role.$": "$$.Execution.Input.ssm_automation_assume_role", 
                    "ssm_child_doc_name.$": "$$.Execution.Input.ssm_child_doc_name",
                    "ssm_description.$": "$$.Execution.Input.ssm_description",
                    "ssm_lambda_for_approved.$": "$$.Execution.Input.ssm_lambda_for_approved",
                    "ssm_lambda_parameters_for_approved.$": "$$.Execution.Input.ssm_lambda_parameters_for_approved",
                    "ssm_lambda_for_reject.$": "$$.Execution.Input.ssm_lambda_for_reject",
                    "ssm_lambda_parameters_for_reject.$": "$$.Execution.Input.ssm_lambda_parameters_for_reject",
                    "ssm_notification_arn.$": "$$.Execution.Input.level1_ssm_notification_arn",
                    "ssm_min_required_approvals.$": "$$.Execution.Input.level1_ssm_min_required_approvals",
                    "ssm_approvers.$": "$$.Execution.Input.level1_ssm_approvers",
                    "confirmation_url.$": "$$.Execution.Input.level1_confirmation_url",
                    "confirmation_file.$": "$$.Execution.Input.level1_confirmation_file"
                  }
                },
                "Retry": [
                  {
                    "ErrorEquals": [
                      "Lambda.ServiceException",
                      "Lambda.AWSLambdaException",
                      "Lambda.SdkClientException",
                      "Lambda.TooManyRequestsException"
                    ],
                    "IntervalSeconds": 2,
                    "MaxAttempts": 6,
                    "BackoffRate": 2
                  }
                ],
                "Catch": [
                    {
                      "ErrorEquals": [
                        "States.ALL"
                      ],
                      "Next": "Fail"
                    }
                ],
                "Next": "Level1ApprovalResult"
              },
              "Level2ExecutionStepFunctionsWithSsmAutomationApproval": {
                "Type": "Task",
                "Resource": "arn:aws:states:::states:startExecution.sync:2",
                "OutputPath": "$.Output",
                "Parameters": {
                  "StateMachineArn": "${StepFunctionsWithSsmAutomationApproval.Arn}", 
                  "Input": {
                    "AWS_STEP_FUNCTIONS_STARTED_BY_EXECUTION_ID.$": "$$.Execution.Id",
                    "region.$": "$$.Execution.Input.region",
                    "ssm_parent_doc_name.$": "$$.Execution.Input.ssm_parent_doc_name",
                    "ssm_automation_assume_role.$": "$$.Execution.Input.ssm_automation_assume_role", 
                    "ssm_child_doc_name.$": "$$.Execution.Input.ssm_child_doc_name",
                    "ssm_description.$": "$$.Execution.Input.ssm_description",
                    "ssm_lambda_for_approved.$": "$$.Execution.Input.ssm_lambda_for_approved",
                    "ssm_lambda_parameters_for_approved.$": "$$.Execution.Input.ssm_lambda_parameters_for_approved",
                    "ssm_lambda_for_reject.$": "$$.Execution.Input.ssm_lambda_for_reject",
                    "ssm_lambda_parameters_for_reject.$": "$$.Execution.Input.ssm_lambda_parameters_for_reject",
                    "ssm_notification_arn.$": "$$.Execution.Input.level2_ssm_notification_arn",
                    "ssm_min_required_approvals.$": "$$.Execution.Input.level2_ssm_min_required_approvals",
                    "ssm_approvers.$": "$$.Execution.Input.level2_ssm_approvers",
                    "confirmation_url.$": "$$.Execution.Input.level2_confirmation_url",
                    "confirmation_file.$": "$$.Execution.Input.level2_confirmation_file"
                  }
                },
                "Retry": [
                  {
                    "ErrorEquals": [
                      "Lambda.ServiceException",
                      "Lambda.AWSLambdaException",
                      "Lambda.SdkClientException",
                      "Lambda.TooManyRequestsException"
                    ],
                    "IntervalSeconds": 2,
                    "MaxAttempts": 6,
                    "BackoffRate": 2
                  }
                ],
                "Catch": [
                    {
                      "ErrorEquals": [
                        "States.ALL"
                      ],
                      "Next": "Fail"
                    }
                ],
                "Next": "Level2ApprovalResult"
              },
              "Level3ExecutionStepFunctionsWithSsmAutomationApproval": {
                "Type": "Task",
                "Resource": "arn:aws:states:::states:startExecution.sync:2",
                "OutputPath": "$.Output",
                "Parameters": {
                  "StateMachineArn": "${StepFunctionsWithSsmAutomationApproval.Arn}", 
                  "Input": {
                    "AWS_STEP_FUNCTIONS_STARTED_BY_EXECUTION_ID.$": "$$.Execution.Id",
                    "region.$": "$$.Execution.Input.region",
                    "ssm_parent_doc_name.$": "$$.Execution.Input.ssm_parent_doc_name",
                    "ssm_automation_assume_role.$": "$$.Execution.Input.ssm_automation_assume_role", 
                    "ssm_child_doc_name.$": "$$.Execution.Input.ssm_child_doc_name",
                    "ssm_description.$": "$$.Execution.Input.ssm_description",
                    "ssm_lambda_for_approved.$": "$$.Execution.Input.ssm_lambda_for_approved",
                    "ssm_lambda_parameters_for_approved.$": "$$.Execution.Input.ssm_lambda_parameters_for_approved",
                    "ssm_lambda_for_reject.$": "$$.Execution.Input.ssm_lambda_for_reject",
                    "ssm_lambda_parameters_for_reject.$": "$$.Execution.Input.ssm_lambda_parameters_for_reject",
                    "ssm_notification_arn.$": "$$.Execution.Input.level3_ssm_notification_arn",
                    "ssm_min_required_approvals.$": "$$.Execution.Input.level3_ssm_min_required_approvals",
                    "ssm_approvers.$": "$$.Execution.Input.level3_ssm_approvers",
                    "confirmation_url.$": "$$.Execution.Input.level3_confirmation_url",
                    "confirmation_file.$": "$$.Execution.Input.level3_confirmation_file"
                  }
                },
                "Retry": [
                  {
                    "ErrorEquals": [
                      "Lambda.ServiceException",
                      "Lambda.AWSLambdaException",
                      "Lambda.SdkClientException",
                      "Lambda.TooManyRequestsException"
                    ],
                    "IntervalSeconds": 2,
                    "MaxAttempts": 6,
                    "BackoffRate": 2
                  }
                ],
                "Catch": [
                    {
                      "ErrorEquals": [
                        "States.ALL"
                      ],
                      "Next": "Fail"
                    }
                ],
                "Next": "Level3ApprovalResult"
              },
              "Level1ApprovalResult": {
                "Type": "Choice",
                "Choices": [
                  {
                    "Variable": "$.is_approved",
                    "BooleanEquals": true,
                    "Next": "Level2ExecutionStepFunctionsWithSsmAutomationApproval"
                  },
                  {
                    "Variable": "$.is_approved",
                    "BooleanEquals": false,
                    "Next": "Rejected"
                  }
                ],
                "Default": "Rejected"
              },
              "Level2ApprovalResult": {
                "Type": "Choice",
                "Choices": [
                  {
                    "Variable": "$.is_approved",
                    "BooleanEquals": true,
                    "Next": "Level3ExecutionStepFunctionsWithSsmAutomationApproval"
                  },
                  {
                    "Variable": "$.is_approved",
                    "BooleanEquals": false,
                    "Next": "Rejected"
                  }
                ],
                "Default": "Rejected"
              },
              "Level3ApprovalResult": {
                "Type": "Choice",
                "Choices": [
                  {
                    "Variable": "$.is_approved",
                    "BooleanEquals": true,
                    "Next": "Approved"
                  },
                  {
                    "Variable": "$.is_approved",
                    "BooleanEquals": false,
                    "Next": "Rejected"
                  }
                ],
                "Default": "Rejected"
              },
              "Approved": {
                "Type": "Succeed"
              },
              "Rejected": {
                "Type": "Succeed"
              },
              "Fail": {
                "Type": "Fail"
              }
            }
          }

  StepFunctionsCallerForApprovalFlowRole:
    Type: AWS::IAM::Role
    DependsOn: 
      - StepFunctionsWithSsmAutomationApproval
    Properties:
      RoleName: !Sub 'IAMRole-StepFunctionsCallerForApprovalFlow-${AWS::Region}'
      Path: /
      MaxSessionDuration: 43200
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - states.amazonaws.com
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
      - PolicyName: !Sub 'IAMPolicy-StepFunctionsCallerForApprovalFlow-${AWS::Region}'
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
              - states:StartExecution
            Resource:
              - !Sub 'arn:aws:states:${AWS::Region}:${AWS::AccountId}:stateMachine:${StepFunctionsWithSsmAutomationApproval.Name}'
          - Effect: Allow
            Action:
              - states:DescribeExecution
              - states:StopExecution
            Resource:
              - !Sub 'arn:aws:states:${AWS::Region}:${AWS::AccountId}:execution:${StepFunctionsWithSsmAutomationApproval.Name}:*'
          - Effect: Allow
            Action:
              - events:PutTargets
              - events:PutRule
              - events:DescribeRule
            Resource:
              - !Sub 'arn:aws:events:${AWS::Region}:${AWS::AccountId}:rule/StepFunctionsGetEventsForStepFunctionsExecutionRule'
      - PolicyName: CloudWatchLogsDeliveryFullAccessPolicy
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
              - logs:DescribeResourcePolicies
              - logs:DescribeLogGroups
              - logs:GetLogDelivery
              - logs:CreateLogDelivery
              - logs:DeleteLogDelivery
              - logs:UpdateLogDelivery
              - logs:ListLogDeliveries
              - logs:PutResourcePolicy
            Resource:
              - '*'
      - PolicyName: XRayAccessPolicy
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
              - xray:PutTraceSegments
              - xray:PutTelemetryRecords
              - xray:GetSamplingRules
              - xray:GetSamplingTargets
            Resource:
              - '*'
  StepFunctionsCallerForApprovalFlowLogGroup:
    Type: AWS::Logs::LogGroup
    Properties: 
      LogGroupName: /aws/vendedlogs/states/Logs-StepFunctionsCallerForApprovalFlow

Outputs:
  Region:
    Value:
      !Ref AWS::Region
  StepFunctionsInputExample:
    Description: "AWS Step Functions Input Example"
    Value: !Sub |-
      {
        "region": "${AWS::Region}",
        "ssm_parent_doc_name": "${SsmParentDocumentApprovalAction}",
        "ssm_automation_assume_role": "${SsmAutomationAssumeRole.Arn}",
        "ssm_child_doc_name": "${SsmChildDocumentForApprovalAction}",
        "ssm_description": "Automation Approval Action For AWS Step Functions.",
        "ssm_lambda_for_approved": "${LambdaForReceivingAutomationResult}",
        "ssm_lambda_parameters_for_approved": "Approved",
        "ssm_lambda_for_reject": "${LambdaForReceivingAutomationResult}",
        "ssm_lambda_parameters_for_reject": "Rejected",
        "level1_ssm_notification_arn": "${SnsAutomationApprovalNotificationLevel1}",
        "level1_ssm_approvers": "${Level1SsmApprovers}",
        "level1_ssm_min_required_approvals": "${Level1SsmMinRequiredApprovals}",
        "level1_confirmation_url": "https://hidekazu-konishi.com/",
        "level1_confirmation_file": "index.html",
        "level2_ssm_notification_arn": "${SnsAutomationApprovalNotificationLevel2}",
        "level2_ssm_approvers": "${Level2SsmApprovers}",
        "level2_ssm_min_required_approvals": "${Level2SsmMinRequiredApprovals}",
        "level2_confirmation_url": "https://hidekazu-konishi.com/",
        "level2_confirmation_file": "index.html",
        "level3_ssm_notification_arn": "${SnsAutomationApprovalNotificationLevel3}",
        "level3_ssm_approvers": "${Level3SsmApprovers}",
        "level3_ssm_min_required_approvals": "${Level3SsmMinRequiredApprovals}",
        "level3_confirmation_url": "https://hidekazu-konishi.com/",
        "level3_confirmation_file": "index.html"
      }

構築手順

  1. AWS Step FunctionsやAWS Systems Manager Automationをサポートしているリージョンで、テンプレートのパラメータに必要な値を入力してAWS CloudFormationでデプロイする。
    AWS CloudFormationスタック作成後にOutputフィールドへAWS Step Functions実行時の入力パラメータ例(JSON形式)がStepFunctionsInputExampleとして出力されるのでメモしておく。
  2. 入力した各承認段階のEmailアドレスにSNSトピックのサブスクリプション承認リクエストが届くので承認しておく。

デモの実行

  1. 上記「構築手順」でメモしたStepFunctionsInputExampleのJSONパラメータのうち、levelX_confirmation_urllevelX_confirmation_fileを修正し、AWS Step FunctionsステートマシンStepFunctionsCallerForApprovalFlowの入力値にして実行する。
    levelX_confirmation_urllevelX_confirmation_fileは各段階のAWS Systems Manager Automation承認アクションのメールに記載されます。levelX_confirmation_urlが承認するために参照するURL、levelX_confirmation_fileが承認するために参照するURL中にあるファイルを想定しています。例えば、levelX_confirmation_urlにAmazon S3コンソールへのURL、levelX_confirmation_fileにAmazon S3オブジェクトのファイル名を記載することなどが考えられます。
    ※「X」には各段階の数値が入ります。

  2. 構築時に指定したEmailアドレスにAWS Systems Manager Automation承認アクションのメールが届くので、承認(Approve)するか拒否(Reject)するかをAWSマネジメントコンソールから選択する。

  3. AWS Step FunctionsステートマシンStepFunctionsWithSsmAutomationApprovalのステップが選択した承認(Approve)、拒否(Reject)の通りに遷移することを確認する。
  4. 上記「2.」~「3.」を各承認段階分実行する。
    各承認段階のいずれかで拒否(Reject)を選択した場合はそれが最終的な拒否の結果となる。
    各承認段階のすべてで承認(Approve)を選択した場合はそれが最終的な承認の結果となる。

削除手順

  1. 「構築手順」で作成したAWS CloudFormationスタックを削除する。


参考:
What is AWS Step Functions? - AWS Step Functions
AWS Systems Manager Automation - AWS Systems Manager
What is AWS CodePipeline? - AWS CodePipeline
Tech Blog with related articles referenced

まとめ

今回はAWS Systems Manager Automationの承認アクションをコンポーネント化し、別のAWS Step Functionsのワークフローから呼び出す多段階承認フローを作成する方法を試しました。
次回はAWS Systems Manager Automationでの承認結果をAmazon EventBridgeルールで検知する方法に変更し、AWS Step FunctionsへAWS Systems Manager Automationを使用した承認フローを追加する方法を紹介したいと思います。

Written by Hidekazu Konishi
Hidekazu Konishi (小西秀和), a Japan AWS Top Engineer and a Japan AWS All Certifications Engineer

執筆者小西秀和

Japan AWS All Certifications Engineer(AWS認定全冠)の知識をベースにAWSクラウドの活用に取り組んでいます。
Amazon.co.jp: 小西 秀和: books, biography, latest update
NRIネットコムBlog: 小西 秀和: 記事一覧