NRIネットコム Blog

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

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

小西秀和です。

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 CodePipelineの承認アクションとAmazon EventBridgeを使用してAWS Step Functionsのワークフローへ承認フローを追加する方法を試してみました。

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

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

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

本記事で試す構成図

今回試す構成は次のようにコンポーネント化したAWS Step Functions承認フローを3回使用する3段階の承認フローとなります。
AWS CodePipeline内部で多段階の承認フローを作成することも可能ですが、今回は3段階それぞれにパイプラインを用意して各パイプラインの承認結果がAWS Step Functionsのワークフローに反映される方式にしています。

AWS Step FunctionsへAWS Lambda、AWS CodePipeline、Amazon EventBridgeで多段階承認フローを作成する構成例
AWS Step FunctionsへAWS Lambda、AWS CodePipeline、Amazon EventBridgeで多段階承認フローを作成する構成例

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

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

参考: AWS Step Functionsのワークフローへ承認フローを追加する方法(AWS CodePipeline & Amazon EventBridge編)

上記の記事でも紹介したように、AWS Systems Manager Automationを使用する承認フローと比較すると、AWS CodePipelineは実行前にステージの設定でSource Artifactを保存するAmazon S3バケット、オブジェクト名、承認時のメッセージ、確認用URLなどを予め決めておく必要があります。

AWS Systems Manager AutomationではSSM Documentで定義したパラメータの値を変更することで、Automationの実行ごとに承認者のIAMロール、最終承認に必要な複数の承認者の承認数、承認時のメッセージ、確認用URL、確認用ファイルなどを変更できます。

一方でAWS CodePipelineはAmazon S3オブジェクトのPUTのみで実行できますが、AWS Systems Manager Automationでは複数のパラメータや実行用IAMロールなどを指定して実行する必要があります。

そのため、注意するべき特徴としてAWS Systems Manager Automationはオートメーションの部品として承認フローを必要とするAWS Step Functionsに柔軟に導入することができますが、AWS CodePipelineは構築するCI/CDのアーキテクチャに応じて承認フローを含むパイプラインの構成やAWS CodePipelineの実行を必要とするAWS Step Functionsの構成を目的に応じて変更する必要があることが挙げられます。

AWS Step FunctionsへAWS Lambda、AWS CodePipeline、Amazon EventBridgeで承認フローを追加する構成例
AWS Step FunctionsへAWS Lambda、AWS CodePipeline、Amazon EventBridgeで承認フローを追加する構成例

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

AWS CloudFormationテンプレート(AWS Step FunctionsへのAWS Lambda、AWS CodePipeline、Amazon EventBridgeによる多段階承認フローの追加)

入力パラメータ例

EventRuleForCodePipelineResultState: ENABLED #Amazon EventBridgeの有効化(ENABLED)、無効化(DISABLED)の設定  
Level1CodePipelineConfirmationCustomData: Approval request has been received. Please review file at the following URL to decide whether to approve or deny. #承認フローの確認ダイアログで表示するカスタムデータ(メッセージ)
Level1CodePipelineConfirmationUrl: https://hidekazu-konishi.com/ #承認フローの確認ダイアログで表示する確認用URL
Level1CodePipelineName: Level1CodePipelineApprovalSample #AWS CodePipelineの名称  
Level1CodePipelineS3bucketKeyContentType text/html #Source Artifactのコンテンツタイプ
Level1CodePipelineS3bucketKeyInput: index_level1.html #AWS CodePipelineを起動させるSource Artifactのファイル名  
Level1CodePipelineS3bucketKeyOutput: index_level1_approved.html #AWS CodePipelineで承認後、最終的にデプロイするArtifactのファイル名  
Level1CodePipelineS3bucketName: h-o2k #AWS CodePipelineでArtifactを保存するAmazon S3バケット名  
Level1EmailForNotification: sample1@h-o2k.com #承認リクエストを送信するメールアドレス
Level2CodePipelineConfirmationCustomData: Approval request has been received. Please review file at the following URL to decide whether to approve or deny. #承認フローの確認ダイアログで表示するカスタムデータ(メッセージ)
Level2CodePipelineConfirmationUrl: https://hidekazu-konishi.com/ #承認フローの確認ダイアログで表示する確認用URL
Level2CodePipelineName: Level2CodePipelineApprovalSample #AWS CodePipelineの名称  
Level2CodePipelineS3bucketKeyContentType text/html #Source Artifactのコンテンツタイプ
Level2CodePipelineS3bucketKeyInput: index_level2.html #AWS CodePipelineを起動させるSource Artifactのファイル名  
Level2CodePipelineS3bucketKeyOutput: index_level2_approved.html #AWS CodePipelineで承認後、最終的にデプロイするArtifactのファイル名  
Level2CodePipelineS3bucketName: h-o2k #AWS CodePipelineでArtifactを保存するAmazon S3バケット名  
Level2EmailForNotification: sample2@h-o2k.com #承認リクエストを送信するメールアドレス
Level3CodePipelineConfirmationCustomData: Approval request has been received. Please review file at the following URL to decide whether to approve or deny. #承認フローの確認ダイアログで表示するカスタムデータ(メッセージ)
Level3CodePipelineConfirmationUrl: https://hidekazu-konishi.com/ #承認フローの確認ダイアログで表示する確認用URL
Level3CodePipelineName: Level3CodePipelineApprovalSample #AWS CodePipelineの名称  
Level3CodePipelineS3bucketKeyContentType text/html #Source Artifactのコンテンツタイプ
Level3CodePipelineS3bucketKeyInput: index_level3.html #AWS CodePipelineを起動させるSource Artifactのファイル名  
Level3CodePipelineS3bucketKeyOutput: index_level3_approved.html #AWS CodePipelineで承認後、最終的にデプロイするArtifactのファイル名  
Level3CodePipelineS3bucketName: h-o2k #AWS CodePipelineでArtifactを保存するAmazon S3バケット名  
Level3EmailForNotification: sample3@h-o2k.com #承認リクエストを送信するメールアドレス

