vcard4reseller/deploy/README.md
Thomas Peterson c49ff37746 Deploy: Terraform-Code-Rollout auf App-Nodes (ohne Recreate)
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>
2026-06-04 19:47:59 +02:00

135 lines
6.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`).