Files
michaelschiemer/docs/deployment/ANSIBLE_DEPLOYMENT.md
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
2025-10-25 19:18:37 +02:00

22 KiB

Ansible-Based Deployment

Fortgeschrittenes Deployment mit Ansible für Multi-Server Orchestrierung und Infrastructure as Code.

Übersicht

Ansible erweitert das Script-based Deployment um:

  • Multi-Server Orchestrierung - Deployment auf mehrere Server gleichzeitig
  • Infrastructure as Code - Versionierte, wiederholbare Server-Konfiguration
  • Idempotenz - Sichere, wiederholbare Ausführung ohne Seiteneffekte
  • Rollenzentralisierung - Wiederverwendbare Konfigurationsbausteine
  • Inventory Management - Verwaltung verschiedener Environments

Wann Ansible verwenden?

Ansible ist sinnvoll für:

  • Multiple Environments: Staging, Production, DR (Disaster Recovery)
  • Load Balancing: Mehrere Application Server hinter Load Balancer
  • Team Collaboration: Mehrere Entwickler deployen
  • Compliance: Dokumentierte, auditierbare Infrastruktur
  • Skalierung: Einfaches Hinzufügen neuer Server
  • Konsistenz: Identische Konfiguration über alle Server

Ansible NICHT notwendig wenn:

  • Single Production Server
  • Kleine Infrastruktur
  • Docker Compose Scripts ausreichend
  • Deployment-Frequenz sehr niedrig

Installation

Lokal (Control Node)

# Python 3 und pip installieren
sudo apt install -y python3 python3-pip

# Ansible installieren
pip3 install ansible

# Verify
ansible --version

# Ansible Collections installieren
ansible-galaxy collection install community.docker
ansible-galaxy collection install community.general

SSH Key Setup

# SSH Key generieren (falls nicht vorhanden)
ssh-keygen -t ed25519 -C "ansible@deployment"

# Public Key auf Server kopieren
ssh-copy-id -i ~/.ssh/id_ed25519.pub deploy@production-server

# SSH Connection testen
ssh deploy@production-server

Projektstruktur

ansible/
├── ansible.cfg                    # Ansible Konfiguration
├── inventory/                     # Server Inventories
│   ├── production                 # Production Server
│   ├── staging                    # Staging Server
│   └── group_vars/               # Group Variables
│       ├── all.yml               # Alle Server
│       ├── production.yml        # Production-spezifisch
│       └── staging.yml           # Staging-spezifisch
├── playbooks/                     # Ansible Playbooks
│   ├── site.yml                  # Master Playbook
│   ├── provision.yml             # Server Provisioning
│   ├── deploy.yml                # Application Deployment
│   ├── rollback.yml              # Deployment Rollback
│   └── maintenance.yml           # Wartungsaufgaben
├── roles/                         # Ansible Roles
│   ├── common/                   # Common Server Setup
│   ├── docker/                   # Docker Installation
│   ├── nginx/                    # Nginx Configuration
│   ├── ssl/                      # SSL Certificate Management
│   ├── vault/                    # Secrets Management
│   └── application/              # Application Deployment
└── files/                         # Statische Files
    └── templates/                # Jinja2 Templates

Ansible Konfiguration

ansible.cfg

[defaults]
inventory = inventory/production
remote_user = deploy
host_key_checking = False
retry_files_enabled = False
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 3600
timeout = 30
pipelining = True
log_path = /var/log/ansible.log
roles_path = roles

[privilege_escalation]
become = True
become_method = sudo
become_user = root
become_ask_pass = False

