D4.3 Deployment Workflows 🔄

Τα 4 βασικά Workflows

┌─────────────────────────────────────────────────────────┐
│  1. Fresh Deploy    → πρώτη εγκατάσταση                 │
│  2. Update          → νέα έκδοση εφαρμογής              │
│  3. Config Update   → αλλαγή ρυθμίσεων                  │
│  4. Rollback        → επιστροφή σε παλιά έκδοση         │
└─────────────────────────────────────────────────────────┘

Workflow 1: Fresh Deploy

# tasks/deploy_fresh.yml
---
# ============================================================
# Fresh Deployment — Πρώτη εγκατάσταση
# ============================================================

- name: Verify Docker is running
  ansible.builtin.service_facts:

- name: Assert Docker running
  ansible.builtin.assert:
    that:
      - ansible_facts.services['docker.service'].state == 'running'
    fail_msg: " Docker is not running!"

- name: Create directory structure
  ansible.builtin.file:
    path:  "{{ item }}"
    state: directory
    mode:  '0755'
    owner: root
    group: docker
  loop:
    - "{{ app_base_dir }}"
    - "{{ app_config_dir }}"
    - "{{ app_config_dir }}/nginx"
    - "{{ app_logs_dir }}"

- name: Render compose files
  ansible.builtin.template:
    src:  "{{ item.src }}"
    dest: "{{ item.dest }}"
    mode: "{{ item.mode | default('0644') }}"
  loop:
    - src:  docker-compose.yml.j2
      dest: "{{ app_base_dir }}/docker-compose.yml"
    - src:  nginx_compose.conf.j2
      dest: "{{ app_config_dir }}/nginx/nginx.conf"
  no_log: false

- name: Render .env file
  ansible.builtin.template:
    src:  app.env.j2
    dest: "{{ app_config_dir }}/.env"
    mode: '0600'
  no_log: true

- name: Pull all images
  community.docker.docker_compose_v2:
    project_src:  "{{ app_base_dir }}"
    project_name: "{{ app_name }}"
    pull:         always
    state:        present
  register: deploy_result
  no_log: true

- name: Wait for services healthy
  ansible.builtin.command:
    cmd: >
      docker compose
      -f {{ app_base_dir }}/docker-compose.yml
      ps --format json
  register:     services_status
  changed_when: false
  retries:      15
  delay:        10
  until: >
    services_status.stdout | from_json |
    selectattr('Health', 'equalto', 'unhealthy') |
    list | length == 0

- name: Fresh deploy complete
  ansible.builtin.debug:
    msg: " Fresh deployment complete!"

Workflow 2: Update (Rolling Update)

# tasks/deploy_update.yml
---
# ============================================================
# Update Deployment — Νέα έκδοση
# ============================================================

# ── Pre-update checks ─────────────────────────
- name: Get current version
  ansible.builtin.command:
    cmd: >
      docker compose
      -f {{ app_base_dir }}/docker-compose.yml
      ps --format json
  register:     current_state
  changed_when: false
  ignore_errors: true

- name: Show current state
  ansible.builtin.debug:
    msg: "Current: {{ current_state.stdout_lines }}"

# ── Backup current compose file ───────────────
- name: Backup current docker-compose.yml
  ansible.builtin.copy:
    src:    "{{ app_base_dir }}/docker-compose.yml"
    dest:   "{{ app_base_dir }}/docker-compose.yml.bak"
    remote_src: true
    mode:       '0644'
  ignore_errors: true

# ── Backup current .env ───────────────────────
- name: Backup current .env
  ansible.builtin.copy:
    src:        "{{ app_config_dir }}/.env"
    dest:       "{{ app_config_dir }}/.env.bak"
    remote_src: true
    mode:       '0600'
  ignore_errors: true
  no_log: true

# ── Render new compose file ───────────────────
- name: Render updated docker-compose.yml
  ansible.builtin.template:
    src:  docker-compose.yml.j2
    dest: "{{ app_base_dir }}/docker-compose.yml"
    mode: '0644'

# ── Render updated .env ───────────────────────
- name: Render updated .env
  ansible.builtin.template:
    src:  app.env.j2
    dest: "{{ app_config_dir }}/.env"
    mode: '0600'
  no_log: true

