devops-курс Все курсы

✻ Урок 7.4 · Тема 7: IaC: Terraform и Ansible

Ansible: инвентарь, модули, ad-hoc

⏱ 3 ч

Зачем это нужно

Terraform создал ВМ, но она пустая: нет Docker, нет пользователя notes, нет каталогов. Зайти по SSH и набрать команды руками можно один раз. На третьем сервере ты забудешь шаг, на десятом получишь машины, которые «почти одинаковые», и никто не вспомнит, чем именно они отличаются. Ansible заходит на серверы по SSH (защищённому удалённому входу, урок 2.2) сам, приводит их к описанному состоянию и не требует ставить на них никаких программ-агентов (постоянно работающих помощников на самом сервере). Это как мастер с чемоданом инструментов, который сам приезжает по списку адресов: не нужно держать по сотруднику в каждом доме.

На работе Ansible встречается везде, где есть виртуальные машины: обновить пакет на 40 серверах, раскатить конфиг, проверить, что везде одна версия. Вопросы про идемпотентность (повторный запуск не ломает и не дублирует результат), инвентарь (список серверов) и become (выполнить действие с правами администратора, как sudo) есть почти на каждом собеседовании по DevOps. Всё это подробно разберём в теории ниже.

Шаг проекта: в репозитории «Заметок» ты создаёшь папку ~/notes/infra/ansible/ (в задании 1) и кладёшь в неё два файла: ansible.cfg (настройки по умолчанию: под каким пользователем и с каким ключом входить) и inventory.yml (список серверов; пока один, с адресом ВМ из terraform output). Плейбуки (файлы с описанием настройки) будут в уроке 7.5.

Что нужно знать

Всё остальное (что такое YAML, инвентарь, модуль, факты) объясняется ниже с нуля: YAML это формат текстовых файлов, где структура задаётся отступами; модуль это готовая «умелка» Ansible под одну задачу; факты это сведения о сервере, которые Ansible узнаёт сам.

Картина целиком

Представь прораба на стройке с десятью одинаковыми домами. Он не берёт молоток и не идёт красить каждую стену сам. У него есть список адресов (какие дома), бригада с инструментом (умеет штукатурить, красить, ставить двери) и задание («в каждом доме должна быть покрашена кухня»). Если кухня уже покрашена, бригада ничего не делает. Если нет, красит.

Ansible работает так же:

  • список адресов это инвентарь (inventory): какие серверы и как к ним подключиться;
  • бригада с инструментом это модули (modules): готовые умелые «рабочие» для пакетов, файлов, пользователей, сервисов;
  • задание это желаемое состояние: «пакет htop должен стоять», а не «выполни apt install».

Вот путь одной команды:

flowchart LR
    subgraph C["твой компьютер: управляющий узел"]
        CFG["ansible.cfg<br>умолчания"]
        INV["inventory.yml<br>кто такие"]
        CMD["команда ansible notes<br>-m apt -a name=htop"]
    end
    subgraph S["сервер: управляемый узел"]
        SSH["sshd и python3"]
        TMP["модуль во временном каталоге<br>ставит htop или нет"]
    end
    CFG --> CMD
    INV --> CMD
    CMD -->|"1. SSH, порт 22: копирует модуль"| SSH
    SSH -->|"2. запускает"| TMP
    TMP -->|"3. JSON: changed true или false"| CMD

На сервере никакая программа Ansible заранее не установлена. Нужны только SSH и Python. За урок ты разберёшь каждый кусок схемы: как устроен инвентарь, что такое модуль и почему «идемпотентно» важное слово, как Ansible получает права root, что такое факты и как проверить действие без последствий.

Теория

Зачем нужен Ansible: проблема «снежинок»

Без автоматизации сервер настраивают руками: зашёл по SSH, поставил пакеты, поправил конфиг, создал пользователя. Через месяц никто не помнит, что именно сделано. Два сервера, настроенные «одинаково», на деле различаются мелочами: на одном забыли открыть порт, на другом стоит другая версия пакета. Такие серверы называют снежинками (snowflake servers): каждый уникален, и повторить его нельзя. Если сервер сгорел, восстановить его так же невозможно.

Ansible решает это так: настройка описана в текстовых файлах. Файлы лежат в git (урок 3.1), их можно перечитать, проверить, применить к новой машине и получить точную копию. Это подход конфигурация как код (configuration as code), а инструмент такого класса называется системой управления конфигурацией (configuration management).

Аналогия: рецепт против «готовил на глаз». По рецепту блюдо повторит любой повар. Оговорка: рецепт описывает только то, что ты в него записал. Если кто-то зашёл на сервер и поправил файл руками, рецепт об этом не знает. Это называется дрейф конфигурации (drift), и ему посвящён урок 7.7.

Terraform и Ansible делят работу. Terraform создаёт ВМ, сети и диски через API облака (урок 7.1). Ansible настраивает то, что внутри ВМ: пакеты, файлы, пользователей, сервисы. Аналогия: Terraform строит дом, Ansible расставляет мебель.

Главное: Ansible описывает настройку внутри ВМ текстом в Git, а Terraform создаёт сами ВМ, сети и диски.

Проверь понимание: ты хочешь создать ВМ с 2 ядрами и 4 ГБ памяти, а потом поставить на неё nginx. Что для этого возьмёшь?

Ответ

Ресурсы (ВМ, сеть, диск) создаёт Terraform: это работа с API облака. Установить nginx внутри ВМ должен Ansible: это работа по SSH внутри машины.

Прикинь сам: Сервер сгорел, а настраивал его коллега полгода назад руками. Сколько времени уйдёт на точную копию?

Неизвестно: никто не помнит всех шагов, и копия выйдет «почти такой же». Если настройка описана в файлах в Git, копия поднимается за минуты и совпадает с оригиналом.

Теперь посмотрим, как Ansible попадает на сервер без всяких агентов.

Как работает Ansible: без агента

Агент (agent) это постоянно работающая программа на управляемой машине, которая принимает команды. Ansible работает без агента (agentless): на сервере нет ничего, кроме SSH-сервера и Python (языка программирования, на котором написаны модули). SSH мы разбирали в уроке 2.2: это способ безопасно войти на удалённую машину и выполнить там команду.

Как это происходит по шагам:

  1. Ты запускаешь ansible на своём компьютере. Он называется управляющий узел (control node).
  2. Ansible читает инвентарь (inventory): список серверов, куда идти. Каждый такой сервер называется управляемый узел (managed node) или хост (host).
  3. Он подключается к каждому хосту по SSH.
  4. Копирует туда маленькую Python-программу: модуль (module). Например, модуль apt умеет ставить пакеты.
  5. Запускает её на хосте.
  6. Забирает результат в формате JSON (текстовый формат "ключ": значение, который удобно читать и людям, и программам) и удаляет программу.

Это модель push («толкать»): изменение начинается с твоей стороны. Противоположная модель pull («тянуть»): агент на сервере сам ходит за конфигурацией (так работают Puppet и Chef).

Плюсы push: на серверах нечего ставить и обновлять, порядок под твоим контролем. Минусы: пока ты не запустил Ansible, сервер сам себя не исправит, нужен SSH-доступ до всех машин, а на тысячах серверов SSH становится узким местом.

Прикинь сам: Ты запустил ansible и погасил ноутбук. Что будет делать сервер после этого?

Ничего: на сервере нет постоянно работающей программы Ansible, и сам себя он не настроит. Всё идёт по твоей команде (модель push).

Осторожно: «без агента» не значит «без Python на сервере». Модули Ansible это Python-скрипты, и интерпретатору Python на цели быть нужно. Постоянно работающего процесса нет, но Python обязателен. Исключение: модуль raw просто гонит команду через SSH и не требует Python (им обычно и ставят Python на голую машину).

Главное: без агента значит без постоянного процесса, но с SSH и Python на сервере; модуль приезжает, отрабатывает и удаляется.

Проверь понимание: зачем Ansible на сервере Python, если он «без агента»?

Ответ

Модули Ansible это Python-скрипты. Ansible копирует модуль на сервер, там его запускает интерпретатор Python, результат возвращается как JSON. Постоянно работающего процесса нет, но Python на цели должен быть. Для голой машины без Python есть модуль raw.

Раз всё идёт по SSH, вспомним, как устроен вход по ключу.

SSH-минимум: ключи и отпечатки

Ansible входит на сервер так же, как ты: по SSH. Чтобы понимать ошибки, вспомни три вещи из урока 2.2.

Пара ключей. Приватный ключ (файл ~/.ssh/id_ed25519) хранится только у тебя. Публичный (~/.ssh/id_ed25519.pub) кладут на сервер в файл ~/.ssh/authorized_keys пользователя, под которым входят. Сервер проверяет, что у тебя есть приватный ключ к этому публичному, и пускает без пароля. Аналогия: публичный ключ это замок, который ты повесил на дверь сервера, приватный это единственный ключ от него. Оговорка: замок можно копировать и раздавать сколько угодно, а ключ нет.

Пользователь. Вход всегда под конкретным именем: ubuntu@203.0.113.10. Если ключ лежит в authorized_keys пользователя ubuntu, то под root этим ключом не войти.

Отпечаток хоста. При первом подключении SSH спрашивает: «это точно тот сервер?» и запоминает его отпечаток (host key) в ~/.ssh/known_hosts. Это защита от подмены сервера. Для Ansible такой вопрос неудобен, поэтому в учебной ВМ проверку можно отключить (host_key_checking = False), но на рабочих серверах этого делать нельзя: пропадает защита от подмены.

