Раздел 5 · К собеседованию — глава 5.1

Terraform и Ansible: практический минимум

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

§1Два инструмента — две границы ответственности

Terraform Ansible ─────────────────────────────────── ───────────────────────────────── API облака / Grafana / VMware SSH / WinRM / API создать сеть, VM, DNS, dashboard установить пакет, положить конфиг ведёт state соответствий state ресурсов не хранит строит граф зависимостей выполняет tasks по порядку удалит ресурс, убранный из кода убранная task не откатит прошлое

Типичная связка: Terraform создаёт VM, сеть и DNS; Ansible устанавливает на VM пакеты и настраивает сервис. В контейнерной среде конфигурацию приложения чаще запекают в образ и выкатывают оркестратором, поэтому Ansible остаётся для ОС, сетевых устройств, legacy и внешних компонентов.

Грабли

Не превращайте Terraform в Ansible через длинные local-exec/remote-exec provisioners. Terraform плохо понимает, что произошло внутри скрипта, не умеет нормально построить зависимости и повторить только нужную часть. Provisioner — крайний мост, а не основной способ настройки.

§2Модель Terraform: конфигурация, provider, state и реальность

В конфигурации описано желаемое состояние. Provider умеет читать и менять конкретный API. State связывает адрес ресурса в коде, например grafana_folder.platform, с настоящим ID во внешней системе. Без этой связи Terraform не знает, какой именно объект уже принадлежит ему.

*.tf: желаемое состояние │ ▼ Terraform + provider ───────→ API реальной системы ▲ │ │ │ refresh └──── terraform.tfstate ──┘ соответствие адрес ↔ remote ID

Terraform не является долгоживущим контроллером: между запусками он ничего не исправляет. Drift обнаруживается при следующем plan, когда provider прочитает реальность и сравнит её с кодом и state.

§3Рабочий цикл: init → plan → apply

$ terraform fmt -check        # единый формат
$ terraform init             # backend, providers, modules
$ terraform validate         # синтаксис и внутренняя согласованность
$ terraform plan -out=tfplan # сохранить точный план
$ terraform show tfplan      # прочитать перед применением
$ terraform apply tfplan     # применить именно просмотренный план

Главная строка плана — не формальность: 3 to add, 1 to change, 7 to destroy. Если ожидали одну безопасную правку, а Terraform хочет заменить половину окружения, применение останавливают и выясняют причину.

В CI обычно разделяют speculative plan для merge request и финальный план перед prod apply. Сохранённый plan применяют только в том же контексте и хранят как чувствительный артефакт: он может содержать секретные значения.

§4State: почему его нельзя потерять или редактировать руками

State — не просто кеш. В нём адреса ресурсов, их remote ID, зависимости и известные атрибуты. Он может содержать секреты в открытом виде, даже если вывод помечен sensitive. Поэтому командная работа требует удалённого backend, шифрования, контроля доступа, версионирования и блокировки от параллельного apply.

Типовые операции:

  • terraform import — связать уже существующий объект с адресом в конфигурации;
  • блок moved — переименовать или перенести ресурс в модуль без уничтожения;
  • terraform state list/show — безопасно посмотреть соответствия;
  • terraform plan -refresh-only — принять намеренное внешнее изменение в state без возврата конфигурации;
  • lifecycle { prevent_destroy = true } — дополнительный предохранитель для критичного ресурса, но не замена чтению плана.

Спросят

«Кто-то поменял ресурс руками. Что сделает Terraform?» — На следующем plan provider увидит drift. Если код не изменили, обычный apply постарается вернуть объект к описанному состоянию. Если ручная правка правильная, её переносят в код или осознанно синхронизируют state; оставлять разницу как постоянную норму опасно.

§5HCL без магии: blocks, expressions, for_each и modules

variable "folders" {
  type    = set(string)
  default = ["Platform", "Applications"]
}

resource "grafana_folder" "this" {
  for_each = var.folders
  title    = each.value
}

output "folder_ids" {
  value = { for name, folder in grafana_folder.this : name => folder.id }
}

Resource управляет объектом. Data source только читает существующий объект. Variable — вход, local — вычисленное внутреннее значение, output — результат root module. Module — обычный каталог Terraform-кода с явными inputs и outputs, а не особый бинарный пакет.

for_each обычно безопаснее count для именованных объектов. Если список ["dev", "prod"] с count вставить элемент в начало, индексы сдвинутся и Terraform может запланировать лишние замены. У for_each адрес привязан к стабильному ключу: resource.this["prod"].

Минимальная структура без преждевременной архитектуры: отдельный root module на окружение или самостоятельную область изменений, удалённый state на эту область и несколько небольших переиспользуемых modules только там, где реально повторяется одна модель.

§6Модель Ansible: inventory → play → tasks → modules

Ansible обычно работает без агента: control node подключается к хостам по SSH, собирает факты и вызывает modules. Inventory отвечает «какие хосты и группы», play — «на каких хостах и с какими правами», tasks — «какое состояние обеспечить».

# inventory.yml
all:
  children:
    web:
      hosts:
        web-01:
          ansible_host: 10.20.0.11
        web-02:
          ansible_host: 10.20.0.12

Порядок важен, но хороший playbook описывает состояние: пакет установлен, файл имеет нужное содержимое, сервис включён и запущен. Повторный запуск без внешних изменений должен вернуть changed=0.

§7Идемпотентный playbook и handlers

- name: Configure web servers
  hosts: web
  become: true

  tasks:
    - name: Install nginx
      ansible.builtin.package:
        name: nginx
        state: present

    - name: Render nginx config
      ansible.builtin.template:
        src: nginx.conf.j2
        dest: /etc/nginx/nginx.conf
        owner: root
        group: root
        mode: "0644"
        validate: "nginx -t -c %s"
      notify: Reload nginx

    - name: Keep nginx enabled and running
      ansible.builtin.service:
        name: nginx
        enabled: true
        state: started

  handlers:
    - name: Reload nginx
      ansible.builtin.service:
        name: nginx
        state: reloaded

Module сначала проверяет состояние и меняет только при необходимости. Handler запускается только если task сообщил changed, и один раз в конце play, даже если его уведомили несколько tasks. Это защищает от бессмысленного reload при каждом запуске.

shell и command не запрещены, но они сами не знают, было ли состояние уже достигнуто. Если готового module нет, нужно явно задать creates, removes, changed_when и failed_when. Иначе каждый запуск «что-то меняет», а ошибки легко маскируются.

§8Variables, roles и секреты

Переменные приходят из inventory, group_vars, host_vars, defaults/vars роли, playbook и -e. Полную таблицу приоритетов зубрить не надо; надо понимать риск: одно имя, определённое в пяти местах, превращает результат в загадку. Держите значение как можно ближе к уровню, которому оно принадлежит, и не переопределяйте без причины.

Role собирает повторяемый компонент: tasks, handlers, templates, files, defaults. Role оправдана, если nginx или node exporter настраивается одинаково в нескольких playbooks. Один короткий playbook не нужно немедленно раскладывать на десять ролей.

Ansible Vault шифрует данные в репозитории, но пароль расшифрования всё равно нужно безопасно доставить runner-у. Не выводите секреты в лог: для чувствительной task ставят no_log: true, ограничивают доступ к CI credentials и проверяют, что template имеет строгие права.

§9Безопасный запуск и диагностика

$ ansible-inventory -i inventory.yml --graph
$ ansible web -i inventory.yml -m ansible.builtin.ping
$ ansible-playbook -i inventory.yml site.yml --syntax-check
$ ansible-playbook -i inventory.yml site.yml --check --diff
$ ansible-playbook -i inventory.yml site.yml --limit web-01
$ ansible-playbook -i inventory.yml site.yml -vv

--check полезен, но не гарантирует точный результат: не каждый module полностью поддерживает dry-run, а зарегистрированное значение из пропущенной task может повлиять на следующие. Для рискованного изменения сначала ограничивают один тестовый хост через --limit или используют serial, затем расширяют волну.

Проверка качества простая: запустить playbook дважды. Первый меняет ожидаемое, второй должен быть зелёным с changed=0. Затем намеренно испортить управляемый файл и убедиться, что следующий запуск исправил drift и вызвал handler ровно один раз.

§10Маршрут на шесть коротких практик

  1. Terraform: создать два простых объекта через for_each, посмотреть plan и state.
  2. Drift: изменить один объект руками, увидеть разницу и вернуть кодом.
  3. Import: создать объект вне Terraform и аккуратно взять под управление.
  4. Ansible: установить nginx на тестовую VM, положить config через template и handler.
  5. Идемпотентность: второй запуск без изменений должен показать changed=0.
  6. Совместно: Terraform создаёт тестовую VM, inventory получает её адрес, Ansible настраивает сервис. Удаление — через Terraform после проверки плана.

Критерий «уже могу сказать, что знаю базу»

Вы можете без подсказки объяснить state, drift и saved plan; прочитать простой HCL с for_each; написать playbook из package/template/service; добиться идемпотентного второго запуска; безопасно ограничить применение одним тестовым объектом и объяснить, как откатите ошибку.

§11Как это звучит в ответе

«Terraform я использую для управления жизненным циклом объектов через provider API. Он строит граф, сопоставляет конфигурацию с реальными объектами через state и в plan показывает переход к желаемому состоянию. В команде state храню удалённо с блокировкой и ограниченным доступом, применяю просмотренный saved plan и отдельно слежу за destroy/replace.

Ansible использую для конфигурации существующих хостов: inventory определяет цели, playbook вызывает идемпотентные modules, а handlers перезапускают сервис только при реальном изменении. Перед широкой раскаткой делаю check/diff, ограничиваю тестовый хост и обязательно проверяю второй запуск с changed=0. Terraform отвечает за существование ресурса, Ansible — за его внутреннее состояние; смешивать их через длинные provisioner-скрипты стараюсь не делать.»