NRIネットコム Blog

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

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

小西秀和です。
以前の記事でAWS Systems Manager Automationの承認アクションを使用してAWS Step Functionsのワークフローへ承認フローを追加する方法を紹介しました。

以前の記事ではAWS Lamba関数で実行したAWS Systems Manager Automationの承認結果をAWS Lamba関数またはAmazon EventBridge経由でAWS Step Functionsステートマシンへ返却していました。
今回はAWS Systems Manager Automationの代わりにAWS CodePipelineによる承認フローを追加して、承認結果をAmazon EventBridgeルールで検知し、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 Lambda、AWS CodePipeline、Amazon EventBridgeで承認フローを追加する構成例

流れとしては、
まず、AWS Step FunctionsステートマシンでwaitForTaskTokenを指定したAWS Lambda関数から、Amazon S3オブジェクト(AWS CodePipelineで指定したSource Artifact)のメタデータにAWS Step Functionsステートマシンのトークンを書き込んでAmazon S3バケットへPUTすることで、それをトリガーにAWS CodePipeline(SSM Automation)が実行されます。 AWS CodePipelineの承認ステージでは承認アクション(Approval)で承認者にAmazon SNSトピックのメール通知で承認の可否を確認します。
承認フローの承認、拒否の決定に伴うAWS CodePipelineの承認ステージのイベントをAmazon EventBridgeルールで検知して、結果返却用のAWS Lambda関数を実行します。
結果返却用のAWS Lambda関数ではイベント該当するAWS CodePipelineの承認ステージの内容から使用したAmazon S3オブジェクトのバージョンを特定し、Amazon S3オブジェクトのメタデータからAWS Step Functionsステートマシンのトークンを取得して、イベントの承認結果をAWS Step Functionsステートマシンに返却します。

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

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

オペレーションの自動化をサポートするAWS Systems Manager AutomationとCI/CDをサポートするAWS CodePipelineではサービス目的が異なるため、承認フローについては特に設定内容を決定するタイミングに違いがあります。

具体的には以前の記事で紹介した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 CloudFormationテンプレートとパラメータの例

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

入力パラメータ例

CodePipelineName: CodePipelineApprovalSample #AWS CodePipelineの名称  
CodePipelineS3bucketName: h-o2k #AWS CodePipelineでArtifactを保存するAmazon S3バケット名  
CodePipelineS3bucketKeyInput: index.html #AWS CodePipelineを起動させるSource Artifactのファイル名  
CodePipelineS3bucketKeyOutput: index_approved.html #AWS CodePipelineで承認後、最終的にデプロイするArtifactのファイル名  
CodePipelineS3bucketKeyContentType: text/html #Source Artifactのコンテンツタイプ
CodePipelineConfirmationCustomData: Approval request has been received. Please review file at the following URL to decide whether to approve or deny. #承認フローの確認ダイアログで表示するカスタムデータ(メッセージ)
CodePipelineConfirmationUrl: https://hidekazu-konishi.com/ #承認フローの確認ダイアログで表示する確認用URL
EmailForNotification: sample@h-o2k.com #承認リクエストを送信するメールアドレス
EventRuleForAutomationResultState: ENABLED #Amazon EventBridgeの有効化(ENABLED)、無効化(DISABLED)の設定  

テンプレート本体

ファイル名:SfnApprovalCFnSfnWithCodePipelineApprovalAndEventBridge.yml

AWSTemplateFormatVersion: '2010-09-09'
Description: 'Add AWS CodePipeline Approval Action to AWS Step Functions.'
Parameters:
  EmailForNotification: 
    Type: String
    Default: "sample@h-o2k.com"
  EventRuleForCodePipelineResultState:
    Type: String
    Default: ENABLED
    AllowedValues:
      - ENABLED
      - DISABLED
  CodePipelineName:
    Type: String
    Default: "CodePipelineApprovalSample"
  CodePipelineS3bucketName: 
    Type: String
    Default: "h-o2k"
  CodePipelineS3bucketKeyInput: 
    Type: String
    Default: "index.html"
  CodePipelineS3bucketKeyOutput: 
    Type: String
    Default: "index_approved.html"
  CodePipelineS3bucketKeyContentType: 
    Type: String
    Default: "text/html"
  CodePipelineConfirmationCustomData:
    Type: String
    Default: "Approval request has been received. Please review file at the following URL to decide whether to approve or deny."
  CodePipelineConfirmationUrl:
    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: "*"

  CodePipelineForApprovalAction:
    DependsOn: 
      - AWSCodePipelineServiceRole
      - SnsCodePipelineApprovalNotification
    Type: AWS::CodePipeline::Pipeline
    Properties:
      ArtifactStore:
        Location: !Ref CodePipelineS3bucketName
        Type: S3
      Name: !Ref CodePipelineName
      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 CodePipelineS3bucketName
                S3ObjectKey: !Ref CodePipelineS3bucketKeyInput
              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 CodePipelineConfirmationCustomData
                ExternalEntityLink: !Ref CodePipelineConfirmationUrl
                NotificationArn: !Ref SnsCodePipelineApprovalNotification
              RunOrder: 1
        - Name: Deploy
          Actions:
            - Name: Deploy
              Region: !Ref AWS::Region
              ActionTypeId: 
                Category: Deploy
                Owner: AWS
                Provider: S3
                Version: '1'
              Configuration:
                BucketName: !Ref CodePipelineS3bucketName
                ObjectKey: !Ref CodePipelineS3bucketKeyOutput
                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 CodePipelineName
          state:
            - 'SUCCEEDED'
            - 'FAILED'
          type:
            category:
              - 'Approval'
      Targets: 
        - Id: 'EventRuleForCodePipelineResultTarget'
          Arn: !GetAtt LambdaForReceivingCodePipelineResult.Arn

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

Outputs:
  Region:
    Value:
      !Ref AWS::Region
  StepFunctionsInputExample:
    Description: "AWS Step Functions Input Example"
    Value: !Sub |-
      {
        "region": "${AWS::Region}",
        "s3_bucket_name": "${CodePipelineS3bucketName}",
        "s3_bucket_key": "${CodePipelineS3bucketKeyInput}",
        "confirmation_file_content-type": "${CodePipelineS3bucketKeyContentType}",
        "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パラメータのうち、confirmation_file_contentを修正し、AWS Step FunctionsステートマシンStepFunctionsWithCodePipelineApprovalの入力値にして実行する。
    confirmation_file_contentはデモ用として用意したAWS CodePipelineのSource Artifactのファイル内容です。実際にAWS CodePipelineを使用してアーティファクトを各AWSリソースにデプロイする場合にはZipファイルなどを使用するため、AWS Step FunctionsのステップやAWS CodePipelineのステージは用途に合わせて構築する必要があります。

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

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

削除手順

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


参考:
What is AWS Step Functions? - AWS Step Functions
AWS Systems Manager Automation - AWS Systems Manager
What is AWS CodePipeline? - AWS CodePipeline
Tech Blog with related articles referenced

まとめ

今回はAWS CodePipelineの承認アクションとAmazon EventBridgeを使用してAWS Step Functionsのワークフローへ承認フローを追加する方法を試しました。
次回は今回試したAWS Step Function承認フローを別のAWS Step Functionsのワークフローから呼び出して多段階承認フローを作成する方法を紹介したいと思います。

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

執筆者小西秀和

Japan AWS All Certifications Engineer(AWS認定全冠)の知識をベースにAWSクラウドの活用に取り組んでいます。
Amazon.co.jp: 小西 秀和: books, biography, latest update
NRIネットコムBlog: 小西 秀和: 記事一覧