Модуль 04 — Переменные, факты, условия, циклы

Один плейбук для всех узлов, а различия между ними — в данных

Прогресс курса Модуль 4 из 9

Что вы освоите в этом модуле

01

Переменные групп и узлов

В модуле 03 имя пакета и порт были вписаны в задачи. Если для базы нужен другой набор пакетов, а у одного веб-узла другой порт, придётся копировать задачи. Вместо этого логику пишут один раз, а различия выносят в переменные. Переменные инвентаря удобнее всего хранить в каталогах рядом с инвентарём:

$ find group_vars host_vars -type f | sort
group_vars/all.yml
group_vars/db.yml
group_vars/web.yml
host_vars/web2.yml

Файл group_vars/ИМЯ.yml — переменные группы, host_vars/ИМЯ.yml — переменные узла; all — общие для всех. Ansible подхватывает их сам, подключать не нужно.

# Для всех узлов
node_role: общий
role_packages: [htop, curl]
admin_users:
  - name: ivanova
    comment: Иванова Анна, эксплуатация
    groups: [sudo]
  - name: petrov
    comment: Петров Олег, разработка
# Для группы web — перекрывает all
node_role: веб-сервер
role_packages: [htop, curl, nginx]
http_port: 80
# Только для web2 — перекрывает группу
http_port: 8080

Что получилось у двух веб-узлов после слияния всех уровней:

$ ansible-inventory --host web1
{
    "admin_users": [
        {
            "comment": "Иванова Анна, эксплуатация",
            "groups": [
                "sudo"
            ],
            "name": "ivanova"
        },
        {
            "comment": "Петров Олег, разработка",
            "name": "petrov"
        }
    ],
    "ansible_host": "172.30.50.11",
    "http_port": 80,
    "node_role": "веб-сервер",
    "role_packages": [
        "htop",
        "curl",
        "nginx"
    ]
}
$ ansible-inventory --host web2
{
    "admin_users": [
        {
            "comment": "Иванова Анна, эксплуатация",
            "groups": [
                "sudo"
            ],
            "name": "ivanova"
        },
        {
            "comment": "Петров Олег, разработка",
            "name": "petrov"
        }
    ],
    "ansible_host": "172.30.50.12",
    "http_port": 8080,
    "node_role": "веб-сервер",
    "role_packages": [
        "htop",
        "curl",
        "nginx"
    ]
}

node_role и role_packages группы web перекрыли значения из all, у web2 порт из host_vars перекрыл порт группы, а список администраторов пришёл из all без изменений. Чем точнее уровень, тем он сильнее.

02

Кто победит: приоритет

Мест, где можно задать переменную, в Ansible больше двадцати. Упрощённо, от слабых к сильным:

ГдеДля чего
defaults ролиЗначения по умолчанию, которые пользователь роли переопределяет (модуль 06)
group_vars/all → group_vars/группа → host_vars/узелДанные инвентаря: что отличает группы и узлы
vars в play, vars ролиКонстанты плейбука и роли
register, set_factЗначения, вычисленные во время прогона
-e в командной строкеСильнее всего: разовые значения для одного запуска

Практическое правило: держите каждую переменную в одном месте. Если одно имя задано на трёх уровнях, через полгода никто не скажет, откуда взялось значение. Проверка — ansible-inventory --host для данных инвентаря и задача debug для всего остального.

Переменная из -e перекрывает всё, в том числе host_vars:

$ ansible-playbook vars.yml --tags port -e http_port=9000

PLAY [Переменные, факты, условия и циклы] **************************************

TASK [Gathering Facts] *********************************************************
ok: [db1]
ok: [web1]
ok: [web2]

TASK [Порт nginx] **************************************************************
ok: [web1] => {
    "msg": "nginx на web1 слушает порт 9000"
}
skipping: [db1]
ok: [web2] => {
    "msg": "nginx на web2 слушает порт 9000"
}

PLAY RECAP *********************************************************************
db1                        : ok=1    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0   
web1                       : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
web2                       : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
03

Плейбук

---
- name: Переменные, факты, условия и циклы
  hosts: all
  become: true
  vars:
    maint_file: /etc/maintenance
  tasks:
    - name: Что известно об узле
      ansible.builtin.debug:
        msg: >-
          {{ inventory_hostname }} — {{ node_role }}:
          {{ ansible_facts['distribution'] }} {{ ansible_facts['distribution_version'] }},
          адрес {{ ansible_facts['default_ipv4']['address'] }}

    - name: Пакеты роли установлены
      ansible.builtin.apt:
        name: "{{ role_packages }}"
        state: present
        update_cache: true
        cache_valid_time: 3600

    - name: Учётные записи администраторов
      ansible.builtin.user:
        name: "{{ item.name }}"
        comment: "{{ item.comment }}"
        groups: "{{ item.groups | default([]) }}"
        append: true
        shell: /bin/bash
      loop: "{{ admin_users }}"
      loop_control:
        label: "{{ item.name }}"

    - name: Есть ли отметка обслуживания
      ansible.builtin.stat:
        path: "{{ maint_file }}"
      register: maint

    - name: Узел на обслуживании — сообщить
      ansible.builtin.debug:
        msg: "{{ inventory_hostname }} на обслуживании (есть {{ maint_file }}, {{ maint.stat.size }} байт)"
      when: maint.stat.exists

    - name: Порт nginx
      ansible.builtin.debug:
        msg: "nginx на {{ inventory_hostname }} слушает порт {{ http_port }}"
      when: "'web' in group_names"
      tags: [port]

