GitHub/CodeBuild/CodePipelineを利用してCloudFormationのCI/CDパイプラインを構築する

2017.03.13

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

こんにちは、中山です。

最近CloudFormation(以下CFn)を書く機会が多いです。いろいろと個人的に思うところもあるのですが、やはりAWS公式サービスなので他サービスとの連携が手厚くサポートされている印象があり、好きなサービスの1つです。例えばLambda-backed Custom Resourceを利用することでイベントドリブンに処理を実装できたりします。

今私が関わっている案件的にチームとして動く機会が少なかったので、CFnテンプレートはローカルで管理することが多かったです。しかし、個人で開発している分にはこれでもよいのですが、チームとして管理する場合には問題が出てきます。テンプレートのテスト、スタックの作成/更新フローなどが統一されていないと、スタックの更新時などに思わぬ事故を引き起こしがちです。また、テンプレートの管理を1人にまかせてしまうと、属人化してしまい、チームとして効率的に動けなくなる可能性があります。

そこで、今回はCFnをチームで管理するためにCI/CDパイプラインをGitHub/CodeBuild/CodePipelineで管理する方法を考えてみたので、本エントリでご紹介したいと思います。なお、本エントリ執筆にあたり以下の記事を参考にしました。それぞれとても良くまとめられているので、参照いただくとより理解が深まるかと思います。

CI/CDパイプラインの概要

今回は以下のような構成を作ってみます。

aws

各種ソースコードはGitHubに上げておきました。ご自由にお使いください。

開発/本番環境のVPC内で動作するリソースはsrc以下のCFnテンプレートと大本のテンプレートであるcfn.ymlで管理します。CI/CDパイプラインも、今回はマネジメントコンソールではなくCFnテンプレートで構築してみました。テンプレートはこちらです。

ブランチ戦略はGitHub Flowをベースにしつつ、リリースブランチとして masterdevelop を利用します。本番環境が master に、開発環境が develop ブランチに相当します。つまり、以下のような開発フローを目指します。

  1. 開発者が develop ブランチからフィーチャーブランチをチェックアウト
  2. ある程度開発の区切りがついたところでフィーチャーブランチから develop ブランチにマージしてプッシュ
  3. develop ブランチにプッシュされるとCodePipelineがそれを検知し、CI/CDパイプラインが開始
  4. CodePipelineのApprovalステージでSNSトピックに通知、開発環境のChangeSetを確認してスタックの作成/更新を承認
  5. 開発環境のスタックが作成/更新された後、テストなどの動作確認を実施して意図した動作をすれば develop ブランチから master ブランチにマージしてプッシュ
  6. master ブランチにプッシュされるとCodePipelineがそれを検知し、CI/CDパイプラインが開始
  7. CodePipelineのApprovalステージでSNSトピックに通知、本番環境のChangeSetを確認してスタックの作成/更新を承認
  8. 本番環境のスタックが作成/更新される

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にはステージという概念があり、あるステージ内でどういった処理をしたいかを定義できます。各ステージ毎にアクションを定義することで処理内容を指定可能です。アクションと指定可能な各種サービスの一覧はこちらのドキュメントにまとまっています。また、各ステージ毎に生成したアーティファクトは、インプット/アウトプットという形で連携することが可能です。つまり、あるステージのアクションで定義した処理を実施、生成したアーティファクトを別のステージに渡すことで一連のパイプラインを作成することができます。

今回はステージの設定を以下のようにしてみました。

  1. ソースコードのフェッチ(Source)
  2. テンプレートのテスト(Test)
  3. ChangeSetの作成(Build)
  4. ChangeSetの承認(Approval)
  5. 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です

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を指定しているので、通知が来ると以下のようなメールが届きます。

approval

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行: ActionModeCHANGE_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サービスを中心としてパイプラインを作成しましたが、同等の機能を他サービスでも実現できると思います。ご自身の環境にあった構成を見つけるとよいのではないでしょうか。

本エントリがみなさんの参考になれば幸いに思います。