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>
This commit is contained in:
Thomas Peterson 2026-06-04 19:47:59 +02:00
parent 4c0aced823
commit c49ff37746
7 changed files with 106 additions and 16 deletions

View File

@ -115,3 +115,20 @@ sobald die Zugangsdaten gesetzt sind. Ohne Konfiguration ist das Feature ausgebl
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`).

View File

@ -51,29 +51,16 @@ write_files:
base64 -d /opt/secrets/private.pem.b64 > backend/config/jwt/private.pem
base64 -d /opt/secrets/public.pem.b64 > backend/config/jwt/public.pem
chmod 640 backend/config/jwt/private.pem
# SPA bauen (Profil-/QR-Links zeigen auf die öffentliche Domain)
docker run --rm -e VITE_PUBLIC_BASE="https://$DOMAIN" -v "$PWD/frontend":/app -w /app node:25-alpine sh -c "npm ci && npm run build"
chown -R 1000:1000 /opt/vcard4
# Hetzner-Privatnetz-NIC (nicht eth0) sicher per DHCP hochziehen (für DB-Zugriff).
# Manchmal kommt das private Interface beim ersten Boot nicht hoch → DB unerreichbar.
PRIV=$(ls /sys/class/net | grep -E '^(enp|ens)' | grep -v '^eth0$' | head -1 || true)
if [ -n "${PRIV:-}" ] && ! ip -4 addr show "$PRIV" | grep -q 'inet 10\.'; then
if [ -n "$${PRIV:-}" ] && ! ip -4 addr show "$PRIV" | grep -q 'inet 10\.'; then
printf '[Match]\nName=%s\n[Network]\nDHCP=ipv4\n' "$PRIV" > "/etc/systemd/network/10-$PRIV.network"
ip link set "$PRIV" up || true
systemctl restart systemd-networkd || true
for i in $(seq 1 30); do ip -4 addr | grep -q 'inet 10\.' && break; sleep 2; done
fi
COMPOSE="docker compose --project-directory /opt/vcard4 -f deploy/compose/docker-compose.prod.yml"
$COMPOSE up -d --build
sleep 20
# PHP-Abhängigkeiten installieren: vendor/ ist gitignored und /app ist als Volume
# gemountet (überdeckt ein im Image gebautes vendor) → hier in den Container hinein.
$COMPOSE exec -T -e COMPOSER_HOME=/tmp/composer php composer install --no-dev --optimize-autoloader --no-interaction --no-scripts
if [ "$RUN_MIGRATIONS" = "true" ]; then
$COMPOSE exec -T php php bin/console doctrine:migrations:migrate --no-interaction || true
# Erst-Befüllung (idempotent: überspringt, wenn admin@vcard4reseller.de existiert)
$COMPOSE exec -T php php bin/console app:seed || true
fi
$COMPOSE exec -T php php bin/console cache:clear || true
# Build + Deploy über das gemeinsame Skript (auch vom Terraform-Code-Rollout genutzt)
DOMAIN="$DOMAIN" RUN_MIGRATIONS="$RUN_MIGRATIONS" bash /opt/vcard4/deploy/update.sh
runcmd:
- bash /opt/deploy.sh > /var/log/vcard4-deploy.log 2>&1

View File

@ -0,0 +1,29 @@
# Code-Rollout auf die App-Nodes ohne Neu-Provisionierung.
# Bei Änderung von var.deploy_version (z. B. Git-SHA) führt `tofu apply` auf jedem
# App-Node ein Update aus: neuesten Code holen, SPA neu bauen, Composer/Autoloader
# auffrischen, Cache leeren Migrationen nur auf app-1. Server bleiben erhalten.
resource "terraform_data" "app_deploy" {
count = var.app_count
triggers_replace = [var.deploy_version]
depends_on = [hcloud_server.app]
connection {
type = "ssh"
host = hcloud_server.app[count.index].ipv4_address
user = "root"
private_key = file(pathexpand(var.ssh_private_key_path))
timeout = "5m"
}
provisioner "remote-exec" {
inline = [
"set -e",
"cd /opt/vcard4",
"git fetch --depth 1 origin ${var.repo_branch}",
"git reset --hard origin/${var.repo_branch}",
"DOMAIN='${var.domain}' RUN_MIGRATIONS='${count.index == 0 ? "true" : "false"}' bash /opt/vcard4/deploy/update.sh",
]
}
}

View File

@ -118,6 +118,12 @@ resource "hcloud_server" "app" {
ip = "10.0.1.${local.app_base_ip + count.index}"
}
# cloud-init zählt nur beim Erstboot; Code-Updates laufen über den Rollout
# (terraform_data.app_deploy) Server nicht wegen user_data-Änderung ersetzen.
lifecycle {
ignore_changes = [user_data]
}
depends_on = [hcloud_network_subnet.subnet, hcloud_server.db]
}

View File

@ -46,3 +46,7 @@ s3_region = "nbg1"
s3_bucket = "vcard4-card-assets"
s3_key = "OBJECT_STORAGE_ACCESS_KEY"
s3_secret = "OBJECT_STORAGE_SECRET_KEY"
# Code-Rollout (optional): Standard-Key ~/.ssh/vcard4_deploy. Zum Ausrollen:
# tofu apply -var deploy_version=$(git rev-parse --short HEAD)
# ssh_private_key_path = "~/.ssh/vcard4_deploy"

View File

@ -154,3 +154,16 @@ variable "s3_secret" {
type = string
sensitive = true
}
# --- Code-Rollout ---
variable "deploy_version" {
description = "Code-Version (z. B. Git-SHA). Ändern → 'tofu apply' rollt den Code auf alle App-Nodes aus."
type = string
default = "init"
}
variable "ssh_private_key_path" {
description = "Privater SSH-Key für den Code-Rollout (Gegenstück zu ssh_public_key)."
type = string
default = "~/.ssh/vcard4_deploy"
}

34
deploy/update.sh Normal file
View File

@ -0,0 +1,34 @@
#!/usr/bin/env bash
# Baut/aktualisiert die App auf einem App-Node aus dem bereits ausgecheckten Repo.
# Erwartet: Repo unter dem Repo-Root vorhanden, backend/.env.prod.local + JWT-Keys da.
# Aufruf: DOMAIN=<domain> RUN_MIGRATIONS=true|false bash deploy/update.sh
# Genutzt von cloud-init (Erstprovisionierung) und vom Terraform-Code-Rollout.
set -euo pipefail
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
cd "$REPO_DIR"
DOMAIN="${DOMAIN:-localhost}"
RUN_MIGRATIONS="${RUN_MIGRATIONS:-false}"
COMPOSE="docker compose --project-directory $REPO_DIR -f deploy/compose/docker-compose.prod.yml"
# SPA bauen (Profil-/QR-Links zeigen auf die öffentliche Domain)
docker run --rm -e VITE_PUBLIC_BASE="https://$DOMAIN" -v "$REPO_DIR/frontend":/app -w /app \
node:25-alpine sh -c "npm ci && npm run build"
chown -R 1000:1000 "$REPO_DIR"
$COMPOSE up -d --build
sleep 8
# PHP-Abhängigkeiten + Autoloader (vendor/ gitignored, /app als Volume gemountet →
# im Container installieren; composer install erneuert den optimierten Autoloader,
# damit neue Klassen aus dem Update gefunden werden).
$COMPOSE exec -T -e COMPOSER_HOME=/tmp/composer php \
composer install --no-dev --optimize-autoloader --no-interaction --no-scripts
if [ "$RUN_MIGRATIONS" = "true" ]; then
$COMPOSE exec -T php php bin/console doctrine:migrations:migrate --no-interaction
# Erst-Befüllung (idempotent: überspringt, wenn admin@vcard4reseller.de existiert)
$COMPOSE exec -T php php bin/console app:seed || true
fi
$COMPOSE exec -T php php bin/console cache:clear