[アップデート] CloudFormationのStack間のリソースの移動や論理IDの変更を簡単に行えるようになりました

[アップデート] CloudFormationのStack間のリソースの移動や論理IDの変更を簡単に行えるようになりました

Stack間でリソースを移動させたい場合や論理IDに悩みがある人待望のアップデート
Clock Icon2025.02.07

もっと簡単にStack間のリソースの移動を行いたい

こんにちは、のんピ(@non____97)です。

皆さんはCloudFormationやAWS CDKを使っていて「もっと簡単にStack間のリソースの移動をしたい」や「論理IDを変更したい」と思ったことはありますか? 私はあります。

従来はそのような対応をする場合、以下のようなステップが必要でした。

  • Deletion PolicyでRetainを設定して、Stack上で削除されても、リソースを削除されないようにする
  • Stack上からリソースを削除する
  • 別のStack or 別の論理IDとしてリソースをインポート

どうしても手間が掛かりますし、非常に神経を使います。

今回アップデートによって、 CloudFormationのStack間のリソースの移動や論理IDの変更を簡単に行えるようになりました。

https://dev.classmethod.jp/articles/refactor-cloudformation/

AWS Blogsにも投稿されています。

https://aws.amazon.com/jp/about-aws/whats-new/2025/02/reshape-aws-cloudformation-stack-refactoring/

個人的にはAWS CDKで論理ID名を後から変更したくなったり、別のカスタムConstructにまとめたくなることが1日に10回はあるので、これは非常にありがたいアップデートです。

実際に試して見ました。

いきなりまとめ

  • リファクタリングをする際には以下の2つの情報が必要
    1. リファクタリング後のStackのテンプレートファイル
    2. リファクタリング後に変更する論理IDをマッピングした定義ファイル
  • AWS CDKでもデプロイしたStackに対しても使用できる
  • AWS CDKでデプロイしているリソースをリファクタリングをする際のステップは以下
    1. AWS CDKのコードの修正
    2. npx cdk diffで差分と修正後の論理IDの確認
    3. npx cdk synthでテンプレートファイルを出力
    4. 出力されたテンプレートファイル内で、リファクタリング対象のリソースのメタデータを修正し、現在の値と一致させる
    5. リファクタリングの定義と実行
    6. npx cdk diffでメタデータのみ差分が出力されることを確認
    7. npx cdk deployでメタデータを反映
  • その他の注意点
    • 新規でStackを作成する際にはConditionParameterを指定できない
    • Construct名の変更などConstructの変更を伴うリファクタリングを行う場合はAWS::CDK::MetadataAnalyticsの出力を無効にする必要がある

論理IDの変更 (AWS CDK)

初期状態

早速実際に試してみましょう。

「CloudFormationのアップデートならAWS CDKでもできるでしょ」ということで、AWS CDKから試します。

以下のようにIAM RoleとIAM Policyを作成するだけのStackをデプロイします。

./lib/construct/iam-role-construct.ts
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";

export interface IamRoleConstructProps {
  roleName: string;
}

export class IamRoleConstruct extends Construct {
  constructor(scope: Construct, id: string, props: IamRoleConstructProps) {
    super(scope, id);

    const policy = new cdk.aws_iam.ManagedPolicy(this, "Policy", {
      statements: [
        new cdk.aws_iam.PolicyStatement({
          effect: cdk.aws_iam.Effect.ALLOW,
          resources: ["*"],
          actions: ["ec2:DescribeTags"],
        }),
      ],
    });

    new cdk.aws_iam.Role(this, "Default", {
      roleName: props?.roleName,
      assumedBy: new cdk.aws_iam.CompositePrincipal(
        new cdk.aws_iam.ServicePrincipal("ec2.amazonaws.com")
      ),
      managedPolicies: [policy],
    });
  }
}
./lib/reshape-cfn-stack-stack.ts
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import { IamRoleConstruct } from "./construct/iam-role-construct";

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

    new IamRoleConstruct(this, "IamRoleConstructA", {
      roleName: "test1",
    });
  }
}

npx cdk synthすると以下のようなテンプレートを確認できます。

> npx cdk synth --no-version-reporting --no-path-metadata
Resources:
  IamRoleConstructAPolicy45ACD7CB:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      Description: ""
      Path: /
      PolicyDocument:
        Statement:
          - Action: ec2:DescribeTags
            Effect: Allow
            Resource: "*"
        Version: "2012-10-17"
  IamRoleConstructABEBB5F34:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
        Version: "2012-10-17"
      ManagedPolicyArns:
        - Ref: IamRoleConstructAPolicy45ACD7CB
      RoleName: test1
Parameters:
  BootstrapVersion:
    Type: AWS::SSM::Parameter::Value<String>
    Default: /cdk-bootstrap/hnb659fds/version
    Description: Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]

npx cdk deployでデプロイします。

Stackデプロイ後のリソース一覧は以下のとおりです。

1.Stack作成後のリソース.png

この後、IAM Policyの論理IDを変更します。

リファクタリングの実行 (1回目)

それではリファクタリングを行います。

PolicyPolicy1に変更します。

./lib/construct/iam-role-construct.ts
  import * as cdk from "aws-cdk-lib";
  import { Construct } from "constructs";

  export interface IamRoleConstructProps {
    roleName: string;
  }

  export class IamRoleConstruct extends Construct {
    constructor(scope: Construct, id: string, props: IamRoleConstructProps) {
      super(scope, id);

+     const policy = new cdk.aws_iam.ManagedPolicy(this, "Policy1", {
        statements: [
          new cdk.aws_iam.PolicyStatement({
            effect: cdk.aws_iam.Effect.ALLOW,
            resources: ["*"],
            actions: ["ec2:DescribeTags"],
          }),
        ],
      });

      new cdk.aws_iam.Role(this, "Default", {
        roleName: props?.roleName,
        assumedBy: new cdk.aws_iam.CompositePrincipal(
          new cdk.aws_iam.ServicePrincipal("ec2.amazonaws.com")
        ),
        managedPolicies: [policy],
      });
    }
  }

npx cdk diffで差分を確認します。

> npx cdk diff --no-change-set
Stack ReshapeCfnStackStack
IAM Policy Changes
┌───┬──────────────────────────────┬──────────────────────────────┐
│   │ Resource                     │ Managed Policy ARN           │
├───┼──────────────────────────────┼──────────────────────────────┤
│ - │ ${IamRoleConstructA/Default}${IamRoleConstructA/Policy}  │
├───┼──────────────────────────────┼──────────────────────────────┤
│ + │ ${IamRoleConstructA/Default}${IamRoleConstructA/Policy1} │
└───┴──────────────────────────────┴──────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Resources
[-] AWS::IAM::ManagedPolicy IamRoleConstructA/Policy IamRoleConstructAPolicy45ACD7CB destroy
[+] AWS::IAM::ManagedPolicy IamRoleConstructA/Policy1 IamRoleConstructAPolicy138F2EF20
[~] AWS::IAM::Role IamRoleConstructA/Default IamRoleConstructABEBB5F34
 └─ [~] ManagedPolicyArns
     └─ @@ -1,5 +1,5 @@
        [ ] [
        [ ]   {
        [-]     "Ref": "IamRoleConstructAPolicy45ACD7CB"
        [+]     "Ref": "IamRoleConstructAPolicy138F2EF20"
        [ ]   }
        [ ] ]

