【第4回】AWS CDK × Python インフラ自動化入門 — デプロイ戦略・ドリフト検出・よくあるハマりどころと対処法まとめ

テックツール

金曜の夜に「ちょっとした修正だから」と手元から cdk deploy を叩いて、その5分後に別のブランチの内容が本番に乗っていたことに気づいた——という話を、自分ではなく知り合いから聞きました。聞いた瞬間に「あ、これ自分もやる」と思ったので、今回のテーマになりました。

前回(第3回)は Stack の分割と dev / prod の環境切り分け、それから assertions モジュールを使ったテストのあたりを紹介しました。コードとしてはひとまず形になったので、シリーズ最後は「それをどう安全に流し込むか」です。CI/CD パイプラインの作り方、デプロイ方式の選び方、ドリフト検出、そして自分が実際に踏んだハマりどころをまとめます。

この記事でわかること

  • デプロイ自動化の3つの方式(手元実行 / GitHub Actions / CDK Pipelines)の使い分け
  • GitHub Actions + OIDC を使った最小構成パイプラインの実装方法
  • CDK Pipelines(CodePipeline)を Python で書くときのポイント
  • デプロイ方式のオプション活用法(direct、hotswap、concurrency など)
  • ドリフト検出の仕組みと注意点
  • 本番環境で起きやすいハマりどころとその対処法 7つ

デプロイ戦略は3択|誰が cdk deploy を叩くのか

AWS CDK のデプロイ自動化を調べていると、だいたい次の3パターンに落ち着くようです。

  • 手元から実行:一番速い。個人開発の dev 環境ならこれで十分。ただし「誰が何をいつ流したか」が残らない
  • GitHub Actions から実行cdk deploy を CI ランナーが叩くだけ。構成がシンプルで、アプリのビルドと同じワークフローに混ぜられる
  • CDK Pipelines(CodePipeline):パイプラインそのものも CDK で定義する。マルチアカウント・マルチリージョンに強い

個人的には、規模が小さいうちは GitHub Actions で十分かなと思っています。CDK Pipelines は強力なんですが、パイプライン自体が CloudFormation スタックになるので、壊れたときのデバッグ対象が一段増えます。1人でアカウント1つを触っている段階でそこまで背負う必要はない気がしていて。

逆に「dev / stg / prod がアカウントごとに分かれていて、承認フローも必要」となると CDK Pipelines のほうが素直です。ちなみに CI を GitHub Actions、CD を CodePipeline にするハイブリッド構成をやっている方もいて、GitHub Actions から CodePipeline を起動して Pipeline Variables で引数を渡す、という手法が紹介されていました。面白いけど自分の規模ではやりすぎですね。

GitHub Actions + OIDC で組む最小構成

まずアクセスキーは使いません。OIDC で IAM ロールを引き受ける形にします。信頼ポリシーはこんな感じです。

{
  "Effect": "Allow",
  "Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com" },
  "Action": "sts:AssumeRoleWithWebIdentity",
  "Condition": {
    "StringEquals": {
      "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
      "token.actions.githubusercontent.com:sub": "repo:nyanchu/cdk-sample:ref:refs/heads/main"
    }
  }
}

ここで sub をワイルドカードで大きく開けてしまうと、意図しないブランチから引き受けられる余地が出るので、ブランチまで固定したほうが安心です。PR でも cdk diff を回したいので、自分は diff 用と deploy 用でロールを分けています。diff 側は読み取り権限だけで足りるはずですし。

付与するポリシーは、ケースによっては CDK 用の権限をベタベタ書かず、bootstrap で作られたロールを引き受ける権限中心で足りることもあります。

{
  "Effect": "Allow",
  "Action": "sts:AssumeRole",
  "Resource": "arn:aws:iam::123456789012:role/cdk-hnb659fds-*"
}

hnb659fds は bootstrap のデフォルト qualifier で、意味のない固定文字列です。初見のとき「何かのハッシュか?」と真面目に調べてしまいました。

ワークフローはこう。

name: deploy
on:
  push:
    branches: [main]
  pull_request:

concurrency:
  group: cdk-${{ github.ref }}
  cancel-in-progress: false

permissions:
  id-token: write
  contents: read
  pull-requests: write

jobs:
  cdk:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - uses: actions/setup-node@v4
        with:
          node-version: "22"
      - run: pip install -r requirements.txt
      - run: npm install -g aws-cdk@2.1029.0   # ここ注意:バージョン固定
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
          aws-region: ap-northeast-1
      - if: github.event_name == 'pull_request'
        run: cdk diff --context env=prod
      - if: github.ref == 'refs/heads/main'
        run: cdk deploy --all --require-approval never --context env=prod

