School

← To the program

Инфраструктура как код: цели, декларативная модель, состояние

Lesson from the course “Курс подготовки DevOps-инженеров”

Хронометраж: занятие 60 минут — теория 30 минут, практика 30 минут. Домашняя работа: 1–2 часа.

Кратко о чём урок: Зачем IaC, декларативный подход против императивного, роль state как связи между кодом и реальностью, место Terraform рядом с Ansible и CI.


1. Что изучаем

После урока вы сможете:

  • объяснять, зачем инфраструктуру описывают кодом, и чем это лучше кликов в консоли и разовых команд;
  • отличать декларативный подход от императивного и показывать сходимость к описанному состоянию;
  • объяснять роль state как связи между кодом и реальностью и что происходит при их расхождении;
  • называть место Terraform рядом с Ansible (блок 10) и CI (блок 09);
  • запускать первый цикл init → plan → apply → destroy на модуле, а не на плоском наборе файлов.

2. Зачем это нужно и где применяется

Весь блок 11 вы создавали ресурсы командами yc — по одной, руками. Это работает для опыта, но не для эксплуатации: команды не повторить одинаково, не проверить на ревью, не откатить. Инфраструктура как код (IaC) описывает желаемое состояние текстом, который лежит в git, проходит ревью и применяется одинаково — как приложение, только вместо кода сервиса тут код инфраструктуры.

Ситуации, из-за которых существует этот урок:

  • «Развернули руками, повторить не можем» — шаги делались в консоли, записи нет;
  • «Прод и стейдж разъехались» — настраивали по отдельности, дрейф никто не видел;
  • «Изменение снесло рабочий ресурс» — не было плана, который показал бы это заранее;
  • «Кто и что менял в инфраструктуре — неизвестно» — нет истории в git и ревью;
  • «Ansible уже есть, зачем ещё Terraform» — не различили провижининг ресурсов и настройку внутри.

3. Теория (30 минут)

Файлы для работы: blocks/12-iac-terraform/examples/12.01-iac-concepts/

Стенд: Terraform 1.5.7 на вашей машине, провайдер kreuzwerker/docker 4.6.0 через зеркало Yandex Cloud (устройство стенда — в examples/docker/README.md). Подложка — локальный Docker: механику Terraform учим бесплатно, реальный Yandex Cloud будет в уроке 12.09. Всё проверено запуском.

3.1. Императивно против декларативно

Разница ровно та же, что в блоке 10 между bash-скриптом и Ansible (урок 10.01), только для инфраструктуры:

  • императивно — вы перечисляете шаги: «создай сеть, создай ВМ, подключи диск». Так работают команды yc из блока 11. Повторный запуск сделает всё заново или упадёт; чтобы понять текущее состояние, надо помнить, что уже сделано;
  • декларативно — вы описываете желаемое состояние: «должна быть сеть, две ВМ, диск», а инструмент сам решает, что создать, что изменить, а что оставить. Так работает Terraform.

Проверено запуском на модуле из двух веб-контейнеров. Первый apply создал четыре ресурса (образ, сеть, два контейнера). Повторный apply не сделал ничего:

$ terraform apply
No changes. Your infrastructure matches the configuration.

Это и есть декларативность: Terraform сравнил описанное с существующим и увидел, что менять нечего. Императивный скрипт на повторном запуске либо создал бы дубликаты, либо упал — здесь же «применить» безопасно запускать сколько угодно. Тот же принцип идемпотентности и сходимости, что у Ansible (10.01), только Terraform применяет его к облачным ресурсам, а не к настройке внутри машины.

3.2. Что даёт описание инфраструктуры кодом