Прикинь сам: Ansible пишет Permission denied (publickey). Это ошибка Ansible или SSH, и как быстро проверить?

Это ответ SSH-сервера: ключ не подходит. Проверь вход командой ssh ubuntu@<адрес>, и если она падает так же, чини SSH, а не Ansible.

Осторожно: ошибка Permission denied (publickey) это не ошибка Ansible, а ответ SSH-сервера: «твой ключ мне не подходит». Проверяй вход вручную командой ssh, и если она падает так же, разбирайся с SSH, а не с Ansible.

Главное: публичный ключ лежит на сервере в authorized_keys, приватный только у тебя; Ansible входит так же, как ты руками.

Теперь освоим формат, на котором пишутся все файлы Ansible: YAML.

YAML: формат, в котором пишется всё

Инвентарь и плейбуки Ansible пишутся на YAML: это текстовый формат для описания данных, где структуру задают отступы (пробелы в начале строки). Знать нужно три конструкции.

# словарь: ключ и значение через двоеточие и пробел
name: notes-vm
port: 22

# вложенный словарь: вложенность задаётся отступом в 2 пробела
server:
  name: notes-vm
  user: ubuntu

# список: каждый пункт начинается с "- "
packages:
  - htop
  - curl

Правила, на которых ломаются новички. Отступы только пробелами, табуляция запрещена. Все элементы одного уровня стоят с одинаковым отступом. После двоеточия обязателен пробел: port:22 не словарь, а просто строка. Комментарий начинается с #. Значения со спецсимволами (двоеточие, {) берут в кавычки.

Аналогия: YAML это оглавление книги, где подпункты сдвинуты вправо. Оговорка: в книге сдвиг просто красота, а в YAML сдвиг это смысл. Лишний пробел превращает «внутри группы» в «рядом с группой».

Ошибка отступа выглядит как ERROR! Unable to parse .../inventory.yml as an inventory source. Дальше в уроке ты увидишь, как проверить файл командой ansible-inventory --graph.

Главное: в YAML структуру задают пробелами: словарь, вложенный словарь и список с «- »; табуляция запрещена.

Проверь понимание: чем отличается packages: с двумя пунктами - htop, - curl от строки packages: htop, curl?

Ответ

Первое это список из двух элементов, второе одна строка htop, curl. Модуль получит один элемент с запятой в названии и не найдёт такого пакета.

Прикинь сам: В строке port:22 нет пробела после двоеточия. Что получит Ansible?

Не пару «ключ и значение», а обычную строку port:22. Такая ошибка не всегда падает сразу, поэтому после двоеточия пробел ставят всегда.

Теперь на YAML опишем список серверов: инвентарь.

Инвентарь: список серверов и групп

Инвентарь (inventory) это файл, который говорит Ansible, куда ходить и как подключаться. Без него Ansible не знает ни одного сервера. Есть два формата: INI (короче) и YAML (нагляднее, когда есть переменные). Мы используем YAML.

Серверы объединяют в группы (groups): например, web, db, notes. Команда «обнови nginx на группе web» сработает сразу на всех её серверах. Аналогия: список контактов в телефоне, где есть папки «Семья» и «Работа»: можно написать одному человеку, а можно всей папке. Оговорка: один сервер может состоять в нескольких группах сразу.

# inventory.yml: одна группа notes с одним сервером
all:                          # корневая группа: в неё входят вообще все хосты
  children:                   # "дети": вложенные группы
    notes:                    # наша группа
      hosts:                  # список хостов группы
        notes-vm:             # имя хоста: метка для тебя
          ansible_host: 203.0.113.10   # реальный адрес, куда подключаться
          ansible_user: ubuntu         # под каким пользователем входить

Разбор. all это корневая группа, в неё автоматически входят все хосты. Внутри неё children перечисляет дочерние группы, у нас одна: notes. В ней в hosts описан хост с именем notes-vm. Имя хоста это метка для тебя, а реальный адрес задаёт переменная ansible_host. Если её не указать, Ansible возьмёт само имя как DNS-имя (урок 2.3). Переменные, чьё имя начинается на ansible_, управляют подключением: ansible_user (пользователь), ansible_port (порт SSH), ansible_ssh_private_key_file (файл приватного ключа).

Помимо notes есть ещё группа ungrouped («без группы»): в неё попадают хосты, которые не указаны ни в одной группе. Группы all и ungrouped существуют всегда.

Переменные на группу и хост. Одинаковые значения не копируют в каждый хост. Их задают на уровне группы блоком vars, как в задании 5. Либо кладут в каталоги рядом с инвентарём: group_vars/<группа>.yml и host_vars/<хост>.yml. Правило приоритета: переменная хоста сильнее переменной группы, а переменная группы сильнее all. Более узкое побеждает более общее. Аналогия: правило «на этой кухне не курить» сильнее общего правила «на территории можно». Полный список приоритетов разберём в уроке 7.5.

Как выбрать хосты в команде. Первый аргумент ansible это шаблон хостов (pattern): имя группы (notes), имя хоста (notes-vm), слово all (все) или список через запятую. Если шаблону ничего не соответствует, Ansible пишет Could not match supplied host pattern и ничего не делает.

Проверить, как Ansible понял инвентарь: ansible-inventory --graph рисует дерево групп и хостов.

Прикинь сам: В группе notes один хост, а ты запустил ansible web -m ping. Что ответит Ansible?

Could not match supplied host pattern: шаблон web ничему не соответствует. Ansible предупредит и ничего не сделает.

Осторожно: имя хоста и адрес. notes-vm в инвентаре не обязано ничего значить для DNS. Ansible подключается к ansible_host, а имя используется в выводе и в шаблонах.

Главное: инвентарь говорит, куда ходить: хосты, группы и переменные подключения ansible_host и ansible_user; более узкая переменная побеждает общую.

Проверь понимание: в инвентаре хост notes-vm без ansible_host. Куда попытается подключиться Ansible?

Ответ

Он возьмёт само имя notes-vm как DNS-имя. Если его нет в DNS и в /etc/hosts, получишь UNREACHABLE с Could not resolve hostname.

Откуда победитель берётся, покажу на числах.

Модули и идемпотентность

Модуль (module) это готовая программа под одну задачу: поставить пакет, создать пользователя, положить файл. Ты не пишешь apt-get install руками, а вызываешь модуль apt и говоришь ему, какое состояние нужно.

Главное свойство хороших модулей: ты описываешь желаемое состояние, а не действие. Запись name=htop state=present значит «пакет htop должен стоять». Сравни:

  • действие: «выполни apt-get install htop». Второй запуск переустановит, а на другом дистрибутиве команды может не быть;
  • состояние: «htop должен стоять». Модуль сам смотрит: стоит? Тогда ничего не делает.

Это свойство называется идемпотентность (idempotency): повторный запуск даёт то же состояние и ничего лишнего не меняет. Аналогия: лифт. Кнопку «5 этаж» можно нажать хоть десять раз, лифт поедет один раз. Оговорка: обычный скрипт с apt-get install ведёт себя как «поезжай на 5 этаж» и при повторе делает лишнюю работу или падает.

Как идемпотентность видна в выводе. Каждый модуль отвечает статусом:

Статус Что значит
SUCCESS / ok ("changed": false) всё выполнено, но менять ничего не пришлось: состояние уже нужное
CHANGED ("changed": true) модуль что-то изменил
FAILED модуль дошёл до сервера, но выполнить не смог
UNREACHABLE до сервера не достучались: SSH, ключ, адрес

Порядок ожидания: первый запуск даёт CHANGED, второй SUCCESS. Если второй запуск снова пишет CHANGED, что-то не идемпотентно.

Основные модули (в этом уроке ты попробуешь первые несколько):

Модуль Что делает
ansible.builtin.ping проверяет, что есть SSH-вход и Python (это не ICMP-ping из урока 2.1)
ansible.builtin.setup собирает факты (facts): ОС, IP, память
ansible.builtin.apt ставит и удаляет пакеты
ansible.builtin.copy кладёт файл или текст на сервер
ansible.builtin.file создаёт каталоги, задаёт владельца и права
ansible.builtin.user создаёт пользователей
ansible.builtin.lineinfile гарантирует наличие строки в файле
ansible.builtin.systemd_service запускает и включает сервисы
ansible.builtin.command выполняет команду без оболочки
ansible.builtin.shell выполняет команду через /bin/sh (работают пайпы \| и >)
ansible.builtin.raw голая команда по SSH, Python не нужен

Имя ansible.builtin.apt называется FQCN (fully qualified collection name, полное имя с коллекцией): коллекция (ansible.builtin, набор модулей, поставляемый вместе с Ansible) плюс имя модуля. Короткое apt тоже работает, но в плейбуках принято полное имя: так нет путаницы, если в другой коллекции есть модуль с таким же именем.

Особые случаи: command и shell. Они запускают произвольную команду, а Ansible не понимает, что она изменила. Поэтому они всегда пишут changed, даже если команда только читает данные. command запускает программу напрямую, без оболочки: пайпы, > и переменные вроде $HOME в ней не работают. shell идёт через /bin/sh, пайпы работают, но выше риск инъекции (когда посторонний текст попадает в команду и исполняется). Правило: где есть готовый модуль, берём модуль. Чтобы сделать command честным, задают параметры creates (не запускай, если этот файл уже есть) или removes (не запускай, если этого файла нет).

