terraform_data.app_deploy führt per remote-exec auf jedem App-Node ein Update
aus (git reset auf origin + deploy/update.sh: SPA bauen, composer, migrate(app-1),
cache:clear), getriggert über var.deploy_version (z. B. Git-SHA). Server werden
NICHT ersetzt: hcloud_server.app ignoriert user_data-Änderungen (cloud-init nur
Erstboot). Gemeinsames deploy/update.sh (cloud-init ruft es ebenfalls auf).
Fix: ${PRIV:-} in der .tftpl als $${PRIV:-} escaped (templatefile-Kollision).
Workflow: tofu apply -var deploy_version=$(git rev-parse --short HEAD)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
135 lines
6.7 KiB
Markdown
135 lines
6.7 KiB
Markdown
# Deployment auf Hetzner Cloud (Terraform)
|
||
|
||
Provisioniert einen **Multi-Node-Test** mit nachgewiesener Skalierbarkeit:
|
||
|
||
```
|
||
Internet (HTTPS)
|
||
│
|
||
vcard4-caddy ── TLS: Portal-Domain (automatisch) + Custom-Domains (On-Demand)
|
||
(Reverse-Proxy + Load-Balancing)
|
||
/ \
|
||
vcard4-app-1 vcard4-app-2 (zustandslose App-Nodes, HTTP im privaten Netz)
|
||
\ /
|
||
┌─────────┴──────────┐ ┌────────┴───────────────┐
|
||
vcard4-db (MariaDB+Vol) Hetzner Object Storage (S3, Assets)
|
||
```
|
||
|
||
**Caddy** übernimmt TLS (Let's Encrypt) und Load-Balancing — kein Hetzner-LB nötig.
|
||
Für **Custom-Domains** der Firmenkunden (§11) macht Caddy *On-Demand-TLS* und fragt
|
||
vorher `GET /internal/tls-allowed?domain=…` in der App (Portal-Domain oder verifizierte
|
||
Domain aus der DB) → schützt vor unbegrenzten Zertifikatsanfragen.
|
||
|
||
App-Nodes sind zustandslos (Code + Docker), **State** liegt in DB (eigene VM) und
|
||
Object Storage (Hintergrund-PDFs/Schriften). Dadurch beliebig horizontal skalierbar.
|
||
|
||
## Voraussetzungen (einmalig, manuell)
|
||
|
||
1. **Terraform** ≥ 1.6 + Hetzner **API-Token** (Projekt → Security → API Tokens, Read&Write).
|
||
2. **Object Storage** in der Hetzner Console anlegen: Bucket + Access-Key/Secret
|
||
(hcloud/Terraform verwalten Object Storage derzeit nicht).
|
||
3. **Git-Repo** erreichbar für die Nodes (öffentlich oder Deploy-Token in der `repo_url`).
|
||
4. **JWT-Schlüsselpaar** einmal erzeugen — auf **allen** Nodes identisch:
|
||
```bash
|
||
# lokal im backend/-Container oder mit openssl
|
||
docker compose exec php php bin/console lexik:jwt:generate-keypair --overwrite
|
||
# → backend/config/jwt/private.pem & public.pem + Passphrase aus .env (JWT_PASSPHRASE)
|
||
```
|
||
Inhalt der beiden PEM-Dateien + Passphrase in `terraform.tfvars` eintragen.
|
||
5. **DNS**: A-Record `domain → caddy_ip` (+ optional `*.zone → caddy_ip` für Subdomains).
|
||
Entweder manuell **oder** automatisch über die **Hetzner DNS API** (`manage_dns = true`
|
||
+ `hetzner_dns_token` + `dns_zone_name`; Zone muss bei Hetzner DNS liegen).
|
||
|
||
## Deploy
|
||
|
||
```bash
|
||
cd deploy/terraform
|
||
cp terraform.tfvars.example terraform.tfvars # ausfüllen
|
||
terraform init
|
||
terraform plan
|
||
terraform apply
|
||
terraform output # load_balancer_ip etc.
|
||
```
|
||
|
||
cloud-init installiert auf jedem App-Node Docker, klont das Repo, schreibt
|
||
`.env.prod.local` + JWT-Keys, baut das SPA und startet
|
||
`deploy/compose/docker-compose.prod.yml`. Migrationen laufen **nur** auf `app-1`.
|
||
Deploy-Log auf dem Node: `/var/log/vcard4-deploy.log`.
|
||
|
||
## Skalierbarkeit verifizieren
|
||
|
||
```bash
|
||
# 1) Login über die Domain (Caddy → App-Nodes)
|
||
TOKEN=$(curl -s -X POST https://<domain>/api/login \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{"email":"admin@vcard4reseller.de","password":"admin"}' | jq -r .token)
|
||
|
||
# 2) Health über Caddy (round-robin auf beide Nodes)
|
||
for i in $(seq 1 6); do curl -s https://<domain>/health; echo; done
|
||
|
||
# 3) Cross-Node-Beweis: Hintergrund über die Domain hochladen, dann
|
||
# auf BEIDEN Nodes rendern (per SSH) – identische PDFs aus Object Storage:
|
||
ssh root@<app-1-ip> 'cd /opt/vcard4 && docker compose -f deploy/compose/docker-compose.prod.yml exec -T php php bin/console app:render-card erika-mustermann'
|
||
ssh root@<app-2-ip> 'cd /opt/vcard4 && docker compose -f deploy/compose/docker-compose.prod.yml exec -T php php bin/console app:render-card erika-mustermann'
|
||
```
|
||
|
||
## Skalieren
|
||
|
||
`app_count` erhöhen → `terraform apply` legt neue App-Nodes an. **Achtung:** die
|
||
Caddy-Upstreams stehen in der Caddyfile (per cloud-init beim ersten Boot gerendert) —
|
||
neue Nodes werden **nicht automatisch** aufgenommen. Optionen: Caddy-Node neu erstellen
|
||
(`terraform taint hcloud_server.caddy && apply`) oder Caddyfile auf dem Caddy-Node
|
||
aktualisieren + `docker exec caddy caddy reload`. (Später besser: Caddy-Config-Templating
|
||
per CI oder Service-Discovery.)
|
||
|
||
## Noch offen / Hinweise
|
||
|
||
- **TLS**: erledigt Caddy (Portal automatisch, Custom-Domains On-Demand). Erste
|
||
Zertifikatsausstellung dauert ein paar Sekunden nach korrektem DNS.
|
||
- **Trusted Proxies**: für korrekte absolute URLs hinter Caddy
|
||
`framework.trusted_proxies` auf `%env(TRUSTED_PROXIES)%` setzen.
|
||
- **Host-basiertes Routing** (Custom-Domain → richtige Firmenseite) ist §11-Folgearbeit;
|
||
Caddy stellt bereits Zertifikate für verifizierte Domains aus.
|
||
- **Seed**: optional einmalig `app:seed` auf `app-1` für Demo-Daten.
|
||
- **Updates**: neuen Stand ausrollen = auf den App-Nodes `git pull` + `docker compose ... up -d --build` (später per CI/Skript).
|
||
|
||
## Wallet-Pässe (Apple / Google) — optional
|
||
|
||
> **Ausführliche Schritt-für-Schritt-Anleitung: [`docs/WALLET-SETUP.md`](../docs/WALLET-SETUP.md).**
|
||
|
||
Auf der öffentlichen Profilseite erscheint ein QR „Zur Wallet hinzufügen" (Landing `/w/{code}`),
|
||
sobald die Zugangsdaten gesetzt sind. Ohne Konfiguration ist das Feature ausgeblendet.
|
||
|
||
**Apple Wallet** (kostenpflichtiger Apple-Developer-Account):
|
||
1. Pass Type ID anlegen, Zertifikat erzeugen → als PEM exportieren (`cert.pem` + `key.pem`).
|
||
2. Apple **WWDR**-Zwischenzertifikat als PEM (`wwdr.pem`).
|
||
3. PEM-Dateien außerhalb des Webroots ablegen, Env setzen:
|
||
`APPLE_WALLET_PASS_TYPE_ID`, `APPLE_WALLET_TEAM_ID`, `APPLE_WALLET_CERT_PATH`,
|
||
`APPLE_WALLET_KEY_PATH`, `APPLE_WALLET_KEY_PASSWORD`, `APPLE_WALLET_WWDR_PATH`, `APPLE_WALLET_ORG_NAME`.
|
||
|
||
**Google Wallet** (kostenlos):
|
||
1. In der Google Cloud Console die **Wallet API** + einen **Issuer** anlegen, Service-Account
|
||
mit Rolle „Wallet Object Issuer" erstellen, JSON-Key herunterladen.
|
||
2. Env setzen: `GOOGLE_WALLET_ISSUER_ID`, `GOOGLE_WALLET_SERVICE_ACCOUNT` (Pfad zur JSON),
|
||
optional `GOOGLE_WALLET_CLASS_SUFFIX`.
|
||
|
||
Hinweis: Selbstsignierte Test-Zertifikate erzeugen ein technisch valides `.pkpass`/JWT,
|
||
werden aber von Apple/Google **nicht akzeptiert** — für die Produktion echte Zugangsdaten nötig.
|
||
Over-the-air-Sync (APNs/Objekt-Patch) ist noch nicht umgesetzt (nur Pass-Erstellung).
|
||
|
||
## Code-Update ausrollen (ohne Neu-Provisionierung)
|
||
|
||
Nach `git push` den neuen Code auf die laufenden App-Nodes bringen:
|
||
|
||
```bash
|
||
cd deploy/terraform
|
||
tofu apply -var deploy_version=$(git rev-parse --short HEAD)
|
||
```
|
||
|
||
`terraform_data.app_deploy` (per `var.deploy_version` getriggert) führt auf jedem
|
||
App-Node aus: `git fetch`/`reset` auf `origin/<branch>`, SPA neu bauen, Composer/
|
||
Autoloader auffrischen, Cache leeren – Migrationen + Seed nur auf app-1
|
||
(`deploy/update.sh`). Die Server bleiben erhalten (cloud-init `user_data` ist via
|
||
`ignore_changes` eingefroren; es zählt nur beim Erstboot). Gleiche `deploy_version`
|
||
= kein Rollout; neuer Git-SHA = Rollout. SSH-Key: `ssh_private_key_path`
|
||
(Default `~/.ssh/vcard4_deploy`).
|