# ── Validate new compose file ─────────────────
- name: Validate new docker-compose.yml
  ansible.builtin.command:
    cmd: docker compose -f {{ app_base_dir }}/docker-compose.yml config
  register:     validate_result
  changed_when: false

# ── Pull new images ───────────────────────────
- name: Pull new images
  community.docker.docker_compose_v2:
    project_src:  "{{ app_base_dir }}"
    project_name: "{{ app_name }}"
    pull:         always
    state:        present
  register: update_result
  no_log: true

# ── Wait for healthy ──────────────────────────
- name: Wait for services healthy
  ansible.builtin.command:
    cmd: >
      docker compose
      -f {{ app_base_dir }}/docker-compose.yml
      ps --format json
  register:     post_update_status
  changed_when: false
  retries:      15
  delay:        10
  until: >
    post_update_status.stdout | from_json |
    selectattr('Health', 'equalto', 'unhealthy') |
    list | length == 0

# ── Verify HTTP ───────────────────────────────
- name: Verify HTTP after update
  ansible.builtin.uri:
    url:         "http://localhost/health"
    status_code: 200
  register:      post_update_http
  retries:       5
  delay:         5

# ── Cleanup backup αν OK ──────────────────────
- name: Remove backup files (update OK)
  ansible.builtin.file:
    path:  "{{ item }}"
    state: absent
  loop:
    - "{{ app_base_dir }}/docker-compose.yml.bak"
    - "{{ app_config_dir }}/.env.bak"
  when: not post_update_http.failed

- name: Update complete
  ansible.builtin.debug:
    msg:
      - " Update complete!"
      - "Version: {{ app_version }}"

Workflow 3: Config Update

# tasks/deploy_config.yml
---
# ============================================================
# Config Update — Μόνο αλλαγές ρυθμίσεων
# ============================================================

- name: Render nginx config
  ansible.builtin.template:
    src:  nginx_compose.conf.j2
    dest: "{{ app_config_dir }}/nginx/nginx.conf"
    mode: '0644'
  register: nginx_config_changed

- name: Reload nginx αν άλλαξε config
  ansible.builtin.command:
    cmd: >
      docker compose
      -f {{ app_base_dir }}/docker-compose.yml
      -p {{ app_name }}
      exec nginx nginx -s reload
  when:     nginx_config_changed.changed
  register: nginx_reload
  ignore_errors: true

- name: Render .env αν άλλαξε
  ansible.builtin.template:
    src:  app.env.j2
    dest: "{{ app_config_dir }}/.env"
    mode: '0600'
  no_log: true
  register: env_changed

- name: Restart app αν άλλαξε .env
  ansible.builtin.command:
    cmd: >
      docker compose
      -f {{ app_base_dir }}/docker-compose.yml
      -p {{ app_name }}
      restart app
  when: env_changed.changed

- name: Config update result
  ansible.builtin.debug:
    msg:
      - "Nginx config: {{ 'Changed ✅' if nginx_config_changed.changed else 'Unchanged' }}"
      - "Env file    : {{ 'Changed ✅' if env_changed.changed else 'Unchanged' }}"

Workflow 4: Rollback

# tasks/deploy_rollback.yml
---
# ============================================================
# Rollback — Επιστροφή σε παλιά έκδοση
# ============================================================

- name: Check backup exists
  ansible.builtin.stat:
    path: "{{ app_base_dir }}/docker-compose.yml.bak"
  register: backup_stat

- name: Assert backup exists
  ansible.builtin.assert:
    that:
      - backup_stat.stat.exists
    fail_msg: >
      ❌ No backup found!
      Cannot rollback without backup.
      Manual intervention required.

- name: Show backup info
  ansible.builtin.debug:
    msg:
      - "Backup found: {{ backup_stat.stat.path }}"
      - "Backup date : {{ backup_stat.stat.mtime | int | strftime('%Y-%m-%d %H:%M') }}"

- name: Restore docker-compose.yml από backup
  ansible.builtin.copy:
    src:        "{{ app_base_dir }}/docker-compose.yml.bak"
    dest:       "{{ app_base_dir }}/docker-compose.yml"
    remote_src: true
    mode:       '0644'

