Files
michaelschiemer/deployment/APPLICATION_STACK_DEPLOYMENT.md

14 KiB

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:


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

- PHP 8.3 Setup
- Composer Dependencies installieren
- Pest Tests ausführen
- PHPStan Code Quality Check
- Code Style Check

Job 2: Build & Push

- 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

git clone <repository> /workspace/repo

Schritt 2: SSH Setup

# 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

apt-get install -y ansible

Schritt 4: Ansible Playbook ausführen

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)

- Lädt Registry Credentials aus Ansible Vault (falls vorhanden)
- Oder verwendet Credentials aus Workflow-Variablen

2. Docker Service prüfen

- Verifiziert, dass Docker läuft
- Startet Docker falls notwendig

3. Verzeichnisse erstellen

- Application Stack Path: ~/deployment/stacks/application
- Backup Verzeichnis: ~/deployment/backups/<timestamp>/

Tasks

1. Backup aktueller Deployment-Status

# 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

# Login zur privaten Registry mit Credentials
docker login git.michaelschiemer.de:5000 \
  -u <registry-username> \
  -p <registry-password>

3. Neues Image Pullen

# 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!

# 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:

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

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

# Wartet 60 Sekunden auf Container-Gesundheit
wait_for:
  timeout: 60

7. Health-Status prüfen

# 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

# 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)

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)

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:

# Commit: abc1234...
# Image Tag generiert: abc1234-1696234567

2. Image wird gebaut und gepusht:

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:

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:

# 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:

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)

# Im Workflow: Rollback on failure
if: failure() && steps.health.outcome == 'failure'
run: |
  ansible-playbook -i inventory/production.yml \
    playbooks/rollback.yml

Manueller Rollback

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

# 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

# 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

# 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