infra-edge-lab · v0.1.0
0014. One root module per environment
- Status: accepted
- Date: 2026-10-21
Context
The same building blocks (machines, a tunnel, DNS records, Access applications) run in four places with different lifetimes and risks: containers on a workstation or a CI runner, a lab on Proxmox VE, a throw-away cloud server for demonstrations, and a long-lived public cloud server. A mistake in the throw-away environment must never destroy the long-lived one, and a test on a CI runner must not need a cloud account.
Considered options
- One root module with workspaces, the differences in variables.
- One root module per environment under
tofu/envs/, each with its own state, providers, names and tests, built from the shared modules. - A wrapper tool that generates the root modules.
Decision
Chosen option: 2, because each environment’s blast radius, credentials and protection are then visible in its own files and checked by its own tests.
local: systemd containers on the local Docker daemon (docker_host), no cloud provider and no secret, so it can run on every pull request, forks included.lab: Proxmox VE virtual machines (pve_vm), its tunnel and Access applications; every host in the groupdbmust be protected.demo: one host that is thrown away after use, never protected.substrate = "hcloud"creates a cloud server;substrate = "docker"creates the same host as a container, so the tunnel can be tested end to end on a CI runner. Names and Access rules are the same for both.showcase: a skeleton for one long-lived cloud server; protection is a fixed value, not a variable, and its primary IPv4 address outlives the server for the DNS-only recordplay.- Every environment returns the same
hostsoutput (ansible_host,connection,groups,ip), so the inventory does not depend on the substrate (ADR 0010). - Each environment has its own name prefix (
lab-,demo-,show-/play) under the guard of ADR 0009, and its own state key (ADR 0013). - Guard tests per environment plan the whole root module against mocked providers: names and
record types, protection, which hostnames sit behind Access, and the
hostsoutput.
Consequences
- Good:
demoandshowcaseshare no state, so a destroy of one cannot touch the other. - Good: the guard tests run without credentials on every change.
- Bad: provider blocks, the backend and the encryption file repeat in each environment; tests check that the encryption file and the provider builds stay the same everywhere.
- Bad:
demowithsubstrate = "docker"still starts the cloud provider, which refuses to start without a token; it gets a placeholder that the API would reject.
docs/adr/0014-one-root-module-per-environment.md on GitHub
Licensed under Apache-2.0. Source: luminare-dev/infra-edge-lab at v0.1.0.