テンプレート本体

ファイル名:SfnApprovalCFnSfnWithMultiLevelCodePipelineApprovalEventBridge.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"

AWSTemplateFormatVersion: '2010-09-09'
Description: 'Add AWS CodePipeline Approval Action to AWS Step Functions.'
Parameters:
  EventRuleForCodePipelineResultState:
    Type: String
    Default: ENABLED
    AllowedValues:
      - ENABLED
      - DISABLED
  Level1EmailForNotification: 
    Type: String
    Default: "sample1@h-o2k.com"
  Level1CodePipelineName:
    Type: String
    Default: "Level1CodePipelineApprovalSample"
  Level1CodePipelineS3bucketName: 
    Type: String
    Default: "h-o2k"
  Level1CodePipelineS3bucketKeyInput: 
    Type: String
    Default: "index_level1.html"
  Level1CodePipelineS3bucketKeyOutput: 
    Type: String
    Default: "index_level1_approved.html"
  Level1CodePipelineS3bucketKeyContentType: 
    Type: String
    Default: "text/html"
  Level1CodePipelineConfirmationCustomData:
    Type: String
    Default: "Approval request has been received. Please review file at the following URL to decide whether to approve or deny."
  Level1CodePipelineConfirmationUrl:
    Type: String
    Default: "https://hidekazu-konishi.com/"
  Level2EmailForNotification: 
    Type: String
    Default: "sample2@h-o2k.com"
  Level2CodePipelineName:
    Type: String
    Default: "Level2CodePipelineApprovalSample"
  Level2CodePipelineS3bucketName: 
    Type: String
    Default: "h-o2k"
  Level2CodePipelineS3bucketKeyInput: 
    Type: String
    Default: "index_level2.html"
  Level2CodePipelineS3bucketKeyOutput: 
    Type: String
    Default: "index_level2_approved.html"
  Level2CodePipelineS3bucketKeyContentType: 
    Type: String
    Default: "text/html"
  Level2CodePipelineConfirmationCustomData:
    Type: String
    Default: "Approval request has been received. Please review file at the following URL to decide whether to approve or deny."
  Level2CodePipelineConfirmationUrl:
    Type: String
    Default: "https://hidekazu-konishi.com/"
  Level3EmailForNotification: 
    Type: String
    Default: "sample3@h-o2k.com"
  Level3CodePipelineName:
    Type: String
    Default: "Level3CodePipelineApprovalSample"
  Level3CodePipelineS3bucketName: 
    Type: String
    Default: "h-o2k"
  Level3CodePipelineS3bucketKeyInput: 
    Type: String
    Default: "index_level3.html"
  Level3CodePipelineS3bucketKeyOutput: 
    Type: String
    Default: "index_level3_approved.html"
  Level3CodePipelineS3bucketKeyContentType: 
    Type: String
    Default: "text/html"
  Level3CodePipelineConfirmationCustomData:
    Type: String
    Default: "Approval request has been received. Please review file at the following URL to decide whether to approve or deny."
  Level3CodePipelineConfirmationUrl:
    Type: String
    Default: "https://hidekazu-konishi.com/"