Прежде чем разбирать механику, стоит назвать, ради чего всё это, — иначе IaC выглядит как лишний слой над понятными командами yc. Код инфраструктуры даёт ровно то, что код приложения:

  • повторяемость — тот же код разворачивает тот же стенд сколько угодно раз и на скольких угодно окружениях (dev, stage, prod из одного модуля, урок 12.06). Руками в консоли два одинаковых окружения не собрать — они разъедутся;
  • история и ревью — инфраструктура лежит в git, изменения проходят pull request и код-ревью, как в блоке 09. Видно, кто, что и зачем менял; спорное изменение отклоняют до применения;
  • план до применения — plan показывает намерение, и опасное (пересоздание, удаление) видно заранее, а не постфактум;
  • откат — вернуть предыдущую версию кода и применить проще, чем вспоминать, что и куда кликали;
  • самодокументируемость — код и есть описание того, что развёрнуто; отдельная «схема инфраструктуры» не устаревает, потому что её нет — есть сам код.

Цена — это ещё один инструмент и ещё один артефакт (state), который надо беречь. Для одноразового эксперимента команды yc из блока 11 быстрее; для того, что живёт и меняется, окупается IaC. Тот же размен, что между разовым bash-скриптом и Ansible в блоке 10.

3.3. State: связь между кодом и реальностью

Откуда Terraform знает, что менять нечего? Из state — файла состояния, где записано соответствие между ресурсами в коде и настоящими ресурсами в облаке. Это третий угол треугольника: код (чего вы хотите), реальность (что есть в облаке), state (что Terraform создал и чем управляет).

Проверено: после apply в state лежат ровно те ресурсы, что описаны, и каждому сопоставлен реальный объект:

$ terraform state list
module.webstand.docker_container.web[0]
module.webstand.docker_container.web[1]
module.webstand.docker_image.web
module.webstand.docker_network.web

При plan/apply Terraform делает три сверки: читает код, читает state, опрашивает реальность — и показывает разницу. Отсюда два следствия, важных на весь блок:

  • state — источник правды о том, чем Terraform управляет. Нет записи в state — Terraform не знает о ресурсе, даже если тот существует (его надо import, урок 12.07); есть запись, а ресурса нет — Terraform его пересоздаст;
  • state ценен и опасен. В нём лежат идентификаторы, а иногда и секреты (урок 12.04); потерять его или разойтись с реальностью — источник самых неприятных аварий блока.

Первоисточник формулирует назначение state ровно в тех же терминах: «The primary purpose of Terraform state is to store bindings between objects in a remote system and resource instances declared in your configuration» — state существует именно ради связки «строка кода ↔ реальный объект», а не как побочный лог действий. Оттуда же прямой запрет на ручную правку: «Do not directly edit this file»; точечные операции со state делаются командой terraform state (list, mv, rm — подробно в уроке 12.04), а не текстовым редактором. Формат при этом самый простой — «State snapshots are stored in JSON format» — файл действительно открывается и читается как обычный текст, и в этом двойная опасность: прочитать его руками легко, а вот отредактировать и не сломать связку с реальностью — уже нет. По умолчанию, локально, state — это один файл terraform.tfstate рядом с кодом в рабочем каталоге; в примере этого урока он и лежит там же, в envs/dev/. У локального файла нет защиты от одновременного запуска: если два человека одновременно выполнят apply из одного каталога, оба прочитают один и тот же state до изменений и оба попытаются его переписать — отсюда требование удалённого backend с блокировкой для любой работы в команде (урок 12.04), а не только для больших проектов.

3.4. Сходимость и дрейф

Реальность может измениться в обход кода — кто-то поправил ресурс руками. Это дрейф (то же понятие, что в 10.01 для конфигурации), и Terraform его видит. Проверено: удалили один контейнер мимо Terraform (docker rm), и plan заметил расхождение:

$ docker rm -f l1101-dev-web-2
$ terraform plan
  # module.webstand.docker_container.web[1] will be created
Plan: 1 to add, 0 to change, 0 to destroy.

apply вернул стенд к описанному состоянию — контейнеров снова два. Это сходимость: Terraform приводит реальность к коду, а не выполняет шаги вслепую. Ключевое отличие plan от apply — план показывает намерение до изменения, и это то, что читают на ревью и что ловит опасные операции (пересоздание, удаление) заранее. В блоке 11 у команд yc такого предварительного показа не было: выполнил — и узнал результат.