Ad-hoc это разовая команда без плейбука. Формат: ansible <шаблон-хостов> -m <модуль> -a "<аргументы>". Удобна для быстрой проверки («какое ядро на всех серверах») или срочного действия. Для повторяемой настройки пишут плейбук (урок 7.5).

Разберём пример целиком:

ansible notes -m ansible.builtin.apt -a "name=htop state=present" --become
   |      |     |                      |                          |
   |      |     |                      |                          +-- получить права root через sudo
   |      |     |                      +-- аргументы модуля: пакет и желаемое состояние
   |      |     +-- какой модуль запустить
   |      +-- флаг -m: "module"
   +-- шаблон хостов: группа notes

Прикинь сам: Ты запустил apt с name=htop state=present дважды. Что напишет второй запуск?

SUCCESS и "changed": false: пакет уже стоит, делать нечего. Если бы второй запуск снова писал CHANGED, модуль не был бы идемпотентным.

Осторожно: «идемпотентно» не значит «без последствий». Идемпотентный модуль делает изменение один раз, а не «ничего не делает». Ещё путают ansible.builtin.ping с сетевым пингом: он проверяет вход по SSH и наличие Python, а не доступность по ICMP.

Главное: ты описываешь желаемое состояние, а не действие; модуль сам проверяет, нужно ли что-то менять, и отвечает changed true или false.

Проверь понимание: чем ansible all -m command -a "useradd bob" хуже модуля user с name=bob?

Ответ

Второй запуск command упадёт с ошибкой «user already exists», а модуль user увидит, что bob есть, и ответит ok. Модуль проверяет состояние перед действием, command просто выполняет.

Как приходит решение «менять или нет», посмотрим на конкретных числах.

Разобранный пример: чья переменная победит

Возьмём два хоста и посмотрим, какое значение получит каждый. Допустим, есть инвентарь и два файла переменных:

inventory.yml            group_vars/notes.yml       host_vars/notes-vm2.yml
-------------            --------------------       -----------------------
all:                     ssh_port: 22               ssh_port: 2222
  children:              app_env: prod
    notes:
      hosts:
        notes-vm1:
        notes-vm2:
flowchart LR
    A["group_vars/all<br>общее"] --> B["group_vars/notes.yml<br>группа"]
    B --> C["host_vars/хост.yml<br>хост"]
    C --> D["-e в команде<br>сильнее всех"]

Слева самое слабое, справа самое сильное: каждое следующее перекрывает предыдущее.

Для notes-vm1 файла host_vars/notes-vm1.yml нет, значит побеждает значение группы: ssh_port = 22. Для notes-vm2 есть переменная самого хоста: ssh_port = 2222, она перекрывает групповую. app_env в файле хоста не задан, поэтому у обоих хостов она берётся из группы: prod. Итог по хостам:

Хост ssh_port app_env Откуда
notes-vm1 22 prod обе из группы
notes-vm2 2222 prod порт из хоста, среда из группы

Что это даёт на практике: общее значение пишешь один раз на группу, а исключение только там, где оно нужно. Если завтра появится notes-vm3 в группе notes, он сам получит 22 и prod без правок. Разовое значение для одного запуска можно задать ключом -e (например -e app_env=stage): оно сильнее всех файлов, но живёт только этот запуск.

Прикинь сам: У хоста notes-vm3 нет своего файла в host_vars, а в группе notes задан ssh_port: 22. Какой порт получит notes-vm3?

22: переменная группы действует на каждого её члена, а своей у хоста нет.

Главное: общее значение пишут один раз на группу, исключение только на хосте, а -e перекрывает всё на один запуск.

Теперь посмотрим, как модуль принимает решение изменить файл или нет.

Разобранный пример: как модуль решает «менять или нет»

Возьмём copy из задания 4 и посмотрим, что происходит на сервере при двух запусках подряд. Текст notes managed by ansible занимает 24 байта (24 символа, каждый один байт). Его контрольная сумма (checksum, число-«отпечаток» содержимого: одинаковый текст даёт одинаковое число, любое изменение даёт другое) равна 397cf16cc9544a3bdcca2c26f8a2f8a12f728122.

flowchart TD
    S["copy: нужен файл с этим текстом"] --> Q{"файл есть и<br>контрольная сумма совпала?"}
    Q -->|"нет"| W["записывает файл<br>changed: true"]
    Q -->|"да"| N["ничего не делает<br>changed: false"]

Первый запуск. Модуль смотрит, есть ли /etc/motd-notes. Файла нет, желаемое состояние «файл с этим текстом» не достигнуто. Модуль записывает файл, ставит права 0644 и владельца root, и отвечает "changed": true.

Второй запуск. Файл есть. Модуль считает контрольную сумму текста на сервере и сравнивает с суммой того, что ты просишь положить. Суммы совпали (397cf16c...), права и владелец тоже. Делать нечего: "changed": false.

Изменили текст на notes managed by Ansible (одна заглавная буква). Контрольная сумма станет другой, модуль перепишет файл и снова ответит changed: true. Так copy замечает изменения без сравнения текстов вручную.

Так же рассуждают остальные модули. apt спрашивает у системы «пакет htop стоит?», user смотрит в /etc/passwd, file проверяет права и владельца. command такой проверки не делает: он просто выполняет строку, и поэтому всегда changed.

Прикинь сам: В файле notes managed by ansible поменяли одну букву на заглавную. Что сделает copy?

Контрольная сумма станет другой, модуль перепишет файл и ответит changed: true.

Осторожно: идемпотентность обеспечивает модуль, а не Ansible «волшебно». Если модуль написан плохо или используется shell, идемпотентности не будет, сколько ни пиши state=present.

Главное: идемпотентность обеспечивает модуль: он сравнивает состояние с желаемым; command и shell такого сравнения не делают.

Для записи в /etc одних прав пользователя не хватит, тут нужен become.

become: работа от имени root

Ansible входит под обычным пользователем (ubuntu), у которого мало прав. Ставить пакеты, писать в /etc и управлять сервисами может только root (урок 1.3). become это механизм Ansible «стать другим пользователем» (по умолчанию root): он оборачивает выполнение модуля в sudo. В ad-hoc это флаг --become (сокращённо -b). В плейбуках become: true.

Как это работает: Ansible входит как ubuntu, потом запускает модуль как sudo <модуль>. Если sudo просит пароль, а Ansible его не знает, задача падает с Missing sudo password. В облачных образах Ubuntu у пользователя ubuntu настроено NOPASSWD (sudo без пароля), поэтому всё работает. Для интерактивной работы есть флаг --ask-become-pass (-K): Ansible спросит пароль sudo. Для автоматизации нужен NOPASSWD.

Аналогия: ты пришёл в офис с обычным пропуском, а серверная открывается по отдельному разрешению. become это «предъяви разрешение перед входом». Оговорка: пропуск выдаёт не Ansible, а настройки sudo на самом сервере.

Прикинь сам: apt падает без --become. Под каким пользователем Ansible вошёл и что поменяет флаг?

Он вошёл как ubuntu. Флаг запустит модуль через sudo от root, а вход по SSH остаётся под ubuntu.

Осторожно: become не меняет пользователя SSH-входа. Вход идёт под ansible_user, become только повышает права внутри уже открытой сессии. Поэтому «войти под root» и «войти под ubuntu и стать root» это разные вещи, и второе безопаснее: root по SSH обычно запрещён.

Главное: become повышает права внутри открытой сессии и не меняет пользователя входа; в автоматизации нужен NOPASSWD.

Проверь понимание: apt без --become падает. В чём причина?

Ответ

Ansible вошёл как ubuntu, у которого нет прав менять систему пакетов. Без --become модуль запускается от его имени и получает отказ в правах. С флагом модуль запустится через sudo от root.

Теперь о том, что Ansible узнаёт о сервере сам: о фактах.

Факты: что Ansible знает о сервере

Факты (facts) это информация о хосте, которую Ansible собирает сам: операционная система, версия, IP-адреса, объём памяти. Их собирает модуль setup. Имена фактов начинаются с ansible_: ansible_distribution («Ubuntu»), ansible_default_ipv4 (основной сетевой интерфейс), ansible_memtotal_mb (память в мегабайтах).

Зачем это нужно: плейбуки используют факты в условиях («если ОС Ubuntu, ставь через apt, если Rocky, через dnf»). Аналогия: анкета при въезде в гостиницу: прежде чем выдать номер, портье узнаёт, кто ты и сколько вас. Оговорка: факты снимаются в момент запуска и потом устаревают, поэтому в каждом запуске собираются заново (это занимает секунды, на больших парках его отключают или кешируют).

Чтобы не читать 500 строк вывода, пользуются фильтром: -a "filter=ansible_distribution*" оставит только факты, имена которых начинаются с ansible_distribution.

Прикинь сам: Зачем плейбуку знать, что сервер на Ubuntu, а не на Rocky?

Чтобы выбрать нужный менеджер пакетов: apt для Ubuntu, dnf для Rocky. Это решается условием по факту ansible_distribution.

Главное: факты собирает модуль setup при каждом запуске, имена начинаются с ansible_; лишнее отсекают фильтром filter=.

