CDK Pipelines、マルチアカウント運用で見えてきた落とし穴と本当の使い方
「CodePipelineをCDKで書けるだけでしょ」と思ってた自分が、4アカウント本番運用で痛い目を見た話。設計の勘どころとハマりポイントを実体験ベースで。
CDK Pipelines、最初は「本当に必要か?」と思ってた
正直に言う。CDK Pipelinesを最初に試したのは2024年末で、そのときは「CodePipelineをCDKで書けるようにしただけでしょ」くらいの認識だった。CodePipeline V2とCodeBuild Fleetでビルド待機時間を短縮した話も以前書いたけど、当時はCodePipelineを素で使ってた。
それが変わったのは、チームのAWSアカウントが4つに増えた2025年初頭。dev / staging / prod の3環境に加えて、セキュリティ監査用アカウントが追加され、「各環境へのデプロイをどう統制するか」という問題が一気に重くなった。その流れでCDK Pipelinesをちゃんと使い始め、以来ほぼ毎日触ってる。
半年以上本番運用してみて、「設計時に知っておけばよかった」という知見がかなり溜まってきた。今日はそれを全部書く。教科書的な説明は省いて、ハマりポイントと実際に動く構成に絞る。
実際に構築したマルチアカウントCI/CD構成
うちが使っているのはこういう構成だ。CDK Pipelinesのセルフミューテーション機能を活かしつつ、ツールチェーンアカウント(パイプライン専用)からデプロイを制御するパターン。
graph TB
subgraph Toolchain["🔧 Toolchain Account"]
subgraph pipeline["CDK Pipelines"]
CP[CodePipeline V2]
CB[CodeBuild Fleet]
ECR[ECR - ビルドキャッシュ]
end
CP --> CB
CB --> ECR
end
subgraph Dev["🟢 Dev Account"]
DevECS[ECS Fargate]
DevRDS[Aurora PostgreSQL]
DevS3[S3 - アプリデータ]
end
subgraph Staging["🟡 Staging Account"]
StgECS[ECS Fargate]
StgRDS[Aurora PostgreSQL]
StgS3[S3 - アプリデータ]
end
subgraph Prod["🔴 Prod Account"]
subgraph vpc_prod["VPC - 本番"]
subgraph az1["AZ-1a"]
ProdECS1[ECS Fargate Task]
ProdRDS1[Aurora Primary]
end
subgraph az2["AZ-1c"]
ProdECS2[ECS Fargate Task]
ProdRDS2[Aurora Replica]
end
end
WAF[WAF v2]
ALB[ALB]
end
subgraph Audit["🔒 Security Audit Account"]
CT[CloudTrail - 全アカウント集約]
Config[AWS Config Aggregator]
SH[Security Hub]
end
CP -->|クロスアカウントデプロイ| Dev
CP -->|手動承認後| Staging
CP -->|変更セット確認後| Prod
Toolchain -->|監査ログ| Audit
ポイントはToolchainアカウントを独立させているところ。これをやらないと、開発者アカウントのIAMロールが肥大化するし、パイプライン自体の変更が本番に即影響するリスクがある。
AWS Organizationsの設計については別記事で書いたが、SCP(サービスコントロールポリシー)との組み合わせがCDK Pipelinesの真価を引き出す。
実際のコード構成と、ここだけはハマった話
パイプラインのエントリーポイント
// lib/pipeline-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import {
CodePipeline,
CodePipelineSource,
ShellStep,
} from 'aws-cdk-lib/pipelines';
import { DevStage } from './stages/dev-stage';
import { StagingStage } from './stages/staging-stage';
import { ProdStage } from './stages/prod-stage';
export class PipelineStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const pipeline = new CodePipeline(this, 'Pipeline', {
pipelineName: 'MyAppPipeline',
// 2026年時点ではV2がデフォルト。明示的に指定推奨
pipelineType: cdk.aws_codepipeline.PipelineType.V2,
selfMutation: true, // ← これがCDK Pipelinesの肝
synth: new ShellStep('Synth', {
input: CodePipelineSource.connection(
'my-org/my-repo',
'main',
{
connectionArn: 'arn:aws:codeconnections:ap-northeast-1:TOOLCHAIN_ACCOUNT:connection/xxxxxxxx',
}
),
commands: [
'npm ci',
'npm run build',
'npx cdk synth',
],
}),
// CodeBuild Fleetで待機時間を削減
codeBuildDefaults: {
buildEnvironment: {
buildImage: cdk.aws_codebuild.LinuxBuildImage.STANDARD_7_0,
computeType: cdk.aws_codebuild.ComputeType.MEDIUM,
},
},
});
// Dev ステージ(自動デプロイ)
const devStage = pipeline.addStage(
new DevStage(this, 'Dev', {
env: {
account: process.env.DEV_ACCOUNT_ID!,
region: 'ap-northeast-1',
},
})
);
// Dev後の統合テスト
devStage.addPost(
new ShellStep('IntegrationTest', {
commands: [
'npm run test:integration',
],
envFromCfnOutputs: {
API_ENDPOINT: devStage.apiEndpoint,
},
})
);
// Staging ステージ(手動承認あり)
const stagingStage = pipeline.addStage(
new StagingStage(this, 'Staging', {
env: {
account: process.env.STAGING_ACCOUNT_ID!,
region: 'ap-northeast-1',
},
}),
{
pre: [
new cdk.pipelines.ManualApprovalStep('ApproveStaging'),
],
}
);
// Prod ステージ(変更セット確認 + 手動承認)
pipeline.addStage(
new ProdStage(this, 'Prod', {
env: {
account: process.env.PROD_ACCOUNT_ID!,
region: 'ap-northeast-1',
},
}),
{
pre: [
new cdk.pipelines.ManualApprovalStep('ApproveProd', {
comment: '本番デプロイ確認。変更セットを必ず確認してください。',
}),
],
post: [
new ShellStep('SmokeTest', {
commands: ['npm run test:smoke'],
}),
],
}
);
}
}
Stage側の実装
// lib/stages/prod-stage.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { AppStack } from '../stacks/app-stack';
import { DatabaseStack } from '../stacks/database-stack';
export class ProdStage extends cdk.Stage {
readonly apiEndpoint: cdk.CfnOutput;
constructor(scope: Construct, id: string, props: cdk.StageProps) {
super(scope, id, props);
const dbStack = new DatabaseStack(this, 'Database', {
envName: 'prod',
multiAz: true, // 本番はMulti-AZ必須
deletionProtection: true, // 誤削除防止
});
const appStack = new AppStack(this, 'App', {
envName: 'prod',
dbSecret: dbStack.dbSecret,
minCapacity: 3,
maxCapacity: 20,
});
// パイプラインの後続ステップに渡す
this.apiEndpoint = appStack.apiEndpoint;
}
}
ここで地味にハマったのが envFromCfnOutputs の使い方。ShellStep でCfnOutputを参照するとき、Stageのプロパティとして CfnOutput を公開しないといけない。これをやらずに直接Stackを参照しようとして30分溶かした。
セルフミューテーションでハマった話
selfMutation: true にすると、CDKコードを変更した際にパイプライン自体も自動更新される。便利なんだけど、最初のデプロイでBootstrap環境が揃っていないと即死する。
クロスアカウントでデプロイするには、各アカウントで以下のBootstrapコマンドを実行しておく必要がある。
# Toolchainアカウント
cdk bootstrap aws://TOOLCHAIN_ACCOUNT/ap-northeast-1
# Devアカウント(Toolchainアカウントからのデプロイを信頼)
cdk bootstrap aws://DEV_ACCOUNT/ap-northeast-1 \
--trust TOOLCHAIN_ACCOUNT \
--trust-for-lookup TOOLCHAIN_ACCOUNT \
--cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess
# Staging・Prodも同様
cdk bootstrap aws://STAGING_ACCOUNT/ap-northeast-1 \
--trust TOOLCHAIN_ACCOUNT \
--trust-for-lookup TOOLCHAIN_ACCOUNT \
--cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess
--cloudformation-execution-policies に AdministratorAccess を渡している点が気になる人もいると思う。個人的には「怖い」と感じる設定で、本番ではCDK Aspects・Nagと組み合わせて、デプロイできるリソースの種類をNagルールで制限する運用にしている。少なくとも「何でもできる」状態を放置しないほうがいい、というのが半年使ってみての結論だ。
パイプライン実行時間の変遷
導入前後でビルド時間がどう変わったかをグラフにしてみた。CodeBuild Fleetを導入したことでキューイング待機がほぼゼロになっている。
xychart-beta
title "CDK Pipelinesデプロイ時間の変遷(分)"
x-axis ["導入前", "CDK Pipeline単体", "+ CodeBuild Fleet", "+ ECRキャッシュ", "現在"]
y-axis "デプロイ時間(分)" 0 --> 40
bar [35, 28, 18, 13, 10]
line [35, 28, 18, 13, 10]
最初(CDK Pipelines導入前のシェルスクリプトデプロイ時代)が35分、現時点で10分まで来た。正直まだ改善余地はあって、Dockerレイヤーキャッシュの最適化とSynth並列化を試してる最中だ。
各フェーズで何がどこまで効いたかをまとめるとこんな感じ。
| フェーズ | デプロイ時間 | 主な改善内容 |
|---|---|---|
| 導入前(シェルスクリプト) | 35分 | ― |
| CDK Pipelines単体 | 28分 | デプロイ処理の整理・並列化 |
| + CodeBuild Fleet | 18分 | キューイング待機をほぼゼロに |
| + ECRキャッシュ | 13分 | Dockerビルドをスキップ |
| 現在 | 10分 | レイヤーキャッシュ最適化 |
運用してわかった設計のポイント3つ
1. StageName はアカウントIDではなく環境名を使え
地味なんだけど、これを間違えると後で泣く。pipeline.addStage() に渡すStageのIDがCloudFormationのスタック名に使われるので、Dev Staging Prod などの意味のある名前にしておかないと、AWSコンソールで迷子になる。うちは最初に Stage-1 とか書いてしまい、3ヶ月後に全部リネームする羽目になった。リネームするとCloudFormationスタック名が変わって削除→再作成が走るので、本番でやると最悪だ。
2. アセットの共有はECRキャッシュ経由にする
Dockerイメージをビルドしている場合、CDK Pipelinesのデフォルトだとビルドとプッシュが毎ステージ走る。ECRにキャッシュを置いてToolchainアカウントから各アカウントへ参照させることで、ビルド時間を大幅削減できた。設定はこんな感じ。
// ECRキャッシュの設定例
const pipeline = new CodePipeline(this, 'Pipeline', {
// ...
dockerEnabledForSynth: true,
dockerEnabledForSelfMutation: true,
assetPublishingCodeBuildDefaults: {
buildEnvironment: {
privileged: true, // Dockerビルドに必要
},
cache: cdk.aws_codebuild.Cache.local(
cdk.aws_codebuild.LocalCacheMode.DOCKER_LAYER
),
},
});
3. ManualApprovalStep の通知は絶対に設定する
デフォルトでは手動承認のタイムアウトが7日間で、誰も気づかないまま放置されることがある。実際うちでも一度やらかした。SNSトピックと連携してSlack通知を飛ばすのは必須だ。
// Slackに通知する手動承認
const approvalTopic = new cdk.aws_sns.Topic(this, 'ApprovalTopic');
const slackAction = new cdk.aws_sns_subscriptions.UrlSubscription(
process.env.SLACK_WEBHOOK_URL!
);
approvalTopic.addSubscription(slackAction);
pipeline.addStage(prodStage, {
pre: [
new cdk.pipelines.ManualApprovalStep('ApproveProd', {
externalEntityLink: 'https://your-deployment-dashboard.example.com',
}),
],
});
ステージ間のテスト戦略
CDK Pipelinesで地味に嬉しいのが、ステージのpre/postフックでテストを組み込めること。「デプロイとテストを同じコードで定義できる」のがCDK Pipelinesの最大の強みだと思っていて、うちのチームでは下図のような流れにしている。
sequenceDiagram
participant Git as GitHub
participant Synth as Synth(CodeBuild)
participant Dev as Dev Account
participant Stg as Staging Account
participant Prod as Prod Account
Git->>Synth: プッシュ検知
Synth->>Synth: cdk synth + ユニットテスト
Synth->>Dev: CloudFormation Deploy
Dev->>Dev: 統合テスト(API/DB疎通確認)
Dev->>Stg: 手動承認後デプロイ
Stg->>Stg: E2Eテスト(Playwright)
Stg->>Prod: 手動承認 + 変更セット確認
Prod->>Prod: デプロイ
Prod->>Prod: スモークテスト
Prod-->>Git: デプロイ完了通知
テスト戦略についてはJest・Vitest・Playwrightの使い分けでも詳しく書いたが、E2EはStagingで通すのが現実的なラインだと思っている。Prodで初めてテストが通る状態はさすがにまずい。
正直まだ迷ってるところ
半年やってみてまだ答えが出ていないことも書いておく。
ドリフト検出の自動化。 CloudFormation Driftは定期的に検出できるが、CDK Pipelinesと連携してドリフトがあったら自動修正するのか、それとも通知だけにするのかの判断が難しい。本番環境で自動修正が走るのはリスキーだけど、放置するのも怖い。今は週次でドリフト検出→Slack通知+チケット起票で止まっている。正直これがベストとも思っていない。
マルチリージョン対応。 東京リージョンをメインにしているが、将来的に大阪リージョンへのフェイルオーバーを考えると、CDK Pipelinesのクロスリージョンアセット転送の設計が複雑になる。crossRegionReplicationBuckets を設定すればできるはずだけど、まだ本格検証できていない。知見がある人がいたらぜひ教えてほしい。
まとめ
CDK Pipelines半年の知見を整理すると次の5点に絞られる。
| # | ポイント | やらかすと… |
|---|---|---|
| 1 | Toolchainアカウントを独立させる | 開発者アカウントのIAMが肥大化し、後でリファクタが大変になる |
| 2 | selfMutationの前にBootstrapを完成させる | 謎のクロスアカウントエラーで詰まる |
| 3 | StageName・CfnOutputの命名規則を最初に決める | リネーム時にスタック削除→再作成が発生する |
| 4 | ManualApprovalStepの通知を設定する | 承認待ちが誰も知らないまま7日間放置される |
| 5 | pre/postフックにテストを全ステージ組み込む | CDK Pipelinesの最大の強みを活かせないままになる |
次のアクションとして、まずBootstrapの設定から始めるといい。既存のCodePipelineからの移行であれば、CDK Migrateで既存リソースを移行する方法も参考になる。
CDK Pipelinesを使い始めて「ここが分からん」ってなったことがあれば、コメントで教えてほしい。同じところでハマってる人、きっといると思う。