`Fn::Sub` (`!Sub` in YAML) substitutes variables into a string at template evaluation time. It replaces the older pattern of building strings with `Fn::Join`, which produced unreadable nested arrays of fragments.
## The three substitution patterns
```yaml
# 1. Reference a Parameter or Resource (equivalent to !Ref inside a string)
!Sub "Hello ${StackName}"
# 2. Reference a Resource attribute (equivalent to !GetAtt)
!Sub "${ApplicationLoadBalancer.DNSName}"
# 3. Reference a pseudo parameter
!Sub "arn:aws:s3:::${AWS::AccountId}-bucket"
```
A common composite use — building an ARN with multiple substitutions:
```yaml
!Sub "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:${MyNotificationTopic}"
```
## Local variable map (rarely needed)
`Fn::Sub` accepts a second argument: a map of substitution names to values. Useful when interpolating something that isn't already a Parameter / Resource / pseudo parameter:
```yaml
!Sub
- "Welcome to ${Environment}"
- Environment: !If [IsProd, "production", "staging"]
```
The map shadows template-level names. Without the map, `!Sub` resolves names against Parameters → Resources → Pseudo parameters in that order.
## Escape sequence: `${!Literal}`
To emit a literal `${...}` in the output (e.g., a shell variable in `UserData`), prefix with `!`:
```yaml
UserData: !Base64 |
#!/bin/bash
echo "Region is ${AWS::Region}" # CFN substitutes
echo "Home is ${!HOME}" # output: Home is ${HOME}
```
This is the most common gotcha when embedding bash scripts that use `${}` for shell expansion.
## Why `Fn::Sub` beat `Fn::Join`
The pre-Sub pattern for an ARN looked like this:
```yaml
!Join
- ''
- - 'arn:aws:sns:'
- !Ref AWS::Region
- ':'
- !Ref AWS::AccountId
- ':'
- !Ref MyTopic
```
Versus the `!Sub` equivalent (one line, readable). Use `Fn::Join` only when:
- You're joining a **list of items** (not just splicing into one string) — `!Join [",", !Ref MyList]`
- You need a delimiter that's not just empty string — `!Join ["/", [a, b, c]]`
For everything else, `!Sub` is the modern default. `cfn-lint` and `rain fmt` will often suggest converting `Join` patterns to `Sub`.
## What `Fn::Sub` cannot do
- **No conditionals** in the substitution string — use `Fn::If` to choose between two values, then pass into `Sub`
- **No expressions** — just variable substitution; no arithmetic, no string manipulation
- **No nested intrinsic functions inside `${...}`** — only names. To inject a computed value, use the local variable map
## Related
- [[CFN Ref vs Fn-GetAtt]]
- [[CFN Pseudo Parameters]]
- [[CFN Conditions Boolean Logic]]