Чтобы не повторять флаги, умолчания собираются в ansible.cfg.

ansible.cfg: умолчания проекта

Каждый раз писать -u ubuntu --private-key ~/.ssh/id_ed25519 -i inventory.yml утомительно. Файл ansible.cfg хранит умолчания. Формат INI: разделы в [квадратных скобках], строки ключ = значение.

Ansible ищет конфиг по порядку и берёт первый найденный:

  1. переменная окружения ANSIBLE_CONFIG;
  2. ansible.cfg в текущем каталоге;
  3. ~/.ansible.cfg;
  4. /etc/ansible/ansible.cfg.

Следствие пункта 2: важно, из какого каталога ты запускаешь команды. Если запустить ansible не из каталога проекта, конфига и инвентаря не будет, а Ansible сообщит provided hosts list is empty, only localhost is available и ничего не сделает. Команда ansible --version в первой строке под версией показывает, какой config file используется: None значит, что конфиг не найден.

Прикинь сам: Ты запустил ansible из домашнего каталога, а не из проекта. Что вероятно будет?

Конфиг и инвентарь не найдены: provided hosts list is empty, only localhost is available. Ansible ищет ansible.cfg в текущем каталоге.

Осторожно: ansible.cfg не секрет, его можно хранить в git (в нём лишь путь к ключу), а сам приватный ключ нельзя никогда.

Главное: порядок поиска конфига: ANSIBLE_CONFIG, текущий каталог, ~/.ansible.cfg, /etc/ansible/ansible.cfg; в Git его можно, ключ нельзя.

Прежде чем менять сервер, полезно посмотреть, что изменилось бы: сухой прогон.

Сухой прогон: режимы check и diff

Режим --check показывает, что изменилось бы, но ничего не меняет. --diff дополнительно показывает разницу в файлах построчно: строки с + добавились, с - удалятся. Вместе --check --diff работают как terraform plan (урок 7.1): сначала посмотрел, потом применил.

Как это устроено: модули с поддержкой check-режима сами сравнивают состояние с желаемым и сообщают итог, но ничего не пишут. Ограничение: command и shell в этом режиме пропускаются, потому что Ansible не знает, что они сделают. Результат тогда неполный, а шаги, зависящие от пропущенных, считаются по устаревшим данным.

Прикинь сам: --check показал одни ok. Можно ли смело запускать без него?

Нет: command и shell в этом режиме пропускаются, и шаги, зависящие от них, считаются по устаревшим данным. Это оценка, а не гарантия.

Осторожно: зелёный --check не гарантия безопасного запуска. Это оценка, а не проверка.

Главное: --check --diff работает как terraform plan: показывает, что изменилось бы, и построчную разницу в файлах.

Проверь понимание: --check показал всё зелёным. Гарантирует ли это, что боевой запуск ничего не сломает?

Ответ

Нет. Задачи command/shell были пропущены, а шаги, зависящие от их результата, могли считаться по устаревшим данным. --check это оценка, а не гарантия.

Если Python на сервере нет, поможет raw.

Python на цели и модуль raw

Ansible ищет интерпретатор Python на цели сам. Настройка interpreter_python = auto_silent в ansible.cfg говорит: определи путь автоматически и не предупреждай. Если Python нет (голый минимальный образ) или путь указан неверно, задача падает с MODULE FAILURE или The module interpreter '...' was not found. Выход: поставить Python модулем raw (он его не требует): ansible notes -m ansible.builtin.raw -a "apt-get install -y python3" --become. Чтобы не повторять, Python кладут в образ или ставят при создании ВМ через cloud-init (урок 7.2). Явно указать путь можно переменной ansible_python_interpreter.

Прикинь сам: Голая ВМ без Python, а ping падает с MODULE FAILURE. Чем поставить Python?

Модулем raw: он гонит команду по SSH и не требует Python на цели. Например, apt-get install -y python3.

Главное: модули Ansible это Python-скрипты, поэтому Python на цели нужен; raw единственный, кому он не нужен.

Остался вопрос: зачем вообще Ansible, если есть bash.

Ansible или bash-скрипт: зачем нужен отдельный инструмент

Первый вопрос новичка: «я же могу написать скрипт, который зайдёт по SSH и выполнит apt install. Чем Ansible лучше?». Если не понять разницу, Ansible покажется сложной обёрткой вокруг того, что и так умеешь.

Скрипт это инструкция «иди на склад и принеси 10 коробок»: запустишь дважды, принесёшь 20. Ansible это заявка «на складе должно быть 10 коробок»: кладовщик посмотрит, сколько есть, и довезёт только недостающее. Оговорка: кладовщика (модуль) кто-то должен был написать, и для нестандартных задач модуля может не быть.

Посмотри на одну и ту же задачу «пользователь bob должен существовать» двумя способами:

  Bash-скрипт Ansible
Запись useradd bob модуль user, name=bob state=present
Первый запуск создал пользователя создал, статус CHANGED
Второй запуск ошибка user 'bob' already exists, скрипт упал или пошёл дальше с ошибкой проверил, bob есть, статус SUCCESS, ничего не менял
Другой дистрибутив команда может называться иначе модуль знает нужную команду сам
40 серверов цикл по SSH, обработку ошибок пишешь ты список хостов в инвентаре, параллельный запуск и отчёт по каждому
Что видно после вывод команд, разбирай сам для каждого хоста ok, changed, FAILED или UNREACHABLE

Прикинь сам: Скрипт echo "..." >> /etc/hosts запустили три раза. Сколько строк в файле?

Три одинаковые. lineinfile в Ansible оставит одну: первый запуск CHANGED, остальные ok.

Осторожно: «Ansible всегда лучше скрипта». Нет: разовая мелочь на одной машине проще скриптом. Ansible окупается, когда серверов больше одного, когда настройку нужно повторять и когда важно видеть, что изменилось. Ещё путают: Ansible не «умнее» bash, он просто требует описывать результат, а не шаги.

Главное: скрипт говорит «сделай», Ansible говорит «пусть будет так»; он окупается там, где серверов больше одного и нужен повторяемый результат.

Проверь понимание: ты написал скрипт, который добавляет строку в /etc/hosts. Что произойдёт, если запустить его три раза, и как это выглядит в Ansible?

Ответ

В скрипте с echo "..." >> /etc/hosts строка добавится три раза. В Ansible модуль lineinfile сначала ищет строку в файле: первый запуск даст CHANGED, второй и третий ok, в файле останется одна строка.

Когда серверов много, их собирают в группы, в том числе вложенные.

Вложенные группы: как хост попадает сразу в несколько групп

Серверы редко делятся только одним способом. Один и тот же сервер может быть «веб-сервером» и «боевым». Если группа только одна, то для команды «обнови все боевые серверы» пришлось бы перечислять хосты руками. Вложенные группы решают это: ты объявляешь группы отдельно и говоришь, кто в кого входит.

Сотрудник одновременно числится в отделе (разработка) и в офисе (Новосибирск). Приказ «всем сотрудникам Новосибирска» дойдёт до него, приказ «всей разработке» тоже. Оговорка: в Ansible, в отличие от отдела кадров, один хост может быть в любом числе групп.

Группа из блока children содержит другие группы, и все их хосты автоматически считаются её членами. Пример:

all:
  children:
    web:                  # группа веб-серверов
      hosts:
        web1:
        web2:
    db:                   # группа баз данных
      hosts:
        db1:
    prod:                 # боевое окружение: состоит из двух групп выше
      children:
        web:
        db:

Пример: Команда ansible-inventory --graph нарисует такое дерево:

@all:
  |--@ungrouped:
  |--@web:
  |  |--web1
  |  |--web2
  |--@db:
  |  |--db1
  |--@prod:
  |  |--@web:
  |  |  |--web1
  |  |  |--web2
  |  |--@db:
  |  |  |--db1

Читай так: @ помечает группу, строки без @ это хосты, отступ показывает вложенность. Теперь шаблон хостов даёт разные наборы: web это web1 и web2; prod это все три хоста; db это только db1. Одна команда ansible prod -m ping проверит все три боевых сервера, не перечисляя их.

flowchart TD
    ALL["all: все хосты"] --> WEB["web"]
    ALL --> DB["db"]
    ALL --> PROD["prod: состоит из групп"]
    WEB --> W1["web1"]
    WEB --> W2["web2"]
    DB --> D1["db1"]
    PROD -.->|"children"| WEB
    PROD -.->|"children"| DB

На схеме сплошные линии это членство хостов в группах, пунктирные это children: группа prod включает группы web и db целиком.

Прикинь сам: Хост web3 вписан в группы web и staging. Что затронет ansible web -m ping?

Все хосты группы web, включая web3, хотя он не боевой: группа не знает про окружения.

Осторожно: Слово children не значит «хосты внутри». Внутри children стоят группы, а хосты пишут в hosts. Если перепутать, Ansible посчитает web1 группой без хостов и не найдёт такого сервера.

Главное: в children стоят группы, а в hosts хосты; хост может быть в любом числе групп, а ansible-inventory --graph показывает итог.

Проверь понимание: в примере выше добавили группу staging с хостом web3, и web3 тоже вписали в web. В какие группы входит web3 и что сделает ansible web -m ping?

Ответ

web3 входит в staging и в web (плюс всегда в all). Команда для группы web затронет web1, web2 и web3, хотя web3 не боевой: группа web не знает про окружения. Поэтому окружения лучше делать отдельными группами и обращаться к нужному пересечению.

