Resources and Resource Addressing
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 type — google_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.
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.
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.
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.
The Terraform address — google_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 name — hatch-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.
- Renaming a resource's local name (
raw→events_raw) and applying without amovedblock orstate mv— Terraform destroyshatch-events-rawand recreates it, taking the data with it. - Assuming
google_storage_bucket.rawand the bucket's GCP namehatch-events-raware 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
counttofor_eachand 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 referencinggoogle_storage_bucket.raw.name— the reference also creates the dependency edge, the literal string does not. - Targeting a
for_eachinstance 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
thisorbucket1up front, then needing to rename them for clarity later — every rename is now state surgery you could have avoided.
- Reference resources by their attribute (
google_storage_bucket.raw.name) everywhere, so the address carries both the value and the dependency edge. - Use a
movedblock 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_eachovercountfor 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.
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
countcreates 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