concurrency は入れておいたほうがいいです。同じスタックに対して2本同時に cdk deploy が走ると、CloudFormation 側が UPDATE_IN_PROGRESS で弾いて片方が失敗します。しかも cancel-in-progress: true にすると走行中のデプロイを途中でぶった切ることになるので、ここは false 推奨です。デプロイ系のジョブをキャンセルするのは基本的に事故のもとっぽいので。

PR で diff を見せる

cdk diff の出力を PR コメントに貼ると、レビューがだいぶ楽になります。差分があるときに終了コードを 0 以外にしたい場合は --fail を付けられます(「差分があったら落とす」用途なので、使う場所を間違えると全部赤くなります)。

cdk diff --all 2>&1 | tee diff.txt
{
  echo '```'
  head -c 60000 diff.txt
  echo '```'
} > body.md
gh pr comment ${{ github.event.number }} --body-file body.md

CDK Pipelines を Python で書くとこうなる

一応こちらも触っておきます。CDK Pipelines の面白いところは「パイプライン自体が自分を更新する」(self-mutating)点です。パイプラインの定義を変えて push すると、まずパイプラインが自分を更新してから、その先のステージを流します。

from aws_cdk import Stack, Stage
from aws_cdk.pipelines import (
    CodePipeline, CodePipelineSource, ShellStep, ManualApprovalStep,
)
from constructs import Construct


class PipelineStack(Stack):
    def __init__(self, scope: Construct, id: str, **kwargs):
        super().__init__(scope, id, **kwargs)

        source = CodePipelineSource.connection(
            "nyanchu/cdk-sample", "main",
            connection_arn=self.node.try_get_context("connection_arn"),
        )

        pipeline = CodePipeline(
            self, "Pipeline",
            synth=ShellStep(
                "Synth",
                input=source,
                install_commands=["pip install -r requirements.txt", "npm i -g aws-cdk"],
                commands=["cdk synth"],
            ),
            docker_enabled_for_synth=True,
        )

        pipeline.add_stage(AppStage(self, "Dev", env=DEV_ENV))
        pipeline.add_stage(
            AppStage(self, "Prod", env=PROD_ENV),
            pre=[ManualApprovalStep("ApproveProd")],
        )

GitHub と繋ぐには CodeConnections(旧 CodeStar Connections)の接続をコンソールで一度作って承認する必要があります。ここだけは手作業で、しかも PENDING のまま放置していると当然デプロイが通りません。自分は接続を作ったつもりで承認ボタンを押しておらず、10分くらい悩みました。

それから、パイプラインの初回だけは手元から cdk deploy PipelineStack を打つ必要があります。鶏と卵なので仕方ない。

デプロイ方式のオプションを使い分ける

cdk deploy はデフォルトで CloudFormation のチェンジセットを作って実行します。ここを状況に応じて変えられます。

  • --method=direct:チェンジセットを作らず直接 update。少し速いが差分が事前に見えない
  • --method=prepare-change-set:チェンジセットだけ作って実行しない。承認ゲートを CloudFormation 側に置きたいとき
  • --hotswap:Lambda のコードや Step Functions の定義など、対応リソースを CloudFormation を通さず直接差し替える。開発ループが劇的に速くなる代わりに、本番では使わないほうがいいやつ
  • --no-rollback:失敗しても自動ロールバックせず止める。原因調査したいときに便利。CI で常用するものではない
  • --concurrency 4:依存関係のないスタックを並列デプロイ。スタック数が増えてきたら効きます

ドリフト検出|テンプレートと現実がズレる問題

IaC を導入しても、コンソールから手で直してしまう瞬間は必ず来ます。障害対応中とかは特に。それを検出するのがドリフト検出です。

CDK CLI にも cdk drift というコマンドが入っていて、スタック単位でチェックできます。

cdk drift MyAppStack-prod

中身は CloudFormation の DetectStackDrift API を呼んでいるので、AWS CLI でも同じことができます。CI に組み込むなら、こちらのほうが JSON で扱いやすいかもしれません。

id=$(aws cloudformation detect-stack-drift \
      --stack-name MyAppStack-prod --query StackDriftDetectionId --output text)

aws cloudformation describe-stack-drift-detection-status \
  --stack-drift-detection-id "$id" \
  --query 'StackDriftStatus'

注意点として、ドリフト検出は全リソースタイプ・全プロパティに対応しているわけではありません。対応していないものは NOT_CHECKED になります。つまり「DRIFTED が出ない = 完全に一致している」ではない、ということです。ここは過信しないほうがよさそうです。

