Infrastructure provisioning for KVM/libvirt VMs, networks, and storage, managed with OpenTofu. The setup is compliance-aligned, CI-gated, and ships documented module interfaces.
Companions:
automation (Ansible
configuration + hardening) and
runbooks (ad-hoc operator
scripts).
The rationale behind every standing decision — OpenTofu over Terraform,
the dmacvicar/libvirt pin, the state-backend strategy, the
cloud-init baseline, the module/environment layout — lives in
docs/adr/.
Infrastructure layer only — what gets created and destroyed. For Ubuntu VMs this is decoupled from configuration management (Ansible, Salt). For the Kubernetes layer it uses Talos Linux, an immutable, API-only OS configured entirely as code (no Ansible — there is no host to configure; ADR-0013).
Current providers:
- KVM/libvirt via
dmacvicar/libvirt, pinned~> 0.9.0(ADR-0002 pin rule; bumped 0.8→0.9 in ADR-0016) - Talos Linux via
siderolabs/talos, pinned~> 0.11.0(ADR-0014)
Planned: Hetzner Cloud (hetznercloud/hcloud) and additional providers
as required.
- OpenTofu — per-root floors:
labpinsrequired_version = ">= 1.10";productionpins">= 1.10.4"to avoid the 1.10.0–1.10.3 S3 SSE-on-lockfile bug, opentofu#2970;talos-lab(and thetalos-clustermodule itself) pins">= 1.11"for the write-only secret arguments, ADR-0017..opentofu-versiontracks1.12.1. The 1.10 floor matches the production S3 backend'suse_lockfilerequirement (ADR-0003, ADR-0011) - A KVM/libvirt host with
qemu-systemandlibvirtdrunning; thedefaultstorage pool and network must already exist — the module does not create them (ADR-0006 Finding 7) - A cloud-init compatible base image — version-neutral across Ubuntu 24.04 noble and Ubuntu 26.04 resolute (kernel 7.0); the shipped cloud-init is distro-neutral
- For the Talos Kubernetes layer: a
Talos disk image and
talosctl - An SSH key pair for VM access
- TFLint — optional for local linting
cd environments/lab
tofu init
export TF_VAR_ssh_public_key="ssh-ed25519 AAAA..."
tofu plan
tofu applyinfra/
├── modules/libvirt-vm/ # KVM/libvirt Ubuntu VM provisioning module
├── modules/talos-cluster/ # Talos Linux Kubernetes on libvirt (siderolabs/talos)
├── environments/
│ ├── lab/ # Ubuntu VMs; local state, single-operator iteration
│ ├── production/ # Remote S3 backend (ADR-0011); no resources yet
│ └── talos-lab/ # Talos Kubernetes cluster; local state
├── scripts/init-backend.sh # Per-environment init helper
├── docs/adr/ # Architecture Decision Records
├── docs/talos-cis-kubernetes.md # Talos hardening -> CIS Kubernetes mapping
└── .github/workflows/ci.yml # CI: fmt + validate + tflint + Trivy + gitleaks + pre-commit + tests
KVM/libvirt Ubuntu VM with cloud-init, configurable CPU/memory/root
disk, and optional additional data disks. The base_image is
version-neutral (Ubuntu 24.04 noble or 26.04 resolute). Full inputs and
outputs: module README.
| Variable | Type | Default | Description |
|---|---|---|---|
vm_name |
string |
— | VM hostname |
vcpus |
number |
2 |
Virtual CPU count |
memory_mib |
number |
2048 |
Memory in MiB |
disk_size_gib |
number |
20 |
Root disk size in GiB |
base_image |
string |
— | Path or URL to cloud image (24.04 or 26.04) |
ssh_public_key |
string |
— | SSH public key (sensitive) |
A Talos Linux Kubernetes cluster on libvirt:
VMs via dmacvicar/libvirt (booting the Talos image directly, no
cloud-init) plus the siderolabs/talos provider for machine secrets,
hardened machine config, apply, etcd bootstrap, and the exported
kube/talos configs. Immutable, API-only, not Ansible-managed
(ADR-0013). Hardened baseline
mapped to the CIS Kubernetes Benchmark in
docs/talos-cis-kubernetes.md. Full
inputs and outputs: module README.
| Variable | Type | Default | Description |
|---|---|---|---|
cluster_name |
string |
— | Cluster name |
cluster_endpoint |
string |
— | Kubernetes API endpoint URL |
talos_image |
string |
— | Path or URL to the Talos disk image |
control_plane_nodes |
map(object) |
— | Control-plane node name → {ip, mac, …} |
worker_nodes |
map(object) |
{} |
Worker node name → {ip, mac, …} |
| Environment | Backend | Status | Notes |
|---|---|---|---|
lab |
Local | Active | Ubuntu VMs; single-operator iteration on a local KVM host |
production |
Remote S3-compatible | Backend live, no resources | backend.tf ships the real backend "s3" (use_lockfile, encrypted; ADR-0011); needs the org bucket + AWS creds for a real init |
talos-lab |
Local | Active | Talos Kubernetes cluster (1 control-plane + 2 workers); secrets gitignored |
Each environment ships its own backend.tf, variables.tf,
terraform.tfvars (non-secret defaults only), versions.tf, and a
committed .terraform.lock.hcl. Initialize via the helper:
./scripts/init-backend.sh labFor production, configure the S3 backend (locked + encrypted at rest — see ADR-0003), set credentials, then init:
export AWS_ACCESS_KEY_ID="..." AWS_SECRET_ACCESS_KEY="..."
./scripts/init-backend.sh productionStanding decisions live in docs/adr/. Each ADR captures
the context, the decision, and the consequences of one significant
choice. Adding a new significant decision means writing a new ADR, not a
README section.
| ID | Title |
|---|---|
| 0001 | Use OpenTofu, not Terraform |
| 0002 | Pin dmacvicar/libvirt to ~> 0.8.0 |
| 0003 | State backend strategy (local lab, S3-compatible production) |
| 0004 | Cloud-init bootstrap conventions |
| 0005 | Module and environment layout |
| 0006 | Code audit 2026-05 findings |
| 0007 | Set meta_data on libvirt_cloudinit_disk |
| 0008 | Omit graphics from libvirt_domain by default |
| 0009 | Begin dmacvicar/libvirt 0.9.x migration evaluation |
| 0010 | Permit module-local supporting files and ship the graphics override |
| 0011 | Realize the production S3 remote state backend |
| 0012 | dmacvicar/libvirt 0.9.x schema-diff inventory (Proposed) |
| 0013 | Adopt Talos Linux for the Kubernetes layer |
| 0014 | Pin siderolabs/talos to ~> 0.11.0 |
| 0015 | Talos machine-config-as-code and secret handling |
| 0016 | Migrate dmacvicar/libvirt to ~> 0.9.0 |
- Remote backends must have encryption at rest enabled
- Remote backends must have state locking — prefer
use_lockfile = true(native S3 locking, OpenTofu 1.10+) overdynamodb_table - Sensitive variables marked
sensitive = trueand never committed - Secrets injected via
TF_VAR_*environment variables - Cloud-init bootstraps every VM into a hardened state: no password auth, no root SSH, locked default user, key-only access
- All changes flow through CI-gated pull requests
| Command | Purpose |
|---|---|
tofu init |
Initialize working directory, download providers |
tofu plan |
Preview changes |
tofu apply |
Apply planned changes |
tofu destroy |
Destroy all managed resources |
tofu fmt -recursive |
Format HCL files |
tofu validate |
Validate configuration syntax |
tofu state list |
List resources in state |
tofu output |
Show output values |
| Check | Tool | Command |
|---|---|---|
| Format | tofu fmt |
tofu fmt -check -recursive |
| Validate | tofu validate |
Per-env tofu init -backend=false && tofu validate |
| Lint | TFLint | tflint --recursive |
| Security | Trivy | trivy config . --severity HIGH,CRITICAL |
| Secret scan | gitleaks | gitleaks dir . (CI runs the digest-pinned image over the full working tree) |
| Module tests | tofu test |
Per module — mock providers, so no libvirtd or live cluster is needed |
| Hygiene | pre-commit | pre-commit run --all-files |
All checks must pass before a PR can merge.
pip install -r requirements-dev.txt && pre-commit install
export PCT_TFPATH=tofu # point pre-commit-terraform at OpenTofu
pre-commit run --all-filesHook set: .pre-commit-config.yaml. OpenTofu
version pinned via .opentofu-version (1.12.1).
Pre-commit version pinned via
requirements-dev.txt; Renovate watches both
this file and the GitHub Actions workflow weekly.
| File | Purpose |
|---|---|
CLAUDE.md |
HCL style, OpenTofu policy, conventions |
CONTRIBUTING.md |
Workflow, ADR expectations |
CODE_OF_CONDUCT.md |
Contributor Covenant 2.1 |
CHANGELOG.md |
Keep a Changelog 1.1.0 |
docs/adr/ |
Architecture Decision Records |
SECURITY.md / .github/SECURITY.md |
Vulnerability reporting (root stub + policy) |
.github/PULL_REQUEST_TEMPLATE.md |
PR checklist |
.github/CODEOWNERS |
Review assignment |
LICENSE / NOTICE |
Apache 2.0 |