- name: Restore .env από backup
  ansible.builtin.copy:
    src:        "{{ app_config_dir }}/.env.bak"
    dest:       "{{ app_config_dir }}/.env"
    remote_src: true
    mode:       '0600'
  no_log: true
  ignore_errors: true

- name: Rollback deployment
  community.docker.docker_compose_v2:
    project_src:  "{{ app_base_dir }}"
    project_name: "{{ app_name }}"
    state:        present
    recreate:     always
  no_log: true

- name: Wait for rollback healthy
  ansible.builtin.command:
    cmd: >
      docker compose
      -f {{ app_base_dir }}/docker-compose.yml
      ps --format json
  register:     rollback_status
  changed_when: false
  retries:      12
  delay:        10
  until: >
    rollback_status.stdout | from_json |
    selectattr('Health', 'equalto', 'unhealthy') |
    list | length == 0

- name: Verify rollback
  ansible.builtin.uri:
    url:         "http://localhost/health"
    status_code: 200
  register:      rollback_http
  retries:       5
  delay:         5

- name: Rollback result
  ansible.builtin.debug:
    msg: >
      {{ '✅ Rollback successful!' if not rollback_http.failed
         else '❌ Rollback failed! Manual intervention needed!' }}

Master Deployment Playbook

cat > ~/ansible/playbooks/compose-deploy.yml << 'EOF'
---
# ============================================================
# Docker Compose Deployment Playbook
# Υποστηρίζει: fresh, update, config, rollback
# Χρήση: -e "deploy_mode=update" -e "app_version=2.0.0"
# ============================================================

- name: Docker Compose Deployment
  hosts: "{{ target | default('all_managed') }}"
  become: true
  gather_facts: true

  vars:
    # ── Deployment mode ───────────────────────
    deploy_mode:  "{{ deploy_mode | default('update') }}"
    # Επιλογές: fresh, update, config, rollback

    # ── App settings ──────────────────────────
    app_name:     mywebapp
    app_version:  "{{ app_version | default('1.0.0') }}"
    app_env:      "{{ app_env     | default('production') }}"
    app_port:     3000

    # ── Paths ─────────────────────────────────
    app_base_dir:   "/opt/{{ app_name }}"
    app_config_dir: "/opt/{{ app_name }}/config"
    app_logs_dir:   "/opt/{{ app_name }}/logs"

    # ── Images ────────────────────────────────
    nginx_image:    nginx:1.25.3-alpine
    postgres_image: postgres:15.4-alpine
    redis_image:    redis:7.2-alpine
    app_image:      "nginx:1.25.3-alpine"    # ← placeholder

    # ── Database ──────────────────────────────
    postgres_db:    "{{ app_name }}_db"
    postgres_user:  "{{ app_name }}_user"
    postgres_port:  5432

    # ── Redis ─────────────────────────────────
    redis_port:    6379
    redis_maxmem:  "128mb"

    # ── Nginx ─────────────────────────────────
    nginx_host_port:   80
    nginx_rate_limit:  true
    monitoring_enabled: false

  pre_tasks:

    # ── Validation ────────────────────────────
    - name: Validate deploy_mode
      ansible.builtin.assert:
        that:
          - deploy_mode in ['fresh', 'update', 'config', 'rollback']
        fail_msg: >
          ❌ Invalid deploy_mode: {{ deploy_mode }}
          Valid options: fresh, update, config, rollback
      tags: always

    - name: Deployment info
      ansible.builtin.debug:
        msg:
          - "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
          - "Docker Compose Deployment"
          - "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
          - "Mode   : {{ deploy_mode | upper }}"
          - "App    : {{ app_name }} v{{ app_version }}"
          - "Env    : {{ app_env }}"
          - "Host   : {{ inventory_hostname }}"
          - "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
      tags: always

    - name: Verify Docker
      ansible.builtin.service_facts:
      tags: always

    - name: Assert Docker running
      ansible.builtin.assert:
        that:
          - ansible_facts.services['docker.service'].state == 'running'
        fail_msg: "❌ Docker is not running!"
      tags: always

  tasks:

    # ════════════════════════════════════════
    # FRESH DEPLOY
    # ════════════════════════════════════════
    - name: Fresh deployment
      ansible.builtin.include_tasks:
        file: "{{ playbook_dir }}/../roles/myapp/tasks/deploy_fresh.yml"
      when: deploy_mode == 'fresh'
      tags: [fresh, deploy]

    # ════════════════════════════════════════
    # UPDATE
    # ════════════════════════════════════════
    - name: Update deployment
      ansible.builtin.include_tasks:
        file: "{{ playbook_dir }}/../roles/myapp/tasks/deploy_update.yml"
      when: deploy_mode == 'update'
      tags: [update, deploy]

    # ════════════════════════════════════════
    # CONFIG UPDATE
    # ════════════════════════════════════════
    - name: Config update
      ansible.builtin.include_tasks:
        file: "{{ playbook_dir }}/../roles/myapp/tasks/deploy_config.yml"
      when: deploy_mode == 'config'
      tags: [config]

    # ════════════════════════════════════════
    # ROLLBACK
    # ════════════════════════════════════════
    - name: Rollback deployment
      ansible.builtin.include_tasks:
        file: "{{ playbook_dir }}/../roles/myapp/tasks/deploy_rollback.yml"
      when: deploy_mode == 'rollback'
      tags: [rollback]

  post_tasks:

    # ── Status report ─────────────────────────
    - name: Get final stack status
      ansible.builtin.command:
        cmd: >
          docker compose
          -f {{ app_base_dir }}/docker-compose.yml
          -p {{ app_name }}
          ps
      register:     final_status
      changed_when: false
      ignore_errors: true
      tags: always

    - name: Final HTTP check
      ansible.builtin.uri:
        url:         "http://localhost/health"
        status_code: 200
      register:      final_http
      ignore_errors: true
      tags: always

    - name: Deployment Summary
      ansible.builtin.debug:
        msg:
          - "╔══════════════════════════════════════╗"
          - "║  DEPLOYMENT SUMMARY                  ║"
          - "╠══════════════════════════════════════╣"
          - "║ Mode   : {{ deploy_mode | upper }}"
          - "║ App    : {{ app_name }} v{{ app_version }}"
          - "║ Host   : {{ inventory_hostname }}"
          - "║ HTTP   : {{ '✅ OK' if not final_http.failed else '❌ FAILED' }}"
          - "╠══════════════════════════════════════╣"
          - "║ Services:                            ║"
          - "{{ final_status.stdout | default('N/A') }}"
          - "╚══════════════════════════════════════╝"
      tags: always
