chore: add new role for OpnForm

This commit is contained in:
Tobias Wüst 2026-05-13 17:23:34 +02:00 committed by Simon Bärlocher
parent 14c81657d7
commit eb51b6a054
No known key found for this signature in database
GPG key ID: 63DE20495932047A
10 changed files with 600 additions and 0 deletions

126
roles/OpnForm/README.md Normal file
View file

@ -0,0 +1,126 @@
# opnform
Deploy [OpnForm](https://github.com/OpnForm/OpnForm) as a self-contained
Docker Compose stack behind Traefik.
## What this role does
- Deploys the full official OpnForm stack: `api`, `api-worker`, `api-scheduler`,
`ui`, `db` (Postgres), `redis`, and `ingress` (nginx)
- Configures all environment variables for self-hosted production use
- Integrates the ingress container with an existing Traefik proxy network
- Waits for the API container to become healthy before returning
## What this role does NOT do (stage 1)
- Does not pre-create an admin user (use the default credentials below)
- Does not pre-configure OIDC / identity_connections — set up via Admin UI
## Architecture note: why two reverse proxies?
```
Browser → Traefik (TLS, host routing) → ingress-nginx → api (PHP-FPM) / ui (Nuxt)
```
The `ingress` container looks like a redundant proxy next to Traefik but
does a different job. OpnForm's `api` image is **PHP-FPM only** — it
speaks the FastCGI protocol on port 9000, not HTTP. Traefik cannot
translate FastCGI, so the ingress nginx is required to:
- Translate HTTP `/api/*` requests into FastCGI calls to `api:9000`
- Rewrite request URIs via the `$api_uri` map
- Set Laravel-specific FastCGI params (`SCRIPT_FILENAME`, `REQUEST_URI`)
- Reverse-proxy `/` to the Nuxt UI container on port 3000
Both containers run on the same Docker network on the same host, so the
performance overhead of the extra hop is negligible (in-kernel memory
copy, not a real network round-trip). Removing the ingress would require
a custom OpnForm image with a built-in HTTP server, which is out of
scope for this role.
## Required variables
Provide via OpenBao, Ansible Vault, or extra-vars. **Never commit real
secrets to version control.**
| Variable | Format | Generate with |
|---|---|---|
| `opnform_app_key` | `base64:<32 bytes base64>` | `echo "base64:$(openssl rand -base64 32)"` |
| `opnform_jwt_secret` | 32 bytes base64 | `openssl rand -base64 32` |
| `opnform_front_api_secret` | 32 bytes base64 | `openssl rand -base64 32` |
| `opnform_db_password` | strong password | `openssl rand -base64 24` |
When `opnform_oidc_enabled` is `true`:
| Variable | Source |
|---|---|
| `opnform_oidc_client_secret` | from your Keycloak/Authentik client |
The `assert` task at the top of the role will fail fast if any secret is
missing or malformed.
## First login
After the role completes, OpnForm seeds a default admin user. Visit
the URL in `opnform_base_url` and log in with:
- Email: `admin@opnform.com`
- Password: `password`
On first login OpnForm will prompt you to change email and password.
Self-hosted instances disable public registration after this — invite
further users via the Admin UI.
### If the login does not respond
The DB seed may have failed. Re-run it manually:
```bash
cd /etc/docker/compose/opnform
docker compose exec api php artisan migrate:refresh --seed
docker compose exec api php artisan app:init-project
```
## OIDC setup (stage 2, not yet automated)
Manual setup via the Admin UI is currently the supported path:
1. Settings → Identity Connections → Add Connection
2. Provider: OIDC
3. Issuer: `https://auth.digitalboard.ch/realms/Digitalboard`
4. Client ID / Secret: from your Keycloak client
5. Add Group Role Mapping: Entra/Keycloak group Object ID → OpnForm role
Direct DB manipulation of `identity_connections` / `group_role_mappings`
is possible but fragile across OpnForm versions. A future iteration of
this role may automate it.
## Example playbook
```yaml
- name: Deploy OpnForm service
hosts: opnform_servers
become: true
roles:
- digitalboard.core.opnform
```
With inventory variables:
```yaml
# group_vars/opnform_servers.yml
opnform_domain: forms.digitalboard.ch
opnform_base_url: "https://forms.digitalboard.ch"
opnform_app_key: "{{ lookup('community.hashi_vault.vault_kv2_get',
'digitalboard/opnform',
mount_point='kv').data.data.app_key }}"
opnform_jwt_secret: "{{ lookup('community.hashi_vault.vault_kv2_get',
'digitalboard/opnform',
mount_point='kv').data.data.jwt_secret }}"
opnform_front_api_secret: "{{ lookup('community.hashi_vault.vault_kv2_get',
'digitalboard/opnform',
mount_point='kv').data.data.front_api_secret }}"
opnform_db_password: "{{ lookup('community.hashi_vault.vault_kv2_get',
'digitalboard/opnform',
mount_point='kv').data.data.db_password }}"
```