- Shell 39.8%
- HCL 28.5%
- Python 14%
- Nix 11.1%
- XSLT 5.8%
- Other 0.8%
|
|
||
|---|---|---|
| control-plane | ||
| hc-vault | ||
| tests | ||
| woodpecker | ||
| worker | ||
| .gitignore | ||
| AGENTS.md | ||
| ARCHITECTURE.md | ||
| flake.lock | ||
| flake.nix | ||
| PLAN.md | ||
| pyproject.toml | ||
| README.md | ||
Milner
General-purpose Talos Linux cluster definition on libvirt/QEMU. Hosts multiple pods:
- HashiCorp Vault — secrets management
- Woodpecker CI — CI/CD (coming soon)
- Evolved-NPCs — architecture plan for simulation workloads
Getting Started
Install Nix:
sh <(curl -L https://nixos.org/nix/install)
Enter the dev shell (provides terraform, qemu, podman, and all other dependencies):
nix develop
How Changes Ship
Infrastructure changes ship as Terraform deltas. A single reviewed terraform plan → terraform apply reconciles exactly the declared delta against the running environment; it never recreates what did not change. There are two primitives — (a) in-place delta (always the default) and (b) -replace of a disposable VM (used only at the VM-level base infrastructure) — and one approach that is always rejected: per-change migration scripts (c).
The layer boundary
Different infrastructure layers support in-place deltas to different degrees. The table below is the authoritative boundary: use the default column for normal reconciliation and the recovery column only when a disposable node must be swapped.
| Layer | Real in-place delta? | Default | Recovery |
|---|---|---|---|
| VM, disk, libvirt network/pool | No | Terraform (a) | Replace only the disposable VM (b); keep volume/network/pool separate and protected |
| Talos machine configuration | Yes | Terraform delta (a) | Stage the changed config (staged_if_needing_reboot), never reset the node |
| Pods / Helm / Compose | Yes | Reconcile the release/chart (a) | Roll back or reconcile, never recreate workers |
(a) Terraform delta — always the default
Past the VM boundary, (a) is the default for every change, unconditionally. All changes run inside nix develop. A change is one value in one release/chart/config; applying it reconciles only that declaration:
terraform plan && terraform apply # only the declared delta reconciles
helm upgrade --install woodpecker . --values values.yaml # only that release
podman-compose -f hc-vault/docker-compose.yml up -d # preserves ./vault_data
Always:
- Run
terraform planand review the diff before anyterraform apply. Never apply without explicit user confirmation. - Never commit Terraform state or secrets.
- Prefer
-replaceover destroy+recreate when a resource genuinely cannot be diffed in place. - Never hand-run
virsh/qemu-imgfor infrastructure lifecycle — all infra changes go through Terraform plan/apply.
(b) -replace only for a disposable VM
(b) is used only at the VM-level base infrastructure, and only for a disposable node. A VM created from an ISO is not a stateful resource Terraform can diff in place, so the honest delta for the VM itself is recreation. Replace the libvirt_domain only — never the disk, network, or pool:
terraform plan -replace='libvirt_domain.control_plane'
terraform apply -replace='libvirt_domain.control_plane'
For (b) to be safe, the domain and its persistent volume must be separate resources so the volume is never a replacement target:
resource "libvirt_volume" "control_plane_disk" {
lifecycle { prevent_destroy = true }
}
Always:
- Keep the persistent volume/network/pool separate from the domain, with the volume protected via
prevent_destroy. - Review every
-replaceplan before applying; confirm exactly which resource is the replacement target. - Never hand-run
virsh/qemu-imgon the volume or node.
(c) per-change migration scripts — always rejected
(c) is never used. Per-change migration scripts are rejected outright because they become a second source of truth that must be kept in sync with reality. The only acceptable script-adjacent pattern is a thin adoption/recovery helper that imports an existing VM once — reference-only, never a migration script per change.
cleanup.sh — full cluster teardown only
control-plane/cleanup.sh has exactly one purpose: full cluster teardown. It is the confirmation-gated tool for removing the entire control-plane environment (managed resources, kubeconfig/talos client contexts, cluster files). It is never a partial-recovery tool — recovery is -replace. Its destructive actions (stopping QEMU, removing managed cluster state) are reserved exclusively for that one full-teardown purpose and must never be pointed at an individual node or at persistent data — those are preserved and recovered via -replace (b). If the environment only needs a disposable node swapped, use (b), not cleanup.sh.
Clean install vs incremental adopt — one graph
There are two pathways but one Terraform graph, not two divergent instruction sets:
- Clean install —
terraform applyfrom an empty state. - Incremental adopt / recover — import the existing VM's UUID → review plan → apply, using the same
main.tf.
Provider-version caveat: the repo pins
dmacvicar/libvirt ~>0.7; domain-UUID import via the provider is confirmed only on libvirt 0.9.6+. Documentation that referencesimport/-replaceadoption must not assume a provider version the repo is not yet on.
Origin Repos
- evolved-npcs — the application this cluster supports