When working with complex AWS CloudFormation templates, we often encounter situations where the same computed value needs to be referenced multiple times. The example given shows how we might construct a hostname by joining various parameters together:
"Fn::Join": [
".", [
{ "Fn::Join": [ "", [ { "Ref": "ELBHostName" }, "-1" ] ] },
{ "Ref": "EnvironmentVersioned" },
{ "Ref": "HostedZone" }
]
]
Repeating this construction throughout the template creates several issues:
- Maintenance becomes difficult when the pattern needs to change
- The template becomes harder to read and understand
- Potential for errors increases with each repetition
- Template size grows unnecessarily
1. Using Mappings Section
While not exactly variables, you can use the Mappings section to define reusable values:
"Mappings": {
"HostNamePatterns": {
"FullHostname": {
"Pattern": "{0}-1.{1}.{2}"
}
}
}
2. Nested Stacks Approach
Create a nested stack that outputs these computed values:
"Resources": {
"HelperStack": {
"Type": "AWS::CloudFormation::Stack",
"Properties": {
"TemplateURL": "helper-template.json",
"Parameters": {
"ELBHostName": { "Ref": "ELBHostName" },
"EnvironmentVersioned": { "Ref": "EnvironmentVersioned" },
"HostedZone": { "Ref": "HostedZone" }
}
}
}
}
3. AWS CDK or SAM Alternative
For more complex cases, consider using AWS CDK or SAM which support proper variables:
// AWS CDK example
const hostNameFull = ${props.elbHostName}-1.${props.environmentVersioned}.${props.hostedZone};
For pure CloudFormation templates, the most maintainable approach is often to:
- Create a separate "helper" stack that computes these values
- Output the computed values from that stack
- Reference these outputs in your main template
"Outputs": {
"FullHostname": {
"Value": {
"Fn::Join": [
".", [
{ "Fn::Join": [ "", [ { "Ref": "ELBHostName" }, "-1" ] ] },
{ "Ref": "EnvironmentVersioned" },
{ "Ref": "HostedZone" }
]
]
}
}
}
AWS CloudFormation continues to evolve, and we may see:
- Native variable support in future versions
- Better tooling for template refactoring
- Improved integration with other AWS services for this purpose
When working with complex AWS CloudFormation templates, we often find ourselves repeating the same intrinsic functions (like Fn::Join
or Fn::Sub
) multiple times to construct similar values. This not only makes templates verbose but also introduces maintenance challenges.
While CloudFormation doesn't have direct variable declarations like programming languages, we can achieve similar functionality through these methods:
# Method 1: Using Mappings
"Mappings": {
"Variables": {
"HostNameComponents": {
"Base": {"Fn::Join": ["", [{"Ref": "ELBHostName"}, "-1"]]},
"DomainParts": [
{"Ref": "EnvironmentVersioned"},
{"Ref": "HostedZone"}
]
}
}
}
# Later in the template:
{"Fn::Join": [".", [
{"Fn::FindInMap": ["Variables", "HostNameComponents", "Base"]},
{"Fn::Select": [0, {"Fn::FindInMap": ["Variables", "HostNameComponents", "DomainParts"]}]},
{"Fn::Select": [1, {"Fn::FindInMap": ["Variables", "HostNameComponents", "DomainParts"]}]}
]]}
For more advanced variable-like functionality, consider creating a CloudFormation macro:
# Macro definition template
"Resources": {
"VariableSubstitutionMacro": {
"Type": "AWS::CloudFormation::Macro",
"Properties": {
"Name": "VariableSubstitution",
"FunctionName": {"Ref": "MacroLambdaFunction"}
}
}
}
# Usage in your template
"Transform": ["VariableSubstitution"],
"Variables": {
"HostNameFull": {
"Fn::Join": [".", [
{"Fn::Join": ["", [{"Ref": "ELBHostName"}, "-1"]]},
{"Ref": "EnvironmentVersioned"},
{"Ref": "HostedZone"}
]]
}
}
# Reference elsewhere
{"Fn::Sub": "${HostNameFull}"}
Here's how you might implement this for an Elastic Beanstalk environment with multiple instances:
{
"AWSTemplateFormatVersion": "2010-09-09",
"Parameters": {
"BaseName": {
"Type": "String",
"Default": "myapp"
}
},
"Mappings": {
"Namespace": {
"Hostname": {
"Pattern": {"Fn::Join": ["", ["${BaseName}-", "${InstanceNum}", ".${Env}.${Domain}"]]}
}
}
},
"Resources": {
"Instance1": {
"Type": "AWS::EC2::Instance",
"Properties": {
"Tags": [{
"Key": "Name",
"Value": {"Fn::Sub": [
{"Fn::FindInMap": ["Namespace", "Hostname", "Pattern"]},
{"BaseName": {"Ref": "BaseName"}, "InstanceNum": "1", "Env": "prod", "Domain": "example.com"}
]}
}]
}
}
}
}
When implementing these patterns:
- Document all "variables" in a dedicated Mappings section
- Use consistent naming conventions (e.g., prefix with Var or NS)
- Consider breaking extremely large templates into nested stacks
- Validate your macros thoroughly before production use