hidekazu-konishi.com

How to Add an Approval Flow to AWS Step Functions Workflow (AWS CodePipeline and Amazon EventBridge Edition)

First Published:
Last Updated:

In a previous article, I introduced a method to add an approval flow to an AWS Step Functions workflow using the approval action of AWS Systems Manager Automation.

In the previous articles, I returned the approval result of AWS Systems Manager Automation executed by an AWS Lambda function to the AWS Step Functions state machine via an AWS Lambda function or Amazon EventBridge.
This time, I'd like to try adding an approval flow using AWS CodePipeline instead of AWS Systems Manager Automation, and detect the approval result with an Amazon EventBridge rule to return it to AWS Step Functions.

This article, like the aforementioned ones, was created with the following motivations and intentions:
In recent years, the rapid advancement of AI technology has made it possible to replace or strongly support traditionally manual approval processes performed by humans with generative AI. However, final judgment by humans with specialized knowledge and authority remains important.
Therefore, with a view to incorporating generative AI into approval flows in the future, I have prototyped an approval flow system using AWS Step Functions with AWS services. The main objectives of this prototype are as follows:
  • By systematizing the approval flow through APIs, decision-making processes can be flexibly switched between humans and generative AI
  • Initially, approvals are performed by humans, and can be gradually transitioned to AI when the capabilities of generative AI are deemed sufficient
  • If there are concerns about the judgment of generative AI or if final confirmation is needed, humans can intervene in the approval process
  • Multi-stage approval flows combining humans and generative AI enable decision-making with higher accuracy
Note that this demo is built using an AWS CloudFormation template. The reasons for not using more advanced IaC tools like AWS CDK or AWS SAM include the following:
  • Reproducibility and portability: By making it self-contained with a single CloudFormation template, it ensures that anyone can obtain the same results regardless of their environment. Using CDK or SAM could potentially reduce reproducibility due to version differences or dependency issues.
  • Lower learning barrier: CloudFormation is a familiar technology for many AWS users. Using CDK or SAM might require knowledge of additional tools or programming languages, which could be a barrier for readers.
  • Direct understanding of AWS resources: CloudFormation templates directly define AWS resources. This makes it easier to understand the detailed settings and behavior of AWS services, increasing educational value.
  • Ease of debugging: Being a single template file, identifying and fixing errors is relatively easy. This facilitates troubleshooting for readers when implementing in their own environments.
  • Simplification of version control and maintenance: Management with a single file simplifies version control and improves long-term maintainability. The basic syntax of CloudFormation has been stable for many years, reducing the risk of future changes or deprecation.
  • Rapid deployment and testing: Without additional build steps, changes to the template can be applied and tested immediately. This allows readers to quickly try it out in their own environments.
  • Compatibility with AWS console: CloudFormation templates can be directly edited and applied in the AWS console. This enables quick changes and confirmation through the GUI, making it more accessible to a wider audience.
For these reasons, I believe that using a CloudFormation template provides a demo environment that more readers can easily understand and implement.


* The source code published in this article and other articles by this author was developed as part of independent research and is provided 'as is' without any warranty of operability or fitness for a particular purpose. Please use it at your own risk. The code may be modified without prior notice.

Architecture Diagram to be tested in this article

The configuration for adding an approval flow to AWS Step Functions using AWS Lambda, AWS CodePipeline, and Amazon EventBridge that I will try this time is as follows:

Example configuration of adding an approval flow to AWS Step Functions using AWS Lambda, AWS CodePipeline, and Amazon EventBridge
Example configuration of adding an approval flow to AWS Step Functions using AWS Lambda, AWS CodePipeline, and Amazon EventBridge

The flow is as follows:
First, in the AWS Step Functions state machine, an AWS Lambda function with waitForTaskToken specified writes the token of the AWS Step Functions state machine to the metadata of an Amazon S3 object (Source Artifact specified in AWS CodePipeline) and PUTs it to an Amazon S3 bucket, which triggers the execution of AWS CodePipeline (SSM Automation). In the approval stage of AWS CodePipeline, the approval action (Approval) confirms approval or denial with the approver via an Amazon SNS topic email notification.
An Amazon EventBridge rule detects the event of the AWS CodePipeline approval stage associated with the approval or denial decision in the approval flow, and executes an AWS Lambda function for returning results.
In the AWS Lambda function for returning results, the version of the Amazon S3 object used is identified from the contents of the AWS CodePipeline approval stage corresponding to the event, the token of the AWS Step Functions state machine is obtained from the metadata of the Amazon S3 object, and the approval result from the event is returned to the AWS Step Functions state machine.
The advantage of using the approval action of AWS CodePipeline as a component in this way is that it allows specifying the authentication of approvers and the permissions for the approval action.
When the Amazon SNS topic email notification sent by the approval action (Approval) of AWS CodePipeline is received, the approver can log in to the AWS Management Console via the link and decide whether to approve or deny only if they have an IAM role or IAM user allowed to perform the approval action.
As AWS Systems Manager Automation, which supports operation automation, and AWS CodePipeline, which supports CI/CD, have different service purposes, there are particularly differences in the timing of determining configuration details for approval flows.
Specifically, compared to the approval flow using AWS Systems Manager Automation introduced in the previous article, AWS CodePipeline requires predetermining the Amazon S3 bucket and object name for storing Source Artifacts, approval messages, confirmation URLs, etc., in the stage settings before execution.
With AWS Systems Manager Automation, you can change the IAM role of the approver, the number of approvals required for final approval from multiple approvers, approval messages, confirmation URLs, confirmation files, etc., for each Automation execution by changing the values of parameters defined in the SSM Document.
On the other hand, while AWS CodePipeline can be executed with just an Amazon S3 object PUT, AWS Systems Manager Automation needs to be executed by specifying multiple parameters and an execution IAM role.
Due to these characteristics, AWS Systems Manager Automation can be flexibly introduced to AWS Step Functions that require approval flow as a component of automation, but AWS CodePipeline requires changing the configuration of pipelines including approval flows and the configuration of AWS Step Functions that require AWS CodePipeline execution according to the CI/CD architecture being built and the purpose.