✨  Number of stacks with differences: 1

論理IDが変わってしまうので、IAM Policyが再作成されそうです。IAM Roleならまだしも、これがRDS DBインスタンスなどデータを保持するリソースの場合、これは避けたいです。

それでは、リファクタリングをします。

まず、リファクタリング対象を定義します。

必要な情報は以下2つです。

  1. リファクタリング後のStackのテンプレートファイル
  2. リファクタリング後に変更する論理IDをマッピングした定義ファイル

厳密にはどちらもファイルである必要はないのですが、ファイルで渡した方がスッキリして見やすいでしょう。

前者の「リファクタリング後のStackのテンプレートファイル」は先ほどnpx cdk diffの際に合成されたcdk.out/ReshapeCfnStackStack.template.jsonを使用します。

後者の「リファクタリング後に変更する論理IDをマッピングした定義ファイル」は以下フォーマットで定義します。

[
  {
    "Source": {
      "StackName": "string",
      "LogicalResourceId": "string"
    },
    "Destination": {
      "StackName": "string",
      "LogicalResourceId": "string"
    }
  }
  ...
]

今回は以下のように定義しました。

./refactor1.json
[
  {
    "Source": {
      "StackName": "ReshapeCfnStackStack",
      "LogicalResourceId": "IamRoleConstructAPolicy45ACD7CB"
    },
    "Destination": {
      "StackName": "ReshapeCfnStackStack",
      "LogicalResourceId": "IamRoleConstructAPolicy138F2EF20"
    }
  }
]

それでは実際にやって見ましょう。

create-stack-refactorを叩きます。

> aws --version
aws-cli/2.23.15 Python/3.12.9 Darwin/24.1.0 source/arm64

> aws cloudformation create-stack-refactor \
  --stack-definitions \
    StackName=ReshapeCfnStackStack,TemplateBody@=file://cdk.out/ReshapeCfnStackStack.template.json \
  --no-enable-stack-creation \
  --resource-mappings file://refactor1.json
{
    "StackRefactorId": "37ae4f3c-9881-4a75-8271-3d9c91aedbb9"
}

> aws cloudformation describe-stack-refactor --stack-refactor-id 37ae4f3c-9881-4a75-8271-3d9c91aedbb9
{
    "StackRefactorId": "37ae4f3c-9881-4a75-8271-3d9c91aedbb9",
    "StackIds": [
        "arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/ReshapeCfnStackStack/92282680-e4f5-11ef-8dae-12951cea36cf"
    ],
    "ExecutionStatus": "UNAVAILABLE",
    "Status": "CREATE_FAILED",
    "StatusReason": "Resource IamRoleConstructAPolicy45ACD7CB in stack arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/ReshapeCfnStackStack/92282680-e4f5-11ef-8dae-12951cea36cf does not match the destination resource's properties."
}

はい、Resource IamRoleConstructAPolicy45ACD7CB in stack arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/ReshapeCfnStackStack/92282680-e4f5-11ef-8dae-12951cea36cf does not match the destination resource's properties.とエラーになってしまいました。

エラー文を見るにリファクタリング対象のリソースが、リファクタリング先のテンプレートとパラメーターが一致しないためにエラーとなっているようです。

もしかすると、IAM Policyに物理名を指定していないためでしょうか。

IAM Policyは物理名を指定しない場合、論理IDから物理名を生成します。

今回、論理IDが変わってしまうに伴い、物理名も変化しようとしてしまい、エラーになるのかもしれません。

リファクタリングの実行 (IAM Policyの物理名を指定してトライ)

IAM Policyの物理名を指定してトライします。

以下のように物理名を指定しました。

./lib/construct/iam-role-construct.ts
  import * as cdk from "aws-cdk-lib";
  import { Construct } from "constructs";

  export interface IamRoleConstructProps {
    roleName: string;
  }

  export class IamRoleConstruct extends Construct {
    constructor(scope: Construct, id: string, props: IamRoleConstructProps) {
      super(scope, id);

      const policy = new cdk.aws_iam.ManagedPolicy(this, "Policy", {
+       managedPolicyName: `${props.roleName}-policy`,
        statements: [
          new cdk.aws_iam.PolicyStatement({
            effect: cdk.aws_iam.Effect.ALLOW,
            resources: ["*"],
            actions: ["ec2:DescribeTags"],
          }),
        ],
      });

      new cdk.aws_iam.Role(this, "Default", {
        roleName: props?.roleName,
        assumedBy: new cdk.aws_iam.CompositePrincipal(
          new cdk.aws_iam.ServicePrincipal("ec2.amazonaws.com")
        ),
        managedPolicies: [policy],
      });
    }
  }

一旦デプロイします。

> npx cdk diff --no-change-set
Stack ReshapeCfnStackStack
Resources
[~] AWS::IAM::ManagedPolicy IamRoleConstructA/Policy IamRoleConstructAPolicy45ACD7CB replace
 └─ [+] ManagedPolicyName (requires replacement)
     └─ test1-policy

✨  Number of stacks with differences: 1

> npx cdk deploy

✨  Synthesis time: 8.59s

ReshapeCfnStackStack: start: Building 6f87046052f4fedec3ad90690831bd05199b5371d1246044a64081ea13f0f19f:current_account-current_region
.
.
(中略)
.
.
ReshapeCfnStackStack | 4/5 | 11:31:50 | UPDATE_COMPLETE      | AWS::CloudFormation::Stack | ReshapeCfnStackStack

 ✅  ReshapeCfnStackStack

✨  Deployment time: 56.2s

Stack ARN:
arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/ReshapeCfnStackStack/92282680-e4f5-11ef-8dae-12951cea36cf

✨  Total time: 64.79s

デプロイ後のリソース一覧は以下のとおりです。

2.IAMポリシーの名前を指定して再デプロイ.png

それでは、この状態で論理IDを変更します。

./lib/construct/iam-role-construct.ts
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";

export interface IamRoleConstructProps {
  roleName: string;
}

export class IamRoleConstruct extends Construct {
  constructor(scope: Construct, id: string, props: IamRoleConstructProps) {
    super(scope, id);

+    const policy = new cdk.aws_iam.ManagedPolicy(this, "Policy1", {
      managedPolicyName: `${props.roleName}-policy`,
      statements: [
        new cdk.aws_iam.PolicyStatement({
          effect: cdk.aws_iam.Effect.ALLOW,
          resources: ["*"],
          actions: ["ec2:DescribeTags"],
        }),
      ],
    });

    new cdk.aws_iam.Role(this, "Default", {
      roleName: props?.roleName,
      assumedBy: new cdk.aws_iam.CompositePrincipal(
        new cdk.aws_iam.ServicePrincipal("ec2.amazonaws.com")
      ),
      managedPolicies: [policy],
    });
  }
}

