- Remove redundant `$container` parameter in `RedisPoolInitializer` instantiation. - Streamline container interactions for improved clarity and maintainability.
578 lines
15 KiB
Markdown
578 lines
15 KiB
Markdown
# 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 docker-compose.base.yml -f docker-compose.production.yml \
|
||
ps --format json > backups/<timestamp>/current_containers.json
|
||
|
||
# Speichert aktuelle docker-compose.yml Konfiguration
|
||
docker compose -f docker-compose.base.yml -f docker-compose.production.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 docker-compose.base.yml -f docker-compose.production.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" \
|
||
docker-compose.base.yml docker-compose.production.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
|