Terraform Tutoring — Session 1 Notes¶
Date: 2026-08-30
Topic: Core mechanics — provider config, init/plan/apply/state, resource
vs data, partial-apply behavior, cross-repo Consul publish/consume.
Method: Live, hands-on against real GCP (my-devops-journey-502420) using the
acme-sampleapp sandbox, not just reading code.
What Actually Happened (chronological)¶
- Confirmed
gcloud auth application-default loginwrites a refresh token to~/.config/gcloud/application_default_credentials.json— read automatically by Terraform'sgoogleprovider and any Google client library, no extra config needed. Access tokens expire hourly; refresh token doesn't (until revoked). Corrected: "some timeout" → precise mechanism. - Ran
terraform initinsample-program/infra— confirmed it downloads providers into.terraform/+ writes.terraform.lock.hcl, and does not createterraform.tfstate. State only gets written by the first realapply/refresh. - Live experiment: commented out the
provider "google" {}block entirely.initstill succeeded (provider plugin download is controlled byrequired_providersinversions.tf, unrelated to the config block).planstill succeeded too, once a confound was ruled out (missingterraform.tfvarswas independently causing a variable prompt). Root cause: every resource inmodules/vpc,modules/gke,modules/dnssets its own explicitproject = var.project_id— so the provider block'sprojectwas never actually being used as a fallback by anything. Key rule discovered: provider-blockproject/region/zoneare fallback defaults only, overridden per-resource whenever a resource sets its own. - Corrected a real misconception:
datablocks do not persist from state if the underlying real resource is deleted. They re-query the live API on everyplan/apply; if the real object is gone, the next plan errors (Failed to find resource), it doesn't silently return a stale cached value. Confirmed whydata "google_client_config" "default"(used for the Kubernetes/Helm provider's auth token) is adatasource specifically: access tokens expire hourly, and only adatasource guarantees a fresh read on every run — aresourcewould only refresh onapply, and only if Terraform detected drift. - Correctly predicted, unprompted, the full 7-resource plan for
sample-program/infra(consul_keys,google_dns_managed_zone,google_compute_network,google_compute_router,google_compute_router_nat, and 2×google_compute_subnetworkviafor_each) — correctly excluded the GKE cluster/node pool since both enable flags defaultfalse. - Real incident, real diagnosis:
terraform applyonsample-programpartially failed — 6/7 resources succeeded (VPC, DNS zone, router, NAT, both subnets),consul_keys.publish_outputsfailed withconnection refusedonlocalhost:8500(no local Consul agent running). Correctly reasoned, unprompted, that the 6 successful resources were already durably written to state —applyis not transactional; each resource commits to state as it individually succeeds, not as one atomic batch. This is why the re-run only needed1 to add, not7 to add— direct, observed proof of state-tracking/idempotency, not just theory. - Correctly reasoned (from microservices/distributed-systems background)
that a real Consul deployment would be a shared, centrally-reachable
service (its own cluster, stable network address, real ACL tokens) —
the local
consul agent -devhere is a single-machine stand-in for exactly that, mechanically identical HCL either way. - Fixed the Consul gap (installed + started
consul agent -dev), re-ranplan→ correctly predicted1 to add, 0 to change, 0 to destroy→ confirmed by actual output. - Verified the actual published Consul value via
consul kv get— saw{"outputs":{"gcp":{"us_central1":{}}}}, correctly reasoned this was becauselocal.gke_clusters_enabled(filtered fromenable_gke_p/np, bothfalse) was an empty map, so thefor_each-built JSON object had no keys at all. - Attempted
backend/infraplannext — first guessed the failure would surface atkubernetes_namespace_v1. Actual failure was earlier:Invalid indexonlocal.program_gcp = jsondecode(...).us_central1[local.p_or_np], becauseus_central1was{}— no"p"key existed since no GKE cluster had been created yet. Correctly diagnosed the root cause (empty map, not a Terraform bug) once shown the error. - Session ended with
enable_gke_p = trueandenable_gke_np = trueset, about to apply real GKE clusters — continues into Session 2.
Key Mechanics Learned (for quick recall later)¶
initvsapply:init= tooling/dependencies only, safe to rerun anywhere, never touches real infra or state.apply= the only thing that creates/modifies real infrastructure and writes state.- Provider block config is a fallback, not a mandate — any resource that
sets its own
project/region/zoneignores the provider block's value entirely. resource= Terraform-owned, full lifecycle, drift-checked onplan.data= read-only, re-fetched live every run, errors if the real thing is gone (never silently stale).applycommits per-resource, not atomically — a failure partway through leaves everything before it as real, state-tracked infrastructure. Re-runningplanafter a partial failure only shows the remainder.- Cross-repo Consul publish/consume is a real dependency chain — if the upstream repo hasn't published real data (e.g. no GKE cluster exists yet), the downstream repo fails fast and specifically at the point it tries to index into the missing data, not at some later unrelated resource.
Corrections Made This Session¶
- "Data blocks persist from state even if the real resource is deleted" → corrected: they re-query live, every run.
- Used "import" when meaning "explicit dependency (
depends_on)" — terminology slip, self-corrected once flagged.importis an unrelated mechanism (bringing an existing real resource under management). - Operational (not conceptual) miss: forgot to
cp terraform.tfvars.example terraform.tfvarstwice before runningplan— not a Terraform misunderstanding, just a setup step to build into habit.
What's Next (Session 2)¶
- Finish applying
sample-programwith both GKE clusters enabled (cost reminder: destroy both when done for the day). - Once
p/npclusters exist andsample-programrepublishes to Consul, re-runbackend/infra plan— confirmlocal.program_gcpresolves. - First real look at the
kubernetes/helmprovider blocks actually authenticating against a live cluster (data.google_client_config.default→ token → provider config chain, tested live this time, not just read). - Continue into
count/for_eachat greater depth (thecount=0gap from the original baseline diagnostic, plus realfor_eachusage already seen insample-program'smodule "gke"andgoogle_project_iam_memberinbackend/infra/iam.tf).