Photo by PJH on Unsplash

On this page

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 { }:

main.tf
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:

main.tf
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:

variables.tf
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:

variables.tf
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:

main.tf
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

  1. 1. In `resource "google_storage_bucket" "logs" { ... }`, what are the labels?

Search articles

Type a word to search published articles.

Open the search page