この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
はじめに
こんにちは、中山です。
最近CloudFormation(以下CFn)を書く機会が多いです。いろいろと個人的に思うところもあるのですが、やはりAWS公式サービスなので他サービスとの連携が手厚くサポートされている印象があり、好きなサービスの1つです。例えばLambda-backed Custom Resourceを利用することでイベントドリブンに処理を実装できたりします。
今私が関わっている案件的にチームとして動く機会が少なかったので、CFnテンプレートはローカルで管理することが多かったです。しかし、個人で開発している分にはこれでもよいのですが、チームとして管理する場合には問題が出てきます。テンプレートのテスト、スタックの作成/更新フローなどが統一されていないと、スタックの更新時などに思わぬ事故を引き起こしがちです。また、テンプレートの管理を1人にまかせてしまうと、属人化してしまい、チームとして効率的に動けなくなる可能性があります。
そこで、今回はCFnをチームで管理するためにCI/CDパイプラインをGitHub/CodeBuild/CodePipelineで管理する方法を考えてみたので、本エントリでご紹介したいと思います。なお、本エントリ執筆にあたり以下の記事を参考にしました。それぞれとても良くまとめられているので、参照いただくとより理解が深まるかと思います。
- CodePipeline で CodeCommit/CodeBuild/CodeDeploy を繋げてデリバリプロセスを自動化してみた #reinvent
- CodePipeline で承認プロセスを設けて本番環境へリリースする #reinvent
- Serverless Frameworkの基礎と開発手法からCI/CDパイプラインの構築まで
- CodePipeline Update – Build Continuous Delivery Workflows for CloudFormation Stacks
- Continuous Delivery with AWS CodePipeline
CI/CDパイプラインの概要
今回は以下のような構成を作ってみます。
各種ソースコードはGitHubに上げておきました。ご自由にお使いください。
開発/本番環境のVPC内で動作するリソースはsrc以下のCFnテンプレートと大本のテンプレートであるcfn.ymlで管理します。CI/CDパイプラインも、今回はマネジメントコンソールではなくCFnテンプレートで構築してみました。テンプレートはこちらです。
ブランチ戦略はGitHub Flowをベースにしつつ、リリースブランチとして master
と develop
を利用します。本番環境が master
に、開発環境が develop
ブランチに相当します。つまり、以下のような開発フローを目指します。
- 開発者が
develop
ブランチからフィーチャーブランチをチェックアウト - ある程度開発の区切りがついたところでフィーチャーブランチから
develop
ブランチにマージしてプッシュ develop
ブランチにプッシュされるとCodePipelineがそれを検知し、CI/CDパイプラインが開始- CodePipelineのApprovalステージでSNSトピックに通知、開発環境のChangeSetを確認してスタックの作成/更新を承認
- 開発環境のスタックが作成/更新された後、テストなどの動作確認を実施して意図した動作をすれば
develop
ブランチからmaster
ブランチにマージしてプッシュ master
ブランチにプッシュされるとCodePipelineがそれを検知し、CI/CDパイプラインが開始- CodePipelineのApprovalステージでSNSトピックに通知、本番環境のChangeSetを確認してスタックの作成/更新を承認
- 本番環境のスタックが作成/更新される
Gitの操作で説明すると以下のように開発を進めていきます。
# ソースコードをクローン
$ git clone https://github.com/knakayama/cfn-ci-cd-demo.git
$ cd cfn-ci-cd-demo
# 開発用ブランチをフェッチ
$ git fetch origin develop:develop
# 開発用ブランチからフィーチャーブランチを作成
$ git branch feat/awesome-feature develop
# フィーチャーブランチにチェックアウト
$ git checkout feat/awesome-feature
# 開発
$ $EDITOR src/<file>
# コミット
$ git add src/<file> && git commit -m 'I developed awesome feature!'
# 開発用ブランチにマージ(ここでCodePipelineがCI/CDパイプラインを開始)
$ git checkout develop && git merge --no-ff feat/awesome-feature && git push origin develop
# 本番用ブランチにマージ(ここでCodePipelineがCI/CDパイプラインを開始)
$ git checkout master && git merge --no-ff develop && git push origin master
CodePipelineの各種ステージについて
CodePipelineにはステージという概念があり、あるステージ内でどういった処理をしたいかを定義できます。各ステージ毎にアクションを定義することで処理内容を指定可能です。アクションと指定可能な各種サービスの一覧はこちらのドキュメントにまとまっています。また、各ステージ毎に生成したアーティファクトは、インプット/アウトプットという形で連携することが可能です。つまり、あるステージのアクションで定義した処理を実施、生成したアーティファクトを別のステージに渡すことで一連のパイプラインを作成することができます。
今回はステージの設定を以下のようにしてみました。
- ソースコードのフェッチ(Source)
- テンプレートのテスト(Test)
- ChangeSetの作成(Build)
- ChangeSetの承認(Approval)
- ChangeSetの実行(Deploy)
以下に、各ステージ毎の処理内容を該当するCFnテンプレートとともに解説します。
Source
このステージではGitHubのリポジトリからソースコードをフェッチさせ、アウトプットでS3にフェッチしたソースコードをputしています。
PipelineDev:
Type: AWS::CodePipeline::Pipeline
Properties:
Name: !Sub ${AppName}-dev
RoleArn: !GetAtt PipelineRole.Arn
ArtifactStore:
Type: S3
Location: !Ref ArtifactStoreBucket
Stages:
- Name: Source
Actions:
- Name: download-source
ActionTypeId:
Category: Source
Owner: ThirdParty
Version: 1
Provider: GitHub
Configuration:
Owner: !Ref Owner
Repo: !Ref Repo
Branch: develop
OAuthToken: !Ref OAuthToken
OutputArtifacts:
- Name: SourceOutput
ハイライトしている箇所を説明します。
- 6 - 8行、23 - 24行: フェッチしたソースコードをS3にputさせています
ArtifactStore
でこのパイプラインで利用するアーティファクトの設置バケットを指定していますName
で指定した文字列がオブジェクトへのプレフィックスになります- アーティファクトの設置場所に指定したバケットを確認すると、以下のようにソースコードが設置されていることを確認できます(
Name
で指定した文字列が途中で切れていますが)
$ aws s3 cp s3://cfn-ci-cd-pipeline-artifactstorebucket-4xhl4urcb0qk/cfn-ci-cd-demo-dev/SourceOutp/21IROZ7.zip - | bsdtar -tvf -
-rwxrwxrwx 0 0 0 0 Mar 12 13:09 .gitignore
-rwxrwxrwx 0 0 0 0 Mar 12 13:09 Makefile
-rwxrwxrwx 0 0 0 0 Mar 12 13:09 buildspec.yml
-rwxrwxrwx 0 0 0 0 Mar 12 13:09 cfn.yml
<snip>
- 21行: CodePipelineを動作させる起点となるブランチ名を指定しています
- 一般的なCI/CDサービスなどでは全てのブランチでパイプラインを起動させ、例えばCircleCIであれば
circle.yml
でブランチ毎の処理を記述するパターンが多いと思いますが、CodePipelineの場合、GitHubをアクションに指定すると1ブランチ1パイプラインで設定するようです - ブランチを1つしか指定できないので、今回はCodePipelineを開発/本番用に2つ作成しています
- CodePipeline自体の料金はアクティブなパイプラインが$1/monthです
- 一般的なCI/CDサービスなどでは全てのブランチでパイプラインを起動させ、例えばCircleCIであれば
2017年4月8日追記
CodePipelineのアップデートによって始めの30日間は無料で使えるようになりました。
Test
Test用ステージではインプットで受け取ったソースコードをCodeBuildで取得し、 buildspec.yml
の内容に従って処理を実施しています。CodeBuildをTestアクションで指定できる機能は最近のアップデートで対応しました。
- Name: Test
Actions:
- InputArtifacts:
- Name: SourceOutput
Name: testing
ActionTypeId:
Category: Test
Owner: AWS
Version: 1
Provider: CodeBuild
OutputArtifacts:
- Name: TestOutput
Configuration:
ProjectName: !Ref CodeBuildProject
ハイライトしている箇所を説明します。
- 3 - 4行: Source用ステージで生成したアーティファクトをインプットに指定しています
- つまり、ソースコードをCodeBuildで取得させています
- 11 - 12行: このステージで生成したアーティファクトを後段のステージで利用するために、アウトプットさせています
- オブジェクトの内容を見ると以下のアーティファクトがputされています
$ aws s3 cp s3://cfn-ci-cd-pipeline-artifactstorebucket-4xhl4urcb0qk/cfn-ci-cd-demo-dev/TestOutput/P3WpHMR - | bsdtar -tvf -
-rwxrwxrwx 0 0 0 0 Mar 11 19:18 packaged.yml
-rwxrwxrwx 0 0 0 0 Mar 11 19:18 param.dev.json
-rwxrwxrwx 0 0 0 0 Mar 11 19:18 param.prod.json
CodeBuild自体の設定と、 buildspec.yml
の内容は以下の通りです。
pipeline/ci-cd.yml
CodeBuildProject:
Type: AWS::CodeBuild::Project
Properties:
Name: !Sub ${AppName}
ServiceRole: !GetAtt CodeBuildRole.Arn
Artifacts:
Type: CODEPIPELINE
Environment:
Type: LINUX_CONTAINER
ComputeType: BUILD_GENERAL1_SMALL
Image: aws/codebuild/ubuntu-base:14.04
EnvironmentVariables:
- Name: AWS_REGION
Value: !Ref AWS::Region
- Name: S3_BUCKET
Value: !Ref CodeBuildBucket
Source:
Type: CODEPIPELINE
今回はCodePipelineと連携させているのでシンプルな内容になっています。CFnでCodeBuildを作成する方法は、以前こちらのエントリでご紹介しました。よろしければ参照ください。
buildspec.yml
version: 0.1
phases:
install:
commands:
- |
pip install -U pip
pip install -r requirements.txt
pre_build:
commands:
- |
[ -d .cfn ] || mkdir .cfn
aws configure set default.region $AWS_REGION
for template in src/* cfn.yml; do
echo "$template" | xargs -I% -t aws cloudformation validate-template --template-body file://%
done
build:
commands:
- |
aws cloudformation package \
--template-file cfn.yml \
--s3-bucket $S3_BUCKET \
--output-template-file .cfn/packaged.yml
artifacts:
files:
- .cfn/*
- params/*
discard-paths: yes
ハイライトしている箇所を説明します。
- 14 - 16行:
aws cloudformation validate-template
でテンプレートのValidationを実施しています - 20 - 23行:
aws cloudformation package
を利用してcfn.yml
で定義しているTemplateURL
プロパティの変換及びアーティファクトをS3にputさせています- このコマンドはAWS Serverless Application Modelと組み合わせて使われることが多いと思いますが、CFnテンプレートでも使えます
- 少し紛らわしいですが、このコマンドでS3にアップロードされるアーティファクトは後段のステージでは特に不要なので、CodeBuild用バケットに保存させています
- 25 - 29行: 後段のステージで利用するアーティファクトをアップロードさせています
- これが先程お見せしたように
TestOutput
プレフィックス付きでアップロードされます
- これが先程お見せしたように
Build
CodePipelineはDeployアクションでCloudFromationを実行することが可能です。ステージ名は今回Buildとしていますが、Buildアクションには対応していないのでDeployアクションを指定しています。
- Name: Build
Actions:
- InputArtifacts:
- Name: TestOutput
Name: create-changeset
ActionTypeId:
Category: Deploy
Owner: AWS
Version: 1
Provider: CloudFormation
OutputArtifacts:
- Name: BuildOutput
Configuration:
ActionMode: CHANGE_SET_REPLACE
ChangeSetName: changeset
RoleArn: !GetAtt CFnRole.Arn
Capabilities: CAPABILITY_IAM
StackName: !Sub ${AppName}-dev
TemplatePath: !Sub TestOutput::${TemplateFilePath}
TemplateConfiguration: !Sub TestOutput::${StackConfigDev}
ハイライトしている箇所を説明します。
- 14 - 15行:
ActionMode
でChangeSetの作成を、ChangeSetName
でChangeSet名を指定しています - 19行:
TemplatePath
プロパティでChangeSetを作成するテンプレートを指定しています<アウトプット>::<テンプレートへのパス>
の形式で指定可能です- 今回の場合は
TestOutput::packaged.yml
になります
- 20行:
TemplateConfiguration
プロパティでテンプレートに利用するパラメータを指定可能です- 先程と同じように
<アウトプット>::<パラメータへのパス>
の形式で指定可能です - この場合開発用なので
TestOutput::param.dev.json
になります - パラメータはJSON形式で以下のように記述します
- 先程と同じように
{
"Parameters": {
"Stage": "dev",
"VPCCidrBlock": "192.168.0.0/16",
"KeyName": "mykey",
"InstanceType": "t2.nano"
}
}
Approval
前段のステージで作成したChangeSetを承認するため、SNSトピックに通知させます。
- Name: Approval
Actions:
- Name: approve-changeset
ActionTypeId:
Category: Approval
Owner: AWS
Version: 1
Provider: Manual
Configuration:
NotificationArn: !Ref CodePipelineSNSTopic
ExternalEntityLink: !Sub https://console.aws.amazon.com/cloudformation/home?region=${AWS::Region}
CustomData: Please review changeset
テンプレート的には単純です。今回トピックのエンドポイントにEmailを指定しているので、通知が来ると以下のようなメールが届きます。
Deploy
Build用ステージで作成したChangeSetを実行しています。
- Name: Deploy
Actions:
- Name: execute-changeset
ActionTypeId:
Category: Deploy
Owner: AWS
Version: 1
Provider: CloudFormation
Configuration:
StackName: !Sub ${AppName}-dev
ActionMode: CHANGE_SET_EXECUTE
ChangeSetName: changeset
RoleArn: !GetAtt CFnRole.Arn
ハイライトしている箇所を説明します。
- 11行:
ActionMode
でCHANGE_SET_EXECUTE
を指定することにより、ChangeSetを実行させています - 12行: 今回対象のChangeSet名は決め打ち(
changeset
)にしています- 現状ワイルドカード形式には対応していないためです
動作確認
スタックの作成後、以下のように出力されればOKです。
- パイプラインが開発/本番用に2つ作成されていることを確認
$ aws codepipeline list-pipelines \
--region us-east-1 \
--query 'pipelines[].name' \
--output text
cfn-ci-cd-demo-dev cfn-ci-cd-demo-prod
- 各ステージが成功していることを確認
$ aws codepipeline get-pipeline-state \
--name "$(aws codepipeline list-pipelines \
--query 'pipelines[?name==`cfn-ci-cd-demo-dev`].name' \
--output text --region us-east-1)" \
--region us-east-1 \
--query 'stageStates[].[latestExecution.status,stageName]' \
--output text
Succeeded Source
Succeeded Test
Succeeded Build
Succeeded Approval
Succeeded Deploy
- 各スタックが正常に作成されていることを確認
$ aws cloudformation list-stacks \
--region us-east-1 \
--query 'StackSummaries[?contains(StackName, `cfn-ci-cd-demo-dev`)].[StackStatus, StackName]' \
--stack-status-filter CREATE_COMPLETE \
--output text
CREATE_COMPLETE cfn-ci-cd-demo-dev-Compute-YONZ9VVH5V2T
CREATE_COMPLETE cfn-ci-cd-demo-dev-Custom-PVWQCPRCD440
CREATE_COMPLETE cfn-ci-cd-demo-dev-SG-307A5W6Y8F42
CREATE_COMPLETE cfn-ci-cd-demo-dev-NW-KQ1QKGPNKC9C
CREATE_COMPLETE cfn-ci-cd-demo-dev-IAM-RD4J4UXZFICD
CREATE_COMPLETE cfn-ci-cd-demo-dev
改善点
とりあえず「動くもの」はできましたが、まだまだ改善すべき余地はあると思っています。以下に私が気付いたポイントをご紹介します。
テンプレートのテスト
今回テンプレートのテストは aws cloudformation validate-template
しているだけです。一応開発環境で動作確認してから本番環境へ適用するというフローにはなっていますが、もう少しテストを実施した方がよいかと思います。CFnのテストをどうやるか/どこまでやるか/どの段階でやるか(アプリケーション側に寄せる)などなどいろいろと考慮するポイントはあると思いますが、まだベストプラクティス的なものは自分の中でまとまっていません。
ChangeSetの差分
今回用意したデモ用テンプレートは AWS::CloudFormation::Stack
リソースでスタックをネストさせています。この場合、あるスタックへの変更が実施されると他のスタックにも影響してしまい、ChangeSetの出力が分かりづらくなる印象があります。つまり、Approval用ステージでレビューするフェーズを設けているにも関わらず、「何が変更されるのか」を確認するのに時間がかかりそうです。
タグを利用したリリース
現状CodePipelineはGitのタグを認識できないようです。 master
ブランチへのプッシュを契機にChangeSetの実行を含むパイプラインが開始されてしまうと、オペミスが発生しそうです。本当は明示的にタグが付いた場合のみChangeSetの実行をするという処理にしたかったのですが、現時点では難しそうです。
一応CodePipelineにはカスタムアクションという標準では用意されていない任意のアクションを定義する機能があります。この機能を利用すればタグの有無に応じて処理を分けるといったこともできそうですが、もう少し調査してみます。
まとめ
いかがだったでしょうか。
GitHub/CodeBuild/CodePipelineを利用したCloudFromationのCI/CDパイプラインについてご紹介しました。冒頭でも記載しましたが、チーム開発をしていく上でCI/CDパイプラインを整備することは非常に重要です。今回はAWSサービスを中心としてパイプラインを作成しましたが、同等の機能を他サービスでも実現できると思います。ご自身の環境にあった構成を見つけるとよいのではないでしょうか。
本エントリがみなさんの参考になれば幸いに思います。