Your First Configuration
The fastest way to make Terraform concrete is to apply one real resource into a real project and watch each step do its job. This topic builds the first piece of the Hatch pipeline: a single Cloud Storage bucket named hatch-events-raw, in the hatch-pipeline-dev project, where clickstream event files will eventually land. Three blocks of HCL, then init, plan, apply, and a look at what each command actually did.
Everything here is the smallest version of the loop you will run thousands of times. The point is not the bucket — it is seeing that the plan is a precise preview, that the apply records what it built into state, and that re-running does not duplicate anything.
The Three Blocks
A configuration that does something real needs three things: a terraform block declaring the provider, a provider "google" block setting the project and region, and one resource declaring the bucket. That is the minimum, and it is the same shape as a configuration with five hundred resources — just smaller.
terraform { required_providers { google = { source = "hashicorp/google" version = "~> 7.0" } } } provider "google" { project = "hatch-pipeline-dev" region = "us-central1" } resource "google_storage_bucket" "raw" { name = "hatch-events-raw" # globally unique across all of GCS location = "US" }
Read it top to bottom: require the google provider, configure it to act on the hatch-pipeline-dev project in us-central1, and declare that a bucket named hatch-events-raw in the US multi-region should exist. The location is set on the bucket itself; the provider's region is the default for resources that take one.
init, plan, apply
Three commands turn that file into a real bucket. terraform init reads the required_providers block, downloads the google provider into .terraform/, and writes the lock file — you run it once per configuration and again whenever providers change. terraform plan compares your configuration to state and reality and prints exactly what it would do. terraform apply shows the same plan, asks for confirmation, then calls the Cloud Storage API and records the result in state.
Run them in that order the first time and the sequence is unmistakable: init prints that the provider was installed, plan shows one resource to add, and apply creates it after you type yes. From the second run on, you mostly live in plan and apply; init only comes back when dependencies change.
Reading the Plan
The plan is the thing you actually review, and its notation is worth reading carefully. A leading + means create, - means destroy, and ~ means update in place. For the bucket, you see + create with the arguments you set — name and location — alongside a list of attributes marked (known after apply), like the bucket's self link and generated ID, which Terraform cannot know until the API responds.
The summary line — Plan: 1 to add, 0 to change, 0 to destroy — is the one-glance check that the plan matches your intent. Reading it is not a formality. It is the review step where you catch a destroy you did not mean to trigger before it happens, rather than after.
Inspecting the Result
After apply succeeds the bucket exists, and you can confirm it two ways. In the Console it appears in the Cloud Storage browser like any other bucket; from the CLI, terraform state list prints google_storage_bucket.raw, showing Terraform now tracks it. That state entry is what makes the next run smart.
Change one argument in the HCL — say, add a force_destroy or a label — and re-run plan. Terraform shows a ~ update against the existing bucket, not a second + create, because state already maps your resource to the real object. The configuration is a description of one bucket, and Terraform converges that one bucket toward it rather than making a new one each time.
Destroy
terraform destroy removes exactly what this configuration created — the one bucket — and nothing else in the project. It reads state to determine the blast radius, prints a plan of what it will delete, and asks for the same confirmation as apply. On a throwaway learning configuration, running it cleans up so you do not leave billable resources running by accident.
That precision is the quiet payoff of state. Destroy does not guess at what belongs to you or scan the project for things that look related — it deletes the specific resources state records as managed by this configuration. State, not guesswork, defines what an apply or a destroy can touch.
- Choosing a short, generic bucket name like
events-raw— bucket names are globally unique across all of Cloud Storage, so the apply fails with a name conflict; prefix with the project, as inhatch-events-raw. - Running
applywithout reading the plan, then being surprised by what changed — the plan is the review step where you catch an unintended destroy, not a formality to skip past. - Leaving the wrong
projectin the provider block and creating the bucket in someone else's project — always confirm the project before the first apply, because moving a resource afterward is far more work. - Deleting the bucket in the Console and expecting Terraform to forget it — state still lists the resource, so the next plan tries to recreate it.
- Treating
(known after apply)attributes as missing data — they are values the API has not returned yet, like the generated ID, and are filled in once apply runs.
- Read the plan before every
apply, even for a single resource, and check thePlan: N to add, change, destroysummary against your intent. - Prefix globally-unique names like buckets with the project or org, as in
hatch-events-raw, to avoid collisions with names someone else already took. - Set
projectandregionexplicitly in theproviderblock so resources land in the project you intend rather than an inherited default. - Run
terraform destroyon throwaway learning configurations so you do not leave billable resources running after you are done. - Use
terraform state listafter an apply to confirm what Terraform now tracks, since state is what defines the blast radius of future changes.
Knowledge Check
What does terraform init do that plan and apply do not?
- Downloads the declared providers into
.terraform/and writes the lock file - Creates every resource defined in the configuration and waits for the API to confirm them
- Compares the recorded state to live infrastructure and prints the resulting diff
- Deletes everything the configuration currently manages
Your apply fails creating a bucket named events-raw with a name conflict. Why?
- Bucket names are globally unique across all of Cloud Storage, and that name is already taken
- Bucket names are required to be uppercase
- The provider region does not match the location declared on the bucket resource
- Terraform reserves for itself any bucket name shorter than twelve characters, including this one
You change one argument on the existing bucket and re-run plan. What does Terraform show, and why?
- A
~ updateon the existing bucket, because state maps the resource to the real object - A second
+ create, because every plan provisions a brand-new bucket from scratch each time - No change at all, because Terraform ignores any edits made after the first apply
- A
- destroyof every resource in the whole project
What determines exactly what terraform destroy removes?
- State — it deletes the resources state records as managed by this configuration, nothing else
- It scans the entire project and deletes anything whose name or labels look related to the config
- It removes every single resource that exists in the GCP project
- It deletes whatever was created most recently, regardless of which tool owns it
You got correct