🎄本記事は、AWS CDK Advent Calendar 2023 の 5日目の記事となります
前回、CloudFormationのGit SyncをCDKで使う方法を紹介しました。
今回はこちらを活用して、AWS CDK用のGitOpsパイプラインを組んでみたいと思います。
前提
CloudForrmationのGit Syncってなんだっけ
CloudFormation側で「GitにあるCloudFormationテンプレートをpullして、デプロイしてくれる機能」です。
これによりCloudFormation基点のpull型deployが可能になりました。
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は基本分けないほうがいいという話もありますが(何度も引用させていただいているブログ)
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
実物は以下にあります。
1. CDKプロジェクトの作成およびGit Syncの設定
前回の記事の1~4を実施し、CDKプロジェクトの作成およびGit Sync関連の設定を行います(今回は詳細割愛します)。
※注意点
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プロジェクトに定義を含める必要はないです。今回は便宜上一つのプロジェクトに集約しているだけです。
ただし今回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
など準備プロセスを実施していきます。
これは以下の記事のワークフローをほぼ流用させていただきました。
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'
変更検知の仕組みは以下を参考にさせていただきました。
AssetsのPublishはGit Sync対象のStackのものに対して行います。
テンプレートをPushする時は変数で設定したBotのEメールアドレスとユーザ名を使用します。
cdk diffをPRコメントに投稿
これはGit Sync実現には不要ですが、cdk diff
の結果をPRコメントに投稿してみやすくするようにしてみました。
以下の記事のものを参考にさせてもらいました。
# 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
)
書き込み権限の付与
今回GitHub Actions上でCloudFormationテンプレートのPushを行うので、書き込みを許可しておく必要があります。
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のパイプラインの選択肢の一つとなりそうです。