はじめに
これまでCloudFormationのスタック作成/更新はコンソールから実施していました。
単純に更新するだけであれば、手間のかかるような作業ではありませんが、cfn-lint, cfn-guardなどによるテストを組み込みたいとなると複雑化していきます。
そのため、スタックの更新をCI/CDとして組み込んで効率化したいと思います。
今回は導入編ということで、以下の流れを試してみます。
スモールスタートのためテストのステージはありません。
- CodeCommitにテンプレートを同期
- CloudFormationの変更セットを作成
- 手動承認
- 承認された場合は変更セットを実行
※詳細に書くと、CodePipelineをトリガーするEventBrigeやArtifactを格納するS3バケットなども必要ですが、流れとしては上図の通りです。
実装
リソースはCloudFormationで作成をします。
テンプレート以下を使用しており、CI/CDで作成するリソースはS3バケットのみを想定しています。
AWSTemplateFormatVersion: "2010-09-09"
Description: CICD CFn
Parameters:
Environment:
Type: String
Default: prod
AllowedValues:
- prod
- dev
- dr
CFnTemplateName:
Type: String
Description: The name of the CloudFormation template file in the source repository
Default: s3.yml
AllowedPattern: '^[a-zA-Z0-9._-]+\.yml$'
CFnConfigurationName:
Type: String
Description: The name of the CloudFormation configuration file in the source repository
Default: cfn-config.json
AllowedPattern: '^[a-zA-Z0-9._-]+\.json$'
CFnStackName:
Type: String
Description: The name of the CloudFormation stack to create or update
Default: cicd-cfn-stack
Resources:
###########################
## CodeCommit
###########################
CodeCommitMain:
Type: AWS::CodeCommit::Repository
Properties:
RepositoryName: !Sub 'cicd-codecommit-${Environment}'
Tags:
- Key: Env
Value: !Ref Environment
##############################
# S3 Bucket
##############################
ArtifactBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub 'cicd-artifact-bucket-${AWS::AccountId}'
##############################
# CodePipeline
##############################
CFnPipeline:
Type: AWS::CodePipeline::Pipeline
Properties:
Name: cfn-pipeline
PipelineType: V2
RoleArn: !GetAtt CodePipelineServiceRole.Arn
ArtifactStore:
Type: S3
Location: !Ref ArtifactBucket
Stages:
- Name: Source
Actions:
- Name: SourceAction
RunOrder: 1
RoleArn: !GetAtt PipelineSourceRole.Arn
ActionTypeId:
Category: Source
Owner: AWS
Provider: CodeCommit
Version: '1'
Configuration:
RepositoryName: !GetAtt CodeCommitMain.Name
BranchName: main
PollForSourceChanges: false
OutputArtifacts:
- Name: SourceOutput
- Name: CreateChangeSet
Actions:
- Name: CloudFormation
RunOrder: 1
ActionTypeId:
Category: Deploy
Owner: AWS
Provider: CloudFormation
Version: '1'
Configuration:
ActionMode: CHANGE_SET_REPLACE
StackName: !Ref CFnStackName
ChangeSetName: pipeline-changeset
TemplatePath: !Sub "SourceOutput::${CFnTemplateName}"
TemplateConfiguration: !Sub "SourceOutput::${CFnConfigurationName}"
Capabilities: CAPABILITY_AUTO_EXPAND
RoleArn: !GetAtt CFnExecuteRole.Arn
InputArtifacts:
- Name: SourceOutput
- Name: ManualApprove
Actions:
- Name: Approval
RunOrder: 1
ActionTypeId:
Category: Approval
Owner: AWS
Provider: Manual
Version: '1'
Configuration:
CustomData: Please review the change set
ExternalEntityLink: !Sub "https://${AWS::Region}.console.aws.amazon.com/cloudformation/home?region=${AWS::Region}#/stacks/"
- Name: ExecutionChangeSet
Actions:
- Name: CloudFormation
RunOrder: 1
RoleArn: !GetAtt PipelineActionRole.Arn
ActionTypeId:
Category: Deploy
Owner: AWS
Provider: CloudFormation
Version: '1'
Configuration:
ActionMode: CHANGE_SET_EXECUTE
StackName: !Ref CFnStackName
ChangeSetName: changeset-by-pipeline
InputArtifacts:
- Name: SourceOutput
##############################
# IAM
##############################
CodePipelineServiceRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub CodePipeline-Service-Role-${AWS::Region}
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- codepipeline.amazonaws.com
Action:
- sts:AssumeRole
Path: /
CodePipelineServiceRolePolicy:
Type: AWS::IAM::RolePolicy
Properties:
RoleName: !Ref CodePipelineServiceRole
PolicyName: CodePipelineServicePolicy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- s3:GetObject*
- s3:GetBucket*
- s3:List*
- s3:PutObject*
- s3:DeleteObject*
- s3:Abort*
Resource:
- !GetAtt ArtifactBucket.Arn
- !Sub '${ArtifactBucket.Arn}/*'
- Effect: Allow
Action:
- cloudformation:CreateStack
- cloudformation:UpdateStack
- cloudformation:DescribeStacks
- cloudformation:DescribeStackResources
- cloudformation:DescribeStackEvents
- cloudformation:GetTemplate
- cloudformation:DescribeChangeSet
- cloudformation:CreateChangeSet
- cloudformation:DeleteChangeSet
- cloudformation:ExecuteChangeSet
Resource:
- !Sub arn:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/${CFnStackName}/*
- Effect: Allow
Action:
- sts:AssumeRole
Resource:
- !GetAtt PipelineSourceRole.Arn
- !GetAtt PipelineActionRole.Arn
- Effect: Allow
Action:
- iam:PassRole
Resource:
- !GetAtt PipelineActionRole.Arn
- !GetAtt PipelineSourceRole.Arn
- !GetAtt CFnExecuteRole.Arn
PipelineSourceRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub CodePipeline-Source-Role-${AWS::Region}
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
AWS: !GetAtt CodePipelineServiceRole.Arn
Action:
- sts:AssumeRole
Path: /
PipelineSourceRolePolicy:
Type: AWS::IAM::RolePolicy
Properties:
RoleName: !Ref PipelineSourceRole
PolicyName: PipelineSourceRolePolicy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- codecommit:CancelUploadArchive
- codecommit:GetBranch
- codecommit:GetCommit
- codecommit:GetRepository
- codecommit:GetUploadArchiveStatus
- codecommit:UploadArchive
Resource:
- !GetAtt CodeCommitMain.Arn
- Effect: Allow
Action:
- s3:GetObject*
- s3:GetBucket*
- s3:List*
- s3:PutObject*
- s3:DeleteObject*
- s3:Abort*
Resource:
- !GetAtt ArtifactBucket.Arn
- !Sub '${ArtifactBucket.Arn}/*'
PipelineActionRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub CodePipeline-Action-Role-${AWS::Region}
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
AWS: !GetAtt CodePipelineServiceRole.Arn
Action:
- sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AWSCloudFormationFullAccess
Path: /
CFnExecuteRole:
Type: AWS::IAM::Role
Properties:
RoleName: CFn-Execute-Role
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: cloudformation.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonS3FullAccess
Path: /
##############################
# EventBridge
##############################
CodeCommitTriggerRole:
Type: AWS::IAM::Role
Properties:
RoleName: Amazon_EventBridge_Rule_Target_CFn_Pipeline
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: events.amazonaws.com
Action: sts:AssumeRole
MaxSessionDuration: 3600
CodeCommitTriggerPolicy:
Type: AWS::IAM::RolePolicy
Properties:
PolicyName: start-pipeline-policy
RoleName: !Ref CodeCommitTriggerRole
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- codepipeline:StartPipelineExecution
Resource:
- !Sub arn:${AWS::Partition}:codepipeline:${AWS::Region}:${AWS::AccountId}:${CFnPipeline}
PipelineTriggerRule:
Type: AWS::Events::Rule
Properties:
Name: codepipeline-trigger-rule
EventPattern: !Sub >-
{"source":["aws.codecommit"],"detail-type":["CodeCommit Repository State
Change"],"resources":["${CodeCommitMain.Arn}"],"detail":{"event":["referenceCreated","referenceUpdated"],"referenceType":["branch"],"referenceName":["main"]}}
State: ENABLED
EventBusName: default
Targets:
- Id: codepipeline-trigger
Arn: !Sub arn:${AWS::Partition}:codepipeline:${AWS::Region}:${AWS::AccountId}:${CFnPipeline}
RoleArn: !GetAtt CodeCommitTriggerRole.Arn
こちらでスタックを作成すると、以下のリソースが作成されます。
- CodeCommit
- CodePipeline
- EventBridge Rule
- S3
- IAMロール / ポリシー
CodePipelineは作成後に初回のパイプラインが動きますが、現時点ではCodeCommitへテンプレート等の資材を格納していないため、エラーになります。

CodeCommitに必要資材を格納し、動作を確かめてみます。
必要な資材は以下の2つです。
- テンプレートファイル:
CFnTemplateNameで指定した値 (s3.ymlなど) - テンプレート構成ファイル:
CFnConfigurationNameで指定した値(cfn-config.jsonなど)
資材は以下の内容で用意しました。
こちらはパイプラインで自動作成されるものになります。
- s3.yml
CI/CDで作成するAWSリソースを定義したテンプレートです。
AWSTemplateFormatVersion: "2010-09-09"
Description: S3 Resource
Parameters:
Environment:
Type: String
Default: Dev
AllowedValues:
- Prod
- Dev
- DR
Mappings:
BucketName:
Environment:
Prod: prod
Test: test
Dev: dev
Resources:
CommonBucket:
Type: AWS::S3::Bucket
Properties:
BucketName:
!Join
- "-"
- - !FindInMap [BucketName, Environment, !Ref Environment]
- "guard"
- "bucket"
- !Ref AWS::Region
- !Ref AWS::AccountId
Tags:
- Key: Env
Value: !Ref Environment
- cfn-config.json
テンプレートで使用するパラメータの設定や共通で付与するタグ、スタックポリシーの設定を定義する構成ファイルです。
{
"Parameters" : {
"Environment" : "Prod"
},
"Tags" : {
"Department" : "Marketing"
}
}
資材を準備したらCodeCommitにアップロードしていきます。
Gitを使用するか、面倒であればコンソールからファイルの追加でも問題ありません。

ファイルをアップロードするとパイプラインが動いています。
変更セットの作成まで完了し、手動承認のステージで保留中となっています。

CloudFormationの画面から変更セットを参照し、期待通りの差分かを確認します。
下図はCodeCommitにアップロードしたテンプレートの内容と一致するため期待通りでした。

問題がなければ「承認します」を選択し、送信します。
変更セットに問題があれば、「却下します」を選択することで、変更セットの実行を行いません。

最後のステージまですべて成功していれば、CloudFormationスタックの自動デプロイは完了です。

CloudFormation上からもスタックの作成が確認できます。

CodeCommitに同期したテンプレートへ変更をかけると、再度パイプラインが起動し、同じスタックに対する変更セットが作成されます。
以上で、CodePipelineを使用したCloudFormationのスタック更新は完了です。
料金はほとんどかからない想定ですが、今回作成したリソースは削除しておきましょう。
まとめ
今回はCodePipelineを使用したCloudFormationのスタック更新を実施しました。
ひとまず動く物は作れましたが、考慮すべき点はまだまだありそうです。
今後は以下を調査/検証できればなと思います。
- CodeBuildによるテストの追加
- スタックがネスト構造の際の実装方法
- CodeCommitの更新対象による、パイプラインの起動を制御
- CodePipelineのIAM権限について