npx cdk diffをして、論理IDが何に変わるか確認しておきます。

> npx cdk diff --no-change-set
Stack ReshapeCfnStackStack
IAM Policy Changes
┌───┬──────────────────────────────┬──────────────────────────────┐
│   │ Resource                     │ Managed Policy ARN           │
├───┼──────────────────────────────┼──────────────────────────────┤
│ - │ ${IamRoleConstructA/Default}${IamRoleConstructA/Policy}  │
├───┼──────────────────────────────┼──────────────────────────────┤
│ + │ ${IamRoleConstructA/Default}${IamRoleConstructA/Policy1} │
└───┴──────────────────────────────┴──────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Resources
[-] AWS::IAM::ManagedPolicy IamRoleConstructA/Policy IamRoleConstructAPolicy45ACD7CB destroy
[+] AWS::IAM::ManagedPolicy IamRoleConstructA/Policy1 IamRoleConstructAPolicy138F2EF20
[~] AWS::IAM::Role IamRoleConstructA/Default IamRoleConstructABEBB5F34
 └─ [~] ManagedPolicyArns
     └─ @@ -1,5 +1,5 @@
        [ ] [
        [ ]   {
        [-]     "Ref": "IamRoleConstructAPolicy45ACD7CB"
        [+]     "Ref": "IamRoleConstructAPolicy138F2EF20"
        [ ]   }
        [ ] ]

✨  Number of stacks with differences: 1

確認した論理IDをrefactor1.jsonで設定してリファクタリングの定義を行います。

> aws cloudformation create-stack-refactor \
  --stack-definitions \
    StackName=ReshapeCfnStackStack,TemplateBody@=file://cdk.out/ReshapeCfnStackStack.template.json \
  --no-enable-stack-creation \
  --resource-mappings file://refactor1.json
{
    "StackRefactorId": "f288e5be-e264-4041-9b8d-c7edd2916a24"
}

> aws cloudformation describe-stack-refactor --stack-refactor-id f288e5be-e264-4041-9b8d-c7edd2916a24
{
    "StackRefactorId": "f288e5be-e264-4041-9b8d-c7edd2916a24",
    "StackIds": [
        "arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/ReshapeCfnStackStack/92282680-e4f5-11ef-8dae-12951cea36cf"
    ],
    "ExecutionStatus": "UNAVAILABLE",
    "Status": "CREATE_FAILED",
    "StatusReason": "Resource IamRoleConstructAPolicy45ACD7CB in stack arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/ReshapeCfnStackStack/92282680-e4f5-11ef-8dae-12951cea36cf does not match the destination resource's properties."
}

はい、同じエラーで失敗しました。

本当に差分はあるのでしょうか。

試しに出力された./cdk.out/ReshapeCfnStackStack.template.jsonとマネジメントコンソール上で表示されている現在のStackのテンプレートをdiffしてみます。

> diff -u ./before.json ./cdk.out/ReshapeCfnStackStack.template.json
--- ./before.json 2025-02-07 11:49:35
+++ ./cdk.out/ReshapeCfnStackStack.template.json 2025-02-07 11:36:11
@@ -1,6 +1,6 @@
 {
  "Resources": {
-  "IamRoleConstructAPolicy45ACD7CB": {
+  "IamRoleConstructAPolicy138F2EF20": {
    "Type": "AWS::IAM::ManagedPolicy",
    "Properties": {
     "Description": "",
\ No newline at end of file
@@ -16,9 +16,6 @@
      ],
      "Version": "2012-10-17"
     }
-   },
-   "Metadata": {
-    "aws:cdk:path": "ReshapeCfnStackStack/IamRoleConstructA/Policy/Resource"
    }
   },
   "IamRoleConstructABEBB5F34": {
\ No newline at end of file
@@ -38,292 +35,13 @@
     },
     "ManagedPolicyArns": [
      {
-      "Ref": "IamRoleConstructAPolicy45ACD7CB"
+      "Ref": "IamRoleConstructAPolicy138F2EF20"
      }
     ],
     "RoleName": "test1"
-   },
-   "Metadata": {
-    "aws:cdk:path": "ReshapeCfnStackStack/IamRoleConstructA/Default/Resource"
    }
-  },
-  "CDKMetadata": {
-   "Type": "AWS::CDK::Metadata",
-   "Properties": {
-    "Analytics": "v2:deflate64:H4sIAAAAAAAA/1WOT4vCQ..(中略)..+EepVIuAQAA"
-   },
-   "Metadata": {
-    "aws:cdk:path": "ReshapeCfnStackStack/CDKMetadata/Default"
-   },
-   "Condition": "CDKMetadataAvailable"
   }
  },
