Почему «я помню, как настраивал тот сервер» не масштабируется на второй сервер
Первый сервер администратор настраивает по шагам: поставить пакеты, создать пользователей, поправить конфигурацию nginx, открыть порты. Через полгода нужен второй такой же — и выясняется, что половина шагов нигде не записана. Через год серверов десять, и все немного разные: на одном обновили OpenSSL, на другом вручную поправили лимит соединений во время аварии и забыли вернуть. Это называют дрейфом конфигурации.
| Проблема | Следствие |
|---|---|
| Шаги настройки в голове или в устаревшей вики | Новый сервер настраивается днями и всё равно не так, как старые |
| Серверы постепенно расходятся | Ошибка воспроизводится только на одном из десяти; обновление ломает «особенный» сервер |
| Нет истории изменений | Непонятно, кто и зачем поменял настройку; откатить нечего |
| Одно и то же руками на многих машинах | Часы рутины и опечатки |
Управление конфигурацией (configuration management) — описать нужное состояние серверов в файлах, хранить их в Git и применять программой. Описание проходит ревью, как код; новый сервер получает ту же конфигурацию за минуты; повторный запуск возвращает «уплывший» сервер к описанному состоянию.
Скрипт на bash описывает действия: «добавь строку в файл», «создай пользователя». Запусти его дважды — строка добавится дважды, а создание пользователя упадёт с ошибкой. Ansible описывает состояние: «пакет установлен», «в файле есть такая строка», «служба запущена». Модуль сначала проверяет, каково текущее состояние, и меняет что-то, только если оно отличается от нужного.
Это свойство называется идемпотентностью: сколько раз ни применяй описание, результат один и тот же, а второй запуск ничего не меняет. На нём держится всё остальное: Ansible можно безопасно запускать по расписанию, на уже настроенных серверах и после аварии, не помня, какие шаги уже сделаны.
| Задача | Скрипт: действие | Ansible: состояние |
|---|---|---|
| Пакет nginx | apt install nginx | apt: name=nginx state=present |
| Пользователь deploy | useradd deploy — второй раз ошибка | user: name=deploy state=present |
| Строка в конфигурации | echo … >> файл — второй раз дубль | lineinfile: line=… state=present |
| Служба | systemctl restart nginx — каждый раз | systemd: name=nginx state=started — только если не запущена |
управляющий узел (ваш компьютер или сервер администрирования)
ansible / ansible-playbook + инвентарь + плейбуки
│ SSH (ключ), по узлам параллельно
├──────────────▶ web1 ┐ на узле нужны только sshd и python3:
├──────────────▶ web2 │ Ansible копирует модуль во временный файл,
└──────────────▶ db1 ┘ выполняет его и удаляет — агента нет
apt, copy, user, systemd, всего их тысячи. Модуль — программа на Python, которую Ansible передаёт на узел и запускает.Такая схема называется push без агента: управляющий узел сам подключается к серверам. У Puppet и Chef по-другому — на каждом сервере работает агент, который забирает конфигурацию с центрального сервера (pull). Push проще начать: не нужно ставить и обновлять агентов, достаточно SSH. Terraform решает соседнюю задачу — создаёт сами серверы, сети и балансировщики в облаке; Ansible настраивает то, что внутри серверов. Их часто используют вместе.
Ansible ставится только на управляющий узел. Проще всего — из пакетов Debian:
$ sudo apt update
$ sudo apt install ansible
$ ansible --version | grep -E "^ansible|python version|jinja version"
ansible [core 2.19.11]
python version = 3.13.5 (main, Aug 10 2026, 12:06:59) [GCC 14.2.0] (/usr/bin/python3)
jinja version = 3.1.6
$ dpkg -l ansible ansible-core | tail -2 | awk "{print \$2, \$3}"
ansible 12.0.0+dfsg-0+deb13u1
ansible-core 2.19.11-0+deb13u1
Пакетов два. ansible-core — сама программа: команды, движок плейбуков и базовые модули из коллекции ansible.builtin. Пакет ansible добавляет к нему десятки коллекций — наборов модулей для конкретных систем: PostgreSQL, файрволов, облаков, сетевого оборудования. Номера версий у пакетов разные, это нормально: Ansible 12 построен на ansible-core 2.19.
$ ansible-galaxy collection list -p /usr/lib/python3/dist-packages 2>/dev/null | grep -E "^(community\.general|ansible\.posix|community\.postgresql|community\.crypto) "
ansible.posix 2.1.0
community.crypto 3.0.3
community.general 11.2.1
community.postgresql 4.1.0
$ ansible-galaxy collection list -p /usr/lib/python3/dist-packages 2>/dev/null | grep -c -E "^[a-z_]+\.[a-z_]+ "
89
Коллекции из пакета не нужно скачивать из интернета: Ansible Galaxy, каталог коллекций, из России открывается не всегда. Свежие версии Ansible выходят и в PyPI (pipx install ansible), но для курса и большинства задач версии из Debian достаточно, а обновления безопасности приходят через apt upgrade.
В курсе три учебных «сервера» с Debian 13: web1 (172.30.50.11), web2 (.12) и db1 (.21). У вас это могут быть три виртуальные машины в одной сети с любыми адресами.
Ansible подключается к узлам отдельным пользователем, а не вашим личным и не root. На каждом узле один раз, руками или скриптом при создании машины:
$ sudo useradd -m -s /bin/bash ansible
$ echo 'ansible ALL=(ALL) NOPASSWD: ALL' | sudo tee /etc/sudoers.d/ansible
$ sudo chmod 440 /etc/sudoers.d/ansible
$ sudo install -d -m 700 -o ansible -g ansible /home/ansible/.ssh
$ sudo install -m 600 -o ansible -g ansible ansible_ed25519.pub /home/ansible/.ssh/authorized_keys
На управляющем узле — отдельный ключ для автоматизации (ssh-keygen -t ed25519 -f ~/.ssh/ansible_ed25519 -C ansible@control), его открытая часть и копируется на узлы:
$ ls ~/.ssh
ansible_ed25519
ansible_ed25519.pub
known_hosts
$ ssh-keygen -lf ~/.ssh/ansible_ed25519.pub
256 SHA256:4wyxH0M3XheIwFKvsrqTc70AYF7msDoeUrtkgC3STCE ansible@control (ED25519)
sshd_config узлов PasswordAuthentication no.sudo без пароля: Ansible выполняет задачи от root через sudo, и спрашивать пароль на каждом из сотни узлов неудобно. Цена — ключ этого пользователя даёт root на всех узлах. Его хранят только на управляющем узле, ни с кем не делятся и меняют, когда уходит человек, у которого был доступ. Вариант строже — sudo с паролем и вводом его при запуске (--ask-become-pass).ansible в логах узла легко отличить от действий людей.При первом подключении SSH спрашивает, доверять ли ключу сервера. Ansible подключается ко многим узлам сразу и отвечать на вопросы не может. Отпечатки заранее собирают в ~/.ssh/known_hosts и сверяют с теми, что показывает сам сервер:
$ ssh-keyscan -t ed25519 172.30.50.11 172.30.50.12 172.30.50.21 2>/dev/null > ~/.ssh/known_hosts
$ ssh-keygen -lf ~/.ssh/known_hosts
256 SHA256:Td6TuDgimAJgbnO3HW9bu0y9jWDtkkjrmwMZLzgvb9c 172.30.50.12 (ED25519)
256 SHA256:gCtIr4PtE6UFGVh2JW8wOCx0jYxZdqCJ7d765RsibYk 172.30.50.11 (ED25519)
256 SHA256:R/LqIOXlXBEt+jf5e14Qv11ZdsyaszbiAHnSZaPQZzU 172.30.50.21 (ED25519)
web1$ ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub
256 SHA256:gCtIr4PtE6UFGVh2JW8wOCx0jYxZdqCJ7d765RsibYk root@web1 (ED25519)
Отпечаток web1 в known_hosts совпадает с отпечатком, который администратор видит в консоли узла. Так проверяют каждый узел — через консоль виртуальной машины, панель облака или того, кто создавал сервер.
host_key_checking = False, «чтобы не мешало». Это выключает проверку подлинности узлов: подменивший адрес сервера получит подключение с ключом, который даёт root на всех ваших машинах. Отпечатки собирают один раз при создании узла — это минута работы.Проверим подключение обычным SSH — пользователь, его права и sudo:
$ ssh -i ~/.ssh/ansible_ed25519 ansible@172.30.50.11 "hostname; id; sudo -n whoami"
web1
uid=1000(ansible) gid=1000(ansible) groups=1000(ansible)
root
Без файлов настройки всё указывается в командной строке: -i — список узлов (запятая в конце означает «это список, а не файл»), -u — пользователь, --private-key — ключ, -m — модуль. Модуль ping не про ICMP: он проверяет, что Ansible может зайти на узел и выполнить там Python:
$ ansible all -i 172.30.50.11, -u ansible --private-key ~/.ssh/ansible_ed25519 -m ping
[WARNING]: Host '172.30.50.11' is using the discovered Python interpreter at '/usr/bin/python3.13', but future installation of another Python interpreter could cause a different interpreter to be discovered. See https://docs.ansible.com/ansible-core/2.19/reference_appendices/interpreter_discovery.html for more information.
172.30.50.11 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3.13"
},
"changed": false,
"ping": "pong"
}
Предупреждение говорит, что Ansible сам нашёл Python на узле. Как убрать его настройкой — в модуле 02; дальше в этом модуле вывод показан без него.
Теперь идемпотентность на деле. Установим пакет tree модулем apt; -b (become) — выполнить через sudo:
$ ansible all -i 172.30.50.11, -u ansible --private-key ~/.ssh/ansible_ed25519 -b -m apt -a 'name=tree state=present update_cache=true' | grep -E '\| (CHANGED|SUCCESS)|"changed"'
172.30.50.11 | CHANGED => {
"changed": true,
CHANGED и "changed": true — пакета не было, Ansible его поставил. Та же команда ещё раз:
$ ansible all -i 172.30.50.11, -u ansible --private-key ~/.ssh/ansible_ed25519 -b -m apt -a 'name=tree state=present' | grep -E '\| (CHANGED|SUCCESS)|"changed"'
172.30.50.11 | SUCCESS => {
"changed": false
SUCCESS и "changed": false: пакет уже есть, ничего не менялось. Модуль command просто выполняет команду — он не знает, меняет она что-то или нет, и поэтому каждый раз честно сообщает CHANGED:
$ ansible all -i 172.30.50.11, -u ansible --private-key ~/.ssh/ansible_ed25519 -m command -a 'tree -L 1 /srv'
172.30.50.11 | CHANGED | rc=0 >>
/srv
0 directories, 0 files
$ ansible all -i 172.30.50.11, -u ansible --private-key ~/.ssh/ansible_ed25519 -m command -a 'tree -L 1 /srv'
172.30.50.11 | CHANGED | rc=0 >>
/srv
0 directories, 0 files
Отсюда правило на весь курс: если для задачи есть модуль, описывающий состояние, используем его. command и shell — последнее средство, и тогда об идемпотентности заботимся сами (модули 03 и 08).
apt install ansible — ansible-core и коллекции, Galaxy не нуженansible, отдельный ключ, отпечатки узлов в known_hosts; host_key_checking = False — никогдаCHANGED — состояние изменилось, SUCCESS с changed: false — уже было таким; command всегда CHANGED