So far, we have defined fixed values. But the real power of Terraform comes from connecting things together and transforming values. For that, we use expressions, references, and functions. With them, your infrastructure stops being a static list and becomes dynamic and intelligent.

References: connecting resources to each other

A reference is the way to use the value of a resource (or variable) elsewhere. It’s what allows resources to connect.

The general syntax to refer to a resource’s attribute is:

resource_type.name.attribute

Example: You have a VPC and want to create a subnet inside it. The subnet needs to know the VPC’s ID. Instead of copying the ID by hand, you reference it:

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
}

resource "aws_subnet" "public" {
  vpc_id     = aws_vpc.main.id   # ← reference to the VPC ID
  cidr_block = "10.0.1.0/24"
}

Here, aws_vpc.main.id means: “the id attribute of the aws_vpc resource I called main.”

Why this is great: You don’t need to know the VPC’s ID in advance (AWS assigns it when creating it). Terraform resolves it automatically. And here something very important happens...

Implicit dependencies

When a resource references another, Terraform automatically understands that there is a dependency: “the subnet needs the VPC, so I’ll create the VPC first.”

This is the magic of the declarative approach (subchapter 9.2): you don’t specify the order; Terraform deduces it from the references and builds a dependency graph to create everything in the correct order.

Remember the taxi analogy: you declare what you want and how things connect; Terraform calculates the creation order. References are what tell it “this depends on this.”

The different references you already know:

  • var.name → a variable (subchapter 10.1).
  • local.name → a local.
  • aws_vpc.main.id → an attribute of a resource.

Expressions: combining and calculating values

An expression is anything that produces a value: a literal value, a reference, an operation, or a combination.

String interpolation

You can insert values inside text using ${ }:

resource "aws_instance" "web" {
  # ...
  tags = {
    Name = "server-${var.environment}-web"
  }
}

If var.environment is "prod", the resulting name will be "server-prod-web". This is called interpolation: putting the value of a variable inside a text.

Operations

You can do math operations, comparisons, and logic:

amount = var.base_amount + 2        # addition
is_prod  = var.environment == "prod"        # comparison (returns true/false)

Built-in functions: transforming values

Terraform includes built-in functions to transform and manipulate values. They’re like spreadsheet functions. You can’t create new functions, but the ones available cover almost everything you need.

The syntax is function_name(arguments). Let’s look at the most useful ones grouped by type:

Text functions

upper("hello")              # → "HELLO"
lower("HELLO")              # → "hello"
"${var.project}-${var.environment}"   # combine texts
trimspace("  hello  ")      # → "hello" (removes spaces)

Collection functions (lists and maps)

length(["a", "b", "c"])    # → 3 (number of elements)
element(var.list, 0)       # → the first element
concat(list1, list2)       # → joins two lists
lookup(var.map, "key", "default_value")  # looks up in a map

Numeric functions

max(5, 10, 3)              # → 10
min(5, 10, 3)              # → 3

Functions widely used in infrastructure

cidrsubnet("10.0.0.0/16", 8, 1)   # automatically calculates subnets
file("script.sh")                  # reads the contents of a file
jsonencode({ key = "value" })      # converts to JSON format

cidrsubnet is very handy: it calculates subnet ranges from the VPC range, so you don’t have to do CIDR math by hand (see Chapter 6). For example, it automatically divides your 10.0.0.0/16 into ordered subnets.

Don’t memorize all the functions. There are dozens. The important thing is to know that they exist and that, when you need to transform a value, there’s almost certainly a function for it. The Terraform documentation lists them all.

An example that brings it all together

variable "project" {
  type    = string
  default = "store"
}

variable "azs" {
  type    = list(string)
  default = ["eu-west-1a", "eu-west-1b"]
}

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
  tags = {
    Name = "${var.project}-vpc"        # interpolation
  }
}

resource "aws_subnet" "public" {
  vpc_id            = aws_vpc.main.id              # reference (dependency)
  cidr_block        = cidrsubnet(aws_vpc.main.cidr_block, 8, 1)  # function
  availability_zone = element(var.azs, 0)              # function on list
  tags = {
    Name = upper("${var.project}-public")            # function + interpolation
  }
}

This code: names the VPC with interpolation, creates a subnet that references the VPC (creating the dependency), calculates its CIDR with a function, picks a zone from the list with another function, and puts the name in uppercase. Everything connected and dynamic.

What you should remember

  • References (aws_vpc.main.id, var.x, local.y) connect resources and values to each other.
  • When a resource references another, Terraform automatically deduces the dependency and the creation order (you don’t specify it).
  • Expressions combine and calculate values: interpolation ${...} inserts values into texts; there are also math and logic operations.
  • Built-in functions transform values (upper, length, lookup, cidrsubnet, file…). You can’t create new functions, but there are many built-in.
  • Don’t memorize functions: remember they exist and look them up when you need them.

In the last HCL subchapter, we’ll see how to create multiple resources at once and make decisions with conditionals and loops (count, for_each, for).

Cloud, AWS & Terraform — From Zero to Expert

Chapter 1 · What is cloud computing

Chapter 2 · The cloud market and major providers

Chapter 3 · Regions, availability zones and edge

Chapter 4 · Compute: EC2

Chapter 5 · Storage: S3

Chapter 6 · Networking: VPC

Chapter 7 · Identity and access: IAM

Chapter 8 · Managed databases

Chapter 9 · Why Infrastructure as Code

Chapter 10 · HCL: the Terraform language

Chapter 11 · Providers and state

Chapter 12 · Your first real infrastructure in Terraform

Chapter 13 · Load balancing and auto scaling

Chapter 14 · Serverless with Lambda

Chapter 15 · Messaging and events

Chapter 16 · Content delivery and DNS

Chapter 17 · Containers on AWS

Chapter 18 · Modules: reuse and composition

Chapter 19 · Workspaces and environment management

Chapter 20 · Remote backends and locking

Chapter 21 · Infrastructure testing

Chapter 22 · Terraform in CI/CD

Chapter 23 · Defense in depth

Chapter 24 · Observability: logs, metrics and traces

Chapter 25 · Cost optimization

Chapter 26 · High availability and disaster recovery

Chapter 27 · AWS Well-Architected Framework

Chapter 28 · Serverless architectures at scale

Chapter 29 · Data platforms on AWS

Chapter 30 · Multi-account and landing zones

Chapter 31 · Platform Engineering and Internal Developer Platform

Chapter 32 · Relevant AWS certifications

Chapter 33 · Projects to consolidate what you've learned

Chapter 34 · Resources and community

© Copyright 2024. All rights reserved