CDK Migrateで200個のAWSリソース一元化した話|落とし穴と成功パターン

マネコンとTerraform混在のAWSリソースをCDK Migrateで統一。6ヶ月運用してわかった実装の流れと、手作業調整が必須な理由をコード例で解説します。

CDK Migrateの存在を知ったきっかけ

うちのチームは正直、AWSリソースの構築がちぐはぐだった。マネジメントコンソールでポチポチ作ったEC2、CloudFormationで一部構築したRDS、Terraformで管理してたネットワーク——こんな感じで混在していたんですよね。2025年の終わりに「全部CDKで一元管理しよう」という経営判断が出て、その時に初めてCDK Migrateの存在を知りました。

最初は「いやいや、リソース数200個以上あるぞ…どうやって移行するんだ」って感じだったんですが、実際に導入してみたら想像以上に便利でした。ただし、当然落とし穴もある。6ヶ月運用してわかったリアルな実装パターンを共有しておきます。

CDK Migrateって何ができるの?

CDK Migrateは、既に存在するAWSリソースをスキャンして、自動的にCDKのコードに変換してくれるツール。AWS側が2025年末にアップデートを大きく入れて、2026年時点ではかなり安定しています。

基本的なフローはこう進みます。

  1. スキャン:対象AWSアカウント・リージョンの既存リソースを自動検出
  2. 変換:CloudFormation経由でCDK(TypeScript/Python)コードを生成
  3. 修正:生成されたコードを手で調整(実務的にはここが結構重要)
  4. 検証:CDK diffで変更を確認
  5. デプロイ:CDK deployで本番反映

実装としては、AWS CDK v2.140以降でcdk migrateコマンドが使えるようになってます。

実装フロー——うちが実際にやったこと

まずは簡単な構成から試してみました。VPC+EC2+セキュリティグループの3つのリソースで検証したんです。

# 既存リソースのスキャン実行
cdk migrate --selector vpc

するとどうなったか。CDKコードがポンと生成されました。生成されたコードはこんな感じです:

import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';

export class MigratedStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // VPC自動生成
    const vpc = new ec2.Vpc(this, 'Vpc-12abc', {
      cidr: '10.0.0.0/16',
      maxAzs: 3,
      natGateways: 1,
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: 'public',
          subnetType: ec2.SubnetType.PUBLIC,
        },
        {
          cidrMask: 24,
          name: 'private',
          subnetType: ec2.SubnetType.PRIVATE_WITH_NAT,
        },
      ],
    });

    // EC2インスタンス
    const instance = new ec2.Instance(this, 'Instance-abc123', {
      vpc,
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.T3,
        ec2.InstanceSize.MEDIUM
      ),
      machineImage: ec2.MachineImage.latestAmazonLinux2(),
    });
  }
}

ここまでは「おっ、いい感じ」って思うんですよ。でも実務はここから始まるんです。

生成コードの問題点——本番運用で気づいたこと

1. リソース命名がランダムすぎる

CDK Migrateが生成するコードのリソース名は、AWS側の物理IDをそのまま使うので、Vpc-12abcみたいなランダムな名前になってました。これ、本番環境ではマジで困ります。

うちのチームは命名規則が厳密に決まってて(例:prod-api-vpcみたいな)、その規則に合わせる必要があったんです。だから生成されたコードは、ほぼ全部の論理IDを手で書き直す羽目になりました。

対策:生成後、即座に論理IDをリネーム。100個以上のリソースがあったから、これだけで2日くらい掛かりました…。

2. セキュリティグループのインライン定義が複雑

EC2に紐付けてるセキュリティグループが、生成されたコード内でインライン定義されてました。

// これ、複数EC2で同じSGを使ってたら超重複
const securityGroup = new ec2.SecurityGroup(this, 'SG-instance-001', {
  vpc,
  allowAllOutbound: false,
});
securityGroup.addIngressRule(
  ec2.Peer.ipv4('0.0.0.0/0'),
  ec2.Port.tcp(443)
);
securityGroup.addIngressRule(
  ec2.Peer.ipv4('0.0.0.0/0'),
  ec2.Port.tcp(80)
);

