← Ansible
15 мин · средне · Урок 6 из 12

Первый playbook

Структура yaml-плейбука: hosts, become, tasks, handlers, notify, vars, when, loop, Jinja2-шаблоны и block/rescue/always. Запуск ansible-playbook.

Playbook: что это

Playbook — это yaml-файл, который описывает что и на каких хостах делать. Внутри — список плеев (plays). Каждый плей говорит: «возьми эти хосты, стань суперпользователем (если нужно) и выполни задачи по порядку».

Один playbook — обычно один файл, один плей. Когда стек растёт, плеев становится больше, но начнём с одного.

Полный рабочий пример

---
- name: configure web server
  hosts: webservers
  become: true

  vars:
    nginx_port: 80
    app_root: /var/www/html
    packages:
      - nginx
      - curl
      - vim

  tasks:
    - name: install packages
      apt:
        name: "{{ packages }}"
        state: present
        update_cache: true

    - name: create site directory
      file:
        path: "{{ app_root }}"
        state: directory
        owner: www-data
        group: www-data
        mode: "0755"

    - name: deploy nginx config
      template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/sites-available/app.conf
        owner: root
        group: root
        mode: "0644"
      notify: restart nginx

    - name: enable config
      file:
        src: /etc/nginx/sites-available/app.conf
        dest: /etc/nginx/sites-enabled/app.conf
        state: link
      notify: restart nginx

    - name: start and enable nginx
      service:
        name: nginx
        state: started
        enabled: true

  handlers:
    - name: restart nginx
      service:
        name: nginx
        state: restarted

Разберём каждую часть.

Анатомия плея

hosts: — к каким хостам применяется. Значение берётся из инвентаря. Можно указать группу (webservers), конкретный хост (web-1), несколько через запятую (web-1,web-2) или all.

become: true — выполнять задачи с правами суперпользователя (sudo). Без этого модули, которые меняют системные файлы или устанавливают пакеты, упадут с ошибкой прав доступа.

vars: — переменные плея. Можно использовать в задачах как {{ имя }}. В примере packages — список, его можно передать прямо в apt: name: "{{ packages }}".

tasks: — список задач. Выполняются сверху вниз, по одной. Каждая задача состоит из name: (человекочитаемое описание) и модуля с аргументами.

handlers: — особые задачи, которые выполняются по уведомлению.

Handlers и notify

Handler — задача, которая запускается, только если кто-то её уведомил (notify), и только если уведомившая задача вернула changed.

Классический пример: перезапустить nginx, только если конфиг реально менялся.

tasks:
  - name: deploy config
    template:
      src: nginx.conf.j2
      dest: /etc/nginx/nginx.conf
    notify: restart nginx   # уведомление по имени handler'а

handlers:
  - name: restart nginx
    service:
      name: nginx
      state: restarted

Если template вернул ok (файл не изменился), handler не запустится. Если несколько задач уведомили одного handler'а, он всё равно запустится один раз, в конце плея. Это защита от лишних рестартов.

TASKS HANDLER QUEUE конец плея template: src: nginx.conf.j2 changed notify file: state=link (включить конфиг) changed notify x2 - схлопнется service: started (уже запущен) ok - без notify handlers: - name: restart nginx service: restarted nginx CHANGED выполняется 1 раз в конце плея changed - уведомляет handler ok - handler не вызывается handler запускается 1 раз
Два notify схлопываются в один вызов, и handler срабатывает один раз в конце плея.

when: условие

when: позволяет выполнить задачу только при определённом условии.

- name: configure firewall (Debian only)
  ufw:
    rule: allow
    port: "{{ nginx_port }}"
  when: ansible_os_family == "Debian"

Ansible собирает факты о хостах (ansible_os_family, ansible_distribution, ansible_architecture и десятки других) автоматически перед выполнением задач.

Условие может опираться и на результат предыдущей задачи. Тогда этот результат сначала надо куда-то положить. Для этого есть register:

- name: check if reboot is required
  ansible.builtin.stat:
    path: /var/run/reboot-required
  register: reboot_required

- name: reboot after kernel update
  ansible.builtin.reboot:
  when: reboot_required.stat.exists

Первая задача проверяет файл и складывает результат в переменную reboot_required, вторая читает из неё поле stat.exists. Без первой задачи вторая упадёт с ошибкой про undefined variable: сама по себе reboot_required ниоткуда не появляется. Частая ошибка при копировании готовых сниппетов — забрать when: и оставить register: в оригинальной статье.

loop: цикл

loop: повторяет задачу для каждого элемента списка. Текущий элемент доступен как {{ item }}.

- name: create users
  user:
    name: "{{ item }}"
    state: present
    shell: /bin/bash
  loop:
    - deploy
    - monitoring
    - backup