- "Conditions": {
-  "CDKMetadataAvailable": {
.
.
(中略)
.
.
-  }
- },
  "Parameters": {
   "BootstrapVersion": {
    "Type": "AWS::SSM::Parameter::Value<String>",
\ No newline at end of file

リソースの"Properties"に差分はないようです。

リファクタリングの実行 (リファクタリング対象のリソースのメタデータを修正してトライ)

もしかすると、aws:cdk:pathのメタデータが異なることが原因でエラーになっているのでしょうか。

実際に試してみます。

diffしやすくするためにJSON形式のテンプレートファイルを合成します。

> npx cdk synth --json
{
  "Resources": {
    "IamRoleConstructAPolicy138F2EF20": {
      "Type": "AWS::IAM::ManagedPolicy",
      "Properties": {
        "Description": "",
        "ManagedPolicyName": "test1-policy",
        "Path": "/",
        "PolicyDocument": {
          "Statement": [
            {
              "Action": "ec2:DescribeTags",
              "Effect": "Allow",
              "Resource": "*"
            }
          ],
          "Version": "2012-10-17"
        }
      },
      "Metadata": {
        "aws:cdk:path": "ReshapeCfnStackStack/IamRoleConstructA/Policy1/Resource"
      }
    },
    "IamRoleConstructABEBB5F34": {
      "Type": "AWS::IAM::Role",
      "Properties": {
        "AssumeRolePolicyDocument": {
          "Statement": [
            {
              "Action": "sts:AssumeRole",
              "Effect": "Allow",
              "Principal": {
                "Service": "ec2.amazonaws.com"
              }
            }
          ],
          "Version": "2012-10-17"
        },
        "ManagedPolicyArns": [
          {
            "Ref": "IamRoleConstructAPolicy138F2EF20"
          }
        ],
        "RoleName": "test1"
      },
      "Metadata": {
        "aws:cdk:path": "ReshapeCfnStackStack/IamRoleConstructA/Default/Resource"
      }
    },
    "CDKMetadata": {
      "Type": "AWS::CDK::Metadata",
      "Properties": {
        "Analytics": "v2:deflate64:H4sIAAAAAAAA/1WOT4vCQ..(中略)..EepVIuAQAA"
      },
      "Metadata": {
        "aws:cdk:path": "ReshapeCfnStackStack/CDKMetadata/Default"
      },
      "Condition": "CDKMetadataAvailable"
    }
  },
  "Conditions": {
    "CDKMetadataAvailable": {
.
.
(中略)
.
.
    }
  },
  "Parameters": {
    "BootstrapVersion": {
      "Type": "AWS::SSM::Parameter::Value<String>",
      "Default": "/cdk-bootstrap/hnb659fds/version",
      "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]"
    }
  }
}

diffで現在のStackのテンプレートとの差分を確認します。

diff -u cdk-iam-role-after.json before.json
--- cdk-iam-role-after.json 2025-02-07 14:54:21
+++ before.json 2025-02-07 14:56:23
@@ -1,6 +1,6 @@
 {
   "Resources": {
-    "IamRoleConstructAPolicy138F2EF20": {
+    "IamRoleConstructAPolicy45ACD7CB": {
       "Type": "AWS::IAM::ManagedPolicy",
       "Properties": {
         "Description": "",
\ No newline at end of file
@@ -18,7 +18,7 @@
         }
       },
       "Metadata": {
-        "aws:cdk:path": "ReshapeCfnStackStack/IamRoleConstructA/Policy1/Resource"
+        "aws:cdk:path": "ReshapeCfnStackStack/IamRoleConstructA/Policy/Resource"
       }
     },
     "IamRoleConstructABEBB5F34": {
\ No newline at end of file
@@ -38,7 +38,7 @@
         },
         "ManagedPolicyArns": [
           {
-            "Ref": "IamRoleConstructAPolicy138F2EF20"
+            "Ref": "IamRoleConstructAPolicy45ACD7CB"
           }
         ],
         "RoleName": "test1"
\ No newline at end of file
@@ -330,5 +330,32 @@
       "Default": "/cdk-bootstrap/hnb659fds/version",
       "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]"
     }
+  },
+  "Rules": {
+    "CheckBootstrapVersion": {
.
.
(中略)
.
.
+    }
   }
 }
\ No newline at end of file

他に怪しいところとすると、aws:cdk:pathのメタデータぐらいです。

合成されたテンプレートのaws:cdk:pathを修正して、現在のStackのテンプレート上と同じ値にします。

> diff -u cdk-iam-role-after.json before.json
--- cdk-iam-role-after.json 2025-02-07 14:56:57
+++ before.json 2025-02-07 14:56:23
@@ -1,6 +1,6 @@
 {
   "Resources": {
-    "IamRoleConstructAPolicy138F2EF20": {
+    "IamRoleConstructAPolicy45ACD7CB": {
       "Type": "AWS::IAM::ManagedPolicy",
       "Properties": {
         "Description": "",
\ No newline at end of file
@@ -38,7 +38,7 @@
         },
         "ManagedPolicyArns": [
           {
-            "Ref": "IamRoleConstructAPolicy138F2EF20"
+            "Ref": "IamRoleConstructAPolicy45ACD7CB"
           }
         ],
         "RoleName": "test1"
\ No newline at end of file
@@ -330,5 +330,32 @@
       "Default": "/cdk-bootstrap/hnb659fds/version",
       "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]"
     }
+  },
+  "Rules": {
+    "CheckBootstrapVersion": {
.
.
(中略)
.
.
+    }
   }
 }
\ No newline at end of file

この状態でリファクタリングの定義を行います。

> aws cloudformation create-stack-refactor \
  --stack-definitions \
    StackName=ReshapeCfnStackStack,TemplateBody@=file://cdk-iam-role-after.json \
  --no-enable-stack-creation \
  --resource-mappings file://refactor1.json
{
    "StackRefactorId": "1faed814-a53b-4fe3-bd12-627613ebb1cb"
}

> aws cloudformation describe-stack-refactor --stack-refactor-id 1faed814-a53b-4fe3-bd12-627613ebb1cb
{
    "StackRefactorId": "1faed814-a53b-4fe3-bd12-627613ebb1cb",
    "StackIds": [
        "arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/ReshapeCfnStackStack/92282680-e4f5-11ef-8dae-12951cea36cf"
    ],
    "ExecutionStatus": "AVAILABLE",
    "Status": "CREATE_COMPLETE"
}

無事完了しました。

list-stack-refactor-actionsで、これによりリファクタリングされる内容を確認します。

> aws cloudformation list-stack-refactor-actions --stack-refactor-id 1faed814-a53b-4fe3-bd12-627613ebb1cb
{
    "StackRefactorActions": [
        {
            "Action": "MOVE",
            "Entity": "RESOURCE",
            "PhysicalResourceId": "arn:aws:iam::<AWSアカウントID>:policy/test1-policy",
            "Description": "No configuration changes detected.",
            "Detection": "MANUAL",
            "TagResources": [],
            "UntagResources": [],
            "ResourceMapping": {
                "Source": {
                    "StackName": "arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/ReshapeCfnStackStack/92282680-e4f5-11ef-8dae-12951cea36cf",
                    "LogicalResourceId": "IamRoleConstructAPolicy45ACD7CB"
                },
                "Destination": {
                    "StackName": "arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/ReshapeCfnStackStack/92282680-e4f5-11ef-8dae-12951cea36cf",
                    "LogicalResourceId": "IamRoleConstructAPolicy138F2EF20"
                }
            }
        }
    ]
}

定義したとおりの変更が行われそうです。

それではリファクタリングの実行をします。

> aws cloudformation execute-stack-refactor --stack-refactor-id 1faed814-a53b-4fe3-bd12-627613ebb1cb

> aws cloudformation describe-stack-refactor --stack-refactor-id 1faed814-a53b-4fe3-bd12-627613ebb1cb
{
    "StackRefactorId": "1faed814-a53b-4fe3-bd12-627613ebb1cb",
    "StackIds": [
        "arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/ReshapeCfnStackStack/92282680-e4f5-11ef-8dae-12951cea36cf"
    ],
    "ExecutionStatus": "EXECUTE_COMPLETE",
    "Status": "CREATE_COMPLETE"
}

一瞬で完了しました。

Stackのイベントを確認します。

6.CDKのスタックのイベント.png

IMPORT_IN_PROGRESSIMPORT_COMPLETEEXPORT_COMPLETEとイベントがあることから内部的にImport/Exportが行われているようです。

リソース一覧を確認しましょう。

7.メタデータが本来と異なるのでツリービューじゃ表示できない.png

はい、ツリービューを展開してもリファクタリング対象のIAM Policyを確認できません。これはメタデータが本来と異なるためです。

実際はリソースは削除されておらず、フラットビューから確認することが可能です。

8.フラットビューでは表示できる.png

実際にIAM Roleにアクセスすると、リファクタリング対象のIAM Policyがアタッチされていることを確認できました。

9.リソースは削除されずに残っている.png

リファクタリング後のStackの更新

ツリービューが正しく表示されなくなるのは気になります。また、リファクタリング後に正常にStackを更新できるかも気になります。

試しにnpx cdk diffしてみましょう。

> npx cdk diff --no-change-set
Stack ReshapeCfnStackStack
Resources
[~] AWS::IAM::ManagedPolicy IamRoleConstructA/Policy1 IamRoleConstructAPolicy138F2EF20
 └─ [~] Metadata
     └─ [~] .aws:cdk:path:
         ├─ [-] ReshapeCfnStackStack/IamRoleConstructA/Policy/Resource
         └─ [+] ReshapeCfnStackStack/IamRoleConstructA/Policy1/Resource

✨  Number of stacks with differences: 1

メタデータのみ更新してくれそうですね。リファクタリングを実行したタイミングでStackのテンプレートも更新されていそうです。

Stackを更新します。

> npx cdk deploy

✨  Synthesis time: 8.67s

ReshapeCfnStackStack: start: Building 2319551a001853907f524f0333755cf12c1b8ec17856895f066450576da4eb8c:current_account-current_region
.
.
(中略)
.
.
ReshapeCfnStackStack | 3/3 | 15:07:11 | UPDATE_COMPLETE      | AWS::CloudFormation::Stack | ReshapeCfnStackStack

 ✅  ReshapeCfnStackStack

✨  Deployment time: 22.21s

Stack ARN:
arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/ReshapeCfnStackStack/92282680-e4f5-11ef-8dae-12951cea36cf

✨  Total time: 30.88s

デプロイ後、ツリービューを確認すると問題なく表示されました。

10.メタデータ更新後ツリービューでも表示できた.png

ということで、AWS CDKでデプロイしているリソースをリファクタリングをする際には以下のステップが必要になります。

  1. AWS CDKのコードの修正
  2. npx cdk diffで差分と修正後の論理IDの確認
  3. npx cdk synthでテンプレートファイルを出力
  4. 出力されたテンプレートファイル内で、リファクタリング対象のリソースのメタデータを修正し、現在の値と一致させる
  5. リファクタリングの定義と実行
  6. npx cdk diffでメタデータのみ差分が出力されることを確認
  7. npx cdk deployでメタデータを反映

論理IDの変更 (CloudFormation)

初期状態

AWS CDKを介さず、純粋なCloudFormationでも試してみます。

テンプレートファイルは以下のとおりです。

Resources:
  IamRoleConstructAPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      Description: ""
      Path: /
      PolicyDocument:
        Statement:
          - Action: ec2:DescribeTags
            Effect: Allow
            Resource: "*"
        Version: "2012-10-17"
  IamRoleConstructABEBB5F34:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
        Version: "2012-10-17"
      ManagedPolicyArns:
        - Ref: IamRoleConstructAPolicy
      RoleName: cfn-test1

今回はIAM Policyの物理名は指定していません。

Stackをデプロイします。

> aws cloudformation create-stack \
  --stack-name IamRoleStacks \
  --template-body file://iam-role-before.yml \
  --capabilities CAPABILITY_NAMED_IAM
{
    "StackId": "arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/IamRoleStacks/9dde0210-e50e-11ef-b1ce-123c895e04bb"
}

デプロイ後のリソース一覧は以下のとおりです。

3.CFnでStackを新規作成.png

リファクタリングの実行

リファクタリングを行います。

差分は以下のとおりで、IamRoleConstructAPolicyIamRoleConstructAPolicy1に変更するのみです。

> diff -u iam-role-before.yml iam-role-after.yml
--- iam-role-before.yml 2025-02-07 13:47:07
+++ iam-role-after.yml 2025-02-07 13:49:29
@@ -1,5 +1,5 @@
 Resources:
-  IamRoleConstructAPolicy:
+  IamRoleConstructAPolicy1:
     Type: AWS::IAM::ManagedPolicy
     Properties:
       Description: ""
@@ -21,5 +21,5 @@
               Service: ec2.amazonaws.com
         Version: "2012-10-17"
       ManagedPolicyArns:
-        - Ref: IamRoleConstructAPolicy
+        - Ref: IamRoleConstructAPolicy1
       RoleName: cfn-test1

定義ファイルも以下のとおり、非常にシンプルです。

./refactor2.json
[
  {
    "Source": {
      "StackName": "IamRoleStacks",
      "LogicalResourceId": "IamRoleConstructAPolicy"
    },
    "Destination": {
      "StackName": "IamRoleStacks",
      "LogicalResourceId": "IamRoleConstructAPolicy1"
    }
  }
]

リファクタリングの定義を作成します。

> aws cloudformation create-stack-refactor \
  --stack-definitions \
    StackName=IamRoleStacks,TemplateBody@=file://iam-role-after.yml \
  --no-enable-stack-creation \
  --resource-mappings file://refactor2.json
{
    "StackRefactorId": "04af50ac-0e46-46cb-8407-c02bbb098c37"
}

> aws cloudformation describe-stack-refactor --stack-refactor-id 04af50ac-0e46-46cb-8407-c02bbb098c37
{
    "StackRefactorId": "04af50ac-0e46-46cb-8407-c02bbb098c37",
    "StackIds": [
        "arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/IamRoleStacks/9dde0210-e50e-11ef-b1ce-123c895e04bb"
    ],
    "ExecutionStatus": "AVAILABLE",
    "Status": "CREATE_COMPLETE"
}

> aws cloudformation list-stack-refactor-actions --stack-refactor-id 04af50ac-0e46-46cb-8407-c02bbb098c37
{
    "StackRefactorActions": [
        {
            "Action": "MOVE",
            "Entity": "RESOURCE",
            "PhysicalResourceId": "arn:aws:iam::<AWSアカウントID>:policy/IamRoleStacks-IamRoleConstructAPolicy-e5y5tHCgdooi",
            "Description": "No configuration changes detected.",
            "Detection": "MANUAL",
            "TagResources": [],
            "UntagResources": [],
            "ResourceMapping": {
                "Source": {
                    "StackName": "arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/IamRoleStacks/9dde0210-e50e-11ef-b1ce-123c895e04bb",
                    "LogicalResourceId": "IamRoleConstructAPolicy"
                },
                "Destination": {
                    "StackName": "arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/IamRoleStacks/9dde0210-e50e-11ef-b1ce-123c895e04bb",
                    "LogicalResourceId": "IamRoleConstructAPolicy1"
                }
            }
        }
    ]
}

正常にできました。

リファクタリングの実行をします。

> aws cloudformation execute-stack-refactor --stack-refactor-id 04af50ac-0e46-46cb-8407-c02bbb098c37

> aws cloudformation describe-stack-refactor --stack-refactor-id 04af50ac-0e46-46cb-8407-c02bbb098c37
{
    "StackRefactorId": "04af50ac-0e46-46cb-8407-c02bbb098c37",
    "StackIds": [
        "arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/IamRoleStacks/9dde0210-e50e-11ef-b1ce-123c895e04bb"
    ],
    "ExecutionStatus": "EXECUTE_COMPLETE",
    "Status": "CREATE_COMPLETE"
}

こちらも正常に完了しました。

Stackのイベントは以下のとおりです。

4.スタックのイベント.png

論理IDが正常に変わっていることを確認できました。

5.論理IDが変わった.png

また、テンプレートもiam-role-after.ymlで指定したものに変更されていました。

5.テンプレートもafterのものに変更されている.png

Stack間のリソースの移動

下準備

Stack間のリソースの移動も試してみます。

IamRoleConstructAを別Stackに移動させます。

このまま移動させてしまうとStack内のリソースが空になってしまい、エラーとなります。

> aws cloudformation describe-stack-refactor \
  --stack-refactor-id 34f28269-d951-4341-a630-157998b44342 \
  --query 'StatusReason'
"arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/ReshapeCfnStackStack/92282680-e4f5-11ef-8dae-12951cea36cf: Empty [/Resources] values are not allowed in templates. CloudFormation does not support empty stack creation."

そのため、IamRoleConstructBというConstructを一つ追加してデプロイしておきます。

./lib/reshape-cfn-stack-stack.ts
  import * as cdk from "aws-cdk-lib";
  import { Construct } from "constructs";
  import { IamRoleConstruct } from "./construct/iam-role-construct";

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

      new IamRoleConstruct(this, "IamRoleConstructA", {
        roleName: "test1",
      });
+     new IamRoleConstruct(this, "IamRoleConstructB", {
+       roleName: "test2",
+     });
    }
  }