Дальше о том, как Ansible обходит много хостов.

Параллельность: почему Ansible не ходит по серверам по одному

Если на 40 серверов идти по очереди и каждый занимает 30 секунд, получится 20 минут. Ansible делает шаг сразу на нескольких хостах, это и есть параллельность.

В магазине с пятью кассами очередь обслуживают пятеро кассиров одновременно, а не один. Оговорка: число касс ограничено, лишние покупатели ждут.

Настройка forks (по умолчанию 5) задаёт, сколько хостов обрабатывается одновременно. Ansible берёт первые пять хостов, выполняет задачу на всех пяти, а следующую пятёрку берёт, когда предыдущая закончит шаг.

Пример: 200 серверов, forks = 5, один шаг занимает 6 секунд на хост:

  • батчей: 200 / 5 = 40;
  • время одного шага: 40 x 6 с = 240 с, то есть 4 минуты;
  • если плейбук состоит из 15 шагов: 15 x 4 мин = 60 минут. Это тот самый «прогон идёт час» из вопросов ниже.

Поднимем forks до 50: батчей 200 / 50 = 4, шаг занимает 4 x 6 = 24 с, плейбук 15 x 24 с = 6 минут. Выигрыш в 10 раз, потому что параллельность выросла в 10 раз.

flowchart LR
    H["200 хостов, шаг 6 с"] -->|"forks = 5"| A["40 батчей<br>240 с на шаг"]
    H -->|"forks = 50"| B["4 батча<br>24 с на шаг"]

Прикинь сам: У тебя 12 хостов и forks = 5. Сколько хостов в последнем батче?

Два: батчи 5, 5 и 2. Остальные места в последнем батче простаивают.

Осторожно: «Чем больше forks, тем лучше». Нет: каждая параллельная сессия занимает память и сетевые соединения на управляющем узле, а при слишком большом числе начинаются таймауты SSH. Поднимай постепенно и смотри на нагрузку.

Главное: forks задаёт число одновременных хостов (по умолчанию 5); растёт он постепенно, потому что каждая сессия занимает память на управляющем узле.

Проверь понимание: у тебя 12 хостов и forks = 5. Сколько батчей на один шаг и сколько хостов в последнем?

Ответ

Батчей три: 5, 5 и 2 (12 = 5 + 5 + 2). В последнем батче работает только два хоста, остальные «места» простаивают.

Когда ВМ создаются и удаляются каждый день, список хостов нужно брать у облака.

Динамический инвентарь: когда серверов слишком много для файла

В облаке ВМ создаются и удаляются каждый день. Поддерживать файл inventory.yml руками значит вечно отставать от реальности. Динамический инвентарь (dynamic inventory) это плагин, который перед запуском сам спрашивает у API облака «какие ВМ сейчас есть?» и строит список.

Статический инвентарь это бумажный список жильцов на подъезде, динамический это запрос в домоуправление в день приезда. Оговорка: для запроса нужны права и доступ к API облака.

Вместо файла со списком хостов ты кладёшь файл настроек плагина (например, для Yandex Cloud или AWS), а Ansible сам строит группы по меткам ВМ. В нашем курсе серверов мало, поэтому мы работаем со статическим инвентарём, а динамический нужно уметь назвать на собеседовании (строка для AWS есть в таблице соответствия ниже).

Прикинь сам: Динамический инвентарь прочитал облако и увидел 40 ВМ. Создаёт ли он их?

Нет: он только читает список. Создаёт ВМ Terraform, а Ansible настраивает.

Осторожно: Динамический инвентарь не заменяет Terraform: он только читает список существующих ВМ, а создаёт их Terraform.

Главное: динамический инвентарь это плагин, который строит группы по меткам ВМ из API облака; он не заменяет Terraform.

Соответствие AWS

Что Yandex Cloud / у нас AWS
Настройка ВМ по SSH без агента Ansible Ansible; Systems Manager Run Command (через агент SSM)
Первичная настройка при создании ВМ cloud-init (user_data) EC2 User Data
Список ВМ для инвентаря динамический инвентарь (плагин облака) плагин amazon.aws.aws_ec2
Вход по SSH ключ в metadata ВМ, пользователь ubuntu Key Pair, пользователь ubuntu или ec2-user

Практика

Нужна ВМ с Ubuntu 24.04 или 26.04 и входом по SSH-ключу. Вариант A: облачная ВМ из урока 7.2. Вариант B без облака: ВМ Multipass (та же программа, что в уроке 1.1).

# Вариант B: локальная ВМ, если нет облачной
multipass launch 24.04 --name notes-vm --cpus 1 --memory 1G --disk 8G
# кладём свой публичный ключ в ВМ, иначе Ansible не войдёт
multipass exec notes-vm -- bash -c "echo '$(cat ~/.ssh/id_ed25519.pub)' >> ~/.ssh/authorized_keys"

Разбор. multipass launch 24.04 --name notes-vm ... создаёт ВМ с Ubuntu 24.04 и заданными ресурсами. Вторая команда запускает bash -c "..." внутри ВМ. Кавычки внутри: $(cat ~/.ssh/id_ed25519.pub) раскрывается на твоём компьютере до отправки и подставляет текст публичного ключа, а >> дописывает его в authorized_keys на ВМ (не затирая). Если ключа ~/.ssh/id_ed25519 у тебя нет, создай его командой ssh-keygen -t ed25519 (из урока 2.2).

Задание 1. Установка ansible-core в venv

Цель: поставить ansible-core 2.21.4 в изолированное окружение, не трогая системный Python.

Что такое ansible-core и venv. ansible-core это сам Ansible без дополнительных коллекций модулей: движок и ansible.builtin. venv (virtual environment, виртуальное окружение) это отдельная папка с копией Python и своими пакетами: то, что ты в неё ставишь, не смешивается с системой. Пакеты Python ставит pip.

Предскажи: сработает ли sudo pip install ansible на Ubuntu? Где окажется команда ansible после установки в venv?

Ответ

Нет: Ubuntu защищает системный Python, pip ответит externally-managed-environment. В venv команда лежит в bin/ окружения и появляется в PATH только после активации (pipx сам кладёт ссылку в ~/.local/bin).

Шаги:

  1. Создай каталог проекта и venv:
mkdir -p ~/notes/infra/ansible && cd ~/notes/infra/ansible
sudo apt install -y python3-venv
python3 -m venv ~/.venvs/ansible

Разбор. mkdir -p создаёт каталог вместе с недостающими родителями. && запускает следующую команду только при успехе предыдущей. sudo apt install -y python3-venv ставит поддержку venv (флаг -y отвечает «да» на вопросы). python3 -m venv ~/.venvs/ansible создаёт окружение в папке ~/.venvs/ansible.

  1. Поставь закреплённую версию и проверь (вариант с pipx: pipx install ansible-core==2.21.4):
~/.venvs/ansible/bin/pip install "ansible-core==2.21.4"
source ~/.venvs/ansible/bin/activate
ansible --version | head -n 3

Разбор. Первая команда вызывает pip именно из venv и ставит версию строго 2.21.4 (== фиксирует версию). source .../activate добавляет bin/ окружения в PATH только для этого терминала: слово (ansible) может появиться в приглашении. | head -n 3 оставляет три первые строки вывода.

Что должно получиться:

ansible [core 2.21.4]
  config file = None
  configured module search path = ['/home/ubuntu/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']

ИИ: если pip или ansible --version выдали непонятную ошибку, вставь её текст нейросети и попроси объяснить построчно: но версию ansible-core и путь venv сверяй с тем, что ты поставил.

Как читать вывод: [core 2.21.4] версия Ansible. config file = None значит, что ansible.cfg не найден (мы его ещё не создали, это нормально). Третья строка это каталоги, где Ansible ищет модули; у тебя в пути будет твой домашний каталог.

Объясни себе: зачем закреплять версию ==2.21.4? Что даёт venv по сравнению с установкой в систему?

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

  • error: externally-managed-environment: pip запущен на системном Python. Используй venv или pipx.
  • ansible: command not found: venv не активирован в этом терминале. Выполни source ~/.venvs/ansible/bin/activate. В новом терминале это нужно повторять.

Задание 2. Инвентарь и ansible.cfg

Цель: описать ВМ в инвентаре и убедиться, что Ansible до неё доходит.

Предскажи: что покажет ansible-inventory --graph для одной группы notes с одним хостом?

Ответ

Дерево: @all содержит @ungrouped: (пусто) и @notes: с notes-vm. Группы all и ungrouped есть всегда.

Шаги:

  1. Узнай адрес. Облако: terraform -chdir=../terraform output -raw public_ip (-chdir запускает Terraform в другом каталоге, -raw печатает значение без кавычек). Multipass: multipass info notes-vm | grep IPv4.
  2. Создай ansible.cfg (в каталоге ~/notes/infra/ansible):
# ansible.cfg: умолчания проекта, чтобы не писать флаги каждый раз
[defaults]
# какой файл считать инвентарём
inventory = inventory.yml
# под каким пользователем входить по SSH
remote_user = ubuntu
# приватный ключ для входа
private_key_file = ~/.ssh/id_ed25519
# не спрашивать отпечаток при первом входе (только для учебной ВМ!)
host_key_checking = False
# сам найти Python на цели и не предупреждать
interpreter_python = auto_silent
  1. Создай inventory.yml (подставь свой адрес вместо 203.0.113.10):
