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年時点ではかなり安定しています。
基本的なフローはこう進みます。
- スキャン:対象AWSアカウント・リージョンの既存リソースを自動検出
- 変換:CloudFormation経由でCDK(TypeScript/Python)コードを生成
- 修正:生成されたコードを手で調整(実務的にはここが結構重要)
- 検証:CDK diffで変更を確認
- デプロイ: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 diffとcdk 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化の時間よりも、修正・検証・ドキュメント化のフェーズに時間が掛かるということを念頭に置いて計画すると、ぶち当たるトラブルが減りますよ。