mazyu36の日記

某SIer所属のクラウドエンジニアのブログ

AWS CloudFormationのGit Syncを使って、AWS CDK用のGitOpsパイプラインを構築する

🎄本記事は、AWS CDK Advent Calendar 2023 の 5日目の記事となります

前回、CloudFormationのGit SyncをCDKで使う方法を紹介しました。

mazyu36.hatenablog.com

今回はこちらを活用して、AWS CDK用のGitOpsパイプラインを組んでみたいと思います。

前提

CloudForrmationのGit Syncってなんだっけ

CloudFormation側で「GitにあるCloudFormationテンプレートをpullして、デプロイしてくれる機能」です。

これによりCloudFormation基点のpull型deployが可能になりました。

aws.amazon.com

CDKにおけるCIOpsとGitOpsの違い

以下、Github ActionsをCDKのCIツールとして使った場合の、従来のPush型deploy(CIOps)とGit Syncを使ったPull型deploy(GitOps)のイメージを示します。

CIOps

CIツールが cdk deployを行うため、deployの権限を持たせる必要があります。これにより以下のような課題があります。

  • CIツールに権限が集中する。
  • CIツールに持たせる権限を何かしらの形で外だしする必要がある(OIDCなり、アクセスキーなり...)

deployは権限的に強力なので、セキュリティ要件が厳しい場合上記がネックになることがあります。

GitOps

CloudFormation自身がテンプレートをpullしてdeployを行います。これによりGitOpsの考え方でCIとCDが分離できるため以下のようなメリットがあります。

  • CIツールからdeployの権限を分離できる(権限の集中を防げる)。
  • cdk deployの権限を外だしする必要がなくなる(deployの権限がAWS内に閉じる)。

※AssetsのPublishは必要なので、その権限をCIツールに持たせる必要はあります。

CDKのGitOpsの使いどころ

で、いつGitOps使うのがいいんだろうという話ですが、個人的にはCIOpsのデメリットが許容できない場合かなと思います。セキュリティ要件や組織構造によりCIとCDの分離や、権限を厳格に管理したい場合はGitOpsの採用のメリットがありそうです。

一方でGitOpsのメリットを見て「これ何が嬉しいんだ?」ってなるような状況であれば特に採用の必要はないと思います。

またGit Syncを使ったGitOpsのデメリットとして1つ感じたのは、マルチStack構成の場合CloudFormationテンプレートが分かれるため、Git Syncの設定がその分必要になり多少複雑になることです。

マルチStackの場合はCIOpsの方が構成がシンプルになると思います(cdk deployでまとめてdeployできる)。

※そもそもStackは基本分けないほうがいいという話もありますが(何度も引用させていただいているブログ)

tmokmss.hatenablog.com

Git Syncにおける CDKのGitOpsパイプラインの構築

では実際に仕組みの構築を行なっていきます。

今回は実開発を想定して、Pull Requestを契機に CFnテンプレート生成やcdk diffの取得を行い、mainにマージされたらGit Syncでデプロイが行われるような仕組みにしたいと思います。

0. プロジェクト構成

今回のCDKプロジェクトの構成を以下に示します。基本的には前回の記事と同様です。

追加でGitHub Actions用のロール作成やワークフローの定義を追加しています。

.
├── .github
│   └── workflows
│       └── git_sync.yaml # GitHub Actionsのワークフロー定義
├── bin
│   └── cdk_git_sync.ts
├── cdk.json
├── cdk.out
├── cfn_git_sync
│   ├── deployment.yaml # Git Syncで使用するデプロイファイル
│   └── template.yaml # CloudFormationテンプレート
├── lambda
│   └── function.py
├── lib
│   ├── cdk_git_sync-stack.ts
│   └── oidc-stack.ts # GitHub Actions用のIAMロールを作成するスタック(詳細後述)
├── node_modules
├── package-lock.json
├── package.json
├── test
│   └── cdk_git_sync.test.ts
└── tsconfig.json

実物は以下にあります。

github.com

1. CDKプロジェクトの作成およびGit Syncの設定

前回の記事の1~4を実施し、CDKプロジェクトの作成およびGit Sync関連の設定を行います(今回は詳細割愛します)。

mazyu36.hatenablog.com

※注意点

Git Sync設定時に作成するCloudFormation のスタック名は、CDKで生成するスタック名と揃えるのが望ましいです。

