GitHub/CodeBuild/CodePipelineを利用してCloudFormationのCI/CDパイプラインを構築する
はじめに
こんにちは、中山です。
最近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サービスを中心としてパイプラインを作成しましたが、同等の機能を他サービスでも実現できると思います。ご自身の環境にあった構成を見つけるとよいのではないでしょうか。
本エントリがみなさんの参考になれば幸いに思います。