Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Terraform: модули и удалённый state

Terraform-модули и remote state: переиспользование без хаоса

Как организовать Terraform-код: свои модули и их версии, remote backend с блокировкой, разделение state по окружениям, import и работа в команде.

Прямой ответ

Базовый Terraform упирается в два предела: код дублируется между окружениями/сервисами (dev/stage/prod — три копии main.tf с расхождениями — дрейф неизбежен) и локальный state-файл (terraform.tfstate — «инфраструктура в файле на одной машине» — команда не работает вместе, файл с секретами/идентификаторами в git не положишь). Решения — модули и remote state. Модуль — каталог с .tf-файлами, переиспользуемый блок с входами (variables) и выходами (outputs): module "web" { source = "./modules/service" instance_type = var.type env = "prod" } — один модуль «сервис» разворачивается N раз с разными параметрами; источники — локальные пути (внутри проекта), git (source = "git::" — ref обязателен — без него каждый apply тянет HEAD — «модуль обновился сам» — неконтролируемые изменения инфраструктуры), registry-модули (готовые — версия пиннится так же). Remote state: backend хранит state вне машины (S3+DynamoDB-блокировка / GCS / Terraform Cloud / HTTP — облачный нейтралитет — OpenTofu-совместимые варианты те же): terraform { backend "s3" { bucket=... key="prod/web.tfstate" dynamodb_table="tf-lock" } } — state общий, блокировка предотвращает параллельные apply (два инженера одновременно — порча state — DynamoDB-lock/Cloud-locking обязательны), версионирование бакета (state перезаписан ошибочно — откат на предыдущую версию — страховка дёшева). Разделение state: один state на окружение-слой (prod-web, prod-db, stage-web — изоляция взрыва (ошибочный destroy задевает свой слой), права (dev-инженеры пишут stage-state, prod — через CI с approval — пайплайн), разная частота изменений (БД-слой трогается редко — веб — часто)); cross-state ссылки — terraform_remote_state data (output одного state читается другим — IP базы в веб-модуль — без дублирования). И правило команды: apply — только из CI (локально — plan/отладка; «apply с ноутбука» — state-гонки и «а кто менял?» — без ответа — аудит через PR + CI-логи).

Модули: структура, версионирование, границы

Анатомия качественного модуля: variables.tf (входы с типами/описаниями/дефолтами — модуль = API — документированность входов = удобство), outputs.tf (что модуль отдаёт наружу — id/URL/IP — потребители не лезут внутрь), main.tf (ресурсы), README + examples (модуль без примера — модуль с поддержкой-вопросами); версия — git-теги (ref=v1.2.0 — semver для инфраструктуры: breaking-изменение модуля = мажор — потребители обновляются осознанно, не «при apply само»); границы модуля — по изменяемости и ответственности (модуль «сервис» — то, что разворачивается вместе и меняется вместе; «модуль на каждый ресурс» — фрагментация без пользы; «один модуль на всё» — монолит — не модуль); входы — достаточные, не всеобъемлющие (переменная на каждый ресурсный параметр — модуль-прокси облачного API — бесполезная прослойка; параметризуется то, что различается между использованиями); монорепо против реестра модулей (5–10 модулей — подкаталоги проекта (монорепо-логика); десятки потребителей/команд — отдельный репозиторий модулей с тегами/CHANGELOG — публикация как контракт); антипаттерны: модуль с зашитыми значениями («переиспользование» одного окружения — копипаста с именем), count внутри модуля без for_each-ясности (инстансы без имён — удаление среднего сдвигает индексы — пересоздание ресурсов — for_each по ключам стабильнее), state внутри модуля (модули state не имеют — state у корневого модуля — вложенные бэкенды не существуют — частая путаница).

State: бэкенды, блокировка, операции

