HCL Syntax: Blocks, Expressions & Functions
By the end of this lesson you will be able to read any Terraform file and know which parts are blocks, arguments and expressions. You will also try types, conditions, for expressions and built-in functions in terraform console, a safe playground that changes nothing.
Blocks and arguments
HCL, the HashiCorp Configuration Language, has only two main building pieces.
A block is a container with a type, zero or more labels, and a body in curly braces. An argument is a name = value line inside a block.
resource "local_file" "notice" { # block type, then two labels
filename = "notice.txt" # argument
content = "Exam on Monday" # argument
lifecycle { # a nested block, no labels
prevent_destroy = false
}
}Think of a block as a form at a bank counter. The block type is the form name ("Account Opening"), the labels fill the top boxes, and the arguments are the fields inside.
A few rules to remember:
- Names (called identifiers) may contain letters, digits, underscores and hyphens, and must not start with a digit.
- Comments start with
#or//for one line, or sit between/*and*/for many lines.#is the usual style. - The order of blocks does not matter. Terraform works out the order from references.
- Run
terraform fmtto line up the=signs and fix indentation. Never argue about spacing.
Types of values
Every value in HCL has a type:
| Type | Example | Meaning |
|---|---|---|
string | "Kochi" | Text in double quotes |
number | 42, 3.5 | Whole or decimal numbers |
bool | true, false | Yes or no |
list / tuple | ["pune", "agra"] | Ordered values, counted from 0 |
map / object | { city = "Pune", pin = 411001 } | Named values |
set | toset(["a", "b"]) | Unique values with no order |
null | null | "No value"; the argument is treated as not set |
Lists use square brackets; maps and objects use curly braces. In a map you can separate items with new lines or commas.
Strings, templates and heredocs
Inside double quotes, ${ ... } inserts the value of an expression. This is called interpolation:
content = "Hello ${var.student_name}, your roll number is ${var.roll_no}."For text with many lines, use a heredoc. <<-EOT starts it and a line with only EOT ends it. The - lets you indent the text neatly, and Terraform removes the common indentation:
content = <<-EOT
[server]
city = Lucknow
port = 8080
EOTEOT is only a convention; any word works as long as the start and end match.
References
An expression can refer to other values by name. You will use these forms constantly:
var.region: an input variable.local.common_tags: a local value.local_file.welcome.filename: an attribute of a resource (type, name, attribute).data.http.ip.response_body: an attribute of a data source.module.network.vpc_id: an output of a module.path.module: the folder of the current file.
When one resource refers to another, Terraform learns that it must create the first one before the second. This is how it builds its dependency graph, the order of work, without you writing any steps.
Try expressions in terraform console
terraform console opens an interactive prompt where you can test expressions. Run it in any folder that has been through terraform init. Nothing is created or changed.
Step 1: start the console and try simple functions.
terraform console
> upper("rohtak")
> length(["pune", "agra", "kochi"])
> join(", ", ["Pune", "Agra"])
> 75 >= 33 ? "pass" : "fail""ROHTAK" 3 "Pune, Agra" "pass"
Strings print in quotes and numbers print plainly. The last line is a conditional expression: condition ? value_if_true : value_if_false. Here a mark of 75 passes the cut-off of 33.
Step 2: try collections and a for expression.
> split(",", "Pune,Agra")
> [for c in ["pune", "agra"] : upper(c)]
> {for c in ["pune", "agra"] : c => length(c)}
> cidrsubnet("10.0.0.0/16", 8, 2)tolist([
"Pune",
"Agra",
])
[
"PUNE",
"AGRA",
]
{
"agra" = 4
"pune" = 4
}
"10.0.2.0/24"Notice what you see:
splitreturns a list, which the console shows wrapped intolist(...).- A
forexpression in square brackets builds a list from another collection, like a teacher reading out each name and writing it in capitals. - A
forexpression in curly braces with=>builds a map. Map keys are shown in alphabetical order. cidrsubnetcuts a network range into smaller ones. You will use it for cloud subnets.
Type exit (or press Ctrl+D) to leave the console.
Useful built-in functions
Terraform has many built-in functions. You cannot write your own in plain HCL, but newer providers can offer extra ones. These cover most daily work:
| Function | What it does | Example result |
|---|---|---|
lower, upper | Change case | lower("PUNE") is "pune" |
join, split | List to string and back | join("-", ["a", "b"]) is "a-b" |
length | Count items or characters | length("kochi") is 5 |
merge | Combine maps | Used for tags |
lookup | Read a map key with a default | lookup(m, "env", "dev") |
coalesce | First value that is not null or empty | Fallback values |
file, templatefile | Read a file, or fill a template file | Start-up scripts, configs |
try | First expression that does not error | Optional nested values |
toset, tolist, tomap | Convert types | Preparing for_each input |
A splat expression is a short form of a for expression. aws_instance.web[*].public_ip means "the public_ip of every instance in the list".
Common mistakes
- Single quotes. HCL strings need double quotes.
'Pune'is an error. - Commas between arguments. Arguments in a block are on separate lines with no commas.
- Mixing up lists and maps.
["a", "b"]has no keys;{ a = 1 }has no order. - Forgetting
${ }inside text."Hello var.name"prints the words literally; write"Hello ${var.name}".
Key points
- HCL is made of blocks (type, labels, body) and arguments (
name = value). - Values have types: string, number, bool, list, map, object, set and null.
${ }interpolates values into strings;<<-EOTheredocs hold many lines.- References such as
var.xandlocal_file.a.filenamealso create the dependency order. - Use conditionals,
forexpressions, splats and built-in functions instead of copying code. terraform consoleis a safe place to test any expression.
Exercise
In terraform console, write one expression that takes the list ["rohan", "isha", "farhan"] and builds a map from each name to a greeting such as "Hello ROHAN". Then write a conditional that returns "senior" when length(["rohan", "isha", "farhan"]) is more than 2, and "junior" otherwise.
Show solution
> {for n in ["rohan", "isha", "farhan"] : n => "Hello ${upper(n)}"}
> length(["rohan", "isha", "farhan"]) > 2 ? "senior" : "junior"{
"farhan" = "Hello FARHAN"
"isha" = "Hello ISHA"
"rohan" = "Hello ROHAN"
}
"senior"The curly-brace for expression with => builds a map, interpolation inserts the upper-case name, and the conditional checks the list length.
Quick Quiz
1. Which line is valid HCL inside a resource block?
Arguments use =, double quotes and no trailing comma.
2. What does [for c in var.cities : upper(c)] produce?
Square brackets around a for expression build a list.
3. How does Terraform decide the order in which to create resources?
When one resource refers to another, Terraform creates the referenced one first.
Learn Terraform & Ansible with a mentor
Free lessons get you started. Our Cloud & DevOps training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
