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, ориентируясь по этим указателям, приводит внутреннее состояние систем к требуемому эталону, корректно обрабатывая временные задержки инициализации машин.