# inventory.yml: адрес берём из terraform output public_ip
all:
  children:
    notes:
      hosts:
        notes-vm:
          ansible_host: 203.0.113.10
  1. Проверь:
ansible-inventory --graph
ansible notes -m ansible.builtin.ping

Что должно получиться:

@all:
  |--@ungrouped:
  |--@notes:
  |  |--notes-vm
notes-vm | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3.12"
    },
    "changed": false,
    "ping": "pong"
}

Как читать вывод: первые четыре строки это дерево групп: @ помечает группу. notes-vm | SUCCESS значит «хост ответил, модуль отработал». discovered_interpreter_python показывает, какой Python Ansible нашёл на цели (на Ubuntu 24.04 это python3.12, на 26.04 будет другая версия). "ping": "pong" ответ модуля: SSH-вход и Python работают.

Объясни себе: откуда Ansible узнал пользователя и ключ, если в команде их нет? Почему host_key_checking = False допустим для учебной ВМ и опасен на проде?

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

  • Failed to connect to the host via ssh: ubuntu@203.0.113.10: Permission denied (publickey).: ключ не тот или не лежит в authorized_keys на ВМ, либо не тот пользователь. Проверь вручную ssh -i ~/.ssh/id_ed25519 ubuntu@<адрес>.
  • [WARNING]: provided hosts list is empty, only localhost is available: команда запущена не из каталога с ansible.cfg. Перейди в ~/notes/infra/ansible.

Задание 3. Ad-hoc: факты, пакеты, идемпотентность

Цель: увидеть на практике разницу между changed и ok.

Предскажи: ты дважды запускаешь apt с name=htop state=present. Что будет в changed в первый раз и во второй?

Ответ

Первый: CHANGED, "changed": true (пакет установлен). Второй: SUCCESS, "changed": false (уже стоит).

Шаги:

# факты: только про дистрибутив (фильтр, чтобы не читать 500 строк)
ansible notes -m ansible.builtin.setup -a "filter=ansible_distribution*"

# первый запуск: установка
ansible notes -m ansible.builtin.apt -a "name=htop state=present update_cache=true" --become

# второй запуск: та же команда
ansible notes -m ansible.builtin.apt -a "name=htop state=present" --become

# для сравнения: command всегда пишет changed
ansible notes -m ansible.builtin.command -a "uptime"

Разбор. update_cache=true перед установкой обновляет список пакетов (аналог apt update). --become даёт права root. В command -a "uptime" аргумент это сама команда, которая показывает, сколько ВМ работает.

Что должно получиться: (вывод сокращён, у apt он намного длиннее)

notes-vm | SUCCESS => {
    "ansible_facts": {
        "ansible_distribution": "Ubuntu",
        "ansible_distribution_file_parsed": true,
        "ansible_distribution_file_path": "/etc/os-release",
        "ansible_distribution_file_variety": "Debian",
        "ansible_distribution_major_version": "24",
        "ansible_distribution_release": "noble",
        "ansible_distribution_version": "24.04",
        "discovered_interpreter_python": "/usr/bin/python3.12"
    },
    "changed": false
}
notes-vm | CHANGED => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3.12"
    },
    "changed": true,
    ...
    "stdout": "Reading package lists...\n...Setting up htop (3.3.0-4build1) ...\n..."
}
notes-vm | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3.12"
    },
    "cache_updated": false,
    "changed": false
}
notes-vm | CHANGED | rc=0 >>
 12:25:02 up 56 min,  0 user,  load average: 0.60, 1.35, 1.35

Время, аптайм и номера версий у тебя будут другие.

ИИ: вывод ansible с UNREACHABLE или FAILED можно целиком отдать нейросети без ключей: она подскажет причины, а проверять их начинай со своего ssh.

Как читать вывод: у первого блока changed: false, потому что setup только читает. У установки changed: true, а в stdout лежит то, что напечатал apt. Второй apt выдаёт SUCCESS и changed: false: htop уже стоит, делать нечего. Формат CHANGED | rc=0 >> короткий: так печатаются command и shell; rc=0 это код возврата команды (0 значит успех, урок 1.6), а ниже сам вывод. uptime только читает, но command всё равно пишет CHANGED: Ansible не знает, что делает произвольная команда.

Объясни себе: почему второй apt дал changed: false? Как сделать command честным (подсказка: параметры creates и removes)? Почему setup всегда changed: false?

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

  • Failed to lock apt for exclusive operation или Could not get lock: на ВМ работает unattended-upgrades (автообновления). Подожди пару минут и повтори.
  • Missing sudo password: у пользователя нет NOPASSWD, см. «Сломай и почини», сценарий 2.

Задание 4. Файлы, --check --diff и ansible-doc

Цель: изменить файл на ВМ и увидеть разницу до применения.

Предскажи: появится ли файл на сервере после запуска с --check --diff? Покажет ли Ansible изменения?

Ответ

Изменения покажет: CHANGED и diff со строками добавления. Файла на сервере не появится: --check ничего не пишет.

Шаги:

# сухой прогон: что изменилось бы
ansible notes -m ansible.builtin.copy \
  -a "content='notes managed by ansible' dest=/etc/motd-notes mode=0644" \
  --become --check --diff

# убедимся, что файла нет
ansible notes -m ansible.builtin.command -a "ls /etc/motd-notes" || true

# применим по-настоящему и повторим
ansible notes -m ansible.builtin.copy \
  -a "content='notes managed by ansible' dest=/etc/motd-notes mode=0644" --become
ansible notes -m ansible.builtin.copy \
  -a "content='notes managed by ansible' dest=/etc/motd-notes mode=0644" --become

# справка по модулю прямо в терминале
ansible-doc ansible.builtin.copy | head -n 20

Разбор. content=... кладёт этот текст в файл, dest= путь файла, mode=0644 права (урок 1.3): владелец пишет, остальные читают. Обратный слэш \ в конце строки переносит команду на следующую строку. || true заставляет оболочку считать команду успешной даже при ошибке (мы ждём, что ls не найдёт файл). ansible-doc показывает справку по модулю без интернета.

Что должно получиться: (первая команда)

--- before
+++ after: /etc/motd-notes
@@ -0,0 +1 @@
+notes managed by ansible
\ No newline at end of file

notes-vm | CHANGED => {
    "changed": true
}

Вторая команда покажет, что файла нет:

notes-vm | FAILED | rc=2 >>
ls: cannot access '/etc/motd-notes': No such file or directory

(перед этим Ansible печатает ещё строку [ERROR]: Task failed: Module failed: ..., её текст зависит от версии).

После настоящего применения: первый запуск CHANGED с "changed": true, "dest": "/etc/motd-notes", "mode": "0644", "owner": "root", второй SUCCESS с "changed": false.

Как читать вывод: блок --- before / +++ after это diff: строка с + появилась бы в файле, @@ -0,0 +1 @@ значит «было 0 строк, стала 1». Слова No newline at end of file только сообщают, что в конце нет перевода строки, это нормально. CHANGED при --check значит «изменил бы», а не «изменил».

Объясни себе: как Ansible понял, что файл уже нужного содержания (подсказка: в ответе есть checksum)? Когда --check не поможет? Где читать параметры модуля без интернета?

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

  • ERROR! couldn't resolve module/action 'ansible.builtin.copyy': опечатка в имени модуля. Найди правильное: ansible-doc -l | grep copy.
  • Destination directory /opt/notes does not exist: copy не создаёт каталоги. Сначала модуль file с state=directory.

Задание 5. Шаг проекта: инвентарь «Заметок» из terraform output

Цель: зафиксировать конфиг Ansible в репозитории и брать адрес ВМ автоматически.

Предскажи: ВМ пересоздали, IP сменился. Что произойдёт с инвентарём, где адрес прописан руками?

Ответ

Ansible пойдёт на старый адрес: получишь UNREACHABLE по таймауту или, хуже, зайдёшь на чужую машину, получившую этот IP. Поэтому адрес берут из terraform output при каждом запуске (подробнее в уроке 7.7).

Шаги:

  1. Замени inventory.yml: адрес читается из переменной окружения NOTES_VM_IP, общие параметры вынесены в vars группы. Двойные фигурные скобки в файле это Jinja2, шаблонизатор Ansible (подробно в уроке 7.5): внутри них Ansible вычисляет выражение. Здесь lookup('env', 'NOTES_VM_IP') значит «прочитай переменную окружения с таким именем».
# infra/ansible/inventory.yml
# Адрес ВМ: export NOTES_VM_IP=$(terraform -chdir=../terraform output -raw public_ip)
all:
  children:
    notes:
      hosts:
        notes-vm:
          ansible_host: "{{ lookup('env', 'NOTES_VM_IP') }}"
      vars:
        ansible_user: ubuntu

Разбор. Блок vars в группе notes задаёт переменные всем её хостам, поэтому ansible_user не дублируется. Кавычки вокруг {{ ... }} обязательны: без них YAML принял бы { за начало словаря.

  1. Запусти и закоммить:
cd ~/notes/infra/ansible
export NOTES_VM_IP=$(terraform -chdir=../terraform output -raw public_ip)   # или адрес Multipass
ansible notes -m ansible.builtin.ping
git add ansible.cfg inventory.yml
git commit -m "ansible: inventory и ansible.cfg"

export делает переменную видимой для запускаемых программ (в том числе ansible). Для Multipass вместо terraform ... подставь адрес: export NOTES_VM_IP=<IPv4 из multipass info>.

Что должно получиться:

notes-vm | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3.12"
    },
    "changed": false,
    "ping": "pong"
}
[main 4c1f9a2] ansible: inventory и ansible.cfg
 2 files changed, 22 insertions(+)