3.5. Место Terraform: не вместо Ansible и CI, а рядом

Terraform не заменяет инструменты из прошлых блоков, а встаёт между ними:

  • Terraform создаёт ресурсы — сети, ВМ, диски, кластеры, managed-сервисы (провижининг). Это граница «что должно существовать»;
  • Ansible настраивает внутри — пакеты, конфиги, службы на уже созданных машинах (блок 10). Это граница «как настроено то, что существует». Разделение разобрано в 10.08 и подробно — в уроке 12.10;
  • CI применяет Terraform — план в pull request на ревью, применение по merge (блок 09). Так инфраструктура получает те же ворота качества, что и код (урок 12.08).

Один из способов запомнить границу: Terraform отвечает на вопрос «какие серверы есть», Ansible — «что на этих серверах установлено», CI — «кто и когда это применил». Пытаться настраивать ОС Terraform-ом или создавать облачные ресурсы Ansible-ом можно, но это работа не тем инструментом.

3.6. Почему модули, а не плоский набор файлов

Можно свалить все ресурсы в один каталог .tf-файлов — Terraform это выполнит. Но правильная единица организации — модуль: набор ресурсов с входами (variables), выходами (outputs) и понятной границей, который вызывают из окружений. В этом блоке инфраструктура с самого начала оформляется модулями (подробно — урок 12.05), и пример 12.01 уже устроен так:

12.01-iac-concepts/
  modules/webstand/     # модуль: variables.tf, main.tf, outputs.tf
  envs/dev/main.tf      # окружение вызывает модуль через module {}

Окружение dev не описывает контейнеры напрямую, а вызывает модуль с параметрами:

module "webstand" {
  source      = "../../modules/webstand"
  name_prefix = "l1101-dev"
  replicas    = 2
}

Почему так с первого урока: модуль переиспользуется (тот же webstand поднимет и dev, и prod с разными параметрами — урок 12.06), читается как единица, тестируется и версионируется. Плоский набор файлов на первый стенд кажется проще, но на втором окружении превращается в копипасту, а на третьем — в источник расхождений. Это тот же довод, что за роли в Ansible (урок 10.05).

3.7. Первый цикл жизни

Четыре команды, которые повторяются весь блок (подробно — урок 12.03):

  • init — скачать провайдеры (через зеркало, урок 10.08 и стенд блока) и подготовить каталог;
  • plan — показать, что Terraform собирается сделать, ничего не меняя;
  • apply — применить: привести реальность к коду;
  • destroy — удалить всё, чем управляет этот state.

Проверено, что цикл замкнут: apply создал четыре ресурса, destroy удалил четыре, контейнеров не осталось. Это и есть базовая гарантия IaC — то, что создано кодом, кодом же и убирается, без ручного дочищения (сравните с дисциплиной уборки в облаке из блока 11, где за командами yc приходилось следить yc-inventory.py).

Две детали цикла, важные с самого начала. init скачивает провайдеры через зеркало (в стенде блока — зеркало Yandex Cloud, находка 10.08): прямой доступ к реестру из региона может не работать, а провайдер yandex вдобавок большой, поэтому кэш плагинов (TF_PLUGIN_CACHE_DIR) избавляет от повторной загрузки. А destroy удаляет только то, что записано в state этого каталога — он не тронет чужие ресурсы, но и не уберёт то, что вы создали мимо Terraform. Отсюда правило: всё, что должно убираться автоматически, должно быть создано через Terraform, а не руками рядом.

