562 lines
14 KiB
Markdown
562 lines
14 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](CODE_CHANGE_WORKFLOW.md)** - Wie Codeänderungen gepusht werden
|
|
- **[CI/CD 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
|
|
```
|
|
|
|
---
|
|
|
|
## 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:
|
|
- git.michaelschiemer.de:5000/framework:latest
|
|
- git.michaelschiemer.de:5000/framework:<tag>
|
|
- git.michaelschiemer.de:5000/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 git.michaelschiemer.de:5000 \
|
|
-u <registry-username> \
|
|
-p <registry-password>
|
|
```
|
|
|
|
**3. Neues Image Pullen**
|
|
```bash
|
|
# Pullt das neue Image von der Registry
|
|
docker pull git.michaelschiemer.de:5000/framework:<tag>
|
|
|
|
# Beispiel:
|
|
# git.michaelschiemer.de:5000/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: git.michaelschiemer.de:5000/framework:latest
|
|
|
|
# Nachher (wenn image_tag != 'latest'):
|
|
services:
|
|
app:
|
|
image: git.michaelschiemer.de:5000/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: git.michaelschiemer.de:5000/framework:latest`
|
|
- `queue-worker` (Queue Worker) - Zeile 120: `image: git.michaelschiemer.de:5000/framework:latest`
|
|
- `scheduler` (Scheduler) - Zeile 165: `image: git.michaelschiemer.de:5000/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: git.michaelschiemer.de:5000/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: git.michaelschiemer.de:5000/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: `git.michaelschiemer.de:5000/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: `git.michaelschiemer.de:5000/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: `git.michaelschiemer.de:5000/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: "git.michaelschiemer.de:5000/framework"
|
|
docker_registry_url: "git.michaelschiemer.de:5000"
|
|
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 git.michaelschiemer.de:5000/framework:latest \
|
|
--tag git.michaelschiemer.de:5000/framework:abc1234-1696234567 \
|
|
--tag git.michaelschiemer.de:5000/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 git.michaelschiemer.de:5000 -u admin -p <password>
|
|
|
|
# 3. Image Pullen
|
|
docker pull git.michaelschiemer.de:5000/framework:abc1234-1696234567
|
|
|
|
# 4. docker-compose.yml aktualisieren
|
|
# Vorher: image: git.michaelschiemer.de:5000/framework:latest
|
|
# Nachher: image: git.michaelschiemer.de:5000/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: git.michaelschiemer.de:5000/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 `git.michaelschiemer.de:5000/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 git.michaelschiemer.de:5000 -u admin -p <password>
|
|
|
|
# Image manuell pullen
|
|
docker pull git.michaelschiemer.de:5000/framework:<tag>
|
|
```
|
|
|
|
### docker-compose.yml wurde nicht aktualisiert
|
|
|
|
```bash
|
|
# Prüfe ob Regex korrekt ist
|
|
grep -E "image:\s+git.michaelschiemer.de:5000/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
|