Chapter 3: The Core Workflow
Topic 16

Resources and Resource Addressing

ConceptState

A resource block is the unit of everything Terraform manages — one block maps to one real object created through a GCP API, like the hatch-events-raw bucket or the event-processor service. It is the smallest thing Terraform creates, updates, and destroys, and almost every line of HCL you write either declares a resource or feeds one.

Every resource has an address of the form type.name, and that address is not cosmetic. It is the resource's identity in state, the handle you use in references, the argument you pass to CLI commands and import, and the thing that decides whether renaming a resource is a harmless no-op or a destroy-and-recreate that takes your data with it.

The Resource Block

A resource block has two labels and a body. The first label is the provider's resource typegoogle_storage_bucket — which the google provider knows how to create. The second is the local name you choose — raw — which is yours to pick and only has meaning inside this module. The body is the desired-state arguments: what the object should look like.

A resource block: type, local name, body
resource "google_storage_bucket" "raw" {
  name     = "hatch-events-raw"   # the GCP object's real, globally-unique name
  location = "US"
}

Read it as: declare a managed Cloud Storage bucket, refer to it locally as raw, and make its real GCP name hatch-events-raw. Notice that the second label and the name argument are two different things — one is Terraform's handle, the other is the cloud's name. That distinction is the heart of this topic.

The Address type.name

The address google_storage_bucket.raw uniquely identifies this resource within its module. You use it two ways. To read one of the resource's attributes elsewhere in config, you append the attribute: google_storage_bucket.raw.url gives the bucket's URL once it exists. And to operate on the resource from the CLI — terraform state show, terraform apply -replace, -target — you name it by this same address.

Because the address is how everything refers to the resource, it is a stable contract. Other resources, outputs, and your CLI muscle memory all depend on it. That is exactly why changing it is consequential, which the next sections make concrete.

Two identities for one bucket
Terraform address
google_storage_bucket.raw is Terraform's internal handle, local to the module; it goes in HCL references and CLI commands. Changeable only through state surgery.
GCP name
hatch-events-raw is the cloud object's real, globally-unique name; it goes in IAM policies and other resources' arguments. Changing it usually forces replacement.

Instance Keys

A plain resource has one instance and the bare address points at it. Add count and the resource becomes a list: google_pubsub_topic.t[0], google_pubsub_topic.t[1], addressed by integer index. Add for_each and it becomes a map: google_pubsub_topic.t["events-ingest"], addressed by string key. The bracketed key is part of the address and part of the identity in state.

That difference has teeth. With count, the index is positional — remove an element from the middle of the list and every later instance shifts down, so Terraform sees a cascade of destroys and creates. With for_each, the key is the meaningful identifier events-ingest itself, so adding or removing one topic touches only that one. This is why for_each is the default for any set of similar resources.

The Address Is Identity in State

Terraform tracks resources by address, not by the GCP object's name. The state file maps google_storage_bucket.raw to whatever real bucket it created. So when you rename the local name from raw to events_raw, Terraform sees an old address with no config and a new address with no state — and plans to destroy hatch-events-raw and create a fresh bucket under the new address.

That destroy takes the data with it, which is almost never what a rename was supposed to mean. The fix is to tell Terraform the resource moved rather than disappeared, using a moved block in config or terraform state mv on the CLI. Either one rewrites the address in state without touching the cloud object, turning a catastrophic destroy/create into a no-op.

A moved block makes a rename a no-op, not a destroy
moved {
  from = google_storage_bucket.raw
  to   = google_storage_bucket.events_raw
}

Meta-Arguments Overview

Most arguments in a resource block are resource-specific — location, name, cidr_block. A handful are not: count, for_each, provider, depends_on, and lifecycle are meta-arguments, understood by the Terraform engine itself rather than the provider. They share one trait — each changes how a resource is managed rather than what it is — and each gets its own treatment in the topics ahead.

Terraform address vs GCP name

The Terraform addressgoogle_storage_bucket.raw — is Terraform's internal identity, local to the module and changeable only through state surgery (moved or state mv). It goes in HCL references and CLI commands.

The GCP namehatch-events-raw — is the cloud object's real name, and changing it usually forces replacement. It goes in IAM policies and other resources' arguments. The two are independent, and confusing them is why a harmless-looking rename triggers a destroy.

Common Mistakes
  • Renaming a resource's local name (rawevents_raw) and applying without a moved block or state mv — Terraform destroys hatch-events-raw and recreates it, taking the data with it.
  • Assuming google_storage_bucket.raw and the bucket's GCP name hatch-events-raw are the same string, and wiring the wrong one into an IAM reference — the address goes in HCL references, the name goes in policies and other resources' arguments.
  • Switching a resource from count to for_each and applying directly — the addresses change from [0] to ["key"], so every instance is destroyed and recreated unless migrated.
  • Hardcoding a resource's GCP name in a second resource (bucket = "hatch-events-raw") instead of referencing google_storage_bucket.raw.name — the reference also creates the dependency edge, the literal string does not.
  • Targeting a for_each instance on the CLI without quoting the key — -target='google_pubsub_topic.t["events-ingest"]' needs the shell quoting right or it silently targets nothing.
  • Choosing throwaway local names like this or bucket1 up front, then needing to rename them for clarity later — every rename is now state surgery you could have avoided.
Best Practices
  • Reference resources by their attribute (google_storage_bucket.raw.name) everywhere, so the address carries both the value and the dependency edge.
  • Use a moved block when you rename or restructure a resource address, so Terraform treats it as a move, not a destroy/create.
  • Pick stable, descriptive local names up front (raw, processor), since changing them later is state surgery.
  • Prefer for_each over count for any set of similar resources, so addresses are keyed by meaningful identifiers and stay stable when the set changes.
  • Keep the GCP name in arguments and IAM policies and the Terraform address in references — never let one stand in for the other.
Comparable tools Pulumi resource URNs and logical names are the analog Config Connector identifies resources by Kubernetes name/namespace gcloud has no stable logical identity — it operates on the GCP name directly

Knowledge Check

In resource "google_storage_bucket" "raw", what are the two labels?

  • The provider resource type, then the local name you choose to reference it by
  • The enclosing GCP project id, then the resource's globally unique name
  • The deployment region, then the resource's human-readable display name string
  • The enclosing module name, then the configured provider alias

You rename a resource's local name from raw to events_raw and apply with no moved block. What happens?

  • Terraform destroys the old bucket and creates a new one, since the address is the resource's identity in state
  • Nothing changes at all in the plan, because the underlying GCP bucket name in the cloud stayed exactly the same
  • Terraform updates the existing bucket in place to reflect the new local name
  • Terraform errors and refuses to run until you re-import the renamed resource

Why is for_each preferred over count for a set of similar topics?

  • Its instances are keyed by meaningful strings, so adding or removing one does not reshuffle the others
  • It creates all the resources in parallel, whereas count creates them one at a time
  • It is the only meta-argument that can produce more than one instance of a resource
  • It stores all of its instances outside the state file entirely, so renaming or removing any one of them is free

Why reference google_storage_bucket.raw.name instead of hardcoding the string "hatch-events-raw" in another resource?

  • The reference also creates the dependency edge between the two resources; the literal string does not
  • The literal string is rejected by the provider at validation time as invalid
  • References are evaluated noticeably faster than plain string literals at plan time
  • A hardcoded literal string forces the bucket to be destroyed and recreated from scratch on every single apply

You got correct