If you're coming to Terraform from Python, JavaScript, or Ruby, HCL can feel uncanny: familiar enough to read, strange enough to second-guess.
You see list-like comprehensions, Ruby-ish blocks, and syntax that looks a bit like assignment without really behaving like a programming language. That is not accidental. HCL is a hybrid—a configuration language designed to be readable by humans and useful for describing infrastructure, relationships, and constraints.
The mental shift that helps most is this:
You are not writing a script that runs top to bottom. You are declaring a desired shape of infrastructure, and Terraform turns that declaration into a dependency graph, a plan, and an execution strategy.
Once that clicks, most of HCL's weirdness starts to make sense.
1. It's a graph, not a script
In Python or JavaScript, source order usually matters because execution is fundamentally sequential. In Terraform, block order does not define execution order. References between objects—and, when needed, explicit depends_on edges—do.
That is why you can scatter related resources across multiple .tf files without teaching Terraform what runs first. Terraform builds a dependency graph from the configuration and uses that graph to generate a plan and sequence operations.
That does not mean every fact is always known before apply. In many cases Terraform can resolve values during planning, but some data sources may be deferred until apply if their inputs are not known yet.
So the right mental model is not “top to bottom.” It is “describe the relationships, then let Terraform walk the graph.”
2. HCL is built from arguments and blocks
The most important syntax distinction in Terraform is not really about operators. It is about arguments and blocks.
resource "aws_instance" "web" {
ami = "ami-1234567890" # argument
instance_type = "t3.micro" # argument
tags = { # argument whose value is an object
Name = "web"
}
ebs_block_device { # nested block
device_name = "/dev/sda1"
volume_size = 50
}
}
Arguments assign values to names. Blocks are containers for more configuration and usually represent schema-defined structure.
They can look similar when you're new to HCL, but they are not interchangeable. Understanding that explains a lot of Terraform's “why does this parse but that doesn't?” moments.
It also explains why dynamic blocks exist. Expressions can compute argument values directly, but nested blocks are structural, so Terraform needs a dedicated construct to generate them.
3. The expression language is where HCL feels Pythonic
Inside argument values, HCL has a compact expression language: functions, conditionals, indexing, attribute access, and for expressions.
This is the part that often feels most familiar to developers coming from general-purpose languages.
subnet_ids = [for s in aws_subnet.private : s.id]
subnet_map = {
for s in aws_subnet.private : s.tags["Name"] => s.id
}
for expressions are one of the cleanest parts of the language. Square brackets produce a tuple/list-like result. Curly braces produce an object/map-like result. In object for expressions, => maps a computed key to a computed value.
There is also only one real inline conditional form:
var.enabled ? 1 : 0
That is deliberate. HCL gives you enough expression power to transform data, but it stops well short of becoming a full programming language with arbitrary control flow.
4. Instance identity matters more than iteration
If you want to understand why Terraform sometimes feels fussy, look at for_each.
For resources and modules, for_each accepts a map or a set of strings. Terraform identifies each instance by the map key or set member. That is the real reason toset() shows up so often: it lets you create instance addresses based on stable values instead of numeric positions.
locals {
environments = toset(["dev", "staging", "prod"])
}
resource "aws_s3_bucket" "env" {
for_each = local.environments
bucket = "my-app-${each.key}"
}
This is safer than list-indexed identity, but it is not magic.
Reordering the original list before converting it to a set does not matter. Changing the key does matter, because Terraform uses that key as the instance identity. If you intentionally rename or move an object address, moved blocks are the right way to preserve that refactor.
5. State is the hidden half of the model
Terraform is declarative, but it is not stateless.
It persists state so it can map configuration objects to real infrastructure, track metadata, and make planning practical at scale. Terraform then compares your configuration with its state and the existing infrastructure to decide what needs to change.
That is why HCL alone is never the whole story. Two identical-looking .tf files can behave differently depending on what Terraform already believes it manages.
This is also why backends, locking, drift, imports, and refactors matter so much operationally: they are all state-management problems wearing different costumes.
6. Where the chimera gets awkward
HCL is excellent at describing structured infrastructure. It is much worse at acting like a general-purpose language.
String manipulation gets unreadable quickly. Branching is intentionally limited. dynamic blocks solve a real problem but can become cryptic fast.
And while Terraform's validation and testing story is better than it used to be—you now have variable validation, preconditions, postconditions, check blocks, and terraform test—it still feels different from application development. Complex module testing can still be heavier and less ergonomic than writing ordinary unit tests in an application language.
That is not a flaw so much as a boundary. HCL works best when you let it describe infrastructure shape, identity, and dependency, and move business logic or heavy data wrangling elsewhere.
Conclusion
HCL feels hybrid because it is.
It borrows just enough from programming languages to be expressive, but it stays rooted in configuration: blocks, arguments, expressions, and graphs.
Once you stop reading it like a script and start reading it like a human-friendly language for declaring structure and dependency, most of its weirdness starts to look intentional.
And that is usually the moment Terraform clicks.
Top comments (0)