Новый инстанс AWS EC2 после запуска представляет собой голую операционную систему с одним ключом SSH, и вся ценность облачной автоматизации сводится к тому, чтобы превратить эту заготовку в рабочий сервер без единого ручного шага. Инструментом превращения служит user data: сценарий, который машина выполняет при первом старте. Связка cloud init и Ansible закрывает весь путь от чистого образа до настроенного узла: cloud init ставит минимальный набор пакетов и регистрирует машину, а Ansible доводит конфигурацию до боевого состояния. В статье разобрана рабочая схема этой связки, типовые ошибки первого запуска и границы того, что вообще стоит класть в user data.
Первый запуск инстанса и выбор формата user data
Механика простая, но важно понимать её этапы. Платформа передаёт в машину метаданные и пользовательские данные через служебный сервис метаданных, и в момент загрузки агент cloud init забирает оттуда содержимое user data и исполняет его от имени суперпользователя. По умолчанию сценарий срабатывает один раз за жизнь инстанса, при самом первом старте, после чего отметка о выполнении сохраняется в каталоге /var/lib/cloud. Перезагрузки этот код больше не трогают, и именно поэтому туда смотрят как на инструмент bootstrap, а не как на постоянный менеджер конфигурации.
Порядок работы cloud init внутри себя тоже небезразличен. Сначала выполняются модули инициализации сети и хранилища, затем модули конфигурации, и только в самом конце сценарии из группы final, куда попадают тяжёлые шаги вроде установки пакетов и вызова внешних систем управления. Если положить всю логику в начало, машина будет выглядеть готовой раньше, чем она ею станет, и балансировщик отправит на неё трафик в неудачный момент.
User data принимает два основных формата, и выбор между ними определяет читаемость всей схемы. Первый формат начинается со строки решётки cloud config и описывает декларативный сценарий: список пакетов, пользователей, файлов, команд. Второй формат это обычный shell скрипт, начинающийся с шебанга, и он подходит там, где нужна произвольная логика. На практике сильная схема выглядит так: декларативная часть делает предсказуемый минимум, а всё сложное отдаётся Ansible.
Пример минимального cloud config для машины, которую дальше поведёт Ansible:
#cloud-config
package_update: true
packages:
- python3
- python3-pip
users:
- name: deploy
groups: [adm, sudo]
shell: /bin/bash
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
ssh_authorized_keys:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGexampleDeployKeyText
runcmd:
- [sed, -i, "s/#PasswordAuthentication yes/PasswordAuthentication no/", /etc/ssh/sshd_config]
- [systemctl, restart, sshd]
final_message: "Cloud init завершён за $UPTIME секунд"
Смысл фрагмента в трёх вещах: обновить индекс пакетов, гарантировать наличие интерпретатора Python, без которого Ansible не умеет работать с узлом, и завести служебного пользователя с ключом, под которым будет приходить система управления. Отключение парольного входа добавлено как страховка поверх настроек образа.
Подключение машины к Ansible без ручных шагов
Дальше возникает вопрос, как система управления узнаёт о новой машине. Классический push по статичному списку хостов здесь неудобен, потому что список меняется при каждом запуске. Решают двумя способами, и часто их комбинируют. Первый способ это динамический инвентарь: плагин aws ec2 опрашивает API облака и собирает машины по тегам, поэтому свежезапущенный инстанс появляется в инвентаре автоматически, как только у него проставлен правильный ярлык. Второй способ это ansible pull, когда машина сама забирает playbook из репозитория и применяет его к себе, и этот вариант удобен в автономных сценариях, где управляющий сервер не нужен вовсе.
Пример описания динамического инвентаря в файле aws_ec2.yml:
plugin: amazon.aws.aws_ec2
regions:
- eu-north-1
filters:
tag:Role: web
instance-state-name: running
keyed_groups:
- key: tags.Environment
prefix: env
Ярлыки решают здесь всё. Машина, размеченная ярлыком Role со значением web, попадает в группу web, а ярлык Environment дополнительно раскладывает хосты по средам. Никаких правок инвентаря при масштабировании не требуется: новый инстанс сам занимает своё место в структуре через несколько секунд после запуска.
Ansible pull как противоположность push модели
Push схема хороша, пока управляющий узел существует и доступен, но в хозяйствах без постоянного контроллера удобнее обратный порядок. Режим ansible pull переворачивает картину: машина сама забирает playbook из репозитория и применяет его к себе по расписанию или по событию. Для этого не нужен ни управляющий сервер, ни открытые входящие порты для подключения, достаточно исходящего доступа к хранилищу кода.
В user data добавляется установка Ansible и первичный вызов pull:
#cloud-config
packages:
- ansible
runcmd:
- [ansible-pull, -U, "https://git.example.com/infra/base.git", site.yml]
Дальше наступает очередь расписания: задание cron или таймер systemd повторяет pull раз в час или в ночные часы, и машина сама выравнивает свою конфигурацию после каждого изменения в репозитории. Такой рисунок называют вытягивающей моделью, и у неё два сильных свойства. Первое: масштабирование не требует никаких действий на стороне управления, десять или сто новых машин просто начнут тянуть конфигурацию сами. Второе: доступ к репозиторию выдают через ключ только для чтения, развёрнутый в машину, и утечка такого ключа не открывает путь к остальным узлам.
Плата за простоту тоже есть. Вытягивающая модель хуже подходит для срочных изменений, потому что применяется по расписанию, а не по команде, и требует аккуратного отношения к порядку применения на машинах, зависящих друг от друга. Поэтому крупные хозяйства часто совмещают оба режима: pull для базовой конфигурации и периодического выравнивания, push для координированных изменений, где последовательность важна.
Разметка ярлыками как основа инвентаря
Динамический инвентарь ценен ровно настолько, насколько аккуратна разметка машин. Ярлыки в облаке работают как адресная книга: Role описывает службу, Environment указывает среду, Project связывает машину с задачей. Стоимость разметки нулевая, а отдача проявляется сразу во всех инструментах: и инвентарь Ansible, и фильтры в консоли облака, и отчёты о расходах опираются на одни и те же ярлыки.
В плагине инвентаря группы собираются автоматически из значений ярлыков: ключ Environment даёт группы prod, stage и dev, ключ Role даёт группы web, api и worker. Дополнительный приём это составные группы, когда машина попадает сразу в несколько сечений, например в группу операционной системы и в группу службы одновременно. Разметку задают в момент запуска машины вместе с user data, поэтому оба артефакта живут в одном шаблоне инфраструктуры и меняются вместе. Ошибки разметки всплывают быстро: машина без ярлыков не попадает в инвентарь, конфигурация на неё не приезжает, и это обнаруживается на первом же прогоне.
Первый прогон конфигурации и порядок шагов
Связка работает как конвейер, и порядок в нём принципиален:
- инстанс запускается с подготовленным user data и ярлыками, по которым его найдёт инвентарь;
- cloud init обновляет пакеты, ставит Python и заводит служебного пользователя;
- плагин динамического инвентаря обнаруживает машину по ярлыкам и добавляет её в группу;
- Ansible применяет playbook: настраивает веб сервер, правила файрвола, приложения;
- проверочные задачи подтверждают готовность, после чего машина включается в балансировщик;
- всё перечисленное живёт в коде, и повторный запуск воспроизводит конфигурацию без отклонений.
Шаги с третьего по пятый обычно оформляют одним playbook с разделением по ролям: базовая настройка, сервис, мониторинг. Тогда добавление компонента превращается в дописывание роли, а не в переписывание сценария запуска.
Пример запуска плейбука против динамической группы:
ansible-playbook -i aws_ec2.yml site.yml --limit env_prod
Ограничитель по среде защищает от дорогой оплошности, когда конфигурация предназначена для боевых машин, а применяется ко всем сразу. В больших хозяйствах к этому добавляют проверочный режим с флагом проверки изменений, чтобы сначала увидеть план, а потом его применить.
Отладка первого запуска и типовые ошибки
Когда что-то пошло не так, в первую очередь смотрят журнал cloud init output, куда падает вывод всех шагов вместе с ошибками. Команда cloud init status показывает стадию выполнения и код результата, а запрос полных журналов помогает найти шаг, застрявший посреди пути. Из частых причин сбоев встречаются три: опечатка в YAML, которая валит весь декларативный сценарий целиком, отсутствие Python на минималистичных образах, и попытка обратиться к метаданным раньше, чем поднялась сеть.
Ещё одна ловушка связана с лимитом объёма: платформа ограничивает размер user data шестнадцатью килобайтами, и в этот лимит не помещаются ни длинные скрипты, ни содержимое ключей в большом количестве. Правило простое: user data держат тонким, а всю тяжесть переносят в playbook. Там, где без длинного скрипта не обойтись, его скачивают из репозитория одной короткой командой.
Что вынести за пределы user data и как проверять готовность
User data хорош ровно до той черты, где конфигурация становится изменяемой. Всё, что машина должна получить один раз при рождении, живёт в нём: пакеты первого дня, пользователь, базовые настройки безопасности. Всё, что будет меняться при каждом обновлении приложения, должно принадлежать Ansible, потому что только там появляются идемпотентные задачи, версии, откаты и разделение по ролям. Смешение этих слоёв даёт самый тяжёлый вид хозяйства, когда непонятно, кто последним менял настройку и почему на двух машинах она разная.
Отдельная рекомендация касается секретов. Сертификаты, токены и пароли не кладут в user data в открытом виде: пользовательские данные читаются через метаданные любой программой на машине, а значит это место не для тайн. Правильный путь это выдача секретов через роль машины в момент применения playbook из хранилища секретов, либо генерация на месте с последующей передачей в закрытое хранилище.
Применение playbook само по себе не означает готовности, поэтому в конец конвейера ставят проверочные задачи. Минимальный набор это проверка слушающего порта сервиса, запрос локального адреса здоровья и просмотр последних строк журнала на предмет ошибок. В Ansible такие проверки оформляют задачами с повторными попытками: машина считается готовой, когда ответ на запрос здоровья приходит с ожидаемым кодом, а не тогда, когда сервис формально запущен.
Отдельного внимания заслуживает стартовая задержка. Приложение может стартовать дольше, чем завершится работа cloud init, и попытка включить машину в балансировщик на этом промежутке обернётся ошибками у посетителей. Правильное место проверки готовности это стык двух систем: cloud init ждёт локальный ответ здоровья перед финальным сообщением, а автоматика развёртывания вычисляет статус той же командой и только затем открывает машину для трафика.
Для чувствительных хозяйств к этому добавляют периодический аудит: раз в неделю свежий инстанс поднимается в тестовой среде по тем же материалам, проходит тот же playbook и ту же проверку готовности, после чего уничтожается. Такой прогон ловит сразу три класса проблем: устаревшие пакеты в репозиториях, изменения в базовом образе и расхождение между документацией и реальностью. Цена вопроса один короткий прогон в неделю, а экономия это ночь без сюрпризов в момент, когда потребовалось срочное масштабирование.
Наконец, сама схема стоит затраченных усилий только при автоматическом тестировании. Рабочий приём это периодический запуск чистого инстанса по тому же user data и playbook с последующей проверкой готовности: так поломка обнаруживается в момент изменения кода конфигурации, а не в ночь, когда потребовалось экстренное масштабирование.