Стратегии деплоя с Ansible

TL;DR: serial: 1 — rolling update (по одному). serial: [1, "100%"] — canary. Blue-green — через atomic symlink. Всегда: healthcheck + вывод из LB + откат.

1. Rolling Deployment (Поэтапное обновление)

Обновляем серверы по очереди. Самый простой и надёжный метод.

- name: Rolling Deployment
  hosts: webservers
  serial: 1                          # по 1 хосту за раз
  max_fail_percentage: 20            # остановить если >20% упало
  become: yes
 
  pre_tasks:
    - name: Вывести из балансировщика
      ansible.builtin.uri:
        url: "http://lb.local/api/disable/{{ inventory_hostname }}"
        method: POST
      delegate_to: localhost
 
  tasks:
    - name: Stop service
      ansible.builtin.systemd:
        name: myapp
        state: stopped
 
    - name: Update code
      ansible.builtin.git:
        repo: "https://github.com/org/app.git"
        dest: /opt/app
        version: "{{ app_version }}"
 
    - name: Install dependencies
      ansible.builtin.command: npm ci --production
      args:
        chdir: /opt/app
 
    - name: Start service
      ansible.builtin.systemd:
        name: myapp
        state: started
 
    - name: Health check
      ansible.builtin.uri:
        url: http://localhost:8080/health
        status_code: 200
      register: health
      until: health.status == 200
      retries: 10
      delay: 5
 
  post_tasks:
    - name: Вернуть в балансировщик
      ansible.builtin.uri:
        url: "http://lb.local/api/enable/{{ inventory_hostname }}"
        method: POST
      delegate_to: localhost

Варианты serial

serial: 1              # по 1 хосту
serial: 3              # по 3 хоста
serial: "30%"          # по 30% от группы
serial:                # прогрессивный
  - 1                  # сначала 1 (canary)
  - 5                  # потом 5
  - "100%"             # потом все остальные

2. Blue-Green Deployment

Два идентичных окружения. Деплоим в «зелёное», переключаем трафик атомарно.

vars:
  app_dir: /opt/app
  releases_dir: "{{ app_dir }}/releases"
  release_name: "{{ ansible_date_time.epoch }}"
  release_path: "{{ releases_dir }}/{{ release_name }}"
  current_path: "{{ app_dir }}/current"
 
tasks:
  - name: Создать директорию релиза
    ansible.builtin.file:
      path: "{{ release_path }}"
      state: directory
 
  - name: Деплой новой версии (Green)
    ansible.builtin.git:
      repo: "https://github.com/org/app.git"
      dest: "{{ release_path }}"
      version: "{{ app_version }}"
 
  - name: Установить зависимости
    ansible.builtin.command: npm ci --production
    args:
      chdir: "{{ release_path }}"
 
  - name: Smoke test (проверить до переключения)
    ansible.builtin.command: node {{ release_path }}/healthcheck.js
    delegate_to: localhost
 
  - name: Переключить трафик (atomic symlink)
    ansible.builtin.file:
      src: "{{ release_path }}"
      dest: "{{ current_path }}"
      state: link
    notify: Reload app
 
  - name: Удалить старые релизы (оставить 5)
    ansible.builtin.shell: |
      ls -1dt {{ releases_dir }}/*/ | tail -n +6 | xargs rm -rf
    args:
      warn: false
    changed_when: false

Откат

# Быстрый откат — переключить symlink на предыдущий релиз
ansible webservers -m file -a \
  "src=/opt/app/releases/PREVIOUS dest=/opt/app/current state=link" -b

3. Canary Deployment

Выкатка на малую часть серверов → проверка метрик → полная выкатка.

- name: Canary Release
  hosts: webservers
  serial:
    - 1                              # Phase 1: один сервер
    - "100%"                         # Phase 2: все остальные
 
  tasks:
    - name: Deploy application
      ansible.builtin.apt:
        name: myapp
        state: latest
 
    - name: Health check
      ansible.builtin.uri:
        url: http://localhost:8080/health
        status_code: 200
      retries: 5
      delay: 3
 
    - name: Пауза после canary (только после 1-го батча)
      ansible.builtin.pause:
        prompt: |
          Canary deployed to {{ inventory_hostname }}.
          Check metrics at http://grafana.local/dashboard
          Press Enter to continue or Ctrl+C to abort...
      when: ansible_play_batch | length == 1
      run_once: true

Типичные ошибки

ОшибкаСимптомРешение
serial не заданВсе серверы обновляются одновременно → полный downtimeserial: 1 минимум
Нет healthcheckСломанная версия на всех серверахuri + until + retries после каждого деплоя
Нет вывода из LBТрафик идёт на сервер во время обновления → 502pre_tasks для вывода из балансировщика
Откат не предусмотренПаника при проблемахSymlink-подход: ln -sfn /releases/old /current
pause без run_onceПауза для каждого хоста в батчеrun_once: true для canary pause