State-файл — карта «ресурсы в коде ↔ реальные ресурсы» (id/атрибуты/зависимости + чувствительные значения в открытом виде — пароли ресурсов лежат в state — доступ к state = доступ к секретам — шифрование бакета + права — секрет-дисциплина); операции: terraform state list/show/mv/rm (перенос/исключение ресурсов без пересоздания — рефакторинг кода — mv сохраняет реальный ресурс; rm — «управляй сам, не удаляя» — вывод из-под управления), terraform import (существующий ресурс под управление — миграция легаси — в 1.5+ import-блоки в коде (декларативный импорт — планируем/ревьюим — CLI-import «на живую» уступает), terraform force-unlock (блокировка зависла после убитого apply — разблокировать с проверкой, что apply не жив — «сломал замок, второй apply пошёл параллельно» — хуже зависания); план как контракт — plan в PR комментарием (CI-джоб — что изменится — ревью инфраструктуры читает diff плана, не только diff кода — «+3 ресурса, ~1 изменение, −0» — сводка против сотен строк — ревью-практика); refresh-семантика (plan сверяет код с реальностью — «кто-то изменил ресурс руками» — drift виден в плане — IaC-дисциплина обнаруживает нарушения собой же); state-миграции (локальный → remote — terraform init -migrate-state; между бакетами — cp+reconfigure; при смене структуры ключей — mv внутри remote — операции редкие, но знать маршрут — иначе «начали с нуля и пересоздали прод»).

Практический сценарий: от копипасты трёх окружений к модулям

Исходник: проект — три каталога (dev/stage/prod) с копиями ~600 строк .tf; расхождения накопились (в prod забыт один security-group-тег, в dev лишний инстанс «на время» живёт полгода); state — локальные файлы у двух инженеров («у меня применилось, у тебя нет» — классика); apply — руками с ноутбуков. Рефакторинг (неделя, без изменений инфраструктуры — цель — ноль diff в плане): (1) remote state: S3-бакет с версионированием+шифрованием, DynamoDB-lock таблица, ключи env/слой.tfstate (dev/app, stage/app, prod/app, prod/db — БД вынесена в отдельный state (меняется редко, права уже, destroy-радиус меньше); migration — init -migrate-state по окружениям; доступ — dev-инженеры: dev/stage, prod — только CI-роль (Actions-секреты — OIDC-роль без долгоживущих ключей); (2) модули: выделены modules/app-service (инстанс+SG+DNS-запись — вход: env/размер/домен), modules/postgres (инстанс+параметры+бэкап-политика), modules/networking (VPC/подсети — общий на окружение); окружения — тонкие корни (env/prod/main.tf — вызовы модулей с прод-значениями — 60 строк вместо 600 — различия окружений — в variables, не в коде — конфиг-принцип в инфраструктурном виде); (3) проверка «нулевого изменения» — план по каждому окружению после рефакторинга — пуст (ресурсы не пересоздаются — mv-операции для переехавших в модули — state-mv заранее — самый нервный шаг, прошедший гладко благодаря репетиции на dev); (4) CI: PR-джоб — fmt+validate+plan (комментарий-сводка в PR), apply — отдельный workflow с environment-approval на prod (ручное подтверждение — деплой-дисциплина для инфраструктуры); локальные apply запрещены политикой (IAM — инженеры read+plan, apply-роль только у CI); (5) версии модулей — теги v1.0.0+ (обновление модуля — PR с планом — «инфраструктура меняется через ревью» — впервые за жизнь проекта); находки по пути: «временный» dev-инстанс удалён (план показал — полгода жил неучтённым), security-тег в prod добавлен (дрейф закрыт — план-ревью нашёл бы и раньше — процесс важнее разовой уборки). Итог: три копии → один модуль+три тонких корня, state общий с блокировкой и историей версий, изменения инфраструктуры — через PR с планом и approval, «у меня/у тебя применилось» исчезло как явление; время на новое окружение (staging для клиента) — 2 дня вместо недели копипасты (новый корень + переменные — модули готовы).

FAQ

Модули против workspace'ов для окружений? Workspaces (terraform workspace) — несколько state в одном бэкенде/коде (dev/prod одним root-модулем — switching контекстов); практика осторожна: один код на все окружения (прод-отличия — if'ы по workspace-имени — «if env == prod» в инфраструктуре — источник ошибок), переключение неочевидно (workspace — скрытый контекст — apply не в тот workspace — классика аварий), права не разделить (один бэкенд); рекомендация сообщества — отдельные каталоги-корни на окружение с общим state-ключом (явность дороже компактности — сценарий выше); workspaces оправданы внутри одного окружения (варианты клиента/региона) — не для dev/prod-разделения.

Как хранить секреты в Terraform? Не в коде (git — навсегда — секрет-дисциплина) и помнить: что попало в ресурс — попало в state (пароль БД в state открытым — шифрование бакета+права — минимум); подходы: переменные с sensitive=true (маска в логах/плане — не шифрование, но гигиена), внешний источник (data vault/aws_secretsmanager — секрет создаётся/берётся вне tf, в state — ссылка, не значение — Vault), генерация ресурсом (random_password — managed-секрет — state хранит — приемлемо при защищённом state); CI — секреты из environment-хранилища (OIDC/секреты раннера — не env-файлы в репо); «пароль в tfvars в git» — инцидент с ротацией и чисткой истории — лучше не проверять.

Что делать при конфликте/порче state? Симптомы: «resource already exists» при apply (ресурс создан вне tf/предыдущий apply умер наполовину), state не соответствует реальности (drift), блокировка не снимается; по шагам: план внимательно (что tf считает и что видит), import для существующего (под управление, не пересоздавать), state rm + import для «раздвоившихся», force-unlock после проверки отсутствия живого apply, восстановление state из версии бакета (versioning — последняя здоровая версия — причина порчи обычно в параллельном apply — блокировка была отключена/отсутствовала — восстановить и включить); профилактика: блокировка всегда, apply из одного места (CI), версионирование state, план перед apply — всегда (terraform apply без просмотра плана — русская рулетка — auto-approve только для тривиального).

Нужны ли модули, если инфраструктура маленькая (5 ресурсов)? Нет — преждевременные модули дороже копипасты (прослойка variables/outputs на 5 ресурсов — церемония без выигрыша — один main.tf читается проще); сигналы «пора»: третье использование одного набора ресурсов (dev/stage/prod — третий — правило трёх), несколько сервисов одного профиля (микросервисы — каждый = вызов модуля), команда больше 2 человек (общий контракт важнее краткости); порядок взросления: плоский код → вынос повторяющегося в локальные модули → общие модули с версиями (при нескольких проектах) — каждый шаг по фактической боли, не «потому что best practice» (IaC-эволюция — там же старт).

Источники: документация Terraform (modules, backends, state locking) — ссылки в шапке.