Skip to content

Configuration and Command Reference

Run commands from the repository root. Relative inventory, role, collection, and local-artifact paths depend on that working directory.

Versions and Artifacts

Component Pin or supported range Location
Host Ubuntu Desktop 26.04 LTS Ansible-managed KVM/libvirt
Terraform >= 1.10, < 2; CI uses 1.16.6 Terraform required-version constraint
libvirt provider dmacvicar/libvirt 0.8.3 Terraform versions and provider lockfile
Ubuntu base 26.04 amd64, build 20260927 Terraform image URL and SHA-256
Guests Ubuntu Server, no desktop installation Cloud-init and guest baseline role
K3s v1.37.1+k3s1 Ansible group variables; binary SHA-256
Helm 3.x; CI uses 3.22.1 Local CLI and CI setup
Monitoring chart 92.9.0 Ansible group variables; archive SHA-256
Authelia 4.39.15 Ansible group variables
Headlamp v0.35.0 Ansible group variables
Homepage v2.7.0 Ansible group variables; declarative platform config
Argo CD Chart 10.11.7, app v3.5.7 Official chart SHA-256 pin; Ansible bootstrap role
Python 3.13 or 3.14; CI uses 3.13 Project constraint and uv.lock
Ansible Core 2.20.6 Project dependencies and uv.lock
Collections ansible.posix 2.1.0, kubernetes.core 6.1.0 Ansible requirements

Image and binary pins are immutable inputs, not permission to ignore security updates. Ubuntu's package repositories evolve; package installation is convergent, not a bit-for-bit image build. Review and update version/checksum pairs together, commit lockfiles, then run all tests and the live gates.

Terraform Inputs

make init-local writes ignored .local/terraform.tfvars.json using the existing PN51 SSH public key. Make passes that file explicitly. The committed example documents overrides and deliberately contains no real key.

Input Type Default Constraint or behavior
libvirt_uri string qemu+ssh://gus@pn51/system Remote system libvirt over the existing SSH identity
host_ssh_alias string pn51 Verified jump-host alias
network_cidr string 10.51.0.0/24 Dedicated canonical IPv4 NAT /24
gateway string 10.51.0.1 Subnet's first usable address
ssh_public_keys list(string) required OpenSSH public keys, not paths or private keys
cluster_name string pn51 2-32 lowercase DNS-safe characters
node_ip_offset number 40 Four usable consecutive addresses, excluding gateway
dns_servers list(string) 1.1.1.1, 9.9.9.9 At least one reachable IPv4 resolver
vm_memory_mb number 4096 Integer 4096-6144; retain host RAM
vm_vcpus number 2 Integer 2-4 per guest
vm_disk_gb number 64 Sparse replaceable root disk, at least 32 GiB
vm_data_disk_gb number 64 Protected retained data disk, at least 32 GiB
image_path / image_checksum nullable strings pinned manifest Reviewed local path and SHA-256 overrides
pod_cidr string 10.42.0.0/16 IPv4 prefix at most /22; immutable on a running cluster
service_cidr string 10.43.0.0/16 IPv4 prefix at most /24; immutable on a running cluster

The node, pod, and service networks must not overlap. Check router, VPN, and remote-site networks too: Terraform cannot discover your external routing table. The private libvirt network supplies deterministic MACs and guest addresses; it does not reserve addresses on the physical LAN or change the Wi-Fi interface.

Changing the cluster name, network identity, imported base image, or disk inputs can be destructive or require an explicit rebuild. Always inspect the plan.

Outputs

nodes maps stable names to MACs, roles, and addresses. ansible_inventory contains one k3s_server, three k3s_agents, SSH username, guest Python path, cluster CIDRs, and the service subnet's host-10 DNS address. Neither output contains passwords, private keys, Kubernetes tokens, or kubeconfig data.

Controller Environment

