1. String Interpolation and Directives#
In most modern programming languages, you can dynamically insert variables into strings (e.g., Template Literals in JavaScript `Hello ${name}`). Terraform supports this natively via Interpolation.
Basic Interpolation#
To inject a variable or an attribute from another resource into a string, wrap it in ${...}.
variable "environment" {
type = string
default = "production"
}
resource "aws_s3_bucket" "app_data" {
# The resulting bucket name will be: "company-app-data-production"
bucket = "company-app-data-${var.environment}"
}Conditional Directives#
Terraform allows you to perform basic inline logic, such as an if/else statement using the ternary operator condition ? true_val : false_val.
resource "aws_instance" "web" {
ami = "ami-12345"
# If the environment is production, use a large instance. Otherwise, use a micro.
instance_type = var.environment == "production" ? "t3.large" : "t3.micro"
}2. The Power of locals (Keeping Code DRY)#
Imagine you have 15 different resources in your main.tf file, and you want to tag all of them with a standard naming convention: ProjectName-Environment-Region.
If you use standard string interpolation on every single resource block, your code violates the DRY (Don’t Repeat Yourself) principle. If the naming convention changes tomorrow, you must update it in 15 different places.
This is where the locals block shines. A local value assigns a name to an expression, allowing you to use it multiple times within a module without repeating it.
Practice: Centralizing Logic with Locals#
Create a new directory named terraform-locals:
mkdir terraform-locals
cd terraform-localsCreate a main.tf file and paste the following:
variable "project" {
default = "payment-gateway"
}
variable "environment" {
default = "staging"
}
# 1. Define the Locals
locals {
# Centralized naming convention logic
common_prefix = "${var.project}-${var.environment}"
# Centralized tags to be applied to all resources
standard_tags = {
Project = var.project
Environment = var.environment
ManagedBy = "Terraform"
}
}
# 2. Use the Locals
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
tags = merge(
local.standard_tags,
{ Name = "${local.common_prefix}-vpc" }
)
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
tags = merge(
local.standard_tags,
{ Name = "${local.common_prefix}-public-subnet" }
)
}Notice how we access the values using the local. prefix (not locals.). By centralizing the common_prefix and standard_tags, any future changes to the tagging strategy only need to be made in one single place.
3. Terraform Built-in Functions#
Terraform comes with dozens of built-in functions to transform and combine values. Note that Terraform does not support user-defined functions; you must use the ones provided by the HCL language.
Here are the most critical functions every Platform Engineer must know:
String Functions#
| Function | Description | Example | Result |
|---|---|---|---|
lower() | Converts all characters to lowercase | lower("PROD") | "prod" |
upper() | Converts all characters to uppercase | upper("dev") | "DEV" |
replace() | Replaces substrings | replace("hello-world", "-", "_") | "hello_world" |
join() | Combines a list into a string | join(",", ["a", "b", "c"]) | "a,b,c" |
Collection Functions#
| Function | Description | Example | Result |
|---|---|---|---|
length() | Returns the number of items in a list or string | length(["a", "b", "c"]) | 3 |
concat() | Combines two or more lists | concat(["a"], ["b"]) | ["a", "b"] |
merge() | Combines two or more maps (dictionaries) | merge({a=1}, {b=2}) | {a=1, b=2} |
keys() | Returns a list of keys from a map | keys({a=1, b=2}) | ["a", "b"] |
File and Encoding Functions#
Often, you need to pass a bash script into an EC2 instance’s user_data field to execute upon boot. Instead of hardcoding a massive string in your HCL, you can use the file() function.
resource "aws_instance" "web" {
ami = "ami-12345"
instance_type = "t3.micro"
# Reads the bash script from disk and injects it as a string
user_data = file("${path.module}/setup.sh")
}Practice: Using the Terraform Console#
You do not need to run a full terraform apply to test functions. Terraform provides an interactive REPL (Read-Eval-Print Loop) console, similar to Python’s or Node.js’s terminal console.
Run the following command in your terminal:
terraform consoleNow, try typing some function expressions directly into the prompt:
> max(5, 12, 9)
12
> split(",", "foo,bar,baz")
[
"foo",
"bar",
"baz",
]
> cidrsubnet("10.0.0.0/16", 8, 2)
"10.0.2.0/24"Type exit to leave the console. The terraform console is your best friend when debugging complex locals logic.
Troubleshooting & Common Errors#
Call to function "file" failed: no file exists at...- Root Cause: The
file()function requires the file to exist on the disk before Terraform runs. You cannot usefile()to read a file that Terraform itself is generating in the same run. - Solution: Verify the path. Always use the interpolation
${path.module}/filenameto ensure relative paths resolve correctly regardless of where the CLI is executed from.
- Root Cause: The
Error: local value cannot reference itself- Root Cause: You accidentally created a circular reference inside your
localsblock (e.g.,a = local.bandb = local.a). - Solution: Break the cycle. Locals evaluate purely based on their definitions; they cannot be recursive.
- Root Cause: You accidentally created a circular reference inside your
Conclusion & Next Steps#
You have now injected dynamic intelligence into your infrastructure code. By utilizing locals, you have made your code DRY, and by leveraging built-in functions, you can manipulate data structures effortlessly.
However, what if you need to create 5 identical EC2 instances? Copy-pasting the resource block 5 times is unacceptable.
In Episode 7: Advanced Looping Strategies, we will master the art of infrastructure iteration using the count and for_each meta-arguments.