ここを揃えておくことで Git Syncによりデプロイしたスタックに対して、cdk diff を実行した際に正しく差分を認識できます。 揃えていない場合、cdk diffの時に存在しないスタックを参照しに行ってしまい常に全量が差分として出てしまいます。

今回の実装で言うとスタック名はCdkGitSyncStackです。

new CdkGitSyncStack(app, 'CdkGitSyncStack', {
  stackName: 'CdkGitSyncStack'. // Stack名
});

マネコン上で作成するCloudFormationのスタック名もこちらと揃えます。

2. OIDCプロバイダーやIAMロールの作成

GitHub ActionsにAssetsのPublish等の権限を持たせる必要があります。セキュリティ観点ではアクセスキーよりもOIDCを使用するのが望ましいですが、OIDCプロバイダーやIAMロールの作成が必要になります。

以下の2記事でCDKで生成する方法がまとめられていたので大いに参考(と言うかほぼまるパクリ)させていただきました。

※OIDCプロバイダー等はCDKで作成する必要はない、かつGitOpsをやりたいCDKプロジェクトに定義を含める必要はないです。今回は便宜上一つのプロジェクトに集約しているだけです。

dev.classmethod.jp

makky12.hatenablog.com

ただし今回GitHub Actionsはデプロイの権限が不要になります。そのためdeployの権限をコメントアウトしておきます。

    const cdkPublishPolicy = new aws_iam.Policy(this, 'CdkPublishPolicy', {
      policyName: 'CdkPublishPolicy',
      statements: [
        new aws_iam.PolicyStatement({
          effect: aws_iam.Effect.ALLOW,
          actions: ['sts:AssumeRole'],
          resources: [
            //  deployの権限をコメントアウト。
            // `arn:aws:iam::${accountId}:role/cdk-hnb659fds-deploy-role-${accountId}-${region}`,
            `arn:aws:iam::${accountId}:role/cdk-hnb659fds-file-publishing-role-${accountId}-${region}`,
            `arn:aws:iam::${accountId}:role/cdk-hnb659fds-image-publishing-role-${accountId}-${region}`,
            `arn:aws:iam::${accountId}:role/cdk-hnb659fds-lookup-role-${accountId}-${region}`,
          ],
        }),
      ],
    });

※権限周りのスライドは以下を参考にさせていただきました。

実装後、OIDCプロバイダー等はあらかじめ作成しておきたいので、このスタックのみ手動でデプロイしておきます。

cdk deploy CdkPublishGhOidcStack

3. GitHub Actionsワークフロー定義の作成

Pull Requestをトリガーに以下を行うワークフローを作成してリポジトリにPushしておきます。

  • cdk synthでCloudFormationテンプレートを生成
  • AssetsのPublish
  • テンプレートをPull Requestに追加
  • cdk diffの結果をPull Reuestのコメントに追加

全体像は以下です。

on:
  pull_request:
    branches:
      - main
    types:
      - opened
      - synchronize
      - reopened

env:
  AWS_REGION: ap-northeast-1
  GITHUB_MAIL: 41898282+github-actions[bot]@users.noreply.github.com
  GITHUB_NAME: github-actions[bot]

