小西秀和です。
以前書いた次の記事で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スタックを削除する。
まとめ
今回はAWS Systems Manager Automationの承認アクションをコンポーネント化し、別のAWS Step Functionsのワークフローから呼び出す多段階承認フローを作成する方法を試しました。
次回はAWS Systems Manager Automationでの承認結果をAmazon EventBridgeルールで検知する方法に変更し、AWS Step FunctionsへAWS Systems Manager Automationを使用した承認フローを追加する方法を紹介したいと思います。