HCL (HashiCorp Configuration Language) is the language used to write Terraform configurations. It's designed to be readable by humans and machines, with a syntax that's more concise than JSON/YAML while still being structured and parseable.
Basic Syntax
HCL files have a .tf extension. A Terraform project can have multiple .tf files — Terraform merges them all before processing.
# This is a comment
# A block has a type, optional labels, and a body
resource "aws_s3_bucket" "my_bucket" {
bucket = "my-unique-bucket-name"
tags = {
Environment = "production"
Team = "platform"
}
}
The structure is: block_type "label1" "label2" { ... }
Block Types
| Block | Purpose |
|---|---|
| terraform | Configure Terraform itself (required version, backend, providers) |
| provider | Configure a provider (credentials, region) |
| resource | Declare a cloud resource to manage |
| data | Read existing resources without managing them |
| variable | Declare an input parameter |
| output | Export a value after apply |
| locals | Compute intermediate values |
| module | Call a reusable module |
Data Types
variable "examples" {
# Primitive types
string_val = "hello"
number_val = 42
bool_val = true
# Collection types
list_val = ["a", "b", "c"]
set_val = toset(["x", "y", "z"])
map_val = { key1 = "value1", key2 = "value2" }
# Complex types
object_val = {
name = "web"
port = 80
}
}
Expressions and References
# Reference another resource's attribute
resource "aws_subnet" "example" {
vpc_id = aws_vpc.main.id # references aws_vpc.main resource
cidr_block = "10.0.1.0/24"
}
# String interpolation
resource "aws_instance" "web" {
tags = {
Name = "${var.env}-web-server" # interpolation
}
}
# Conditional expression
instance_type = var.env == "production" ? "t3.large" : "t3.micro"
# For expression (creates a list/map from another collection)
locals {
instance_names = [for i in var.instances : "${var.prefix}-${i}"]
}
Built-in Functions
Terraform provides many built-in functions. You cannot define your own, but the built-ins cover most needs:
locals {
# String functions
upper_env = upper(var.env) # "PRODUCTION"
trimmed = trimspace(" hello ") # "hello"
formatted = format("app-%s-%d", "web", 1) # "app-web-1"
# Collection functions
combined = concat(var.list_a, var.list_b)
joined = join(",", var.tags) # "a,b,c"
unique_list = distinct(var.ids)
# Map functions
value = lookup(var.region_map, "us-east-1", "default")
keys_list = keys(var.tags_map)
# Type conversion
as_set = toset(var.my_list)
as_list = tolist(var.my_set)
# Numeric
max_val = max(5, 10, 3) # 10
ceil_val = ceil(1.2) # 2
}
Locals
locals blocks define intermediate computed values — like variables but derived from other values rather than input:
locals {
common_tags = {
Project = var.project_name
Environment = var.environment
ManagedBy = "terraform"
}
name_prefix = "${var.project_name}-${var.environment}"
}
resource "aws_instance" "web" {
tags = local.common_tags # reference local values
}
Count and for_each
Create multiple resources without copy-pasting:
# count: create N copies
resource "aws_iam_user" "devs" {
count = length(var.dev_names)
name = var.dev_names[count.index]
}
# for_each: create one per map key (preferred — avoids ordering issues)
resource "aws_s3_bucket" "logs" {
for_each = toset(["access-logs", "app-logs", "audit-logs"])
bucket = "${var.prefix}-${each.value}"
}
Prefer for_each over count for resources that may be added or removed mid-list, as count uses index-based addressing and removing an element can cause unintended resource replacements.
Next: Providers and Resources — the core building blocks of any Terraform configuration.