Chapter 1: Foundations
Topic 06

Your First Configuration

Workflow

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.

main.tf — the smallest real configuration
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.

The core loop
init
plan
apply
destroy

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.

Common Mistakes
  • 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 in hatch-events-raw.
  • Running apply without 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 project in 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.
Best Practices
  • Read the plan before every apply, even for a single resource, and check the Plan: N to add, change, destroy summary 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 project and region explicitly in the provider block so resources land in the project you intend rather than an inherited default.
  • Run terraform destroy on throwaway learning configurations so you do not leave billable resources running after you are done.
  • Use terraform state list after an apply to confirm what Terraform now tracks, since state is what defines the blast radius of future changes.
Comparable tools AWS provider the same first-resource walkthrough with an S3 bucket Pulumi the same loop expressed as a program's up command gcloud storage buckets create the imperative one-shot contrast

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 ~ update on 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 - destroy of 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