Ещё одна деталь, которая объясняет, почему порядок команд init → plan → apply → destroy не нужно расписывать самому, как в bash-скрипте: внутри plan/apply Terraform строит граф зависимостей между ресурсами. Первоисточник формулирует это одной фразой: «Terraform builds a resource graph to determine resource dependencies and creates or modifies non-dependent resources in parallel». В примере этого урока сеть и образ не зависят друг от друга — оба нужны раньше контейнеров, но не друг после друга — и Terraform создаёт их одновременно, а не в порядке, в котором ресурсы написаны в main.tf. Порядок в файле — вопрос читаемости для человека, а не инструкция для Terraform: реальный порядок применения он вычисляет сам из ссылок между ресурсами (docker_container ссылается на docker_image.web.image_id и docker_network.web.name, поэтому оба должны появиться раньше). Это прямое следствие декларативности из раздела 3.1: вы описываете, что должно быть, а не в каком порядке это собирать, и граф зависимостей — тот механизм, который порядок восстанавливает.

3.8. Сводка: что показали замеры

Утверждение Что показал замер
«повторный apply что-то доделает» No changes — Terraform декларативен, сверяет состояние
«Terraform помнит ресурсы сам» помнит через state; terraform state list показывает, чем управляет
«ручную правку Terraform не заметит» plan увидел удалённый контейнер: «1 to add»
«apply выполняет шаги» apply приводит реальность к коду (сходимость), а не повторяет шаги
«модули — это для больших проектов» окружение вызывает модуль с первого стенда, иначе копипаста на втором
«созданное надо дочищать руками» destroy убрал ровно то, что создал apply

3.9. Примеры и варианты реализации

Одна задача — описать инфраструктуру так, чтобы её можно было повторить и проверить, — в трёх средах.

Среда Как решается Что нужно иметь
Локальный стенд (Docker) Terraform + провайдер docker: контейнеры и сети как код (стенд этого блока) Terraform, Docker
VPS / bare-metal Terraform + провайдеры для железа/гипервизора (libvirt) или обычные ВМ в облаке Terraform, доступ к гипервизору/облаку
Yandex Cloud Terraform + провайдер yandex: сети, ВМ, managed-сервисы как код (урок 12.09) Terraform, каталог, зеркало провайдеров

Локальный стенд. Механику Terraform (state, plan/apply, модули, дрейф, import) полностью видно на провайдере docker без единого рубля — на нём и построен блок. Понятия те же, что в облаке.

VPS / bare-metal. Terraform умеет и локальные провайдеры (libvirt для ВМ на KVM), и обычные ВМ в облаке без managed-сервисов (урок 12.09). Код тот же, меняется провайдер.

Yandex Cloud. Провайдер yandex-cloud/yandex (ставится через зеркало, урок 10.08) описывает всё, что в блоке 11 вы делали командами yc, — но кодом. Это урок 12.09.

Что выбрать. Учиться — на docker/local (бесплатно, быстро). Для реальной инфраструктуры — провайдер той среды, где она живёт. Код и подход одни, различается провайдер и цена ошибки.

4. Практика на занятии (30 минут)

Курс проходят самостоятельно, поэтому каждый шаг говорит, что ввести, зачем, что должно получиться и что делать при неудаче. Опыты — в 12.01-iac-concepts/. Нужны terraform, docker и настройка зеркала провайдеров (см. examples/docker/README.md):

export TF_CLI_CONFIG_FILE="$(git rev-parse --show-toplevel)/blocks/12-iac-terraform/examples/terraformrc"
export TF_PLUGIN_CACHE_DIR="$HOME/.terraform.d/plugin-cache"; mkdir -p "$TF_PLUGIN_CACHE_DIR"
# macOS + Docker Desktop: указать сокет
export DOCKER_HOST="unix://$HOME/.docker/run/docker.sock"
cd blocks/12-iac-terraform/examples/12.01-iac-concepts/envs/dev

Стенд — Terraform на вашей машине, провайдер kreuzwerker/docker через зеркало Yandex Cloud (без зеркала init может не найти провайдер из вашего региона, урок 10.08). Уборка — обязательный шаг 4: Terraform убирает только то, что сам создал, а не всё, что похоже на его работу.

