Hedronite · Dev Lesson · Polyglot-Dev / Go · Track TF Day 9 · Fri 2026-07-31 · terratest first fire · First Bundle Trio

Terratest — the test that rents real infrastructure

How do you know a Terraform module works? You apply it. Everything else is opinion.

Lesson Class: Dev (Go · terratest)
Track / Day: TF (Deep Terraform) — round-robin day 9, fourth TF visit · tf_day_dev_counter 2
Domains: DevOps · Terraform · Go · Testing · GCP
Word Count: ~1,950 (code-heavy)
Grounding: Brikman, Terraform: Up and Running 3rd ed — Ch 9 Unit testing pp. 488-496 · Test stages pp. 527-533 · Retries p. 536
Paired Ops: Terraform Workspaces and Environment Isolation on GCP
Paired Cert: Terraform Testing and Validation — validate, Conditions, check, terraform test
Discipline: ROD v3 · earth-accent meta-card · go-cyan code borders · bundle shape (Maghrib fills quiz + lab-ref)
Rent with Options
terraform.Options names the module dir, the vars, and which errors the cloud is allowed to fumble before the test calls it broken.
Sign Demolition First
defer terraform.Destroy lands before InitAndApply. Go runs it on any exit: pass, fail, or panic. The rental always ends.
Assert Against the World
Outputs check the module's claims; HttpGetWithRetry checks reality. Clouds converge rather than switch, so the assert retries.

The HCL validations proved the inputs parse. The plan proved the graph computes. Neither proves the site serves. Rent one and ask it.

§ IFrame

The TF-day Dev slot has walked a triad. The HCL lesson made a module an API: typed variables, validations, outputs as the public surface. The terrasnek lesson drove the platform that runs modules at team scale. Today the triad closes on the question both left open: nothing so far proves that applying the module produces infrastructure that works. Terratest answers by brute honesty. A Go test runs terraform init and terraform apply against a real cloud project, asserts against real outputs and real HTTP responses, and destroys everything on the way out. Brikman, whose company Gruntwork wrote the library, gives it the center of his testing chapter, and his one-line summary survives every fashion cycle since: there is no local-only way to know Terraform code works.

Coin for the day: the test that rents real infrastructure. Rent is the honest word. The test pays in minutes and cloud spend, holds the goods for the length of its assertions, and the lease ends whether the assertions passed or failed. Today's Ops lesson named the disposable environment; this lesson is that environment with a meter running and a contract that guarantees demolition.

§ IILanguage Idiom — Why This Library Is a Go Library

defer is the demolition contract. A deferred call runs when the enclosing function returns, in last-in-first-out order, no matter whether the function returned normally, failed an assertion, or panicked. Write defer terraform.Destroy(t, opts) on the line after you build the options and the cleanup is guaranteed before a single assertion runs. Python needs a finally or a fixture; shell needs a trap and luck. Go's version is one word, and the LIFO order means a test that rents three things demolishes them in reverse dependency order for free.

t *testing.T is the ledger. Every terratest helper takes t as its first argument and files failures through it, so a failed apply, a missing output, and a failed HTTP assertion all land in the same go test report your CI already reads. There is no separate harness. t.Parallel() at the top of a test opts it into concurrent execution with every other parallel test in the package, which matters because infrastructure tests are minutes long and a serial suite of ten is an hour.

The third mechanic is the timeout. go test kills any test binary after ten minutes by default, and almost every real terratest suite overrides it: go test -timeout 45m. Forgetting this is the classic first-week failure: the apply succeeds, the test dies mid-assert, and the deferred destroy never runs, which is how a team learns that the rental contract binds only while the tenant is alive. Pair the flag with a janitor sweep against the sandbox project and even the killed-tenant case has a landlord.

§ IIICode Worked Example — Renting a GCS Static Site

The module under test is the GCS static-website module from this arc's world: it creates a bucket in a sandbox GCP project, uploads an index object, and outputs the bucket name and site URL. The test rents one copy, checks the goods, and walks away.

func TestGcsStaticSite(t *testing.T) {
	t.Parallel()

	suffix := strings.ToLower(random.UniqueId())

	opts := &terraform.Options{
		TerraformDir: "../modules/static-site",
		Vars: map[string]interface{}{
			"project_id":  "hedronite-sandbox",
			"name_suffix": suffix,
		},
		RetryableTerraformErrors: map[string]string{
			".*googleapi.*503.*":   "GCP transient availability error",
			".*connection reset.*": "transient network failure",
		},
		MaxRetries:         3,
		TimeBetweenRetries: 30 * time.Second,
	}

	defer terraform.Destroy(t, opts)
	terraform.InitAndApply(t, opts)

	bucket := terraform.Output(t, opts, "bucket_name")
	url := terraform.Output(t, opts, "site_url")

	assert.Equal(t, fmt.Sprintf("hedronite-site-%s", suffix), bucket)
	http_helper.HttpGetWithRetry(t, url, nil, 200, "hello from hedronite",
		12, 10*time.Second)
}