[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o ServerAliveInterval=60
control_path = /tmp/ansible-ssh-%%h-%%p-%%r

Inventory Setup

Production Inventory

inventory/production:

[app_servers]
app1.yourdomain.com ansible_host=203.0.113.10
app2.yourdomain.com ansible_host=203.0.113.11

[db_servers]
db1.yourdomain.com ansible_host=203.0.113.20

[cache_servers]
redis1.yourdomain.com ansible_host=203.0.113.30

[load_balancers]
lb1.yourdomain.com ansible_host=203.0.113.5

[production:children]
app_servers
db_servers
cache_servers
load_balancers

[production:vars]
ansible_user=deploy
ansible_python_interpreter=/usr/bin/python3

Group Variables

inventory/group_vars/all.yml:

---
# Common variables für alle Server
app_name: app
app_user: www-data
app_group: www-data
app_base_dir: /var/www/app
log_dir: /var/log/app
backup_dir: /backups

# Docker
docker_compose_version: "2.20.0"

# Git
git_repo: "git@github.com:yourusername/app.git"
git_version: "HEAD"

# Timezone
server_timezone: "Europe/Berlin"

inventory/group_vars/production.yml:

---
# Production-spezifische Variables
environment: production
domain_name: yourdomain.com
app_url: "https://{{ domain_name }}"

# SSL
ssl_cert_email: admin@yourdomain.com
ssl_provider: letsencrypt

# Resources
app_memory_limit: "2g"
app_cpu_limit: "2"
worker_count: 3

# Backup
backup_retention_days: 30
backup_schedule: "0 2 * * *"

# Monitoring
prometheus_enabled: true
grafana_enabled: true

Ansible Roles

Role: common (Server Grundkonfiguration)

roles/common/tasks/main.yml:

---
- name: Update apt cache
  apt:
    update_cache: yes
    cache_valid_time: 3600

- name: Install required system packages
  apt:
    name:
      - apt-transport-https
      - ca-certificates
      - curl
      - gnupg
      - lsb-release
      - git
      - vim
      - htop
      - ufw
    state: present

- name: Set timezone
  timezone:
    name: "{{ server_timezone }}"

- name: Create application user
  user:
    name: "{{ app_user }}"
    shell: /bin/bash
    createhome: yes
    groups: sudo
    append: yes

- name: Create application directories
  file:
    path: "{{ item }}"
    state: directory
    owner: "{{ app_user }}"
    group: "{{ app_group }}"
    mode: '0755'
  loop:
    - "{{ app_base_dir }}"
    - "{{ log_dir }}"
    - "{{ backup_dir }}"
    - "/opt/vault"

- name: Configure UFW firewall
  ufw:
    rule: "{{ item.rule }}"
    port: "{{ item.port }}"
    proto: "{{ item.proto }}"
  loop:
    - { rule: 'allow', port: '22', proto: 'tcp' }
    - { rule: 'allow', port: '80', proto: 'tcp' }
    - { rule: 'allow', port: '443', proto: 'tcp' }
  notify: Enable UFW

- name: Set UFW default policies
  ufw:
    direction: "{{ item.direction }}"
    policy: "{{ item.policy }}"
  loop:
    - { direction: 'incoming', policy: 'deny' }
    - { direction: 'outgoing', policy: 'allow' }

Role: docker (Docker Installation)

roles/docker/tasks/main.yml:

---
- name: Add Docker GPG key
  apt_key:
    url: https://download.docker.com/linux/ubuntu/gpg
    state: present

- name: Add Docker repository
  apt_repository:
    repo: "deb [arch=amd64] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
    state: present

- name: Install Docker
  apt:
    name:
      - docker-ce
      - docker-ce-cli
      - containerd.io
      - docker-compose-plugin
    state: present
    update_cache: yes

- name: Add user to docker group
  user:
    name: "{{ app_user }}"
    groups: docker
    append: yes

- name: Ensure Docker service is running
  systemd:
    name: docker
    state: started
    enabled: yes

- name: Install Docker Python library
  pip:
    name:
      - docker
      - docker-compose
    state: present

Role: ssl (SSL Certificate Management)

roles/ssl/tasks/main.yml:

---
- name: Install Certbot
  apt:
    name:
      - certbot
      - python3-certbot-nginx
    state: present

- name: Check if certificate exists
  stat:
    path: "/etc/letsencrypt/live/{{ domain_name }}/fullchain.pem"
  register: cert_exists

- name: Obtain SSL certificate
  command: >
    certbot certonly --nginx
    -d {{ domain_name }}
    -d www.{{ domain_name }}
    --email {{ ssl_cert_email }}
    --agree-tos
    --non-interactive
  when: not cert_exists.stat.exists

- name: Copy certificates to application directory
  copy:
    src: "/etc/letsencrypt/live/{{ domain_name }}/{{ item.src }}"
    dest: "/etc/ssl/app/{{ item.dest }}"
    remote_src: yes
    mode: "{{ item.mode }}"
  loop:
    - { src: 'fullchain.pem', dest: 'cert.pem', mode: '0644' }
    - { src: 'privkey.pem', dest: 'key.pem', mode: '0600' }

- name: Setup certificate auto-renewal
  cron:
    name: "Renew SSL certificates"
    minute: "0"
    hour: "3"
    job: >
      certbot renew --quiet &&
      cp /etc/letsencrypt/live/{{ domain_name }}/fullchain.pem /etc/ssl/app/cert.pem &&
      cp /etc/letsencrypt/live/{{ domain_name }}/privkey.pem /etc/ssl/app/key.pem &&
      docker compose -f {{ app_base_dir }}/docker-compose.production.yml restart nginx

Role: application (Application Deployment)

roles/application/tasks/main.yml:

---
- name: Clone/Update git repository
  git:
    repo: "{{ git_repo }}"
    dest: "{{ app_base_dir }}"
    version: "{{ git_version }}"
    force: yes
  become_user: "{{ app_user }}"

- name: Copy environment file
  template:
    src: env.production.j2
    dest: "{{ app_base_dir }}/.env.production"
    owner: "{{ app_user }}"
    group: "{{ app_group }}"
    mode: '0600'

- name: Install Composer dependencies
  command: docker compose -f docker-compose.production.yml run --rm php composer install --no-dev --optimize-autoloader
  args:
    chdir: "{{ app_base_dir }}"
  become_user: "{{ app_user }}"

- name: Build frontend assets
  command: "{{ item }}"
  args:
    chdir: "{{ app_base_dir }}"
  become_user: "{{ app_user }}"
  loop:
    - docker compose -f docker-compose.production.yml run --rm nodejs npm ci
    - docker compose -f docker-compose.production.yml run --rm nodejs npm run build

- name: Build Docker images
  command: docker compose -f docker-compose.production.yml build
  args:
    chdir: "{{ app_base_dir }}"
  become_user: "{{ app_user }}"

- name: Run database migrations
  command: docker compose -f docker-compose.production.yml exec -T php php console.php db:migrate
  args:
    chdir: "{{ app_base_dir }}"
  become_user: "{{ app_user }}"
  register: migration_result
  failed_when: false

- name: Start/Restart Docker containers
  command: docker compose -f docker-compose.production.yml up -d
  args:
    chdir: "{{ app_base_dir }}"
  become_user: "{{ app_user }}"

- name: Wait for application to be ready
  uri:
    url: "http://localhost/health"
    status_code: 200
  register: result
  until: result.status == 200
  retries: 30
  delay: 2

- name: Run health checks
  uri:
    url: "http://localhost/health/detailed"
    return_content: yes
  register: health_check

- name: Display health check results
  debug:
    var: health_check.json

roles/application/templates/env.production.j2:

# Application
APP_ENV={{ environment }}
APP_DEBUG=false
APP_URL={{ app_url }}

# Database
DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=app_{{ environment }}
DB_USERNAME={{ vault_db_username }}
DB_PASSWORD={{ vault_db_password }}

# Cache
CACHE_DRIVER=redis
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD={{ vault_redis_password }}

# Queue
QUEUE_DRIVER=redis
QUEUE_CONNECTION=redis

# Vault
VAULT_ENCRYPTION_KEY={{ vault_encryption_key }}

# Admin Access
ADMIN_ALLOWED_IPS={{ admin_allowed_ips }}

# Logging
LOG_PATH={{ log_dir }}
LOG_LEVEL=info

Playbooks

Master Playbook

playbooks/site.yml:

---
- name: Full Site Deployment
  hosts: all
  gather_facts: yes
  roles:
    - common

- name: Setup Docker
  hosts: app_servers
  roles:
    - docker

- name: Setup SSL Certificates
  hosts: app_servers
  roles:
    - ssl

- name: Deploy Application
  hosts: app_servers
  roles:
    - application

- name: Setup Monitoring
  hosts: app_servers
  roles:
    - monitoring
  when: prometheus_enabled | default(false)

Deployment Playbook

playbooks/deploy.yml:

---
- name: Deploy Application Update
  hosts: app_servers
  serial: 1  # Ein Server nach dem anderen (zero-downtime)
  vars_prompt:
    - name: git_version
      prompt: "Git branch/tag/commit to deploy"
      default: "production"
      private: no

  pre_tasks:
    - name: Create backup
      command: >
        docker compose -f {{ app_base_dir }}/docker-compose.production.yml exec -T db
        mysqldump -u {{ vault_db_username }} -p{{ vault_db_password }} app_{{ environment }}
      register: backup
      changed_when: false

    - name: Save backup
      copy:
        content: "{{ backup.stdout }}"
        dest: "{{ backup_dir }}/backup_{{ ansible_date_time.iso8601_basic_short }}.sql"

  tasks:
    - name: Health check before deployment
      uri:
        url: "http://localhost/health"
        status_code: 200
      register: pre_health
      failed_when: pre_health.status != 200

    - name: Deploy application
      include_role:
        name: application

  post_tasks:
    - name: Health check after deployment
      uri:
        url: "http://localhost/health/detailed"
        return_content: yes
      register: post_health
      failed_when: post_health.json.overall_healthy != true

    - name: Run smoke tests
      uri:
        url: "{{ item }}"
        status_code: 200
      loop:
        - "http://localhost/health"
        - "http://localhost/metrics"
      register: smoke_tests
      failed_when: smoke_tests.status != 200

Rollback Playbook

playbooks/rollback.yml:

---
- name: Rollback Application
  hosts: app_servers
  vars_prompt:
    - name: git_version
      prompt: "Git version to rollback to"
      private: no
    - name: rollback_migrations
      prompt: "Number of migrations to rollback (0 for none)"
      default: "0"
      private: no

  tasks:
    - name: Checkout previous version
      git:
        repo: "{{ git_repo }}"
        dest: "{{ app_base_dir }}"
        version: "{{ git_version }}"
        force: yes
      become_user: "{{ app_user }}"

    - name: Rollback database migrations
      command: docker compose -f docker-compose.production.yml exec -T php php console.php db:rollback {{ rollback_migrations }}
      args:
        chdir: "{{ app_base_dir }}"
      when: rollback_migrations | int > 0

    - name: Rebuild Docker images
      command: docker compose -f docker-compose.production.yml build
      args:
        chdir: "{{ app_base_dir }}"

    - name: Restart containers
      command: docker compose -f docker-compose.production.yml up -d --force-recreate
      args:
        chdir: "{{ app_base_dir }}"

    - name: Wait for application
      uri:
        url: "http://localhost/health"
        status_code: 200
      register: result
      until: result.status == 200
      retries: 30
      delay: 2

    - name: Verify rollback
      uri:
        url: "http://localhost/health/detailed"
        return_content: yes
      register: health_check
      failed_when: health_check.json.overall_healthy != true

Provisioning Playbook

playbooks/provision.yml:

---
- name: Provision New Server
  hosts: all
  roles:
    - common
    - docker
    - nginx
    - ssl

  tasks:
    - name: Setup log rotation
      template:
        src: logrotate.j2
        dest: /etc/logrotate.d/app
        mode: '0644'

    - name: Setup backup cron
      cron:
        name: "Daily backup"
        minute: "0"
        hour: "2"
        job: "{{ app_base_dir }}/scripts/deployment/backup-database.sh"

    - name: Setup monitoring
      include_role:
        name: monitoring
      when: prometheus_enabled | default(false)

Verwendung

Server Provisioning (Einmalig)

cd ansible

# Alle Server provisionieren
ansible-playbook -i inventory/production playbooks/provision.yml

# Nur App Server
ansible-playbook -i inventory/production playbooks/provision.yml --limit app_servers

# Mit Vault Password
ansible-playbook -i inventory/production playbooks/provision.yml --ask-vault-pass

Application Deployment

# Standard Deployment
ansible-playbook -i inventory/production playbooks/deploy.yml

# Spezifischer Branch/Tag
ansible-playbook -i inventory/production playbooks/deploy.yml -e "git_version=v2.1.0"

# Dry-Run (keine Änderungen)
ansible-playbook -i inventory/production playbooks/deploy.yml --check

# Nur ein Server
ansible-playbook -i inventory/production playbooks/deploy.yml --limit app1.yourdomain.com

Rollback

# Rollback zu spezifischer Version
ansible-playbook -i inventory/production playbooks/rollback.yml

# Mit Migration Rollback
ansible-playbook -i inventory/production playbooks/rollback.yml -e "rollback_migrations=3"

Ad-hoc Commands

# Health Check auf allen Servern
ansible app_servers -i inventory/production -m uri -a "url=http://localhost/health"

# Docker Container Status
ansible app_servers -i inventory/production -m shell -a "cd /var/www/app && docker compose ps"

# Log Tail
ansible app_servers -i inventory/production -m shell -a "tail -20 /var/log/app/app.log"

# Service Restart
ansible app_servers -i inventory/production -m shell -a "cd /var/www/app && docker compose restart php"

Secrets Management mit Ansible Vault

Vault erstellen

# Neues Vault File
ansible-vault create inventory/group_vars/production/vault.yml

# Content:
---
vault_db_username: app_user
vault_db_password: <strong-database-password>
vault_redis_password: <strong-redis-password>
vault_encryption_key: <vault-encryption-key>
admin_allowed_ips: "203.0.113.0/24,198.51.100.10"

Vault verwenden

# Deployment mit Vault
ansible-playbook -i inventory/production playbooks/deploy.yml --ask-vault-pass

# Oder Vault Password File
echo "your-vault-password" > .vault_pass
chmod 600 .vault_pass

ansible-playbook -i inventory/production playbooks/deploy.yml --vault-password-file .vault_pass

Vault editieren

# Vault bearbeiten
ansible-vault edit inventory/group_vars/production/vault.yml

# Vault anzeigen
ansible-vault view inventory/group_vars/production/vault.yml

# Vault rekey
ansible-vault rekey inventory/group_vars/production/vault.yml

CI/CD Integration

GitHub Actions

.github/workflows/deploy-production.yml:

name: Deploy to Production

on:
  push:
    branches: [production]
  workflow_dispatch:

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Setup Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.10'

      - name: Install Ansible
        run: |
          pip install ansible
          ansible-galaxy collection install community.docker

      - name: Setup SSH
        env:
          SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
        run: |
          mkdir -p ~/.ssh
          echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519
          ssh-keyscan -H production-server >> ~/.ssh/known_hosts

      - name: Deploy with Ansible
        env:
          VAULT_PASSWORD: ${{ secrets.ANSIBLE_VAULT_PASSWORD }}
        run: |
          cd ansible
          echo "$VAULT_PASSWORD" > .vault_pass
          ansible-playbook -i inventory/production playbooks/deploy.yml --vault-password-file .vault_pass
          rm .vault_pass

Monitoring Integration

Prometheus Metrics Collection

roles/monitoring/tasks/main.yml:

---
- name: Install Prometheus Node Exporter
  apt:
    name: prometheus-node-exporter
    state: present

- name: Setup Prometheus scraping
  template:
    src: prometheus.yml.j2
    dest: /etc/prometheus/prometheus.yml
  notify: Restart Prometheus

- name: Add health check monitoring
  cron:
    name: "Health check monitoring"
    minute: "*/5"
    job: "curl -f http://localhost/health || echo 'Health check failed' | mail -s 'Production Health Alert' admin@yourdomain.com"

Best Practices

1. Idempotenz sicherstellen

# ✅ Idempotent
- name: Ensure application directory exists
  file:
    path: "{{ app_base_dir }}"
    state: directory

# ❌ Nicht idempotent
- name: Create directory
  shell: mkdir -p {{ app_base_dir }}

2. Fehlerbehandlung

- name: Run migrations
  command: php console.php db:migrate
  register: migration_result
  failed_when: false

- name: Check migration result
  fail:
    msg: "Migrations failed: {{ migration_result.stderr }}"
  when: migration_result.rc != 0 and 'already applied' not in migration_result.stderr

3. Atomare Deployments

# Serial deployment für zero-downtime
- hosts: app_servers
  serial: 1  # Ein Server nach dem anderen
  max_fail_percentage: 0  # Stop bei Fehler

4. Backup vor Deployment

pre_tasks:
  - name: Backup database
    include_tasks: backup.yml

Vergleich: Script-Based vs Ansible

Feature Script-Based Ansible
Setup Complexity Niedrig Mittel
Multi-Server Manuell Automatisch
Idempotenz Teilweise Vollständig
Rollback Manuell Automatisiert
Secrets Management .env Files Ansible Vault
Infrastructure as Code Teilweise Vollständig
Learning Curve Niedrig Mittel
Best For Single Server, einfache Deployments Multiple Servers, komplexe Infrastruktur

Empfehlung für dein Projekt

Start: Script-Based

  1. Schneller Start
  2. Einfache Wartung
  3. Ausreichend für initiales Setup
  4. Docker Compose als Basis

Später: Ansible hinzufügen wenn

  • Zweiter Production Server
  • Staging Environment
  • Team wächst
  • Compliance-Anforderungen
  • Multi-Region Deployment

Hybrid-Ansatz (Empfohlen)

  1. Phase 1: Script-based deployment für initial setup
  2. Phase 2: Ansible für Server Provisioning (einmalig)
  3. Phase 3: Ansible Playbooks als Option für Team-Deployments
  4. Phase 4: CI/CD mit Ansible für automatisierte Deployments

Beide Optionen parallel verfügbar - Team kann wählen!


Nächste Schritte

  1. Start mit Script-based deployment (siehe DEPLOYMENT_WORKFLOW.md)
  2. 📝 Ansible Struktur vorbereiten (optional)
  3. 🔄 Bei Bedarf auf Ansible migrieren
  4. 🚀 CI/CD Pipeline einrichten

Die Scripts funktionieren weiterhin - Ansible ist Enhancement, kein Replacement!