HCL Syntax Basics
HashiCorp Configuration Language is the declarative syntax every Terraform configuration is written in. On the surface it reads like structured data — labelled blocks with name = value arguments inside — but it behaves like a small language underneath: you can interpolate values, read one resource's attribute from inside another, and let Terraform infer the order of operations from those references rather than writing it out.
This topic establishes the shape of HCL, not the depth. Conditionals, for expressions, and the function library each get their own chapter; what you need now is the vocabulary — blocks, arguments, types, references — and one idea that trips up newcomers from every other tool: how Terraform treats the files in a directory and how it works out ordering.
Blocks and Arguments
A block is a labelled container. resource, provider, variable, output, and module are the block types you meet first, and most take one or two labels after the type — resource "google_storage_bucket" "raw" names a resource type and a local name. Inside the braces, name = value arguments and nested blocks describe the thing.
resource "google_storage_bucket" "raw" { # type + local name name = "hatch-events-raw" # a string argument location = "US" versioning { # a nested block enabled = true # a bool argument } }
The two strings after resource matter in different ways. The first, google_storage_bucket, tells Terraform which provider and API to use. The second, raw, is a label only you and your other configuration use — it is how you address this bucket elsewhere, and it never appears in GCP.
Types and Literals
HCL values are typed: strings, numbers, bools, and the collection types — lists like ["a", "b"], maps like { env = "dev" }, and objects with named, mixed-type fields. Terraform infers the type from how an argument is used and from the provider's schema, so you rarely declare a type by hand outside of variable definitions.
Strings can carry interpolation with "${...}", which splices an expression into the middle of text — useful when you are genuinely building a string like "hatch-${var.env}-raw". For a value that is just one expression, the modern style drops the wrapper: write google_storage_bucket.raw.name, not "${google_storage_bucket.raw.name}". The interpolated form is for assembling text, not for every reference.
References and Dependencies
A reference reads an attribute of another object: google_storage_bucket.raw.name is the name attribute of the raw bucket, and var.region is the value of a variable. References are how data flows through a configuration — one resource's output becomes another's input without you copying a literal value between them.
Those same references are how Terraform learns ordering. When a Pub/Sub notification references google_storage_bucket.raw.name, Terraform reads that as "the bucket must exist before the notification" and builds the bucket first — automatically, with no instruction from you. You express dependencies by referencing attributes, and the graph falls out of the references. This is the single most important idea in the chapter, and the rest of the course leans on it.
How Terraform Merges Files
Terraform loads every .tf file in a directory and treats them as one merged configuration — file boundaries carry no meaning, and neither does file order. A resource in buckets.tf can reference a variable defined in variables.tf that appears alphabetically later, and it resolves fine, because Terraform reads all the files first and then works out order from references rather than from where things sit on disk.
That frees you to split by role for readability — the common convention is main.tf for resources, variables.tf for inputs, and outputs.tf for outputs. The split is purely for humans; Terraform reassembles the pieces into one configuration regardless. Expecting file position to control ordering is a habit carried over from imperative tools, and it does not apply here.
Comments, Formatting, and fmt
Comments use # for a single line (or //, which is equivalent) and /* ... */ for a block. Beyond comments, HCL has one canonical style — consistent indentation, aligned = signs, predictable spacing — and terraform fmt rewrites your files into it in place.
Run fmt on save, or at minimum before every commit, so formatting never shows up as noise in a code review. Hand-formatting HCL is wasted effort and a source of pointless diffs; let the tool own style so reviews are about what changed in meaning, not in whitespace. With the language's shape in hand, the next chapter turns to the part that is genuinely particular to Google Cloud — the provider, projects, and APIs.
- Splitting configuration across files and assuming order matters — Terraform loads every
.tfin the directory and resolves order from references, not file position, so this misconception produces wrong fixes for ordering bugs. - Hand-formatting HCL instead of running
terraform fmt— inconsistent indentation and spacing creates whitespace noise in every diff and buries the real change. - Wrapping every reference in
"${...}"where a bare expression is clearer — modern HCL prefersbucket.nameover"${bucket.name}", and the wrapper only belongs when building a string. - Adding an explicit
depends_onfor a dependency Terraform could infer from a reference — it adds noise and hides the real relationship the reference already expressed. - Confusing the resource's local name with its real GCP name — the second label after
resourceis an internal handle and never appears in Google Cloud, while thenameargument is what GCP actually sees.
- Reference attributes between resources to express dependencies instead of hardcoding values, so Terraform infers the correct order from the reference graph.
- Run
terraform fmt, ideally on save, so every file is in canonical style and reviews focus on meaning rather than whitespace. - Split configuration into
main.tf,variables.tf, andoutputs.tfby role for readability, knowing Terraform merges every.tfregardless of file boundaries. - Prefer bare expressions over
"${...}"interpolation except where you are genuinely assembling a string from parts. - Reserve
depends_onfor true dependencies Terraform cannot see, letting references carry every relationship they can express on their own.
Knowledge Check
How does Terraform determine the order in which it creates resources?
- From the references between them — referencing one resource's attribute in another fixes the order
- From the order the resource blocks happen to appear in the file, reading strictly from top to bottom
- Alphabetically by each resource's local name within the configuration
- From an explicit numeric
orderargument set on each resource block
You define a variable in variables.tf and reference it from main.tf. Does file order matter?
- No — Terraform merges every
.tfin the directory into one configuration and orders from references - Yes — the file declaring the variable must sort alphabetically ahead of any file that references it
- Yes — variables have to be defined in the very same file that references them
- No — but only if you list the files in a specific load order on the CLI
When is the interpolation form "${...}" the right choice over a bare expression?
- When you are assembling a string from parts, like
"hatch-${var.env}-raw" - Always — bare expressions have been deprecated since HCL2 and warn
- Only inside
outputblocks, where it is the one supported form - Whenever you reference another resource's attribute anywhere in the config
What is the downside of adding depends_on for a dependency a reference already expresses?
- It adds noise and can hide the real relationship the reference already captured
- It makes every
applyin the working directory run roughly twice as slowly by default - It prevents Terraform from parsing and loading the rest of the file
- It silently deletes the referenced resource on apply
You got correct