Read it top to bottom as a rental agreement. random.UniqueId() gives this run a name no sibling run shares, which is the Ops lesson's unique-suffix discipline doing for tests what workspaces do for spikes: two engineers and a CI runner can execute this test at the same instant against the same sandbox project and never collide. The Vars map is the module's public API from the HCL lesson, exercised exactly as a consumer would. The retry fields teach the test the difference between a broken module and a cloudy afternoon: any apply error matching the listed patterns is retried up to three times, thirty seconds apart, and anything else fails immediately. Then the two lines that carry the pattern, in the only order that is safe. Demolition is signed before move-in: defer terraform.Destroy first, InitAndApply second. Reverse them and a failed apply leaks a bucket.

The assertions come in two strengths. terraform.Output reads the module's own claims and assert.Equal checks them, which catches wiring mistakes. HttpGetWithRetry ignores the module's claims and asks the world: fetch the URL up to twelve times, ten seconds apart, until it returns 200 with the expected body. Eventual consistency is real; a bucket website can take a minute to serve. The retry loop is the test admitting that clouds converge rather than switch.

§ IVTest Stages — Paying Rent Once While You Iterate

A full rent-assert-demolish cycle on a real module runs five to twenty minutes, and a developer tightening one assertion does not want to pay the apply tax on every iteration. Brikman's answer is test stages via terratest's test_structure package: break the test into named stages, and let environment variables skip stages that have already run.

func TestGcsStaticSiteStaged(t *testing.T) {
	t.Parallel()
	dir := "../modules/static-site"

	defer test_structure.RunTestStage(t, "teardown", func() {
		opts := test_structure.LoadTerraformOptions(t, dir)
		terraform.Destroy(t, opts)
	})

	test_structure.RunTestStage(t, "deploy", func() {
		opts := makeOptions(t)
		test_structure.SaveTerraformOptions(t, dir, opts)
		terraform.InitAndApply(t, opts)
	})

	test_structure.RunTestStage(t, "validate", func() {
		opts := test_structure.LoadTerraformOptions(t, dir)
		validateSite(t, opts)
	})
}

Each stage runs unless SKIP_<stage>=true sits in the environment. The working rhythm: first run with SKIP_teardown=true to deploy and validate but keep the infrastructure standing; then iterate on the validate stage alone with SKIP_teardown=true SKIP_deploy=true, re-running assertions in seconds against the standing copy; finally drop the skips to demolish. SaveTerraformOptions persists the options to disk between invocations so the later run can find what the earlier run rented. The stage skip turns a twenty-minute loop into a five-second loop precisely when a human is in it, and CI, which sets no skip variables, still pays full rent and full demolition every time.

§ VConnection to Today's Ops Lesson and the Prior Arc

The Ops lesson drew the line between durable environments behind project walls and disposable environments that die by dinner. The terratest run is the extreme of the second class, and it obeys the first class's rules anyway: the test targets hedronite-sandbox, a project that exists so that nothing durable lives where tests demolish, which is the same project-boundary wall prod hides behind. The reach back through the Dev triad: the HCL lesson built the module API this test exercises through Vars and outputs; the terrasnek lesson drove runs through HCP Terraform's API, and a CI pipeline pairs the two naturally, terratest proving a module version before the promotion boundary lets it near staging; and the 07-22 plan-assertion work in Python was the cheap gate, reading a plan JSON without paying rent. Plan assertions are minutes cheaper and a class weaker: a valid plan can still fail to produce a working site. Today's test is the strong gate the cheap gate defers to.

§ VIClosing

A terratest test is four commitments in one function: rent with options, sign the demolition before move-in, assert against the world rather than the claims, and retry only what the cloud is allowed to fumble. Stage the test when a human is iterating; run it whole when a machine is judging. Go carries the pattern because defer makes the demolition contract one word long.

Take one module you rely on and write only the rental frame for it: options, deferred destroy, InitAndApply, one output assertion. Run it against a sandbox project and time it. That number is what your promotion boundary costs to defend, and whether it is four minutes or forty changes how often you can afford the defense.

🫡 ⚖️ 📜
Leo.Syri — Praetor Consulate, Imperium Luminaura
Filed 2026-07-31 Fajr · Dev lesson · TF deep-mastery track (day 9, visit 4) · terratest counter-2 first fire