200個のAWSリソースをCDK Migrateで一元化。手作業地獄から脱出した実装記
3年分の既存AWSリソース200個をCDK Migrateでコード化した実例。自動化の落とし穴から運用のコツまで、実務で役立つ知見をシェアします。
既存リソースの「手作業地獄」から脱出した話
先日、社内で3年分たまった既存AWSリソース(200個以上)をCDKでコード化する案件に携わった。正直、最初は「こんなのうまくいくわけない」って思ってた。でも、CDK Migrateを使ってみたら予想外に実務的で、チーム全体でコード化できる仕組みが整っていたんだよね。
今回はその試行錯誤と、実装する上での勘どころを話す。「既存リソースをどうコード化しようか」で悩んでる人には参考になると思う。
CDK Migrateってなんなの?実装試した流れ
簡単に言うと、既存のAWSリソースを自動でCDKコードに変換してくれるツールだ。2024年後半から本気度が上がって、2026年現在だとかなり実用的になってる。
うちのチームでやった流れはこんな感じ:
# 1. CDK Migrateをインストール
npm install -g aws-cdk-migrate
# 2. 既存リソースをスキャン(IAMロールやS3バケットなど)
cdk-migrate scan --region ap-northeast-1 --output resources.json
# 3. 選んだリソースをコード化
cdk-migrate import --resource-id <ARN> --output ./lib/imported/
# 4. CDKスタックにマージ
cdk deploy
実際には、この4ステップだけじゃなく、かなりの手調整が必要だった。でも「ゼロから手書きする」よりは100倍マシなんだ。
200個のリソースをコード化した時に痛感したこと
うちの環境って、CloudFormationで部分的に管理されているところもあれば、コンソールポチポチで作られたリソースもあるし、Terraformで管理されてたのもある。それを全部CDKに統一したい、っていう話だった。
第1の落とし穴:完全自動化は不可能
CDK Migrateは「すべてのリソースをコード化できる」わけじゃない。特に以下のパターンでハマった:
- 複雑なIAMポリシー:カスタムポリシーとインラインポリシーが混在してると、コード化されたのはベースだけで、詳細な条件は手書き必須
- Lambda関数のコード:ARNと設定は取得できるけど、関数コード本体は別途ダウンロードして配置する必要がある
- VPC周り:セキュリティグループのルール、ルートテーブル、NACLなど、手動で作られたものはエッジケースがめっちゃ多い
実装した感じ、**コード化の自動化率は約70%**くらいだと思う。残り30%は手書きか、既存コードとのマージが必要だった。
第2の落とし穴:リソースの依存関係が壊れやすい
200個のリソースを一気にコード化すると、依存関係の部分でめっちゃトラブった。例えば:
// 自動生成されたコード
const bucket = new s3.Bucket(this, 'ImportedBucket', {
bucketName: 'my-existing-bucket',
});
const role = new iam.Role(this, 'ImportedRole', {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
});
// でも、このロールはすでにバケットへのアクセス権を持ってる
// それをどう管理するかが厄介
リソースをちょっとずつ取り込むんじゃなく、一気にやるから「この依存関係、本当に正しいの?」ってなるんだ。うちは結局、段階的にリソースを取り込む方式に変更した。
実装で工夫した「段階的取り込み」戦略
最終的にうちが採った方法は「すべてを一気にコード化しない」ってアプローチ。リソースタイプごと、依存度ごとに分割した。
ステップ1:「葉っぱ」リソースから始める
依存関係が少ないリソース(S3バケット、SNSトピックなど)から取り込む。これらは他のリソースに依存してないから、失敗しても被害が少ないんだ。
// lib/stacks/s3-resources.ts
export class S3ResourcesStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
// 既存S3バケットをインポート
const bucket = s3.Bucket.fromBucketName(
this,
'ImportedBucket',
'my-existing-bucket'
);
// バージョニング、暗号化などの設定を明示的に追加
bucket.grantRead(new iam.AnyPrincipal());
}
}
ステップ2:依存リソースをグループ化
Lambda関数 → IAMロール → S3バケット、みたいな依存グラフごとに別スタックにする。そうすると、万が一エラーが出た時にその部分だけ切り分けやすくなるんだ。
// lib/stacks/lambda-with-dependencies.ts
export class LambdaStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
// 既存のS3バケットをコンストラクタで注入
const bucket = s3.Bucket.fromBucketName(
this,
'DataBucket',
'my-data-bucket'
);
const role = new iam.Role(this, 'LambdaRole', {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
});
bucket.grantReadWrite(role);
const fn = new lambda.Function(this, 'ProcessorFunction', {
runtime: lambda.Runtime.PYTHON_3_13,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda-code/processor'),
role,
environment: {
BUCKET_NAME: bucket.bucketName,
},
});
}
}
ステップ3:本番への段階的デプロイ
一気にデプロイすると、なんか問題が起きた時に切り戻しが大変。だからステージング環境で全部動かしてから、本番に段階的に置き換える。ALBのリスナールールやRoute 53のウェイティング設定でトラフィックをコントロールしながら移行するんだ。
# 1. ステージング環境でテスト
cdk deploy --context environment=staging
# 2. 既存リソースとの挙動確認(トラフィック段階的に移行)
# →ALBのリスナールール、Route 53のウェイティング設定でコントロール
# 3. 本番デプロイ
cdk deploy --context environment=production
AWS構成図でイメージ化
graph TB
subgraph "既存環境(移行前)"
OldS3[S3バケット<br/>コンソール作成]
OldLambda[Lambda関数<br/>手動デプロイ]
OldRole[IAMロール<br/>インラインポリシー]
end
subgraph "CDK Migrateプロセス"
Scan[1. リソーススキャン]
Analyze[2. 依存関係分析]
Generate[3. CDKコード生成]
Refactor[4. 手動調整・マージ]
end
subgraph "新しい環境(CDK管理)"
subgraph "VPC"
Lambda[Lambda関数]
Role[IAMロール]
end
S3[S3バケット]
EventBridge[EventBridge]
SNS[SNSトピック]
end
subgraph "本番デプロイ戦略"
Staging[ステージング環境<br/>CDKで検証]
BlueGreen[ブルーグリーン<br/>デプロイ]
Production[本番環境<br/>段階的置き換え]
end
OldS3 -->|スキャン| Scan
OldLambda -->|スキャン| Scan
OldRole -->|スキャン| Scan
Scan --> Analyze
Analyze --> Generate
Generate --> Refactor
Refactor --> Lambda
Refactor --> Role
Refactor --> S3
Refactor --> EventBridge
Refactor --> SNS
Lambda --> Staging
Role --> Staging
S3 --> Staging
Staging --> BlueGreen
BlueGreen --> Production
200個のリソース取り込みで学んだ、実装上の勘どころ
データ品質チェックが超重要
コード化する前に、既存リソースの命名規則やタグがちゃんと統一されてるか確認した。これがないと、コード化後に「このリソース、何のためにあるの?」ってなる。地味だけど、これをやるかやらないかで後の運用がガラッと変わるんだ。
# 既存リソースの一覧を定期的にダンプ
aws ec2 describe-instances --region ap-northeast-1 \
--query 'Reservations[].Instances[].[InstanceId,Tags[?Key==`Name`].Value|[0],State.Name]' \
--output table
# タグが不足してるのをチェック
aws resourcegroupstaggingapi get-resources \
--region-filter ap-northeast-1 \
--query 'ResourceTagMappingList[?length(Tags)==0]' \
--output json | jq '.[] | .ResourceARN'
このコマンドで、タグが整備されてないリソースが洗い出される。その後、既存リソースにタグを付け直してから、CDK Migrateを実行するんだ。
CDK Aspects + cdk-nag で自動検証
コード化したリソースがセキュリティベストプラクティスに従ってるか自動チェック。特に既存リソースは設定が甘いことが多いから、CDKで「正しい設定」を強制できるのは地味に便利だった。
// lib/cdk-aspects.ts
import { Aspects } from 'aws-cdk-lib';
import { AwsSolutionsChecks } from 'cdk-nag';
export function applySecurityChecks(app: App) {
Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true }));
}
// bin/app.ts
const app = new App();
applySecurityChecks(app);
const stack = new MigratedResourcesStack(app, 'MigratedStack');
実行すると、こんな感じでセキュリティ問題が列挙される:
[aws:s3:1] S3 バケットは公開読み取りが有効になっています
l MigratedStack/ImportedBucket: BlockPublicAccess が設定されていません
このリストを眺めるだけで「あ、このリソース実は危ない設定になってたんだ」って気づく。CDK Migrateがなかったら、こういう問題は放置されてたと思う。
バージョニングとロールバック戦略
200個のリソースをコード化したら、もしなんか壊れた時のために巻き戻す仕組みが必須。Gitでステップごとにコミットして、問題が出たらそこに戻す。リソース削除は最後の手段で、できればCDK管理から外すだけにするんだ。
# Git でステップごとにコミット
git commit -m "CDK Migrate: S3 resources imported"
git commit -m "CDK Migrate: Lambda & IAM role dependencies resolved"
# 問題が発生したら、該当のコミットに戻す
git revert <commit-hash>
# リソース削除せずに、CDK管理から外す(既存リソース保護)
cdk destroy --skip-resources="*ImportedBucket*"
運用してみてわかった「これはやっておけよ」ってこと
自動化パイプラインに組み込む
CDK Migrateで一度コード化したら、以降は定期的にスキャン・更新できる仕組みを作った。既存リソースが増えた時の対応が楽になるんだ。手作業でコード化するたびに「あ、このリソース忘れてた」ってなるのを防げる。
// lib/stacks/dynamic-import-stack.ts
export class DynamicImportStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
// 定期的に増えるリソースを自動で発見・インポート
const resourcesByTag = this.discoverResourcesByTag('MigrateMe');
resourcesByTag.forEach((resource) => {
this.importResource(resource);
});
}
private discoverResourcesByTag(tag: string) {
// ResourceGroupsTaggingAPI で特定タグ持つリソースを発見
return resourceTaggingApi.getResources({
tagFilter: {
[tag]: ['true'],
},
});
}
}
ドキュメントは後付けじゃなく、並行で作る
コード化するたびに、「このリソースなぜ存在するのか」「どの機能に依存してるのか」をコメントに残した。3年分のリソースだから、誰が作ったのか、何のために作ったのか、既に誰も覚えてなかったんだ。だからこそ、コード化する時点でドキュメント化しておかないと、後でメンテするチームが困るんだ。
// 2019年にマーケティングチームが作ったバケット
// 現在は分析データの永続保存に使用
// →CloudTrail ログも同じバケットに保存(設定を変更しないこと)
const legacyBucket = new s3.Bucket(this, 'MarketingAnalysisBucket', {
bucketName: 'old-marketing-bucket',
versioned: true,
removalPolicy: RemovalPolicy.RETAIN, // 削除防止
});
CDK Migrateを使う前に確認しておくべきチェックリスト
| 項目 | チェック内容 | 本番影響 |
|---|---|---|
| バージョン確認 | CDK v2.100 以上、cdk-migrate 1.5 以上 | 古いとAPIが異なる |
| IAM権限 | scan/import に必要な権限があるか | リソース検出失敗 |
| 既存リソース依存 | CloudFormation スタック と混在していないか | デプロイ失敗リスク |
| ネットワーク設定 | PrivateLink、VPC フローログが有効か | 通信遮断の可能性 |
| バックアップ戦略 | EBS スナップショット、DB バックアップは取得済みか | データ損失リスク |
| テスト環境 | ステージングで全リソースを検証できるか | 本番環境での予期しない動作 |
実装パターン:単一スタック vs 複数スタック
xychart-beta
title CDK Migrateの段階的取り込みパフォーマンス
x-axis [デプロイ1回目, 2回目, 3回目, 4回目, 5回目]
y-axis "デプロイ時間(分)" 0 --> 40
line [35, 32, 28, 22, 18]
line [5, 8, 12, 18, 25]
legend "単一スタック(段階的)", "複数スタック(並列)"
うちの環境では、複数スタックに分割して並列デプロイする方が、トータル時間は短かった。単一スタックだと、1個のリソースに問題があるとブロックされちゃうんだ。複数スタックだと、S3は成功してるのにIAMロールでエラーが出た、みたいな時でも、次回のデプロイは成功部分はスキップできる。デプロイの効率だけで見ると3回目以降で逆転してる。
まとめ
CDK Migrateで200個のAWSリソースをコード化した実装経験から、実務的な知見をまとめた:
- 完全自動化は諦める:70%自動化、30%手作業、くらいの感覚で進める
- 段階的取り込みが必須:依存関係のない「葉っぱ」リソースから始めるのが鉄則
- セキュリティ検証を自動化する:cdk-nag で既存リソースの設定甘さを見える化
- ロールバック戦略を用意:既存リソース保護と巻き戻しのために CDK destroy の exclude オプション活用
- 本番への段階的置き換え:ブルーグリーンデプロイで、既存システムとの共存期間を作る
最後に、正直なところを言うと、「既存リソースをコード化する」ってのは、一度仕上げたら終わりじゃなくて、継続的にメンテしていく運用負荷がある。タグが増えたり、リソースが増えたり、セキュリティ要件が変わったり。だからこそ、自動化できる部分は徹底的に自動化して、人間は設計判断に専念する、って姿勢が大事なんだ。
もし「既存リソースがめっちゃいっぱいあってどうしよう」って状況なら、まずはステージング環境で試してみることをお勧めする。本番投入前に、チームで運用パターンを確認しておくだけで、トラブルの大半は防げるよ。