小西秀和です。
AI技術の急速な進化に伴い、従来の業務プロセスを見直す必要性が高まっています。
特に、多段階承認フローのあり方について、新しい視点からアプローチしたいと考え、この記事を執筆しました。
これまで多段階承認フローは、しばしば冗長で効率が悪いと批判されてきました。しかし同時に、専門知識や権限を持つ人間が最終判断を下す重要な場でもあります。
そこで私は次のような理由から、将来的に生成AIを多段階承認フローに組み込むことを見据え、AWS Step Functionsを活用した多段階承認フローシステムを試作してみました。
- 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テンプレートを採用しました。もちろん、CDKやSAMなどの高度なツールにも大きな利点があり、より複雑なインフラストラクチャの管理や大規模なプロジェクトでは非常に有用です。しかし、このような規模のデモでは、CloudFormationテンプレートの直接性と簡潔さが、より多くの読者の方々にとって有益であると判断しました。
さて、ここから本題に入りたいと思います。
以前書いた次の記事でAWS Systems Manager Automationの承認アクションを使用してAWS Step Functionsのワークフローへ承認フローを追加する方法を試してみました。
今回はこのAWS Step Functionsの承認フローをコンポーネント化し、別のAWS Step Functionsのワークフローから呼び出して多段階承認フローを作成する方法を試してみたいと思います。
※本記事および当執筆者のその他の記事で掲載されているソースコードは自主研究活動の一貫として作成したものであり、動作を保証するものではありません。使用する場合は自己責任でお願い致します。また、予告なく修正することもありますのでご了承ください。
今回の記事の内容は次のような構成になっています。
本記事で試す構成図
今回試す構成は次のようにコンポーネント化したAWS Step Functions承認フローを3回使用する3段階の承認フローとなります。
コンポーネント化したAWS Step Functions承認フロー
コンポーネント化したAWS Step Functions承認フローは次の記事で紹介したものです。
参考: AWS Step Functionsのワークフローへ承認フローを追加する方法(AWS Systems Manager Automation編)
上記の記事でも紹介したように、AWS CloudFormationテンプレートを更新する場合の考慮事項として、親SSM Document、子SSM Documentの定義を更新する場合にAWS CloudFormationの仕様上、SSM Document名を変更して再作成する必要があることが挙げられます。
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" }
構築手順
- AWS Step FunctionsやAWS Systems Manager Automationをサポートしているリージョンで、テンプレートのパラメータに必要な値を入力してAWS CloudFormationでデプロイする。
AWS CloudFormationスタック作成後にOutput
フィールドへAWS Step Functions実行時の入力パラメータ例(JSON形式)がStepFunctionsInputExample
として出力されるのでメモしておく。 - 入力した各承認段階のEmailアドレスにSNSトピックのサブスクリプション承認リクエストが届くので承認しておく。
デモの実行
上記「構築手順」でメモした
StepFunctionsInputExample
のJSONパラメータのうち、levelX_confirmation_url
とlevelX_confirmation_file
を修正し、AWS Step FunctionsステートマシンStepFunctionsCallerForApprovalFlow
の入力値にして実行する。
levelX_confirmation_url
とlevelX_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」には各段階の数値が入ります。構築時に指定したEmailアドレスにAWS Systems Manager Automation承認アクションのメールが届くので、承認(
Approve
)するか拒否(Reject
)するかをAWSマネジメントコンソールから選択する。- AWS Step Functionsステートマシン
StepFunctionsWithSsmAutomationApproval
のステップが選択した承認(Approve
)、拒否(Reject
)の通りに遷移することを確認する。 - 上記「2.」~「3.」を各承認段階分実行する。
各承認段階のいずれかで拒否(Reject
)を選択した場合はそれが最終的な拒否の結果となる。
各承認段階のすべてで承認(Approve
)を選択した場合はそれが最終的な承認の結果となる。
削除手順
- 「構築手順」で作成したAWS CloudFormationスタックを削除する。
参考:
What is AWS Step Functions? - AWS Step Functions
AWS Systems Manager Automation - AWS Systems Manager
What is AWS CodePipeline? - AWS CodePipeline
How to Add an Approval Flow to AWS Step Functions Workflow (AWS Systems Manager Automation Edition)
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を使用した承認フローを追加する方法を紹介したいと思います。