はじめに
私は現在AWSの設計構築のメンバーとして主にCloudFormationの構築に携わっています。
CloudFormation構築メンバーは他にも3名いましたが、プロジェクトから離脱したり、別のプロジェクトへ異動してしまったりなどがあり、現在では私一人となってしまいました。
そのため、CloudFormationテンプレートの改修が入った場合には全面的に私が対応しています。
(大変な反面全て網羅できるため勉強にはなりますね)
CI/CDを担当していたメンバーも既にいなくなってしまったため、ここ最近で大きな改修が入り、そのコードの書き方の効率が良いと思いましたので今回はその効率性について書き記していきたいと思います。
課題
CodePipeline/CodeBuildを使用して複数の「役割」や「名称」の異なるコンテナにアプリケーションをリリースする際に、複数のResourceをコード内に記述するとコード量が多くなり、無駄が多くなる。
解決策
CloudFormationのネストスタックを使用して親子スタックで定義することで、複数のResourceコードを記述することなく、効率化することができる。
ネストされたスタックとは
AWS Black Belt


詳細は上記のAWS Black Beltに記載がありますのでそちらをご覧ください。
構成例
実際の構成例は以下です。
親スタック A
親スタック B
親スタック C
親スタック D
└子スタック (CodePipelineひな形)
CodePipelineA~Dまでのリソースが存在し、このリソースは全て名称が異なります。
例えば、以下のように各コンテナが機能ごとに分かれており、その各コンテナに対するCodePipelineを定義する場合です。
authloginapifrontend
上記の各機能ごとのCodePipelineリソースを作成する場合、同じリソースを4つ定義しなければいけないのかな?と思い同じコードを何度も記述しなければいけないのは非効率です。
そこで、親スタックからパラメータだけを渡すコードを書き、子スタック側にはそのパラメータを受け取るコードを一つ定義することで効率化することができます。
実際にコードの例を見ていきましょう。
CloudFormationの例
親スタック
親スタックはParametersセクションで定義している値を子スタックに継承するように記述します。
# 認証用CodePipeline
CreateCodePipelineA:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: "https://xxxxxxx-${AccountId}.s3.ap-northeast-1.amazonaws.com/cdp-template.yaml"
Parameters:
stackPrefix: !Ref stackPrefix
stackEnvironment: !Ref stackEnvironment
stackConnectionArn: !Ref stackConnectionArn
stackCodePipelineName: "auth" # authを名称として子スタックに継承
# ログイン用CodePipeline
CreateCodePipelineB:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: "https://xxxxxxx-${AccountId}.s3.ap-northeast-1.amazonaws.com/cdp-template.yaml"
Parameters:
stackPrefix: !Ref stackPrefix
stackEnvironment: !Ref stackEnvironment
stackConnectionArn: !Ref stackConnectionArn
stackCodePipelineName: "login" # loginを名称として子スタックに継承
# API用CodePipeline
CreateCodePipelineC:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: "https://xxxxxxx-${AccountId}.s3.ap-northeast-1.amazonaws.com/cdp-template.yaml"
Parameters:
stackPrefix: !Ref stackPrefix
stackEnvironment: !Ref stackEnvironment
stackConnectionArn: !Ref stackConnectionArn
stackCodePipelineName: "api" # apiを名称として子スタックに継承
# フロントエンド用CodePipeline
CreateCodePipelineD:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: "https://xxxxxxx-${AccountId}.s3.ap-northeast-1.amazonaws.com/cdp-template.yaml"
Parameters:
stackPrefix: !Ref stackPrefix
stackEnvironment: !Ref stackEnvironment
stackConnectionArn: !Ref stackConnectionArn
stackCodePipelineName: "frontend" # frontendを名称として子スタックに継承
子スタック
## ::RESOURCES::
# Resource Section: Defines AWS resources included in the stack
Resources:
# CodePipeline作成
CodePipeline:
Type: "AWS::CodePipeline::Pipeline"
Properties:
Name: !Sub "${stackPrefix}-${stackEnvironment}-codepipeline-${stackCodePipelineName}"
PipelineType: V2
ExecutionMode: QUEUED
RoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/${stackPrefix}-${stackEnvironment}-role-codepipeline"
ArtifactStore:
Type: "S3"
Location: !Sub "${stackPrefix}-${stackEnvironment}-s3-codepipeline-artifact-${AWS::AccountId}"
Stages:
- Name: "Source"
Actions:
- Name: "From_GitHub"
ActionTypeId:
Category: Source
Owner: AWS
Provider: CodeStarSourceConnection
Version: "1"
OutputArtifacts:
- Name: SourceArtifact
Configuration:
ConnectionArn: !Sub "arn:aws:codeconnections:ap-northeast-1:${AWS::AccountId}:connection/${stackConnectionArn}"
FullRepositoryId: !Sub "infra/${stackPrefix}-${stackEnvironment}-git-${stackCodePipelineName}"
BranchName: "main"
DetectChanges: true
- Name: "From_S3"
ActionTypeId:
Category: Source
Owner: AWS
Provider: S3
Version: "1"
OutputArtifacts:
- Name: S3SourceArtifact
Configuration:
S3Bucket: !Sub "${stackPrefix}-${stackEnvironment}-s3-${AWS::AccountId}"
S3ObjectKey: !Sub "application/${stackPrefix}/${stackCodePipelineName}/${stackCodePipelineName}.zip"
PollForSourceChanges: "false"
- Name: Build
Actions:
- Name: Build
ActionTypeId:
Category: Build
Owner: AWS
Provider: CodeBuild
Version: "1"
InputArtifacts:
- Name: SourceArtifact
- Name: S3SourceArtifact
Configuration:
ProjectName: !Sub "${stackPrefix}-${stackEnvironment}-codebuild"
PrimarySource: SourceArtifact
EnvironmentVariables: !Sub |
[
{"name":"AWS_DEFAULT_REGION", "value":"ap-northeast-1", "type":"PLAINTEXT"},
{"name":"FILENAME", "value":"${stackCodePipelineName}", "type":"PLAINTEXT"},
{"name":"TARGET_ACCOUNT", "value":"${AWS::AccountId}", "type":"PLAINTEXT"},
{"name":"ECR_REPOSITORY", "value":"${stackCodePipelineName}", "type":"PLAINTEXT"},
{"name":"TARGET_ECR_REPOSITORY", "value":"${stackCodePipelineName}", "type":"PLAINTEXT"},
{"name":"IMAGE_TAG", "value":"${stackImageTag}", "type":"PLAINTEXT"},
{"name":"S3BUCKET", "value":"${stackPrefix}-${stackEnvironment}-s3-${AWS::AccountId}/dev", "type":"PLAINTEXT"}
]
継承の仕組み
親スタックの論理IDはCodePipelineA~Dが定義されています。
また、各リソースに定義されているParametersには以下のように名称の異なるパラメータを定義しています。
stackCodePipelineName: "auth"stackCodePipelineName: "login"stackCodePipelineName: "api"stackCodePipelineName: "frontend"
このパラメータを子スタックへ継承することで、上記パラメータを受け継いだ子スタックが、それぞれのCodePipelineを構築する仕組みです。
子スタックのCodePipelineのリソースの一部を再掲します。
# CodePipeline作成
CodePipeline:
Type: "AWS::CodePipeline::Pipeline"
Properties:
Name: !Sub "${stackPrefix}-${stackEnvironment}-codepipeline-${stackCodePipelineName}"
Properties.Nameに記述されている!Sub "${stackPrefix}-${stackEnvironment}-codepipeline-${stackCodePipelineName}"の箇所で${stackCodePipelineName}が親スタックからパラメータを受け取って、auth~frontendの名前のCodePipelineを構築します。
イメージは以下です。
上記のようにしておくことで、別のCodePipelineが増えたときに、子スタックをいじることなく、親スタック側に増えた分のパラメータの異なる情報を定義してやれば良いわけです。
まとめ
CloudFormationも書き方次第で色々と応用できることがわかりました。
私自身IaCの実務はCloudFormationでしか触ったことがないのですが、今後も新たな発見があれば記事にまとめたいと思います。