37.3 Intrinsic Functions: Ref, Fn::Sub, Fn::GetAtt, Fn::If, Fn::Select
Right, let’s talk about the real magic trick of CloudFormation: intrinsic functions. These are the little spells you cast within your templates to make them dynamic, to pull in values you don’t know upfront, and to generally avoid having to hardcode every single thing. They’re the difference between a static, brittle configuration file and a powerful, reusable infrastructure definition. And some of them are a bit… odd. We’ll get to that.
The Workhorses: Ref and Fn::GetAtt
These two are your bread and butter, and you’ll use them more than all the others combined. They are often confused, but the distinction is simple, even if AWS’s naming conventions aren’t.
Use Ref on a resource to get its primary identifier. For most resources, this is the ARN. But for some, it’s the name. For an AWS::EC2::VPC, Ref gives you the VPC ID (e.g., vpc-12345). For an AWS::IAM::Role, it gives you the ARN. It’s inconsistent, and you just have to… know. Or, you know, check the documentation. I know, I hate it too.
Fn::GetAtt (short for “Get Attribute”) is for when you need something other than the primary identifier. You want the VPC’s CIDR block? You can’t get that with Ref. You need its specific attribute.
Resources:
MyVPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
MySecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: "A group that allows HTTP"
VpcId: !Ref MyVPC # This gets the VPC ID, e.g., vpc-12345
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: !GetAtt MyVPC.CidrBlock # This gets "10.0.0.0/16"
The Pitfall: The most common mistake is trying to use Ref when you need a specific attribute. You’ll stare at a failed creation with a Parameter validation failed error for an hour before you realize you needed Fn::GetAtt MyS3Bucket.WebsiteURL instead of !Ref MyS3Bucket.
The Swiss Army Knife: Fn::Sub
This is arguably the most powerful and useful function. Fn::Sub does string substitution. You can use it in two ways.
First, the simple way: it replaces variables like ${MyParameter} with their resolved value. This is incredibly clean for constructing ARNs or concatenating strings.
Parameters:
EnvironmentName:
Type: String
Resources:
MyBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub "${EnvironmentName}-my-application-bucket"
Second, the advanced way: you can provide a list of variables to substitute. Why would you do this? Because the first method only works with parameters, resource IDs, or other values that are already available to your template. If you need to use an attribute from a resource and do string substitution, you must use the second form.
Resources:
MyInstance:
Type: AWS::EC2::Instance
Properties:
# ...other properties...
UserData:
Fn::Base64:
!Sub |
#!/bin/bash
echo "The VPC's CIDR is ${MyVPCCidr}" > /tmp/test.txt
- { MyVPCCidr: !GetAtt MyVPC.CidrBlock } # This is the key
The Gotcha: The shorthand !Sub syntax is fantastic, but that second form must be written in the full YAML map style. You can’t use the !Sub shortcut for it. It’s a small syntax quirk that will bite you eventually.
The Logic: Fn::If and Fn::Select
These are the functions you use to make your templates slightly smarter, often in conjunction with Conditions and Mappings.
Fn::If does exactly what you think: it returns one value if a condition is true, another if it’s false. The catch? Both the “true” and “false” values are evaluated by CloudFormation, even if they aren’t chosen. This means the value you don’t use still has to be valid. If your false case references a resource that won’t be created, your entire stack will fail validation.
Fn::Select is for when you have a list and you need one element from it. The classic use is with the Fn::GetAZs function, which returns a list of Availability Zones. You might use Fn::Select to pick a specific one for a subnet.
Parameters:
CreateProdResource:
Type: String
AllowedValues: [true, false]
Conditions:
IsProd: !Equals [!Ref CreateProdResource, "true"]
Resources:
MyInstance:
Type: AWS::EC2::Instance
Properties:
InstanceType: !If [IsProd, "m5.large", "t3.micro"] # If true, m5.large; if false, t3.micro
AvailabilityZone: !Select [0, !Ref AvailabilityZones] # Picks the first AZ in the list
The Major Headache: Fn::Select uses a zero-based index. [0, ...] gets the first item, [1, ...] the second, and so on. If you’re like me and your brain sometimes defaults to 1-based indexing, you will waste 30 minutes of your life you’ll never get back debugging why your instance is in us-east-1b instead of us-east-1a. Consider this your official warning.