Files
michaelschiemer/deployment/docs/reference/application-stack.md

578 lines
15 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Application Stack Deployment Prozess
## Übersicht
Dieses Dokument erklärt, wie genau das Deployment in den Application Stack abläuft, wenn die CI/CD Pipeline ausgeführt wird.
**📖 Verwandte Dokumentation:**
- **[Code Changes Workflow](../guides/code-change-workflow.md)** - Wie Codeänderungen gepusht werden
- **[CI/CD Status](../status/ci-cd-status.md)** - Aktueller Status der Pipeline
---
## Deployment-Flow
```
CI/CD Pipeline (Gitea Actions)
1. Tests & Build
2. Docker Image Build & Push zur Registry
3. Ansible Playbook ausführen
4. Application Stack aktualisieren
```
### Secret Handling für Redis
- Die Redis-Zugangsdaten liegen verschlüsselt in `deployment/ansible/secrets/production.vault.yml`
unter dem Schlüssel `vault_redis_password`.
- Die Application-Rolle (`roles/application/tasks/sync.yml`) bricht den Deploy ab,
wenn kein Passwort aus dem Vault oder via `-e redis_password=...` vorhanden ist.
- Während des Deployments wird das Passwort in `stacks/application/.env` geschrieben
und steht damit allen PHP-Containern über `REDIS_PASSWORD` zur Verfügung.
- Docker Secrets mit `REDIS_PASSWORD_FILE` werden weiterhin unterstützt, da der
Entry-Point das Secret lädt bevor PHP-FPM startet.
- Das `APP_KEY` stammt ebenfalls aus dem Vault (`vault_app_key`); der Deploy
stoppt, falls kein Schlüssel hinterlegt ist.
- Weitere sicherheitskritische Variablen (z.B. `VAULT_ENCRYPTION_KEY`) werden
aus dem Vault übernommen und in die generierte `.env` geschrieben, damit die
Container-Konfiguration 1:1 mit dem Projekt-Template übereinstimmt.
---
## Detaillierter Ablauf
### Phase 1: CI/CD Pipeline (`.gitea/workflows/production-deploy.yml`)
#### Job 1: Tests
```yaml
- PHP 8.3 Setup
- Composer Dependencies installieren
- Pest Tests ausführen
- PHPStan Code Quality Check
- Code Style Check
```
#### Job 2: Build & Push
```yaml
- Docker Image Build (Dockerfile.production)
- Image mit Tags pushen:
- registry.michaelschiemer.de/framework:latest
- registry.michaelschiemer.de/framework:<tag>
- registry.michaelschiemer.de/framework:git-<short-sha>
```
#### Job 3: Deploy (Ansible)
**Schritt 1: Code Checkout**
```bash
git clone <repository> /workspace/repo
```
**Schritt 2: SSH Setup**
```bash
# SSH Private Key aus Secret wird in ~/.ssh/production geschrieben
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/production
chmod 600 ~/.ssh/production
```
**Schritt 3: Ansible Installation**
```bash
apt-get install -y ansible
```
**Schritt 4: Ansible Playbook ausführen**
```bash
cd /workspace/repo/deployment/ansible
ansible-playbook -i inventory/production.yml \
playbooks/deploy-update.yml \
-e "image_tag=<generated-tag>" \
-e "git_commit_sha=<commit-sha>" \
-e "deployment_timestamp=<timestamp>" \
-e "docker_registry_username=${{ secrets.REGISTRY_USER }}" \
-e "docker_registry_password=${{ secrets.REGISTRY_PASSWORD }}"
```
---
### Phase 2: Ansible Deployment (`deploy-update.yml`)
Das Ansible Playbook führt folgende Schritte auf dem Production-Server aus:
#### Pre-Tasks
**1. Secrets laden (optional)**
```yaml
- Lädt Registry Credentials aus Ansible Vault (falls vorhanden)
- Oder verwendet Credentials aus Workflow-Variablen
```
**2. Docker Service prüfen**
```yaml
- Verifiziert, dass Docker läuft
- Startet Docker falls notwendig
```
**3. Verzeichnisse erstellen**
```yaml
- Application Stack Path: ~/deployment/stacks/application
- Backup Verzeichnis: ~/deployment/backups/<timestamp>/
```
#### Tasks
**1. Backup aktueller Deployment-Status**
```bash
# Speichert aktuellen Container-Status
docker compose -f ~/deployment/stacks/application/docker-compose.yml \
ps --format json > backups/<timestamp>/current_containers.json
# Speichert aktuelle docker-compose.yml Konfiguration
docker compose -f ~/deployment/stacks/application/docker-compose.yml \
config > backups/<timestamp>/docker-compose-config.yml
```
**2. Docker Registry Login**
```bash
# Login zur privaten Registry mit Credentials
docker login registry.michaelschiemer.de \
-u <registry-username> \
-p <registry-password>
```
**3. Neues Image Pullen**
```bash
# Pullt das neue Image von der Registry
docker pull registry.michaelschiemer.de/framework:<tag>
# Beispiel:
# registry.michaelschiemer.de/framework:abc1234-1696234567
```
**4. docker-compose.yml aktualisieren**
**Wichtig:** Das Playbook aktualisiert die `docker-compose.yml` Datei direkt auf dem Server!
```yaml
# Vorher:
services:
app:
image: registry.michaelschiemer.de/framework:latest
# Nachher (wenn image_tag != 'latest'):
services:
app:
image: registry.michaelschiemer.de/framework:<tag>
```
**Regex-Replace:**
```yaml
regexp: '^(\s+image:\s+){{ app_image }}:.*$'
replace: '\1{{ app_image }}:{{ image_tag }}'
```
**Betroffene Services (werden alle aktualisiert):**
- `app` (PHP-FPM) - Zeile 6: `image: registry.michaelschiemer.de/framework:latest`
- `queue-worker` (Queue Worker) - Zeile 120: `image: registry.michaelschiemer.de/framework:latest`
- `scheduler` (Scheduler) - Zeile 165: `image: registry.michaelschiemer.de/framework:latest`
**Hinweis:**
- Alle drei Services verwenden das gleiche Image, daher werden alle mit dem neuen Tag aktualisiert
- Der Regex matched **alle Zeilen** die mit `image: registry.michaelschiemer.de/framework:` beginnen
- `nginx` und `redis` bleiben unverändert (verwenden andere Images)
**5. Application Stack neu starten**
```bash
docker compose -f ~/deployment/stacks/application/docker-compose.yml \
up -d \
--pull always \
--force-recreate \
--remove-orphans
```
**Was passiert dabei:**
- `--pull always`: Zieht Images immer neu (auch wenn schon vorhanden)
- `--force-recreate`: Erstellt Container immer neu, auch wenn Konfiguration unverändert ist
- `--remove-orphans`: Entfernt Container, die nicht mehr in der Compose-Datei definiert sind
- `-d`: Läuft im Hintergrund (detached)
**Container-Neustart-Reihenfolge:**
1. Abhängigkeiten werden gestoppt (app hängt von redis ab)
2. Container werden neu erstellt mit neuem Image
3. Container werden in korrekter Reihenfolge gestartet (redis → app → nginx)
4. Health-Checks werden ausgeführt
**6. Warten auf Health-Checks**
```yaml
# Wartet 60 Sekunden auf Container-Gesundheit
wait_for:
timeout: 60
```
**7. Health-Status prüfen**
```bash
# Prüft alle Container-Health-Status
docker compose ps --format json | \
jq -r '.[] | select(.Health != "healthy" and .Health != "") | "\(.Name): \(.Health)"'
```
**8. Deployment-Metadaten speichern**
```
~/deployment/backups/<timestamp>/deployment_metadata.txt
```
**Inhalt:**
```
Deployment Timestamp: 2025-10-31T02:35:04Z
Git Commit: abc1234...
Image Tag: abc1234-1696234567
Deployed Image: registry.michaelschiemer.de/framework:abc1234-1696234567
Image Pull: SUCCESS
Stack Deploy: UPDATED
Health Status: All services healthy
```
**9. Alte Backups aufräumen**
```bash
# Behält nur die letzten X Backups (Standard: 5)
ls -dt */ | tail -n +6 | xargs -r rm -rf
```
---
### Phase 3: Application Stack Services
Der Application Stack besteht aus mehreren Services:
#### Services im Stack
**1. `app` (PHP-FPM Application)**
- Image: `registry.michaelschiemer.de/framework:<tag>`
- Container: `app`
- Health Check: `php-fpm-healthcheck`
- Netzwerk: `app-internal`
- Abhängigkeiten: `redis` (condition: service_healthy)
**2. `nginx` (Web Server)**
- Image: `nginx:1.25-alpine`
- Container: `nginx`
- Health Check: `wget --spider http://localhost/health`
- Netzwerke: `traefik-public`, `app-internal`
- Abhängigkeiten: `app` (condition: service_healthy)
- Traefik Labels für Routing
**3. `redis` (Cache/Session/Queue)**
- Image: `redis:7-alpine`
- Container: `redis`
- Health Check: `redis-cli --raw incr ping`
- Netzwerk: `app-internal`
**4. `queue-worker` (Background Jobs)**
- Image: `registry.michaelschiemer.de/framework:<tag>` (gleiches wie app)
- Container: `queue-worker`
- Health Check: `pgrep -f 'queue:work'`
- Netzwerk: `app-internal`
- Command: `php console.php queue:work`
- Abhängigkeiten: `app`, `redis`
**5. `scheduler` (Cron Jobs)**
- Image: `registry.michaelschiemer.de/framework:<tag>` (gleiches wie app)
- Container: `scheduler`
- Health Check: `pgrep -f 'scheduler:run'`
- Netzwerk: `app-internal`
- Command: `php console.php scheduler:run`
- Abhängigkeiten: `app`, `redis`
---
## Container-Neustart-Details
### Was passiert bei `docker compose up -d --force-recreate`?
**1. Container-Stop:**
- Laufende Container werden gestoppt
- Volumes bleiben erhalten (persistente Daten)
- Netzwerke bleiben erhalten
**2. Container-Entfernung:**
- Alte Container werden entfernt
- Neue Container werden mit neuem Image erstellt
**3. Container-Start:**
- Container starten in Abhängigkeits-Reihenfolge
- Health-Checks werden ausgeführt
- Falls Health-Check fehlschlägt, wird Container neu gestartet (restart: unless-stopped)
**4. Zero-Downtime?**
- **Teilweise:** Container werden nacheinander neu gestartet
- `nginx` wartet auf `app` Health-Check
- Während Neustart kann es zu kurzen Verbindungsfehlern kommen
- Kein echter Blue-Green-Deployment (das wäre möglich, aber nicht implementiert)
---
## Konfigurationsvariablen
### Inventory-Variablen (`inventory/production.yml`)
```yaml
app_image: "registry.michaelschiemer.de/framework"
docker_registry_url: "registry.michaelschiemer.de"
backups_path: "~/deployment/backups"
max_rollback_versions: 5
deploy_user_home: "~/deployment"
```
### Workflow-Variablen (aus CI/CD)
```yaml
image_tag: "abc1234-1696234567" # Generiert aus: <short-sha>-<timestamp>
git_commit_sha: "abc1234567890..."
deployment_timestamp: "2025-10-31T02:35:04Z"
docker_registry_username: "<from-secret>"
docker_registry_password: "<from-secret>"
```
---
## Beispiel-Deployment
### Schritt-für-Schritt Beispiel
**1. CI/CD Pipeline startet:**
```bash
# Commit: abc1234...
# Image Tag generiert: abc1234-1696234567
```
**2. Image wird gebaut und gepusht:**
```bash
docker buildx build \
--tag registry.michaelschiemer.de/framework:latest \
--tag registry.michaelschiemer.de/framework:abc1234-1696234567 \
--tag registry.michaelschiemer.de/framework:git-abc1234 \
--push \
.
```
**3. Ansible Playbook wird ausgeführt:**
```bash
ansible-playbook -i inventory/production.yml \
playbooks/deploy-update.yml \
-e "image_tag=abc1234-1696234567" \
-e "git_commit_sha=abc1234567890..." \
-e "deployment_timestamp=2025-10-31T02:35:04Z"
```
**4. Auf Production-Server:**
```bash
# 1. Backup erstellen
mkdir -p ~/deployment/backups/2025-10-31T02-35-04Z
# 2. Registry Login
docker login registry.michaelschiemer.de -u admin -p <password>
# 3. Image Pullen
docker pull registry.michaelschiemer.de/framework:abc1234-1696234567
# 4. docker-compose.yml aktualisieren
# Vorher: image: registry.michaelschiemer.de/framework:latest
# Nachher: image: registry.michaelschiemer.de/framework:abc1234-1696234567
# 5. Stack neu starten
cd ~/deployment/stacks/application
docker compose up -d --pull always --force-recreate --remove-orphans
# 6. Health-Checks warten
sleep 60
# 7. Status prüfen
docker compose ps
# app: healthy
# nginx: healthy
# redis: healthy
# queue-worker: healthy
# scheduler: healthy
```
**5. Deployment-Metadaten speichern:**
```bash
cat > ~/deployment/backups/2025-10-31T02-35-04Z/deployment_metadata.txt <<EOF
Deployment Timestamp: 2025-10-31T02:35:04Z
Git Commit: abc1234567890...
Image Tag: abc1234-1696234567
Deployed Image: registry.michaelschiemer.de/framework:abc1234-1696234567
Image Pull: SUCCESS
Stack Deploy: UPDATED
Health Status: All services healthy
EOF
```
---
## Wichtige Hinweise
### 1. Image-Tag-Update
**Wichtig:** Das Playbook aktualisiert die `docker-compose.yml` Datei **direkt auf dem Server**!
- Die Datei wird mit `replace` Modul geändert
- Alle Services mit dem Image `registry.michaelschiemer.de/framework:*` werden aktualisiert
- Das bedeutet: `app`, `queue-worker`, und `scheduler` bekommen alle das neue Image
### 2. Force-Recreate
**Achtung:** `--force-recreate` startet Container **immer** neu, auch wenn Konfiguration unverändert ist.
- Container-IDs ändern sich
- Kurze Downtime möglich
- Neue Container bekommen neue IPs im Docker-Netzwerk
### 3. Health-Checks
**Health-Checks sind kritisch:**
- Container müssen "healthy" werden, sonst starten abhängige Container nicht
- `nginx` wartet auf `app` Health-Check
- `app` wartet auf `redis` Health-Check
**Health-Check Timeouts:**
- `app`: 40s start_period, dann 30s interval
- `redis`: 10s start_period, dann 30s interval
- `nginx`: 10s start_period, dann 30s interval
### 4. Backups
**Backup-Strategie:**
- Jedes Deployment erstellt ein Backup-Verzeichnis
- Enthält: Container-Status, docker-compose.yml, Deployment-Metadaten
- Alte Backups werden automatisch gelöscht (Standard: Behält 5 Backups)
**Backup-Pfad:**
```
~/deployment/backups/
├── 2025-10-31T02-35-04Z/
│ ├── current_containers.json
│ ├── docker-compose-config.yml
│ └── deployment_metadata.txt
├── 2025-10-31T01-20-15Z/
│ └── ...
└── ...
```
---
## Rollback-Prozess
Falls das Deployment fehlschlägt:
### Automatischer Rollback (via Workflow)
```yaml
# Im Workflow: Rollback on failure
if: failure() && steps.health.outcome == 'failure'
run: |
ansible-playbook -i inventory/production.yml \
playbooks/rollback.yml
```
### Manueller Rollback
```bash
cd ~/deployment/ansible
ansible-playbook -i inventory/production.yml \
playbooks/rollback.yml \
-e "rollback_timestamp=2025-10-31T01-20-15Z"
```
**Was passiert beim Rollback:**
1. Liest vorheriges Backup
2. Pullt altes Image
3. Aktualisiert `docker-compose.yml` mit altem Image-Tag
4. Startet Stack neu mit altem Image
---
## Verbesserungsmöglichkeiten
### 1. Blue-Green Deployment
- Zwei parallele Stacks (blue/green)
- Traffic-Switching via Traefik
- Zero-Downtime möglich
### 2. Rolling Updates
- Container werden nacheinander aktualisiert
- Reduzierte Downtime
### 3. Database-Migration vor Deployment
- Migrationen werden aktuell nach Deployment ausgeführt
- Besser: Migrationen vor Deployment testen
### 4. Canary Deployment
- Neue Version zuerst auf Subset der Traffic
- Graduelle Rollout
---
## Troubleshooting
### Container starten nicht
```bash
# Logs prüfen
docker compose -f ~/deployment/stacks/application/docker-compose.yml logs
# Health-Check-Status prüfen
docker compose ps
```
### Image wird nicht gepullt
```bash
# Registry-Login testen
docker login registry.michaelschiemer.de -u admin -p <password>
# Image manuell pullen
docker pull registry.michaelschiemer.de/framework:<tag>
```
### docker-compose.yml wurde nicht aktualisiert
```bash
# Prüfe ob Regex korrekt ist
grep -E "image:\s+registry.michaelschiemer.de/framework" \
~/deployment/stacks/application/docker-compose.yml
# Prüfe Backup für vorherige Version
ls -la ~/deployment/backups/
```
---
## Zusammenfassung
**Deployment-Ablauf:**
1. CI/CD Pipeline baut Image und pusht es zur Registry
2. Ansible Playbook wird auf Production-Server ausgeführt
3. Neues Image wird gepullt
4. `docker-compose.yml` wird mit neuem Image-Tag aktualisiert
5. Application Stack wird mit `--force-recreate` neu gestartet
6. Health-Checks werden ausgeführt
7. Deployment-Metadaten werden gespeichert
8. Workflow führt abschließenden Health-Check aus
**Alle Services erhalten das neue Image:**
- `app` (PHP-FPM)
- `queue-worker` (Queue Worker)
- `scheduler` (Scheduler)
**Zero-Downtime:** Nein, Container werden neu gestartet (kurze Downtime möglich)
**Rollback:** Automatisch via Workflow oder manuell via Rollback-Playbook