Terraform Expressions
Introduction
Terraform expressions allow you to reference, combine, and manipulate values within your Terraform configuration files. Expressions are a fundamental part of making your infrastructure code dynamic and flexible. In this guide, we'll explore different types of expressions in Terraform, how they work, and how you can use them to create more powerful and adaptable infrastructure configurations.
Expressions appear within interpolation sequences (${ ... }
), inside template strings, and in various other contexts. They're a core part of Terraform's HCL (HashiCorp Configuration Language) that helps you avoid hardcoding values and instead make your configurations more dynamic.
Types of Terraform Expressions
Literal Expressions
The simplest form of expressions are literal values:
resource "aws_instance" "example" {
instance_type = "t2.micro" # String literal
count = 5 # Number literal
monitoring = true # Boolean literal
}
Reference Expressions
Reference expressions allow you to use values defined elsewhere in your configuration:
resource "aws_instance" "web" {
ami = "ami-a1b2c3d4"
instance_type = "t2.micro"
tags = {
Name = "Web Server"
}
}
resource "aws_eip" "ip" {
instance = aws_instance.web.id # Reference expression
}
In the example above, aws_instance.web.id
is a reference expression that accesses the id
attribute of the aws_instance
resource named "web".
Variable Expressions
You can reference input variables using the var.
prefix:
variable "instance_type" {
description = "EC2 instance type"
default = "t2.micro"
}
resource "aws_instance" "app" {
ami = "ami-a1b2c3d4"
instance_type = var.instance_type # Variable expression
}
Interpolation Syntax
When you need to embed expressions within strings, you use the interpolation syntax with ${...}
:
resource "aws_instance" "server" {
ami = "ami-a1b2c3d4"
instance_type = "t2.micro"
tags = {
Name = "Server-${var.environment}" # String interpolation
}
}
For Expressions
For expressions create collections by transforming another collection:
variable "users" {
default = ["alice", "bob", "charlie"]
}
# Create IAM users for each name in the list
resource "aws_iam_user" "example" {
for_each = toset(var.users)
name = each.key
}
# Extract user ARNs into a list
output "user_arns" {
value = [for user in aws_iam_user.example : user.arn] # For expression
}
Input:
var.users = ["alice", "bob", "charlie"]
Output:
user_arns = [
"arn:aws:iam::123456789012:user/alice",
"arn:aws:iam::123456789012:user/bob",
"arn:aws:iam::123456789012:user/charlie"
]
Conditional Expressions
You can use conditional expressions to choose between two values based on a condition:
variable "environment" {
default = "development"
}
resource "aws_instance" "server" {
ami = "ami-a1b2c3d4"
instance_type = var.environment == "production" ? "m5.large" : "t2.micro"
# Conditional expression: condition ? true_value : false_value
}
In this example, if var.environment
equals "production", the instance type will be "m5.large"; otherwise, it will be "t2.micro".
Splat Expressions
Splat expressions are a concise way to extract a specific attribute from all items in a list:
resource "aws_instance" "cluster" {
count = 3
ami = "ami-a1b2c3d4"
instance_type = "t2.micro"
}
output "private_ips" {
value = aws_instance.cluster[*].private_ip # Splat expression
}
The splat expression [*]
is equivalent to writing:
output "private_ips" {
value = [for instance in aws_instance.cluster : instance.private_ip]
}
Working with Complex Expressions
String Templates and Heredoc Syntax
For multi-line strings, you can use the heredoc syntax:
resource "aws_iam_policy" "example" {
name = "example-policy"
description = "Example IAM policy"
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "ec2:*",
"Effect": "Allow",
"Resource": "*"
}
]
}
EOF
}
For more complex string interpolation, you can use the %{...}
syntax inside strings:
variable "items" {
default = ["apple", "banana", "orange"]
}
output "items_message" {
value = "You have %{if length(var.items) == 0}no items%{else}${length(var.items)} items: ${join(", ", var.items)}%{endif}."
}
Dynamic Blocks with Expressions
You can use expressions with dynamic blocks to generate repeated nested blocks:
variable "ingress_rules" {
default = [
{
port = 80
description = "HTTP"
protocol = "tcp"
},
{
port = 443
description = "HTTPS"
protocol = "tcp"
}
]
}
resource "aws_security_group" "web" {
name = "web-sg"
description = "Web server security group"
dynamic "ingress" {
for_each = var.ingress_rules
content {
description = ingress.value.description
from_port = ingress.value.port
to_port = ingress.value.port
protocol = ingress.value.protocol
cidr_blocks = ["0.0.0.0/0"]
}
}
}
Operators in Expressions
Terraform supports various operators for combining and manipulating values:
Arithmetic Operators
locals {
base_storage = 50
extra_storage = var.environment == "production" ? 100 : 20
total_storage = local.base_storage + local.extra_storage # Addition
}
Supported arithmetic operators include:
+
(addition)-
(subtraction)*
(multiplication)/
(division)%
(modulo)
Comparison Operators
resource "aws_instance" "example" {
count = var.instance_count > 0 ? var.instance_count : 1 # Comparison
# ...
}
Supported comparison operators include:
==
(equal)!=
(not equal)>
(greater than)>=
(greater than or equal)<
(less than)<=
(less than or equal)
Logical Operators
locals {
create_instance = var.enabled && var.instance_count > 0 # Logical AND
use_custom_ami = var.custom_ami != "" || var.use_latest # Logical OR
}
Terraform supports:
&&
(logical AND)||
(logical OR)!
(logical NOT)
Real-world Examples
Example 1: Dynamic AWS VPC Configuration
variable "environment" {
description = "Deployment environment"
default = "development"
}
variable "vpc_cidr" {
description = "CIDR block for the VPC"
default = "10.0.0.0/16"
}
locals {
env_suffix = var.environment == "production" ? "prod" : "dev"
# Calculate subnet CIDR blocks based on the VPC CIDR
subnet_cidrs = [
cidrsubnet(var.vpc_cidr, 8, 0),
cidrsubnet(var.vpc_cidr, 8, 1),
cidrsubnet(var.vpc_cidr, 8, 2),
]
common_tags = {
Environment = var.environment
Project = "terraform-tutorial"
ManagedBy = "terraform"
}
}
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
tags = merge(
local.common_tags,
{
Name = "vpc-${local.env_suffix}"
}
)
}
resource "aws_subnet" "public" {
count = length(local.subnet_cidrs)
vpc_id = aws_vpc.main.id
cidr_block = local.subnet_cidrs[count.index]
tags = merge(
local.common_tags,
{
Name = "public-subnet-${count.index + 1}-${local.env_suffix}"
Type = "Public"
}
)
}
In this example:
- We use variables and local values to make our configuration flexible
- The
cidrsubnet()
function is used with expressions to calculate subnet ranges - We use the
merge()
function to combine tag maps - String interpolation creates resource names that include the environment suffix
- The count meta-argument uses the
length()
function to determine how many subnets to create
Example 2: Conditional Resource Creation
variable "create_database" {
description = "Whether to create a database"
type = bool
default = true
}
variable "db_name" {
description = "Name of the database"
type = string
default = "app-db"
}
resource "aws_db_instance" "database" {
count = var.create_database ? 1 : 0
identifier = "${var.db_name}-${terraform.workspace}"
engine = "postgres"
instance_class = "db.t3.micro"
allocated_storage = 20
# Other DB configuration...
tags = {
Name = var.db_name
Environment = terraform.workspace
}
}
# The database endpoint will be empty if no database is created
output "database_endpoint" {
value = try(aws_db_instance.database[0].endpoint, "No database created")
}
In this example:
- We use a conditional expression with
count
to conditionally create the database - The
terraform.workspace
value is used to namespace resources by workspace - The
try()
function handles the case where the database might not exist
Summary
Terraform expressions are a powerful feature that allows you to create dynamic, flexible, and reusable infrastructure configurations. Here's what we've covered:
- Literal values (strings, numbers, booleans)
- References to resources and their attributes
- Variable expressions with
var.
- String interpolation with
${...}
- For expressions for transformation and filtering
- Conditional expressions with the ternary operator
- Splat expressions for concise attribute access
- Complex string templates and heredoc syntax
- Dynamic blocks for generating repeated configurations
- Various operators for combining and manipulating values
- Real-world examples showing practical applications
By mastering Terraform expressions, you can write more concise, maintainable, and flexible infrastructure as code. Expressions allow you to reduce repetition, make your configurations more dynamic, and create reusable modules that can adapt to different environments and requirements.
Additional Resources
Here are some exercises to help you practice working with Terraform expressions:
-
Exercise 1: Create a configuration that generates a security group with ingress rules for different ports specified in a variable.
-
Exercise 2: Write a module that creates S3 buckets with names and tags that are dynamically generated based on input variables.
-
Exercise 3: Create a configuration that conditionally creates different types of EC2 instances based on an "environment" variable.
-
Exercise 4: Write a configuration that uses
for_each
to create multiple IAM users with different permission policies. -
Exercise 5: Create a dynamic configuration that generates different subnet configurations based on the size of the VPC CIDR block.
For further learning, check out:
- The Terraform Language Documentation
- HashiCorp's Learn Terraform platform
- The Terraform Registry for module examples that use expressions effectively
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)