CloudFormation’s intrinsic functions are the secret sauce that makes your infrastructure dynamic, allowing you to reference resources, substitute values, and make decisions right within your templates.

Let’s see Fn::Sub in action. Imagine you want to create a parameter for a VPC CIDR block and then use that CIDR block to define a subnet.

Parameters:
  VPCCidr:
    Type: String
    Default: "10.0.0.0/16"

Resources:
  MyVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCidr

  MySubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref MyVPC
      CidrBlock: !Sub "${MyVPC.CidrBlock}.1" # This is where Fn::Sub shines
      Tags:
        - Key: Name
          Value: !Sub "MySubnet-in-${AWS::Region}"

In this example, !Ref VPCCidr directly pulls the value from the VPCCidr parameter. But notice !Sub "${MyVPC.CidrBlock}.1". This is Fn::Sub doing its magic. It’s taking the CidrBlock attribute from the MyVPC resource (which itself was defined using the VPCCidr parameter) and appending .1 to it. This dynamically creates a subnet CIDR like 10.0.1.0/24 if the VPC CIDR is 10.0.0.0/16. Similarly, !Sub "MySubnet-in-${AWS::Region}" constructs a tag value using the AWS region where the stack is deployed.

The core problem intrinsic functions solve is templating infrastructure as code. Without them, you’d be copy-pasting values, hardcoding resource IDs, and managing separate configuration files for each environment. Intrinsic functions enable a single template to generate vastly different, yet consistent, infrastructure across multiple accounts or regions.

Internally, CloudFormation processes these functions during stack creation or update. When it encounters a !Ref, it looks for a resource with that logical ID or a parameter with that name. For !Sub, it parses the string, identifies bracketed placeholders like ${MyVPC.CidrBlock} or ${AWS::Region}, and substitutes them with the actual resolved values at that moment. This substitution happens before the AWS API calls are made to create the resources.

The exact levers you control are the functions themselves and the values you pass to them. !Ref is straightforward, referencing a resource or parameter. Fn::Sub (or !Sub) offers string interpolation, allowing you to embed resource attributes and other intrinsic function outputs within strings. Fn::Join concatenates a list of strings with a delimiter, useful for creating security group rules or IAM policies. Fn::GetAtt retrieves attributes from a resource, like the ARN of an S3 bucket or the DNS name of an ELB. Fn::ImportValue is crucial for cross-stack references, allowing one stack to output a value that another stack can consume.

The Fn::Join function, while seemingly simple, can be a powerful tool for constructing complex configurations programmatically. For instance, if you have a list of security group IDs that need to be associated with an EC2 instance, you can use Fn::Join to create a comma-separated string suitable for the SecurityGroupIds property of an AWS::EC2::Instance resource. Instead of manually concatenating them in your template, which becomes cumbersome with many IDs, Fn::Join keeps it clean and manageable, especially when those IDs might themselves be outputs from other resources or stacks.

The next concept you’ll grapple with is managing dependencies between resources and handling potential race conditions when resources are created or updated in parallel.

Want structured learning?

Take the full Cloudformation course →