Reorganised deployment scripts and added rollback functionality. Updated documentation and workflow for container image builds.
This commit is contained in:
@@ -17,10 +17,13 @@ WATERMAPS_RUNTIME_DIR=/srv/watermaps-runtime
|
||||
# Verhindert Speicherabbrüche beim vollständigen DE/NL-Indexaufbau auf 4-GB-Servern.
|
||||
WATERMAPS_SWAP_SIZE_GB=4
|
||||
|
||||
WATERMAPS_APP_IMAGE=watermaps:production
|
||||
# Gitea Container Registry. Die commitgenauen App-Referenzen werden beim
|
||||
# Deployment separat erzeugt und niemals hier von Hand auf `latest` gesetzt.
|
||||
WATERMAPS_REGISTRY=gitea.incoso.eu
|
||||
WATERMAPS_REGISTRY_OWNER=kevin_janssen
|
||||
|
||||
WATERMAPS_NGINX_IMAGE=nginx:1.30.4-alpine
|
||||
WATERMAPS_CERTBOT_IMAGE=certbot/certbot:v5.7.0
|
||||
WATERMAPS_ROUTE_DATA_IMAGE=watermaps-route-data:production
|
||||
|
||||
# Nur auf true setzen, wenn bei jedem Deployment Deutschland und die
|
||||
# Niederlande erneut geprüft und der Routingindex neu gebaut werden sollen.
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
.env.production
|
||||
.env.images
|
||||
.env.images.candidate
|
||||
!.env.production.example
|
||||
|
||||
+78
-13
@@ -12,11 +12,11 @@ cp deploy/.env.production.example deploy/.env.production
|
||||
editor deploy/.env.production
|
||||
```
|
||||
|
||||
Mindestens `WATERMAPS_ACME_EMAIL` muss angepasst werden. Der Hetzner-API-Token
|
||||
gehört **nicht** in diese Datei. Er bleibt lokal in der ignorierten Datei
|
||||
`infra/opentofu/terraform.tfvars` (alternativ kann der Provider
|
||||
`TF_VAR_hcloud_token` lesen). Der Upload schließt Terraform-Variablen, Pläne,
|
||||
State, lokale Env-Dateien und `.terraform` in jeder Verzeichnistiefe aus.
|
||||
Mindestens `WATERMAPS_ACME_EMAIL` muss angepasst werden. Außerdem müssen
|
||||
`WATERMAPS_REGISTRY` und `WATERMAPS_REGISTRY_OWNER` auf die Gitea Container
|
||||
Registry zeigen. Der Hetzner-API-Token gehört **nicht** in 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.
|
||||
@@ -26,24 +26,64 @@ Angaben erforderlich. 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
|
||||
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>
|
||||
```
|
||||
|
||||
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`
|
||||
|
||||
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.
|
||||
|
||||
## Deployment
|
||||
|
||||
Nach `tofu apply` liest dieses Skript standardmäßig den Output `server_ipv4`,
|
||||
überträgt das Projekt ohne State, lokale Daten oder Node-Module und startet den
|
||||
Serverbootstrap sowie den Docker-Stack:
|
||||
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/`, nicht den Anwendungsquellcode. Die Image-Tags entsprechen exakt
|
||||
`git rev-parse HEAD`.
|
||||
|
||||
Ist die Registry privat, werden einmalig beziehungsweise nach Tokenwechsel
|
||||
lokale Pull-Zugangsdaten mitgegeben:
|
||||
|
||||
```bash
|
||||
./deploy/scripts/upload-and-deploy.sh --identity ~/.ssh/watermaps_hetzner_ed25519
|
||||
WATERMAPS_REGISTRY_USERNAME=kevin_janssen \
|
||||
WATERMAPS_REGISTRY_TOKEN='<package-read-token>' \
|
||||
./deploy/scripts/upload-and-deploy.sh \
|
||||
--identity ~/.ssh/watermaps_hetzner_ed25519
|
||||
```
|
||||
|
||||
Alternativ:
|
||||
Der Token benötigt auf dem Produktionsserver nur `package: Read`. Er wird per
|
||||
SSH an `docker login --password-stdin` übergeben, nicht in
|
||||
`.env.production`, einem Container oder dem Image gespeichert. Docker legt die
|
||||
Anmeldung root-lesbar in seiner lokalen Client-Konfiguration ab. Bei späteren
|
||||
Deployments muss der Token nicht erneut angegeben werden, solange die
|
||||
Anmeldung gültig ist:
|
||||
|
||||
```bash
|
||||
WATERMAPS_SERVER_IPV4=203.0.113.10 \
|
||||
WATERMAPS_SSH_KEY=~/.ssh/watermaps_hetzner_ed25519 \
|
||||
./deploy/scripts/upload-and-deploy.sh
|
||||
./deploy/scripts/upload-and-deploy.sh \
|
||||
--identity ~/.ssh/watermaps_hetzner_ed25519
|
||||
```
|
||||
|
||||
Das Serverskript lädt App, Routingdaten-Builder, 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.
|
||||
|
||||
Der erste Datenaufbau lädt die Geofabrik-Extrakte für Deutschland und die
|
||||
Niederlande herunter und kann entsprechend der Serverleistung längere Zeit
|
||||
dauern. Der produktive Index liegt auf dem Server unter:
|
||||
@@ -63,6 +103,30 @@ persistente Swap-Reserve ein.
|
||||
Vor dem Livegang antwortet Nginx nur für ACME-Challenges. Alle anderen
|
||||
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- und Routentests erneut aus.
|
||||
|
||||
Das unmittelbar vorherige Release lässt sich auch manuell aktivieren:
|
||||
|
||||
```bash
|
||||
./deploy/scripts/remote-rollback.sh \
|
||||
--identity ~/.ssh/watermaps_hetzner_ed25519
|
||||
```
|
||||
|
||||
Oder ein bestimmter, bereits erfolgreich deployter Commit:
|
||||
|
||||
```bash
|
||||
./deploy/scripts/remote-rollback.sh \
|
||||
--revision 0123456789abcdef0123456789abcdef01234567 \
|
||||
--identity ~/.ssh/watermaps_hetzner_ed25519
|
||||
```
|
||||
|
||||
Der Rollback verwendet gespeicherte Digests, nicht einen beweglichen Tag.
|
||||
|
||||
## DNS und SSL-Livegang
|
||||
|
||||
Sobald der OpenTofu-Output bekannt ist, kann folgender DNS-Eintrag gesetzt
|
||||
@@ -103,5 +167,6 @@ journalctl -u watermaps-certbot-renew.service
|
||||
docker compose \
|
||||
--project-directory /opt/watermaps \
|
||||
--env-file /opt/watermaps/deploy/.env.production \
|
||||
--env-file /opt/watermaps/deploy/.env.images \
|
||||
-f /opt/watermaps/deploy/compose.production.yml ps
|
||||
```
|
||||
|
||||
@@ -2,10 +2,7 @@ name: watermaps-production
|
||||
|
||||
services:
|
||||
watermaps:
|
||||
image: ${WATERMAPS_APP_IMAGE:-watermaps:production}
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: ${WATERMAPS_APP_IMAGE:?WATERMAPS_APP_IMAGE fehlt}
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
HOST: 0.0.0.0
|
||||
@@ -85,10 +82,7 @@ services:
|
||||
target: /etc/letsencrypt
|
||||
|
||||
route-data:
|
||||
image: ${WATERMAPS_ROUTE_DATA_IMAGE:-watermaps-route-data:production}
|
||||
build:
|
||||
context: .
|
||||
dockerfile: deploy/route-data.Dockerfile
|
||||
image: ${WATERMAPS_ROUTE_DATA_IMAGE:?WATERMAPS_ROUTE_DATA_IMAGE fehlt}
|
||||
profiles: ["maintenance"]
|
||||
environment:
|
||||
WATERMAPS_GEOFABRIK_DIR: /workspace/data/geofabrik
|
||||
|
||||
@@ -5,6 +5,7 @@ set -Eeuo pipefail
|
||||
WM_DEPLOY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
WM_ROOT_DIR="$(cd "$WM_DEPLOY_DIR/.." && pwd)"
|
||||
WM_ENV_FILE="${WATERMAPS_ENV_FILE:-$WM_DEPLOY_DIR/.env.production}"
|
||||
WM_IMAGES_ENV_FILE="${WATERMAPS_IMAGES_ENV_FILE:-$WM_DEPLOY_DIR/.env.images}"
|
||||
WM_COMPOSE_FILE="$WM_DEPLOY_DIR/compose.production.yml"
|
||||
|
||||
wm_die() {
|
||||
@@ -47,13 +48,112 @@ wm_assert_data_mount() {
|
||||
}
|
||||
|
||||
wm_compose() {
|
||||
wm_validate_images_env "$WM_IMAGES_ENV_FILE"
|
||||
docker compose \
|
||||
--project-directory "$WM_ROOT_DIR" \
|
||||
--env-file "$WM_ENV_FILE" \
|
||||
--env-file "$WM_IMAGES_ENV_FILE" \
|
||||
--file "$WM_COMPOSE_FILE" \
|
||||
"$@"
|
||||
}
|
||||
|
||||
wm_use_images_env() {
|
||||
local images_env_file="$1"
|
||||
wm_validate_images_env "$images_env_file"
|
||||
WM_IMAGES_ENV_FILE="$images_env_file"
|
||||
export WATERMAPS_IMAGES_ENV_FILE="$images_env_file"
|
||||
}
|
||||
|
||||
wm_validate_images_env() {
|
||||
local images_env_file="$1"
|
||||
local revision app_image route_data_image
|
||||
|
||||
[[ -f "$images_env_file" ]] ||
|
||||
wm_die "Image-Konfiguration fehlt: $images_env_file"
|
||||
|
||||
revision="$(wm_env_value "$images_env_file" WATERMAPS_DEPLOY_REVISION)"
|
||||
app_image="$(wm_env_value "$images_env_file" WATERMAPS_APP_IMAGE)"
|
||||
route_data_image="$(wm_env_value "$images_env_file" WATERMAPS_ROUTE_DATA_IMAGE)"
|
||||
|
||||
[[ "$revision" =~ ^[0-9a-f]{40}$ ]] ||
|
||||
wm_die "WATERMAPS_DEPLOY_REVISION muss eine vollständige Git-Commit-SHA sein."
|
||||
wm_validate_image_reference "$app_image" WATERMAPS_APP_IMAGE
|
||||
wm_validate_image_reference "$route_data_image" WATERMAPS_ROUTE_DATA_IMAGE
|
||||
wm_validate_release_image "$app_image" "$revision" WATERMAPS_APP_IMAGE
|
||||
wm_validate_release_image \
|
||||
"$route_data_image" \
|
||||
"$revision" \
|
||||
WATERMAPS_ROUTE_DATA_IMAGE
|
||||
}
|
||||
|
||||
wm_env_value() {
|
||||
local env_file="$1"
|
||||
local requested_key="$2"
|
||||
local value
|
||||
|
||||
value="$(
|
||||
awk -F= -v requested_key="$requested_key" '
|
||||
$1 == requested_key {
|
||||
print substr($0, length($1) + 2)
|
||||
matches += 1
|
||||
}
|
||||
END {
|
||||
if (matches != 1) {
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
' "$env_file"
|
||||
)" || wm_die "$requested_key fehlt oder ist mehrfach in $env_file vorhanden."
|
||||
printf '%s\n' "$value"
|
||||
}
|
||||
|
||||
wm_validate_image_reference() {
|
||||
local image_reference="$1"
|
||||
local variable_name="$2"
|
||||
|
||||
[[ -n "$image_reference" &&
|
||||
"$image_reference" != *[[:space:]]* &&
|
||||
"$image_reference" == */* &&
|
||||
"$image_reference" != *REPLACE* ]] ||
|
||||
wm_die "$variable_name enthält keine gültige Registry-Image-Referenz."
|
||||
}
|
||||
|
||||
wm_validate_release_image() {
|
||||
local image_reference="$1"
|
||||
local revision="$2"
|
||||
local variable_name="$3"
|
||||
|
||||
[[ "$image_reference" == *":$revision" ||
|
||||
"$image_reference" =~ @sha256:[0-9a-f]{64}$ ]] ||
|
||||
wm_die "$variable_name muss auf den Commit-Tag $revision oder einen SHA256-Digest zeigen."
|
||||
}
|
||||
|
||||
wm_resolve_image_digest() {
|
||||
local image_reference="$1"
|
||||
local requested_repository last_component resolved_reference
|
||||
|
||||
requested_repository="${image_reference%%@*}"
|
||||
last_component="${requested_repository##*/}"
|
||||
if [[ "$last_component" == *:* ]]; then
|
||||
requested_repository="${requested_repository%:*}"
|
||||
fi
|
||||
|
||||
resolved_reference="$(
|
||||
docker image inspect \
|
||||
--format '{{range .RepoDigests}}{{println .}}{{end}}' \
|
||||
"$image_reference" |
|
||||
awk -v requested_repository="$requested_repository" '
|
||||
index($0, requested_repository "@sha256:") == 1 {
|
||||
print
|
||||
exit
|
||||
}
|
||||
'
|
||||
)"
|
||||
[[ "$resolved_reference" =~ @sha256:[0-9a-f]{64}$ ]] ||
|
||||
wm_die "Für $image_reference konnte kein lokaler Registry-Digest ermittelt werden."
|
||||
printf '%s\n' "$resolved_reference"
|
||||
}
|
||||
|
||||
wm_route_file() {
|
||||
printf '%s/local/germany-netherlands-fairways.json\n' "$WATERMAPS_DATA_DIR"
|
||||
}
|
||||
|
||||
+103
-9
@@ -5,12 +5,35 @@ set -Eeuo pipefail
|
||||
WM_DEPLOY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
# shellcheck source=common.sh
|
||||
source "$WM_DEPLOY_DIR/scripts/common.sh"
|
||||
|
||||
images_env_file="$WM_IMAGES_ENV_FILE"
|
||||
|
||||
usage() {
|
||||
printf 'Verwendung: %s [--images-file DATEI]\n' "$0"
|
||||
}
|
||||
|
||||
while [[ "$#" -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--images-file)
|
||||
[[ "$#" -ge 2 ]] || wm_die "Wert für --images-file fehlt."
|
||||
images_env_file="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
wm_die "Unbekannte Option: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ "$(id -u)" -eq 0 ]] ||
|
||||
wm_die "Dieses Skript muss als root ausgeführt werden."
|
||||
wm_load_env
|
||||
wm_assert_data_mount
|
||||
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
wm_die "Dieses Skript muss als root ausgeführt werden."
|
||||
fi
|
||||
wm_use_images_env "$images_env_file"
|
||||
|
||||
install -d -m 0755 \
|
||||
"$WATERMAPS_DATA_DIR/geofabrik" \
|
||||
@@ -18,20 +41,81 @@ install -d -m 0755 \
|
||||
"$WATERMAPS_DATA_DIR/certbot/www" \
|
||||
"$WATERMAPS_DATA_DIR/certbot/letsencrypt" \
|
||||
"$WATERMAPS_RUNTIME_DIR/nginx/conf.d" \
|
||||
"$WATERMAPS_RUNTIME_DIR/locks"
|
||||
"$WATERMAPS_RUNTIME_DIR/locks" \
|
||||
"$WATERMAPS_RUNTIME_DIR/deployments"
|
||||
|
||||
wm_acquire_route_lock ||
|
||||
wm_die "Deployment abgebrochen, weil gerade Routingdaten aktualisiert werden."
|
||||
|
||||
active_images_file="$WM_DEPLOY_DIR/.env.images"
|
||||
rollback_images_file=""
|
||||
resolved_images_file=""
|
||||
deployment_complete=false
|
||||
|
||||
if [[ -f "$active_images_file" ]]; then
|
||||
wm_validate_images_env "$active_images_file"
|
||||
rollback_images_file="$(
|
||||
mktemp "$WATERMAPS_RUNTIME_DIR/deployments/.rollback-images.XXXXXX"
|
||||
)"
|
||||
install -m 0644 "$active_images_file" "$rollback_images_file"
|
||||
fi
|
||||
|
||||
wm_finish_deployment() {
|
||||
local status=$?
|
||||
trap - EXIT HUP INT TERM
|
||||
set +e
|
||||
|
||||
if [[ "$status" -ne 0 && "$deployment_complete" != "true" ]]; then
|
||||
if [[ -n "$rollback_images_file" && -f "$rollback_images_file" ]]; then
|
||||
wm_log "Deployment fehlgeschlagen; vorheriges Container-Release wird wiederhergestellt."
|
||||
wm_use_images_env "$rollback_images_file"
|
||||
wm_compose up --detach --no-build --remove-orphans watermaps nginx
|
||||
wm_wait_for_health watermaps 240
|
||||
wm_wait_for_health nginx 120
|
||||
if wm_route_data_ready; then
|
||||
wm_smoke_test_route
|
||||
fi
|
||||
else
|
||||
wm_log "Deployment fehlgeschlagen; für das erste Release existiert noch kein Rollback."
|
||||
fi
|
||||
fi
|
||||
|
||||
[[ -z "$resolved_images_file" ]] || rm -f "$resolved_images_file"
|
||||
[[ -z "$rollback_images_file" ]] || rm -f "$rollback_images_file"
|
||||
exit "$status"
|
||||
}
|
||||
|
||||
trap wm_finish_deployment EXIT
|
||||
trap 'exit 130' HUP INT TERM
|
||||
|
||||
if [[ ! -f "$WATERMAPS_RUNTIME_DIR/nginx/conf.d/default.conf" ]]; then
|
||||
install -m 0644 \
|
||||
"$WM_DEPLOY_DIR/nginx/bootstrap.conf" \
|
||||
"$WATERMAPS_RUNTIME_DIR/nginx/conf.d/default.conf"
|
||||
fi
|
||||
|
||||
wm_log "Anwendungsimage wird gebaut."
|
||||
wm_compose build watermaps
|
||||
wm_compose up --detach --remove-orphans watermaps nginx
|
||||
revision="$(wm_env_value "$images_env_file" WATERMAPS_DEPLOY_REVISION)"
|
||||
requested_app_image="$(wm_env_value "$images_env_file" WATERMAPS_APP_IMAGE)"
|
||||
requested_route_data_image="$(wm_env_value "$images_env_file" WATERMAPS_ROUTE_DATA_IMAGE)"
|
||||
|
||||
wm_log "Container-Images für Commit $revision werden aus den Registries geladen."
|
||||
wm_compose --profile maintenance pull watermaps nginx certbot route-data
|
||||
|
||||
resolved_app_image="$(wm_resolve_image_digest "$requested_app_image")"
|
||||
resolved_route_data_image="$(wm_resolve_image_digest "$requested_route_data_image")"
|
||||
resolved_images_file="$(
|
||||
mktemp "$WATERMAPS_RUNTIME_DIR/deployments/.resolved-images.XXXXXX"
|
||||
)"
|
||||
{
|
||||
printf 'WATERMAPS_DEPLOY_REVISION=%s\n' "$revision"
|
||||
printf 'WATERMAPS_APP_IMAGE=%s\n' "$resolved_app_image"
|
||||
printf 'WATERMAPS_ROUTE_DATA_IMAGE=%s\n' "$resolved_route_data_image"
|
||||
} >"$resolved_images_file"
|
||||
chmod 0644 "$resolved_images_file"
|
||||
wm_use_images_env "$resolved_images_file"
|
||||
|
||||
wm_log "Commit $revision wird mit unveränderlichen Image-Digests gestartet."
|
||||
wm_compose up --detach --no-build --remove-orphans watermaps nginx
|
||||
|
||||
wm_wait_for_health watermaps 240
|
||||
wm_wait_for_health nginx 120
|
||||
@@ -55,4 +139,14 @@ wm_assert_route_data
|
||||
wm_wait_for_health watermaps 240
|
||||
wm_smoke_test_route
|
||||
|
||||
wm_log "Deployment ist bereit. Vor dem Livegang liefert Port 80 nur ACME-Challenges und ansonsten HTTP 404."
|
||||
release_images_file="$WATERMAPS_RUNTIME_DIR/deployments/$revision.env"
|
||||
install -m 0644 "$resolved_images_file" "$release_images_file"
|
||||
if [[ -n "$rollback_images_file" ]]; then
|
||||
install -m 0644 \
|
||||
"$rollback_images_file" \
|
||||
"$WATERMAPS_RUNTIME_DIR/deployments/previous.env"
|
||||
fi
|
||||
install -m 0644 "$resolved_images_file" "$active_images_file"
|
||||
|
||||
deployment_complete=true
|
||||
wm_log "Deployment von Commit $revision ist bereit. Vor dem Livegang liefert Port 80 nur ACME-Challenges und ansonsten HTTP 404."
|
||||
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
WM_DEPLOY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
# shellcheck source=remote-common.sh
|
||||
source "$WM_DEPLOY_DIR/scripts/remote-common.sh"
|
||||
|
||||
server_ipv4=""
|
||||
identity_file=""
|
||||
revision="previous"
|
||||
|
||||
usage() {
|
||||
printf 'Verwendung: %s [--revision SHA|previous] [--host IPV4] [--identity DATEI] [--user BENUTZER]\n' "$0"
|
||||
}
|
||||
|
||||
while [[ "$#" -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--revision)
|
||||
[[ "$#" -ge 2 ]] || wm_local_die "Wert für --revision fehlt."
|
||||
revision="$2"
|
||||
shift 2
|
||||
;;
|
||||
--host)
|
||||
[[ "$#" -ge 2 ]] || wm_local_die "Wert für --host fehlt."
|
||||
server_ipv4="$2"
|
||||
shift 2
|
||||
;;
|
||||
--identity)
|
||||
[[ "$#" -ge 2 ]] || wm_local_die "Wert für --identity fehlt."
|
||||
identity_file="$2"
|
||||
shift 2
|
||||
;;
|
||||
--user)
|
||||
[[ "$#" -ge 2 ]] || wm_local_die "Wert für --user fehlt."
|
||||
WATERMAPS_SSH_USER="$2"
|
||||
export WATERMAPS_SSH_USER
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
wm_local_die "Unbekannte Option: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ "$revision" == "previous" || "$revision" =~ ^[0-9a-f]{40}$ ]] ||
|
||||
wm_local_die "--revision muss 'previous' oder eine vollständige Git-Commit-SHA sein."
|
||||
|
||||
wm_local_resolve_ssh "$server_ipv4" "$identity_file"
|
||||
wm_local_wait_for_ssh
|
||||
remote_prefix=""
|
||||
if [[ -n "$WM_REMOTE_SUDO" ]]; then
|
||||
remote_prefix="$WM_REMOTE_SUDO "
|
||||
fi
|
||||
|
||||
ssh "${WM_SSH_OPTIONS[@]}" "$WM_SSH_TARGET" \
|
||||
"${remote_prefix}/opt/watermaps/deploy/scripts/rollback.sh '$revision'"
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
WM_DEPLOY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
# shellcheck source=common.sh
|
||||
source "$WM_DEPLOY_DIR/scripts/common.sh"
|
||||
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
wm_die "Dieses Skript muss als root ausgeführt werden."
|
||||
fi
|
||||
|
||||
wm_load_env
|
||||
revision="${1:-previous}"
|
||||
deployments_dir="$WATERMAPS_RUNTIME_DIR/deployments"
|
||||
|
||||
if [[ "$revision" == "previous" ]]; then
|
||||
images_file="$deployments_dir/previous.env"
|
||||
else
|
||||
[[ "$revision" =~ ^[0-9a-f]{40}$ ]] ||
|
||||
wm_die "Rollback-Ziel muss 'previous' oder eine vollständige Git-Commit-SHA sein."
|
||||
images_file="$deployments_dir/$revision.env"
|
||||
fi
|
||||
|
||||
[[ -f "$images_file" ]] ||
|
||||
wm_die "Gespeichertes Release nicht gefunden: $images_file"
|
||||
|
||||
exec "$WM_DEPLOY_DIR/scripts/deploy.sh" --images-file "$images_file"
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
|
||||
TEST_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$TEST_DIR"' EXIT
|
||||
|
||||
# shellcheck source=../common.sh
|
||||
source "$ROOT_DIR/deploy/scripts/common.sh"
|
||||
|
||||
revision="0123456789abcdef0123456789abcdef01234567"
|
||||
valid_env="$TEST_DIR/valid.env"
|
||||
cat >"$valid_env" <<EOF
|
||||
WATERMAPS_DEPLOY_REVISION=$revision
|
||||
WATERMAPS_APP_IMAGE=registry.example:5000/team/watermaps:$revision
|
||||
WATERMAPS_ROUTE_DATA_IMAGE=registry.example:5000/team/watermaps-route-data:$revision
|
||||
EOF
|
||||
wm_validate_images_env "$valid_env"
|
||||
|
||||
invalid_env="$TEST_DIR/invalid.env"
|
||||
cat >"$invalid_env" <<EOF
|
||||
WATERMAPS_DEPLOY_REVISION=$revision
|
||||
WATERMAPS_APP_IMAGE=registry.example/team/watermaps:latest
|
||||
WATERMAPS_ROUTE_DATA_IMAGE=registry.example/team/watermaps-route-data:$revision
|
||||
EOF
|
||||
if (wm_validate_images_env "$invalid_env" >/dev/null 2>&1); then
|
||||
printf 'Beweglicher latest-Tag wurde als Release akzeptiert.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
fake_bin="$TEST_DIR/bin"
|
||||
mkdir -p "$fake_bin"
|
||||
cat >"$fake_bin/docker" <<'SH'
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
if [[ "${1:-}" == "image" && "${2:-}" == "inspect" ]]; then
|
||||
printf '%s\n' \
|
||||
'mirror.example/team/watermaps@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' \
|
||||
'registry.example:5000/team/watermaps@sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
|
||||
exit 0
|
||||
fi
|
||||
exit 1
|
||||
SH
|
||||
chmod +x "$fake_bin/docker"
|
||||
PATH="$fake_bin:$PATH"
|
||||
|
||||
resolved="$(
|
||||
wm_resolve_image_digest \
|
||||
"registry.example:5000/team/watermaps:$revision"
|
||||
)"
|
||||
[[ "$resolved" == \
|
||||
"registry.example:5000/team/watermaps@sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" ]]
|
||||
|
||||
printf 'Commit-Tag- und Digest-Prüfungen: OK\n'
|
||||
@@ -10,6 +10,7 @@ FAKE_BIN="$TEST_DIR/bin"
|
||||
DATA_DIR="$TEST_DIR/data"
|
||||
RUNTIME_DIR="$TEST_DIR/runtime"
|
||||
ENV_FILE="$TEST_DIR/production.env"
|
||||
IMAGES_ENV_FILE="$TEST_DIR/images.env"
|
||||
mkdir -p "$FAKE_BIN" "$DATA_DIR/local" "$RUNTIME_DIR"
|
||||
|
||||
cat >"$ENV_FILE" <<EOF
|
||||
@@ -17,6 +18,12 @@ WATERMAPS_DATA_DIR=$DATA_DIR
|
||||
WATERMAPS_RUNTIME_DIR=$RUNTIME_DIR
|
||||
EOF
|
||||
|
||||
cat >"$IMAGES_ENV_FILE" <<'EOF'
|
||||
WATERMAPS_DEPLOY_REVISION=0123456789abcdef0123456789abcdef01234567
|
||||
WATERMAPS_APP_IMAGE=registry.example/watermaps:0123456789abcdef0123456789abcdef01234567
|
||||
WATERMAPS_ROUTE_DATA_IMAGE=registry.example/watermaps-route-data:0123456789abcdef0123456789abcdef01234567
|
||||
EOF
|
||||
|
||||
cat >"$FAKE_BIN/id" <<'SH'
|
||||
#!/usr/bin/env bash
|
||||
if [[ "${1:-}" == "-u" ]]; then
|
||||
@@ -95,6 +102,7 @@ chmod +x "$FAKE_BIN/id" "$FAKE_BIN/mountpoint" "$FAKE_BIN/docker"
|
||||
|
||||
export PATH="$FAKE_BIN:$PATH"
|
||||
export WATERMAPS_ENV_FILE="$ENV_FILE"
|
||||
export WATERMAPS_IMAGES_ENV_FILE="$IMAGES_ENV_FILE"
|
||||
export WM_TEST_ROOT="$ROOT_DIR"
|
||||
|
||||
# A first successful activation creates an active index and a validated marker.
|
||||
|
||||
@@ -145,7 +145,7 @@ if wm_route_data_ready; then
|
||||
fi
|
||||
|
||||
wm_log "Deutschland-/Niederlande-Routingdaten werden in einer Stagingdatei gebaut."
|
||||
if ! wm_compose --profile maintenance run --rm --build \
|
||||
if ! wm_compose --profile maintenance run --rm \
|
||||
--env "WATERMAPS_LOCAL_FAIRWAYS_PATH=$container_candidate" \
|
||||
--env "WATERMAPS_ROUTE_MARKER_PATH=$container_candidate_marker" \
|
||||
route-data; then
|
||||
|
||||
@@ -24,8 +24,13 @@ Optionen:
|
||||
-h, --help Hilfe anzeigen
|
||||
|
||||
Die Datei deploy/.env.production muss lokal vorhanden sein. Sie wird separat
|
||||
mit Dateimodus 0600 übertragen. Terraform-State, .terraform, data,
|
||||
node_modules, lokale Env-Dateien und Git-Metadaten werden ausgeschlossen.
|
||||
mit Dateimodus 0600 übertragen. Es wird ausschließlich der Deployment-Ordner
|
||||
übertragen; App und Routingdaten-Builder werden als commitgenaue Images aus der
|
||||
in .env.production konfigurierten Gitea Registry geladen.
|
||||
|
||||
Für eine private Registry können WATERMAPS_REGISTRY_USERNAME und
|
||||
WATERMAPS_REGISTRY_TOKEN lokal gesetzt werden. Der Token wird nur über SSH an
|
||||
`docker login --password-stdin` übergeben und nicht auf den Server hochgeladen.
|
||||
USAGE
|
||||
}
|
||||
|
||||
@@ -62,10 +67,49 @@ while [[ "$#" -gt 0 ]]; do
|
||||
done
|
||||
|
||||
command -v rsync >/dev/null 2>&1 || wm_local_die "rsync ist lokal nicht installiert."
|
||||
command -v git >/dev/null 2>&1 || wm_local_die "git ist lokal nicht installiert."
|
||||
local_env="$WM_DEPLOY_DIR/.env.production"
|
||||
[[ -f "$local_env" ]] ||
|
||||
wm_local_die "Bitte zuerst deploy/.env.production aus der Vorlage erstellen und konfigurieren."
|
||||
|
||||
if [[ -n "$(git -C "$WM_ROOT_DIR" status --porcelain --untracked-files=normal)" ]]; then
|
||||
wm_local_die "Deployment nur aus einem vollständig committeten, sauberen Git-Stand erlaubt."
|
||||
fi
|
||||
revision="$(git -C "$WM_ROOT_DIR" rev-parse HEAD)"
|
||||
[[ "$revision" =~ ^[0-9a-f]{40}$ ]] ||
|
||||
wm_local_die "Aktueller Git-Commit konnte nicht als vollständige SHA ermittelt werden."
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$local_env"
|
||||
set +a
|
||||
registry="${WATERMAPS_REGISTRY:-}"
|
||||
registry_owner="${WATERMAPS_REGISTRY_OWNER:-}"
|
||||
[[ "$registry" =~ ^[a-z0-9][a-z0-9.-]*(:[0-9]{1,5})?$ ]] ||
|
||||
wm_local_die "WATERMAPS_REGISTRY muss ein Registry-Hostname ohne URL-Schema sein."
|
||||
[[ "$registry_owner" =~ ^[A-Za-z0-9_.-]+$ ]] ||
|
||||
wm_local_die "WATERMAPS_REGISTRY_OWNER enthält keinen gültigen Gitea-Besitzer."
|
||||
|
||||
registry_username="${WATERMAPS_REGISTRY_USERNAME:-}"
|
||||
registry_token="${WATERMAPS_REGISTRY_TOKEN:-}"
|
||||
if [[ -n "$registry_username" || -n "$registry_token" ]]; then
|
||||
[[ -n "$registry_username" && -n "$registry_token" ]] ||
|
||||
wm_local_die "WATERMAPS_REGISTRY_USERNAME und WATERMAPS_REGISTRY_TOKEN müssen gemeinsam gesetzt werden."
|
||||
[[ "$registry_username" =~ ^[A-Za-z0-9_.-]+$ ]] ||
|
||||
wm_local_die "WATERMAPS_REGISTRY_USERNAME enthält ungültige Zeichen."
|
||||
fi
|
||||
|
||||
images_env="$(mktemp)"
|
||||
trap 'rm -f "$images_env"' EXIT
|
||||
chmod 0600 "$images_env"
|
||||
{
|
||||
printf 'WATERMAPS_DEPLOY_REVISION=%s\n' "$revision"
|
||||
printf 'WATERMAPS_APP_IMAGE=%s/%s/watermaps:%s\n' \
|
||||
"$registry" "$registry_owner" "$revision"
|
||||
printf 'WATERMAPS_ROUTE_DATA_IMAGE=%s/%s/watermaps-route-data:%s\n' \
|
||||
"$registry" "$registry_owner" "$revision"
|
||||
} >"$images_env"
|
||||
|
||||
wm_local_resolve_ssh "$server_ipv4" "$identity_file"
|
||||
wm_local_wait_for_ssh
|
||||
|
||||
@@ -80,7 +124,7 @@ printf '[watermaps] Warte auf Cloud-init und das persistente Hetzner-Volume.\n'
|
||||
ssh "${WM_SSH_OPTIONS[@]}" "$WM_SSH_TARGET" \
|
||||
"cloud-init status --wait && ${remote_prefix}systemctl start watermaps-volume-setup.service && mountpoint --quiet /srv/watermaps-data"
|
||||
|
||||
printf '[watermaps] Übertrage Projekt nach %s:/opt/watermaps\n' "$WM_SSH_TARGET"
|
||||
printf '[watermaps] Übertrage Deployment-Dateien nach %s:/opt/watermaps/deploy\n' "$WM_SSH_TARGET"
|
||||
ssh "${WM_SSH_OPTIONS[@]}" "$WM_SSH_TARGET" \
|
||||
"${remote_prefix}install -d -m 0755 /opt/watermaps /opt/watermaps/deploy"
|
||||
|
||||
@@ -90,24 +134,11 @@ rsync \
|
||||
--delete-delay \
|
||||
--human-readable \
|
||||
--rsync-path="$rsync_path" \
|
||||
--exclude='.git/' \
|
||||
--exclude='.terraform/' \
|
||||
--exclude='.terraform.tfstate.lock.info' \
|
||||
--exclude='*.tfvars' \
|
||||
--exclude='*.tfvars.json' \
|
||||
--exclude='*.tfplan' \
|
||||
--exclude='*.tfstate*' \
|
||||
--exclude='crash.log' \
|
||||
--exclude='crash.*.log' \
|
||||
--exclude='/data/' \
|
||||
--exclude='/.tools/' \
|
||||
--exclude='node_modules/' \
|
||||
--exclude='**/.env' \
|
||||
--exclude='**/.env.*' \
|
||||
--exclude='/deploy/.env.production' \
|
||||
--exclude='.env.production' \
|
||||
--exclude='.env.images*' \
|
||||
-e "ssh ${WM_SSH_OPTIONS[*]@Q}" \
|
||||
"$WM_ROOT_DIR/" \
|
||||
"$WM_SSH_TARGET:/opt/watermaps/"
|
||||
"$WM_DEPLOY_DIR/" \
|
||||
"$WM_SSH_TARGET:/opt/watermaps/deploy/"
|
||||
|
||||
rsync \
|
||||
--archive \
|
||||
@@ -117,15 +148,32 @@ rsync \
|
||||
"$local_env" \
|
||||
"$WM_SSH_TARGET:/opt/watermaps/deploy/.env.production"
|
||||
|
||||
rsync \
|
||||
--archive \
|
||||
--chmod=F600 \
|
||||
--rsync-path="$rsync_path" \
|
||||
-e "ssh ${WM_SSH_OPTIONS[*]@Q}" \
|
||||
"$images_env" \
|
||||
"$WM_SSH_TARGET:/opt/watermaps/deploy/.env.images.candidate"
|
||||
|
||||
if [[ -n "$registry_token" ]]; then
|
||||
printf '[watermaps] Aktualisiere den privaten Registry-Login auf dem Server.\n'
|
||||
printf '%s\n' "$registry_token" |
|
||||
ssh "${WM_SSH_OPTIONS[@]}" "$WM_SSH_TARGET" \
|
||||
"${remote_prefix}docker login '$registry' --username '$registry_username' --password-stdin"
|
||||
fi
|
||||
|
||||
remote_command="${remote_prefix}chmod +x /opt/watermaps/deploy/scripts/*.sh"
|
||||
remote_command+=" && ${remote_prefix}/opt/watermaps/deploy/scripts/bootstrap-server.sh"
|
||||
remote_command+=" && ${remote_prefix}/opt/watermaps/deploy/scripts/deploy.sh"
|
||||
remote_command+=" && ${remote_prefix}/opt/watermaps/deploy/scripts/deploy.sh --images-file /opt/watermaps/deploy/.env.images.candidate"
|
||||
ssh "${WM_SSH_OPTIONS[@]}" "$WM_SSH_TARGET" "$remote_command"
|
||||
ssh "${WM_SSH_OPTIONS[@]}" "$WM_SSH_TARGET" \
|
||||
"${remote_prefix}rm -f /opt/watermaps/deploy/.env.images.candidate"
|
||||
|
||||
if [[ "$run_go_live" == "true" ]]; then
|
||||
ssh "${WM_SSH_OPTIONS[@]}" "$WM_SSH_TARGET" \
|
||||
"${remote_prefix}/opt/watermaps/deploy/scripts/go-live.sh '$WM_SERVER_IPV4'"
|
||||
else
|
||||
printf '[watermaps] Deployment abgeschlossen. Nach dem DNS-Eintrag:\n'
|
||||
printf '[watermaps] Commit %s wurde aus der Registry deployt. Nach dem DNS-Eintrag:\n' "$revision"
|
||||
printf ' ./deploy/scripts/remote-go-live.sh --host %s\n' "$WM_SERVER_IPV4"
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user