如何在同一AWS CloudFormation模板中声明多组相似资源并避免代码重复?
我明白你的需求——原来通过参数Role单实例部署服务层,现在要在同一个模板里一次性创建inbound/filter/outbound三个层,还要避免重复写相同的资源代码,同时保留CloudFormation的回滚和流水线集成能力。这是CloudFormation里非常常见的复用场景,推荐两种最优解法:
方法1:嵌套栈(Nested Stacks)——最经典的复用方案
嵌套栈是CloudFormation官方推荐的资源复用方式,核心思路是把单个服务层的所有公共逻辑(比如安全组、Auto Scaling组、启动配置)抽成一个参数化的嵌套模板,然后在主模板里三次调用这个嵌套栈,分别传入不同的Role参数。
具体步骤:
抽离公共逻辑到嵌套模板
创建一个独立的嵌套模板(比如service-layer.yaml),把你原来单个层的资源定义、Role参数、Conditions逻辑都放进去。比如这个嵌套模板的核心结构:AWSTemplateFormatVersion: "2010-09-09" Parameters: Role: Type: String AllowedValues: ["inbound", "filter", "outbound"] Environment: Type: String AllowedValues: ["eu-staging", "eu-production"] Conditions: IsStaging: !Equals [!Ref Environment, "eu-staging"] Resources: ServiceSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: !Sub "SMTP ${Role} Layer Security Group" SecurityGroupEgress: - !If - IsStaging - CidrIp: "10.243.74.0/24" IpProtocol: tcp FromPort: 9094 ToPort: 9094 - !Ref AWS::NoValue Tags: - Key: Name Value: !Sub "smtp-service-security-group-${Role}" - Key: Role Value: !Ref Role # 这里添加Auto Scaling组、启动配置等其他层资源...主模板调用嵌套栈三次
在你的主模板里,创建三个AWS::CloudFormation::Stack资源,分别对应三个角色,传递对应的参数:AWSTemplateFormatVersion: "2010-09-09" Parameters: Environment: Type: String Default: "eu-staging" AllowedValues: ["eu-staging", "eu-production"] # 其他全局参数(比如VPC ID、子网ID等)... Resources: InboundLayer: Type: AWS::CloudFormation::Stack Properties: TemplateURL: ./service-layer.yaml # 如果是线上部署,换成S3路径比如https://your-bucket.s3.eu-west-1.amazonaws.com/service-layer.yaml Parameters: Role: inbound Environment: !Ref Environment # 传递其他需要的参数(比如VPC ID)... FilterLayer: Type: AWS::CloudFormation::Stack Properties: TemplateURL: ./service-layer.yaml Parameters: Role: filter Environment: !Ref Environment # 传递其他需要的参数... OutboundLayer: Type: AWS::CloudFormation::Stack Properties: TemplateURL: ./service-layer.yaml Parameters: Role: outbound Environment: !Ref Environment # 传递其他需要的参数...
这种方案的优势:
- 完全避免重复代码,所有公共逻辑只写一次
- CloudFormation的回滚、变更集功能会自动覆盖所有嵌套栈,和现有流水线完美兼容
- 后续修改层逻辑只需要更新嵌套模板,不用改主模板
方法2:CloudFormation宏(AWS::CloudFormation::Macro)——单模板内生成多资源
如果你不想拆分多个模板,也可以用宏来在单个模板中自动生成三个角色对应的资源。宏允许你通过Lambda函数自定义模板转换逻辑,动态生成重复资源。
具体步骤:
编写宏的Lambda函数
创建一个Lambda函数,接收CloudFormation模板的内容,然后遍历inbound/filter/outbound三个角色,为每个角色生成对应的资源(安全组、Auto Scaling组等)。Lambda的核心逻辑大概是解析输入模板,添加三个资源实例,然后返回修改后的模板。注册宏到CloudFormation
在CloudFormation控制台或CLI中注册这个Lambda作为宏,比如命名为GenerateServiceLayers。在主模板中调用宏
主模板里通过宏来生成所有层的资源,示例:AWSTemplateFormatVersion: "2010-09-09" Parameters: Environment: Type: String Default: "eu-staging" AllowedValues: ["eu-staging", "eu-production"] Transform: - Name: GenerateServiceLayers Parameters: Roles: ["inbound", "filter", "outbound"] Environment: !Ref Environment # 宏会自动生成三个角色对应的所有资源,不需要手动写重复代码
这种方案的优势是所有内容都在单个模板里,但需要额外维护Lambda宏函数,复杂度比嵌套栈高一些。
备选方案:简化重复代码(无嵌套/宏)
如果暂时不想引入嵌套栈或宏,也可以通过!Sub等函数减少重复代码的冗余度,虽然还是要写三个资源,但代码会简洁很多:
AWSTemplateFormatVersion: "2010-09-09" Parameters: Environment: Type: String Default: "eu-staging" Conditions: IsStaging: !Equals [!Ref Environment, "eu-staging"] Resources: InboundSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: "SMTP Inbound Layer Security Group" SecurityGroupEgress: !If [IsStaging, [{CidrIp: "10.243.74.0/24", IpProtocol: tcp, FromPort: 9094, ToPort: 9094}], !Ref AWS::NoValue] Tags: - Key: Name Value: !Sub "smtp-service-security-group-inbound" - Key: Role Value: inbound FilterSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: "SMTP Filter Layer Security Group" SecurityGroupEgress: !If [IsStaging, [{CidrIp: "10.243.74.0/24", IpProtocol: tcp, FromPort: 9094, ToPort: 9094}], !Ref AWS::NoValue] Tags: - Key: Name Value: !Sub "smtp-service-security-group-filter" - Key: Role Value: filter OutboundSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: "SMTP Outbound Layer Security Group" SecurityGroupEgress: !If [IsStaging, [{CidrIp: "10.243.74.0/24", IpProtocol: tcp, FromPort: 9094, ToPort: 9094}], !Ref AWS::NoValue] Tags: - Key: Name Value: !Sub "smtp-service-security-group-outbound" - Key: Role Value: outbound
不过这种方案还是有重复代码,只是减少了冗余,推荐优先用嵌套栈。
内容的提问来源于stack exchange,提问作者Antônio