Хэш коммита (4c1f9a2) и число строк у тебя будут другими.

Свой результат сверяй с каталогом ~/notes/infra/ansible, который ты создал в задании 1: в нём должны лежать ansible.cfg и inventory.yml, а приватного ключа быть не должно.

Объясни себе: зачем адрес вынесен из файла в переменную окружения? Почему приватного ключа в репозитории быть не должно, а ansible.cfg с путём к нему можно?

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

  • Failed to connect to the host via ssh: hostname contains invalid characters (в старых версиях Could not resolve hostname : Name or service not known): NOTES_VM_IP пуста. Проверь echo "$NOTES_VM_IP" и повтори export. Переменные живут только в терминале, где ты их задал.
  • ERROR! Unable to parse .../inventory.yml as an inventory source: сбит отступ в YAML. Проверь ansible-inventory --graph.

Сломай и почини

Здесь скрипта поломки нет: ты ломаешь стенд руками тремя способами, каждый раз возвращая рабочее состояние. Делай по одному сценарию за раз. Ты в каталоге ~/notes/infra/ansible, NOTES_VM_IP задана.

Симптом

Три картины при ansible notes -m ansible.builtin.ping или при apt ... --become:

  • UNREACHABLE! ... Permission denied (publickey);
  • FAILED! ... Missing sudo password (в старых версиях sudo: a password is required);
  • The module interpreter '/usr/bin/python9' was not found (или MODULE FAILURE ... /usr/bin/python3: not found).

Как воспроизвести каждую:

# 1. другой пользователь: ключа этого пользователя на сервере нет
ansible notes -m ansible.builtin.ping -e ansible_user=nosuchuser

# 2. пользователь есть, ключ принят, но sudo без NOPASSWD (создаём на ВМ отдельного)
ansible notes -m ansible.builtin.shell --become -a "useradd -m -s /bin/bash -G sudo t74user && install -d -m 700 -o t74user -g t74user /home/t74user/.ssh && install -m 600 -o t74user -g t74user /home/ubuntu/.ssh/authorized_keys /home/t74user/.ssh/authorized_keys"
ansible notes -m ansible.builtin.apt -a update_cache=true --become -e ansible_user=t74user

# 3. неверный путь к Python
ansible notes -m ansible.builtin.ping -e ansible_python_interpreter=/usr/bin/python9

Ключ -e (extra vars) подменяет переменную для одного запуска, поэтому файлы конфигурации не портятся.

Гипотезы

  • Ansible не может войти: другой пользователь, другой ключ, ключа нет в authorized_keys.
  • Вход есть, но sudo требует пароль, а Ansible его не передал.
  • Вход и sudo есть, но на цели нет Python или интерпретатор указан неверно.

Проверки

# 1. подробный вывод: какой ключ и пользователь реально используются
ansible notes -m ansible.builtin.ping -vvv 2>&1 | grep -i 'ssh\|identity\|user' | head
# 2. то же вручную, без Ansible
ssh -i ~/.ssh/id_ed25519 ubuntu@"$NOTES_VM_IP" 'sudo -n true && echo sudo-ok'
# 3. есть ли Python на цели (raw не требует Python)
ansible notes -m ansible.builtin.raw -a "command -v python3 || echo no-python"

Разбор. -vvv включает подробный вывод: видно команду ssh с ключом и пользователем. 2>&1 направляет сообщения об ошибках в тот же поток, чтобы grep их увидел. sudo -n true запускает sudo без запроса пароля (-n, non-interactive): если пароль нужен, команда сразу падает, а не ждёт ввода.

Исправление

Разбор трёх сценариев

1. Permission denied (publickey). Текст: UNREACHABLE! => {"msg": "Task failed: Failed to connect to the host via ssh: nosuchuser@203.0.113.10: Permission denied (publickey,password)."} (в скобках перечислены методы, которые пробовал SSH). Ручной ssh падает так же, значит проблема в SSH, а не в Ansible. Причины: неверный remote_user, неверный private_key_file, публичного ключа нет в ~/.ssh/authorized_keys, права ~/.ssh шире 700. Исправление: положить нужный ключ и указать верного пользователя в ansible.cfg или ansible_user. В нашем случае достаточно убрать -e ansible_user=nosuchuser.

2. Missing sudo password. Текст: FAILED! => {"changed": false, "msg": "Task failed: Missing sudo password"} (в старых версиях sudo: a password is required). Вход работает, а sudo -n true просит пароль. Причина: нет NOPASSWD. Исправление: на цели создать /etc/sudoers.d/<пользователь> со строкой <пользователь> ALL=(ALL) NOPASSWD:ALL через visudo -f (урок 1.3) или запускать с --ask-become-pass (-K). Для автоматизации нужен NOPASSWD. Убери тестового пользователя: ansible notes -m ansible.builtin.user -a "name=t74user state=absent remove=true" --become.

3. Python не найден. Текст: The module interpreter '/usr/bin/python9' was not found. Consider overriding the configured interpreter path for this host. Если на цели Python нет совсем, текст будет "module_stdout": "/bin/sh: 1: /usr/bin/python3: not found". raw покажет путь к Python или no-python. Исправление: поставить Python модулем raw: ansible notes -m ansible.builtin.raw -a "apt-get install -y python3" --become, либо указать верный путь в ansible_python_interpreter. В нашем случае достаточно убрать -e ansible_python_interpreter=....

ИИ в помощь

Нейросеть быстро объясняет вывод Ansible и пишет ad-hoc команды, но не видит твой сервер и может предложить shell там, где есть модуль. Общие правила работы с ней: ИИ-помощник.

Задача: разобрать ошибку подключения.

Ansible выдал UNREACHABLE для хоста notes-vm. Вот текст ошибки: <вставь вывод без ключей>. Вот мой inventory.yml без секретов: <вставь>. Перечисли причины от самых вероятных и команды, которыми проверить каждую.

Проверь ответ: сначала должна идти ручная проверка ssh ubuntu@<адрес>, а не правка Ansible. Типичная ошибка: нейросеть советует отключить проверку отпечатков хоста для всех серверов, включая рабочие.

Задача: подобрать модуль вместо shell.

Мне нужно на Ubuntu 24.04 создать пользователя deploy, положить его публичный ключ и дать sudo без пароля. Предложи ad-hoc команды ansible с готовыми модулями (не command и не shell) и скажи, какой статус я должен увидеть при втором запуске.

Проверь ответ: в ответе должны быть модули user, authorized_key и copy или lineinfile с проверкой visudo, а при повторе SUCCESS. Типичная ошибка: command: useradd, которая при повторе падает.

Задача: прочитать вывод --check --diff.

Вот вывод ansible с --check --diff: <вставь>. Что изменилось бы на сервере и какие задачи могли быть пропущены в check-режиме?

Проверь ответ: она должна упомянуть, что command и shell пропускаются. Типичная ошибка: уверенное «всё безопасно» по зелёному --check.

Словарик урока

Термин Простыми словами
Ansible Программа, которая по SSH приводит серверы к описанному состоянию
Управляющий узел (control node) Компьютер, с которого ты запускаешь Ansible
Управляемый узел, хост (managed node, host) Сервер, который настраивает Ansible
Агент (agent) Постоянно работающая программа на сервере; у Ansible её нет (agentless)
Push / pull Управление «толкает» изменения с твоей стороны или сервер сам «тянет» их у центра
Инвентарь (inventory) Файл со списком серверов, групп и параметров подключения
Группа (group) Набор хостов под одним именем: notes, web
Шаблон хостов (pattern) Первый аргумент ansible: имя группы, хоста, all
Модуль (module) Готовая программа под одну задачу: пакет, файл, пользователь, сервис
FQCN Полное имя модуля вида ansible.builtin.apt
Ad-hoc Разовая команда Ansible без плейбука
Идемпотентность (idempotency) Повторный запуск не меняет результат: желаемое состояние достигнуто один раз
changed / ok Изменил ли модуль что-то в этот запуск
become Выполнение модуля с повышенными правами (обычно через sudo)
Факты (facts) Данные о хосте, которые Ansible собирает сам: ОС, IP, память
ansible.cfg Файл умолчаний Ansible для проекта
--check / --diff Сухой прогон без изменений и построчная разница файлов
YAML Текстовый формат данных, где структуру задают отступы
Вложенная группа (children) Группа, состоящая из других групп; хост может входить в несколько групп
forks Сколько хостов Ansible обрабатывает одновременно (по умолчанию 5)
Динамический инвентарь Плагин, который строит список хостов из API облака, а не из файла
venv Отдельная папка с Python и своими пакетами
Дрейф (drift) Расхождение между описанием в коде и реальным состоянием сервера
raw Модуль, который гонит команду по SSH и не требует Python на цели

Вопросы с собеседований

Раздел для повторения: ответь вслух, потом открой ответ. Короткие вопросы с пометкой [на скорость] тренируй на время: ответ за 30 секунд.

1. [junior] [часто] Чем Ansible отличается от Terraform и когда что берёшь?