Resources:
  AWSCodePipelineServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub 'AWSCodePipelineServiceRole-${AWS::Region}'
      Path: /
      MaxSessionDuration: 43200
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - codepipeline.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: !Sub 'IAMPolicy-AWSCodePipelineServiceRole-${AWS::Region}'
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Action:
                  - "iam:PassRole"
                Resource: "*"
                Effect: Allow
                Condition:
                  StringEqualsIfExists:
                    "iam:PassedToService":
                      - cloudformation.amazonaws.com
                      - elasticbeanstalk.amazonaws.com
                      - ec2.amazonaws.com
                      - ecs-tasks.amazonaws.com
              - Action:
                  - "codecommit:CancelUploadArchive"
                  - "codecommit:GetBranch"
                  - "codecommit:GetCommit"
                  - "codecommit:GetRepository"
                  - "codecommit:GetUploadArchiveStatus"
                  - "codecommit:UploadArchive"
                Resource: "*"
                Effect: Allow
              - Action:
                  - "codedeploy:CreateDeployment"
                  - "codedeploy:GetApplication"
                  - "codedeploy:GetApplicationRevision"
                  - "codedeploy:GetDeployment"
                  - "codedeploy:GetDeploymentConfig"
                  - "codedeploy:RegisterApplicationRevision"
                Resource: "*"
                Effect: Allow
              - Action:
                  - "codestar-connections:UseConnection"
                Resource: "*"
                Effect: Allow
              - Action:
                  - "elasticbeanstalk:*"
                  - "ec2:*"
                  - "elasticloadbalancing:*"
                  - "autoscaling:*"
                  - "cloudwatch:*"
                  - "s3:*"
                  - "sns:*"
                  - "cloudformation:*"
                  - "rds:*"
                  - "sqs:*"
                  - "ecs:*"
                Resource: "*"
                Effect: Allow
              - Action:
                  - "lambda:InvokeFunction"
                  - "lambda:ListFunctions"
                Resource: "*"
                Effect: Allow
              - Action:
                  - "opsworks:CreateDeployment"
                  - "opsworks:DescribeApps"
                  - "opsworks:DescribeCommands"
                  - "opsworks:DescribeDeployments"
                  - "opsworks:DescribeInstances"
                  - "opsworks:DescribeStacks"
                  - "opsworks:UpdateApp"
                  - "opsworks:UpdateStack"
                Resource: "*"
                Effect: Allow
              - Action:
                  - "cloudformation:CreateStack"
                  - "cloudformation:DeleteStack"
                  - "cloudformation:DescribeStacks"
                  - "cloudformation:UpdateStack"
                  - "cloudformation:CreateChangeSet"
                  - "cloudformation:DeleteChangeSet"
                  - "cloudformation:DescribeChangeSet"
                  - "cloudformation:ExecuteChangeSet"
                  - "cloudformation:SetStackPolicy"
                  - "cloudformation:ValidateTemplate"
                Resource: "*"
                Effect: Allow
              - Action:
                  - "codebuild:BatchGetBuilds"
                  - "codebuild:StartBuild"
                  - "codebuild:BatchGetBuildBatches"
                  - "codebuild:StartBuildBatch"
                Resource: "*"
                Effect: Allow
              - Effect: Allow
                Action:
                  - "devicefarm:ListProjects"
                  - "devicefarm:ListDevicePools"
                  - "devicefarm:GetRun"
                  - "devicefarm:GetUpload"
                  - "devicefarm:CreateUpload"
                  - "devicefarm:ScheduleRun"
                Resource: "*"
              - Effect: Allow
                Action:
                  - "servicecatalog:ListProvisioningArtifacts"
                  - "servicecatalog:CreateProvisioningArtifact"
                  - "servicecatalog:DescribeProvisioningArtifact"
                  - "servicecatalog:DeleteProvisioningArtifact"
                  - "servicecatalog:UpdateProduct"
                Resource: "*"
              - Effect: Allow
                Action:
                  - "cloudformation:ValidateTemplate"
                Resource: "*"
              - Effect: Allow
                Action:
                  - "ecr:DescribeImages"
                Resource: "*"
              - Effect: Allow
                Action:
                  - "states:DescribeExecution"
                  - "states:DescribeStateMachine"
                  - "states:StartExecution"
                Resource: "*"
              - Effect: Allow
                Action:
                  - "appconfig:StartDeployment"
                  - "appconfig:StopDeployment"
                  - "appconfig:GetDeployment"
                Resource: "*"

  CodePipelineForApprovalActionLevel1:
    DependsOn: 
      - AWSCodePipelineServiceRole
      - SnsCodePipelineApprovalNotificationLevel1
    Type: AWS::CodePipeline::Pipeline
    Properties:
      ArtifactStore:
        Location: !Ref Level1CodePipelineS3bucketName
        Type: S3
      Name: !Ref Level1CodePipelineName
      RoleArn: !GetAtt AWSCodePipelineServiceRole.Arn
      Stages:
        - Name: Source
          Actions:
            - Name: Source
              Region: !Ref AWS::Region
              ActionTypeId: 
                Category: Source
                Owner: AWS
                Provider: S3
                Version: '1'
              Configuration:
                S3Bucket: !Ref Level1CodePipelineS3bucketName
                S3ObjectKey: !Ref Level1CodePipelineS3bucketKeyInput
              OutputArtifacts:
                - Name: SourceArtifact
              RunOrder: 1
        - Name: Approval
          Actions:
            - Name: Approval
              Region: !Ref AWS::Region
              ActionTypeId:
                Category: Approval
                Owner: AWS
                Provider: Manual
                Version: '1'
              Configuration:
                CustomData: !Ref Level1CodePipelineConfirmationCustomData
                ExternalEntityLink: !Ref Level1CodePipelineConfirmationUrl
                NotificationArn: !Ref SnsCodePipelineApprovalNotificationLevel1
              RunOrder: 1
        - Name: Deploy
          Actions:
            - Name: Deploy
              Region: !Ref AWS::Region
              ActionTypeId: 
                Category: Deploy
                Owner: AWS
                Provider: S3
                Version: '1'
              Configuration:
                BucketName: !Ref Level1CodePipelineS3bucketName
                ObjectKey: !Ref Level1CodePipelineS3bucketKeyOutput
                Extract: false
              InputArtifacts:
                - Name: SourceArtifact
              RunOrder: 1

  CodePipelineForApprovalActionLevel2:
    DependsOn: 
      - AWSCodePipelineServiceRole
      - SnsCodePipelineApprovalNotificationLevel2
    Type: AWS::CodePipeline::Pipeline
    Properties:
      ArtifactStore:
        Location: !Ref Level2CodePipelineS3bucketName
        Type: S3
      Name: !Ref Level2CodePipelineName
      RoleArn: !GetAtt AWSCodePipelineServiceRole.Arn
      Stages:
        - Name: Source
          Actions:
            - Name: Source
              Region: !Ref AWS::Region
              ActionTypeId: 
                Category: Source
                Owner: AWS
                Provider: S3
                Version: '1'
              Configuration:
                S3Bucket: !Ref Level2CodePipelineS3bucketName
                S3ObjectKey: !Ref Level2CodePipelineS3bucketKeyInput
              OutputArtifacts:
                - Name: SourceArtifact
              RunOrder: 1
        - Name: Approval
          Actions:
            - Name: Approval
              Region: !Ref AWS::Region
              ActionTypeId:
                Category: Approval
                Owner: AWS
                Provider: Manual
                Version: '1'
              Configuration:
                CustomData: !Ref Level2CodePipelineConfirmationCustomData
                ExternalEntityLink: !Ref Level2CodePipelineConfirmationUrl
                NotificationArn: !Ref SnsCodePipelineApprovalNotificationLevel2
              RunOrder: 1
        - Name: Deploy
          Actions:
            - Name: Deploy
              Region: !Ref AWS::Region
              ActionTypeId: 
                Category: Deploy
                Owner: AWS
                Provider: S3
                Version: '1'
              Configuration:
                BucketName: !Ref Level2CodePipelineS3bucketName
                ObjectKey: !Ref Level2CodePipelineS3bucketKeyOutput
                Extract: false
              InputArtifacts:
                - Name: SourceArtifact
              RunOrder: 1

  CodePipelineForApprovalActionLevel3:
    DependsOn: 
      - AWSCodePipelineServiceRole
      - SnsCodePipelineApprovalNotificationLevel3
    Type: AWS::CodePipeline::Pipeline
    Properties:
      ArtifactStore:
        Location: !Ref Level3CodePipelineS3bucketName
        Type: S3
      Name: !Ref Level3CodePipelineName
      RoleArn: !GetAtt AWSCodePipelineServiceRole.Arn
      Stages:
        - Name: Source
          Actions:
            - Name: Source
              Region: !Ref AWS::Region
              ActionTypeId: 
                Category: Source
                Owner: AWS
                Provider: S3
                Version: '1'
              Configuration:
                S3Bucket: !Ref Level3CodePipelineS3bucketName
                S3ObjectKey: !Ref Level3CodePipelineS3bucketKeyInput
              OutputArtifacts:
                - Name: SourceArtifact
              RunOrder: 1
        - Name: Approval
          Actions:
            - Name: Approval
              Region: !Ref AWS::Region
              ActionTypeId:
                Category: Approval
                Owner: AWS
                Provider: Manual
                Version: '1'
              Configuration:
                CustomData: !Ref Level3CodePipelineConfirmationCustomData
                ExternalEntityLink: !Ref Level3CodePipelineConfirmationUrl
                NotificationArn: !Ref SnsCodePipelineApprovalNotificationLevel3
              RunOrder: 1
        - Name: Deploy
          Actions:
            - Name: Deploy
              Region: !Ref AWS::Region
              ActionTypeId: 
                Category: Deploy
                Owner: AWS
                Provider: S3
                Version: '1'
              Configuration:
                BucketName: !Ref Level3CodePipelineS3bucketName
                ObjectKey: !Ref Level3CodePipelineS3bucketKeyOutput
                Extract: false
              InputArtifacts:
                - Name: SourceArtifact
              RunOrder: 1

  LambdaForCodePipelineExecution:
    Type: AWS::Lambda::Function
    DependsOn: 
      - LambdaForCodePipelineExecutionRole
    Properties:
      FunctionName: LambdaForCodePipelineExecution
      Description : 'LambdaForCodePipelineExecution'
      Runtime: python3.9
      MemorySize: 10240
      Timeout: 900
      Role: !GetAtt LambdaForCodePipelineExecutionRole.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"]

          s3_client = boto3.client('s3', region_name=region)

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

              try:
                  s3_put_res = s3_client.put_object(
                      Body=event['confirmation_file_content'], 
                      Bucket=event['s3_bucket_name'], 
                      Key=event['s3_bucket_key'],
                      ContentType=event['confirmation_file_content-type'],
                      Metadata={
                          'x-amz-meta-sfntoken': event['token']
                      }
                  )
                  print('s3_client.put_object: ')
                  print(s3_put_res)
              except Exception as ex:
                  print(f'Exception:{ex}')
                  tb = sys.exc_info()[2]
                  print(f's3_client put_object FAIL. Exception:{str(ex.with_traceback(tb))}')
                  raise

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

  LambdaForCodePipelineExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub 'IAMRole-LambdaForCodePipelineExecutionRole-${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-LambdaForCodePipelineExecutionRole-${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/LambdaForCodePipelineExecutionRole:*'
            - Effect: Allow
              Action:
                - s3:PutObject
              Resource:
                - '*'

  LambdaForReceivingCodePipelineResult:
    Type: AWS::Lambda::Function
    DependsOn:
      - LambdaForReceivingCodePipelineResultRole
    Properties:
      FunctionName: LambdaForReceivingCodePipelineResult
      Description : 'LambdaForReceivingCodePipelineResult'
      Runtime: python3.9
      MemorySize: 10240
      Timeout: 900
      Role: !GetAtt LambdaForReceivingCodePipelineResultRole.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)
          cpl_client = boto3.client('codepipeline', region_name=region)
          s3_client = boto3.client('s3', 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のexecution-idからPipeline実行で使用したrevisionId(Amazon S3オブジェクトバージョン)を特定し、Step Functionsのトークンを取得する。
                  cpl_res_exe = cpl_client.get_pipeline_execution(
                      pipelineName=event['detail']['pipeline'],
                      pipelineExecutionId=event['detail']['execution-id']
                  )
                  print('cpl_client.get_pipeline_execution: ')
                  print(cpl_res_exe)
                  
                  s3_version_id = cpl_res_exe['pipelineExecution']['artifactRevisions'][0]['revisionId']
                  print(f's3_version_id: {s3_version_id}')

                  cpl_res = cpl_client.get_pipeline(
                      name=cpl_res_exe['pipelineExecution']['pipelineName'],
                      version=cpl_res_exe['pipelineExecution']['pipelineVersion']
                  )

                  #パイプライン名とパイプラインバージョンから使用しているAmazon S3バケットとオブジェクトキーを取得する。  
                  s3_bucket_name = cpl_res['pipeline']['stages'][0]['actions'][0]['configuration']['S3Bucket']
                  print(f's3_bucket_name: {s3_bucket_name}')
                  s3_bucket_key = cpl_res['pipeline']['stages'][0]['actions'][0]['configuration']['S3ObjectKey']
                  print(f's3_bucket_key: {s3_bucket_key}')
                  
                  #バージョンIDに対応するAmazon S3オブジェクトのメタデータにあるStep Functionsのトークンを取得する。
                  s3_get_res = s3_client.get_object(Bucket=s3_bucket_name, Key=s3_bucket_key, VersionId=s3_version_id)
                  print('s3_client.get_object: ')
                  print(s3_get_res)
                  #file_content = s3_get_res['Body'].read().decode('utf-8')
                  sfn_token = s3_get_res['Metadata']['x-amz-meta-sfntoken']
                  print(f'sfn_token: {sfn_token}')

                  #Eventのstateから承認結果を取得する。
                  approval_result = event['detail'].get('state','')
                  print(f'approval_result:{approval_result}')
                  if approval_result == 'SUCCEEDED':
                      is_approved = True

              except Exception as ex:
                  print(f'Exception:{ex}')
                  tb = sys.exc_info()[2]
                  print(f'cpl_client.get_pipeline_execution, s3_client.get_object 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}

  LambdaForReceivingCodePipelineResultRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub 'IAMRole-LambdaForReceivingCodePipelineResult-${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-LambdaForReceivingCodePipelineResult-${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/LambdaForReceivingCodePipelineResult:*'
            - Effect: Allow
              Action:
                - 's3:GetObject*'
                - codepipeline:GetPipelineExecution
                - codepipeline:GetPipeline
              Resource:
                - '*'
            - Effect: Allow
              Action:
                - states:ListActivities
                - states:ListExecutions
                - states:ListStateMachines
                - states:DescribeActivity
                - states:DescribeExecution
                - states:DescribeStateMachine
                - states:DescribeStateMachineForExecution
                - states:GetExecutionHistory
                - states:SendTaskSuccess
              Resource:
                - '*'

  LambdaForReceivingCodePipelineResultPermission:
    Type: AWS::Lambda::Permission
    DependsOn: 
      - LambdaForReceivingCodePipelineResult
      - EventRuleForCodePipelineResult
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !GetAtt LambdaForReceivingCodePipelineResult.Arn
      Principal: events.amazonaws.com
      SourceArn: !GetAtt EventRuleForCodePipelineResult.Arn

  EventRuleForCodePipelineResult:
    Type: AWS::Events::Rule
    DependsOn: 
      - LambdaForReceivingCodePipelineResult
    Properties: 
      Name: EventRuleForCodePipelineResult
      EventBusName: default
      Description: 'EventRuleForCodePipelineResult'
      State: !Ref EventRuleForCodePipelineResultState
      EventPattern: 
        source: 
          - 'aws.codepipeline'
        detail-type:
          - 'CodePipeline Action Execution State Change'
        detail: 
          pipeline: 
            - !Ref Level1CodePipelineName
            - !Ref Level2CodePipelineName
            - !Ref Level3CodePipelineName
          state:
            - 'SUCCEEDED'
            - 'FAILED'
          type:
            category:
              - 'Approval'
      Targets: 
        - Id: 'EventRuleForCodePipelineResultTarget'
          Arn: !GetAtt LambdaForReceivingCodePipelineResult.Arn

  SnsCodePipelineApprovalNotificationLevel1:
    Type: AWS::SNS::Topic
    Properties: 
      TopicName: CodePipelineApprovalNotificationLevel1
      DisplayName: CodePipelineApprovalNotificationLevel1
      FifoTopic: False
      Subscription: 
        - Endpoint: !Ref Level1EmailForNotification
          Protocol: email
  SnsCodePipelineApprovalNotificationLevel2:
    Type: AWS::SNS::Topic
    Properties: 
      TopicName: CodePipelineApprovalNotificationLevel2
      DisplayName: CodePipelineApprovalNotificationLevel2
      FifoTopic: False
      Subscription: 
        - Endpoint: !Ref Level2EmailForNotification
          Protocol: email
  SnsCodePipelineApprovalNotificationLevel3:
    Type: AWS::SNS::Topic
    Properties: 
      TopicName: CodePipelineApprovalNotificationLevel3
      DisplayName: CodePipelineApprovalNotificationLevel3
      FifoTopic: False
      Subscription: 
        - Endpoint: !Ref Level3EmailForNotification
          Protocol: email

  StepFunctionsWithCodePipelineApproval: 
    Type: AWS::StepFunctions::StateMachine
    DependsOn: 
      - LambdaForCodePipelineExecution
      - LambdaForReceivingCodePipelineResult
      - StepFunctionsWithCodePipelineApprovalRole
      - StepFunctionsWithCodePipelineApprovalLogGroup
    Properties: 
      StateMachineName: StepFunctionsWithCodePipelineApproval
      StateMachineType: STANDARD
      RoleArn: !GetAtt StepFunctionsWithCodePipelineApprovalRole.Arn
      LoggingConfiguration: 
        Level: ALL
        IncludeExecutionData: true
        Destinations: 
          - CloudWatchLogsLogGroup:
              LogGroupArn: !GetAtt StepFunctionsWithCodePipelineApprovalLogGroup.Arn
      DefinitionString: !Sub |-
        {
            "Comment": "Sample of adding an Approval flow to AWS Step Functions.",
            "TimeoutSeconds": 604800, 
            "StartAt": "InvokeLambdaForCodePipelineExecution",
            "States": {
              "InvokeLambdaForCodePipelineExecution": {
                "Type": "Task",
                "Resource": "arn:aws:states:::lambda:invoke.waitForTaskToken",
                "Parameters": {
                  "FunctionName": "${LambdaForCodePipelineExecution.Arn}:$LATEST",
                  "Payload": {
                    "step.$": "$$.State.Name",
                    "token.$": "$$.Task.Token",
                    "s3_bucket_name.$": "$$.Execution.Input.s3_bucket_name",
                    "s3_bucket_key.$": "$$.Execution.Input.s3_bucket_key",
                    "confirmation_file_content-type.$": "$$.Execution.Input.confirmation_file_content-type",
                    "confirmation_file_content.$": "$$.Execution.Input.confirmation_file_content"   
                  }
                },
                "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"
              }
            }
          }

  StepFunctionsWithCodePipelineApprovalRole:
    Type: AWS::IAM::Role
    DependsOn: 
      - LambdaForCodePipelineExecution
      - LambdaForReceivingCodePipelineResult
    Properties:
      RoleName: !Sub 'IAMRole-StepFunctionsWithCodePipelineApproval-${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-StepFunctionsWithCodePipelineApproval-${AWS::Region}'
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
              - lambda:InvokeFunction
            Resource:
              - !Sub '${LambdaForCodePipelineExecution.Arn}:*'
              - !Sub '${LambdaForReceivingCodePipelineResult.Arn}:*'
          - Effect: Allow
            Action:
              - lambda:InvokeFunction
            Resource:
              - !Sub '${LambdaForCodePipelineExecution.Arn}'
              - !Sub '${LambdaForReceivingCodePipelineResult.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:
              - '*'
  StepFunctionsWithCodePipelineApprovalLogGroup:
    Type: AWS::Logs::LogGroup
    Properties: 
      LogGroupName: /aws/vendedlogs/states/Logs-StepFunctionsWithCodePipelineApproval

  StepFunctionsCallerForApprovalFlow: 
    Type: AWS::StepFunctions::StateMachine
    DependsOn: 
      - StepFunctionsWithCodePipelineApproval
      - 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": 604800, 
            "StartAt": "Level1ExecutionStepFunctionsWithCodePipelineApproval",
            "States": {
              "Level1ExecutionStepFunctionsWithCodePipelineApproval": {
                "Type": "Task",
                "Resource": "arn:aws:states:::states:startExecution.sync:2",
                "OutputPath": "$.Output",
                "Parameters": {
                  "StateMachineArn": "${StepFunctionsWithCodePipelineApproval.Arn}", 
                  "Input": {
                    "AWS_STEP_FUNCTIONS_STARTED_BY_EXECUTION_ID.$": "$$.Execution.Id",
                    "region.$": "$$.Execution.Input.region",
                    "s3_bucket_name.$": "$$.Execution.Input.level1_s3_bucket_name",
                    "s3_bucket_key.$": "$$.Execution.Input.level1_s3_bucket_key",
                    "confirmation_file_content-type.$": "$$.Execution.Input.level1_confirmation_file_content-type",
                    "confirmation_file_content.$": "$$.Execution.Input.level1_confirmation_file_content"
                  }
                },
                "Retry": [
                  {
                    "ErrorEquals": [
                      "Lambda.ServiceException",
                      "Lambda.AWSLambdaException",
                      "Lambda.SdkClientException",
                      "Lambda.TooManyRequestsException"
                    ],
                    "IntervalSeconds": 2,
                    "MaxAttempts": 6,
                    "BackoffRate": 2
                  }
                ],
                "Catch": [
                    {
                      "ErrorEquals": [
                        "States.ALL"
                      ],
                      "Next": "Fail"
                    }
                ],
                "Next": "Level1ApprovalResult"
              },
              "Level2ExecutionStepFunctionsWithCodePipelineApproval": {
                "Type": "Task",
                "Resource": "arn:aws:states:::states:startExecution.sync:2",
                "OutputPath": "$.Output",
                "Parameters": {
                  "StateMachineArn": "${StepFunctionsWithCodePipelineApproval.Arn}", 
                  "Input": {
                    "AWS_STEP_FUNCTIONS_STARTED_BY_EXECUTION_ID.$": "$$.Execution.Id",
                    "region.$": "$$.Execution.Input.region",
                    "s3_bucket_name.$": "$$.Execution.Input.level2_s3_bucket_name",
                    "s3_bucket_key.$": "$$.Execution.Input.level2_s3_bucket_key",
                    "confirmation_file_content-type.$": "$$.Execution.Input.level2_confirmation_file_content-type",
                    "confirmation_file_content.$": "$$.Execution.Input.level2_confirmation_file_content"
                  }
                },
                "Retry": [
                  {
                    "ErrorEquals": [
                      "Lambda.ServiceException",
                      "Lambda.AWSLambdaException",
                      "Lambda.SdkClientException",
                      "Lambda.TooManyRequestsException"
                    ],
                    "IntervalSeconds": 2,
                    "MaxAttempts": 6,
                    "BackoffRate": 2
                  }
                ],
                "Catch": [
                    {
                      "ErrorEquals": [
                        "States.ALL"
                      ],
                      "Next": "Fail"
                    }
                ],
                "Next": "Level2ApprovalResult"
              },
              "Level3ExecutionStepFunctionsWithCodePipelineApproval": {
                "Type": "Task",
                "Resource": "arn:aws:states:::states:startExecution.sync:2",
                "OutputPath": "$.Output",
                "Parameters": {
                  "StateMachineArn": "${StepFunctionsWithCodePipelineApproval.Arn}", 
                  "Input": {
                    "AWS_STEP_FUNCTIONS_STARTED_BY_EXECUTION_ID.$": "$$.Execution.Id",
                    "region.$": "$$.Execution.Input.region",
                    "s3_bucket_name.$": "$$.Execution.Input.level3_s3_bucket_name",
                    "s3_bucket_key.$": "$$.Execution.Input.level3_s3_bucket_key",
                    "confirmation_file_content-type.$": "$$.Execution.Input.level3_confirmation_file_content-type",
                    "confirmation_file_content.$": "$$.Execution.Input.level3_confirmation_file_content"
                  }
                },
                "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": "Level2ExecutionStepFunctionsWithCodePipelineApproval"
                  },
                  {
                    "Variable": "$.is_approved",
                    "BooleanEquals": false,
                    "Next": "Rejected"
                  }
                ],
                "Default": "Rejected"
              },
              "Level2ApprovalResult": {
                "Type": "Choice",
                "Choices": [
                  {
                    "Variable": "$.is_approved",
                    "BooleanEquals": true,
                    "Next": "Level3ExecutionStepFunctionsWithCodePipelineApproval"
                  },
                  {
                    "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: 
      - StepFunctionsWithCodePipelineApproval
    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:${StepFunctionsWithCodePipelineApproval.Name}'
          - Effect: Allow
            Action:
              - states:DescribeExecution
              - states:StopExecution
            Resource:
              - !Sub 'arn:aws:states:${AWS::Region}:${AWS::AccountId}:execution:${StepFunctionsWithCodePipelineApproval.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}",
        "level1_s3_bucket_name": "${Level1CodePipelineS3bucketName}",
        "level1_s3_bucket_key": "${Level1CodePipelineS3bucketKeyInput}",
        "level1_confirmation_file_content-type": "${Level1CodePipelineS3bucketKeyContentType}",
        "level1_confirmation_file_content": "<!DOCTYPE html><html><head><meta http-equiv=\"refresh\" content=\"10; URL=https:\/\/hidekazu-konishi.com\/\"><title>Demo of adding approval actions with AWS CodePipeline to AWS Step Functions<\/title><\/head><body>Demo of adding approval actions with AWS CodePipeline to AWS Step Functions.<br\/><\/body><\/html>",
        "level2_s3_bucket_name": "${Level2CodePipelineS3bucketName}",
        "level2_s3_bucket_key": "${Level2CodePipelineS3bucketKeyInput}",
        "level2_confirmation_file_content-type": "${Level2CodePipelineS3bucketKeyContentType}",
        "level2_confirmation_file_content": "<!DOCTYPE html><html><head><meta http-equiv=\"refresh\" content=\"10; URL=https:\/\/hidekazu-konishi.com\/\"><title>Demo of adding approval actions with AWS CodePipeline to AWS Step Functions<\/title><\/head><body>Demo of adding approval actions with AWS CodePipeline to AWS Step Functions.<br\/><\/body><\/html>",
        "level3_s3_bucket_name": "${Level3CodePipelineS3bucketName}",
        "level3_s3_bucket_key": "${Level3CodePipelineS3bucketKeyInput}",
        "level3_confirmation_file_content-type": "${Level3CodePipelineS3bucketKeyContentType}",
        "level3_confirmation_file_content": "<!DOCTYPE html><html><head><meta http-equiv=\"refresh\" content=\"10; URL=https:\/\/hidekazu-konishi.com\/\"><title>Demo of adding approval actions with AWS CodePipeline to AWS Step Functions<\/title><\/head><body>Demo of adding approval actions with AWS CodePipeline to AWS Step Functions.<br\/><\/body><\/html>"
      }

構築手順

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

デモの実行

  1. 上記「構築手順」でメモしたStepFunctionsInputExampleのJSONパラメータのうち、levelX_confirmation_file_contentを修正し、AWS Step FunctionsステートマシンStepFunctionsCallerForApprovalFlowの入力値にして実行する。
    levelX_confirmation_file_contentはデモ用として用意したAWS CodePipelineのSource Artifactのファイル内容です。実際にAWS CodePipelineを使用してアーティファクトを各AWSリソースにデプロイする場合にはZipファイルなどを使用するため、AWS Step FunctionsのステップやAWS CodePipelineのステージは用途に合わせて構築する必要があります。
    ※「X」には各段階の数値が入ります。

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

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

削除手順

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


参考:
Manage AWS Step Functions Executions as an Integrated Service - AWS Step Functions
IAM Policies for integrated services:AWS Step Functions - AWS Step Functions
How to Add an Approval Flow to AWS Step Functions Workflow (AWS CodePipeline and Amazon EventBridge Edition)
Tech Blog with related articles referenced

まとめ

今回はAWS CodePipelineの承認アクションをコンポーネント化し、別のAWS Step Functionsのワークフローから呼び出す多段階承認フローを作成する方法を試しました。
機会があれば今まで紹介したAWSサービスを使用した承認フローについて各パターンの特徴をまとめたいと思います。

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