NRIネットコム Blog

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

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

小西秀和です。
AWSで承認フローの機能を提供するサービスにはAWS Systems Manager AutomationやAWS CodePipelineの承認アクションなどがあります。

近年、AI技術の急速な進化により、従来人間が手動で行っていた承認プロセスを生成AIで置き換えたり、強力にサポートしたりすることが可能になってきました。しかし、専門知識や権限を持つ人間による最終判断も依然として重要です。

そこで私は、将来的に生成AIを承認フローに組み込むことを見据え、AWS Step Functionsを活用した承認フローシステムをAWSサービスを使用して試作しました。この試作の主な目的は以下の通りです。

  • APIを介して承認フローをシステム化することで、人間と生成AIの間で意思決定プロセスを柔軟に切り替えられる
  • 初期段階では人間が承認を行い、生成AIの能力が十分と判断された場合に段階的にAIへ移行できる
  • 生成AIの判断に不安がある場合や、最終確認が必要な場合は、人間が承認プロセスに介入できる
  • 人間と生成AIを組み合わせた多段階承認フローにより、より高い精度での意思決定が可能になる

なお、本記事ではAWS CloudFormationテンプレートを使用してこのデモを構築しています。AWS CDKやAWS SAMなど、より高度なIaCツールを使用しなかった理由には以下のことが挙げられます。

  • 再現性と可搬性: CloudFormationテンプレート一つで完結させることで、環境に依存せず、誰でも同じ結果を得られるようにしました。CDKやSAMを使用すると、バージョンの違いや依存関係の問題が生じる可能性があり、再現性が低下する恐れがあります。
  • 学習障壁の低さ: 多くのAWSユーザーにとって、CloudFormationは馴染みのある技術です。CDKやSAMを使用すると、追加のツールやプログラミング言語の知識が必要になる場合があり、読者の方々にとって障壁となる可能性があります。
  • AWSリソースの直接的な理解: CloudFormationテンプレートでは、AWSリソースを直接定義します。これにより、AWSサービスの詳細な設定や動作を理解しやすくなり、教育的な価値が高まります。
  • デバッグの容易さ: 単一のテンプレートファイルであるため、エラーの特定や修正が比較的容易です。これは、読者の方々が自身の環境で実装する際のトラブルシューティングを容易にします。
  • バージョン管理とメンテナンスの簡素化: 単一ファイルでの管理は、バージョン管理を簡素化し、長期的な保守性を高めます。CloudFormationの基本的な構文は長年にわたり安定しており、将来的な変更や非推奨化のリスクが低いです。
  • 迅速な展開とテスト: 追加のビルドステップが不要なため、テンプレートの変更をすぐに適用してテストできます。これにより、読者の方々が自身の環境で素早く試すことができます。
  • AWSコンソールとの互換性: CloudFormationテンプレートは、AWSコンソールで直接編集・適用できます。これにより、GUIを通じた迅速な変更や確認が可能となり、より多くの方々にとって扱いやすいものとなります。

これらの理由により、CloudFormationテンプレートを使用することで、より多くの読者の方々が容易に理解し、実践できるデモ環境を提供できると考えました。

今回はAWSサービスを使用して構築できる承認フローのうち、AWS Systems Manager Automationの承認アクションを使用してAWS Step Functionsのワークフローへ承認フローを追加する方法を試してみたいと思います。

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

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

本記事で試す構成図

今回試すAWS Step FunctionsへAWS LambdaとAWS Systems Manager Automationで承認フローを追加する構成は次のようになります。

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

流れとしては、
まず、AWS Step FunctionsステートマシンでwaitForTaskTokenを指定したAWS Lambda関数から親SSM Documentを実行(親SSM Automation)します。
親SSM Automationは子SSM Documentを実行(子SSM Automation)し、子SSM Automationは承認アクション(aws:approve)で承認者にAmazon SNSトピックのメール通知で承認の可否を確認します。
承認フローの結果が承認だった場合は子SSM Automationから承認とトークンのパラメータを結果返却用のAWS Lambdaに渡します。
承認フローの結果が拒否だった場合は親SSM Automationから拒否とトークンのパラメータを結果返却用のAWS Lambdaに渡します。

