小西秀和です。
以前の記事でAWS Systems Manager Automationの承認アクションを使用してAWS Step Functionsのワークフローへ承認フローを追加する方法を紹介しました。
- AWS Step Functionsのワークフローへ承認フローを追加する方法(AWS Systems Manager Automation編)
- AWS Step Functionsのワークフローへ承認フローを追加する方法(AWS Systems Manager Automation & Amazon EventBridge編)
以前の記事ではAWS Lamba関数で実行したAWS Systems Manager Automationの承認結果をAWS Lamba関数またはAmazon EventBridge経由でAWS Step Functionsステートマシンへ返却していました。
今回はAWS Systems Manager Automationの代わりにAWS CodePipelineによる承認フローを追加して、承認結果をAmazon EventBridgeルールで検知し、AWS Step Functionsへ返却する方法を試してみたいと思います。
この記事も前述の記事と同様に以下の動機および意図から作成したものです。
近年、AI技術の急速な進化により、従来人間が手動で行っていた承認プロセスを生成AIで置き換えたり、強力にサポートしたりすることが可能になってきました。しかし、専門知識や権限を持つ人間による最終判断も依然として重要です。
そこで私は、将来的に生成AIを承認フローに組み込むことを見据え、AWS Step Functionsを活用した承認フローシステムをAWSサービスを使用して試作しました。この試作の主な目的は以下の通りです。
なお、本記事ではAWS CloudFormationテンプレートを使用してこのデモを構築しています。AWS CDKやAWS SAMなど、より高度なIaCツールを使用しなかった理由には以下のことが挙げられます。
- APIを介して承認フローをシステム化することで、人間と生成AIの間で意思決定プロセスを柔軟に切り替えられる
- 初期段階では人間が承認を行い、生成AIの能力が十分と判断された場合に段階的にAIへ移行できる
- 生成AIの判断に不安がある場合や、最終確認が必要な場合は、人間が承認プロセスに介入できる
- 人間と生成AIを組み合わせた多段階承認フローにより、より高い精度での意思決定が可能になる
これらの理由により、CloudFormationテンプレートを使用することで、より多くの読者の方々が容易に理解し、実践できるデモ環境を提供できると考えました。
- 再現性と可搬性: CloudFormationテンプレート一つで完結させることで、環境に依存せず、誰でも同じ結果を得られるようにしました。CDKやSAMを使用すると、バージョンの違いや依存関係の問題が生じる可能性があり、再現性が低下する恐れがあります。
- 学習障壁の低さ: 多くのAWSユーザーにとって、CloudFormationは馴染みのある技術です。CDKやSAMを使用すると、追加のツールやプログラミング言語の知識が必要になる場合があり、読者の方々にとって障壁となる可能性があります。
- AWSリソースの直接的な理解: CloudFormationテンプレートでは、AWSリソースを直接定義します。これにより、AWSサービスの詳細な設定や動作を理解しやすくなり、教育的な価値が高まります。
- デバッグの容易さ: 単一のテンプレートファイルであるため、エラーの特定や修正が比較的容易です。これは、読者の方々が自身の環境で実装する際のトラブルシューティングを容易にします。
- バージョン管理とメンテナンスの簡素化: 単一ファイルでの管理は、バージョン管理を簡素化し、長期的な保守性を高めます。CloudFormationの基本的な構文は長年にわたり安定しており、将来的な変更や非推奨化のリスクが低いです。
- 迅速な展開とテスト: 追加のビルドステップが不要なため、テンプレートの変更をすぐに適用してテストできます。これにより、読者の方々が自身の環境で素早く試すことができます。
- AWSコンソールとの互換性: CloudFormationテンプレートは、AWSコンソールで直接編集・適用できます。これにより、GUIを通じた迅速な変更や確認が可能となり、より多くの方々にとって扱いやすいものとなります。
※本記事および当執筆者のその他の記事で掲載されているソースコードは自主研究活動の一貫として作成したものであり、動作を保証するものではありません。使用する場合は自己責任でお願い致します。また、予告なく修正することもありますのでご了承ください。
今回の記事の内容は次のような構成になっています。
本記事で試す構成図
今回試す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>"
}
構築手順
- AWS Step FunctionsやAWS CodePipelineをサポートしているリージョンで、テンプレートのパラメータに必要な値を入力してAWS CloudFormationでデプロイする。
AWS CloudFormationスタック作成後にOutputフィールドへAWS Step Functions実行時の入力パラメータ例(JSON形式)がStepFunctionsInputExampleとして出力されるのでメモしておく。 - 入力したEmailアドレスにSNSトピックのサブスクリプション承認リクエストが届くので承認しておく。
デモの実行
上記「構築手順」でメモした
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のステージは用途に合わせて構築する必要があります。構築時に指定したEmailアドレスにAWS CodePipeline承認アクションのメールが届くので、承認(
Approve)するか拒否(Reject)するかをAWSマネジメントコンソールから選択する。- AWS Step Functionsステートマシン
StepFunctionsWithCodePipelineApprovalのステップが選択した承認(Approve)、拒否(Reject)の通りに遷移することを確認する。
削除手順
- 「構築手順」で作成した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 Functionsのワークフローに柔軟な承認プロセスを組み込むことが確認できました。
また、承認が許諾された場合に加えて、承認が拒否された場合にも適切に継続的な処理を行うことができ、複雑な承認フローが必要なプロセスにも対応可能です。
次のステップとして、この承認フローを他のAWSサービスと連携させたり、複数の承認ステップを持つ多段階承認フローへ拡張したりすることも考えられます。
一方で、この試みの中で以前に試したAWS Systems Manager Automationと今回のAWS CodePipelineの承認フローには、それぞれ特徴があることが分かりました。
AWS Systems Manager Automationは実行時のパラメータ変更に柔軟に対応できる一方、AWS CodePipelineは事前に設定された一貫したプロセスを提供します。
これによって、用途や要件に応じて適切な承認フローを選択することが重要であることが明確になりました。
AWSのサーバーレスサービスを組み合わせることで、メンテナンスの手間を最小限に抑えつつ、柔軟で応用可能な承認ワークフローを実現できることが分かりました。
今後もこのようなAWSサービスを使用したアプローチによる承認ワークフロー管理を試していきたいと思います。
次回は今回試したAWS Step Function承認フローを別のAWS Step Functionsのワークフローから呼び出して多段階承認フローを作成する方法を紹介したいと思います。
- [English Edition] How to Add an Approval Flow to AWS Step Functions Workflow (AWS CodePipeline and Amazon EventBridge Edition)