変更箇所の確認

IamRoleConstructBのデプロイが完了したら、Stack間のリソースの移動を行うための差分を確認します。

以下のようにReshapeCfnStackStackIamRoleConstructAReshapeCfnStack2StackIamRoleConstructA2として移動させます。

./bin/reshape-cfn-stack.ts
  #!/usr/bin/env node
  import * as cdk from "aws-cdk-lib";
  import { ReshapeCfnStackStack } from "../lib/reshape-cfn-stack-stack";
+ import { ReshapeCfnStack2Stack } from "../lib/reshape-cfn-stack2-stack";

  const app = new cdk.App();
  new ReshapeCfnStackStack(app, "ReshapeCfnStackStack", {});
+ new ReshapeCfnStack2Stack(app, "ReshapeCfnStack2Stack", {});
./lib/reshape-cfn-stack2-stack.ts
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import { IamRoleConstruct } from "./construct/iam-role-construct";

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

    new IamRoleConstruct(this, "IamRoleConstructA2", {
      roleName: "test1",
    });
  }
}
./lib/reshape-cfn-stack-stack.ts
  import * as cdk from "aws-cdk-lib";
  import { Construct } from "constructs";
  import { IamRoleConstruct } from "./construct/iam-role-construct";

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

+    // new IamRoleConstruct(this, "IamRoleConstructA", {
+    //   roleName: "test1",
+    // });
     new IamRoleConstruct(this, "IamRoleConstructC", {
       roleName: "test2",
     });
    }
  }