jobs:
  publish:
    permissions:
      id-token: write
      contents: write
      pull-requests: write
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
        with:
          ref: ${{ github.event.pull_request.head.ref }}

      - name: Get temporary credentials with OIDC
        uses: aws-actions/configure-aws-credentials@v2
        with:
          role-to-assume: ${{ vars.AWS_OIDC_ROLE_ARN }}
          aws-region: ${{env.AWS_REGION}}

      - name: Cache Dependency
        uses: actions/cache@v3
        id: cache_dependency_id
        env:
          cache-name: cache-cdk-dependency
        with:
          path: node_modules
          key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('package-lock.json') }}
          restore-keys: ${{ runner.os }}-build-${{ env.cache-name }}-

      - name: Install CDK Dependency
        if: steps.cache_dependency_id.outputs.cache-hit != 'true'
        run: npm install

      - name: Synthesize CDK stacks
        run: npx cdk synth ${{ vars.STACK_NAME }} > cfn_git_sync/template.yaml

      # テンプレートが更新されたかをチェック
      - name: Git diff
        id: diff
        run: |
          git add -N .
          git diff --name-only --exit-code
        continue-on-error: true

      # テンプレートが更新された場合のみ以降実施
      # AssetsをPublish
      - name: Assets publish
        run: npx cdk-assets publish -p cdk.out/${{ vars.STACK_NAME }}.assets.json
        if: steps.diff.outcome == 'failure'

      # テンプレートをPush
      - name: Add generated files to the pull request
        run: |
          git config --local user.email ${{ env.GITHUB_MAIL }}
          git config --local user.name ${{ env.GITHUB_NAME }}
          git add .
          git commit -m "Add generated template.yaml to the pull request"
          git push origin HEAD:${{ github.head_ref }}
        if: steps.diff.outcome == 'failure'

      # cdk diffを実施してPRにコメントとして追加
      - name: CDK diff
        run: npx cdk diff ${{ vars.STACK_NAME }} 2>&1 2>&1 | tee output.log
        if: steps.diff.outcome == 'failure'
      - name: Save output
        id: output_log
        run: |
          echo "data<<EOF" >> $GITHUB_OUTPUT
          echo "$(cat output.log)" >> $GITHUB_OUTPUT
          echo "EOF" >> $GITHUB_OUTPUT
        if: steps.diff.outcome == 'failure'
      - name: Post diff in comment
        uses: mshick/add-pr-comment@v2
        with:
          message-id: cdk-diff
          message: |
            #### cdk diff
            <details>
              <summary>Show diff</summary>
              <pre>
              <code>
              ${{ steps.output_log.outputs.data }}
              </code>
              </pre>
            </details>
        if: steps.diff.outcome == 'failure'

トリガー契機および変数

mainブランチ向けのPull Requestが作成・更新された場合にトリガーされるようにしています。

また変数でリージョンと、テンプレートPush時に使用するメールアドレス、ユーザ名(Bot)を設定しています。

on:
  pull_request:
    branches:
      - main
    types:
      - opened
      - synchronize
      - reopened

env:
  AWS_REGION: ap-northeast-1
  GITHUB_MAIL: 41898282+github-actions[bot]@users.noreply.github.com
  GITHUB_NAME: github-actions[bot]

チェックアウト〜npm installまで

ブランチの内容のチェックアウト、OIDCによるクレデンシャル取得、npm installなど準備プロセスを実施していきます。

これは以下の記事のワークフローをほぼ流用させていただきました。

dev.classmethod.jp

obs:
  publish:
    permissions:
      id-token: write
      contents: write
      pull-requests: write # PR書き込みのための権限を追加(cdk diffの結果をコメントに追加するため)
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
        with:
          ref: ${{ github.event.pull_request.head.ref }}

      - name: Get temporary credentials with OIDC
        uses: aws-actions/configure-aws-credentials@v2
        with:
          role-to-assume: ${{ vars.AWS_OIDC_ROLE_ARN }}
          aws-region: ${{env.AWS_REGION}}

      - name: Cache Dependency
        uses: actions/cache@v3
        id: cache_dependency_id
        env:
          cache-name: cache-cdk-dependency
        with:
          path: node_modules
          key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('package-lock.json') }}
          restore-keys: ${{ runner.os }}-build-${{ env.cache-name }}-

      - name: Install CDK Dependency
        if: steps.cache_dependency_id.outputs.cache-hit != 'true'
        run: npm install

cdk synthによりテンプレートを生成

CloudFormationテンプレートを生成します。

Stack名は動的に変えられるようにRepository Variablesから取得するようにしています。

またテンプレートの出力先はAWS側でGIt Sync用に設定するものと一致させます(前回の記事と同様のパスを設定しています)

      - name: Synthesize CDK stacks
        run: npx cdk synth ${{ vars.STACK_NAME }} > cfn_git_sync/template.yaml

AssetsのPublishおよびテンプレートのPush

テンプレートが更新されている場合(差分がある場合)のみ、AssetsのPublishとテンプレートのPush(Pull Requestへの追加)を行なっていきます。

      # テンプレートが更新されたかをチェック
      - name: Git diff
        id: diff
        run: |
          git add -N .
          git diff --name-only --exit-code
        continue-on-error: true

      # テンプレートが更新された場合のみ以降実施
      # AssetsをPublish
      - name: Assets publish
        run: npx cdk-assets publish -p cdk.out/${{ vars.STACK_NAME }}.assets.json
        if: steps.diff.outcome == 'failure'

      # テンプレートをPush
      - name: Add generated files to the pull request
        run: |
          git config --local user.email ${{ env.GITHUB_MAIL }}
          git config --local user.name ${{ env.GITHUB_NAME }}
          git add .
          git commit -m "Add generated template.yaml to the pull request"
          git push origin HEAD:${{ github.head_ref }}
        if: steps.diff.outcome == 'failure'

