AWS CDK で安全にリファクタリングする方法 4 選
2026-10-05 | Author : 後藤 健太 (AWS DevTools Hero)
はじめに
皆さん、こんにちは。AWS DevTools Hero の後藤と申します。普段、AWS Cloud Development Kit (AWS CDK) へのコントリビュート活動を行っており、Top Contributor、並びに Community Reviewer に選定いただいています。
AWS CDK は、使い慣れたプログラミング言語でクラウドリソースを定義できるオープンソースのフレームワークです。
AWS CDK では、リソースの定義をカスタムコンストラクトへ切り出すといったリファクタリングを行うと、デプロイ時にリソースが置換される、つまり削除して再作成されることがあります。ステートフルなリソースであれば、格納されていたデータなどが失われてしまいます。
今回は、この置換を起こさずに安全にリファクタリングする方法を 4 つご紹介します。
※本記事は、AWS CDK (aws-cdk-lib) v2.263.0、AWS CDK CLI (aws-cdk) v2.1139.0 時点の内容です。今回ご紹介する cdk refactor と cdk orphan を使うには、CDK CLI だけでなく、ブートストラップスタックも両コマンドに対応している必要があります。これらのコマンドがエラーになる場合は、CDK CLI を更新したうえで cdk bootstrap を再実行し、ブートストラップスタックを更新してください。
builders.flash メールメンバー登録
builders.flash メールメンバー登録で、毎月の最新アップデート情報とともに、AWS を無料でお試しいただけるクレジットコードを受け取ることができます。
コンストラクトツリーとパス
まずは、リファクタリングでリソースが置換される仕組みから見ていきます。
AWS CDK でリソースを定義するときは、new Queue(this, 'Queue') のように、第 1 引数へ親となるコンストラクトを、第 2 引数へコンストラクト ID と呼ばれる文字列を渡します。第 1 引数は多くの場合、定義中のスタックやコンストラクトを指す `this` です。コンストラクト ID は、同じ親の配下でコンストラクトを識別するための文字列です。
こうして App を頂点にコンストラクトが親子でつながった階層構造はコンストラクトツリーと呼ばれ、ツリー上の位置は、コンストラクト ID を親からつなげた MyStack/Queue のようなパスで表されます。
リファクタリング前のコード
ここからは、実際に置換が起こる例を見てみましょう。スタック直下に、Amazon SQS のキュー、Amazon SNS のトピック、AWS Lambda の関数の 3 つのリソースを定義した以下のコードを考えます。キューには処理待ちの通知メッセージが溜まっており、削除されると稼働中のシステムに影響が出ます。一方、トピックはステートレスなリソースであり、置換を許容できるとします。なお、Lambda 関数のコードを指定している Code.fromAsset は、ローカルの lambda ディレクトリの中身を Lambda のコードとしてデプロイする指定方法です。
export class MyStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
new Queue(this, 'Queue');
new Topic(this, 'Topic');
new Function(this, 'Function', {
runtime: Runtime.NODEJS_24_X,
handler: 'index.handler',
code: Code.fromAsset('lambda'),
});
}
}
リファクタリング後のコード
先ほどの 3 つのリソースのうち、通知機能を構成するキューとトピックを Notifications というカスタムコンストラクトへ切り出し、スタックからはそれを呼び出す形にします。Lambda 関数はスタック直下に残します。デプロイされる AWS リソースの構成そのものは変わりません。
export class Notifications extends Construct {
constructor(scope: Construct, id: string) {
super(scope, id);
new Queue(this, 'Queue');
new Topic(this, 'Topic');
}
}
export class MyStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
new Notifications(this, 'Notifications');
new Function(this, 'Function', {
runtime: Runtime.NODEJS_24_X,
handler: 'index.handler',
code: Code.fromAsset('lambda'),
});
}
}
論理 ID の変化と置換の発生
AWS CDK は、パスをもとに AWS CloudFormation テンプレート上の論理 ID を自動生成します。たとえばリファクタリング前の Queue であれば、Queue4A7E3555 のような、パス由来の文字列にハッシュ値を付けた論理 ID になります。つまり、リファクタリングでコンストラクトのツリー上の位置やコンストラクト ID を変えると、パスとともに論理 ID も変わります。
今回の例でも、Queue のパスは MyStack/Queue から MyStack/Notifications/Queue へ変わり、論理 ID も Queue4A7E3555 から NotificationsQueue91395D8F へ変化します。Topic も同様に、TopicBFC7AF6E から NotificationsTopicAE679CBD へ変わります。一方、スタック直下に残した Lambda 関数はパスが変わらないため、論理 ID もそのままです。
CloudFormation は、リソースを論理 ID で識別します。そのため、この状態でデプロイすると、論理 ID が変わったキューとトピックは別のリソースとして扱われ、削除されて新しく作り直されます。トピックの置換は許容できる前提でしたが、キューに溜まっていた通知メッセージは失われてしまいます。
リファクタリング後の論理 ID の変化
上記の例を図示すると、以下のようになります。
安全にリファクタリングする 4 つの方法
この置換を防ぐ方法として、以下の 4 つをご紹介します。いずれも、デプロイ済みの実リソースを作り直すことなく、リファクタリング後のコードへ引き継ぐための方法です。
- cdk refactor コマンド
- cdk orphan + cdk import コマンド
- コンストラクト ID の Default 指定
- 論理 ID のオーバーライド
1. cdk refactor コマンド
1 つ目は、cdk refactor コマンドです。実行すると、コンストラクトツリー上で位置が変わったリソースが検出され、再作成されることなく新しい位置へ移動されます。同じスタック内の移動にも、別のスタックへの移動にも対応しています。冒頭の例のような複数リソースの移動もまとめて反映でき、キューだけでなくトピックも置換されずに引き継がれます。
1-1. cdk refactor の使い方
リファクタリングを行ったら、デプロイの前に cdk refactor を実行します。--dry-run を付けると、適用はせずに、移動が検出されたリソースの一覧を表示できます。意図した移動になっていることを確認できたら、--dry-run を外して実行します。確認のプロンプトに答えると移動が適用され、最後に、パスのメタデータなど、テンプレートの細部をコードとそろえるためのデプロイが自動で実行されます。後述のとおり、リソースの追加・削除やプロパティの変更が含まれる状態では cdk refactor が失敗するため、このデプロイで、移動したリソースにもそれ以外のリソースにも変更が入ることはありません。
※利用には --unstable=refactor の指定が必要で、今後オプションや挙動が変わる可能性があります。
--dry-run を付けた実行例
以下は、--dry-run を付けた実行例です。
$ npx cdk refactor --unstable=refactor --dry-run
The following resources were moved or renamed:
┌─────────────────┬────────────────────────┬──────────────────────────────────────┐
│ Resource Type │ Old Construct Path │ New Construct Path │
├─────────────────┼────────────────────────┼──────────────────────────────────────┤
│ AWS::SQS::Queue │ MyStack/Queue/Resource │ MyStack/Notifications/Queue/Resource │
├─────────────────┼────────────────────────┼──────────────────────────────────────┤
│ AWS::SNS::Topic │ MyStack/Topic/Resource │ MyStack/Notifications/Topic/Resource │
└─────────────────┴────────────────────────┴──────────────────────────────────────┘
1-2. cdk refactor が使えないケース
手軽な一方で、以下のようなケースには使えません。
- リソースの追加・削除やプロパティの変更を伴う移動
- コンストラクトツリーのパスから自動生成された文字列をプロパティに持つリソースの移動 (Amazon ECS のタスク定義のファミリー名や、Amazon CloudFront のオリジンの識別子など)
- 対応していないリソースの移動(カスタムリソースなど)
1 つ目について、cdk refactor が扱えるのはリソースの移動だけです。それ以外の変更を伴う場合は、コードの変更を 2 段階に分けます。先にコード上で移動だけを行って cdk refactor で反映し、その後にリソースの追加やプロパティの変更を行って cdk deploy で反映します。
2 つ目も、実は 1 つ目と同じ理由です。パス由来の文字列は、移動するとパスとともに値が変わります。そのため、コード上では移動しか行っていなくても、プロパティの変更を伴う移動として扱われてしまいます。
3 つ目について、cdk refactor が内部で利用している CloudFormation の スタックリファクタリング 機能には、対応していないリソースがあります。代表例がカスタムリソースで、BucketDeployment のように内部でカスタムリソースを使う L2 コンストラクトも移動できません。
2. cdk orphan + cdk import コマンド
2 つ目は、cdk orphan と cdk import コマンドの組み合わせで、cdk refactor が使えない構成でも対応できます。置換を防ぎたいリソースをいったんスタックの外へ切り離し、リファクタリング後の新しい位置へ取り込み直す、という流れです。手順は以下のとおりです。
- 置換を防ぎたいリソースを cdk orphan でスタックから切り離す
- コードをリファクタリングする
- cdk import で、切り離したリソースを新しい位置へ取り込む
- cdk deploy で残りの変更を反映する
2-1. cdk orphan の実行
cdk orphan は、リソースを削除せずにスタックの管理から外すコマンドです。内部では、対象のリソースが削除されないように CloudFormation の DeletionPolicy を Retain に設定し、他のリソースからの参照をキューの URL や ARN などの実際の値へ置き換えたうえで、テンプレートから取り除きます。そのため、スタックの管理からは外れますが、リソース自体は AWS アカウントに残り続けます。
※利用には --unstable=orphan の指定が必要で、今後オプションや挙動が変わる可能性があります。
cdk orphan の実行例
以下のように対象のリソースをパスで指定して実行すると、切り離されるリソースの一覧が表示され、確認のプロンプトに答えると処理が始まります。冒頭の例で置換を防ぎたいのはキューだけなので、キューのみを指定します。トピックは切り離さず、手順の最後の cdk deploy で置換させます。
$ npx cdk orphan --unstable=orphan MyStack/Queue
Stack: MyStack
Resources to orphan (1):
Queue4A7E3555 (AWS::SQS::Queue) - /MyStack/Queue/Resource
Do you wish to orphan these resources? This will perform 3 CloudFormation deployments. (y/n) y
...
✅ Resources orphaned from MyStack
2-2. cdk import の実行
cdk orphan が完了したら、コードをリファクタリングしたうえで cdk import を実行します。取り込むリソースの識別子を入力するプロンプトが表示されるので、キューには URL を入力し、トピックは何も入力せずに Enter を押してスキップします。これにより、切り離していたキューが、メッセージを保持したまま新しい論理 ID でスタックへ取り込まれます。実行例は以下のとおりです。
$ npx cdk import
...
Perform import? (y/n) y
MyStack/Notifications/Queue/Resource (AWS::SQS::Queue): enter QueueUrl (empty to skip) https://sqs.us-east-1.amazonaws.com/123456789012/MyStack-Queue4A7E3555-pNg4kbUdhvRE
MyStack/Notifications/Topic/Resource (AWS::SNS::Topic): enter TopicArn (empty to skip)
Skipping import of MyStack/Notifications/Topic/Resource
✅ MyStack
Some resources were skipped. Run another cdk import or a cdk deploy to bring the stack up-to-date with your CDK app definition.
2-3. 仕上げの cdk deploy と注意点
最後に cdk deploy を実行すると、トピックの置換を含む残りの変更が反映され、一連の手順は完了です。
なお、この方法には以下の 3 つの注意点があります。
- cdk import で取り込めるのは、CloudFormation のリソースインポートに対応したリソースタイプのみ
- cdk import は、必ずコードのリファクタリングを済ませてから実行する
- cdk orphan から cdk import までの間に、通常のデプロイを挟まない
1 つ目について、対応しているリソースタイプの一覧は、CloudFormation の リソースインポート のドキュメントに掲載されています。
2 つ目について、cdk import は手元のコードを合成して取り込み先を決めるため、リファクタリング前のコードのまま実行すると、リソースが元の位置に取り込まれてしまいます。
3 つ目について、間にデプロイを挟むと、コード上のキューの定義が、既存のキューとは別の新しい空のリソースとして作成されてしまいます。
3. コンストラクト ID の Default 指定
3 つ目にご紹介するのは、切り出し先のカスタムコンストラクトの中で、リソースのコンストラクト ID を Default にする方法です。論理 ID はパスから生成されるため、リファクタリング後もパスが変わらないようにコンストラクト ID を付ける、という考え方です。ここまでの 2 つの方法と違い、追加のコマンドは必要ありません。
3-1. L1 コンストラクトとパス
この方法の前提として、冒頭の例のキューのパスをもう少し詳しく見てみます。実は、Queue のような L2 コンストラクトの内部には、CloudFormation のリソースと 1 対 1 で対応する L1 コンストラクトが、Resource というコンストラクト ID で定義されています。そのため、論理 ID の計算に使われる実際のパスは MyStack/Queue/Resource で、ここから Queue4A7E3555 が生成されていました。1-1 の実行例で、パスの末尾に /Resource が付いていたのもこのためです。
3-2. Default による論理 ID の維持
AWS CDK は論理 ID を決める際、パスの構成要素のうち Default という文字列を計算の対象から除外します。この性質を利用して、切り出したカスタムコンストラクトの内部で、論理 ID を維持したいキューのコンストラクト ID を `Default` にします。そのうえで、Notifications のコンストラクト ID には、切り出し前のキューと同じ Queue を指定します。
これにより、キューの L1 コンストラクトのパスは MyStack/Queue/Default/Resource となります。Default は計算から除外されるため、論理 ID は切り出し前と同じ Queue4A7E3555 のまま維持されます。あとは通常どおり cdk deploy を実行します。このデプロイで、論理 ID が変わらないキューには何の変更も起こらず、トピックの置換など、それ以外の変更だけが反映されます。
実際のコード
コードは以下のとおりです。
export class Notifications extends Construct {
constructor(scope: Construct, id: string) {
super(scope, id);
// パスは MyStack/Queue/Default/Resource となり、論理 ID は変わらない
new Queue(this, 'Default');
// トピックの論理 ID は変わるが、置換を許容する
new Topic(this, 'Topic');
}
}
// コンストラクト ID には、切り出し前のキューと同じ 'Queue' を指定する
new Notifications(this, 'Queue');
3-3. 適用できる条件
この方法を適用するためには、以下の 2 つの条件があります。
- 論理 ID を維持したいリソースが、切り出し先のカスタムコンストラクトの中で 1 つだけであること
- 切り出し先のカスタムコンストラクトのコンストラクト ID を、移動したいリソースやコンストラクトの元の ID と同じにすること
1 つ目について、Default は同じ親の下に 1 つしか付けられません。上記の例でも、論理 ID を維持したのはキューだけで、トピックは前提のとおり置換を許容しています。一方で、論理 ID を維持する必要がないリソースは、コンストラクトの中にいくつあっても構いません。新しく追加するリソースはもちろん、冒頭の例のトピックのように、置換を許容できる既存のリソースも一緒に移せます。
2 つ目について、上記の例でも Notifications に Queue というコンストラクト ID を指定しており、切り出しにあわせて別の名前を付けたい場合には使えません。
Default が有効なケース
この方法は、置換を防ぎたいリソース 1 つを中心にまとめるような切り出しで有効です。ここまでのパスと論理 ID の関係をまとめると、以下のとおりです。
4. 論理 ID のオーバーライド
最後にご紹介するのは、AWS CDK が自動的に決める論理 ID を、リファクタリング前の値へ固定する方法です。
論理 ID が変わらなければ、CloudFormation から見たリソースは同じものです。そのため、リファクタリングしたコードを通常の cdk deploy で反映するだけで済みます。デプロイの際、論理 ID を固定したリソースには何の変更も起こらず、それ以外の変更だけが反映されます。リソースインポートに対応していないリソースタイプにも使えるため、他の方法が使えない場合の最終手段になります。固定の方法は 2 つあります。
4-1. overrideLogicalId による固定
AWS CDK には、L2 コンストラクトの内部にある L1 コンストラクトを取り出して直接操作する、エスケープハッチ (escape hatch) と呼ばれる仕組みがあります。以下のように、各コンストラクトが持つ node プロパティの defaultChild で L1 コンストラクトを取り出し、overrideLogicalId にリファクタリング前の論理 ID を渡します。
export class Notifications extends Construct {
constructor(scope: Construct, id: string) {
super(scope, id);
const queue = new Queue(this, 'Queue');
// リファクタリング前の論理 ID へ固定する(トピックは固定せず置換を許容)
const cfnQueue = queue.node.defaultChild as CfnQueue;
cfnQueue.overrideLogicalId('Queue4A7E3555');
new Topic(this, 'Topic');
}
}
4-2. renameLogicalId による固定
もう 1 つが、スタックの renameLogicalId メソッドです。こちらはコンストラクトを経由せず、論理 ID そのものを対象に付け替えを行います。そのため、L2 コンストラクトが内部で自動生成する関連リソースのように、defaultChild では取り出せないリソースにも使えます。 使い方は次の順序になります。まず、リファクタリングだけを行ったコードを一度 cdk synth し、新しく生成される論理 ID を出力されたテンプレートで確認します。そのうえで、以下のように、確認した論理 ID を第 1 引数に、維持したいリファクタリング前の論理 ID を第 2 引数にして、renameLogicalId を書き加えます。
export class MyStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
// リソースの定義は省略
this.renameLogicalId('NotificationsQueue91395D8F', 'Queue4A7E3555');
}
}
4-3. 固定用のコードが残る難点
この方法の難点は、論理 ID を固定するコードがリファクタリング後も残り続けることです。本来は AWS CDK が自動で決める値を明示的に指定した状態になるため、なぜ固定しているのかをコメントで残しておくとよいでしょう。
それぞれの方法の使い分け
置換を防ぎたいリソースが移動しないのであれば、対策は不要でそのままデプロイして問題ありません。対策が必要な場合は、まず、コマンド 1 つで完結する cdk refactor を検討します。使えない構成であれば cdk orphan + cdk import を検討します。それも使えなければ、行いたい切り出しが 3-3 の条件に当てはまる場合はコンストラクト ID の Default 指定を使い、当てはまらない場合は、最後の手段として論理 ID のオーバーライドを使います。
補足 1. ステートレスなリソースの置換の許容
前述のとおり、すべてのリソースで置換を防ぐ必要はありません。では、どのようなリソースなら置換を許容できるのでしょうか。
Lambda 関数や、冒頭の例で見た SNS のトピックのようなステートレスなリソースは、保持しているデータがないため、置換されても基本的には問題ありません。対策が必要なのは主に、Amazon DynamoDB のテーブルや Amazon S3 のバケット、冒頭の例のキューのように、置換されるとデータごと失われるリソースです。それ以外の置換を許容すれば、リファクタリングの手間を大きく減らせます。
補足 2. リソースの物理名の未指定
置換を許容する場合、リソースの物理名の扱いに注意が必要です。物理名は、バケット名や関数名といった、AWS 上での実際のリソース名のことです。CloudFormation の置換では新しいリソースの作成が先に行われ、その後に古いリソースの削除が行われます。そのため、コードで物理名を指定していると、同じ名前を持つ元のリソースが作成のタイミングでまだ存在しており、「already exists」のエラーでデプロイが失敗してしまいます。
これを避けるには、物理名を指定しないのが基本です。省略すれば、一意な名前が自動生成されるため、置換の際にも名前が衝突しません。物理名を指定しないでおくことは、リファクタリングの柔軟性にもつながります。
さいごに
AWS CDK で安全にリファクタリングするための方法として、以下の 4 つをご紹介しました。
- cdk refactor コマンド
- cdk orphan + cdk import コマンド
- コンストラクト ID の Default 指定
- 論理 ID のオーバーライド
これらを知っておくことで、リリース済みのスタックに対しても安心してコードを整理できます。あわせて、ステートレスなリソースの置換は許容し、物理名は指定しないという方針も有効です。ぜひ、日々の開発に取り入れてみてください。
筆者プロフィール
後藤 健太 (AWS DevTools Hero / @365_step_tech)
AWS CDK のコントリビュート活動を行っており、Top Contributor や Community Reviewer に選定。2024 年 2 月に発足されたコミュニティ駆動の CDK コンストラクトライブラリである Open Constructs Library では、メンテナーを担っている。
また、cls3 や delstack といった自作 AWS ツールの OSS 開発も行なっている。2024 年 3 月、AWS DevTools Hero に選出。