Terraform boundary
Terraform boundary
Goal: Draw a clean boundary—Terraform (or OpenTofu) provisions cloud resources; NixOS owns system configuration—and produce a sketch you could implement without turning TF into a second module system. Images from the generators chapter + deploy from deploy-rs/Colmena sit on the Nix side; pin OS stories to 26.05.
Full multi-cloud automation is out of scope. A sharp sketch with one example resource graph is the bar. Implement only if you already have cloud credentials and want the stretch.
Why it matters
Nix people overreach into provisioning; TF people template snowflake user-data forever. The 2026 happy path for many teams:
Terraform/OpenTofu: VPC, VM, disks, DNS, IAM, object storage
│
│ outputs: IP, IDs, SSH target
▼
Nix flake: Disko/image/deploy-rs/Colmena configure the OS
Without a boundary doc, every new hire reinvents the split—and usually badly.
Theory 1 — Responsibility split
| Concern | Owner | Why |
|---|---|---|
| Cloud account / IAM | TF | Lifecycle & policy engines |
| VM size, subnet, SG | TF | Provider API native |
| Disk attach / snapshots API | TF | Cloud object model |
| Packages, services, users | Nix | Pure rebuild story |
| SSH hardening | Nix | Modules |
| App releases | Nix / CI | Closures |
| DNS record for service | Often TF | Or Nix + provider with care |
| Golden AMI/GCE image build | Nix + CI | Images chapter |
| Instance launch from image | TF | Provider resource |
Smell: 500-line user_data shell scripts installing docker by curl.
Smell: Nix trying to create AWS VPCs with ad-hoc runCommand and keys in the store.
Theory 2 — Handoff patterns
Pattern A — Image + launch
CI: nix build image → upload AMI/GCE image
TF: instance from image ID
Nix: optional second-phase deploy-rs for fast iteration
Pattern B — Generic distro image + first deploy
TF: stock NixOS or minimal image + SSH key
deploy-rs/Colmena: converge to desired closure
Pattern C — NixOS on metal (no TF)
Disko + install + deploy-rs
TF only for adjacent cloud DNS/objects if any
| Pattern | Best when |
|---|---|
| A | Immutable cattle, slow-changing base |
| B | Fast module iteration |
| C | Homelab / colo |
Theory 3 — What to put in TF state vs flake lock
| Artifact | System of record |
|---|---|
| VPC ID, instance ID | TF state (remote backend) |
| nixpkgs rev, module graph | flake.lock + git |
| Deploy proof file | Nix modules |
| Object storage bucket for backups | TF create; Nix restic config consumes name |
| DNS API token | Secret manager; neither plaintext git |
Never put TF state in the Nix store. Never put flake lock “truth” only in a random S3 object without git.
Theory 4 — OpenTofu vs Terraform
For this book, treat OpenTofu and Terraform as interchangeable at the boundary level. Choose based on org license/policy. Examples may say TF generically.
# versions.tf — illustrative
terraform {
required_version = ">= 1.6.0"
required_providers {
# provider pins…
}
}
Theory 5 — Minimal resource graph (sketch)
[network]
vpc
subnet
security_group (22 from admin CIDR; 80/443 public if web)
[compute]
instance (ami/image from Nix pipeline or stock)
eip / dns A record
[data]
data_volume (optional)
backup_bucket
# pseudo-HCL — not a full provider config
resource "aws_instance" "edge" {
ami = var.nixos_ami # produced by image pipeline
instance_type = "t3.small"
subnet_id = aws_subnet.main.id
vpc_security_group_ids = [aws_security_group.edge.id]
key_name = var.ssh_key_name # or cloud-init only
tags = {
Role = "edge"
Nix = "flake-managed"
}
}
output "edge_ip" {
value = aws_instance.edge.public_ip
}
# after apply
deploy .#edge # hostname from TF outputTheory 6 — Secrets across the boundary
| Secret | Where |
|---|---|
| Cloud provider credentials | TF env / CI OIDC—not flake |
| SSH deploy key | Agent / CI secret—not world-readable |
| sops age keys | Host persist / secure bootstrap |
| DB passwords | sops-nix on Nix side after instance exists |
TF sensitive outputs still land in state—protect state backends.
Theory 7 — Anti-patterns gallery
| Anti-pattern | Prefer |
|---|---|
TF remote-exec apt-get |
NixOS image + modules |
Nix pkgs.writeScript creating AWS resources with static keys |
TF + IAM roles |
| Duplicating SG rules in nginx modules only | TF SG + Nix firewall both intentional |
| Mutating instance packages by hand post-TF | redeploy Nix |
| One mega root module for all envs | workspaces/stacks + clear envs |
Worked example — BOUNDARY.md template
# BOUNDARY.md
## Owners
- Cloud resources: Terraform/OpenTofu in repo `infra/`
- OS/app: Nix flake in repo `nix/`
## Handoff
1. TF apply → outputs IP, instance id, bucket name
2. Update flake host inventory / sops if needed
3. deploy-rs/Colmena apply
4. Health checks
## Image pipeline
- Built by: CI job …
- Consumed by: TF var nixos_ami
## Non-goals
- TF will not install packages
- Nix will not create VPCs
## Secrets
- …Worked example — inventory bridge
# hosts/edge/deployment-ip.nix — generated or hand-updated from TF output
{
# Prefer automation later; honesty first
# deployment target:
# networking…
}tofu output -raw edge_ip > /tmp/edge_ip
# feed deploy hostnameAutomating TF output → flake is optional stretch; document the manual bridge first.
Lab — multi-step
Suggested notes: ~/lab/nixos/ops/terraform
Step 1 — Write BOUNDARY.md
Fill the template even with zero cloud resources.
Step 2 — Draw resource graph
ASCII or mermaid for one environment (lab).
Step 3 — Choose pattern A/B/C
Record why for your context.
Step 4 — Optional implement
If credentials exist: one VM + SG + output IP; deploy Nix once.
Step 5 — User-data audit
If any user-data remains, cap it at <20 lines or delete.
Step 6 — State backend note
Where would state live? Who accesses it?
Step 7 — Failure story
“TF created VM, deploy failed”—recovery steps.
Step 8 — Stretch destroy
tofu destroy only for disposable lab; never prod without ceremony.
Exercises
- Assign 15 concerns to TF vs Nix vs Shared in a table.
- Rewrite a fictional 80-line user-data into image+modules bullets.
- Design IAM least privilege for CI that builds AMIs.
- Explain why flake lock is not TF state (one paragraph).
- Multi-env: how do you stop lab AMI IDs leaking into prod TF?
Common gotchas
| Symptom / mistake | What to do |
|---|---|
| Drift: hand-edited security group | TF is source; import or recreate |
| Deploy to wrong IP | Outputs + inventory discipline |
| Secrets in TF console output logs | sensitive + log hygiene |
| NixOS firewall vs cloud SG confusion | Both; document layers |
| AMI stale | Image pipeline cadence |
| Two teams, no BOUNDARY.md | Write it |
remote-exec growth |
Delete; bake image |
Checkpoint
- BOUNDARY.md written
- Resource graph sketched
- Pattern A/B/C chosen
- Handoff steps listed
- Optional real TF apply or explicit skip
- User-data policy stated
- State backend thought through
Commit
git add .
git commit -m "ops: terraform/nix boundary sketch"Write three personal gotchas before nixosTest.