CDK Pipelinesで月30件のデプロイを自動化した話。マルチアカウント運用の地雷と対策
CDK Pipelinesでマルチアカウント環境を1年運用。デプロイ停止、キャッシング問題、権限設定の失敗を乗り越えた実装パターンを実例ベースで解説。
CDK Pipelines本番運用で学んだマルチアカウントCI/CD構築の地雷と対策2026
先日、うちのチームでCDK Pipelinesをマルチアカウント環境に導入してから1年が経ちました。正直、最初の3ヶ月は地獄でした。デプロイが突然止まったり、キャッシングが予想外の挙動をしたり、アカウント間の権限設定でハマったり。でも今は落ち着いて、月30件以上のインフラ変更を自動でさばいています。今日は、その過程で学んだ本当のコツを共有したいと思います。
CDK Pipelinesを選んだ理由と初期の失敗
うちが以前使ってたのはCodePipelineとCloudFormationの組み合わせでした。悪くはないんですが、テンプレートの管理が属人化して、デプロイ順序を手で設定するのが毎回めんどくさかった。それでCDK Pipelinesに乗り換えたんです。
CDK Pipelinesの魅力は、パイプライン自体もInfrastructure as Codeで定義できるってことですよね。つまり、パイプラインの変更も自動的にGitから適用される。これで管理の一元化ができるはずでした。
ただ、本番環境への適用は想像より複雑でした。最初は開発アカウントだけで試してたから、本当の課題が見えてなかったんです。本番環境のセキュリティ要件、複数リージョン、マルチアカウント間のクロスアカウントロール設定……こういった要素が組み合わさると、ドキュメント通りではいかないんだなって痛感しました。
マルチアカウント環境での構成設計
話を簡潔にするために、我々の構成図を先に見せます。
graph TB
subgraph CodeAccount["Code Pipeline Account"]
CodeRepo["GitHub Repository"]
Pipeline["CDK Pipeline"]
PipelineRole["IAM Role<br/>cdk-pipeline-role"]
end
subgraph DevAccount["Dev Account"]
DevRole["Cross Account Role<br/>cdk-cross-account-role"]
DevStage["CloudFormation Stack<br/>Dev Environment"]
end
subgraph ProdAccount["Prod Account"]
ProdRole["Cross Account Role<br/>cdk-cross-account-role"]
ProdStage["CloudFormation Stack<br/>Prod Environment"]
ManualApproval["Manual Approval"]
end
subgraph ArtifactBucket["S3 Artifact Bucket<br/>cdk-artifacts-***"]
CloudAssembly["Cloud Assembly<br/>tree.json"]
end
CodeRepo -->|Webhook| Pipeline
Pipeline -->|Assume Role| DevRole
Pipeline -->|Store Assembly| ArtifactBucket
DevRole -->|Deploy| DevStage
DevStage -->|Success| ManualApproval
ManualApproval -->|Assume Role| ProdRole
ProdRole -->|Deploy| ProdStage
PipelineRole -->|Assume Role| DevRole
PipelineRole -->|Assume Role| ProdRole
PipelineRole -->|Write| ArtifactBucket
style CodeAccount fill:#e8f4f8
style DevAccount fill:#f0e8f8
style ProdAccount fill:#f8e8e8
style ArtifactBucket fill:#f8f8e8
ポイントはこれです。Pipeline実行用アカウント(Code Account) でGitHubのコミットをトリガーにパイプラインが動きます。その際、各環境のアカウントに対して クロスアカウントロール を仮定することで、権限を持たずにデプロイを実行する。アーティファクト(Cloud Assembly)はS3に保存して、各アカウントが読み取るという構図ですね。
そして、本番環境へのデプロイは手動で承認する仕掛けにしてます。これだけなら標準的ですが、実装には結構ハマりどころがあります。
クロスアカウント信頼関係で踏んだ地雷
最初の失敗は、クロスアカウントロールの信頼関係設定です。CDK Pipelinesは自動でロールを作成してくれるんですが、うっかり権限を削ってしまった。結果、Dev環境へのデプロイが突然失敗し始めました。
実装してた時のコード、残ってたので見直してみました。
import * as cdk from 'aws-cdk-lib';
import * as pipelines from 'aws-cdk-lib/pipelines';
import * as iam from 'aws-cdk-lib/iam';
export class CDKPipelinesStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Pipeline用の実行ロール
const pipelineRole = new iam.Role(this, 'PipelineRole', {
assumedBy: new iam.ServicePrincipal('codepipeline.amazonaws.com'),
});
// Dev アカウント用のクロスアカウントロール
const devCrossAccountRole = new iam.Role(this, 'DevCrossAccountRole', {
assumedBy: new iam.AccountPrincipal(this.account),
roleName: 'cdk-cross-account-role',
});
// CloudFormation実行権限
devCrossAccountRole.addToPrincipalPolicy(new iam.PolicyStatement({
actions: [
'cloudformation:*',
'iam:PassRole',
's3:GetObject',
's3:GetObjectVersion',
],
resources: ['*'],
effect: iam.Effect.ALLOW,
}));
// Pipeline が Dev ロールを assume できるようにする
devCrossAccountRole.assumeRolePolicy?.addStatements(
new iam.PolicyStatement({
principals: [new iam.AccountPrincipal(props?.env?.account || this.account)],
actions: ['sts:AssumeRole'],
effect: iam.Effect.ALLOW,
})
);
}
}
これ自体は悪くないんですが、本番で気づいたのが CloudFormation実行ロール(CFN Execution Role)の権限 を考慮してなかったこと。CloudFormationって、デプロイ自体をするロールと、実際のリソースを作成するロール、二段階に分かれてるんです。前者(CFN実行ロール)がないと、CloudFormationがリソース作成に必要な権限を持てないんですよね。
正しくは、こんな感じです:
// CloudFormation が使う実行ロール
const cfnRole = new iam.Role(this, 'CFNExecutionRole', {
assumedBy: new iam.ServicePrincipal('cloudformation.amazonaws.com'),
roleName: 'cdk-cfn-execution-role',
});
// CFNロールに、実際のリソース作成権限を与える
cfnRole.addToPrincipalPolicy(new iam.PolicyStatement({
actions: [
'ec2:*',
's3:*',
'iam:CreateRole',
'iam:AttachRolePolicy',
'iam:PutRolePolicy',
'iam:PassRole',
// ... リソースに応じた権限
],
resources: ['*'],
effect: iam.Effect.ALLOW,
}));
// クロスアカウントロールが、このCFNロールをpassできるようにする
devCrossAccountRole.addToPrincipalPolicy(new iam.PolicyStatement({
actions: ['iam:PassRole'],
resources: [cfnRole.roleArn],
effect: iam.Effect.ALLOW,
}));
これは試行錯誤で気づきました。CloudFormation自体が二段構えのロール設定を使ってるって、最初は全く頭になかったんです。「なぜデプロイが進まないんだ……」って一晩中デバッグしたのは良い思い出です。
アーティファクトキャッシング問題
もう一つ大きなハマりポイントが、アーティファクトキャッシング。CDK Pipelinesは、デフォルトでCloud Assemblyをキャッシュします。同じコミットなら、キャッシュされたアーティファクトを再利用するってわけです。
これ自体は良い機能なんですが、本番で思わぬ副作用がありました。コミットは変わらないけど、環境変数や外部データが変わったケース です。例えば、うちのチームでは、パラメータストアから値を読み込んでスタック構成を決めてました。
const dbPassword = cdk.SecretValue.secretsManager(
'rds/prod/password',
{ jsonField: 'password' }
);
このパスワードが変わっても、コミットは同じ。だからアーティファクトはキャッシュされたままで、古いパスワードでデプロイされてしまう。最初、これで本番パスワードが旧版になって、アプリケーション層で認証エラーが出ました。まじで焦りました。
対策としては、キャッシュを無効化する仕掛けを入れました:
const pipeline = new pipelines.CodePipeline(this, 'Pipeline', {
synth: new pipelines.ShellStep('Synth', {
input: pipelines.CodePipelineSource.gitHub(
'your-org/your-repo',
'main',
{
authentication: cdk.SecretValue.secretsManager('github-token'),
}
),
commands: [
'npm install',
'npm run build',
// キャッシュを明示的に破棄するタイムスタンプを含める
`npm run synth -- --context invalidateCache=${Date.now()}`,
],
}),
selfMutation: true,
cliVersion: '2.150.0', // 2026年8月時点での推奨版
});
もう一つの対策は、Secrets Managerの値の取得を、合成時じゃなくデプロイ時に遅延させる こと。CloudFormationのダイナミックリファレンスを活用すれば、スタック構成自体には影響を与えずに、値だけをリアルタイム取得できます。こっちのほうが本来は安全ですね。
デプロイ順序とスタック依存関係
マルチアカウント環境だと、デプロイ順序が重要になります。例えば、ネットワークスタックが先に必要で、その後アプリケーションスタック。それぞれ別のアカウントに入る場合、どう順序付ければいい?
CDK PipelinesはaddStage()で順序を定義しますが、ここで気をつけるポイントがあります:
const devStage = pipeline.addStage(
new AppStage(this, 'Dev', {
env: { account: devAccountId, region: 'ap-northeast-1' },
})
);
const prodStage = pipeline.addStage(
new AppStage(this, 'Prod', {
env: { account: prodAccountId, region: 'ap-northeast-1' },
})
);
// 本番環境のデプロイには手動承認を挟む
prodStage.addPre(
new pipelines.ManualApprovalStep('ApprovalBeforeProd')
);
これはシンプルですが、スタック内での依存関係はどう定義する? ってのがある。例えば、VPCスタックとRDSスタックがあって、RDSはVPCのサブネット情報が必要な場合です。
うちはこう対応しました:
export class AppStage extends cdk.Stage {
constructor(
scope: cdk.App,
id: string,
props: AppStageProps
) {
super(scope, id, props);
// VPCスタック(先に作成される)
const vpcStack = new VpcStack(this, 'VpcStack', {
env: props.env,
});
// RDSスタック(VPC作成完了後に作成される)
const rdsStack = new RdsStack(this, 'RdsStack', {
env: props.env,
vpcId: vpcStack.vpcId,
subnetIds: vpcStack.privateSubnetIds,
});
// 明示的な依存関係
rdsStack.addDependency(vpcStack);
}
}
これでパイプライン内での順序は自動制御されます。ただし、複数リージョンにデプロイする場合 は別です。そこまで来ると、Wave(波)という概念が必要になります。
const apneStage = pipeline.addStage(
new AppStage(this, 'Prod-APNE', {
env: { account: prodAccountId, region: 'ap-northeast-1' },
})
);
const useastStage = pipeline.addStage(
new AppStage(this, 'Prod-USEAST', {
env: { account: prodAccountId, region: 'us-east-1' },
})
);
// 両方同時にデプロイ(並列実行)
const wave = pipeline.addWave('MultiRegionProd');
wave.addStage(apneStage);
wave.addStage(useastStage);
これで複数リージョンへの同時デプロイが可能ですね。地味に便利です。
実運用で気づいた落とし穴
CDK本体のバージョン固定
パイプラインが実行される環境で、どのバージョンのCDK CLIを使うかって、明示的に指定しないと予想外のバージョンになることがあります。うちは、ここで2回失敗しました。新しいバージョンがリリースされると、自動で使われてしまって、古いコードとの互換性がなくなったり。
const pipeline = new pipelines.CodePipeline(this, 'Pipeline', {
synth: new pipelines.ShellStep('Synth', {
input: pipelines.CodePipelineSource.gitHub(
'your-org/your-repo',
'main',
{ authentication: cdk.SecretValue.secretsManager('github-token') }
),
commands: [
'npm install -g aws-cdk@2.150.0', // 明示的にバージョン指定
'npm install',
'npm run build',
'npx cdk synth',
],
}),
cliVersion: '2.150.0', // パイプラインでも指定
});
バージョン固定は本当に大事です。マイナーアップデートくらいなら大丈夫だろう、って思ってると痛い目見ます。
IAMロールの権限蠕食
時間が経つと、いつの間にか権限が足りなくなってる、ってことがありました。原因は、新しいサービスを追加する度に、ロールに権限を足すのを忘れてたこと。最初は「*」で許可してたんで気づきませんでしたが、セキュリティ審査が入ってから、最小権限に絞ることになりました。
もう今は、最初から最小権限で定義して、足りなくなったら足す、という方針にしてます。CloudTrail + Athena で未承認アクションを検出するパイプラインも用意してます。
-- Athenaで未承認アクションを検出
SELECT
useridentity.arn,
eventsource,
eventname,
COUNT(*) as count
FROM cloudtrail_logs
WHERE errorcode = 'AccessDenied'
AND eventtime > date_format(
current_date - interval '1' day,
'%Y-%m-%dT%H:%i:%SZ'
)
GROUP BY useridentity.arn, eventsource, eventname
ORDER BY count DESC;
これを週1で走らせて、足りない権限を探すってわけです。地味ですが、セキュリティと運用性のバランスが取れますね。
デプロイのロールバック戦略がない
これ、盲点でした。CDK Pipelinesはデプロイを自動化しますが、失敗時の自動ロールバック機能は標準では提供されていません。CloudFormation自体にはロールバック機能がありますが、全段階が自動で戻るわけじゃない。
うちは今、こんなふうに対応してます。
Dev環境では自動ロールバック を設定して、何か問題が出たら自動で前のバージョンに戻る。Prod環境では手動確認 にして、デプロイ後にヘルスチェックを見て、おかしければ手動でロールバック。ついでに SNSで通知 を送って、チーム全体が把握できるようにしてます。
本当は、Blue-Greenデプロイでより安全にしたいんですが、ECS・EKS以外のリソースでは難しい。Lambda@Edgeとか、独特の構造があるので。完全なゼロダウンタイムデプロイは、リソースタイプに応じて工夫が必要ですね。
本番運用のメトリクス
うちのチームが1年間で実装したCDK Pipelinesで、どれくらいの効果が出たか、数字で見てみましょう。
xychart-beta
title CDK Pipelines導入前後でのデプロイ効率比較
x-axis [導入前, 導入後]
y-axis "時間(分)" 0 --> 120
line [90, 12] title "平均デプロイ時間"
line [50, 5] title "デプロイ失敗時の手作業修復時間"
グラフを見ると、デプロイ時間は大幅に短縮されました。90分かかってたのが12分で済むように。手作業修復も50分から5分に短縮されてます。
もう一つのメトリクス、月間デプロイ回数とデプロイ失敗率の推移を見てみます:
| 期間 | 総デプロイ数 | 失敗数 | 失敗率 | 主な原因 |
|---|---|---|---|---|
| 導入前(半年) | 25 | 8 | 32% | 手作業ミス、権限不足 |
| 導入直後(3ヶ月) | 45 | 12 | 27% | キャッシング問題、権限設定 |
| 現在(直近3ヶ月) | 85 | 2 | 2.4% | 本番に関連した予期しないリソース削除 |
失敗率がここまで下がったのは、自動テスト・ユニットテストをパイプラインに組み込んだおかげが大きいです。コミット時点で問題を弾けるようになったんですね。
おまけに月間デプロイ回数が25回から85回に増えた。自動化されたから、もっと頻繁にデプロイするようになったんです。小さな変更でも即座にリリース、みたいな文化になってきた。これはCI/CDの本来の狙いでもあります。
まとめ
CDK Pipelines、確かに強力ですが、マルチアカウント環境で本当に価値が出ます。ただしハマりどころも多い。最後に、次のアクションを整理しておきます。
次のアクションとしては:
- クロスアカウント信頼関係を最初からちゃんと設定する — CloudFormation実行ロールのことを忘れずに。二段階のロール設定を意識すること
- アーティファクトキャッシュの仕組みを理解する — 外部データが変わる場合は、キャッシュ無効化の工夫を入れる
- IAMは最小権限で始める — 後から足すほうが安全で、未承認アクション検出パイプラインも作ると運用が楽
- デプロイ順序と依存関係を明示的に定義する — 複数リージョンの場合はWaveを活用する
- 本番環境には必ず手動承認ステージを挟む — 自動化の落とし穴を防ぐためにも重要
うちのチームでは、この設計で安定して運用できてます。正直、最初の3ヶ月は辛かったですが、今は本当に便利です。きっと皆さんのチームでも役に立つと思いますよ。何か質問あれば、気軽にどうぞ。