Variable Purpose
UV_CACHE_DIR, ANSIBLE_HOME, TMPDIR Make keeps generated state inside .local
HELM_CONFIG_HOME, HELM_CACHE_HOME, HELM_DATA_HOME Repo-local Helm state
ANSIBLE_CONFIG Normally unset; the root configuration is selected automatically
ANSIBLE_VAULT_PASSWORD_FILE Optional absolute path outside Git; mode 0600
SSL_CERT_FILE Optional verified CA bundle for Python tooling behind a private CA
UV_SYSTEM_CERTS=true Use system trust when installing controller tools behind a private CA

Make selects an existing system CA bundle: Ubuntu's /etc/ssl/certs/ca-certificates.crt or macOS's /etc/ssl/cert.pem. An explicit SSL_CERT_FILE override is preserved. A platform-specific nonexistent path must not replace the default trust store on another operating system.

Never set provider insecure mode, disable SSH host checks, or turn off Kubernetes TLS validation to get past a trust failure. Install the correct CA or verify the host fingerprint instead.

Vault Schema

make vault generates and encrypts these fields without writing plaintext:

Key Requirement
k3s_token 64 lowercase hexadecimal characters; preserve the original for restore
grafana_admin_password At least 24 characters; generated value is longer
authelia_password Generated bootstrap password; retrieve only through a trusted terminal
authelia_password_hash Argon2id hash for the file-backed administrator
authelia_session_secret Random 64-character hex session key
authelia_storage_key Random 64-character hex storage encryption key
authelia_jwt_secret Random 64-character hex identity-validation key

Guest passwords remain locked. Grafana's secret is a bootstrap credential: changing it does not reset an already-initialized Grafana database.

make fitness-credentials appends the following fields to an existing encrypted vault without changing cluster credentials. A second run preserves them:

Key Requirement
fitness_tracker_password Generated athlete password; retrieve only in a trusted terminal
fitness_tracker_password_hash PBKDF2-SHA256 with 600,000 rounds and a random salt
fitness_tracker_api_token Random backend/proxy token; never browser configuration

An incomplete existing set is rejected rather than silently rotated. These fields stay outside Git and Argo CD resource ownership.

Commands

Command Effect Live resources?
make setup Install frozen Python dependencies and pinned local collections No
make init-local Prepare private Terraform inputs, encrypted vault, vault password, internal PKI No
make host Reconcile Ubuntu KVM/libvirt, NAT egress, and host suspend policy Yes; remote sudo prompt
make image Download and SHA-256-verify the pinned cloud image No
make init Initialize Terraform provider/backend No mutation of VMs
make plan Refresh and preview VM changes Reads remote libvirt
make apply Apply after Terraform confirmation Yes
make inventory Validate Terraform outputs and write private JSON inventory Reads state
make vault Prompt for vault password and create encrypted credentials No
make fitness-credentials Append stable application credentials to the encrypted vault No cluster mutation
make fitness-check Strict manifest/image gate; optional ALLOW_UNRELEASED=1 is draft validation only No
make fitness-deploy Verify published manifests, digest pins, and external Secrets; create only the Argo CD Application Yes
make trust-guest-keys Read verified public guest SSH keys into .local/known_hosts Reads libvirt guest agents
make tunnel Keep API forwarded to localhost:16443 No cluster mutation
make configure Reconcile server guests, K3s, monitoring, Homepage, Argo CD bootstrap, authentication, and ingress Yes
make cutover CONFIRM=retire-host-k3s Cold legacy backup, verified off-host copy, owned LAN ingress proxy Yes; guarded legacy outage
make kube-shell Open a controller shell with this lab's kubeconfig selected No mutation of cluster resources
make verify Check nodes, versions, workloads, PVCs, health, scrape targets, Helm test Read checks plus temporary Helm test pod
make idempotence Apply once, then require zero changes on a full second run Yes
make replace-worker NODE=pn51-worker-2 Plan, guard, confirm, drain, and replace only that worker Yes; destroys its root disk
make destroy Plan full destruction; protected data disks block it Requires separate reviewed removal of protection
make lint Terraform formatting, Ruff, and production Ansible lint No
make chart-check Verify checksum, lint/render real chart, parse CRDs No
make argocd-chart-check Verify official chart checksum, three CRDs, private access, and application-project scope No
make test-templates Render actual Ansible templates twice for four fake guests No
make platform-check Render and validate MFA, TLS, restricted pods, RBAC, and network policies No
make test Mocked Terraform, chart, template, and Python tests No
make check Lint, tests, and strict documentation build No
make secrets-scan Redacted Gitleaks scan of Git history No
make docs / make docs-serve Build / preview documentation No

