Cloud Formation Stack Refactoring試してみた
概要
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のエクスポートのみを実施するようになる。
手順
- 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
- before.yamlを使いMySnsスタックを作ります。
aws cliのコマンドです
aws cloudformation create-stack --stack-name MySns --template-body file://before.yaml --capabilities CAPABILITY_IAM
- 新しいテンプレートafterSns.yamlを作ります。
このテンプレートは新しいSNSトピックを作り、そのARNのエクスポートが定義されています。
afterSsn.yaml
AWSTemplateFormatVersion: "2010-09-09"
Resources:
Topic:
Type: AWS::SNS::Topic
Outputs:
TopicArn:
Value: !Ref Topic
Export:
Name: TopicArn
- 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
- リソースマッピング、refactor.json、ファイルを作ります。
リソースの論理IDを変更するファイルです。リファクタリング対象とする、入力と出力のスタック名とリソースの論理IDを指定します。
つまりここでは、MySnsスタックのMyFunctionリソースとMyLambdaSubscriptionスタックのFunctionリソースは同じものですよ。ということですね。
[
{
"Source": {
"StackName": "MySns",
"LogicalResourceId": "MyFunction"
},
"Destination": {
"StackName": "MyLambdaSubscription",
"LogicalResourceId": "Function"
}
}
]
- スタックリファクタリングのタスクを作ります
--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は後ほど使うので記録しておきます。
- スタックリファクタータスクを評価します
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": {}
}
},
...
- スタックリファクタリングを実行します
aws cloudformation execute-stack-refactor --stack-refactor-id 56b06a9a-72ff-4f87-8205-32111bff83f9
- スタックリファクタリングの完了を待ちます
以下で状況を確認できます
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