小西秀和です。
以前の記事でAWS Systems Manager Automationの承認アクションを使用してAWS Step Functionsのワークフローへ承認フローを追加する方法を紹介しました。
以前の記事ではAWS Lamba関数で実行したAWS Systems Manager AutomationからAWS Lamba関数を経由して承認結果をAWS Step Functionsステートマシンへ返却していました。
今回はAWS Systems Manager Automationでの承認結果をAmazon EventBridgeルールで検知する方法に変更し、AWS Step FunctionsへAWS Systems Manager Automationを使用した承認フローを追加する方法を試してみたいと思います。
※本記事および当執筆者のその他の記事で掲載されているソースコードは自主研究活動の一貫として作成したものであり、動作を保証するものではありません。使用する場合は自己責任でお願い致します。また、予告なく修正することもありますのでご了承ください。
今回の記事の内容は次のような構成になっています。
本記事で試す構成図
今回試すAWS Step FunctionsへAWS Lambda、AWS Systems Manager Automation、Amazon EventBridgeで承認フローを追加する構成は次のようになります。

流れとしては、
まず、AWS Step FunctionsステートマシンでwaitForTaskToken
を指定したAWS Lambda関数からAWS Systems Manager Document(SSM Document)をAWS Systems Manager Automation(SSM Automation)として実行します。
SSM Automationの実行ステップは承認アクション(aws:approve
)で承認者にAmazon SNSトピックのメール通知で承認の可否を確認します。
承認フローの承認、拒否の決定に伴うSSM Automation実行ステップのイベントをAmazon EventBridgeルールで検知して、結果返却用のAWS Lambda関数を実行します。
結果返却用のAWS Lambda関数ではSSM Automationの実行ステップの内容からAWS Step Functionsステートマシンのトークンと承認結果を取得して、結果をAWS Step Functionsステートマシンに返却します。
このようにAWS Systems Manager Automationの承認アクションを部品化して使用する利点は、承認者の認証と承認アクションの権限を指定できることにあります。
SSM Documentの承認アクション(aws:approve
)で送信されるAmazon SNSトピックのメール通知が届くと、承認者はリンクからAWSマネジメントコンソールへログインし、承認アクションを許可されたIAMロールまたはIAMユーザーの場合のみ承認の可否を決定できます。
次の記事で紹介した親子関係のあるSSM Automationの実行結果をAWS Lamba関数で連携してく方法に比べて、今回のAmazon EventBridgeルールでSSM Automation実行ステップのイベントを検知する方法はSSM Automationを親子関係にする必要がなく、イベントベースで承認結果を取得するので処理がシンプルになります。
一方で上記の記事と同様に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 #承認リクエストを送信するメールアドレス EventRuleForAutomationResultState: ENABLED #Amazon EventBridgeの有効化(ENABLED)、無効化(DISABLED)の設定 SsmApprovers: arn:aws:iam::XXXXXXXXXXXX:role/ho2k.com #承認リクエストに対して承認、拒否を決定するIAMロールまたはIAMユーザー SsmMinRequiredApprovals: 1 #承認に必要な人数。ここに記載の人数が承認して初めて処理として承認される。 SsmAutomationAssumeRoleName: SsmAutomationAssumeRole #作成するAutomationAssumeRoleの名称(SSM Automationの実行にこのロールを使用する) SsmDocumentForApprovalActionName: SsmParentDocumentApprovalAction #SSM Documentの名称 SsmDocumentForApprovalActionVersionName: 1 #SSM Documentのバージョン名
テンプレート本体
ファイル名:SfnApprovalCFnSfnWithSsmApprovalAndEventBridge.yml
AWSTemplateFormatVersion: '2010-09-09' Description: 'Add AWS Systems Manager Automation Approval Action to AWS Step Functions.' Parameters: SsmAutomationAssumeRoleName: Type: String Default: "SsmAutomationAssumeRole" SsmDocumentForApprovalActionName: Type: String Default: "SsmDocumentForApprovalAction" SsmDocumentForApprovalActionVersionName: 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" EventRuleForAutomationResultState: Type: String Default: ENABLED AllowedValues: - ENABLED - DISABLED 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_doc_name'], Parameters={ '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']], 'SfnToken': [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) ssm_client = boto3.client('ssm', region_name=region) sfn_client = boto3.client('stepfunctions', region_name=region) def lambda_handler(event, context): print(("Received event: " + json.dumps(event, indent=2))) sfn_token = '' is_approved = False try: #EventのExecutionIdからSSM DocumentパラメータにあるStep Functionsのトークンを取得する。 ssm_exe_res = ssm_client.get_automation_execution( AutomationExecutionId=event['detail']['ExecutionId'] ) print('ssm_client.get_automation_execution: ') print(ssm_exe_res) sfn_token = ssm_exe_res['AutomationExecution']['Parameters']['SfnToken'][0] print(f'sfn_token: {sfn_token}') #EventのExecutionIdとActionから承認結果を取得する。 ssm_step_res = ssm_client.describe_automation_step_executions( AutomationExecutionId=event['detail']['ExecutionId'], Filters=[ { 'Key': 'Action', 'Values': ['aws:approve'] }, ] ) print('ssm_client.describe_automation_step_executions: ') print(ssm_step_res) approval_result = ssm_step_res['StepExecutions'][0]['Outputs']['ApprovalStatus'][0] print(f'approval_result:{approval_result}') #EventのExecutionIdとステップ名から承認結果を取得する。 if approval_result == 'Approved': is_approved = True except Exception as ex: print(f'Exception:{ex}') tb = sys.exc_info()[2] print(f'ssm_client get_automation_execution, describe_automation_step_executions FAIL. Exception:{str(ex.with_traceback(tb))}') is_approved = False try: #コールバックしたトークンでSFN側にタスクの成功を送信する。 sfn_res = sfn_client.send_task_success( taskToken=sfn_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: - ssm:GetAutomationExecution - ssm:DescribeAutomationStepExecutions Resource: - '*' - Effect: Allow Action: - states:ListActivities - states:ListExecutions - states:ListStateMachines - states:DescribeActivity - states:DescribeExecution - states:DescribeStateMachine - states:DescribeStateMachineForExecution - states:GetExecutionHistory - states:SendTaskSuccess Resource: - '*' LambdaForReceivingAutomationResultPermission: Type: AWS::Lambda::Permission DependsOn: - LambdaForReceivingAutomationResult - EventRuleForAutomationResult Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt LambdaForReceivingAutomationResult.Arn Principal: events.amazonaws.com SourceArn: !GetAtt EventRuleForAutomationResult.Arn EventRuleForAutomationResult: Type: AWS::Events::Rule DependsOn: - LambdaForReceivingAutomationResult - EventRuleForAutomationResultRole Properties: Name: EventRuleForAutomationResult EventBusName: default Description: 'EventRuleForAutomationResult' State: !Ref EventRuleForAutomationResultState EventPattern: source: - aws.ssm detail-type: - 'EC2 Automation Step Status-change Notification' detail: Definition: - !Ref SsmDocumentForApprovalActionName Status: - 'Success' - 'Failed' Action: - 'aws:approve' Targets: - Id: 'EventRuleForAutomationResultTarget' Arn: !GetAtt LambdaForReceivingAutomationResult.Arn EventRuleForAutomationResultRole: Type: AWS::IAM::Role DependsOn: - LambdaForReceivingAutomationResult Properties: RoleName: !Sub 'EventRuleForAutomationResultRole-${AWS::Region}' Path: / AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - events.amazonaws.com - lambda.amazonaws.com Action: - sts:AssumeRole Policies: - PolicyName: !Sub 'EventRuleForAutomationResultRole-${AWS::Region}' PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - lambda:InvokeFunction Resource: - !Sub '${LambdaForReceivingAutomationResult.Arn}:*' SsmDocumentForApprovalAction: Type: AWS::SSM::Document Properties: Name: !Ref SsmDocumentForApprovalActionName DocumentType: Automation VersionName: !Ref SsmDocumentForApprovalActionVersionName DocumentFormat: YAML Content: description: 'SsmDocumentForApprovalAction' 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: 'SsmDocumentForApprovalAction' 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' Approvers: description: 'The IAM User or IAM Role of the Approver.' type: StringList SfnToken: description: 'AWS Step Functions State Machine Token' type: String default: '' MinRequiredApprovals: description: MinRequiredApprovals type: Integer default: 1 mainSteps: - name: ApprovalAction action: 'aws:approve' timeoutSeconds: 604800 inputs: Message: '{{Message}}' NotificationArn: '{{NotificationArn}}' Approvers: '{{Approvers}}' MinRequiredApprovals: '{{MinRequiredApprovals}}' 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": 604800, "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_automation_assume_role.$": "$$.Execution.Input.ssm_automation_assume_role", "ssm_doc_name.$": "$$.Execution.Input.ssm_doc_name", "ssm_description.$": "$$.Execution.Input.ssm_description", "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 Outputs: Region: Value: !Ref AWS::Region StepFunctionsInputExample: Description: "AWS Step Functions Input Example" Value: !Sub |- { "region": "${AWS::Region}", "ssm_automation_assume_role": "${SsmAutomationAssumeRole.Arn}", "ssm_doc_name": "${SsmDocumentForApprovalAction}", "ssm_description": "Automation Approval Action For AWS Step Functions.", "ssm_notification_arn": "${SnsAutomationApprovalNotification}", "ssm_approvers": "${SsmApprovers}", "ssm_min_required_approvals": "${SsmMinRequiredApprovals}", "confirmation_url": "https://hidekazu-konishi.com/", "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パラメータのうち、confirmation_url
とconfirmation_file
を修正し、AWS Step FunctionsステートマシンStepFunctionsWithSsmAutomationApproval
の入力値にして実行する。
confirmation_url
とconfirmation_file
はAWS Systems Manager Automation承認アクションのメールに記載されます。confirmation_url
が承認するために参照するURL、confirmation_file
が承認するために参照するURL中にあるファイルを想定しています。例えば、confirmation_url
にAmazon S3コンソールへのURL、confirmation_file
にAmazon S3オブジェクトのファイル名を記載することなどが考えられます。構築時に指定したEmailアドレスにAWS Systems Manager Automation承認アクションのメールが届くので、承認(
Approve
)するか拒否(Reject
)するかをAWSマネジメントコンソールから選択する。- AWS Step Functionsステートマシン
StepFunctionsWithSsmAutomationApproval
のステップが選択した承認(Approve
)、拒否(Reject
)の通りに遷移することを確認する。
削除手順
- 「構築手順」で作成したAWS CloudFormationスタックを削除する。
まとめ
今回はAWS Systems Manager Automationの承認アクションとAmazon EventBridgeを使用してAWS Step Functionsのワークフローへ承認フローを追加する方法を試しました。
次回は今回試したAWS Step Function承認フローを別のAWS Step Functionsのワークフローから呼び出して多段階承認フローを作成する方法を紹介したいと思います。