EOF

Εκτέλεση όλων των Workflows

# ── Fresh deployment ──────────────────────────
ansible-playbook playbooks/compose-deploy.yml \
    --limit nextcloud \
    -e "deploy_mode=fresh" \
    -e "app_version=1.0.0" \
    -v

# ── Update σε νέα έκδοση ──────────────────────
ansible-playbook playbooks/compose-deploy.yml \
    --limit nextcloud \
    -e "deploy_mode=update" \
    -e "app_version=1.1.0" \
    -v

# ── Config update μόνο ────────────────────────
ansible-playbook playbooks/compose-deploy.yml \
    --limit nextcloud \
    -e "deploy_mode=config" \
    -v

# ── Rollback αν κάτι πήγε στραβά ─────────────
ansible-playbook playbooks/compose-deploy.yml \
    --limit nextcloud \
    -e "deploy_mode=rollback" \
    -v

# ── Dry run πριν update ───────────────────────
ansible-playbook playbooks/compose-deploy.yml \
    --limit nextcloud \
    -e "deploy_mode=update" \
    -e "app_version=2.0.0" \
    --check --diff

Σύνοψη D4.3

Deployment Workflows
├── Fresh Deploy:
│   create dirs → render templates
│   → pull images → deploy → verify
│
├── Update:
│   backup → render new templates
│   → validate → pull → deploy
│   → verify → cleanup backup
│
├── Config Update:
│   render config → reload nginx
│   → restart app αν .env άλλαξε
│
├── Rollback:
│   check backup → restore files
│   → redeploy → verify
│
└── Master Playbook:
    -e "deploy_mode=fresh|update|config|rollback"
    -e "app_version=X.Y.Z"