npx cdk diffで差分と論理IDを確認します。

> npx cdk diff --no-change-set
Stack ReshapeCfnStackStack
IAM Statement Changes
┌───┬──────────────────────────────────┬────────┬────────────────┬───────────────────────────┬───────────┐
│   │ Resource                         │ Effect │ Action         │ Principal                 │ Condition │
├───┼──────────────────────────────────┼────────┼────────────────┼───────────────────────────┼───────────┤
│ - │ ${IamRoleConstructA/Default.Arn} │ Allow  │ sts:AssumeRole │ Service:ec2.amazonaws.com │           │
└───┴──────────────────────────────────┴────────┴────────────────┴───────────────────────────┴───────────┘
IAM Policy Changes
┌───┬──────────────────────────────┬──────────────────────────────┐
│   │ Resource                     │ Managed Policy ARN           │
├───┼──────────────────────────────┼──────────────────────────────┤
│ - │ ${IamRoleConstructA/Default}${IamRoleConstructA/Policy1} │
└───┴──────────────────────────────┴──────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Resources
[-] AWS::IAM::ManagedPolicy IamRoleConstructA/Policy1 IamRoleConstructAPolicy138F2EF20 destroy
[-] AWS::IAM::Role IamRoleConstructA/Default IamRoleConstructABEBB5F34 destroy

Stack ReshapeCfnStack2Stack
IAM Statement Changes
┌───┬───────────────────────────────────┬────────┬────────────────┬───────────────────────────┬───────────┐
│   │ Resource                          │ Effect │ Action         │ Principal                 │ Condition │
├───┼───────────────────────────────────┼────────┼────────────────┼───────────────────────────┼───────────┤
│ + │ ${IamRoleConstructA2/Default.Arn} │ Allow  │ sts:AssumeRole │ Service:ec2.amazonaws.com │           │
└───┴───────────────────────────────────┴────────┴────────────────┴───────────────────────────┴───────────┘
IAM Policy Changes
┌───┬───────────────────────────────┬───────────────────────────────┐
│   │ Resource                      │ Managed Policy ARN            │
├───┼───────────────────────────────┼───────────────────────────────┤
│ + │ ${IamRoleConstructA2/Default}${IamRoleConstructA2/Policy1} │
└───┴───────────────────────────────┴───────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Parameters
[+] Parameter BootstrapVersion BootstrapVersion: {"Type":"AWS::SSM::Parameter::Value<String>","Default":"/cdk-bootstrap/hnb659fds/version","Description":"Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]"}

Conditions
[+] Condition CDKMetadata/Condition CDKMetadataAvailable: {.
.
(中略)
.
.}

Resources
[+] AWS::IAM::ManagedPolicy IamRoleConstructA2/Policy1 IamRoleConstructA2Policy1CFD08057
[+] AWS::IAM::Role IamRoleConstructA2/Default IamRoleConstructA296AD5240
✨  Number of stacks with differences: 2

リファクタリングの実行 (1回目)

それではリファクタリングをします。

npx cdk synthでStackのテンプレートを合成して、ファイルとして保存しておきます。

> npx cdk synth ReshapeCfnStackStack
Resources:
  IamRoleConstructBPolicy1A4484B55:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      Description: ""
      ManagedPolicyName: test2-policy
      Path: /
      PolicyDocument:
        Statement:
          - Action: ec2:DescribeTags
            Effect: Allow
            Resource: "*"
        Version: "2012-10-17"
    Metadata:
      aws:cdk:path: ReshapeCfnStackStack/IamRoleConstructB/Policy1/Resource
  IamRoleConstructBC974AF92:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
        Version: "2012-10-17"
      ManagedPolicyArns:
        - Ref: IamRoleConstructBPolicy1A4484B55
      RoleName: test2
    Metadata:
      aws:cdk:path: ReshapeCfnStackStack/IamRoleConstructB/Default/Resource
  CDKMetadata:
    Type: AWS::CDK::Metadata
    Properties:
      Analytics: v2:deflate64:H4sIAAAAAAAA/1WOT4vCQA..(中略)..+A+EepVIuAQAA
    Metadata:
      aws:cdk:path: ReshapeCfnStackStack/CDKMetadata/Default
    Condition: CDKMetadataAvailable
Conditions:
  CDKMetadataAvailable:
