Terraform uses HCL (HashiCorp Configuration Language). The full language reference is at developer.hashicorp.com/terraform/language.
The two building blocks
Everything in HCL is either an argument or a block (docs).
Arguments assign a value to a name:
zone = "us-central1-a"Blocks group related arguments under a keyword, with optional labels:
resource "google_compute_instance" "vm" {
# arguments and nested blocks go here
}The pattern is: block_type "label_1" "label_2" { ... }. Not all block types take labels — terraform {} takes none, resource takes two (type + local name), variable takes one (name).
Block types you’ll encounter
| Block | Purpose |
|---|---|
resource | Creates and manages an infrastructure object |
data | Reads existing infrastructure — no create or modify |
variable | Declares an input the caller must provide |
output | Exposes a value for display or for parent modules to consume |
locals | Computes intermediate values for reuse within the module |
module | Calls a child module (a folder of .tf files) |
provider | Configures the cloud provider (credentials, region, etc.) |
Reference patterns
Values are referenced by a dotted path. The type of the first segment tells you what you’re reading (docs):
| Reference | Reads from |
|---|---|
var.zone | Input variable named zone |
google_compute_instance.vm.id | id attribute of a resource block labelled vm |
data.google_compute_zones.available.names | names attribute of a data block |
local.project_prefix | A locals block value |
module.network.subnet_id | Output subnet_id exported by the module "network" call |
data vs resource
A resource block creates something. A data block only reads something that already exists — Terraform makes no changes to it (docs):
# creates a VM
resource "google_compute_instance" "vm" { ... }
# reads available zones — nothing is created
data "google_compute_zones" "available" {
region = var.region
}locals
locals lets you name a computed expression and reuse it, avoiding repetition (docs):
locals {
name_prefix = "${var.project}-${var.env}"
}
resource "google_compute_instance" "vm" {
name = "${local.name_prefix}-vm"
}Meta-arguments
Every resource block accepts a set of meta-arguments that control its lifecycle (docs):
| Meta-argument | What it does |
|---|---|
depends_on | Forces Terraform to complete another resource before this one, even without an implicit reference |
count | Creates N identical copies of the resource |
for_each | Creates one resource per entry in a map or set |
lifecycle | Controls create/destroy behaviour (see below) |
Common lifecycle rules:
lifecycle {
create_before_destroy = true # spin up replacement before destroying old
prevent_destroy = true # block any apply that would destroy this resource
ignore_changes = [tags] # don't drift-detect changes to these attributes
}Modules
A module block calls a child module — a separate folder of .tf files. The label in quotes is a local name you assign to that call:
module "network" {
source = "./modules/network"
zone = var.zone
}"network"— local label, arbitrary; used to reference the module’s outputs asmodule.network.<output>source— path to the folder containing the child module’s.tffiles- everything else — input variables passed into the child
The same module can be called multiple times under different labels to create separate instances:
module "network_us" { source = "./modules/network"; zone = "us-central1-a" }
module "network_eu" { source = "./modules/network"; zone = "europe-west1-b" }Variable scoping: child modules do not inherit the parent’s variables. The child must declare its own variables.tf, and values must be passed explicitly. Nothing leaks in automatically (docs).
See also
- terraform-variables — variables.tf / tfvars / var.<name> in detail
- gcp-vm-terraform-gotchas — practical example using these concepts