Automate immutable production deployments
This commit is contained in:
+89
-37
@@ -27,52 +27,87 @@ diese Datei. Er bleibt lokal in der ignorierten Datei
|
||||
`infra/opentofu/terraform.tfvars` (alternativ kann der Provider
|
||||
`TF_VAR_hcloud_token` lesen).
|
||||
|
||||
Die private SSH-Keydatei wird ebenfalls nicht gespeichert. Sie kann beim
|
||||
Deployment mit `--identity` oder über `WATERMAPS_SSH_KEY` angegeben werden.
|
||||
Falls OpenTofu keinen Output `ssh_private_key_path` bereitstellt und der Key
|
||||
nicht bereits über den SSH-Agenten verfügbar ist, ist eine dieser beiden
|
||||
Angaben erforderlich. Der vorbereitete SSH-Benutzer heißt standardmäßig
|
||||
`deploy`; die privilegierten Installationsschritte laufen über dessen
|
||||
passwortloses `sudo`.
|
||||
Die private SSH-Keydatei wird ebenfalls nicht gespeichert. Standardmäßig
|
||||
verwenden die lokalen Deployment-Skripte
|
||||
`~/.ssh/watermaps_deploy_ed25519`; alternativ kann sie mit `--identity` oder
|
||||
über `WATERMAPS_SSH_KEY` angegeben werden. Der vorbereitete SSH-Benutzer heißt
|
||||
standardmäßig `deploy`; die privilegierten Installationsschritte laufen über
|
||||
dessen passwortloses `sudo`.
|
||||
|
||||
## Gitea Actions und Container Registry
|
||||
|
||||
Der Workflow `.gitea/workflows/container-images.yml` führt Typechecks und Tests
|
||||
aus. Nach einem erfolgreichen Push auf `main` baut und veröffentlicht er zwei
|
||||
aus. Nach einem erfolgreichen Push auf `main` baut und veröffentlicht er drei
|
||||
OCI-Images unter der vollständigen Git-Commit-SHA:
|
||||
|
||||
```text
|
||||
gitea.incoso.eu/kevin_janssen/watermaps:<commit-sha>
|
||||
gitea.incoso.eu/kevin_janssen/watermaps-route-data:<commit-sha>
|
||||
gitea.incoso.eu/kevin_janssen/watermaps-release:<commit-sha>
|
||||
```
|
||||
|
||||
Das dritte Image ist ein nicht ausführbares Release-Bundle mit Compose-Datei,
|
||||
Nginx-Konfiguration, Systemd-Units, Deployment-Skripten und versionierten
|
||||
Datenbankmigrationen. Sein geprüftes Manifest bindet die exakten
|
||||
Registry-Digests von App und Routingdaten; `SHA256SUMS` schützt alle Dateien des
|
||||
Bundles. Erst nachdem alle drei Commit-Images veröffentlicht wurden, wird
|
||||
`watermaps-release:main` als einzelner atomarer Release-Pointer verschoben.
|
||||
App- und Routingdaten-Images werden nie über bewegliche Tags gestartet.
|
||||
|
||||
In Gitea müssen Repository Actions aktiviert und ein Docker-fähiger
|
||||
`ubuntu-latest`-Runner registriert sein. Unter
|
||||
`Repository → Settings → Actions → Secrets` werden benötigt:
|
||||
|
||||
- `REGISTRY_USERNAME`: Gitea-Benutzer, dem die Packages gehören
|
||||
- `REGISTRY_TOKEN`: Personal Access Token mit `package: Read and Write`
|
||||
- `REGISTRY_TOKEN`: eigener CI-Personal-Access-Token ausschließlich mit
|
||||
`write:package`
|
||||
|
||||
Der separate PAT ist derzeit nötig, weil Giteas eingebauter Job-Token
|
||||
OCI-Pakete noch nicht zuverlässig veröffentlichen kann. Für Pull Requests
|
||||
werden nur Tests ausgeführt; Registry-Secrets werden dabei nicht verwendet.
|
||||
Der separate PAT ist nötig, weil Gitea 1.26 mit dem eingebauten Job-Token noch
|
||||
keine OCI-Pakete veröffentlichen kann. Für Pull Requests werden nur Tests
|
||||
ausgeführt; Registry-Secrets werden dabei nicht verwendet.
|
||||
|
||||
Die Secrets lassen sich mit zwei getrennten, lokal ignorierten Token-Dateien
|
||||
konfigurieren:
|
||||
|
||||
- `deploy/.gitea-actions-config-token`: kurzlebiger Konfigurations-PAT nur mit
|
||||
`write:repository`, um die Actions-Secrets über die Repository-API zu setzen
|
||||
- `deploy/.gitea-registry-publish-token`: CI-PAT nur mit `write:package`, der
|
||||
anschließend als `REGISTRY_TOKEN` in Gitea Actions hinterlegt wird
|
||||
|
||||
Beide Dateien enthalten jeweils nur den Token in einer einzelnen Zeile und
|
||||
müssen exakt Dateimodus 600 haben:
|
||||
|
||||
```bash
|
||||
chmod 600 \
|
||||
deploy/.gitea-actions-config-token \
|
||||
deploy/.gitea-registry-publish-token
|
||||
./deploy/scripts/configure-gitea-actions.sh
|
||||
```
|
||||
|
||||
Das Skript nutzt den Konfigurations-PAT ausschließlich lokal für den API-Aufruf
|
||||
und speichert ihn ausdrücklich nicht als Actions-Secret. Anschließend sollten
|
||||
beide lokalen Token-Dateien gelöscht werden; Gitea zeigt den gespeicherten
|
||||
Registry-Secret-Wert nicht wieder an.
|
||||
|
||||
## Deployment
|
||||
|
||||
Nach einem erfolgreichen Image-Build liest das Skript standardmäßig den
|
||||
OpenTofu-Output `server_ipv4`. Es erlaubt ausschließlich einen sauberen,
|
||||
vollständig committeten Git-Stand und überträgt nur den kleinen Ordner
|
||||
`deploy/` sowie das kanonische `database/schema.sql`, nicht den
|
||||
Anwendungsquellcode. Die Image-Tags entsprechen exakt `git rev-parse HEAD`.
|
||||
vollständig committeten Git-Stand und überträgt für die Ersteinrichtung nur
|
||||
Bootstrap-Dateien und die getrennte Produktionskonfiguration, nicht den
|
||||
Anwendungsquellcode. Der Server bezieht das kanonische Release anschließend
|
||||
selbst aus der Gitea Registry und prüft, dass es exakt `git rev-parse HEAD`
|
||||
entspricht.
|
||||
|
||||
Ist die Registry privat, werden einmalig beziehungsweise nach Tokenwechsel
|
||||
lokale Pull-Zugangsdaten mitgegeben:
|
||||
Pakete des öffentlichen Gitea-Benutzers können ohne Server-Credential gelesen
|
||||
werden. Wird die Registry später privat geschaltet, werden einmalig
|
||||
beziehungsweise nach Tokenwechsel lokale Pull-Zugangsdaten mitgegeben:
|
||||
|
||||
```bash
|
||||
WATERMAPS_REGISTRY_USERNAME=kevin_janssen \
|
||||
WATERMAPS_REGISTRY_TOKEN='<package-read-token>' \
|
||||
./deploy/scripts/upload-and-deploy.sh \
|
||||
--identity ~/.ssh/watermaps_hetzner_ed25519
|
||||
--identity ~/.ssh/watermaps_deploy_ed25519
|
||||
```
|
||||
|
||||
Der Token benötigt auf dem Produktionsserver nur `package: Read`. Er wird per
|
||||
@@ -84,14 +119,17 @@ Anmeldung gültig ist:
|
||||
|
||||
```bash
|
||||
./deploy/scripts/upload-and-deploy.sh \
|
||||
--identity ~/.ssh/watermaps_hetzner_ed25519
|
||||
--identity ~/.ssh/watermaps_deploy_ed25519
|
||||
```
|
||||
|
||||
Das Serverskript lädt App, den kombinierten Routing-/Feature-Daten-Builder,
|
||||
PostGIS, Nginx und Certbot mit `docker compose pull`. Die beiden commitgenauen
|
||||
Gitea-Tags werden anschließend in ihre unveränderlichen Registry-Digests
|
||||
aufgelöst. Erst danach werden die Container mit `--no-build` gestartet. Der
|
||||
Produktionsserver benötigt deshalb weder Git noch Node/npm oder den Quellcode.
|
||||
Das Serverskript lädt das Release-Manifest, App, den kombinierten
|
||||
Routing-/Feature-Daten-Builder, PostGIS, Nginx und Certbot. App und Builder
|
||||
werden ausschließlich über die im Release gebundenen Digests gestartet.
|
||||
Geordnete SQL-Dateien unter `database/migrations/` werden mit
|
||||
`watermaps_schema_migrations` protokolliert und jeweils in einer Transaktion
|
||||
ausgeführt. Neue Migrationen müssen nach dem Expand/Contract-Prinzip mit dem
|
||||
vorherigen App-Release kompatibel bleiben. Der Produktionsserver benötigt
|
||||
weder Git noch Node/npm oder einen Repository-Checkout.
|
||||
|
||||
Der erste Datenaufbau lädt die Geofabrik-Extrakte für Deutschland und die
|
||||
Niederlande, baut den Fahrroutenindex und importiert daraus die
|
||||
@@ -131,18 +169,19 @@ HTTP-Anfragen erhalten 404.
|
||||
## Rollback
|
||||
|
||||
Jedes erfolgreiche Release wird mit Commit-SHA und den aufgelösten
|
||||
Image-Digests unter `/srv/watermaps-runtime/deployments/` gespeichert. Scheitert
|
||||
ein Deployment nach dem Containerwechsel, startet `deploy.sh` automatisch das
|
||||
vorherige Release und führt die Health-, Routen- und Featuretests erneut aus.
|
||||
Ein Rollback wechselt nur die unveränderlichen App-/Builder-Images. Die
|
||||
persistente PostGIS-Datenbank bleibt erhalten; das Schema und der Import sind
|
||||
aufwärtskompatibel und idempotent.
|
||||
Image-Digests unter `/srv/watermaps-runtime/deployments/` gespeichert; sein
|
||||
vollständiges Bundle liegt unter `/srv/watermaps-data/releases/<commit>`.
|
||||
`/opt/watermaps/current` wird erst nach Health-, Routen- und Featuretests
|
||||
atomar auf ein neues Bundle umgeschaltet. Scheitert ein Deployment, wird das
|
||||
vorherige unveränderliche Bundle erneut ausgeführt und vollständig geprüft.
|
||||
Die persistente PostGIS-Datenbank bleibt erhalten; Migrationen sind deshalb
|
||||
bewusst vorwärtskompatibel zu gestalten.
|
||||
|
||||
Das unmittelbar vorherige Release lässt sich auch manuell aktivieren:
|
||||
|
||||
```bash
|
||||
./deploy/scripts/remote-rollback.sh \
|
||||
--identity ~/.ssh/watermaps_hetzner_ed25519
|
||||
--identity ~/.ssh/watermaps_deploy_ed25519
|
||||
```
|
||||
|
||||
Oder ein bestimmter, bereits erfolgreich deployter Commit:
|
||||
@@ -150,7 +189,7 @@ Oder ein bestimmter, bereits erfolgreich deployter Commit:
|
||||
```bash
|
||||
./deploy/scripts/remote-rollback.sh \
|
||||
--revision 0123456789abcdef0123456789abcdef01234567 \
|
||||
--identity ~/.ssh/watermaps_hetzner_ed25519
|
||||
--identity ~/.ssh/watermaps_deploy_ed25519
|
||||
```
|
||||
|
||||
Der Rollback verwendet gespeicherte Digests, nicht einen beweglichen Tag.
|
||||
@@ -167,7 +206,7 @@ A watermaps.incoso.eu <server_ipv4>
|
||||
Nach der DNS-Propagation wird der Livegang lokal ausgelöst:
|
||||
|
||||
```bash
|
||||
./deploy/scripts/remote-go-live.sh --identity ~/.ssh/watermaps_hetzner_ed25519
|
||||
./deploy/scripts/remote-go-live.sh --identity ~/.ssh/watermaps_deploy_ed25519
|
||||
```
|
||||
|
||||
Das Serverskript prüft, dass sämtliche A-Records ausschließlich auf die
|
||||
@@ -178,7 +217,19 @@ HTTPS um.
|
||||
|
||||
## Automatik
|
||||
|
||||
`bootstrap-server.sh` installiert zwei systemd-Timer:
|
||||
`bootstrap-server.sh` installiert drei systemd-Timer:
|
||||
|
||||
- `watermaps-auto-deploy.timer`: prüft ausgehend jede Minute
|
||||
`watermaps-release:main`. Das Revision-Label muss exakt dem aktuellen
|
||||
Gitea-`main`-Commit entsprechen. Der Server prüft Manifest, Dateiprüfsummen,
|
||||
gebundene Image-Digests und OCI-Revisionen und deployt erst nach einer
|
||||
zweiten Pointer-/Branch-Prüfung. Es ist weder ein eingehender Gitea-Webhook
|
||||
noch ein SSH-Key im Actions-Runner nötig. Aktives, vorheriges und die über
|
||||
`WATERMAPS_RELEASE_RETENTION` festgelegte Zahl lokaler Releases bleiben
|
||||
erhalten; ältere Watermaps-Artefakte werden gezielt bereinigt. Vor dem ersten
|
||||
Release verwendet ein stabiler Systemd-Einstiegspunkt das Bootstrap-Skript,
|
||||
danach automatisch das atomar aktivierte `current`. Noch nicht fertige erste
|
||||
Registry-Builds werden dadurch zuverlässig erneut versucht.
|
||||
|
||||
- `watermaps-route-update.timer`: täglich neue Deutschland- und
|
||||
Niederlande-Daten. Der Fahrroutenindex wird atomar aktiviert. Der
|
||||
@@ -193,7 +244,7 @@ Ein vollständiger manueller Feature-Neuimport lässt sich über die
|
||||
Option `--force-marine` erzwingen:
|
||||
|
||||
```bash
|
||||
/opt/watermaps/deploy/scripts/update-route-data.sh --force-marine
|
||||
/opt/watermaps/current/deploy/scripts/update-route-data.sh --force-marine
|
||||
```
|
||||
|
||||
`WATERMAPS_REBUILD_MARINE_DATA=true` in `.env.production` erzwingt den
|
||||
@@ -205,11 +256,12 @@ Status und Logs:
|
||||
|
||||
```bash
|
||||
systemctl list-timers 'watermaps-*'
|
||||
journalctl -u watermaps-auto-deploy.service
|
||||
journalctl -u watermaps-route-update.service
|
||||
journalctl -u watermaps-certbot-renew.service
|
||||
docker compose \
|
||||
--project-directory /opt/watermaps \
|
||||
--project-directory /opt/watermaps/current \
|
||||
--env-file /opt/watermaps/deploy/.env.production \
|
||||
--env-file /opt/watermaps/deploy/.env.images \
|
||||
-f /opt/watermaps/deploy/compose.production.yml ps
|
||||
-f /opt/watermaps/current/deploy/compose.production.yml ps
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user