.
.
(中略)
.
.
Parameters:
  BootstrapVersion:
    Type: AWS::SSM::Parameter::Value<String>
    Default: /cdk-bootstrap/hnb659fds/version
    Description: Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]

> npx cdk synth ReshapeCfnStack2Stack
Resources:
  IamRoleConstructA2Policy1CFD08057:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      Description: ""
      ManagedPolicyName: test1-policy
      Path: /
      PolicyDocument:
        Statement:
          - Action: ec2:DescribeTags
            Effect: Allow
            Resource: "*"
        Version: "2012-10-17"
    Metadata:
      aws:cdk:path: ReshapeCfnStack2Stack/IamRoleConstructA2/Policy1/Resource
  IamRoleConstructA296AD5240:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
        Version: "2012-10-17"
      ManagedPolicyArns:
        - Ref: IamRoleConstructA2Policy1CFD08057
      RoleName: test1
    Metadata:
      aws:cdk:path: ReshapeCfnStack2Stack/IamRoleConstructA2/Default/Resource
  CDKMetadata:
    Type: AWS::CDK::Metadata
    Properties:
      Analytics: v2:deflate64:H4sIAAAAAAAA/1WOT4vCQA..(中略)..+A+EepVIuAQAA
    Metadata:
      aws:cdk:path: ReshapeCfnStack2Stack/CDKMetadata/Default
    Condition: CDKMetadataAvailable
Conditions:
  CDKMetadataAvailable:
.
.
(中略)
.
.
Parameters:
  BootstrapVersion:
    Type: AWS::SSM::Parameter::Value<String>
    Default: /cdk-bootstrap/hnb659fds/version
    Description: Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]

論理IDをマッピングした定義ファイルは以下のようにしました。

./refactor3.json
[
  {
    "Source": {
      "StackName": "ReshapeCfnStackStack",
      "LogicalResourceId": "IamRoleConstructAPolicy138F2EF20"
    },
    "Destination": {
      "StackName": "ReshapeCfnStack2Stack",
      "LogicalResourceId": "IamRoleConstructBPolicy1A4484B55"
    }
  },
  {
    "Source": {
      "StackName": "ReshapeCfnStackStack",
      "LogicalResourceId": "IamRoleConstructABEBB5F34"
    },
    "Destination": {
      "StackName": "ReshapeCfnStack2Stack",
      "LogicalResourceId": "IamRoleConstructBC974AF92"
    }
  }
]

それでは、リファクタリングの定義を作成します。


> aws cloudformation create-stack-refactor \
  --stack-definitions \
    StackName=ReshapeCfnStackStack,TemplateBody@=file://after-ReshapeCfnStackStack.yml \
    StackName=ReshapeCfnStack2Stack,TemplateBody@=file://after-ReshapeCfnStack2Stack.yml \
  --enable-stack-creation \
  --resource-mappings file://refactor3.json
{
    "StackRefactorId": "c4778c94-8f8d-4e49-946f-9050d09c46be"
}

> aws cloudformation describe-stack-refactor --stack-refactor-id c4778c94-8f8d-4e49-946f-9050d09c46be
{
    "StackRefactorId": "c4778c94-8f8d-4e49-946f-9050d09c46be",
    "StackIds": [
        "arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/ReshapeCfnStackStack/92282680-e4f5-11ef-8dae-12951cea36cf",
        "ReshapeCfnStack2Stack"
    ],
    "ExecutionStatus": "UNAVAILABLE",
    "Status": "CREATE_FAILED",
    "StatusReason": "arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/ReshapeCfnStack2Stack/24778a97-9ddf-4de7-a192-82cf73941da9: Following template sections are not allowed when creating a stack while refactoring: Conditions"
}

新規Stackを作成するにあたって、Conditionは使用できないようです。

Condition周りを削って再度リファクタリングの定義を作成したが、以下のようにエラーになります。

(aws cloudformation create-stack-refactorは省略)

> aws cloudformation describe-stack-refactor \
  --stack-refactor-id 14b01a0b-651d-4bd4-82d0-be4971a63d93 \
  --query 'StatusReason'
"arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/ReshapeCfnStack2Stack/636c942d-c79b-41a9-a30c-142ad9b7b298: Following template sections are not allowed when creating a stack while refactoring: Parameters"

同様にStack作成時にParametersは指定できないとのことです。

さらにParameter周りも削りましたが、まだエラーになります。

(aws cloudformation create-stack-refactorは省略)

> aws cloudformation describe-stack-refactor \
  --stack-refactor-id 9125c90e-672a-4319-b714-2dd70582dfe5 \
  --query 'StatusReason'
"Found an action type that is not permitted during refactor operations: Modify"

はい、リファクタリング中にはModifyは許可されていないとエラーになりました。

npx cdk diffではModifyとなるようなものはありませんでした。

変更セットを作成して確認してみます。

11.変更セット.png
12.Properties.Analyticsが変更される.png

はい、AWS::CDK::MetadataAnalyticsModifyと判定されているようです。

こちらにはConstructのリストが含まれているため、今回のようにConstruct自体を変更する際にはAnalyticsに変更が発生します。

Analytics プロパティは、スタック内のコンストラクトのリストを gzip圧縮し、base64 エンコードして、プレフィックスエンコードしたものです。

AWS CDK CLI リファレンス - AWS Cloud Development Kit (AWS CDK) v2

リファクタリングの実行 (analyticsReportingをfalseにして再トライ)

Analyticsを含む場合必ずエラーとなるので、analyticsReporting: falseでメタデータとして出力しないようにします。

 #!/usr/bin/env node
 import * as cdk from "aws-cdk-lib";
 import { ReshapeCfnStackStack } from "../lib/reshape-cfn-stack-stack";
 import { ReshapeCfnStack2Stack } from "../lib/reshape-cfn-stack2-stack";

+const app = new cdk.App({ analyticsReporting: false });
 new ReshapeCfnStackStack(app, "ReshapeCfnStackStack");
 new ReshapeCfnStack2Stack(app, "ReshapeCfnStack2Stack", {});

この状態で、手元のテンプレートファイルを更新して、リファクタリングの定義を作成しようとしても以下のようにエラーになります。

(aws cloudformation create-stack-refactorは省略)

> aws cloudformation describe-stack-refactor \
  --stack-refactor-id f4dfc043-1d4c-4201-bd71-3f562c6b3629 \
  --query 'StatusReason'
"arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/ReshapeCfnStackStack/92282680-e4f5-11ef-8dae-12951cea36cf: Stack Refactor does not support AWS::CDK::Metadata."

一度デプロイしてStack上にこのメタデータが含まれないようにする必要があります。

> npx cdk diff ReshapeCfnStackStack --no-change-set
Stack ReshapeCfnStackStack
Conditions
[-] Condition CDKMetadataAvailable: {.
.
(中略)
.
.}

✨  Number of stacks with differences: 1

> npx cdk deploy ReshapeCfnStackStack

✨  Synthesis time: 7.65s