VAULT_FILE defaults to .local/vault.yml. VAULT_ARGS defaults to --vault-password-file .local/vault-password; an operator-managed password can use make configure VAULT_ARGS='--vault-password-file /absolute/private/path'. CONFIG_FILE is unset by default; set it to .local/config.yml for optional non-secret Ansible settings. Both configuration and idempotence runs use the same file. Secret values still belong in the encrypted vault, not Make arguments. UV can select a different uv executable. Do not put secret values in Make arguments or extra-vars strings.

GitOps Bootstrap Settings

Variable Default Contract
homepage_image ghcr.io/gethomepage/homepage:v2.7.0 Version-pinned, credential-free service dashboard
argocd_chart_version 10.11.7 Update together with its reviewed SHA-256
argocd_namespace argocd Trusted bootstrap/controller namespace
argocd_application_namespace fitness-tracker Only admitted application destination
argocd_application_repository Public TheNewThinkTank homelab HTTPS Git URL Only admitted application source; no embedded credentials
fitness_tracker_enabled true Reconcile private app credentials; does not create an Application

Argo CD's initial administrator password is generated in Kubernetes and is not an Ansible Vault field. Its bootstrap, CRDs, projects, and ingress remain Ansible/Helm-owned. See GitOps ownership and service access before enabling application sync.

Optional Dotfiles

The sibling dotfiles repository owns user configuration and its node package manifest. Homelab fetches a pinned public commit and delegates configuration to that repository; it does not copy workstation dotfiles or private overrides.

Variable Default Contract
dotfiles_enabled false Enable explicitly through your private configuration
dotfiles_repository Public TheNewThinkTank dotfiles HTTPS Git URL Credential-free GitHub URL
dotfiles_revision b703d09fe26c5bd7e9daac92450f3c313a4c7c6a Reviewed full lowercase 40-character published commit SHA
dotfiles_profile personal personal or work; independent of package selection
dotfiles_home /home/ubuntu Guest account boundary; the role rejects another home
dotfiles_checkout /home/ubuntu/.local/share/dotfiles/source User-owned source, never root's personal configuration

The role requires the new --check and --package-set interfaces from the selected commit and always selects the lightweight homelab package set. It does not invoke a full workstation package installer on every run. Ansible owns APT convergence; dotfiles owns the package names, plugins, configuration, backup/restore, and official kubectl version/checksums.

kubectl is downloaded directly from dl.k8s.io, verified by SHA-256, and checked against its declared version. The selected client must be within one Kubernetes minor of K3s. An existing unrelated user-local binary is preserved and causes a clear failure rather than a silent replacement. Updating the pin may require an explicit, backed-up binary migration.

Configuration and plugin checks return 0 when current, 3 when changes are needed, and another nonzero status on errors. Ansible installs only missing plugins and deploys configuration only after a real content difference. An unchanged second run creates no new configuration backups. Full live acceptance remains required.

Monitoring Budget

Component CPU request Memory request / limit Persistent storage
Prometheus 250m 768 MiB / 2 GiB 12 GiB; 3 days or 8 GB retention
Grafana 100m 128 MiB / 512 MiB 2 GiB
Alertmanager 25m 64 MiB / 256 MiB 1 GiB
Operator 100m 128 MiB / 256 MiB None
kube-state-metrics 50m 64 MiB / 256 MiB None
Node exporter, per node 25m 32 MiB / 128 MiB None

Grafana sidecars and chart jobs add small extra requests. The storage class is homelab-retained unless the non-secret Ansible variable monitoring_storage_class selects an existing class. This does not install NFS, CSI, external storage, or backups for you.

See provisioning, operations, and monitoring access.