# 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
