Terraform Tutoring — Session 3 Notes¶
Date: 2026-08-31 (resumed after a ~5 hour pause, following a full destroy/rebuild) Topic: Resuming from a clean-slate teardown, confirming Session 2's fixes generalize on a fresh apply, a real multi-hypothesis debugging sequence, and closing out the Kubernetes/Helm provider auth verification deferred across two prior sessions. Method: Fully hands-on against real GCP, starting from zero (everything destroyed before this session began).
What Actually Happened (chronological)¶
- Retrieval check on resume — asked to recall why Session 2 predicted
2 secrets (not 3) before looking anything up. First answer was vague
("keep costs low... if condition for default/np/p"); re-anchored with the
precise mechanism (only
npcluster existed,prodmaps to the never-createdpcluster, sofor_eachover dev/qa/prod could only resolve 2 of 3). - Confirmed a full destroy had happened — clean slate, not a partial
state. Chose to rebuild deliberately via the
restore-session2.shscript rather than skip straight to typing commands. - infrastructure/infra re-applied from scratch, verified the fix
generalizes: exactly
6 resources added(2 secrets × 1 version × 1 consul publish each = 6),prodcorrectly absent from the output on a completely fresh apply — not just the run the fix was originally written for. Good evidence the guard-propagation fix was general, not a one-off patch that happened to work once. - backend/infra plan succeeded immediately after
infrastructurewas re-applied —app_infraresolved,cloudsql_enabled = falseconfirmed the guarded-degradation design (from Session 2's comparison) behaving exactly as reasoned, live. - Deliberate decision point: apply
backend/infranow in mock-data mode (closes the still-outstanding Kubernetes/Helm auth verification, free) vs. applycloudsql/infrafirst (costs more, more realistic). Chose mock-data mode first — correctly prioritized closing a twice-deferred item over completeness. - Real, multi-step debugging incident —
terraform applyinbackend/infrafailed on the exact sameapp_infraline that had just resolved cleanly inplanmoments earlier. Three hypotheses tested and ruled out in order, each with actual verification rather than assumption: - H1: different terminal/environment,
CONSUL_HTTP_ADDRnot inherited. Checkedpgrep -f "consul agent -dev"(running) andconsul kv getdirectly on the exact key (data present, correct JSON). Ruled out. - H2: wrong Terraform workspace (
defaultinstead ofdev, which would map to the never-publishedprod/pbranch). Checkedterraform workspace show→dev, correct. Ruled out. - H3 (found by reading code, not guessing further): the
data "consul_keys" "remote_outputs"block's"infrastructure"key had a hardcoded path ending in/default, never parameterized by environment — unlike the"cloudsql"key right below it in the same block, which was correctly parameterized (${contains(...) ? terraform.workspace : "dev"}). Sinceinfrastructure/infraonly ever published to/devand/qa(Session 2's Option B), this read was doomed regardless of workspace, Consul health, or terminal — a genuine bug in the repo's own code, not a usage mistake. Fixed by parameterizing with the already-computedlocal.infra_env_key, matching thecloudsqlkey's pattern. - Fix verified:
terraform plansucceeded cleanly, exit code 0, no error, after the one-line path fix. - First real Kubernetes-facing
applyinbackend/infra— succeeded. - Kubernetes/Helm provider auth chain verified end-to-end, independently — not just via Terraform's own success message:
gcloud container clusters get-credentials+kubectl configcurrent-context— real kubeconfig context created.kubectl get ns | grep acme-sampleapp— real namespace (acme-sampleapp-backend-dev) confirmedActive.kubectl get sa -n ...— found the K8s ServiceAccount (acme-sampleapp-backend-sa).- Correctly distinguished (after one clarifying nudge) that the K8s ServiceAccount and the GCP IAM service account are two separate identities linked by annotation, not by name-matching.
kubectl get sa ... -o jsonpath='{.metadata.annotations}'— confirmed the actual Workload Identity annotation (iam.gke.io/gcp-service-account: sa-backend-dev@...) present and correct, verified viakubectlindependently of Terraform's own state.
Key Mechanics Learned (for quick recall later)¶
- A fix that resolves one instance of a bug should be re-tested on a fresh apply, not just trusted — this session deliberately re-verified Session 2's guard-propagation fix from a full teardown, not just assumed it still worked.
- Inconsistent parameterization between structurally similar code blocks
is a classic, easy-to-miss real bug pattern — the
"cloudsql"key was parameterized by environment; the"infrastructure"key, right next to it, was hardcoded. Both looked equally plausible at a glance; only one was correct. - When multiple plausible hypotheses exist, test them in order and rule each one out with direct evidence rather than jumping straight to a code change: environment/terminal mismatch → workspace mismatch → actual code bug, each checked with a concrete command, not assumed.
- Workload Identity is two separate identities linked by annotation, not
by name. A Kubernetes ServiceAccount and a GCP IAM service account can
have completely different names —
iam.gke.io/gcp-service-accounton the K8s SA's annotations is the actual link, and it's worth verifying directly viakubectl, not just trusting Terraform'sapplysuccess. - Terraform state's confirmation isn't the same as independent
verification —
kubectl(a completely separate tool reading the same live cluster) confirming the namespace, service account, and annotation all matched is stronger evidence than theapplylog alone.
What's Next (Session 4)¶
cloudsql/infrastill hasn't been applied this session — decide whether to add a real Cloud SQL instance (switchescloudsql_enabledtotrue, real cost) or continue building outfrontend/infrafirst while staying in mock-data mode.frontend/infrahasn't been touched yet — natural next step now thatbackend/infrais confirmed working end to end.- Good candidate for a transfer check (Gate 4): give a fresh, different for_each-guard scenario (not the one already taught in Session 2) and see if the pattern gets applied independently this time, without re-teaching it.
- Also worth verifying: does the same hardcoded-path bug class exist
anywhere else in the sandbox? Worth a deliberate grep-and-check pass
across
frontend/infraandcloudsql/infrabefore assuming they're clean — directly applying this session's "don't assume, verify" lesson. - Cost reminder:
npGKE cluster + 2 Secret Manager secrets + backend namespace/resources are all currently live and billing. Same destroy/restore workflow applies going into any future pause.