6  Configuration Management

Note

pyinfra for agentless deploys. Static files, not templates. One repo per host.

6.1 Why pyinfra

  • pyinfra is agentless: runs over SSH from a laptop, nothing installed on the target
  • Deploy scripts are Python, not YAML DSL; you get real control flow, imports, and type checking
  • Alternative considered: Ansible (larger ecosystem, but slower execution, YAML templating is painful at scale)

6.2 The instance repo

  • One Git repo per physical host (e.g., carmine for one machine)
  • Contains: Containerfile (tier 3 image), quadlet files, pyinfra deploy scripts, rootful service configs, Makefile
  • The repo is the single source of truth for what runs on that host

6.3 Repo layout

  • quadlets/{service}/.container, .pod, Caddyfile per service
  • rootful/ — Envoy and Alloy configs (the two rootful components)
  • pyinfra/ — deploy orchestration, service definitions, bootstrap tasks
  • Containerfile — instance image definition (FROM base image + users + policy)
  • Makefilemake deploy-all, make deploy-{service}, make dry-{service}

6.4 Static files over templates

  • Quadlet files and Caddyfiles are committed as-is, not generated from templates
  • What you read in the repo is exactly what lands on the host
  • Secrets are the exception: injected at runtime via podman secret, never in the repo

6.5 The deploy pattern

  • pyinfra/services.py defines every service: user, UID, FQDN, port, quadlet directory, secrets, ZFS volumes
  • pyinfra/tasks/deploy_service.py syncs quadlet files to ~/.config/containers/systemd/ for the service user, then daemon-reload
  • Rootful services (Envoy, Alloy) deploy to system-level systemd paths
  • pyinfra/tasks/bootstrap.py handles one-time setup: user creation, subuid/subgid, ZFS verification, SSH hardening, firewall, SELinux booleans

6.6 Targeting a single service

  • make deploy-forge deploys only the forge service
  • pyinfra supports --data target=forge to filter the service list
  • Useful for iterating on one service without touching the rest

6.7 Drift detection

  • CI runs ruff and py_compile on pyinfra scripts
  • Quadlet validation via systemd-analyze verify (planned)
  • The image build pipeline detects base image changes and triggers instance rebuilds