そして --hotswap との相性。hotswap は CloudFormation を経由せずに実物を書き換えるので、原理的にはテンプレートと実物がズレます。ただ Lambda のコードのようにドリフト検出の対象外になるプロパティもあるので、必ず DRIFTED として拾えるとは限らないっぽいです。正直この挙動はまだ自分の中で整理しきれていません。いずれにせよ hotswap を使った環境は「一度ちゃんと cdk deploy し直す」のが安全だと思います。

定期チェックは EventBridge Scheduler から Lambda を叩いて、DRIFTED なら SNS で通知、くらいで足ります。せっかく CDK を使っているので、この監視自体も CDK で書けるのが気持ちいいところです(ここは第2回で作った Lambda + EventBridge の構成をほぼ流用できます)。

よくあるハマりどころと対処法

bootstrap のバージョンが古い

This CDK deployment requires bootstrap stack version 'X', found 'Y' というエラー。新しい機能を使うと bootstrap テンプレートの更新が要求されます。cdk bootstrap を打ち直すだけで解決しますが、CI ロールに bootstrap 権限は与えていないことが多いので、手元から実行することになります。現在のバージョンは SSM パラメータで確認できます。

aws ssm get-parameter --name /cdk-bootstrap/hnb659fds/version --query Parameter.Value

CLI と aws-cdk-lib のバージョンが噛み合わない

CDK CLI は独立したバージョン体系になり、2.1000.x 系の番号が振られています。aws-cdk-lib2.x.y とは別物です。ここを混同すると「requirements.txt に書いたバージョンと CI の CLI が合わない」という地味な事故が起きます。CI では CLI もライブラリも明示的に固定しておくのが無難です。Renovate や Dependabot に上げてもらう運用が結局ラクでした。

Docker バンドリングが CI で落ちる

aws_lambda_python_alphaPythonFunction などは、設定によっては bundling のために Docker を使うことがあります。GitHub Actions の ubuntu ランナーには Docker があるので基本は動くんですが、Lambda を arm64(Graviton)にしていると x86 ランナー上でのビルドで詰まることがあります。素直に runs-on: ubuntu-24.04-arm を使うか、そもそもバンドリング不要な構成にするのが早いです。

fromLookup が CI で失敗する

Vpc.from_lookup() のような lookup 系は synth 時に AWS へ問い合わせて、結果を cdk.context.json にキャッシュします。これを .gitignore に入れていると CI で毎回問い合わせが走り、権限が足りずに落ちます。cdk.context.json はコミットするものです。逆に、環境を作り直したときは cdk context --clear を忘れると古い VPC ID を掴み続けます。

ROLLBACK_COMPLETE から進めない

スタックの初回作成が失敗すると ROLLBACK_COMPLETE という状態になり、更新も再作成もできません。これは削除してやり直すしかないです。2回目以降の失敗(UPDATE_ROLLBACK_COMPLETE)は普通に更新できるので、慌てなくて大丈夫です。

Export cannot be deleted as it is in use

スタック間参照を消すときの定番。参照している側と参照されている側を同時に変更すると、CloudFormation が Export を消せずに失敗します。「先に参照側のデプロイを済ませて、次に Export を消す」という2段階に分けるのが定石です。cdk deploy StackAcdk deploy StackB と明示的に順番を指定して逃げました。

cfn-exec-role が強すぎる

デフォルトの bootstrap では、CloudFormation の実行ロールに AdministratorAccess が付きます。気になるなら --cloudformation-execution-policies で絞れます。絞りすぎて何も作れなくなるところまでは一度やってみる価値があります(やりました)。

※この記事にはプロモーションが含まれます

ちなみに、お名前.com レンタルサーバー(WordPressに特化した高速レンタルサーバー。月額990円〜、独自ドメイン実質0円)も気になっています。お名前.com レンタルサーバー

まとめ

4回かけて、環境構築からリソース定義、環境の切り分け、そしてデプロイ自動化まで一通り歩いてみました。書き始めた時点では「CloudFormation の YAML を Python で書けるやつ」くらいの理解だったんですが、結局いちばん効いたのは「インフラの変更が PR のレビュー対象になる」という部分でした。cdk diff がコメントに出るだけで、心理的な安全度がまるで違います。

残っている宿題としては、CDK と Terraform をどう住み分けるか。既存リソースの取り込みは Terraform のほうが素直な印象があって、このへんは実際に両方使い込んでみないと語れない気がしています。もし「CDK Pipelines をやめて GitHub Actions に寄せた(あるいは逆)」みたいな経験がある方がいたら、その理由をぜひ聞きたいです。

📚 シリーズ「AWS CDK × Python インフラ自動化入門」(第4回 / 全4回)

← 前回の記事: 前回の記事はこちら

🎉 このシリーズは今回で完結です!

タイトルとURLをコピーしました