Skip to content

infra

CI

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/.

Scope

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:

Planned: Hetzner Cloud (hetznercloud/hcloud) and additional providers as required.

Prerequisites

  • OpenTofu — per-root floors: lab pins required_version = ">= 1.10"; production pins ">= 1.10.4" to avoid the 1.10.0–1.10.3 S3 SSE-on-lockfile bug, opentofu#2970; talos-lab (and the talos-cluster module itself) pins ">= 1.11" for the write-only secret arguments, ADR-0017. .opentofu-version tracks 1.12.1. The 1.10 floor matches the production S3 backend's use_lockfile requirement (ADR-0003, ADR-0011)
  • A KVM/libvirt host with qemu-system and libvirtd running; the default storage 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

Quick start

cd environments/lab
tofu init
export TF_VAR_ssh_public_key="ssh-ed25519 AAAA..."
tofu plan
tofu apply

Repository structure

infra/
├── 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

Modules

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, …}

Environments

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 lab

For 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 production

Architecture decisions

Standing 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

State safety

  • Remote backends must have encryption at rest enabled
  • Remote backends must have state locking — prefer use_lockfile = true (native S3 locking, OpenTofu 1.10+) over dynamodb_table
  • Sensitive variables marked sensitive = true and 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

Common commands

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

CI / quality

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.

Development

pip install -r requirements-dev.txt && pre-commit install
export PCT_TFPATH=tofu     # point pre-commit-terraform at OpenTofu
pre-commit run --all-files

Hook 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.

Governance

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

About

Infrastructure-as-code with OpenTofu for KVM/libvirt VMs and Talos Linux Kubernetes — CI-gated and compliance-aligned

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages