Практикум IaC: Развертывание базового веб-кластера

Интеграционный курс по созданию автоматизированного стенда в Yandex Cloud. Вы научитесь связывать Terraform и Ansible в единый конвейер для развертывания инфраструктуры и настройки веб-стека.

1. Сквозная автоматизация: интеграция Terraform и Ansible через Dynamic Inventory

Команда terraform apply успешно завершена. В консоли зеленая надпись: Apply complete! Resources: 50 added, 0 changed, 0 destroyed. В облаке поднялся целый кластер: балансировщики, веб-серверы, узлы баз данных. Инфраструктура готова, но серверы абсолютно пусты — на них установлена только базовая ОС. Теперь в дело должен вступить Ansible, чтобы накатить конфигурацию. Возникает критическая проблема передачи контекста: откуда Ansible узнает IP-адреса этих пятидесяти новых машин, если они были выданы облачным провайдером динамически всего пару секунд назад?

Проблема передачи состояния от инструмента Provisioning (Terraform) к инструменту Configuration Management (Ansible) — классический архитектурный вызов при построении IaC-конвейеров. Если решать эту задачу в лоб, конвейер быстро обрастает хрупкими скриптами-костылями.

Антипаттерны интеграции: как делать не нужно

Исторически инженеры выработали несколько способов связать Terraform и Ansible, многие из которых сегодня считаются техническим долгом.

Первый и самый распространенный антипаттерн — использование local-exec provisioner внутри Terraform. Идея заключается в том, чтобы заставить Terraform при создании виртуальной машины выполнить локальный bash-скрипт, который допишет IP-адрес нового ресурса в статический файл hosts.ini.

Этот подход нарушает базовые принципы декларативности. Во-первых, файл hosts.ini становится общим изменяемым состоянием (Shared Mutable State) на файловой системе машины, запускающей конвейер. Если конвейер работает в эфемерных Docker-контейнерах (например, в GitLab CI), этот файл просто исчезнет после завершения джоба Terraform, и следующий джоб Ansible его не найдет. Во-вторых, если виртуальная машина будет удалена через веб-консоль облака и пересоздана автоскейлером, Terraform об этом не узнает до следующего запуска, а статический файл сохранит невалидный IP-адрес.

Второй подход — парсинг самого файла terraform.tfstate. Существуют плагины и скрипты, которые читают JSON-структуру стейта Terraform и извлекают оттуда IP-адреса. Это лучше, чем local-exec, так как мы обращаемся к задокументированному состоянию. Однако стейт-файл часто хранится в удаленном зашифрованном бакете S3, доступ к которому жестко ограничен. Заставлять Ansible подключаться к S3, расшифровывать стейт и зависеть от внутренней, периодически меняющейся структуры JSON-файла Terraform — значит создавать жесткую связность (Tight Coupling) между двумя независимыми системами.

!Схема независимой работы Terraform и Ansible через Cloud API

Cloud API как единый источник истины

Единственный надежный способ сквозной автоматизации — полностью развязать Terraform и Ansible, сделав их независимыми потребителями одного и того же API облачного провайдера.

В этой архитектуре Terraform выступает исключительно как писатель (Writer). Его задача — создать ресурсы и правильно их разметить с помощью метаданных (тегов или лейблов). Ansible выступает как читатель (Reader). Он обращается напрямую к API облака, запрашивает список всех существующих ресурсов и динамически формирует группы на основе тегов.

Разметка инфраструктуры в Terraform

Чтобы Ansible смог понять, где находится база данных, а где веб-сервер, ресурсы в облаке должны быть классифицированы. В Yandex Cloud для этого используются labels (метки в формате ключ-значение).

В коде Terraform это выглядит как добавление блока меток к ресурсу:

Критически важно стандартизировать ключи меток в рамках всей компании. Если одна команда использует тег role: web, а другая type: frontend, написать универсальный плейбук Ansible будет невозможно. Минимальный стандарт обычно включает метки окружения (env), роли сервера (role) и принадлежности к проекту/продукту (project).

Настройка Dynamic Inventory Plugin

На стороне Ansible для работы с API облака используется механизм Dynamic Inventory плагинов. В отличие от старых скриптов динамического инвентаря (написанных на Python или Bash), современные плагины встроены в ядро Ansible и конфигурируются декларативно через YAML.

Для Yandex Cloud используется плагин yandex_cloud_compute. Конфигурационный файл (например, yc_inventory.yaml) описывает, как именно Ansible должен опрашивать облако и как группировать полученные серверы.

Здесь происходит магия интеграции. Блок compose решает проблему определения правильного IP-адреса. У виртуальной машины может быть внутренний IP и внешний (NAT). С помощью Jinja2-выражения мы явно указываем Ansible брать внешний адрес (one_to_one_nat.address) и присваивать его стандартной переменной ansible_host. Если пайплайн запускается внутри закрытой сети (VPC), выражение меняется на захват внутреннего адреса.

Блок keyed_groups отвечает за автоматическую маршрутизацию серверов по группам. Инструкция выше говорит плагину: «Возьми значение метки role, добавь к нему префикс role_ и создай такую группу».

!Процесс формирования инвентаря через Cloud API

Если Terraform создал машину с меткой role: frontend, Ansible при запуске динамически создаст группу role_frontend и поместит туда этот сервер. В самом плейбуке разработчику остается лишь сослаться на эту группу:

Эта связка невероятно устойчива. Если ночью автоскейлер удалит один узел role_frontend и создаст два новых, утренний запуск Ansible отработает корректно без единого изменения в коде. Плагин просто опросит API, увидит новые машины с нужным тегом и применит к ним конфигурацию.

Ловушка Eventual Consistency: задержка готовности SSH

При интеграции Terraform и Ansible в единый CI/CD пайплайн (когда шаг ansible-playbook запускается ровно через секунду после terraform apply) инженеры неизбежно сталкиваются со сбоями подключения. Ansible падает с ошибкой UNREACHABLE! Connection refused.

Причина кроется в механике работы облачных API и свойстве Eventual Consistency (согласованность в конечном счете). Когда Terraform рапортует об успешном создании yandex_compute_instance, это означает лишь то, что API платформы приняло запрос, выделило ресурсы (CPU, RAM, диск) и запустило процесс гипервизора.

С точки зрения Terraform ресурс создан. Но внутри виртуальной машины в этот момент только начинает загружаться ядро Linux, монтируются файловые системы, запускается systemd, и лишь в самом конце стартует демон sshd. Этот процесс занимает от 15 до 60 секунд. Ansible, стартующий мгновенно, пытается открыть SSH-сессию к машине, которая еще физически не готова принимать сетевые пакеты на 22 порт.

Попытка решить это добавлением команды sleep 60 между этапами пайплайна — плохая практика. Время загрузки недетерминировано: сегодня это 20 секунд, а завтра при высокой нагрузке на СХД провайдера — 90 секунд. Скрипт либо упадет, либо будет тратить лишнее время впустую.

Правильный паттерн — делегировать ожидание самому Ansible с помощью модуля wait_for_connection. Это специальный модуль, который не требует наличия установленного Python на целевой машине (работает на уровне сетевых сокетов) и циклично пытается установить SSH-соединение с заданным таймаутом.

Для реализации этого паттерна в плейбук добавляется предварительный этап (pre-task):

Директива gather_facts: false здесь критически важна. По умолчанию Ansible перед выполнением первой задачи пытается подключиться к серверу и выполнить Python-скрипт для сбора фактов о системе (ОС, память, диски). Если SSH еще не готов, сбор фактов упадет до того, как задача wait_for_connection успеет выполниться. Отключив автоматический сбор фактов, мы даем модулю ожидания отработать корректно. После того как SSH станет доступен, факты можно собрать явно с помощью модуля setup в следующем шаге.

Построение моста между декларативным описанием ресурсов и их конфигурацией требует отказа от жестких связей. Переход от статических файлов и парсинга локальных стейтов к динамическому опросу облачных API через метки делает инфраструктурный конвейер устойчивым к изменениям среды. Terraform формирует ландшафт и расставляет указатели, а Ansible, ориентируясь по этим указателям, приводит внутреннее состояние систем к требуемому эталону, корректно обрабатывая временные задержки инициализации машин.