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-policiesAdministratorAccess を渡している点が気になる人もいると思う。個人的には「怖い」と感じる設定で、本番では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 Fleet18分キューイング待機をほぼゼロに
+ 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点に絞られる。

#ポイントやらかすと…
1Toolchainアカウントを独立させる開発者アカウントのIAMが肥大化し、後でリファクタが大変になる
2selfMutationの前にBootstrapを完成させる謎のクロスアカウントエラーで詰まる
3StageName・CfnOutputの命名規則を最初に決めるリネーム時にスタック削除→再作成が発生する
4ManualApprovalStepの通知を設定する承認待ちが誰も知らないまま7日間放置される
5pre/postフックにテストを全ステージ組み込むCDK Pipelinesの最大の強みを活かせないままになる

次のアクションとして、まずBootstrapの設定から始めるといい。既存のCodePipelineからの移行であれば、CDK Migrateで既存リソースを移行する方法も参考になる。

CDK Pipelinesを使い始めて「ここが分からん」ってなったことがあれば、コメントで教えてほしい。同じところでハマってる人、きっといると思う。

U

Untanbaby

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

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

関連記事