1. Связующее звено: Интеграция Terraform и Ansible через динамический Inventory
Связующее звено: Интеграция Terraform и Ansible через динамический Inventory
Команда terraform apply успешно завершена. В облаке запущены десятки виртуальных машин: балансировщики, узлы базы данных, воркеры для фоновых задач. Провайдер Yandex Cloud динамически выделил им внутренние и публичные IP-адреса. Следующий шаг — настройка операционных систем и установка Docker через Ansible. Возникает фундаментальный разрыв: Terraform знает всё о созданной инфраструктуре, но не умеет настраивать ОС, а Ansible обладает мощными модулями конфигурации, но понятия не имеет, по каким IP-адресам стучаться и какие роли назначены серверам. Ручное копирование адресов в статический файл hosts.ini уничтожает саму идею автоматизации и делает невозможным использование автомасштабируемых групп или прерываемых машин.
Проблема передачи состояния между инструментами IaC (Infrastructure as Code) и инструментами управления конфигурацией — один из главных вызовов при построении CI/CD пайплайнов. Инфраструктура в облаке эфемерна. Виртуальная машина может быть пересоздана в любой момент, получив новый IP-адрес. Если Ansible опирается на жестко заданный текстовый файл, возникает рассинхронизация: конфигуратор пытается подключиться к несуществующим узлам или пропускает новые.
Для решения этой задачи применяется паттерн Dynamic Inventory (динамический инвентарь). Вместо чтения статического файла, Ansible в реальном времени опрашивает внешний источник правды, чтобы получить актуальный список хостов, их адреса и метаданные.
Существует три архитектурных подхода к интеграции Terraform и Ansible.
| Подход | Механика | Преимущества | Недостатки |
|---|---|---|---|
| Генерация файла (Push) | Terraform через local-exec и функцию templatefile() создает файл hosts.ini после развертывания ВМ. | Не требует настройки Ansible. Максимально просто в реализации. | Файл остается на диске локальной машины. При удалении ВМ файл нужно перегенерировать. Нарушается идемпотентность. |
| Облачный API (Pull) | Ansible использует плагин yandex_cloud_compute для прямого опроса API Yandex Cloud. | Ansible независим от Terraform. Всегда видит реальное состояние облака. | Требует передачи сервисных ключей облака в Ansible. Дублирование логики (Ansible должен сам понимать, какие ВМ относятся к проекту). |
| Чтение State-файла (State Parsing) | Ansible читает файл terraform.tfstate напрямую через плагин terraform. | Единый источник правды. Не нужны доступы к облаку для Ansible. Точное соответствие тому, что создал Terraform. | Требует доступа к локальному или удаленному backend'у Terraform (например, S3-бакету). |
В современной инженерной практике стандартом де-факто является чтение State-файла. Файл состояния Terraform (.tfstate) — это подробный JSON-документ. В нем зафиксированы не только IP-адреса, но и все атрибуты ресурсов: идентификаторы, зоны доступности, размеры дисков и, что самое важное, пользовательские метки (labels) и теги.
!Схема интеграции: Terraform State как источник данных для Ansible
Настройка плагина Terraform Inventory
Для того чтобы Ansible научился понимать формат файла состояния Terraform, используется официальная коллекция community.general. Внутри нее находится плагин terraform.
Процесс интеграции начинается с создания конфигурационного файла инвентаря в формате YAML (например, inventory.terraform.yml). Расширение .yml и структура файла указывают Ansible, что это не статический список, а инструкция для вызова плагина.
Базовая конфигурация файла выглядит следующим образом:
Директива project_path указывает на директорию, где находится инициализированный проект Terraform (там, где лежат файлы .tf и скрытая папка .terraform). Плагин самостоятельно найдет локальный terraform.tfstate или обратится к удаленному S3-бакету, если в коде Terraform настроен backend.
Когда мы запускаем команду ansible-inventory -i inventory.terraform.yml --graph, плагин парсит JSON-дерево состояния. Он ищет все ресурсы, которые похожи на вычислительные узлы (например, yandex_compute_instance), извлекает из них атрибут network_interface.0.nat_ip_address (или внутренний IP, если работа идет в закрытом контуре) и формирует плоский список хостов. Имена хостов в Ansible будут соответствовать именам ресурсов в HCL-коде.
Однако просто получить список IP-адресов недостаточно. В реальном проекте инфраструктура гетерогенна. Базе данных PostgreSQL нужны одни настройки (тюнинг ядра, установка пакетов БД), а веб-серверу Nginx — совершенно другие. Если Ansible применит плейбук для БД к веб-серверу, система выйдет из строя.
Маршрутизация конфигураций: Динамическая группировка
В статическом инвентаре проблема решается секциями [webservers] и [dbservers]. В динамическом инвентаре группы должны формироваться алгоритмически на основе метаданных, заложенных на этапе описания инфраструктуры в Terraform.
Лучшая практика — использование меток (labels) на уровне облачного провайдера. В Yandex Cloud ресурс виртуальной машины поддерживает блок labels.
Опишем две машины в Terraform:
Теперь необходимо научить Ansible-плагин читать эти метки и распределять хосты по группам. Для этого в inventory.terraform.yml добавляется мощный механизм keyed_groups (группировка по ключу).
При парсинге State-файла плагин извлечет значения labels.role и создаст группы с заданным префиксом. Виртуальная машина frontend-proxy автоматически попадет в группы role_web и env_production. Машина backend-db окажется в role_database и env_production.
!Механика динамической группировки хостов при изменении тегов
В Ansible-плейбуках теперь можно безопасно обращаться к этим группам:
Если завтра нагрузка на веб-серверы возрастет, и через Terraform будет добавлен блок count = 3 для ресурса yandex_compute_instance.nginx, новые машины автоматически получат метку role = "web". При следующем запуске Ansible плагин прочитает обновленный State-файл, увидит три новых IP-адреса, динамически добавит их в группу role_web и раскатает на них Nginx. Ни одна строка конфигурации Ansible при этом не изменится. В этом заключается истинная мощь связки декларативной инфраструктуры и динамического инвентаря.
Управление адресацией и переменными хоста
По умолчанию плагин terraform может выбрать неправильный IP-адрес для подключения. Например, если у ВМ есть и внутренний (ip_address), и публичный (nat_ip_address) адреса, Ansible должен точно знать, какой из них использовать для SSH-соединения.
Если CI/CD Runner (или компьютер инженера) находится вне облака, подключение должно идти по публичному адресу. Если Runner находится внутри той же VPC, безопаснее и быстрее использовать внутренний адрес.
Для явного указания адреса подключения используется директива compose. Она позволяет формировать переменные хоста (host variables) на лету, вычисляя их из атрибутов Terraform.
> Директива compose работает как преобразователь (маппер) данных. Слева указывается стандартная переменная Ansible (например, ansible_host), а справа — путь к данным внутри JSON-объекта конкретного ресурса в Terraform State.
В примере выше мы не только указали адрес для подключения, но и создали кастомную переменную internal_ip. Это критически важно для настройки кластерных сервисов. Например, при настройке репликации PostgreSQL ведущий узел должен знать внутренние IP-адреса ведомых узлов, чтобы разрешить им подключение в pg_hba.conf. Благодаря compose, внутренний IP каждой машины всегда доступен в плейбуках Ansible в виде переменной.
Проблема гонки состояний (Race Condition)
При интеграции Terraform и Ansible в единый автоматизированный контур (например, в GitLab CI) возникает классическая проблема синхронизации по времени.
Пайплайн выглядит так:
terraform apply.RUNNING.ansible-playbook.Connection refused или Timeout.Причина кроется в том, что статус RUNNING в облаке означает лишь то, что виртуальная машина включена. Операционная система внутри нее еще загружается. Более того, отрабатывает процесс Cloud-Init, который генерирует SSH-ключи, создает пользователей и обновляет системные пакеты. На этот процесс может уйти от 30 секунд до нескольких минут. Ansible пытается подключиться к серверу, на котором демон sshd еще не запущен.
Решение этой проблемы лежит на стороне Ansible. В начало корневого плейбука добавляется специальная задача предварительной проверки доступности, использующая модуль wait_for_connection.
Модуль wait_for_connection не выполняет никаких команд на удаленном сервере. Он циклично, с заданным интервалом, пытается установить TCP-соединение на порт 22 (SSH). Как только соединение успешно установлено, задача помечается как выполненная, и Ansible переходит к полноценной настройке серверов, включая сбор системных фактов (gather_facts).
Интеграция через чтение State-файла превращает Terraform и Ansible из двух разрозненных инструментов в единый конвейер. Terraform берет на себя ответственность за физическое (виртуальное) наличие серверов, сетей и их маркировку. Ansible, выступая в роли умного потребителя этих данных, автоматически адаптирует свои действия под текущий ландшафт. Отсутствие промежуточных ручных шагов и статических файлов устраняет человеческий фактор и делает инфраструктуру по-настоящему масштабируемой и предсказуемой.