上記のようにSSM Documentを親子関係にしている理由は、AWS Systems Manager Automationの承認アクション(aws:approve)は拒否するとFailedして後続のAWS Lambda関数が呼ばれないためです。
そこで、子SSM Documentの承認アクションの拒否によるFailedを親SSM Documentでキャッチして拒否した結果を返却するAWS Lambda関数を実行するようにしています

このようにAWS Systems Manager Automationの承認アクションを部品化して使用する利点は、承認者の認証と承認アクションの権限を指定できることにあります

子SSM Documentの承認アクション(aws:approve)で送信されるAmazon SNSトピックのメール通知が届くと、承認者はリンクからAWSマネジメントコンソールへログインし、承認アクションを許可されたIAMロールまたはIAMユーザーの場合のみ承認の可否を決定できます

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

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

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

入力パラメータ例

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

テンプレート本体

ファイル名:SfnApprovalCFnSfnWithSsmApproval.yml

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"
  SsmApprovers:
    Type: String
    Default: "arn:aws:iam::XXXXXXXXXXXX:role/ho2k.com"
  SsmMinRequiredApprovals:
    Type: String
    Default: "1"
  EmailForNotification: 
    Type: String
    Default: "sample@h-o2k.com"
Resources:
  SsmAutomationAssumeRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Ref SsmAutomationAssumeRoleName
      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'
          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'
      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'
          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

  SnsAutomationApprovalNotification:
    Type: AWS::SNS::Topic
    Properties: 
      TopicName: AutomationApprovalNotification
      DisplayName: AutomationApprovalNotification
      FifoTopic: False
      Subscription: 
        - Endpoint: !Ref EmailForNotification
          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'
      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'
        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

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",
        "ssm_notification_arn": "${SnsAutomationApprovalNotification}",
        "ssm_approvers": "${SsmApprovers}",
        "ssm_min_required_approvals": "${SsmMinRequiredApprovals}",
        "confirmation_url": "https://hidekazu-konishi.com/",
        "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パラメータのうち、confirmation_urlconfirmation_fileを修正し、AWS Step FunctionsステートマシンStepFunctionsWithSsmAutomationApprovalの入力値にして実行する。
    confirmation_urlconfirmation_fileはAWS Systems Manager Automation承認アクションのメールに記載されます。confirmation_urlが承認するために参照するURL、confirmation_fileが承認するために参照するURL中にあるファイルを想定しています。例えば、confirmation_urlにAmazon S3コンソールへのURL、confirmation_fileにAmazon S3オブジェクトのファイル名を記載することなどが考えられます。

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

  3. AWS Step FunctionsステートマシンStepFunctionsWithSsmAutomationApprovalのステップが選択した承認(Approve)、拒否(Reject)の通りに遷移することを確認する。

削除手順

  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 Step Functionsのワークフローに柔軟な承認プロセスを組み込むことが確認できました。
AWS Systems Manager Automationの承認アクションとIAMロールを利用することで、承認者の認証と承認アクションの権限も制御できできます。

また、承認が許諾された場合に加えて、承認が拒否された場合にも適切に継続的な処理を行うことができ、複雑な承認フローが必要なプロセスにも対応可能です。
次のステップとして、この承認フローを他のAWSサービスと連携させたり、複数の承認ステップを持つ多段階承認フローへ拡張したりすることも考えられます。

AWSのサーバーレスサービスを組み合わせることで、メンテナンスの手間を最小限に抑えつつ、柔軟で応用可能な承認ワークフローを実現できることが分かりました。
今後もこのようなAWSサービスを使用したアプローチによる承認ワークフロー管理を試していきたいと思います。

次回は今回試したAWS Step Function承認フローを別のAWS Step Functionsのワークフローから呼び出して多段階承認フローを作成する方法を紹介したいと思います。

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

執筆者小西秀和

Japan AWS Top Engineer, Japan AWS All Certifications Engineer(AWS認定全冠)として、知識と実践的な経験を活かし、AWSの活用に取り組んでいます。
NRIネットコムBlog: 小西 秀和: 記事一覧
Amazon.co.jp: 小西 秀和: books, biography, latest update
Personal Tech Blog