This chapter follows Getting started with Terraform on Google Cloud. HCL is the default syntax for .tf files; Terraform also accepts JSON with the same semantics in .tf.json files.
Blocks, labels, and arguments
HCL is built from blocks. A block has a type, optional labels in quotes, and a body in { }:
resource "google_storage_bucket" "logs" { name = "my-project-logs" location = "EU"}Here resource is the block type; "google_storage_bucket" and "logs" are labels (resource type and local name); name and location are arguments assigned with =.
Some blocks use one label (variable "region"), two (resource "google_compute_network" "vpc"), or none (terraform { }). Arguments can be literals, references, or expressions. Attributes you read from other objects (for example google_compute_network.vpc.id) appear on the right side of assignments or in expressions.
Core block types
terraform — Settings for Terraform itself: required_version, required_providers, and backend configuration.
provider — Configures a plugin (region, project, credentials). Alias multiple providers when you manage resources in more than one project or region.
resource — Managed infrastructure objects Terraform creates, updates, and destroys.
data — Read-only lookups. Data sources never appear in the destroy path as managed resources, but they can feed resource arguments.
variable — Input parameters with optional type, default, validation, and sensitivity.
locals — Named expressions for reuse within a module; not set from outside the module.
output — Values exported after apply, optionally marked sensitive.
Example combining several block types:
data "google_project" "current" {}
resource "google_storage_bucket" "artifacts" { name = "${data.google_project.current.project_id}-artifacts" location = var.region}
output "artifacts_bucket" { value = google_storage_bucket.artifacts.name}Types and expressions
Terraform’s type system includes strings, numbers, booleans, lists, maps, objects, and tuples. Variables and outputs can constrain types:
variable "labels" { type = map(string) default = { env = "dev" }}References tie resources together: google_compute_subnetwork.subnet.id creates an implicit dependency on the subnetwork.
Conditionals use condition ? true_val : false_val. For expressions build lists or maps:
locals { zones = ["${var.region}-a", "${var.region}-b"] bucket_names = { for k, v in var.labels : k => "${v}-bucket" }}Built-in functions (merge, jsonencode, cidrsubnet, try, and many others) transform values. Prefer functions and locals over duplicating literal strings.
Input variables and outputs
Variables accept values from defaults, tfvars files, environment variables (TF_VAR_name), and -var / -var-file on the CLI. Precedence increases toward command-line flags and explicit -var assignments over tfvars and defaults.
Use validation blocks inside variable to reject bad input early:
variable "region" { type = string
validation { condition = can(regex("^europe-", var.region)) error_message = "This example expects a europe-* region." }}Outputs expose values to humans, CI, or other Terraform configurations via remote state. Mark secrets with sensitive = true so Terraform redacts them in logs (they may still exist in state).
Meta-arguments
Meta-arguments change how a resource block behaves:
| Meta-argument | Purpose |
|---|---|
count |
Create N instances indexed by count.index |
for_each |
Create one instance per map or set key |
depends_on |
Force ordering when no attribute reference exists |
provider |
Select a non-default provider configuration |
lifecycle |
create_before_destroy, prevent_destroy, ignore_changes, and related rules |
for_each is usually preferable to count when instances are identified by stable keys:
variable "buckets" { type = set(string) default = ["logs", "backups"]}
resource "google_storage_bucket" "by_name" { for_each = var.buckets name = "${var.project_id}-${each.key}" location = var.region}lifecycle { prevent_destroy = true } blocks destroy for critical resources; combine with review in production modules.
Dependencies and the graph
When resource A references an attribute of resource B, Terraform orders B before A. That is an implicit dependency.
Use explicit depends_on only when ordering matters but no attribute is shared—for example a bucket policy that must exist after an IAM binding created elsewhere:
resource "google_storage_bucket_iam_member" "reader" { bucket = google_storage_bucket.logs.name role = "roles/storage.objectViewer" member = "serviceAccount:${google_service_account.viewer.email}"}Terraform builds a directed acyclic graph from these edges, then walks it during plan and apply. Cycles in dependencies produce a clear error at plan time.
File layout and JSON syntax
There is no required filename beyond the .tf extension; conventions help reviewers:
| File | Typical contents |
|---|---|
versions.tf |
terraform block, required_providers, backend |
providers.tf |
provider blocks |
variables.tf |
variable blocks |
locals.tf |
locals blocks |
main.tf |
Primary resource and data blocks |
outputs.tf |
output blocks |
Split files merge into one module. .tf.json uses JSON objects with the same block types for generated or machine-authored config.
Next: Formatting and validating Terraform files covers fmt, validate, and checks before apply.
Check your understanding
Question 1 of 7
0 of 7 checked