Для установки пакетов лучше передавать список прямо в apt, так быстрее (один вызов apt вместо нескольких):

- name: install packages
  apt:
    name:
      - nginx
      - curl
      - vim
    state: present

Но loop: незаменим, когда нужно создать несколько файлов, пользователей или ресурсов с разными параметрами.

Шаблоны Jinja2: что внутри .j2

Выше задача template разворачивала nginx.conf.j2, но мы не заглянули внутрь. Шаблон — это обычный конфиг, в который вставлены подстановки и управляющие конструкции Jinja2. На хост попадает уже отрендеренный файл, без всякого Jinja.

# {{ ansible_managed }}
server {
    listen {{ nginx_port }};
    server_name {{ inventory_hostname }};
    root {{ app_root }};

    {% if enable_gzip | default(false) %}
    gzip on;
    {% endif %}

    {% for path in deny_paths | default([]) %}
    location {{ path }} {
        deny all;
    }
    {% endfor %}
}

Что здесь работает:

Типичная ошибка — значение, начинающееся с {{, в YAML без кавычек: парсер примет его за начало словаря и упадёт. Пиши name: "{{ var }}".

block / rescue / always: обработка ошибок

block группирует задачи, rescue ловит ошибку внутри блока, always выполняется в любом случае. Это аналог try / except / finally, удобный для деплоя с откатом.

- name: deploy with rollback
  block:
    - name: roll out new version
      copy:
        src: "app-{{ version }}.jar"
        dest: /opt/app/app.jar
      notify: restart app

    - name: wait until service is healthy
      uri:
        url: http://localhost:8080/health
        status_code: 200
      register: health
      retries: 5
      delay: 3
      until: health.status == 200

  rescue:
    - name: roll back to previous version
      command: /opt/app/rollback.sh

  always:
    - name: remove lock file
      file:
        path: /tmp/deploy.lock
        state: absent

Если любая задача в block упала, Ansible переходит в rescue (там доступен результат через ansible_failed_result). always отрабатывает всегда: и при успехе, и при провале. Поэтому в него кладут очистку.

Запуск playbook

Базовая команда:

ansible-playbook -i inventory.ini site.yml

Полезные флаги:

ФлагЧто делает
-i inventory.iniуказать файл инвентаря
--checkdry-run: показать, что изменится, без применения
--diffпоказать построчный diff файлов и шаблонов
--limit web-1применить только к этому хосту/группе
-Kспросить sudo-пароль (become_password)
--tags deployвыполнить только задачи с тегом deploy
--skip-tags slowпропустить задачи с тегом slow
-v / -vv / -vvvувеличить детальность вывода

Пример с несколькими флагами — посмотреть, что изменится только на web-1:

ansible-playbook -i inventory.ini site.yml --limit web-1 --check --diff

Идемпотентность в контексте playbook

Второй прогон полного playbook'а должен показать changed=0. Nginx уже стоит, конфиг совпадает, сервис запущен: все задачи вернут ok. Handler не вызовется ни разу.

Это и есть проверка качества playbook'а: запусти дважды, посмотри на PLAY RECAP. Если changed > 0 на втором прогоне, значит, что-то нарушает идемпотентность.

Что важно запомнить

Playbook = yaml-файл с плеями. Плей = hosts + tasks + (опционально) handlers, vars, become.

Handlers запускаются только при changed и только один раз в конце плея. Это идеальный инструмент для рестартов сервисов при изменении конфига.

when: и loop: — основные инструменты управления потоком. Для пакетов предпочитай список прямо в модуле, а не loop:.

ansible-playbook --check --diff перед реальным применением — хорошая привычка.

В следующем уроке — роли и структура их папок.

Самопроверка

Впишите команду / ключевую строку. Регистр и пробелы не важны.

1. Запусти playbook site.yml с инвентарём inventory.ini.
2. Прогон вхолостую: посмотреть что изменится, без применения.
3. Примени playbook только к хосту web-1.

Дрилл · вопросы как на собесе

Проверь себя без подсказок

Сформулируй ответ вслух или письменно, потом раскрой и честно отметь: знал или нет.

01

Как использовать `loop`, `when`, `block`/`rescue`/`always` для условной и повторяющейся логики?

ANS-012
02

Что такое playbook и из каких частей он состоит (plays, tasks, handlers)?

ANS-002
03

Playbook отрабатывает с разным результатом на разных хостах одной группы. Алгоритм отладки по шагам.

ANS-016
04

Что такое handlers и зачем они нужны?

ANS-007
05

Шаблоны Jinja2 в Ansible: фильтры, циклы, условия, типичные ошибки экранирования.

ANS-015

Прошли материал до конца? Отметьте урок пройденным.