🐈

Cloud Formation Stack Refactoring試してみた

2025/02/09に公開

概要

2025/02/06にクラウドフォーメーションのリファクタリング機能が発表されました!
スタックリソースの変更を従来よりも簡単にできるようになりました
aws CLIやSDKを使い新しく追加されたコマンドを使うことでリファクタリングできます。

まだまだ実運用で使ってみないとピンとこないところもありますが、試してみると便利さが実感できました。
以下に情報を整理します。

whats-new

  • スタックリファクタリングによりリソースのスタック間移動が可能になります。
  • モノリシックなスタックを小さなコンポーネントに分離できます
  • リソースの論理名を変更できます

従来の作業

  • ターゲットとするリソースを維持するようにtemplateでretain設定をする。
  • 現在のスタックから削除する
  • 新しいスタックにインポートする

新しい作業

  • 新しい状態のtemplateを作る
  • リファクター作業のプレビューを作リ、確認する
  • 問題なければアトミックな変更操作を実施する

user-guide

シナリオ

新しいスタックを作り、その中のリソースを、更に新しい別のスタックに移動します。
SNSとSNSをsubscribeするlambdaを作ります。しかしその後SNSの利用が広がってきたために別のスタックに移動したいと考えています。
before.yamlをafterSns.yamlとafterLambda.yamlに分割します。

もともと作成済みだったMySnsスタックから、Lambdaを分離してMyLambdaSubscritpionスタックとして新設する。MySnsスタックはSNS及びそのリソースIDのエクスポートのみを実施するようになる。

手順

  1. before.yamlを作ります
    lambda関数とIAM Role、SNSが定義されています。
before.yaml
AWSTemplateFormatVersion: "2010-09-09"

Resources:
  Topic:
    Type: AWS::SNS::Topic

  MyFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: my-function
      Handler: index.handler
      Runtime: python3.12
      Code:
        ZipFile: |
          import json
          def handler(event, context):
            print(json.dumps(event))
            return event
      Role: !GetAtt FunctionRole.Arn
      Timeout: 30

  Subscription:
    Type: AWS::SNS::Subscription
    Properties:
      Endpoint: !GetAtt MyFunction.Arn
      Protocol: lambda
      TopicArn: !Ref Topic

  FunctionInvokePermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      Principal: sns.amazonaws.com
      FunctionName: !GetAtt MyFunction.Arn
      SourceArn: !Ref Topic

  FunctionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Action:
              - sts:AssumeRole
            Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Condition:
              StringEquals:
                aws:SourceAccount: !Ref AWS::AccountId
              ArnLike:
                aws:SourceArn: !Sub "arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:my-function"
      Policies:
        - PolicyName: LambdaPolicy
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource:
                  - arn:aws:logs:*:*:*
                Effect: Allow
  1. before.yamlを使いMySnsスタックを作ります。
    aws cliのコマンドです

aws cloudformation create-stack --stack-name MySns --template-body file://before.yaml --capabilities CAPABILITY_IAM

  1. 新しいテンプレートafterSns.yamlを作ります。
    このテンプレートは新しいSNSトピックを作り、そのARNのエクスポートが定義されています。
afterSsn.yaml
AWSTemplateFormatVersion: "2010-09-09"
Resources:
  Topic:
    Type: AWS::SNS::Topic
Outputs:
  TopicArn:
    Value: !Ref Topic
    Export:
      Name: TopicArn
  1. afterLambda.yamlを作ります
    before.yamlに対して、!Ref Topic が エクスポートされたリソースを参照する !ImportValue TopicArn に変わっています。
afterLambda.yaml
AWSTemplateFormatVersion: "2010-09-09"
Resources:
  Function:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: my-function
      Handler: index.handler
      Runtime: python3.12
      Code:
        ZipFile: |
          import json
          def handler(event, context):
            print(json.dumps(event))
            return event
      Role: !GetAtt FunctionRole.Arn
      Timeout: 30
  Subscription:
    Type: AWS::SNS::Subscription
    Properties:
      Endpoint: !GetAtt Function.Arn
      Protocol: lambda
      TopicArn: !ImportValue TopicArn
  FunctionInvokePermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      Principal: sns.amazonaws.com
      FunctionName: !GetAtt Function.Arn
      SourceArn: !ImportValue TopicArn
  FunctionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Action:
              - sts:AssumeRole
            Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Condition:
              StringEquals:
                aws:SourceAccount: !Ref AWS::AccountId
              ArnLike:
                aws:SourceArn: !Sub "arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:my-function"
      Policies:
        - PolicyName: LambdaPolicy
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource:
                  - arn:aws:logs:*:*:*
                Effect: Allow