ReshapeCfnStackStack: deploying... [1/1]
.
.
(中略)
.
.
ReshapeCfnStackStack | 3/3 | 18:45:44 | UPDATE_COMPLETE      | AWS::CloudFormation::Stack | ReshapeCfnStackStack

 ✅  ReshapeCfnStackStack

✨  Deployment time: 22.89s

Stack ARN:
arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/ReshapeCfnStackStack/92282680-e4f5-11ef-8dae-12951cea36cf

✨  Total time: 30.54s

この状態でリファクタリングの定義を作成しようとしても、まだダメです。以下のようにプロパティがマッチしないとエラーになります。

> aws cloudformation describe-stack-refactor \
  --stack-refactor-id b07f65c2-72b9-4b00-a9d6-d2d8790f3fba \
  --query 'StatusReason'
"Resource IamRoleConstructAPolicy138F2EF20 in stack arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/ReshapeCfnStackStack/92282680-e4f5-11ef-8dae-12951cea36cf does not match the destination resource's properties."

この場合も以下のようにaws:cdk:pathをリファクタリング対象のリソースのものと同じ値にする必要があります。

 Resources:
   IamRoleConstructA2Policy1CFD08057:
     Type: AWS::IAM::ManagedPolicy
     Properties:
       Description: ""
       ManagedPolicyName: test1-policy
       Path: /
       PolicyDocument:
         Statement:
           - Action: ec2:DescribeTags
             Effect: Allow
             Resource: "*"
         Version: "2012-10-17"
     Metadata:
+      aws:cdk:path: ReshapeCfnStackStack/IamRoleConstructA/Policy1/Resource
   IamRoleConstructA296AD5240:
     Type: AWS::IAM::Role
     Properties:
       AssumeRolePolicyDocument:
         Statement:
           - Action: sts:AssumeRole
             Effect: Allow
             Principal:
               Service: ec2.amazonaws.com
         Version: "2012-10-17"
       ManagedPolicyArns:
         - Ref: IamRoleConstructA2Policy1CFD08057
       RoleName: test1
     Metadata:
+      aws:cdk:path: ReshapeCfnStackStack/IamRoleConstructA/Default/Resource

こちらでリファクタリングの定義の作成を行います。

> aws cloudformation create-stack-refactor \
  --stack-definitions \
    StackName=ReshapeCfnStackStack,TemplateBody@=file://after-ReshapeCfnStackStack.yml \
    StackName=ReshapeCfnStack2Stack,TemplateBody@=file://after-ReshapeCfnStack2Stack.yml \
  --enable-stack-creation \
  --resource-mappings file://refactor3.json
{
    "StackRefactorId": "b871c83b-bcda-4e9c-9d5a-cf708151436b"
}

> aws cloudformation describe-stack-refactor --stack-refactor-id b871c83b-bcda-4e9c-9d5a-cf708151436b
{
    "StackRefactorId": "b871c83b-bcda-4e9c-9d5a-cf708151436b",
    "StackIds": [
        "arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/ReshapeCfnStackStack/92282680-e4f5-11ef-8dae-12951cea36cf",
        "arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/ReshapeCfnStack2Stack/0b09cdf0-e524-11ef-9310-0affccb0cbdb"
    ],
    "ExecutionStatus": "AVAILABLE",
    "Status": "CREATE_COMPLETE"
}

作成できました。

リファクタリングの内容を確認すると、確かに2つのリソースに対してMOVEが走るようです。

> aws cloudformation list-stack-refactor-actions --stack-refactor-id b871c83b-bcda-4e9c-9d5a-cf708151436b
{
    "StackRefactorActions": [
        {
            "Action": "CREATE",
            "Entity": "STACK",
            "Description": "Stack arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/ReshapeCfnStack2Stack/0b09cdf0-e524-11ef-9310-0affccb0cbdb created.",
            "Detection": "MANUAL",
            "TagResources": [],
            "UntagResources": [],
            "ResourceMapping": {
                "Source": {},
                "Destination": {}
            }
        },
        {
            "Action": "MOVE",
            "Entity": "RESOURCE",
            "PhysicalResourceId": "test1",
            "Description": "Resource configuration changes will be validated during refactor execution.",
            "Detection": "MANUAL",
            "TagResources": [],
            "UntagResources": [],
            "ResourceMapping": {
                "Source": {
                    "StackName": "arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/ReshapeCfnStackStack/92282680-e4f5-11ef-8dae-12951cea36cf",
                    "LogicalResourceId": "IamRoleConstructABEBB5F34"
                },
                "Destination": {
                    "StackName": "arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/ReshapeCfnStack2Stack/0b09cdf0-e524-11ef-9310-0affccb0cbdb",
                    "LogicalResourceId": "IamRoleConstructA296AD5240"
                }
            }
        },
        {
            "Action": "MOVE",
            "Entity": "RESOURCE",
            "PhysicalResourceId": "arn:aws:iam::<AWSアカウントID>:policy/test1-policy",
            "Description": "No configuration changes detected.",
            "Detection": "MANUAL",
            "TagResources": [],
            "UntagResources": [],
            "ResourceMapping": {
                "Source": {
                    "StackName": "arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/ReshapeCfnStackStack/92282680-e4f5-11ef-8dae-12951cea36cf",
                    "LogicalResourceId": "IamRoleConstructAPolicy138F2EF20"
                },
                "Destination": {
                    "StackName": "arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/ReshapeCfnStack2Stack/0b09cdf0-e524-11ef-9310-0affccb0cbdb",
                    "LogicalResourceId": "IamRoleConstructA2Policy1CFD08057"
                }
            }
        }
    ]
}

あとは実行するだけです。

> aws cloudformation execute-stack-refactor --stack-refactor-id b871c83b-bcda-4e9c-9d5a-cf708151436b

> aws cloudformation describe-stack-refactor --stack-refactor-id b871c83b-bcda-4e9c-9d5a-cf708151436b
{
    "StackRefactorId": "b871c83b-bcda-4e9c-9d5a-cf708151436b",
    "StackIds": [
        "arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/ReshapeCfnStackStack/92282680-e4f5-11ef-8dae-12951cea36cf",
        "arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/ReshapeCfnStack2Stack/0b09cdf0-e524-11ef-9310-0affccb0cbdb"
    ],
    "ExecutionStatus": "EXECUTE_IN_PROGRESS",
    "Status": "CREATE_COMPLETE"
}

なかなか完了しない and 文字数が5万文字近いので続きは別記事で書きます。

Stack間でリソースを移動させたい場合や論理IDに悩みがある人待望のアップデート

CloudFormationのStack間のリソースの移動や論理IDの変更を簡単に行えるようになったアップデートを紹介しました。

Stack間でリソースを移動させたい場合や論理IDに悩みがある人にとっては待望のアップデートですね。

Constructの分割方針に失敗して、リファクタリングに絶望することが少しは減りそうです。

この記事が誰かの助けになれば幸いです。

以上、クラウド事業本部 コンサルティング部の のんピ(@non____97)でした!

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.