Раздел 5 · К собеседованию — глава 5.1
Terraform и Ansible: практический минимум
Эти инструменты часто перечисляют через запятую, хотя они управляют разными слоями. Terraform создаёт и связывает ресурсы через API, опираясь на сохранённое состояние. Ansible заходит на уже существующие машины и приводит их конфигурацию к нужному виду. Для собеседования важнее понять эту модель и безопасный рабочий цикл, чем выучить десятки команд.
§1Два инструмента — две границы ответственности
Типичная связка: 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 не знает, какой именно объект уже принадлежит ему.
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Маршрут на шесть коротких практик
- Terraform: создать два простых объекта через
for_each, посмотреть plan и state. - Drift: изменить один объект руками, увидеть разницу и вернуть кодом.
- Import: создать объект вне Terraform и аккуратно взять под управление.
- Ansible: установить nginx на тестовую VM, положить config через template и handler.
- Идемпотентность: второй запуск без изменений должен показать
changed=0. - Совместно: 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-скрипты стараюсь не делать.»