diff
--- before.yaml	2025-02-08 21:38:54
+++ afterLambda.yaml.txt	2025-02-08 21:39:15
@@ -1,65 +1,58 @@
 AWSTemplateFormatVersion: "2010-09-09"
-
 Resources:
-  Topic:
-    Type: AWS::SNS::Topic
-
-  MyFunction:
+  Function:
     Type: AWS::Lambda::Function
     Properties:
       FunctionName: my-function
       Handler: index.handler
       Runtime: python3.12
       Code:
         ZipFile: |
           import json
           def handler(event, context):
             print(json.dumps(event))
             return event
       Role: !GetAtt FunctionRole.Arn
       Timeout: 30
-
   Subscription:
     Type: AWS::SNS::Subscription
     Properties:
-      Endpoint: !GetAtt MyFunction.Arn
+      Endpoint: !GetAtt Function.Arn
       Protocol: lambda
-      TopicArn: !Ref Topic
-
+      TopicArn: !ImportValue TopicArn
   FunctionInvokePermission:
     Type: AWS::Lambda::Permission
     Properties:
       Action: lambda:InvokeFunction
       Principal: sns.amazonaws.com
-      FunctionName: !GetAtt MyFunction.Arn
-      SourceArn: !Ref Topic
-
+      FunctionName: !GetAtt Function.Arn
+      SourceArn: !ImportValue TopicArn
   FunctionRole:
     Type: AWS::IAM::Role
     Properties:
       AssumeRolePolicyDocument:
         Version: "2012-10-17"
         Statement:
           - Action:
               - sts:AssumeRole
             Effect: Allow
             Principal:
               Service:
                 - lambda.amazonaws.com
             Condition:
               StringEquals:
                 aws:SourceAccount: !Ref AWS::AccountId
               ArnLike:
                 aws:SourceArn: !Sub "arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:my-function"
       Policies:
         - PolicyName: LambdaPolicy
           PolicyDocument:
             Version: "2012-10-17"
             Statement:
               - Action:
                   - logs:CreateLogGroup
                   - logs:CreateLogStream
                   - logs:PutLogEvents
                 Resource:
                   - arn:aws:logs:*:*:*
                 Effect: Allow
  1. リソースマッピング、refactor.json、ファイルを作ります。
    リソースの論理IDを変更するファイルです。リファクタリング対象とする、入力と出力のスタック名とリソースの論理IDを指定します。
    つまりここでは、MySnsスタックのMyFunctionリソースとMyLambdaSubscriptionスタックのFunctionリソースは同じものですよ。ということですね。
[
    {
        "Source": {
            "StackName": "MySns",
            "LogicalResourceId": "MyFunction"
        },
        "Destination": {
            "StackName": "MyLambdaSubscription",
            "LogicalResourceId": "Function"
        }
    }
]
  1. スタックリファクタリングのタスクを作ります
    --enable-stack-creationオプションを使ってリファクタリング機能に出力先のスタックを作るように指示しています。出力先のスタックがすでにある場合は不要です。
    今回はMySnsスタックは作成済みですが、myLambdaSubscriptionはまだ存在していないので、作るということですね。

aws cloudformation create-stack-refactor --stack-definitions StackName=MySns,TemplateBody@=file://afterSns.yaml StackName=MyLambdaSubscription,TemplateBody@=file://afterLambda.yaml --enable-stack-creation --resource-mappings file://refactor.json

結果

{
    "StackRefactorId": "56b06a9a-72ff-4f87-8205-32111bff83f9"
}

スタックリファクターIDは後ほど使うので記録しておきます。

  1. スタックリファクタータスクを評価します

aws cloudformation describe-stack-refactor --stack-refactor-id 56b06a9a-72ff-4f87-8205-32111bff83f9

結果