複数のEC2で同じセキュリティグループを共有してたら、それぞれで定義が重複してしまいます。リソース量が増えるし、保守性も下がる。

対策:セキュリティグループを一度分析して、共有できるやつは共有リソースとして別ファイルに切り出しました。実装パターンはこんな感じです:

// shared-security-groups.ts
export const createApiSecurityGroup = (vpc: ec2.Vpc) => {
  const sg = new ec2.SecurityGroup(vpc, 'ApiSG', {
    description: 'API tier security group',
    allowAllOutbound: false,
  });
  sg.addIngressRule(
    ec2.Peer.ipv4('10.0.0.0/16'),
    ec2.Port.tcp(443)
  );
  return sg;
};

3. IAM ロールの権限が広すぎる

これはマジで地雷でした。EC2インスタンスに紐付いてたIAM ロールのポリシーが、Effect: Allow, Action: '*', Resource: '*'みたいな感じで生成されてました。

当然ながら、セキュリティ監査(うちのチームではSOC2をやってます)で即座にNGが出ます。手作業で権限を最小限に調整する必要があったんです。

対策:IAMポリシーだけは、CDK Migrateの自動生成に依存せず、チームで最小権限ポリシーを先に設計して適用する方式に変更しました。

AWS構成図——実装の流れ

graph TB
    subgraph "Existing Infrastructure"
        Mgmt["AWS Management Console<br/>(手作業で構築)"] 
        CF["CloudFormation<br/>(一部)"] 
        TF["Terraform<br/>(ネットワーク)"] 
    end

    subgraph "CDK Migrate Process"
        Scan["cdk migrate --selector"] 
        Generate["CloudFormation仲介<br/>CDKコード自動生成"] 
        Analyze["リソース分析<br/>重複・セキュリティ確認"]
        Adjust["手作業修正<br/>・命名規則統一<br/>・リソース統合<br/>・IAM権限調整"]
    end

    subgraph "CDK Management"
        CDKRepo["CDK Repository"] 
        Diff["cdk diff<br/>変更確認"] 
        Deploy["cdk deploy<br/>本番反映"]
    end

    subgraph "AWS Accounts"
        subgraph "Production VPC"
            VPC["VPC<br/>10.0.0.0/16"]
            subgraph "Public AZ-a"
                PublicSub1["Public Subnet"]
            end
            subgraph "Private AZ-a"
                PrivSub1["Private Subnet"]
                EC2["EC2 Instance"]
            end
            subgraph "Private AZ-b"
                PrivSub2["Private Subnet"]
                RDS["RDS Postgres"]
            end
            NATGw["NAT Gateway"]
            SG["Security Group<br/>(統合)"]
        end
    end

    Mgmt --> Scan
    CF --> Scan
    TF --> Scan
    Scan --> Generate
    Generate --> Analyze
    Analyze --> Adjust
    Adjust --> CDKRepo
    CDKRepo --> Diff
    Diff --> Deploy
    Deploy --> VPC
    EC2 --> SG
    RDS --> SG
    NATGw --> PublicSub1

本番運用で失敗した3つのパターン

パターン1:スナップショット時点でのリソース取り込み

CDK Migrateは実行時点でのリソース状態をスキャンします。だから、スキャン後に他の誰かがリソースを追加したり、手で変更されると、CDKコードと実際のリソースが乖離してしまうんです。

うちは最初、マイグレーションプロジェクトの開始日を「リソース固定日」と決めずに進めてたから、スキャン後も開発チームが引き続きマネジメントコンソールでポチポチやってて…CDKコードと実際のリソースがズレまくりました。

解決策:マイグレーション期間中は、すべてのリソース変更を一度CDKで実装してからデプロイするというルールを厳密に決めました。

パターン2:CloudFormation Drift

CDK Migrateの背後ではCloudFormationが使われています。だから、CDKコードをデプロイ後に、マネジメントコンソールで手作業修正すると「CloudFormation Drift」が発生します。

実際、初期段階で一部のエンジニアが「あ、このセキュリティグループのルール、ちょっと調整しとこ」みたいにコンソール操作しちゃって、CDK管理との整合性が崩れました。

