diff --git a/deploy/README.md b/deploy/README.md index e6353f4..2578ddf 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -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/`, 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`). diff --git a/deploy/terraform/cloud-init-app.yaml.tftpl b/deploy/terraform/cloud-init-app.yaml.tftpl index 798374a..de00890 100644 --- a/deploy/terraform/cloud-init-app.yaml.tftpl +++ b/deploy/terraform/cloud-init-app.yaml.tftpl @@ -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 diff --git a/deploy/terraform/deploy.tf b/deploy/terraform/deploy.tf new file mode 100644 index 0000000..fbb3fc3 --- /dev/null +++ b/deploy/terraform/deploy.tf @@ -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", + ] + } +} diff --git a/deploy/terraform/main.tf b/deploy/terraform/main.tf index 8a04ac4..beb9059 100644 --- a/deploy/terraform/main.tf +++ b/deploy/terraform/main.tf @@ -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] } diff --git a/deploy/terraform/terraform.tfvars.example b/deploy/terraform/terraform.tfvars.example index fba592c..7e938dd 100644 --- a/deploy/terraform/terraform.tfvars.example +++ b/deploy/terraform/terraform.tfvars.example @@ -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" diff --git a/deploy/terraform/variables.tf b/deploy/terraform/variables.tf index ee5bb39..9490887 100644 --- a/deploy/terraform/variables.tf +++ b/deploy/terraform/variables.tf @@ -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" +} diff --git a/deploy/update.sh b/deploy/update.sh new file mode 100644 index 0000000..614c55a --- /dev/null +++ b/deploy/update.sh @@ -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= 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