{
    "StackRefactorId": "56b06a9a-72ff-4f87-8205-32111bff83f9",
    "StackIds": [
        "arn:aws:cloudformation:<<AWS::Region>>:<<AWS::AccountId>>:stack/MySns/a10bfd30-cc67-11ef-877a-023cc5780193",
        "arn:aws:cloudformation:<<AWS::Region>>:<<AWS::AccountId>>:stack/MyLambdaSubscription/6d117360-cc68-11ef-ba33-06338dcc9d39"
    ],
    "ExecutionStatus": "AVAILABLE",
    "Status": "CREATE_COMPLETE"
}

もしスタックリファクターIDがわからなくなったら以下で取得できます

aws cloudformation list-stack-refactors

リファクターにより実行されるアクションのリストを取得できます

aws cloudformation list-stack-refactor-actions —stack-refactor-id 56b06a9a-72ff-4f87-8205-32111bff83f9

新しいスタックが作られ、移動されるリソースがわかります。
MySnsスタックのFunctionRoleがMyLambdaSubscriptionのFunctionRoleに移動されます。
MyLambdaSubscriptionスタックが作られます。
ここの例では「MySnsスタックのFunctionRole」と「MyLambdaSubscriptionスタックのFunctionRole」のマッピングしか記載されていませんが、実際試すと、以下についてもマッピングが作られました

  • MySnsスタックのMyFunction -> MyLambdaSubscription->Function
  • MySnsスタックのFunctionInvokePermission -> MyLambdaSubscription->FunctionInvokePermission
  • MySnsスタックのSubscription -> MyLambdaSubscription->Subscription"
    リソースマッピングは全て実施する必要はなく、関連するものはリファクタリングしてくれるようです。
action list
{
    "StackRefactorActions": [
        {
            "Action": "Move",
            "Entity": "Resource",
            "PhysicalResourceId": "MySns-FunctionRole-BMO7ohLu4S6a",
            "Description": "No configuration changes detected.",
            "Detection": "Auto",
            "TagResources": [],
            "UntagResources": [],
            "ResourceMapping": {
                "Source": {
                    "StackName": "arn:aws:cloudformation:<<AWS::Region>>:<<AWS::AccountId>>:stack/MySns/a10bfd30-cc67-11ef-877a-023cc5780193",
                    "LogicalResourceId": "FunctionRole"
                },
                "Destination": {
                    "StackName": "arn:aws:cloudformation:<<AWS::Region>>:<<AWS::AccountId>>:stack/MyLambdaSubscription/6d117360-cc68-11ef-ba33-06338dcc9d39",
                    "LogicalResourceId": "FunctionRole"
                }
            }
        },
        {
            "Action": "Create",
            "Entity": "Stack",
            "Description": "Stack arn:aws:cloudformation:<<AWS::Region>>:<<AWS::AccountId>>:stack/MyLambdaSubscription/6d117360-cc68-11ef-ba33-06338dcc9d39 created.",
            "Detection": "Manual",
            "TagResources": [],
            "UntagResources": [],
            "ResourceMapping": {
                "Source": {},
                "Destination": {}
            }
        },
...
  1. スタックリファクタリングを実行します

aws cloudformation execute-stack-refactor --stack-refactor-id 56b06a9a-72ff-4f87-8205-32111bff83f9

  1. スタックリファクタリングの完了を待ちます
    以下で状況を確認できます

aws cloudformation describe-stack-refactor --stack-refactor-id 56b06a9a-72ff-4f87-8205-32111bff83f9

{
    "StackRefactorId": "56b06a9a-72ff-4f87-8205-32111bff83f9",
    "StackIds": [
        "arn:aws:cloudformation:<<AWS::Region>>:<<AWS::AccountId>>:stack/MySns/a10bfd30-cc67-11ef-877a-023cc5780193",
        "arn:aws:cloudformation:<<AWS::Region>>:<<AWS::AccountId>>:stack/MyLambdaSubscription/6d117360-cc68-11ef-ba33-06338dcc9d39"
    ],
    "ExecutionStatus": "EXECUTE_COMPLETE",
    "Status": "CREATE_COMPLETE"
}

ちなみにコンソールで見るとこんな感じでした

変更前

MySnsスタック

変更後

MySnsスタック

MyLambdaSubscription

Discussion