Зачем управление конфигурацией: идемпотентность и декларативность
Lesson from the course “Курс подготовки DevOps-инженеров”
Хронометраж: занятие 60 минут — теория 30 минут, практика 30 минут. Домашняя работа: 1–2 часа.
Кратко о чём урок: Проблема дрейфа конфигурации и «уникальных» серверов, push- и pull-модели, идемпотентность как основное требование, место Ansible рядом с образами и IaC.
1. Что изучаем
После урока вы сможете:
- показывать дрейф конфигурации числами, а не описывать его словами;
- объяснять, чем декларативное описание состояния отличается от скрипта, и в чём именно выигрыш;
- проверять идемпотентность и понимать, почему без неё автоматизация опасна;
- различать push- и pull-модели и называть, что каждая требует от инфраструктуры;
- определять границу: что решает управление конфигурацией, а что — образы и IaC.
2. Зачем это нужно и где применяется
Блоки 03 и 04 дали умение настроить сервер: оболочка, пакеты, сеть, службы. Блок 08 научил доставлять изменения пайплайном. Остался вопрос, который возникает на третьем сервере и становится болезненным на тридцатом: как сделать так, чтобы все они были одинаковыми — и оставались такими.
Ситуации, из которых вырос этот блок:
- «на втором сервере не работает, а на первом работает» — и никто не помнит, чем они отличаются;
- «настройка нового узла занимает день, потому что инструкция в вики устарела»;
- «после ночного инцидента поправили конфигурацию на одном узле, записать забыли»;
- «скрипт настройки запустили второй раз, и он перезапустил сервис на рабочем сервере».
Всё измеримое в уроке измерено на стенде из трёх «серверов» в контейнерах — том же приёме, что в блоках 05 и 09.09.
3. Теория (30 минут)
3.1. Дрейф конфигурации: как одинаковые серверы становятся разными
Начнём с опыта. Три сервера настроены одним и тем же скриптом
(demo/setup-by-hand.sh) — это честная
запись того, что человек делает руками по инструкции из вики. Сразу после настройки:
свойств проверено: 4
совпадают на всех узлах: 4
разошлись: 0
узлы одинаковы: дрейфа нет
Проходит месяц. На node2 подняли число воркеров под нагрузку. На node3 ночью чинили
инцидент: поменяли порт, увеличили таймаут и заодно каталог остался от root. Каждое
изменение было осмысленным и правильным в тот момент. Смотрим снова:
свойство node1 node2 node3
владелец каталога appuser appuser root <- РАСХОЖДЕНИЕ
воркеров 2 4 2 <- РАСХОЖДЕНИЕ
порт 8080 8080 8081 <- РАСХОЖДЕНИЕ
таймаут 30 30 60 <- РАСХОЖДЕНИЕ
свойств проверено: 4
совпадают на всех узлах: 0
разошлись: 4
Все четыре свойства разошлись, и ни один сервер при этом не сломан — каждый работает. Именно поэтому дрейф незаметен: он обнаруживается не мониторингом, а сравнением, которое никто не делает.
Отсюда понятие «уникального сервера»: узел, состояние которого больше нельзя воспроизвести, потому что история изменений известна только частично. Когда такой сервер умирает, замена занимает не час, а неделю — и новый узел всё равно окажется чуть другим.
Инструмент для сравнения лежит в примерах —
drift-report.py. Он ничего не чинит, только
показывает: это первый шаг и для домашней работы тоже. Одна деталь в нём сделана намеренно:
при найденных расхождениях он возвращает код 1. Так его можно поставить в пайплайн
шагом, который падает, когда серверы разъехались, — ровно по логике ворот из урока 09.07.
3.2. Скрипт описывает действия, описание состояния — результат
Тот же самый результат можно записать иначе. Сравните два файла из примеров: скрипт
setup-by-hand.sh и описание
demo/site.yml. Разница не в синтаксисе:
- скрипт говорит, что сделать: создай каталог, запиши файл, добавь пользователя;
- описание говорит, каким должно быть состояние: каталог существует и принадлежит
appuser, файл содержит вот это, пользователь заведён.
Во втором случае решение «что предпринять» принимает инструмент: он выясняет текущее состояние и меняет только несовпавшее. Применяем описание к нашим разъехавшимся серверам:
TASK [Конфигурация соответствует образцу]
ok: [node1]
changed: [node2]
changed: [node3]
node1 : ok=3 changed=0
node2 : ok=3 changed=1
node3 : ok=3 changed=2
Три узла, три разных числа изменений — ровно по величине расхождения. node1 не тронули
вовсе, node3 починили в двух местах. И проверка после:
свойств проверено: 4
совпадают на всех узлах: 4
разошлись: 0
Дрейф устранён, и — что важнее — описание состояния теперь и есть документация. Не вики, которая устаревает, а файл, который применяется.
Оговорка, без которой картина неполная: декларативность избавляет от описания действий,
но не от зависимостей внутри состояния. В нашем первом варианте site.yml каталог с владельцем
appuser стоял раньше задачи, создающей этого пользователя, — на чистом сервере всё падало.
Порядок задач всё равно имеет значение; просто это порядок частей состояния, а не шагов
процедуры.
3.3. Идемпотентность: главное требование
Свойство, ради которого всё затевается: повторное применение ничего не меняет, если менять нечего. Проверяется одним опытом — запустить дважды подряд.
первый запуск: node1: ok=3 changed=3
второй запуск: node1: ok=3 changed=0
Ноль изменений во втором прогоне. Здесь легко перепутать два счётчика в отчёте, и путаница
опасна. ok=3 у node1 означает, что все три задачи были оценены: инструмент подключился
к узлу, прочитал текущее состояние каждого свойства и сравнил с описанным в site.yml.
changed=0 означает, что из этих трёх оценённых задач ни одна не потребовала правки. Это
не то же самое, что «задачи не выполнялись» — если бы одна из них не выполнилась вовсе
(например, из-за неверного условия when или ошибки соединения), она не попала бы даже
в ok, и отчёт выглядел бы короче, а не с нулями в графе changed. Отсюда практическое
правило чтения отчёта: ok=N changed=0 значит «проверено и в порядке», а ok меньше
общего числа задач — сигнал, что часть проверок вообще не прошла и дрейф на этих свойствах
остался бы незамеченным. Путать эти два состояния — верный способ решить, что дрейфа нет,
когда на самом деле не работает сама проверка.
Теперь то же самое со скриптом:
метка времени файла до: 1789549727
метка времени файла после: 1789549738 (файл ПЕРЕЗАПИСАН)
Скрипт переписал файл, хотя содержимое совпадало. На конфигурации это безобидно. Но тот же скрипт обычно заканчивается перезапуском службы — и вот это уже обрыв соединений клиентов на пустом месте, измеренный в уроке 09.09.
Практическое следствие важнее формулировки: идемпотентная автоматизация безопасна для регулярного запуска. Её можно выполнять по расписанию, и она будет возвращать серверы к описанному состоянию — то есть чинить дрейф автоматически. Неидемпотентную по расписанию запускать нельзя, а значит она применяется редко и вручную, и дрейф копится между запусками.
Проверять идемпотентность надо не верой в инструмент, а прогоном: модуль, вызывающий произвольную команду, идемпотентным не будет, сколько бы деклараций вокруг ни было.
3.4. Режим проверки: узнать, не трогая
У идемпотентности есть следствие, которое ценят не меньше её самой: если инструмент умеет выяснять текущее состояние, он может показать, что изменится, ничего не меняя. Опыт: вносим расхождения на два узла и запускаем проверку.
проверка без изменений (--check):
node1 : ok=3 changed=0
node2 : ok=3 changed=1
node3 : ok=3 changed=1
а что на серверах на самом деле:
node2 воркеров: 4 (осталось как было — верно)
node3 владелец каталога: root (осталось как было — верно)
Отчёт показал расхождения, серверы не тронуты. Это отвечает на вопрос, который в эксплуатации задают постоянно: «что произойдёт, если я это применю». У скрипта такого ответа нет в принципе — чтобы узнать, что он сделает, его надо выполнить.
Практическое применение шире, чем «посмотреть перед выкатом»:
- аудит дрейфа: проверка по расписанию показывает, какие узлы разъехались, не меняя их;
- ревью изменений: к правке плейбука прикладывается вывод проверки — видно, что именно поменяется на каждом узле;
- проверка в пайплайне: шаг, который падает, если расхождение обнаружено, — по той же логике, что ворота в 09.07.
Важное ограничение, о котором стоит знать сразу: режим проверки достоверен настолько, насколько модули умеют предсказывать результат. Задача, вызывающая произвольную команду, в этом режиме обычно просто пропускается — и отчёт молчит о том, что она бы сделала. Подробнее это разбирается в 10.07.
3.5. Цена: инструмент проигрывает на малом и выигрывает на большом
Честный замер на нашем стенде из трёх узлов:
по очереди скриптом, 3 узла: 1323 мс
описанием состояния, 3 узла: 7598 мс
Инструмент медленнее почти в шесть раз. Он на каждом шаге выясняет текущее состояние каждого узла, и это стоит времени — то же наблюдение, что в 09.09 при сравнении скрипта и Ansible.
Значит выигрыш не в скорости. Он в трёх других вещах:
- одинаковость: результат на тридцати узлах такой же, как на трёх, и это проверяемо;
- повторяемость: новый узел получает то же состояние, что старые, без чтения вики;
- обратимость: изменение состояния — это изменение в файле, которое проходит ревью и попадает в историю (блок 03).
Ориентир для решения: на одном сервере, который трогают раз в полгода, управление конфигурацией — лишняя сложность. От трёх узлов или при регулярных изменениях оно окупается, и чем дальше, тем сильнее.
3.6. Push и pull: кто кого вызывает
Две модели доставки состояния, и разница практическая, а не идеологическая.
Push (так работает Ansible): управляющая машина подключается к серверам и применяет состояние. Требует: доступ по SSH с управляющей машины до всех узлов и момент, когда кто-то запускает применение.
Pull (так работают Puppet, Chef, а в кластерах — GitOps из блока 14): на каждом узле живёт агент, который сам ходит за описанием и применяет его по расписанию. Требует: установить и обслуживать агента, держать сервер с описаниями.
| push | pull | |
|---|---|---|
| что нужно на узле | ничего, кроме SSH и Python | агент |
| кто инициирует | человек или пайплайн | сам узел, по расписанию |
| дрейф чинится | когда запустят | сам, в следующем цикле |
| новый узел | надо добавить в инвентарь | подключается сам |
| доступ | управляющая машина → узлы | узлы → сервер описаний |
Последняя строка часто решает вопрос: в push-модели управляющей машине нужен доступ ко всем серверам — это ровно тот ключ в пайплайне, о котором шла речь в 09.09 и 09.04. В pull-модели наоборот: узлы сами ходят наружу, и доступа внутрь не нужно.
Ansible выбран для курса потому, что он не требует агента: достаточно SSH и Python на узле. Это же его ограничение — применение происходит только когда его запустили.
3.7. Граница: что этим не решается
Управление конфигурацией занимает узкую полосу, и путаница с соседями обходится дорого.
| Задача | Чем решается | Где в курсе |
|---|---|---|
| создать виртуальную машину, сеть, балансировщик | IaC (Terraform) | блок 12 |
| первичная настройка при первом запуске машины | cloud-init | 10.08 |
| собрать образ с предустановленным ПО | Packer, docker build |
блок 08 |
| привести существующий сервер в нужное состояние | управление конфигурацией | блок 10 |
| выкатить новую версию приложения | пайплайн выката | 09.09, 09.10 |
| запустить контейнеризованное приложение | оркестратор | блоки 13–14 |
Две границы стоит проговорить отдельно.
Ansible против образов. Если сервис работает в контейнере, состояние «какие пакеты стоят» описывается Dockerfile, а не плейбуком (блок 08). Управление конфигурацией остаётся для самого узла: ядро, демон контейнеров, пользователи, агенты наблюдения.
Ansible против выката. Плейбуком можно выкатить приложение — в 09.09 мы так и делали. Но выкат и приведение к состоянию живут по разным правилам: выкат про версии, откат и стратегии (09.10, 09.11), а состояние — про одинаковость. Смешивать их в одном плейбуке обычно неудобно; разбор — в 10.08.
И самое частое заблуждение: инструмент не отменяет знания системы. Плейбук, который ставит nginx, надо писать зная, как устроены его конфигурация, права и юнит systemd (02.08). Управление конфигурацией — способ записать это знание, а не заменитель.
3.8. Примеры и варианты реализации
Задача: привести несколько серверов к одинаковому состоянию и удерживать их в нём.
| Среда | Как решается | Что нужно иметь |
|---|---|---|
| Локальный стенд (Docker / VM / kind) | несколько контейнеров с sshd и Ansible с управляющего | Docker и course-deploy:13 |
| VPS / bare-metal | Ansible с управляющей машины или из пайплайна | SSH-доступ, ключ, Python на узлах |
| Yandex Cloud | Ansible к созданным ВМ; создание самих ВМ — Terraform | аккаунт, сеть, ключи |
Локальный стенд. Три «сервера» в контейнерах дают всё нужное для опытов: дрейф,
сходимость, идемпотентность. Своего образа блоку пока не потребовалось —
подошёл course-deploy:13 из урока 09.09, в нём уже есть ansible-core, ssh и python3.
Для урока 10.07 понадобится ansible-lint, которого там нет; об этом сказано в README стенда.
VPS / bare-metal. Основной случай. Управляющей машиной может быть ваш ноутбук, отдельный сервер или раннер пайплайна. Последнее удобнее (нет расхождений «у меня работало»), но требует ключа в секретах — со всеми оговорками из 09.04.
Yandex Cloud. Разделение простое: Terraform создаёт машины, Ansible их настраивает. Часть работы при этом уходит в cloud-init — что именно, разбирается в 10.08. Запуском в облаке не проверялось: учётной записи облака на стенде нет; практика с облаком — блок 11.
Что выбрать. Начинать стоит с того, что болит: возьмите операцию, которую делаете руками чаще всего, и опишите её. Полный переход «всё в коде» за один заход не получается и не нужен; первый плейбук окупается тем, что на нём становится видно, из чего вообще состоит настройка вашего сервера.
4. Практика на занятии (30 минут)
Курс проходят самостоятельно, поэтому каждый шаг говорит, что ввести, зачем, что должно
получиться и что делать при неудаче. Наблюдаем дрейф на трёх серверах и смотрим, что
с ним делает описание состояния; стенд — три контейнера с sshd на образе
course-deploy:13 — поднимается автоматически на первом же шаге.
Файлы для работы: blocks/10-ansible/examples/10.01-cm-concepts/
Шаг 1 (5 минут). Дрейф: как «одинаковые» серверы расходятся.
- Что ввести:
cd blocks/10-ansible/examples/10.01-cm-concepts ./lab-cm-concepts.sh drift - Зачем: увидеть без слов, как три сервера, настроенные одним и тем же скриптом
setup-by-hand.sh, расходятся после «месяца эксплуатации», хотя каждое отдельное изменение было осмысленным. - Ожидаемый результат: сразу после настройки расхождений ноль:
после «месяца» разошлись все четыре свойства (владелец каталога, воркеров, порт, таймаут).свойств проверено: 4 совпадают на всех узлах: 4 разошлись: 0 - Если не так: если скрипт останавливается сообщением «стенд не поднялся» — проверьте,
что Docker запущен (
docker ps), и что параллельно не работает другой режим лаборатории: уборка одного прогона снимает контейнеры другого по общему префиксу имени.
Шаг 2 (5 минут). Сходимость: что делает описание состояния.
- Что ввести:
./lab-cm-concepts.sh converge - Зачем: увидеть разницу с шагом 1 — там расхождения только показывали, здесь их
устраняют применением описания, а не последовательности команд. Откройте заодно
demo/site.yml: в нём нет ни одной команды. - Ожидаемый результат:
три узла — три разных числа изменений, ровно по величине расхождения; после применения повторная проверка снова показывает ноль расхождений.node1 : ok=3 changed=0 node2 : ok=3 changed=1 node3 : ok=3 changed=2 - Если не так: если
changed=0у всех трёх узлов сразу — расхождения не были внесены в этом прогоне; выполнитеdrift, затемconvergeещё раз и сравните отчёты «было/стало».
Шаг 3 (5 минут). Идемпотентность: что происходит при повторном запуске.
- Что ввести:
./lab-cm-concepts.sh idempotent - Зачем: проверить главное требование управления конфигурацией прогоном, а не верой в инструмент.
- Ожидаемый результат:
а тот же скрипт «настройки руками» при повторном запуске переписывает файл заново (метка времени меняется), хотя содержимое совпадало.первый запуск: node1: ok=3 changed=3 второй запуск: node1: ok=3 changed=0 - Если не так: если во втором прогоне
changedбольше нуля — значит задача описывает действие, а не состояние (например, вызывает произвольную команду); идемпотентной такая задача сама по себе не станет — это разбирается в 10.07. - Ответьте: чем опасно то, что скрипт переписывает файл, хотя менять было нечего — вспомните, чем обычно заканчивается такой скрипт на реальном сервере (подсказка в 3.3).
Шаг 4 (5 минут). Проверка без изменений.
- Что ввести:
./lab-cm-concepts.sh check - Зачем: убедиться, что инструмент способен ответить на вопрос «что изменится, если применить», не трогая сервер, — то, чего у скрипта нет в принципе.
- Ожидаемый результат:
в отчёте — расхождения, а на самих серверах воркеров наnode2 : ok=3 changed=1 node3 : ok=3 changed=1node2и владелец каталога наnode3остаются прежними (пометка «осталось как было — верно»). - Если не так: если вместо «осталось как было — верно» напечатано «ИЗМЕНИЛОСЬ» — режим
--checkне сработал для этой задачи; так бывает с задачами, вызывающимиcommandилиshell, — в--checkони обычно просто пропускаются (подробнее в 10.07).
Шаг 5 (5 минут). Цена.
- Что ввести:
./lab-cm-concepts.sh scale - Зачем: сравнить время работы скрипта и инструмента числами, а не ощущением «стало медленнее».
- Ожидаемый результат:
инструмент медленнее почти в шесть раз — он на каждом шаге выясняет текущее состояние каждого узла.по очереди скриптом, 3 узла: 1323 мс описанием состояния, 3 узла: 7598 мс - Если не так: если разрыв сильно отличается от приведённого — на машине в этот момент шла сборка образа или другая нагрузка; важен порядок величины (инструмент медленнее в разы), а не конкретные миллисекунды — абсолютные числа плавают (правило 4a).
- Ответьте: если инструмент вшестеро медленнее скрипта, зачем он тогда нужен? Сформулируйте своими словами, не подсматривая в теорию 3.5.
Шаг 6 (5 минут). Уборка.
- Что ввести:
./lab-cm-concepts.sh clean - Зачем: снять контейнеры, сеть и одноразовые ключи — стенд не должен продолжать работать после урока.
- Ожидаемый результат: строка
убрано: контейнеры lab901-*, сеть, каталоги; посмотрите заодноdemo/inventory.ini— как заданы три узла. - Если не так: если после уборки контейнеры остались — проверьте
docker ps -a | grep lab901-и снимите остатокdocker rm -f; урок должен заканчиваться пустым стендом.
Проверка результата. Вы можете показать дрейф числами; объяснить, чем changed=0
отличается от «ничего не произошло»; и назвать задачу, которую управлением конфигурации
решать не нужно.
5. Домашняя работа (1–2 часа)
Задание. Найдите дрейф в своей инфраструктуре и выберите первый кусок для автоматизации.
-
Инвентаризация дрейфа — заготовка
drift-audit.md. Возьмите не меньше трёх серверов одной роли (подойдут учебные, рабочие тестовые, узлы домашней лаборатории) и сравните не меньше пяти свойств. Снимайте командой, а не глазами; можно тем жеdrift-report.py. Для каждого расхождения ответьте, объяснимо оно или нет. -
План автоматизации — заготовка
automation-plan.md. Выпишите операции, которые делаете руками, с частотой и временем, и выберите первую для описания кодом. Обязательный раздел — «чем это НЕ решается»: он не даст начать с задачи, для которой нужен другой инструмент.
Если своих серверов нет, поднимите стенд лабораторной (./lab-cm-concepts.sh drift) и
работайте с ним: три узла, расхождения уже внесены.
Критерии приёмки.
- Сравнение проведено командой, команда записана; не меньше трёх узлов и пяти свойств.
- Заполнена таблица результата с числами: проверено, совпало, разошлось.
- Для каждого расхождения указано, объяснимо оно или нет.
- Отвечено, сколько занимает настройка нового узла и по какому документу.
- В плане автоматизации не меньше трёх ручных операций с частотой и временем.
- Для каждой указано, кто её умеет выполнять.
- Выбрана первая операция, выбор обоснован частотой и ценой ошибки.
- Заполнен раздел «чем это не решается» — с конкретными инструментами.
6. Частые ошибки и подводные камни
Считать, что одинаково настроенные серверы останутся одинаковыми. Симптом: «на втором не работает». Причина: каждое ручное изменение осмысленно и нигде не записано — измерено: через «месяц» разошлись все четыре свойства из четырёх. Что делать: сравнивать регулярно, а лучше применять описание по расписанию.
Писать плейбук как скрипт. Симптом: задачи вида «выполни команду», changed всегда равен
числу задач. Причина: описаны действия, а не состояние. Что делать: смотреть на счётчик
changed при повторном запуске — у декларативного описания он нулевой.
Не проверять идемпотентность. Симптом: автоматизация перезапускает службы при каждом
прогоне. Причина: считали инструмент идемпотентным по умолчанию. Что делать: запускать дважды
и смотреть changed; у модулей, вызывающих произвольные команды, идемпотентность надо
обеспечивать самому.
Ждать выигрыша в скорости. Симптом: «раньше скрипт отрабатывал за секунду, теперь минута». Причина: инструмент выясняет состояние перед изменением — измерено: 7598 мс против 1323 мс на трёх узлах. Что делать: считать выигрыш в одинаковости и повторяемости, а не в секундах.
Автоматизировать всё сразу. Симптом: месяц пишется «полная автоматизация», в продакшене ничего не изменилось. Причина: выбран слишком большой первый кусок. Что делать: начать с операции, которая повторяется чаще всего.
Путать управление конфигурацией с созданием инфраструктуры. Симптом: плейбук, который пытается создать виртуальную машину. Причина: границы инструментов не разведены. Что делать: создание — IaC (блок 12), настройка существующего — Ansible.
Считать, что инструмент заменит знание системы. Симптом: плейбук написан по примеру из интернета и «вроде работает». Причина: скопировано без понимания. Что делать: сначала уметь настроить руками, потом записывать это кодом.
7. Проверка усвоения
- Три сервера настроены одним скриптом. Почему через полгода они окажутся разными и почему этого никто не заметит?
- Чем
changed=0при повторном запуске отличается от «скрипт отработал без ошибок»? - В чём разница между «описать действия» и «описать состояние»? Приведите пример, где она даёт разный результат.
- Инструмент управления конфигурацией оказался вшестеро медленнее скрипта. Зачем он тогда нужен?
- Что нужно установить на сервер для push-модели и что — для pull-модели? Какой доступ требуется в каждом случае?
- Дрейф чинится сам в pull-модели и не чинится в push. Почему и что с этим делать?
- Приложение работает в контейнере. Что в этом случае описывает плейбук, а что — Dockerfile?
- Назовите три задачи, которые управлением конфигурации решать не нужно, и скажите, чем они решаются.
8. Материалы
Первоисточники:
- Ansible: How Ansible works — модель без агента, инвентарь, модули. Первое, что стоит прочитать целиком.
- Ansible: Desired state and idempotency — определение идемпотентности в терминах самого инструмента.
- Martin Fowler. SnowflakeServer — откуда взялось понятие «уникального сервера» и чем он опасен.
- Martin Fowler. PhoenixServer — противоположный подход: сервер, который можно уничтожить и воссоздать.
Дополнительно:
- Ansible: Best practices — пригодится начиная с урока 10.03, но заглянуть полезно сразу.
← Предыдущий: 09.12. GitLab CI: сравнение с GitHub Actions и перенос пайплайна · Блок 10 · Следующий: 10.02. Установка, inventory, подключения, ad-hoc команды →
Practice materials
Scripts, cheat sheets and starter files for the lesson tasks. The lesson text is on this page.
-
Lesson materials 10.01 «Зачем управление конфигурацией идемпотентность и декларативность».zip