Ответ

Terraform создаёт и удаляет ресурсы облака (ВМ, сети, диски) через API и хранит state. Ansible настраивает содержимое машин по SSH: пакеты, файлы, сервисы. Порядок: Terraform создаёт ВМ, Ansible её настраивает. Для одноразовой первичной настройки иногда хватает cloud-init (скрипт, который ВМ выполняет при первом запуске).

Что хотят услышать: создание против настройки, state против его отсутствия, API против SSH, стык через terraform output в инвентарь.

Красный флаг: «это одно и то же, просто другой синтаксис».

2. [junior] [часто] Что такое идемпотентность и как её видно в выводе Ansible?

Ответ

Повторный запуск даёт то же состояние и ничего не меняет. Первый запуск пишет changed, второй ok. Так работают apt, file, copy: они сначала проверяют состояние. command и shell не знают состояния и всегда пишут changed, если не заданы creates/removes.

Что хотят услышать: пример changed -> ok, отличие модулей от command, параметры creates/removes.

Красный флаг: «скрипт можно запускать много раз» без объяснения, как этого добиться.

3. [junior] Что такое инвентарь и что кладут в group_vars?

Ответ

Инвентарь это список хостов и групп. В group_vars/<группа>.yml лежат переменные всей группы: порт, пользователь, версия пакета. host_vars сильнее групповых: узкое побеждает общее.

Что хотят услышать: группы и вложенность, приоритет host над group над all, статический и динамический инвентарь (динамический строится по API облака).

Красный флаг: пароли открытым текстом в group_vars.

4. [middle] Ansible пишет UNREACHABLE! Permission denied (publickey). Что проверишь?

Ответ

Повторю то же руками: ssh -i ключ user@host. Если и так не входит, дело в SSH: не тот пользователь или ключ, ключа нет в authorized_keys, права на ~/.ssh. Затем ansible -vvv: какой ключ и пользователь реально использованы.

Что хотят услышать: отделить SSH от Ansible, -vvv, remote_user, private_key_file, права 700/600.

Красный флаг: «отключу проверку ключей» или сразу вход по паролю.

5. [middle] Задача падает на Missing sudo password. Что делаешь?

Ответ

Вход работает, а become не получает root: нет NOPASSWD. Для интерактива запускаю с -K. Для автоматизации добавляю в /etc/sudoers.d/ NOPASSWD только для нужного пользователя и проверяю visudo -c. Пароль в репозитории не храню.

Что хотят услышать: become, --ask-become-pass, sudoers.d, наименьшие привилегии.

Красный флаг: вход по SSH сразу под root или пароль в плейбуке.

6. [middle] command, shell и raw: в чём разница?

Ответ

command запускает программу без оболочки (нет пайпов и переменных). shell идёт через /bin/sh, пайпы работают, но выше риск инъекций. raw просто гонит команду по SSH и не требует Python на цели: им ставят Python. Ни один не идемпотентен без creates/removes.

Что хотят услышать: когда нужен raw, риск shell, предпочтение готовым модулям.

Красный флаг: везде shell, «потому что привычнее».

7. [middle] Прогон на 200 серверов идёт час. Как ускорить?

Ответ

Поднять forks (сколько хостов обрабатывается параллельно, по умолчанию 5), включить pipelining (меньше SSH-операций на задачу), отключить сбор фактов там, где они не нужны (gather_facts: false) или кешировать их, включить ControlPersist (переиспользование SSH-соединения). Раскатывать по частям через --limit и serial.

Что хотят услышать: forks, pipelining, gather_facts, strategy: free, serial для безопасной раскатки.

Красный флаг: «взять сервер помощнее» без поиска узкого места.

8. [middle] Чем полезен --check и где он вводит в заблуждение?

Ответ

Показывает, что изменилось бы, ничего не трогая, а --diff показывает правки файлов. Врёт там, где есть command/shell (пропускаются) и где следующий шаг зависит от результата предыдущего. Это оценка, а не гарантия.

Что хотят услышать: аналогия с terraform plan, ограничения, check_mode у отдельных задач.

Красный флаг: «check зелёный, катим на прод без оглядки».

9. [junior] На свежей ВМ Ansible ругается, что нет /usr/bin/python3. Что делаешь?

Ответ

Модули требуют Python на цели. Ставлю его через raw (apt-get install -y python3): raw Python не нужен. Затем проверяю ansible_python_interpreter. Чтобы не повторять, ставлю Python в образ или через cloud-init.

Что хотят услышать: raw, причина (минимальный образ), cloud-init.

Красный флаг: «надо ставить агент Ansible на сервер».

10. [middle] Плюсы и минусы agentless-подхода по сравнению с агентом на сервере?

Ответ

Плюсы: на серверах ничего не установлено и не обновляется, порядок под контролем. Минусы: сервер сам не исправит дрейф, нужен SSH-доступ ко всем машинам, на тысячах узлов медленно. Агентная модель (Puppet, ansible-pull) сама забирает конфигурацию, но агент нужно ставить и обновлять.

Что хотят услышать: бастион (промежуточный сервер для входа) и доступ по SSH, масштаб, ansible-pull, ночной прогон в CI против дрейфа.

Красный флаг: «agentless всегда лучше» без минусов.

11. [junior] Что такое facts в Ansible и зачем gather_facts?

Ответ

Facts - это данные о хосте, которые Ansible собирает в начале play модулем setup: ОС и её версия, IP-адреса, память, диски. Их можно использовать в условиях и шаблонах, например ansible_facts['os_family'] для выбора пакетного менеджера. Сбор занимает время на каждом хосте, поэтому если факты не нужны, я ставлю в play gather_facts: false и ускоряю прогон. Посмотреть, что собралось, можно так: ansible web -m setup.

Что хотят услышать: факты собирает модуль setup, используются в условиях и шаблонах, сбор можно отключить для ускорения.

Красный флаг: думать, что факты надо заводить руками в инвентаре.

12. [middle] Что такое динамический инвентарь и когда он нужен?

Ответ

Статичный инвентарь - файл со списком хостов, который ведёшь руками. Динамический строится в момент запуска из внешнего источника: API облака, файл состояния Terraform, CMDB. Он нужен, когда серверы создаются и удаляются автоматически и список устаревает за часы. Реализуется плагином инвентаря или скриптом, который отдаёт JSON. Что получилось, я проверяю командой ansible-inventory --list, прежде чем запускать плейбук.

Что хотят услышать: инвентарь строится из внешнего источника при запуске, нужен для изменчивой инфраструктуры, плагин или скрипт, проверка через ansible-inventory.

Красный флаг: каждое утро вручную править файл hosts «по списку из консоли облака».

13. [junior] Ansible на новой ВМ падает с Host key verification failed. Что делаешь?

Ответ

SSH видит хост впервые, его ключа нет в known_hosts, а Ansible не может задать вопрос «доверять?» в неинтерактивном режиме. Правильно: проверить отпечаток ключа по доверенному каналу и добавить хост, например ssh-keyscan -H адрес >> ~/.ssh/known_hosts, либо один раз подключиться руками. Можно отключить проверку через host_key_checking = False или переменную ANSIBLE_HOST_KEY_CHECKING=False, и в учебном стенде это допустимо. На проде так делать не стоит: пропадает защита от подмены сервера (MITM).

Что хотят услышать: причина в отсутствующем ключе в known_hosts, ssh-keyscan с проверкой отпечатка, отключение проверки только в лаборатории и почему.

Красный флаг: отключить проверку ключей везде «чтобы не мешало».

Проверено на версиях

  • ansible-core 2.21.4 в venv на Ubuntu 24.04 (Python 3.12): прогнаны ansible --version, ansible-inventory --graph, ping, setup, apt (дважды), command, copy с --check --diff и без, ansible-doc, lookup('env', ...) в инвентаре, сценарии «Сломай и почини» (Permission denied, Missing sudo password, неверный интерпретатор) и raw. Вход шёл по SSH на 127.0.0.1 в контейнере с sshd, а не в облачную ВМ: формат вывода тот же.
  • Ubuntu 26.04 как цель не прогонялась (версия Python в discovered_interpreter_python там другая).
  • Terraform-часть (terraform output) и Multipass не запускались, команды взяты из уроков 7.2 и 1.1.
  • Скрипта поломки для этого урока нет: сценарии выполняются руками.

Итог урока: ты умеешь

  • умею поставить ansible-core в venv или pipx с закреплённой версией
  • умею описать серверы в inventory.yml с группами и ansible_host
  • умею настроить ansible.cfg и проверить связь через ansible notes -m ansible.builtin.ping
  • умею запускать ad-hoc с --become и отличать changed от ok
  • умею смотреть изменения через --check --diff и знаю их ограничения
  • умею искать модули и параметры через ansible-doc
  • умею диагностировать UNREACHABLE, ошибку sudo и отсутствие Python
  • умею брать адрес ВМ из terraform output для инвентаря

Дальше: Урок 7.5: Ansible: плейбуки, handlers и Jinja2

Проверь себя

Короткий тест по уроку: 5 вопросов из банка в 30. Засчитывается только полностью правильный ответ, порог 60%. Каждая новая попытка даёт другие вопросы, пока банк не закончится. Ответы видны после проверки.

Тест работает с включённым JavaScript.

тема 7 урок 7.4 3 ч курс 0/0 ← → уроки