Example of AWS CloudFormation template and parameters

AWS CloudFormation template (Adding an approval flow to AWS Step Functions using AWS Lambda and AWS CodePipeline)

Example of input parameters

CodePipelineName: CodePipelineApprovalSample #Name of AWS CodePipeline  
CodePipelineS3bucketName: ho2k #Name of Amazon S3 bucket to store Artifacts in AWS CodePipeline  
CodePipelineS3bucketKeyInput: index.html #File name of Source Artifact to trigger AWS CodePipeline  
CodePipelineS3bucketKeyOutput: index_approved.html #File name of Artifact to deploy finally after approval in AWS CodePipeline  
CodePipelineS3bucketKeyContentType: text/html #Content type of Source Artifact
CodePipelineConfirmationCustomData: Approval request has been received. Please review file at the following URL to decide whether to approve or deny. #Custom data (message) to display in the confirmation dialog of the approval flow
CodePipelineConfirmationUrl: https://hidekazu-konishi.com/ #Confirmation URL to display in the confirmation dialog of the approval flow
EmailForNotification: sample@ho2k.com #Email address to send approval requests
EventRuleForAutomationResultState: ENABLED #Setting for enabling (ENABLED) or disabling (DISABLED) Amazon EventBridge  

Template body

File name: SfnApprovalCFnSfnWithCodePipelineApprovalAndEventBridge.yml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Add AWS CodePipeline Approval Action to AWS Step Functions.'
Parameters:
  EmailForNotification: 
    Type: String
    Default: "sample@ho2k.com"
  EventRuleForCodePipelineResultState:
    Type: String
    Default: ENABLED
    AllowedValues:
      - ENABLED
      - DISABLED
  CodePipelineName:
    Type: String
    Default: "CodePipelineApprovalSample"
  CodePipelineS3bucketName: 
    Type: String
    Default: "ho2k"
  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:
                  #Identify the revisionId (Amazon S3 object version) used in the Pipeline execution from the execution-id in the Event, and obtain the Step Functions token.
                  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']
                  )

                  #Get the Amazon S3 bucket and object key used from the pipeline name and pipeline version.
                  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}')
                  
                  #Get the Step Functions token from the metadata of the Amazon S3 object corresponding to the version ID.
                  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}')

                  #Get the approval result from the state in the Event.
                  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:
                  #Send task success to the SFN side with the callback token.
                  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>"
      }

Build procedure

  1. Deploy with AWS CloudFormation by entering the necessary values for the template parameters in a region that supports AWS Step Functions and AWS CodePipeline.
    After creating the AWS CloudFormation stack, an example of input parameters (in JSON format) for AWS Step Functions execution will be output as StepFunctionsInputExample in the Output field, so make a note of it.
  2. Approve the SNS topic subscription request that will be sent to the email address you entered.

Executing the demo

  1. Modify the confirmation_file_content in the JSON parameters of StepFunctionsInputExample noted in the "Build procedure" above, and use it as the input value to execute the AWS Step Functions state machine StepFunctionsWithCodePipelineApproval.
    The confirmation_file_content is the file content of the Source Artifact prepared for the demo in AWS CodePipeline. When actually using AWS CodePipeline to deploy artifacts to various AWS resources, you may need to use zip files, so the AWS Step Functions steps and AWS CodePipeline stages should be constructed according to your use case.
  2. When you receive the email for the AWS CodePipeline approval action at the email address specified during setup, choose to approve (Approve) or reject (Reject) from the AWS Management Console.
  3. Confirm that the steps of the AWS Step Functions state machine StepFunctionsWithCodePipelineApproval transition according to the chosen approval (Approve) or rejection (Reject).

Deletion procedure

  1. Delete the AWS CloudFormation stack created in the "Build procedure".

References:
Tech Blog with curated related content
What is AWS Step Functions? - AWS Step Functions
AWS Systems Manager Automation - AWS Systems Manager
What is AWS CodePipeline? - AWS CodePipeline

Summary

In this article, I tried adding an approval flow to an AWS Step Functions workflow using the approval action of AWS CodePipeline and Amazon EventBridge.

By using this method, I confirmed that a flexible approval process can be incorporated into AWS Step Functions workflows.
It was also confirmed that appropriate continuous processing can be performed not only when approval is granted but also when it is denied, enabling it to handle complex approval flows that require intricate processes.
As next steps, we could consider integrating this approval flow with other AWS services or expanding it into a multi-stage approval flow with multiple approval steps.

On the other hand, through this experiment, I found that the approval flows of AWS Systems Manager Automation, which I tried previously, and AWS CodePipeline, which I tried this time, each have their own characteristics.
While AWS Systems Manager Automation can flexibly respond to parameter changes at execution time, AWS CodePipeline provides a consistent process that is configured in advance.
This made it clear that it's important to choose the appropriate approval flow based on the use case and requirements.

I learned that by combining AWS serverless services, we can achieve flexible and applicable approval workflows while minimizing maintenance efforts.
I look forward to continuing to explore approval workflow management approaches using AWS services like this in the future.


Written by Hidekazu Konishi


Copyright © Hidekazu Konishi ( hidekazu-konishi.com ) All Rights Reserved.