解決策:CDK Deployメント後、すべてのリソース変更はCDKコード→CDK Deploy経由に統一。さらに、定期的にcdk diffを実行して、Driftがないか確認する自動チェックをCI/CDパイプラインに組み込みました。

パターン3:ステートフルなリソースの取り扱い

RDSデータベースやS3バケットみたいなステートフルリソースは、CDK化がめっちゃ難しい。特にS3バケットのポリシーやアクセス権限が複雑だと、自動生成コードだけでは不完全になります。

// 生成されたS3バケット定義
const bucket = new s3.Bucket(this, 'DataBucket', {
  versioned: true,
  blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
});

// でも、実際にはこんなカスタムポリシーがあったり…
const policy = new iam.PolicyStatement({
  effect: iam.Effect.ALLOW,
  actions: ['s3:GetObject', 's3:PutObject'],
  resources: [bucket.arnForObjects('app/*')],
  principals: [/* 複数のサービスロール */],
  conditions: {
    StringEquals: {
      'aws:SourceVpc': vpc.vpcId,
    },
  },
});
bucket.addToResourcePolicy(policy);

解決策:ステートフルなリソースに関しては、CDK Migrateの自動生成を参考情報として使って、実装は手書きする方式にしました。

実践的な運用パターン

マイグレーション戦略:段階的アプローチ

ぶっちゃけ、全200個のリソースを一気にCDK化しようとしたら死にます。うちは「レイヤー別」でマイグレーションスケジュールを組みました。

Phase 1(Week 1-2):ネットワーク

  • VPC、Subnet、Route Table
  • SecurityGroup(共有リソース)
  • NAT Gateway

Phase 2(Week 3-4):ステートレスコンピュート

  • EC2(ウェブサーバ層)
  • Launch Template
  • Auto Scaling Group

Phase 3(Week 5-8):ステートフル・データベース

  • RDS(手書き実装)
  • ElastiCache
  • S3(手書き実装)

Phase 4(Week 9-12):運用自動化

  • CloudWatch、SNS
  • Lambda関数(既存IAM Role)
  • EventBridge Rules

この段階的アプローチは、各フェーズでcdk diffcdk deployを実行して、本番リソースが期待通りか確認できるメリットがあります。いきなりぶち込むと、デプロイでトラブったときの影響範囲が広すぎるんですよね。

命名規則の統一

CDK化するなら、リソース名も機械的に管理する方式に変更するのがおすすめです。うちが採用したパターンはこれです:

// config/naming.ts
const naming = {
  environment: 'prod',
  service: 'api',
  region: 'ap-northeast-1',
  
  vpc: () => `${naming.environment}-${naming.service}-vpc`,
  ec2: (index: number) => `${naming.environment}-${naming.service}-ec2-${String(index).padStart(2, '0')}`,
  rds: () => `${naming.environment}-${naming.service}-postgres`,
  sg: (layer: string) => `${naming.environment}-${naming.service}-sg-${layer}`,
};

export default naming;

CDK Migrateで生成されたコードの論理IDは適当だから、リソース作成時にこの命名関数を使うように修正してます。

まとめ

CDK Migrateは確実に時間の節約になります。200個のリソースを手で全部CDK化しようとしたら、3〜4ヶ月掛かったと思う。実際には生成コード+修正で2ヶ月で終わりました。

ただし、実務的なポイントは3つですね:

1. 自動生成は80%まで——残り20%の手作業(命名規則、IAM権限、リソース統合)が本番運用を左右する。地味だけど絶対に手を抜けない部分です。

2. 段階的マイグレーション——全リソースを一気にやると、トラブル時に全体が止まる。レイヤー別に進めるだけでリスクが大きく変わります。

3. スナップショット管理が重要——マイグレーション期間は「リソース変更はCDK経由のみ」というルール徹底が必須。これを曖昧にすると、後から地獄を見ます。

正直、2026年時点ではCDK Migrateはかなり実用的。ただ、既存リソースが汚い環境ほど、後工程の修正コストが大きくなります。手作業構築→CDK化の時間よりも、修正・検証・ドキュメント化のフェーズに時間が掛かるということを念頭に置いて計画すると、ぶち当たるトラブルが減りますよ。

U

Untanbaby

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

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

関連記事