Перед прогоном отметим один узел как находящийся на обслуживании:

$ ansible db1 -b -m copy -a "dest=/etc/maintenance content=\"замена диска 25.10\n\"" | grep -E "\| |\"changed\""
db1 | CHANGED => {
    "changed": true,
$ ansible-playbook vars.yml

PLAY [Переменные, факты, условия и циклы] **************************************

TASK [Gathering Facts] *********************************************************
ok: [db1]
ok: [web1]
ok: [web2]

TASK [Что известно об узле] ****************************************************
ok: [web1] => {
    "msg": "web1 — веб-сервер: Debian 13.7, адрес 172.30.50.11"
}
ok: [web2] => {
    "msg": "web2 — веб-сервер: Debian 13.7, адрес 172.30.50.12"
}
ok: [db1] => {
    "msg": "db1 — база данных: Debian 13.7, адрес 172.30.50.21"
}

TASK [Пакеты роли установлены] *************************************************
changed: [web1]
changed: [db1]
changed: [web2]

TASK [Учётные записи администраторов] ******************************************
changed: [web1] => (item=ivanova)
changed: [db1] => (item=ivanova)
changed: [web2] => (item=ivanova)
changed: [web1] => (item=petrov)
changed: [web2] => (item=petrov)
changed: [db1] => (item=petrov)

TASK [Есть ли отметка обслуживания] ********************************************
ok: [web1]
ok: [web2]
ok: [db1]

TASK [Узел на обслуживании — сообщить] *****************************************
skipping: [web1]
skipping: [web2]
ok: [db1] => {
    "msg": "db1 на обслуживании (есть /etc/maintenance, 30 байт)"
}

TASK [Порт nginx] **************************************************************
ok: [web1] => {
    "msg": "nginx на web1 слушает порт 80"
}
skipping: [db1]
ok: [web2] => {
    "msg": "nginx на web2 слушает порт 8080"
}

PLAY RECAP *********************************************************************
db1                        : ok=6    changed=2    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0   
web1                       : ok=6    changed=2    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0   
web2                       : ok=6    changed=2    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0   

Повторный прогон — changed=0, плейбук идемпотентен:

$ ansible-playbook vars.yml | tail -5
PLAY RECAP *********************************************************************
db1                        : ok=6    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0   
web1                       : ok=6    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0   
web2                       : ok=6    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0   
04

Циклы: loop

Задача «Учётные записи администраторов» выполняется для каждого элемента списка admin_users; текущий элемент — item. Элементы здесь — словари, и поля берутся через точку: item.name, item.comment. У Петрова нет поля groups, поэтому оно взято с фильтром default([]) — без него задача упала бы на неопределённом значении.

loop_control: label оставляет в выводе только имя, а не весь словарь — с паролями или ключами в словаре это ещё и вопрос безопасности. Модули, которые умеют принимать список, лучше вызывать без цикла: apt с name: "{{ role_packages }}" ставит все пакеты одной операцией, а не по одной на каждый.

05

register и when

register сохраняет результат задачи в переменную. Что в нём лежит, видно, если вызвать модуль отдельно — у stat это словарь stat со свойствами файла:

$ ansible db1 -m stat -a path=/etc/maintenance | grep -E "\| |\"exists\"|\"size\"|\"pw_name\""
db1 | SUCCESS => {
        "exists": true,
        "pw_name": "root",
        "size": 30,

В плейбуке результат проверки сохранён в maint, и следующая задача выполняется только при when: maint.stat.exists. На web1 и web2 файла нет — задача skipping. Условие when пишется без {{ }}: это уже выражение Jinja2. Условия по фактам выглядят так же: when: ansible_facts['distribution'] == 'Debian'.

Пара «проверить — сделать по результату» встречается постоянно: узнать версию установленной программы и обновить, если старая; проверить, инициализирована ли база, и инициализировать один раз. Именно так делают идемпотентными задачи на command: проверка через register, действие — по when.

06

Неопределённая переменная

$ ansible web1 -m debug -a "msg={{ db_password }}" | tail -4
web1 | FAILED! => {
    "changed": false,
    "msg": "Task failed: Finalization of task args for 'ansible.builtin.debug' failed: Error while resolving value for 'msg': 'db_password' is undefined"
}

Ansible не подставляет пустую строку вместо неизвестной переменной, а останавливает задачу. Это защита: опечатка в имени переменной не превратится в пустой пароль или пустой путь в rm. Когда значение действительно необязательное, это говорят явно фильтром default: {{ http_port | default(80) }}.

Ключевые выводы модуля

Запомните главное

  • Логика — в плейбуке, различия узлов — в group_vars и host_vars
  • Чем точнее уровень, тем сильнее: узел > группа > all; -e сильнее всего
  • Каждую переменную — в одном месте; проверка — ansible-inventory --host и debug
  • Значение из одной подстановки — в кавычках; в when скобки {{ }} не нужны
  • loop + item; списки пакетов — одной задачей без цикла; label скрывает лишнее в выводе
  • register + when — «проверить, потом сделать»; необязательные значения — через default
Практические задания — на учебном портале. Задания, критерии оценивания, сдача работ и оценки преподавателя — в курсе на portal.nevabit.ru. Учётную запись выдаёт преподаватель.
Модуль 03: Плейбуки Все модули Модуль 05: Шаблоны и обработчики