Skip to main content

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:

hcl
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:

hcl
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:

hcl
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 ${...}:

hcl
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:

hcl
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:

hcl
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:

hcl
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:

hcl
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:

hcl
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:

hcl
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:

hcl
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

hcl
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

hcl
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

hcl
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

hcl
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:

  1. We use variables and local values to make our configuration flexible
  2. The cidrsubnet() function is used with expressions to calculate subnet ranges
  3. We use the merge() function to combine tag maps
  4. String interpolation creates resource names that include the environment suffix
  5. The count meta-argument uses the length() function to determine how many subnets to create

Example 2: Conditional Resource Creation

hcl
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:

  1. We use a conditional expression with count to conditionally create the database
  2. The terraform.workspace value is used to namespace resources by workspace
  3. 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:

  1. Exercise 1: Create a configuration that generates a security group with ingress rules for different ports specified in a variable.

  2. Exercise 2: Write a module that creates S3 buckets with names and tags that are dynamically generated based on input variables.

  3. Exercise 3: Create a configuration that conditionally creates different types of EC2 instances based on an "environment" variable.

  4. Exercise 4: Write a configuration that uses for_each to create multiple IAM users with different permission policies.

  5. 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:



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)