Шаг 1 (8 минут). Первый цикл init → plan → apply → state list.

  • Что ввести:
terraform init
terraform plan     # что будет создано
terraform apply    # применить
terraform state list
  • Зачем: увидеть весь цикл разом — от скачивания провайдера до записи созданных ресурсов в state; без этого шага «state» остаётся абстракцией, а не файлом, который только что заполнился.
  • Ожидаемый результат: apply создаёт четыре ресурса (образ, сеть, два контейнера); terraform state list показывает ровно эти четыре записи — docker_image.web, docker_network.web, docker_container.web[0], docker_container.web[1].
  • Если не так: init не находит провайдер — проверьте, что TF_CLI_CONFIG_FILE указывает на файл зеркала и что переменная вообще экспортирована в этой оболочке; apply падает на подключении к Docker — проверьте DOCKER_HOST и что Docker Desktop запущен (docker ps должен отвечать, а не виснуть).
  • Ответьте: сколько ресурсов в state? Как код replicas = 2 превратился в конкретные контейнеры?

Шаг 2 (5 минут). Декларативность: повторный apply.

  • Что ввести:
terraform apply    # ещё раз, без единой правки кода
  • Зачем: убедиться, что Terraform сверяет состояние, а не выполняет шаги заново — это и есть декларативность, которую урок обещает с первой страницы.
  • Ожидаемый результат: No changes. Your infrastructure matches the configuration. — apply ничего не создаёт и не трогает.
  • Если не так: если apply что-то меняет — значит, между шагами кто-то (или вы сами) правил файлы модуля, либо реальность уже разошлась с кодом до начала опыта; сначала выполните terraform plan и прочитайте, что именно он хочет поменять.
  • Ответьте: почему второй apply говорит No changes? Чем это отличается от повторного запуска bash-скрипта?

Шаг 3 (10 минут). Дрейф и сходимость.

  • Что ввести:
docker rm -f l1101-dev-web-2   # правим реальность мимо Terraform
terraform plan                 # Terraform это видит
terraform apply                # и возвращает к описанному
  • Зачем: показать, что дрейф (правка мимо кода) не проходит незамеченным и что apply не выполняет шаги вслепую, а приводит реальность к описанному состоянию.
  • Ожидаемый результат: plan печатает # module.webstand.docker_container.web[1] will be created и Plan: 1 to add, 0 to change, 0 to destroy.; после apply контейнеров снова два.
  • Если не так: plan говорит No changes — проверьте docker ps -a, точно ли удалённый контейнер назывался l1101-dev-web-2 (имя зависит от name_prefix в envs/dev/main.tf); если имя другое, удалите правильный контейнер и повторите.
  • Ответьте: что показал plan? Как это связано с дрейфом конфигурации из урока 10.01?

Шаг 4 (5 минут). Уборка.

  • Что ввести:
terraform destroy
docker ps -a --filter name=l1101   # пусто
  • Зачем: проверить обратную гарантию IaC — то, что создано кодом, кодом же и убирается, без ручного дочищения контейнеров и сети.
  • Ожидаемый результат: destroy удаляет ровно четыре ресурса (столько же, сколько создал apply на шаге 1); docker ps -a --filter name=l1101 не находит ни одного контейнера.
  • Если не так: что-то осталось — скорее всего, ресурс был создан мимо Terraform (например, контейнер, восстановленный вручную под другим именем) и в state не попал; уберите его docker rm отдельно и запомните: destroy не тронет то, чего нет в state.

Что обсудить в конце занятия. У вас есть Ansible (блок 10) и CI (блок 09). Где в этой связке место Terraform и что он делает такого, чего не делают они? Приведите по одной задаче для каждого инструмента.

5. Домашняя работа (1–2 часа)

Задание. Опишите модулем небольшой стенд (например, три веб-контейнера в своей сети) по заготовке iac-plan.md: пройдите цикл init → plan → apply, покажите state, устройте дрейф и верните состояние, разберите, что делает Terraform, а что осталось бы Ansible и CI. Оформите инфраструктуру модулем с variables/outputs, а не плоским набором файлов.

Критерии приёмки.

  • Инфраструктура оформлена модулем (variables.tf, main.tf, outputs.tf), окружение вызывает его через module {}.
  • Показан цикл init → plan → apply, приложен вывод terraform state list.
  • Показана декларативность: повторный apply даёт No changes.
  • Устроен дрейф (правка мимо Terraform) и показано, что plan его видит, а apply возвращает состояние.
  • Для стенда названо: что делает Terraform, что делал бы Ansible, что — CI.
  • После работы terraform destroy, ресурсов не осталось.

6. Частые ошибки и подводные камни

Правят инфраструктуру руками при живом Terraform. Симптом: следующий apply откатывает ваши правки. Причина: Terraform приводит реальность к коду. Что делать: менять через код и apply, а не мимо; ручная правка — это дрейф, который Terraform сотрёт.

Потеряли или испортили state. Симптом: Terraform не видит существующие ресурсы или хочет пересоздать всё. Причина: state — источник правды о том, чем он управляет. Что делать: беречь state (урок 12.04), не редактировать руками; существующее вводить через import (12.07).

Свалили всё в плоский набор файлов. Симптом: на втором окружении копипаста, на третьем — расхождения. Причина: нет модульной границы. Что делать: с первого стенда оформлять модулем с входами и выходами (урок 12.05).

Применяют без plan. Симптом: apply снёс или пересоздал важный ресурс. Причина: не посмотрели намерение заранее. Что делать: всегда читать plan перед apply, особенно строки про destroy и replace (урок 12.03); в CI план идёт на ревью (12.08).

Пытаются настраивать ОС Terraform-ом. Симптом: сложные provisioner и хрупкие конфигурации. Причина: перепутаны провижининг и настройка. Что делать: Terraform создаёт ресурсы, Ansible настраивает внутри (урок 12.10).

Провайдер не ставится (init падает). Симптом: init не может скачать провайдер. Причина: прямой доступ к реестру из региона может не работать (10.08). Что делать: зеркало Yandex Cloud (стенд блока), кэш плагинов, чтобы качать раз.

7. Проверка усвоения

  1. Чем декларативный подход Terraform отличается от императивных команд yc из блока 11? Что делает повторный apply?
  2. Что такое state и почему это «источник правды» о том, чем Terraform управляет?
  3. Кто-то изменил ресурс руками. Как Terraform это обнаружит и что сделает при apply?
  4. Чем plan отличается от apply и почему план важен для ревью и безопасности?
  5. Где место Terraform относительно Ansible и CI? Приведите по задаче для каждого.
  6. Почему инфраструктуру оформляют модулем, а не плоским набором файлов, уже на первом стенде?
  7. Что гарантирует destroy и как это связано с дисциплиной уборки из блока 11?

8. Материалы

  • Terraform: What is Terraform — цели IaC, декларативная модель, рабочий процесс; отправная точка. На английском.
  • Terraform: State — зачем нужен state, что в нём и почему его берегут (подробно — урок 12.04). На английском.
  • Infrastructure as Code (Kief Morris) — принципы IaC поверх конкретного инструмента: почему код, дрейф, идемпотентность. На английском.
  • Yandex Cloud: начало работы с Terraform — установка, зеркало провайдеров, первый ресурс в Yandex Cloud (пригодится к уроку 12.09).
  • Блок 10 этого курса (Ansible) — декларативность и сходимость для настройки; Terraform применяет те же идеи к провижинингу ресурсов.

← Предыдущий: 11.11. Serverless: функции и контейнеры по запросу · Блок 12 · Следующий: 12.02. HCL: провайдеры, ресурсы, переменные, выводы, data-источники →

Practice materials

Scripts, cheat sheets and starter files for the lesson tasks. The lesson text is on this page.