Deep, code-grounded tree-docs guidebook under vibe/guidebooks/factory-provisioning/, explored from the actual playbooks/roles and tofu code: - Hub: the two provisioning engines (operator-run Ansible vs CI-applied OpenTofu), a green-field bring-up flow, master index, maintenance rule. - ansible/ sub-tree: ordered pages 01-system .. 06-recover, an inventory & variables concept page, and a Tier-1/Tier-2 roles reference (hashicorp_vault, step_ca, crowdsec, pihole, deploy_docker_compose + the gitea_* family and helpers). - opentofu/ sub-tree: factory-iac (Cloudflare/OVH/GCP/Gitea/Vault edge + cloudflare_token module), postgres-iac (per-app DB/role/pgbouncer lookup), ci-apply-flow (Gitea OIDC-JWT -> Vault -> auto-approve apply). Cross-linked bidirectionally with the lab-ecosystem guidebook and the safe-env ADR/PRD (the sandbox rehearses exactly these engines). 14 mermaid diagrams MCP-validated; zero dead links. Authored by the Lab Cartographer cohort. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
10 KiB
vibe > Guidebooks > Factory provisioning > Ansible > Inventory & variables
Inventory & variables
Note
Status: ✅ active · Last Updated: 2026-06-23 Upstream: Ansible sub-hub · Lab ecosystem · 01 factory Downstream: Roles reference Related: Secrets & Vault · Storage & recovery · Naming conventions · ADR-0001 safe prod-like environment · PRD · isolation boundary
The inventory is the single source of truth for which machines exist and which service each machine runs. It is a directory inventory — inventory/hosts.yml plus a layered group_vars/ tree — passed to every playbook with -i ansible/arcodange/factory/inventory.
Important
This inventory describes live production. The three IPs
192.168.1.201-203are the real Pis that run the public CMS, the Dolibarr ERP, and business email. A playbook pointed at this inventory mutates prod. The safe-environment work treats this file as the prod blast-radius and requires a separate sandbox inventory + a prod-IP guard before any sandbox apply — see the ADR-0001 and the first row of the PRD isolation boundary.
Hosts
Defined in inventory/hosts.yml. Three physical Pis are each reachable two ways — over the LAN (the canonical path) and through an internet port-forward managed at the firewall — plus the control node as localhost.
| Host | ansible_host |
preferred_ip |
Port | Reach |
|---|---|---|---|---|
pi1 |
pi1.home |
192.168.1.201 |
22 | LAN |
pi2 |
pi2.home |
192.168.1.202 |
22 | LAN |
pi3 |
pi3.home |
192.168.1.203 |
22 | LAN |
internetPi1 |
rg-evry.changeip.co |
— | 51022 |
WAN port-forward → pi1 |
internetPi2 |
rg-evry.changeip.co |
— | 52022 |
WAN port-forward → pi2 |
internetPi3 |
rg-evry.changeip.co |
— | 53022 |
WAN port-forward → pi3 |
localhost |
(local connection) | — | — | control node |
Note
The
internetPiNentries share one DNS name (rg-evry.changeip.co) and differ only by SSH port (5N022). The hosts file documents the choice ofchangeip.cooverarcodange.duckdns.org: changeip is managed directly with the firewall rather than depending on a DuckDNS registry update, so the forward is stable.preferred_ipis a custom hostvar (not a connection variable) — roles read it to build DNS records, the Gitea SSH domain, and the Pi-hole local-DNS table.
Groups
Groups map machines to roles. The membership is small and deliberate; read the table as "this service runs on these hosts".
| Group | Members | Defined as | What it is for |
|---|---|---|---|
raspberries |
pi1, pi2, pi3 + internetPi1-3 |
explicit hosts | Every Pi, LAN and WAN handles. Carries the shared ansible_user: pi. |
local |
localhost, pi1, pi2, pi3 |
explicit hosts | The control-node-facing group; localhost runs kubectl/tofu/docker tasks that talk to the cluster. |
postgres |
pi2 |
explicit host | The single PostgreSQL node. pi2 is the database host. |
gitea |
pi2 (via children: postgres) |
child of postgres |
Gitea co-locates with its database, so the group simply inherits postgres. groups.gitea[0] resolves to pi2 everywhere. |
pihole |
pi1, pi3 |
explicit hosts | The HA DNS pair (Pi-hole + Gravity Sync). |
step_ca |
pi1, pi2, pi3 |
explicit hosts | Every Pi runs a step-ca node (primary pi1, standbys pi2/pi3). |
all |
everything (children: raspberries) |
implicit + child | Ansible's universal group; group_vars/all/ applies to all hosts. |
Tip
Because
giteais a child ofpostgresandpostgreshas exactly one host, every reference togroups.gitea[0](the Gitea container, the API base URLhttp://{{ groups.gitea[0] }}:3000, the SSH domain) points atpi2. Move Postgres and Gitea follows automatically.
Connection variables
| Variable | Where set | Value / effect |
|---|---|---|
ansible_user |
raspberries.vars |
pi — the SSH login on every Pi. |
ansible_ssh_extra_args |
per-host (pi1/pi2/pi3) |
-o StrictHostKeyChecking=no — Pis get reimaged, so host-key churn is expected; the check is disabled rather than forcing known_hosts edits. |
ansible_port |
internetPiN |
51022 / 52022 / 53022 — the firewall's per-Pi SSH forwards. |
ansible_connection |
localhost |
local — run on the control node, no SSH. |
ansible_python_interpreter |
localhost |
"{{ ansible_playbook_python }}" — uses the uv-managed venv's Python, no hardcoded path. |
The control-node tooling chain (scp_if_ssh = True) is set in ansible.cfg; the collections_path lives there too.
group_vars/ layering
Variables are split by group so each service owns its own file. The path group_vars/<group>/<file>.yml is auto-loaded for every host in <group>.
| File | Scope | Declares |
|---|---|---|
all/common.yml |
all hosts | user_home — the control user's $HOME, looked up from the environment. |
all/ssh.yml |
all hosts | SSH-public-key discovery: first_found over id_ed25519_arcodange.pub → id_ed25519.pub → id_rsa.pub, then splits the file into ssh_public_key, ssh_key_title, ssh_key_algorithm. Roles push this key to authorized hosts. |
all/gitea.yml |
all hosts | gitea_secret_propagation_users: [arcodange] — user namespaces that must also receive org-level Gitea Action secrets (see the gitea_secret role). |
gitea/gitea.yml |
gitea |
gitea_version: 1.25.5, the gitea_database triple, and the full Gitea Docker Compose: Postgres backend (postgres:5432), the smtps/orange.fr mailer, SSH on 2222:22, ROOT_URL https://gitea.arcodange.lab/, registration disabled. SSH domain is built from hostvars[groups.gitea[0]].preferred_ip. |
gitea/gitea_vault.yml |
gitea |
VAULTED. The gitea_vault.* map — GITEA__mailer__PASSWD (consumed by the compose above) plus the github_api_token / gitlab_api_token read by the mirror roles. |
postgres/postgres.yml |
postgres |
The Postgres Docker Compose — postgres:16.3-alpine, 5432:5432, data under /home/pi/arcodange/docker_composes/postgres/data — plus the pgbouncer auth-user block. |
step_ca/step_ca.yml |
step_ca |
step_ca_primary: pi1, step_ca_fqdn: ssl-ca.arcodange.lab, the step user/home/dir, and step_ca_listen_address: ":8443". |
step_ca/step_ca_vault.yml |
step_ca |
VAULTED. vault_step_ca_password (the CA root password) and vault_step_ca_jwk_password (the cert-manager JWK provisioner password). |
Note
Encrypted files are conventionally suffixed
_vault.yml. They are normalgroup_varsfiles whose contents areansible-vault-encrypted; non-vault siblings hold the plaintext structure that references the vaulted keys (e.g.gitea/gitea.ymlinterpolatesgitea_vault.GITEA__mailer__PASSWD).
The vault model
Two distinct mechanisms share the word "vault" here — keep them apart:
ansible-vaultencrypts the*_vault.ymlfiles at rest in git (AES256). Decryption happens transparently at playbook runtime.- The vault password itself is never on disk.
ANSIBLE_VAULT_PASSWORD_FILEpoints at a tiny executable that fetches the password from the K8s secretarcodange-ansible-vaultin thekube-systemnamespace:
kubectl get secret -n kube-system arcodange-ansible-vault \
--template='{{index .data.pass | base64decode}}'
So decrypting any *_vault.yml requires kubectl access to the live cluster — the cluster is the key custodian. The setup recipe (and the kubectl create secret to seed it) lives in ansible/README.md; how this fits the broader secret hierarchy is in Secrets & Vault.
Caution
This is not HashiCorp Vault. HashiCorp Vault (
vault.arcodange.lab) is a separate, cluster-resident service installed by thehashicorp_vaultrole in the04 · Toolsstage. Thearcodange-ansible-vaultK8s secret only holds theansible-vaultpassword and is also read by the Gitea CI runners for the mailer.
Why this page matters for safe-prod
The variables above bind Ansible directly to live infrastructure: the host IPs, the prod Vault address, the prod Postgres superuser, and the prod Gitea forge. The safe-environment design maps each of these to a sandbox control — a parallel inventory/sandbox/hosts.yml with VM/cloud hosts, a pre-task guard that aborts on any 192.168.1.201-203 target unless i_mean_prod=true, and per-service overrides — detailed in the PRD isolation boundary. Until that lands, assume every run is a prod run.