# Repo layout & inventories [← Documentation index](README.md) ## Repo layout and role origin ```text reference-ansible/ ├── Makefile # deploy targets, OIDC login, OBJC fork workaround ├── ansible.cfg # collections_path, remote_user=root, hashi_vault auth_method=token ├── requirements.yml # community.hashi_vault + digitalboard.core (Git) ├── playbooks/site.yml # play sequence (see ansible.md) ├── scripts/bao-seed.sh # seed/merge OpenBao secrets per inventory ├── docs/ # this documentation ├── collections/ # ← installed by `make install`, gitignored │ └── ansible_collections/ │ └── digitalboard/core/ │ └── roles/ # 🔑 THE ROLES LIVE HERE, NOT in the repo root └── inventories/ ├── demo-gymburgdorf/ # reference inventory of this documentation ├── demo-mbazürich/ ├── demo-phbern/ └── vagrant/ # local test inventory with its own topology ``` > **Important:** There is **no** `roles/` directory in the repo root. All > roles come from the `digitalboard.core` collection (see > [requirements.yml](../requirements.yml)), installed via > `make install` into `./collections/`. Plays reference them by > FQCN `digitalboard.core.`. ## Available inventories | Inventory | Purpose | | --- | --- | | [`demo-gymburgdorf/`](../inventories/demo-gymburgdorf/) | Demo tenant — **recommended as the template for new tenants** | | [`demo-mbazürich/`](../inventories/demo-mbazürich/) | Demo tenant | | [`demo-phbern/`](../inventories/demo-phbern/) | Demo tenant | | [`vagrant/`](../inventories/vagrant/) | local test VMs; **incompatible group topology** with the demo inventories | ## Inventory topology (`demo-gymburgdorf`) ```mermaid flowchart LR classDef dmz fill:#fee2e2,stroke:#991b1b,color:#000 classDef app fill:#dcfce7,stroke:#166534,color:#000 classDef stor fill:#dbeafe,stroke:#1e40af,color:#000 classDef turn fill:#fef9c3,stroke:#854d0e,color:#000 subgraph ALL["group: all_servers"] direction LR subgraph DMZ["DMZ 172.16.9.0/24"] RP["reverseproxy
172.16.9.111
traefik_mode: dmz"]:::dmz TURN["turn
172.16.9.112
(no role in site.yml yet)"]:::turn end subgraph BE["Backend 172.16.19.0/24
group: backend_servers"] APP["application
172.16.19.101
traefik_mode: backend
+ authentik, authentik_outpost_ldap,
nextcloud, collabora, drawio, …"]:::app ST["storage
172.16.19.102
traefik_mode: backend
+ garage (S3)"]:::stor end end RP -.HTTPS in, HTTP out.-> APP RP -.HTTPS in, HTTP out.-> ST ``` **Group memberships (from [hosts.yml](../inventories/demo-gymburgdorf/hosts.yml)):** | Group | Members | Purpose | | --- | --- | --- | | `all_servers` | `reverseproxy`, `application`, `storage`, `turn` | base role for all hosts | | `traefik_servers` | `children: all_servers` (all 4 hosts) | Traefik everywhere; DMZ/backend via `traefik_mode` | | `backend_servers` | `application`, `storage` | sets `traefik_mode: backend` via group_var | | `garage_servers` | `storage` | single-host wrapper for the Garage role | | `nextcloud_servers`, `collabora_servers`, `drawio_servers`, `authentik_servers`, `authentik_outpost_ldap_servers`, `send_servers`, `opnform_servers`, `homarr_servers`, `bookstack_servers` | only `application` each | single-host wrappers | > **Difference from the `vagrant` inventory:** `vagrant` structures > Traefik differently — via the children groups `traefik_servers_dmz` and > `traefik_servers_backend` instead of via `backend_servers` + > `host_vars` override. The two topologies are **structurally > incompatible**; a 1:1 mapping is not possible. For new tenants, therefore, > take `demo-gymburgdorf` as the template. ## Standard folder structure of an inventory entry A fully built-out inventory follows this layout (example `demo-gymburgdorf`). Currently only this inventory is built out; `demo-mbazürich` and `demo-phbern` so far contain only `hosts.yml`. ```text inventories/demo-/ ├── hosts.yml # REQUIRED — hosts, IPs, group topology ├── group_vars/ │ ├── all/ │ │ ├── vault.yml # REQUIRED — vault_addr, vault_mount (Bao) │ │ ├── ansible.yml # ansible_python_interpreter etc. │ │ └── docker.yml # docker_registry_mirrors │ ├── traefik_servers/ │ │ └── traefik.yml # ACME/TSIG, TLS — applies to ALL Traefik instances │ └── backend_servers/ │ └── traefik.yml # traefik_mode: backend (default for app + storage) └── host_vars/ ├── reverseproxy/ │ └── traefik.yml # traefik_mode: dmz + DMZ-specific ACME overrides ├── application/ │ ├── main.yml # comment only: which services run here │ ├── traefik.yml # traefik_dmz_exposed_services (what the DMZ routes) │ └── .yml # one file per service (nextcloud, authentik, …) └── storage/ ├── main.yml # same as above ├── traefik.yml # traefik_extra_hosts + traefik_dmz_exposed_services └── garage.yml # service vars for garage ``` **Conventions:** - **`hosts.yml` is the only hard required file.** Vars are optional — if one is missing, the role defaults from `digitalboard.core` take effect. A new inventory therefore starts minimally with only `hosts.yml` (just like `demo-mbazürich`/`demo-phbern`). - **`group_vars/all/vault.yml`** is effectively required as soon as Bao lookups are supposed to work — without `vault_mount`/`vault_addr` the secret lookups fail. - **One file per service** under `host_vars//.yml`. The file name is free (Ansible loads all YAMLs in the directory); by convention it is named like the role. Which variables belong where: [ansible.md § Where parameters belong](ansible.md#where-parameters-belong). - **`main.yml` per host** is pure documentation — a comment indicating which services run on the host. Carries no productive vars. - **`host_vars//traefik.yml`** declares via `traefik_dmz_exposed_services` which local services the DMZ Traefik should make reachable from outside. The DMZ reads this list via `hostvars[]` and renders its routers from it. A new service exposed externally = a new entry here. Mechanics: [ansible.md § traefik](ansible.md#traefik). ## Walkthrough: Creating a new demo tenant Recommended template: **`demo-gymburgdorf`** (not `vagrant`, because its group topology is incompatible). 1. **Copy the inventory:** ```bash cp -r inventories/demo-gymburgdorf inventories/demo- ``` 2. **Adjust `hosts.yml`:** IPs, hostnames per host. 3. **`group_vars/all/vault.yml`** — set `vault_mount` to the new tenant mount (`demo-`). 4. **`group_vars/traefik_servers/traefik.yml`** — point `traefik_acme_dns_zone` and the `acme-tsig` lookup paths to the new zone / the new Bao path. 5. Go through **`host_vars/application/*.yml`** and **`host_vars/storage/*.yml`**: FQDNs to the new domain pattern (e.g. `*..souveredu.ch`), Bao lookup paths to `demo-/data/…`. 6. **Prepare OpenBao** (out-of-band, not via Ansible): - Create a new KV-v2 mount `demo-`. - Write secrets: `acme-tsig`, `authentik`, `nextcloud`, `garage`, … — conveniently via `make seed_bao_` (see [scripts/bao-seed.sh](../scripts/bao-seed.sh) and [secrets.md § Demo-Only-Defaults](secrets.md#demo-only-defaults--must-be-overridden)). - Policy for the deploy token: read on `demo-/data/*`. 7. **DNS:** Create the TSIG update zone (`demo-._acme.digitalboard.ch`) at `ns1.digitalboard.ch`, CNAMEs `_acme-challenge.*..` pointing there. 8. **Makefile** — add a new target modeled on `deploy_site_demo_gymburgdorf` and add it to `deploy_site_demo`; likewise a `seed_bao_` target. 9. **Smoke test:** `ansible all -i inventories/demo-/hosts.yml -m ping`. 10. **Deploy:** Bao login + `make deploy_site_demo_`.