CDK は 論理 ID にハッシュ値のサフィックスを自動付与します。

  • 例:SampleVpc → SampleVpc07DAD426
  • 例外:スタック直下に置いた L1(Cfn で始まるクラス)は、パスが 1 階層(Default を除いて数える)のためハッシュが付かず、construct ID がそのまま 論理 ID になる
    • 例:CDKMetadata/Default → CDKMetadata

論理 ID の組み立て

論理 ID は「人が読める部分」と「ハッシュ」をつなげたものです。

  1. スタックからの相対パス(スタック ID は含まない)を / で分割する
  2. Default の要素を取り除く(Default と完全一致。DefaultSample などは取り除かない)
  3. 要素が 1 つだけなら、英数字以外を除いた文字列をそのまま論理 ID とする(ハッシュなし)
  4. ハッシュ:残った要素を / でつないだ文字列の MD5 の先頭 8 桁(大文字)
  5. 人が読める部分:重複を除き(直前の要素がその要素で終わっていればスキップ)、Resource を除き、英数字以外を削除してつなげる(最大 240 文字)
  6. 人が読める部分 + ハッシュ

Default と Resource の扱いの違いは以下のとおりです。

  ハッシュ 人が読める部分
Default 除く 除く
Resource 含める 除く

例:SampleJob/ExecutionRole/DefaultPolicy/Resource

  • ハッシュ:md5("SampleJob/ExecutionRole/DefaultPolicy/Resource") → 5F0F8C58
  • 人が読める部分:SampleJobExecutionRoleDefaultPolicy
  • 論理 ID:SampleJobExecutionRoleDefaultPolicy5F0F8C58

実装は aws-cdk-lib/core/lib/private/uniqueid.js の makeUniqueId です。

サフィックスのハッシュ値について

ハッシュは、スタックからの相対パス(スタック ID は含まない)から生成されます。
多くの L2 コンストラクトは、内部で作る L1 に Resource という ID を付けるため、Resource までを含んだパス(例:SampleVpc/Resource)が対象になります。

ただし Resource 以外の ID を付ける L2 もあります。

  • ecs.FargateService → FargateService/Service
  • ec2.Vpc → Vpc/PublicSubnet1/Subnet、Vpc/IGW など(1 つの L2 から複数の L1 を作る)

実際のパスは cdk.out/<Stack>.template.json の各リソースの Metadata にある aws:cdk:path で確認できます。

  • 同じコードならパスは同じなので、常に同じハッシュになる
  • コンストラクト ID を変更するとパスが変わり、ハッシュ(と論理 ID)が変わるので、リソースの再作成(置換)が発生する
  • 親のコンストラクトの ID を変えたり、間に階層を挟んだりしても、パスが変わるので同様に置換が発生する
    • 例外:挟んだコンストラクトの子の ID を Default にすると、Default はパスから除かれるため論理 ID は変わらない
  • スタック ID を変更しても論理 ID は変わらない。ただしスタック自体が別物として新規作成されるため、旧スタックのリソースは引き継がれない
    • 旧スタックは自動では削除されず残る
    • CDK / CloudFormation が自動生成する物理名にはスタック名が含まれるため(例:ECS タスク定義のファミリー名 SampleStackSampleJobTaskDefinition287C5E0B)、新スタックでは別名のリソースになる

物理名の自動生成との違い

論理 ID とは別に、L2 が自動で付ける物理名もパスから生成されることがあります。こちらはスタック ID を含むフルパスで、Resource を含まない L2 自身のパスから計算されます。

項目 元になるパス 値
論理 ID SampleJob/TaskDefinition/Resource SampleJobTaskDefinitionC9937766
ファミリー名 SampleStack/SampleJob/TaskDefinition SampleStackSampleJobTaskDefinition287C5E0B

これが CDK でコンストラクト ID を安易に変更してはいけない理由です。

例

Stack や L2 コンストラクトなど多くの Construct のコンストラクタは (scope: Construct, id: string, props?) という形を取り、scope に親を渡すことで実行時にツリー構造が組まれる仕組みです(constructs ライブラリの Composite pattern)。 例外として App は scope を取りません。

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

      // this(SampleStackインスタンス自身)をscopeとして渡す
      // → SampleVpcはSampleStackの子になる
      const vpc = new ec2.Vpc(this, 'SampleVpc', { /* ... */ });
    }
}

const app = new cdk.App();
new SampleStack(app, 'SampleStack');

このとき vpc.node.path は SampleStack/SampleVpc になります。this を渡すことで 今のクラスの中に子コンストラクトを吊るす という親子関係が作られ、さらに深くネストしたい場合は自作の Construct クラスを間に挟んで scope を渡していけば、SampleStack/MyConstruct/SampleVpc のように何段階でも階層を作れます。

論理 ID のハッシュ計算に使われるのは、スタックを除いたパスです。この例の VPC は SampleVpc/Resource から計算されるので、論理 ID は SampleVpc にハッシュが付いた形になります。

論理 ID の変更を避ける方法

コンストラクト ID の変更やリファクタリングが避けられない場合は、論理 ID を固定して、置換を防ぎます。

// 1. L1 の論理 ID を固定する
const cfnVpc = vpc.node.defaultChild as ec2.CfnVPC;
cfnVpc.overrideLogicalId('SampleVpcXXXXXXXX'); // 変更前の論理 ID

// 2. スタックで、生成される論理 ID を別の ID に置き換える
// シグネチャは renameLogicalId(oldId, newId)
// oldId = CDK が新しく生成する論理 ID、newId = 使いたい(変更前の)論理 ID
this.renameLogicalId('NewGeneratedLogicalId', 'OldLogicalId');
  • どちらの方法でも、変更前後の論理 ID を cdk diff で確認してから使う
  • 物理名(bucketName など)を明示したリソースは、置換時に「新リソース作成 → 旧リソース削除」の順で処理されるため、同名のリソースがすでに存在して失敗する。物理名は CloudFormation の自動生成に任せるのが安全

cdk refactor

cdk refactor は CloudFormation のスタックリファクタリング機能を使い、リソースを作り直さずに論理 ID の変更やスタック間の移動を行うコマンドです。比較的新しい機能のため、利用前に公式ドキュメントで制約を確認してください。

cdk diff での見え方

置換は cdk diff で以下のように表示されます。

  • 論理 ID が変わった場合:replace ではなく、旧論理 ID の削除([-])と新論理 ID の追加([+])の組として表示される
  • 論理 ID は同じで、置換が必要なプロパティを変更した場合:replace(requires replacement)と表示される

どちらも実際にはリソースが再作成されます。