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>
19 KiB
vibe > Guidebooks > Factory provisioning > Ansible > Roles reference
Roles reference
Note
Status: ✅ active · Last Updated: 2026-06-23 Upstream: Ansible sub-hub · Lab ecosystem · 01 factory Downstream: Inventory & variables Related: Secrets & Vault · Storage & recovery · Naming conventions · ADR-0001 safe prod-like environment
Roles live in two places, by reuse scope:
- Shared roles — reusable across stages — live in
ansible/arcodange/factory/roles/and are referenced by FQCNarcodange.factory.<role>. - Nested roles — owned by one playbook stage — live under
playbooks/<stage>/roles/and are auto-discovered by that stage's playbook.
This page is split by altitude. Tier 1 covers the heavyweight platform-service roles (one subsection each); Tier 2 is a single table of the smaller building-block roles.
Tier 1 — platform-service roles
hashicorp_vault
playbooks/tools/roles/hashicorp_vault · runs on localhost in the 04 · Tools stage. It initializes and unseals the cluster Vault and wires Gitea as an OIDC provider so CI jobs can authenticate to Vault.
The tasks/main.yml flow is:
- Init (
init.yml) — first run only. Lists the Vault server pods in thetoolsnamespace, checksvault operator init -status, and if uninitialized runsvault operator initwithkey-shares=1,key-threshold=1(defaults fromdefaults/main.yml). The JSON output — unseal keys + initial root token — is written to~/.arcodange/cluster-keys.json(dir0700, file0600). - Unseal (
unseal.yml) — required after every reboot. Reads the keys file and runsvault operator unsealfor each server, then revokes the initial root token (idempotent — tolerates an already-revoked token). - Generate a fresh root token (
new_root_token.yml) — runs thegenerate-rootOTP/nonce dance using the unseal keys to mint a short-livedvault_root_token. - Set up Gitea OIDC (
gitea_oidc_auth.yml) — drives Gitea through the bundledplaywright_setupGiteaApp.js(via theplaywrightrole) to create an OAuth2 app, then applies the bundled OpenTofuhashicorp_vault.tfinside a disposableghcr.io/opentofu/opentofucontainer (state on a throwaway docker volume) to provision the Vault JWT/OIDC backend. Finally it rendersoidc_jwt_token.sh.j2into the Gitea Actions secretvault_oauth__sh_b64(base64) at org scope, then propagates the same secret to each user ingitea_secret_propagation_users(Action secrets are per-owner, so user-owned repos can't read org secrets). - Revoke the temp root token — the
alwaysblock ofmain.ymlrevokesvault_root_tokenno matter how step 4 ended, so no long-lived root token survives the run.
| Var | Default | Meaning |
|---|---|---|
vault_unseal_keys_path |
~/.arcodange/cluster-keys.json |
Where unseal keys + root token are stored. |
vault_unseal_keys_shares / _key_threshold |
1 / 1 |
Single-key seal (lab posture; threshold <= shares). |
vault_address |
https://vault.arcodange.lab |
The cluster Vault endpoint. |
gitea_admin_user / gitea_admin_password |
arcodange@gmail.com / (prompted) |
Credentials Playwright uses to create the OAuth app. |
vault_oidc_force_reset |
false |
When true, vault auth disable gitea + gitea_jwt before re-applying. |
Caution
vault_oidc_force_reset=trueis destructive: it disables and wipes allgitea_cicd_*per-app JWT roles created by the bundled tofu, every run. Default is off. Likewise, losing~/.arcodange/cluster-keys.jsonmeans the Vault can never be unsealed again — that file is the single point of failure for the whole secret plane (see Secrets & Vault).
step_ca
playbooks/ssl/roles/step_ca · runs on the step_ca group (all three Pis) in the 01 · System stage via ssl/step-ca.yml. It is the lab's internal ACME/CA for *.arcodange.lab certificates, run active/standby: primary pi1, replicas pi2/pi3. The tasks/main.yml imports five task files in order:
- install — install the
step/step-cabinaries. - init (
init.yml) — primary only.step ca init(non-interactive, password file) withcreates:guard so it is idempotent. The CA name isArcodange Lab CA, DNSssl-ca.arcodange.lab, listen:8443. - sync (
sync.yml) — replicates the CA from primary to standbys. It takes a lockfile on the primary (.sync.lock), computes a deterministictar | sha256sumchecksum of~/.step, compares it to the last checksum cached on the controller, and onlyrsyncs (pull → controller → push to standbys) when the checksum changed. This is how the standbys hold an identical CA without a shared filesystem. - systemd — install/enable the
step-caunit (therestart step-cahandler fires on cert/config change). - provisioners (
provisioners.yml) — primary only. Ensures a JWK provisioner namedcert-managerexists: lists provisioners, generates the JWK keypair (creates:guard) under~/.step/provisioners/, andstep ca provisioner adds it. This is what lets in-cluster cert-manager request certs from the CA.
| Var | Default | Meaning |
|---|---|---|
step_ca_primary |
pi1 |
The writable CA node; standbys sync from it. |
step_ca_fqdn |
ssl-ca.arcodange.lab |
CA DNS name; URL is https://{fqdn}:8443. |
step_ca_provisioner_name / _type |
cert-manager / JWK |
The cert-manager provisioner. |
step_ca_force_reinit |
false |
When true, stops the service and wipes ~/.step before re-init. |
| Secret | Source |
|---|---|
vault_step_ca_password |
CA root password — from vaulted step_ca/step_ca_vault.yml. |
vault_step_ca_jwk_password |
cert-manager JWK provisioner password — same vaulted file. |
Caution
step_ca_force_reinit=truewipes the entire CA (~/.step) on the primary and re-issues a new root — every previously issued*.arcodange.labcert immediately becomes untrusted until clients reload the new root. Use only for a deliberate PKI rebuild.
crowdsec
playbooks/tools/roles/crowdsec · runs on localhost in the 04 · Tools stage. It wires CrowdSec's decisions into Traefik as a bouncer middleware with a Turnstile CAPTCHA. The tasks/main.yml flow:
- Vault → K8s secret plumbing — creates a
ServiceAccount(factory-ansible-tool-crowdsec-traefik-plugin), aVaultAuth(kubernetes auth, rolefactory_crowdsec_conf), and aVaultStaticSecretthat readskvv2/cms/factory/turnstileinto a K8s secret (refreshAfter: 30s). The Turnstile sitekey/secret come from there. - Bouncer key — finds the CrowdSec LAPI pod in
toolsand runscscli bouncers add traefik-plugin(deletes + re-adds on conflict) to obtain the bouncer API key. - CAPTCHA HTML —
inject_captcha_html.ymlpushescaptcha.htmlinto the Traefik PVC; this task is taggednever(opt-in only) so the default run skips it. - Traefik Middleware — applies a
traefik.io/v1alpha1Middlewarenamedcrowdsec-bouncer(crowdsecinkube-system) configured with the bouncer key, stream mode, Turnstile (captchaProvider: turnstile+ site/secret keys), and a Redis cache atredis.tools:6379. - Restart Traefik — scales the Traefik Deployment to 0 then back to 1 (with a
rescue/alwaysguard guaranteeing it scales back up) to load the new middleware.
| Var | Default | Meaning |
|---|---|---|
traefik_pvc_name |
traefik |
The PVC the (tagged-never) captcha.html inject targets. |
| Secret | Source |
|---|---|
| Turnstile sitekey + secret | Vault kvv2/cms/factory/turnstile, surfaced via VaultStaticSecret. |
| Bouncer API key | Minted at runtime by cscli bouncers add. |
pihole
playbooks/dns/roles/pihole · runs on the pihole group (pi1, pi3) in the 01 · System stage. It configures HA DNS: two Pi-hole nodes kept in sync. The tasks/main.yml includes three task files:
ha_pihole_setup.yml— waits for a manual Pi-hole install (it prints thecurl … | sudo bashcommand andwait_fors/etc/pihole/pihole-FTL.dbfor up to 10 minutes; Pi-hole itself is not installed by Ansible). It then patchespihole.toml(listen port,listeningMode = "ALL", enable/etc/dnsmasq.d) and writes three dnsmasq drop-ins:10-custom-rules.conf(wildcardaddress=/fqdn/ipfrompihole_custom_dns),20-rpis.conf(<host>.home→preferred_ipfor every Pi), and99-upstream.conf(explicit upstream frompihole_upstream_dns).gravity_setup.yml— sets up Gravity Sync between the two nodes: apihole_gravitysystem user with a freshly rotated ed25519 keypair each run, cross-authorizedauthorized_keys, full sudo (/etc/sudoers.d/gravity-sync), the installer, and a generatedgravity-sync.conf(each node pointsREMOTE_HOSTat the other), then runs the sync.client_setup.yml— points DNS clients at the Pi-hole pair by editing/etc/resolv.conf(insert nameservers aftersearch) and the active NetworkManager connections vianmcli(per-interfaceipv4.dns+dns-priority, eth0 50 / wlan0 100).
| Var | Default | Meaning |
|---|---|---|
pihole_primary |
pi1 |
First node; the other is derived as the secondary. |
pihole_ports |
8081o,443os,… |
Web-interface listen ports. |
pihole_custom_dns |
{} |
FQDN→IP wildcard records (validated as IPv4). |
pihole_upstream_dns |
[8.8.8.8, 1.1.1.1, 8.8.4.4] |
Explicit upstreams (avoids DHCP-provided DNS). |
Warning
This role is not fully idempotent: it depends on a human running the Pi-hole installer first, it rotates the gravity SSH key on every run, and it grants the
pihole_gravityuser passwordless sudo ALL. Treat reruns as state-changing, not no-ops.
deploy_docker_compose
roles/deploy_docker_compose · shared. This is the generic compose mechanism every app deploy builds on. The caller passes a dockercompose_content dict; the tasks/main.yml:
- Derives
app_namefromdockercompose_content.nameand creates/<root_path>/<partition>/<app_name>/plusdata/andscripts/. - Writes the compose file with
to_nice_yamland validates it withvalidate: 'docker compose -f %s config'— a bad compose fails the task before anything is written live. - Writes a small wrapper script
scripts/docker-composethat runsdocker compose -f <the file> "$@", so the app can be driven without remembering the path.
| Var | Default | Meaning |
|---|---|---|
app_name |
(dockercompose_content.name) |
App directory name. |
app_owner / app_group |
pi / docker |
File ownership. |
root_path |
/home/pi/arcodange |
Base path; partition (docker_composes) nests under it. |
Tier 2 — building-block roles
Smaller roles, mostly Gitea/forge plumbing and one-shot helpers. Shared roles live in roles/; deploy_gitea/deploy_postgresql are nested under playbooks/setup/roles/.
| Role | Purpose | Key vars / notes | Secrets |
|---|---|---|---|
gitea_repo |
Ensure a repo exists across Gitea + GitHub + GitLab and add 8h push mirrors (sync_on_commit: true) to GitHub/GitLab. |
Creates missing repos on each forge; mirror URLs + namespace IDs in vars/main.yml. |
github_api_token, gitlab_api_token (from gitea_vault). |
gitea_token |
Generate / replace / delete a Gitea access token via docker exec … gitea admin user generate-access-token. |
Stores the raw token in the fact named by gitea_token_fact_name; gitea_token_replace / gitea_token_delete toggles; scopes default to write:admin,organization,package,repository,user. |
The minted token itself (a fact, not persisted). |
gitea_secret |
PUT a Gitea Actions secret at user or org scope. |
gitea_secret_name / _value; gitea_owner_type (user|org) selects the API path. |
gitea_api_token (Authorization). |
gitea_sync |
List repos on all three forges, diff them, and call gitea_repo for the repos missing somewhere. |
Computes repos_incomplete = all − common; loops gitea_repo over the gaps. |
GitHub/GitLab/Gitea API tokens. |
traefik_certs |
Extract the live *.arcodange.lab cert from Traefik's acme.json. |
kubectl exec into Traefik → jq the LetsEncrypt wildcard cert → traefik_cert_pem fact; no-op if already set. |
— (reads in-cluster acme.json). |
playwright |
Run a Playwright browser-automation script in Docker. | Builds playwright:<version> (default 1.47.0) from files/, runs the script with playwright_env injected as -e; default script loginGitea.js. Used by hashicorp_vault for the OIDC app setup. |
Script-specific env (e.g. Gitea admin creds). |
deploy_gitea |
Deploy Gitea: template app.ini.j2, docker compose up, then health-check :3000 until ready. |
Compose source is /home/pi/arcodange/docker_composes/gitea; admin user arcodange. |
(consumes the vaulted Gitea compose env). |
deploy_postgresql |
Deploy Postgres via compose, then per-app create DB + user (create_db_and_user.yml). |
Waits on pg_isready, loops applications_databases ({app: {db_name, db_user, db_password}}). |
Per-app DB passwords from applications_databases. |
Role dependency view
How the roles relate: shared building blocks feed the setup-stage app deploys, and a few platform-service roles include shared roles directly.
%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#1f2937','primaryTextColor':'#f9fafb','lineColor':'#6b7280','fontSize':'14px'}}}%%
flowchart TD
classDef shared fill:#1e3a5f,stroke:#3b82f6,color:#f9fafb;
classDef setup fill:#1e4620,stroke:#22c55e,color:#f9fafb;
classDef platform fill:#4a2c1e,stroke:#f59e0b,color:#f9fafb;
dc["deploy_docker_compose<br/>generic compose writer"]:::shared
pw["playwright<br/>browser automation"]:::shared
gt["gitea_token<br/>mint access token"]:::shared
gs["gitea_secret<br/>PUT Actions secret"]:::shared
gr["gitea_repo<br/>mirror to GitHub/GitLab"]:::shared
gsync["gitea_sync<br/>diff 3 forges"]:::shared
tc["traefik_certs<br/>extract lab cert"]:::shared
dpg["deploy_postgresql"]:::setup
dgi["deploy_gitea"]:::setup
hv["hashicorp_vault"]:::platform
sca["step_ca"]:::platform
cs["crowdsec"]:::platform
ph["pihole"]:::platform
gsync --> gr
hv --> pw
hv --> gs
dc -. "used by app deploys" .-> dpg
dc -. "used by app deploys" .-> dgi
gitea_sync→gitea_repo— the sync role include-loopsgitea_repofor each repo missing from one of the three forges.hashicorp_vault→playwright— Vault's OIDC setup drives Gitea through Playwright to create the OAuth app.hashicorp_vault→gitea_secret— the renderedvault_oauth__sh_b64is published as a Gitea Actions secret at org and user scope.deploy_docker_compose→deploy_postgresql/deploy_gitea— the generic compose writer is the substrate thesetup-stage app deploys lean on.step_ca,crowdsec,piholestand alone — they configure their own services (PKI, WAF, DNS) without including other roles.
See also
- Inventory & variables — the groups (
gitea,postgres,step_ca,pihole) these roles target, and the vaultedgroup_varsthey read. - Secrets & Vault — where
hashicorp_vault's OIDC tokens and thekvv2/cms/factory/turnstilepath fit the broader secret model. - Storage & recovery — how the compose
data/dirs and the step-ca state relate to backup and disaster recovery.