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分に短縮されてます。

もう一つのメトリクス、月間デプロイ回数とデプロイ失敗率の推移を見てみます:

期間総デプロイ数失敗数失敗率主な原因
導入前(半年)25832%手作業ミス、権限不足
導入直後(3ヶ月)451227%キャッシング問題、権限設定
現在(直近3ヶ月)8522.4%本番に関連した予期しないリソース削除

失敗率がここまで下がったのは、自動テスト・ユニットテストをパイプラインに組み込んだおかげが大きいです。コミット時点で問題を弾けるようになったんですね。

おまけに月間デプロイ回数が25回から85回に増えた。自動化されたから、もっと頻繁にデプロイするようになったんです。小さな変更でも即座にリリース、みたいな文化になってきた。これはCI/CDの本来の狙いでもあります。

まとめ

CDK Pipelines、確かに強力ですが、マルチアカウント環境で本当に価値が出ます。ただしハマりどころも多い。最後に、次のアクションを整理しておきます。

次のアクションとしては

  1. クロスアカウント信頼関係を最初からちゃんと設定する — CloudFormation実行ロールのことを忘れずに。二段階のロール設定を意識すること
  2. アーティファクトキャッシュの仕組みを理解する — 外部データが変わる場合は、キャッシュ無効化の工夫を入れる
  3. IAMは最小権限で始める — 後から足すほうが安全で、未承認アクション検出パイプラインも作ると運用が楽
  4. デプロイ順序と依存関係を明示的に定義する — 複数リージョンの場合はWaveを活用する
  5. 本番環境には必ず手動承認ステージを挟む — 自動化の落とし穴を防ぐためにも重要

うちのチームでは、この設計で安定して運用できてます。正直、最初の3ヶ月は辛かったですが、今は本当に便利です。きっと皆さんのチームでも役に立つと思いますよ。何か質問あれば、気軽にどうぞ。

U

Untanbaby

ソフトウェアエンジニア|AWS / クラウドアーキテクチャ / DevOps

10年以上のIT実務経験をもとに、現場で使える技術情報を発信しています。 記事の誤りや改善点があればお問い合わせからお気軽にご連絡ください。

関連記事