OpenTofu root module for libvirt/KVM infrastructure.
Agents are authorized to push directly to main in this repository.
Pre-commit configuration is centralized in makeitworkcloud/images/tfroot-runner/pre-commit-config.yaml. The CI workflow fetches this config at runtime.
Do not create or modify .pre-commit-config.yaml in this repository.
For local development, run:
make testThis automatically fetches the canonical config if not present.
The pre-commit-terraform hooks call terraform from PATH. In CI the
tfroot-runner image symlinks tofu → terraform so the call resolves to
OpenTofu. Locally most developers have HashiCorp terraform from Homebrew,
which rejects tofu-only backend attributes (e.g. assume_role_duration_seconds).
make test already exports PCT_TFPATH=$(command -v tofu) so the hooks
invoke OpenTofu. For git commit-triggered pre-commit runs, either:
- use direnv:
direnv allowwill source the repo's.envrc; or - export it manually:
export PCT_TFPATH=$(command -v tofu)in your shell.
This repo uses the shared opentofu.yml workflow from shared-workflows, but with custom configuration:
- Runner:
arc-dind(self-hosted, notubuntu-latest) - Container:
ghcr.io/makeitworkcloud/tfroot-runner:latest
The self-hosted runner is required because the workflow needs SSH access to the libvirt host, which is only reachable from the runner network.
make init / make plan / make apply need:
sopsavailable locally with the team's age key (sodata.sops_file.secret_varsdecrypts)- The makefile's
libvirt-sshtarget (auto-run byinit) materializes the qemu+ssh keypair from sops into.terraform/libvirt-ssh/— no~/.ssh/id_rsaneeded tofuon PATH, plusdirenv(recommended) so.envrcexportsPCT_TFPATHfor pre-commit
Both VMs are behind the libvirt host. The cloud-init user is user, not your local username:
ssh -J user@hero.makeitwork.cloud user@192.168.102.2 # k3s
ssh -J user@hero.makeitwork.cloud user@192.168.102.12 # runnerVolume Upload Failed: unexpected EOFwhile creating boot disks — flaky upload of the ~700 MB Fedora qcow2. Just re-runmake apply; partial volumes get cleaned up automatically on retry. Boot-disk creation legitimately takes 5–7 minutes per VM.Storage volume X exists alreadyon a fresh apply — host has stale volumes (e.g. from a previous failed apply). Delete viassh user@hero "sudo virsh -c qemu:///system vol-delete --pool <pool> <volname>".sudois required. Runpool-refresh <pool>after.Storage volume not found: no storage vol with matching path …during refresh — state references a volume that was deleted out-of-band.tofu state rm <addr>and re-apply to recreate.- Boot-disk filenames are a deterministic URL hash (e.g.
k3s-94d57345.qcow2). Tofu won't recreate them when the boot image content changes server-side or when cloud-init templates change. Force a rebuild withtofu taint module.<vm>.libvirt_volume.boot module.<vm>.libvirt_volume.cloudinit module.<vm>.libvirt_cloudinit_disk.commoninit. - Cluster + runner state survives boot-disk replacement.
/var/lib/rancher(k3s) and/opt/actions-runnerare on persistent xfsextravolumes (overwrite: false). Cloud-init scripts are idempotent against this — see the[ ! -f .runner ]check in the runner template and thekubectl get … || createin the k3s template. - Pre-commit failures — the canonical config may have changed.
rm .pre-commit-config.yaml && make testfetches the latest.
images- Contains tfroot-runner image and canonical pre-commit configshared-workflows- Contains the reusable OpenTofu workflow and canonical pre-commit config