変更検知の仕組みは以下を参考にさせていただきました。

zenn.dev

AssetsのPublishはGit Sync対象のStackのものに対して行います。

テンプレートをPushする時は変数で設定したBotのEメールアドレスとユーザ名を使用します。

cdk diffをPRコメントに投稿

これはGit Sync実現には不要ですが、cdk diffの結果をPRコメントに投稿してみやすくするようにしてみました。

以下の記事のものを参考にさせてもらいました。

engineering.hashnode.com

      # cdk diffを実施してPRにコメントとして追加
      - name: CDK diff
        run: npx cdk diff ${{ vars.STACK_NAME }} 2>&1 2>&1 | tee output.log
        if: steps.diff.outcome == 'failure'
      - name: Save output
        id: output_log
        run: |
          echo "data<<EOF" >> $GITHUB_OUTPUT
          echo "$(cat output.log)" >> $GITHUB_OUTPUT
          echo "EOF" >> $GITHUB_OUTPUT
        if: steps.diff.outcome == 'failure'
      - name: Post diff in comment
        uses: mshick/add-pr-comment@v2
        with:
          message-id: cdk-diff
          message: |
            #### cdk diff
            <details>
              <summary>Show diff</summary>
              <pre>
              <code>
              ${{ steps.output_log.outputs.data }}
              </code>
              </pre>
            </details>
        if: steps.diff.outcome == 'failure'

4. GitHubにおける設定

以下実施しておきます。

Repository Variable

今回以下2つを使用するので設定しておきます。

  • OIDC用のIAMロールのARN(AWS_OIDC_ROLE_ARN
  • デプロイ対象のStack名(STACK_NAME

developer.mamezou-tech.com

書き込み権限の付与

今回GitHub Actions上でCloudFormationテンプレートのPushを行うので、書き込みを許可しておく必要があります。

zenn.dev

GitOpsパイプラインの動作確認

ここまでで仕組みの整備が完了しました。動作を確認してみます。

CDKプロジェクト上でブランチを作成して変更をPush

適当にfeatureブランチを作成し、差分のcommit, pushを行います。

 git checkout -b feature_test

差分としてSQSキューを1つ追加してみました。追加後、

    const queue = new sqs.Queue(this, 'CdkGitTestQueue', {
      visibilityTimeout: cdk.Duration.seconds(300)
    });

    // 追加
    const queue2 = new sqs.Queue(this, 'CdkGitTestQueue2', {
      visibilityTimeout: cdk.Duration.seconds(300)
    });


    // 以下略

Pull Requestを作成

GitHub上でPRを作成します。この時点ではFiles changedは1になっています(先程SQSキューを定義した実装ファイル)。

PR作成後、GitHub Actionsのワークフローがトリガーされました。

全部終わりました。ワークフローの中で以下が実行されています。

  • cdk synthでCloudFormationテンプレートを生成
  • AssetsのPublish
  • テンプレートをPull Requestに追加
  • cdk diffの結果をPull Reuestのコメントに追加

この状態でPull Requestを開くと以下更新されていることが確認できます。

  • botがCloudFormationテンプレートを追加していること
  • Files changedが2になっていること(テンプレートが変更差分として追加)
  • cdk diffの結果がコメントとして追加されていること

Files changedタブを開くと、想定通りCloudFormationテンプレートが更新されています。

PRのマージおよびGit Syncによるデプロイ

ではPRをマージします。

これにより mainのCloudFormationテンプレートが更新され、Git Syncが変更を検知しデプロイが開始されます。デプロイが完了したら一連のフローとしては終わりです。

Construct tree ビューでも追加したSQSキューが生成されていることを確認できました。こちらで動作確認終了です。

終わりに

以下のようなgit syncを活用したCDKにおけるGitOpsパイプラインを構築できました。

セキュリティ要件が厳しい際に使えるCDKのパイプラインの選択肢の一つとなりそうです。