✻ Урок 7.3 · Тема 7: IaC: Terraform и Ansible
Terraform: remote state, модули, окружения
Содержание урока
Зачем это нужно
Пока state (файл состояния: список того, что Terraform уже создал, урок 7.1) лежит у тебя на ноутбуке, инфраструктура принадлежит одному человеку. Это как записная книжка прораба, которую он носит в кармане: бригада не знает, какие окна уже вставлены, а если прораб потерял книжку, стройка продолжает стоить денег, но никто не понимает, что на ней построено. Два человека с двумя разными книжками начнут вставлять одно и то же окно дважды. Два одновременных apply (команда, которая вносит изменения в облако) могут к тому же испортить сам файл состояния.
Копипаста кода для dev и stage (dev это окружение для разработки и проб, stage это «репетиция» перед боевым) быстро расходится: в одном файле поправили, в другом забыли, и окружения перестают быть похожими. Из-за этого на «репетиции» всё работает, а на боевой системе нет.
Решения два. Первое: общий state в бакете (bucket: хранилище файлов в облаке, как шкаф с ячейками; разбирали в уроке 6.2) с блокировкой, чтобы одновременно писал только один человек. Второе: модуль (module), то есть готовый «кирпич» кода, из которого собираются окружения: описал сеть и ВМ один раз, вызвал дважды. Подробно оба понятия разберём в теории. Это базовый уровень командной работы с Terraform, и про него спрашивают на собеседованиях.
Шаг проекта: state «Заметок» переезжает в бакет Object Storage (так Yandex Cloud называет хранилище файлов, аналог Amazon S3), сеть и ВМ выносятся в модуль infra/terraform/modules/notes-vm, а окружение dev вызывает этот модуль.
Что нужно знать
- Урок 7.1: IaC и Terraform - план, apply, что такое state и почему он хранит секреты.
- Урок 7.2: переменные, ВМ и outputs - файлы
variables.tf,vm.tf,outputs.tf, которые мы переносим в модуль. - Урок 6.1: облачные модели и соответствие AWS - сервисные аккаунты и ключи доступа.
- Урок 6.2: ВМ, сеть, хранилище - как выглядят сеть, ВМ и группа безопасности руками.
- Урок 3.1: основы Git -
.gitignore, почему*.tfstateне коммитят.
Картина целиком
Представь небольшую строительную фирму. Раньше у прораба была одна записная книжка (local state). Теперь фирма завела общий журнал работ на проходной (remote state в бакете): любой прораб сначала читает журнал, потом работает. На журнале висит табличка «занято» (блокировка): пока один прораб делает записи, второй ждёт у двери. У журнала есть архив старых версий (версионирование бакета): если кто-то испортил страницу, её можно восстановить.
А чтобы каждая новая стройка не начиналась с нуля, у фирмы есть типовой проект (модуль): «дом с сетью, сервером и охраной». Для каждой площадки (dev, stage) берут один типовой проект, подставляют свои размеры и получают свой дом и свой журнал.
flowchart LR
T["ты или CI<br>terraform в envs/dev"] -->|"1. блокировка"| L["notes/dev/terraform.tfstate.tflock<br>табличка «занято»"]
T -->|"2. читает и пишет"| D["notes/dev/terraform.tfstate<br>журнал dev, старые версии хранятся"]
T -.->|"у stage свой ключ"| ST["notes/stage/terraform.tfstate<br>журнал stage"]
T -->|"вызывает"| M["modules/notes-vm<br>сеть, диск, группа, ВМ"]
E1["envs/dev<br>свои значения"] --> M
E2["envs/stage<br>свои значения"] --> M
Части схемы:
- remote state и блокировка - общий журнал и табличка «занято»;
- модуль - типовой проект, который вызывают из окружений;
- окружение - каталог с вызовом модуля, своим backend и своим state;
- import и
moved- два способа сказать Terraform «этот объект уже существует» и «он просто переехал на другой адрес»; - зеркало провайдеров - обходной путь, если сайт, откуда Terraform качает плагины, недоступен.
Теория
Почему локальный state не подходит для команды
Чтобы понимать, какую именно проблему решает всё остальное в уроке. По умолчанию state (terraform.tfstate) лежит рядом с кодом, на твоём диске. Для одного человека и одного ноутбука это работает.
Записная книжка прораба. Пока он один, книжки хватает. Когда на стройке три бригады, а книжка у одного в кармане, остальные работают вслепую.
В уроке 7.1 мы выяснили: Terraform сравнивает три вещи (код, state, реальность) и без state не знает, какие объекты облака «его». У локального файла три слабых места:
- Файла нет у коллеги. Он клонирует репозиторий, запускает
planи видит «нужно создать всё». Если он применит, получатся дубликаты или ошибка «имя занято». - В нём секреты. Terraform записывает атрибуты ресурсов открытым текстом, в том числе пароли и ключи (урок 7.2 показал это на пароле БД). Положить такой файл в git нельзя.
- Нет защиты от одновременной работы. Два
applyодновременно читают одну версию state и пишут разные результаты, выигрывает тот, кто записал последним, а ресурсы первого «теряются».
Пример: Аня и Борис работают с одним кодом. Аня применила изменение: в облаке появилась ВМ, в Аниной копии state появилась запись о ней. Борис на своём ноутбуке копии Аниного state не имеет. Он запускает terraform apply, Terraform сверяет код со своим (пустым) state и создаёт ВМ ещё раз. Результат: две ВМ вместо одной, счёт вырос, а если имя ВМ должно быть уникальным, apply упадёт с ошибкой конфликта.
Прикинь сам: Аня применила изменение и создала ВМ, у Бориса нет её state. Что произойдёт, если Борис запустит
applyс тем же кодом?
Terraform сверит код со своим пустым state и решит, что ВМ ещё нет. Получатся две ВМ или ошибка «имя занято»: Борис не знает о том, что уже сделала Аня.
Осторожно: «Закоммитим state в git, и у всех будет один файл». Не выйдет: секреты в истории git остаются навсегда, а сливать два разных state-файла через merge нельзя (это не текст для людей, а внутренний формат со счётчиком версий).
Главное: локальный state живёт у одного человека, хранит секреты и не защищён от двух одновременных
apply. Всё остальное в уроке решает эти три слабости.
Проверь понимание: почему нельзя положить
terraform.tfstateв git, даже если репозиторий приватный?
Ответ
Во-первых, в state значения ресурсов лежат открытым текстом, включая пароли и ключи, а круг людей с доступом к репозиторию шире, чем к секретам. Во-вторых, у git нет блокировки: два человека закоммитят разные версии, а слить state-файл merge’ем нельзя. В-третьих, история git навсегда сохранит секрет, даже если файл потом удалить.
Значит, state нужно вынести в общее место. Разберём, как это делает remote backend.
Remote backend: общий state в бакете
Нужно место без трёх слабостей выше: общее для всех, с правами доступа, с историей версий. Такое место называется удалённым хранилищем состояния (remote backend). Backend (бэкенд) это «драйвер хранения»: кусок Terraform, который знает, где и как читать и писать state. По умолчанию backend local: файл на диске.
Общий журнал на проходной вместо личной записной книжки. Журнал один, лежит в известном месте, у входа проверяют пропуск. Оговорка: если проходная закрыта (бакет недоступен), работать с Terraform нельзя вообще, поэтому бакет должен быть надёжным.
Для S3-совместимых хранилищ (Yandex Object Storage, AWS S3, MinIO) используется backend s3. Бакет (bucket) это хранилище объектов: объект это файл плюс его имя-ключ (key), например notes/dev/terraform.tfstate. Каталогов в бакете на самом деле нет: слэши в ключе это просто часть имени, а консоли рисуют из них «папки» для удобства. Порядок работы такой:
- Ты запускаешь
terraform planилиapply. - Terraform по настройкам backend идёт в бакет и скачивает объект-state.
- Считает план, при
applyвносит изменения в облако. - Записывает новую версию state обратно в бакет.
Сам Terraform ничего локально надолго не хранит: в каталоге .terraform/ лежит только запись о том, какой backend настроен. Поэтому после смены backend нужен terraform init.
Чтобы пустить Terraform в бакет, нужен ключ доступа. В S3-мире это пара: идентификатор ключа (access_key) и секрет (secret_key), как логин и пароль для программы. Terraform читает их из переменных окружения AWS_ACCESS_KEY_ID и AWS_SECRET_ACCESS_KEY, даже когда хранилище не Amazon (S3-протокол общий, поэтому названия унаследованы). Ключ выпускается для сервисного аккаунта (учётка для программы, урок 6.1) с правами только на этот бакет: утечка такого ключа не даст доступа ко всему облаку.
Пример: Блок backend из задания 2, по строкам:
bucket = "notes-tfstate-CHANGE_ME" имя бакета, где лежит state (имя уникально на весь Object Storage)
key = "notes/dev/terraform.tfstate" имя объекта внутри бакета: путь «проект / окружение / файл»
region = "ru-central1" регион; для Yandex формальность, но S3-протокол его требует
endpoints = { s3 = "https://storage.yandexcloud.net" } адрес «окошка» хранилища (у AWS не нужен)
use_lockfile = true блокировка файлом в том же бакете (следующий раздел)
skip_* = true отключить проверки, рассчитанные на Amazon (аккаунт, регионы)
Ключ notes/dev/terraform.tfstate выбран не случайно: у окружения stage будет notes/stage/terraform.tfstate, а бакет общий. Состояния разные, хотя бакет один.
Прикинь сам: Ты запускаешь
planна новом ноутбуке, где ещё нет ни одного файла state. Откуда Terraform узнает, что уже создано?
Из бакета: настройки backend лежат в коде, и Terraform сам скачает объект notes/dev/terraform.tfstate. Локальный state не нужен, нужен только init и ключ доступа к бакету.
Осторожно:
- «Backend можно параметризовать переменными». Нет: блок
backendчитается раньше, чем Terraform вычисляет переменные, поэтому имя бакета и ключ пишутся в коде буквально или передаются приinitфлагом-backend-config. - «Бакет для state создаст сам Terraform». Можно, но state этого бакета придётся хранить где-то ещё: курица и яйцо. Поэтому бакет для state создают один раз руками (или отдельным маленьким проектом), как в задании 1.
Главное: remote backend это общий журнал состояния в бакете: у каждого окружения свой ключ, а доступ даёт сервисный аккаунт только на этот бакет.
Проверь понимание: в бакете лежит объект
notes/dev/terraform.tfstate. Есть ли в бакете каталогnotes?
Ответ
Нет. В объектном хранилище есть только объекты с именами. Слэши в имени это часть ключа, а «папки» рисует интерфейс для удобства. Поэтому «создать каталог» не нужно: достаточно записать объект с нужным именем.
Общий журнал не спасает от двух людей, которые пишут в него одновременно. Для этого нужна блокировка.
Блокировка: табличка «занято»
Даже в общем журнале два прораба могут одновременно вписать разные данные в одну страницу. Блокировка (state lock) гарантирует, что в каждый момент state изменяет только один процесс.
Табличка «не беспокоить» на двери бухгалтерии: вошёл, повесил, закончил, снял. Оговорка: табличка не охранник, она работает, пока все её уважают. Если процесс умер и табличка осталась висеть, её придётся снять вручную (force-unlock).
Перед операцией, которая может изменить state, Terraform создаёт запись о блокировке, а после завершения удаляет её. Способа два:
- Старый: отдельная таблица в базе данных DynamoDB (в Yandex это YDB в режиме, совместимом с DynamoDB). Блокировка это строка в таблице. Минус: лишний ресурс, который тоже нужно создать и оплачивать.
- Новый: параметр
use_lockfile = true(появился в Terraform 1.10). Блокировка это маленький файл<ключ>.tflockрядом со state в том же бакете. Создаётся он «условной записью»: хранилище принимает объект только если такого ещё нет. Поэтому способ работает лишь там, где хранилище условную запись поддерживает. Для Yandex Object Storage не считай это очевидным, а проверь на своём бакете (задание 2).
Схема работы:
sequenceDiagram
participant A as Процесс A
participant B as бакет
participant C as Процесс B
A->>B: создать .tflock
B-->>A: ok, файла не было
Note over A: работает, apply
C->>B: создать .tflock
B-->>C: отказ, файл уже есть (Error acquiring the state lock)
A->>B: записать state
A->>B: удалить .tflock
Пример: Сообщение об ошибке из задания 2, поле за полем:
Lock Info:
ID: 3d2c6a1e-... уникальный номер блокировки, нужен для force-unlock
Path: ... какой state заблокирован
Operation: OperationTypeApply что делает владелец: apply, plan и т. д.
Who: user@laptop кто держит (пользователь и машина)
Читать это нужно как вопрос «кто и зачем?»: если Who живой человек или CI-задача, которая ещё идёт, ждёшь. Если процесс завершился аварийно (закрыли ноутбук, убили kill -9, оборвалась сеть), блокировка осталась «висеть». Тогда terraform force-unlock <ID> её снимает.
Прикинь сам: Ты запустил
applyи закрыл ноутбук, блокировка осталась. Что получит коллега и что он сделает первым?
Ошибку Error acquiring the state lock с полями ID, Who, Operation. Сначала он выяснит по Who, жив ли процесс, и только потом снимет табличку через force-unlock <ID>. Флаг -lock=false не ставят: он отключает всю защиту.
Осторожно: Блокировка берётся и на plan, не только на apply, поэтому параллельные запуски plan в CI могут мешать друг другу. Второе: force-unlock не чинит state, а только убирает табличку. Если убитый apply успел создать часть ресурсов, после разблокировки нужно прочитать plan: state мог остаться в промежуточном состоянии.
Главное: блокировка не даёт двум процессам менять state одновременно; новый способ это файл
.tflockрядом со state (use_lockfile = true), аforce-unlockснимает только зависшую табличку.
Проверь понимание: ты запустил
apply, закрыл крышку ноутбука, сеть пропала. Коллега получаетError acquiring the state lock. Что он делает первым?
Ответ
Смотрит поле Who и время, выясняет, жив ли процесс (пишет владельцу). Если владелец подтвердил, что запуск мёртв, или ответа нет и процесс явно завис, снимает блокировку terraform force-unlock <ID>, затем запускает plan и проверяет состояние. Сразу -lock=false не отключают: так теряется вся защита.
Блокировка защищает от гонки, но не от порчи данных. От порчи спасает версионирование бакета.
Версионирование бакета: кнопка «отменить»
State можно повредить: неудачная запись, ошибочное удаление, чужая команда state rm. Версионирование (versioning) бакета хранит все прошлые версии объекта, и испорченный файл можно откатить. Без него потеря state означает ручной импорт каждого ресурса.
История изменений в текстовом редакторе: случайно всё стёр, открыл предыдущую версию. Оговорка: история занимает место и стоит денег, но state маленький, так что это копейки.
При включённом версионировании перезапись объекта создаёт новую версию, а старая остаётся с собственным идентификатором версии. «Удаление» тоже не стирает данные, а ставит метку удаления поверх. Восстановить значит скопировать нужную старую версию как текущую. Включать версионирование нужно заранее: задним числом прошлые версии не появятся.
Пример: В задании 1 вывод yc storage bucket get ... | jq '{name, versioning}' показывает "versioning": "VERSIONING_ENABLED". Это и есть проверка: если там VERSIONING_DISABLED, то после первой же аварии откатываться будет не к чему.
Прикинь сам: Версионирование включили в понедельник, state испортили во вторник. Можно ли откатиться? А если включить его в среду?
Во вторник испорченный state восстановить можно: старая версия лежит в бакете. Если включить версионирование в среду, откатиться не к чему: прошлые версии задним числом не появляются.
Осторожно: Версионирование не заменяет блокировку: оно спасает от последствий, а блокировка предотвращает гонку.
Главное: версионирование включают заранее: оно хранит все прошлые версии state и не заменяет блокировку.
С общим state разобрались. Теперь займёмся кодом: как не копировать его для каждого окружения.
Соответствие AWS
| Что делаем | Yandex Cloud | AWS |
|---|---|---|
| Бакет для state | Object Storage | S3 |
| Блокировка (старый способ) | YDB (Document API) | DynamoDB |
| Ключи для backend | статический ключ сервисного аккаунта | IAM access key |
| Права на state | роль на бакет | IAM policy на бакет |
| Шифрование | ключ Yandex KMS | SSE-KMS |
| Провайдер | yandex-cloud/yandex |
hashicorp/aws |
| Backend в коде | backend "s3" с endpoints |
backend "s3" |
Backend s3 один и тот же. Отличается адрес endpoint и несколько флагов, отключающих проверки, специфичные для AWS. (IAM это система прав в AWS, аналог ролей в Yandex; KMS это сервис хранения ключей шифрования.)
Модуль: типовой проект
В уроках 7.1 и 7.2 у тебя в каталоге лежат сеть, диск, группа безопасности и ВМ. Когда понадобится stage, первая мысль: скопировать файлы. Копия начинает жить отдельно: в одну добавили правило, в другую забыли. Модуль (module) даёт одно описание, которое вызывают несколько раз с разными значениями.
Функция в программировании или типовой проект дома: чертёж один, а размеры и цвет стен выбирают при заказе. Оговорка: в отличие от функции, у модуля нет «возвращаемого значения» в привычном виде: наружу он отдаёт только то, что явно объявлено в output.
Модуль это обычный каталог с .tf файлами. Тот каталог, из которого ты запускаешь terraform, называется корневым модулем (root module), у нас это envs/dev. Из него блоком module вызывают дочерний модуль (child module):
flowchart TD
R["envs/dev: корневой модуль<br>main.tf, backend.tf, moved.tf<br>здесь запускаешь terraform"] -->|"входы: my_ip_cidr, vm_cores"| C["modules/notes-vm: дочерний модуль<br>variables.tf, network.tf, vm.tf, outputs.tf"]
C -->|"выход: public_ip"| O["module.notes_vm.public_ip<br>в envs/dev"]
Правила, которые спасают на практике:
- Модуль знает только то, что ему передали. Значения идут через
variable. Он не читает переменные вызывающего и не берёт чужой state. - Наружу выдаёт только
output. Можноmodule.notes_vm.public_ip, нельзяmodule.notes_vm.yandex_compute_instance.notes. - Провайдера настраивают в корневом модуле, а не в дочернем: у нас
versions.tfиproviders.tfостаются вenvs/dev, модуль их использует. - Выносить стоит то, что повторяется хотя бы дважды и меняется вместе. Модуль на один ресурс без логики это лишний слой.
Версию модуля фиксируют. У локального source (путь на диске) версии нет, поэтому правка модуля сразу видна всем окружениям, которые его вызывают. Для модуля из git пишут source = "git::https://.../notes-vm.git?ref=v1.2.0" (тег), для реестра Terraform version = "~> 1.2" (разбор такого ограничения был в уроке 7.1).
Пример: В модуле notes-vm объявлена переменная vm_cores со значением по умолчанию 2. Вызов:
module "notes_vm" {
source = "../../modules/notes-vm" # путь от корневого модуля до каталога модуля
vm_cores = 2 # значение для variable "vm_cores" внутри модуля
}
Внутри модуля в ресурсе написано cores = var.vm_cores, и Terraform подставит 2. Если из envs/stage вызвать тот же модуль с vm_cores = 4, получится ВМ побольше, а код один. Terraform при init нужно «установить» модуль: для локального пути это просто запоминание ссылки, для git-модуля ещё и скачивание. Поэтому после добавления блока module нужен terraform init (иначе Error: Module not installed).
Прикинь сам: В модуле
notes-vmуvm_coresзначение по умолчанию 2. Изenvs/stageвызвали его сvm_cores = 4. Сколько ядер у ВМ в stage и в dev?
В stage 4, в dev 2 (если там значение не задано). Код один, значения свои у каждого вызова.
Осторожно:
- Модуль и окружение это не одно и то же. Модуль это «как устроено», окружение это «с какими значениями и в каком state».
- «Вынес в модуль, значит, стало чище». Если модуль вызывают один раз и внутри нет логики, прибыли нет.
Главное: модуль это каталог с
.tfфайлами, который вызывают блокомmodule; он знает только то, что ему передали, и отдаёт наружу толькоoutput.
Проверь понимание: внутри модуля нужно знать
my_ip_cidr(адрес, с которого разрешён SSH). Где его объявить и как передать?
Ответ
Объявить variable "my_ip_cidr" внутри модуля (это вход модуля) и передать значением в блоке module в корневом модуле: my_ip_cidr = var.my_ip_cidr. Сам корневой модуль, в свою очередь, тоже объявляет variable "my_ip_cidr" и получает значение из terraform.tfvars. Модуль сам «наружу за переменной» не ходит.
Если перенести ресурсы в модуль, их адреса поменяются. Что при этом будет с state, разберём дальше.
Адрес ресурса и блок moved
Это самое частое место, где новички ломают инфраструктуру при рефакторинге. Terraform находит ресурс в state по его адресу. Если адрес изменился, старый ресурс для него «исчез», а новый «появился».
Почтовый адрес. Если дом перенумеровали, почтальон, который ищет старый номер, не найдёт никого и решит, что жильцов нет. Запись в книге «дом 5 теперь дом 7» объясняет, что жильцы те же. Такая запись и есть moved.
Адрес складывается из пути: тип.имя для ресурса в корневом модуле, module.<имя вызова>.тип.имя внутри модуля, ["ключ"] для экземпляров for_each (экземпляр, instance: одна из копий ресурса, которые for_each создаёт по одной на каждый ключ, как три правила группы безопасности из урока 7.2). Когда ты переносишь ресурс в модуль, к адресу добавляется префикс. plan видит: старого адреса в коде нет (значит, удалить), нового адреса в state нет (значит, создать). Для сети или ВМ это «удалить и создать заново», то есть потеря машины и её данных.
Блок moved { from = ..., to = ... } сообщает Terraform: это один и тот же объект. Блок пишут в корневом модуле, потому что он отвечает за то, как выглядит состояние его дерева. Команда terraform state mv делает то же самое императивно, прямо в state, но без следа в коде, поэтому в команде предпочитают moved.
Пример: До переноса в state лежит ресурс с адресом yandex_compute_instance.notes. После переноса файла vm.tf в модуль и вызова module "notes_vm" ресурс в коде имеет адрес module.notes_vm.yandex_compute_instance.notes.
- Без
moved:planпокажетyandex_compute_instance.notes will be destroyedиmodule.notes_vm.yandex_compute_instance.notes will be created, итогPlan: N to add, 0 to change, N to destroy. - С
moved:planпишетyandex_compute_instance.notes has moved to module.notes_vm.yandex_compute_instance.notes, итог0 to add, 0 to change, 0 to destroy.
Для ресурсов с for_each (у нас yandex_vpc_security_group_rule.ingress) блок moved пишется на весь ресурс одним блоком: Terraform сам сопоставит экземпляры ["ssh"], ["http"], ["https"].
Прикинь сам: Ты перенёс ВМ в модуль и запустил
planбез блокаmoved. Какой будет итоговая строка плана?
Что-то вроде Plan: N to add, 0 to change, N to destroy: старый адрес исчез из кода (удалить), новый не найден в state (создать). Для ВМ это потеря машины и данных.
Осторожно: Блок moved не нужно удалять сразу после apply: если у коллеги или в другом окружении state ещё со старыми адресами, блок им тоже нужен. Обычно его оставляют на несколько релизов.
Главное: Terraform ищет ресурс в state по адресу; смена адреса выглядит как «удалить и создать», а блок
movedобъясняет, что объект тот же.
Проверь понимание: ты переименовал ресурс внутри модуля с
notesнаmain. Что покажетplanи как исправить?
Ответ
Адрес поменялся, поэтому Terraform предложит удалить старый и создать новый (для ВМ это потеря машины). Исправление: блок moved { from = module.notes_vm.yandex_compute_instance.notes to = module.notes_vm.yandex_compute_instance.main }. Он сообщает, что ресурс тот же и просто сменил адрес.
Один код, много окружений: теперь решим, как их разделять, каталогами или workspaces.
Окружения: каталоги или workspaces
Окружение это полный независимый набор инфраструктуры (dev для проб, stage для репетиции, prod для настоящей работы). Нужно, чтобы эксперимент в dev не мог случайно изменить prod.
Каталоги: три отдельные комнаты с табличками на дверях, в каждой свой журнал. Workspaces: одна комната, а журналов три на одной полке, и ты сам помнишь, какой достал. Оговорка: в комнате с одной дверью проще всего ошибиться полкой.
Есть два подхода:
- Каталоги:
envs/dev/,envs/stage/. У каждого свойbackend.tfс отдельнымkeyи свой state, общий код лежит в модулях. Многословно, зато на экране видно, где ты (по пути в терминале), и права на state можно разделить: dev открыт всем, prod только CI. - Workspaces (рабочие пространства): один код и много state в одном backend, переключение командой
terraform workspace select stage. Текущий workspace нигде в коде не виден, права на бакет общие.
flowchart LR
subgraph K["Каталоги: видно по пути, где ты"]
K1["envs/dev"] --> KS1["key = notes/dev/..."]
K2["envs/stage"] --> KS2["key = notes/stage/..."]
end
subgraph W["Workspaces: видно только по workspace show"]
W0["один каталог"] --> W1["workspace dev: env:/dev/..."]
W0 --> W2["workspace stage: env:/stage/..."]
end
Пример: Ты в envs/stage и случайно запустил terraform destroy. Худшее, что произойдёт: удалится stage. Тот же промах в workspace prod без проверки terraform workspace show удалит prod, потому что каталог и код те же самые.
Прикинь сам: Ты случайно запустил
terraform destroy. В каком случае пострадает prod: если ты в каталогеenvs/stageили в workspaceprod?
В workspace prod: код и каталог те же, текущий workspace нигде не виден. В envs/stage удалился бы только stage, и это видно по пути в терминале.
Осторожно: «Workspaces это то же самое, что окружения». Workspace это только отдельный state. Различия в размерах, именах и адресах придётся задавать условиями в коде, и он быстро усложняется. Команды чаще выбирают каталоги, а workspaces оставляют для одинаковых временных копий (например, окружение на время ревью). В курсе идём через каталоги.
Главное: каталоги дают явное разделение (свой backend и свои права), workspaces дают только отдельный state; в курсе идём через каталоги.
Проверь понимание: зачем у
devиstageразныеkeyв backend, если бакет один?
Ответ
Если ключ одинаковый, оба окружения будут читать и перезаписывать один и тот же state и «увидят» ресурсы друг друга: apply в stage начнёт менять или удалять ресурсы dev. Разные ключи дают разные независимые state в одном бакете.
У окружений есть общий риск: в state лежат секреты. Посмотрим, как их защитить.
Секреты и права вокруг state
Мы перенесли state в общий бакет, значит, теперь в нём лежат секреты для всех, у кого есть доступ к бакету. Удобство общего хранилища означает и общий риск: один неверно выданный доступ открывает пароли и ключи, которые Terraform записал в state.
Общий журнал на проходной, в котором, кроме списка окон, записаны коды от сейфов. Любой, кто может читать журнал, может и открыть сейфы. Поэтому журнал держат в закрытой комнате, а не на общем столе.
Защита state строится из четырёх слоёв:
- Права на бакет. Читать и писать state может только сервисный аккаунт Terraform и небольшая группа людей (урок 6.1: чем уже права, тем дешевле ошибка). Публичный доступ к бакету выключен.
- Шифрование. Хранилище шифрует объекты «на диске» (ключ управляется облачным сервисом KMS, строка в таблице соответствия AWS выше). Это защита от утечки самих дисков провайдера, но не от человека с правом чтения бакета.
- Версионирование. Старые версии state тоже содержат старые секреты, поэтому права на них те же.
- Минимум секретов в самом state. Учти: если Terraform сам прочитал пароль (через
data-источник из хранилища секретов) или подставил его вmetadata.user-data, значение всё равно попадёт в state. Надёжнее передавать ВМ только ссылку (например, id секрета), а сам пароль получать уже на ВМ из хранилища секретов (тема 9). Новые версии Terraform (1.10 и новее) умеют ещё «эфемерные» ресурсы, а с 1.11 «write-only» аргументы: такие значения в state не записываются, но работают они только там, где их поддерживает провайдер. Пока мы передаём пароль черезTF_VAR_notes_db_password(урок 7.2), он окажется в state.
Пример: Проверка «открывается ли state без ключа». Без AWS_ACCESS_KEY_ID и AWS_SECRET_ACCESS_KEY любое обращение к бакету (terraform plan, yc storage s3api list-objects с пустым профилем) должно закончиться ошибкой доступа, а не содержимым файла. Если содержимое читается анонимно, бакет открыт всему интернету, и секреты из state считай утёкшими.
Прикинь сам: Бакет зашифрован на стороне хранилища. Может ли человек с правом чтения бакета увидеть пароль из state?
Может: хранилище расшифровывает объект для любого, кто имеет право чтения. Шифрование защищает диски провайдера, а не доступ.
Осторожно: «Бакет шифруется, значит, секреты в state в безопасности». Шифрование на стороне хранилища расшифровывается автоматически для любого, кто имеет право чтения. Защищает именно доступ, а не шифрование.
Главное: state с секретами защищают правами на бакет, отдельным узким ключом, закрытым публичным доступом и минимумом секретов внутри.
Проверь понимание: ты выдал сервисному аккаунту Terraform роль на весь каталог «чтобы точно работало». Что плохого, если его ключ утечёт?
Ответ
Злоумышленник получит доступ не только к бакету со state, но и ко всему каталогу: сможет создавать и удалять ВМ, читать данные, запускать платные ресурсы. Ключ для backend должен открывать только один бакет: тогда ущерб ограничен чтением state, а это уже повод сменить секреты в нём, но не потеря всего облака.
Бывает, что backend приходится менять. Как не потерять state при переезде, разберём дальше.
Смена backend: migrate-state и reconfigure
Backend не вечен: бакет переименовали, окружение разделили, key поправили. Terraform помнит, какой backend использовал в прошлый раз, и при несовпадении не гадает, куда ты хотел писать state. Иначе он мог бы молча начать работу с пустым state и предложить создать всё заново.
Переезд в другой офис. Перевезти архив со старого места (-migrate-state) или начать с пустых шкафов в новом (-reconfigure): решение нужно принять явно, оба варианта законны, но имеют разные последствия.
После init Terraform сохраняет настройки backend в .terraform/terraform.tfstate (служебный файл, не путай со state). Перед каждой операцией он сравнивает запись с backend.tf. Если они различаются, выдаётся Error: Backend configuration changed, и нужно запустить init с одним из флагов:
terraform init -migrate-state: скопировать state из старого места в новое. Если в новом месте уже что-то лежит, спросит, перезаписывать ли.terraform init -reconfigure: просто переключиться на новый backend, ничего не копируя и забыв прежние настройки. Годится, когда state уже перенесён руками или не нужен.
Пример: Ты поменял в backend.tf key с notes/dev/terraform.tfstate на notes/dev2/terraform.tfstate. С -reconfigure Terraform пойдёт в новое место, найдёт там пустоту и решит, что ничего не создавал: plan предложит создать всё заново. С -migrate-state он сначала скопирует state в notes/dev2/..., и plan покажет No changes. Поэтому «не помогло, добавлю -reconfigure» не универсальный совет.
Прикинь сам: Ты поменял
keyвbackend.tfи запустилinit -reconfigure. Что покажетplan?
Предложит создать всё заново: по новому ключу state пуст, а -reconfigure ничего не копирует. Нужен был -migrate-state.
Осторожно: rm -rf .terraform как лекарство от этой ошибки. Каталог удалится, init пройдёт, но state при этом не скопирован: получится то же, что -reconfigure.
Главное:
-migrate-stateкопирует state на новое место,-reconfigureпросто переключает backend; выбирай осознанно.
Проверь понимание: в каком случае безопасен
-reconfigure?
Ответ
Когда state в новом месте уже лежит (его перенесли руками или по смыслу это тот же объект, например сменился только способ подключения к тому же бакету). Если нужный state остался только в старом месте, -reconfigure даст пустую картину, и правильный флаг -migrate-state.
Иногда state приходится править напрямую. Для этого есть команды terraform state.
Ручные операции над state
Иногда state нужно поправить без изменения инфраструктуры: переименовать адрес, убрать ресурс из-под управления Terraform, посмотреть атрибуты. Для этого есть команды семейства terraform state.
Правка инвентарной книги без перестановки вещей на складе: переписать номер полки, вычеркнуть строку. Вещи остаются на месте, меняется только запись о них.
Основные команды:
terraform state listпечатает адреса всех ресурсов (мы делали это в задании 2);terraform state show <адрес>показывает атрибуты одного ресурса (осторожно: там могут быть секреты);terraform state mv <откуда> <куда>меняет адрес (то же, что блокmoved, но сразу и без следа в коде);terraform state rm <адрес>убирает ресурс из state: объект в облаке остаётся жить, но Terraform о нём «забывает»;terraform state pullпечатает весь state (мы использовали его для страховки).
Любая запись в state идёт под блокировкой, а каждая запись это новая версия объекта в бакете: откат возможен.
Пример: Нужно временно вывести диск из-под Terraform, не удаляя его. terraform state rm yandex_compute_disk.data удалит запись. Следующий plan увидит в коде ресурс yandex_compute_disk.data, а в state его нет, и предложит создать диск заново. Значит, state rm нужно сочетать с удалением блока из кода (или блоком removed в новых версиях).
Прикинь сам: Ты выполнил
terraform state rm yandex_compute_disk.data, а блок диска остался в коде. Что покажет следующийplan?
Предложит создать диск заново: в коде он есть, в state его нет. Сам диск в облаке не удалён и продолжает стоить денег.
Осторожно: «state rm удаляет ресурс». Он удаляет только запись, а сам объект в облаке продолжает работать и стоить денег. Для удаления самого ресурса нужен destroy (или убрать блок из кода и применить).
Главное:
state rmудаляет только запись,state mvменяет адрес без следа в коде, аmovedделает то же самое, но виден в Git.
Проверь понимание: чем
terraform state mvотличается от блокаmoved?
Ответ
Результат тот же: ресурс сохраняется под новым адресом без пересоздания. Но state mv правит state немедленно и вручную, и в репозитории не остаётся следа, поэтому коллеги со своими копиями кода и state могут не узнать о переносе. Блок moved лежит в коде, попадает в git и применяется при обычном plan/apply в любом окружении.
А если запись о ресурсе потеряна совсем, а сам ресурс жив, пригодится импорт.
Import и потеря state
Terraform управляет только тем, что записано в его state. Ресурс, созданный руками в консоли, или весь объект после потери state для него не существует: apply попытается создать дубликат и упадёт на конфликте имён. Нужен способ сказать «вот этот объект облака соответствует этому блоку кода».
Инвентарная книга склада. Если вещь лежит на складе, но в книге её нет, никто не знает, что она есть. Импорт это запись в книгу: «вот этот ящик с номером 123 числится как блок X».
Два пути восстановления:
- Восстановить state из версии бакета (версионирование включают заранее). Самый быстрый способ: скопировать предыдущую версию объекта как текущую.
- Привязать существующие ресурсы заново. Старая форма:
terraform import <адрес> <id>. С Terraform 1.5 можно записать блокimportв коде, и привязка произойдёт при обычномapply:
import {
to = module.notes_vm.yandex_vpc_network.notes # адрес в коде
id = "enp7example0000000000" # идентификатор объекта в облаке
}
Идентификатор берут из облака: yc vpc network list и аналогичные команды. Важно: импорт не пишет код. Он записывает в state реальные значения объекта, а описание ресурса в .tf ты создаёшь сам (с Terraform 1.5 есть помощник terraform plan -generate-config-out=файл.tf, который набрасывает заготовку, но её всё равно нужно проверять глазами; работает для блока import в корневом модуле, для ресурса внутри модуля код переноси вручную).
Пример: Ты импортировал сеть и запустил plan. Возможны два исхода:
No changes. Your infrastructure matches the configuration.- код совпадает с реальностью, работа закончена;~ update in-placeс перечнем атрибутов - код отличается от реального объекта (например, в коде нет описания, а в облаке оно есть). Правишь код, пока не получишьNo changes, либо осознанно применяешь, чтобы привести объект к коду.
Прикинь сам: После
terraform importкомандаplanпоказывает~ update in-place. Что это значит и что делать?
Код не совпадает с настоящим ресурсом: импорт записал в state реальные значения, а описание в .tf другое. Правь код, пока не получишь No changes.
Осторожно: «Import создаст ресурс». Нет, он не создаёт и не меняет ничего в облаке, только пишет строку в state. Второе: после потери state нельзя запускать apply «на пустом»: он создаст дубликаты. Сначала восстановление или импорт, затем plan.
Главное: import только пишет строку в state и ничего не создаёт и не пишет в код; после потери state сначала восстанавливают или импортируют, потом делают
plan.
Проверь понимание: после
terraform importкомандаplanпоказывает изменения. Что это значит?
Ответ
Код не совпадает с реальным ресурсом: импорт положил в state настоящие значения, а в коде другие. Правь код, пока plan не покажет No changes, либо осознанно применяй, если ресурс нужно привести к коду.
Остался вопрос воспроизводимости: откуда берутся провайдеры и как закрепить их версии.
Lock-файл, каталог .terraform и зеркало провайдеров
Провайдер (плагин, который умеет работать с API облака, урок 7.1) Terraform скачивает из интернета. Это создаёт две проблемы: версия через месяц может стать другой и сломать код, а сайт registry.terraform.io может быть недоступен (из России доступ к нему нестабилен).
Список покупок и магазин. Lock-файл это точный список «какой бренд, какой вес, контрольная пломба» (чтобы через месяц купить то же самое). Зеркало это второй магазин, где те же товары, когда первый закрыт.
Как это устроено:
.terraform.lock.hclпоявляется послеterraform init: записывает точную версию каждого провайдера и хеши (контрольные суммы) пакетов. При следующихinitTerraform берёт именно эти версии и проверяет хеши, поэтому зеркало не сможет подложить другую сборку. Файл коммитят. Обновить версии осознанно:terraform init -upgrade..terraform/это кэш: скачанные бинарники провайдеров и запись о настроенном backend. Собирается заново командойinit, в git не нужен.- Зеркало провайдеров (provider mirror) это сервер с копией пакетов. Настраивается в файле
~/.terraformrc(он относится к твоему пользователю, а не к проекту) блокомprovider_installation:network_mirrorговорит, откуда качать провайдерыyandex-cloud/*, аdirectоставляет остальные на прямую загрузку. Адрес зеркала меняется, поэтому в примере он помечен: проверь актуальный адрес в документации Yandex Cloud по Terraform.
flowchart TD
I["terraform init"] --> A[".terraform.lock.hcl<br>какая версия и какие хеши"]
I --> B["~/.terraformrc<br>откуда качать: зеркало или registry"]
A --> D["качает провайдер, сверяет хеш"]
B --> D
D --> E["кладёт в .terraform/providers/"]
Пример: Без lock-файла через месяц init поставит провайдер новее, чем у коллеги, и один из вас увидит в плане лишние изменения. С lock-файлом оба получат 0.232.0. Если lock-файл составлен на macOS, а CI работает на Linux, в нём могут не оказаться хеши для Linux: тогда помогает terraform providers lock -platform=linux_amd64 -platform=darwin_arm64, который записывает хеши для нескольких платформ сразу.
Прикинь сам: Коллега закоммитил
.terraform.lock.hcl, ты нет. Через месяц у вас разные версии провайдера. Почему?
Без lock-файла init берёт самую свежую версию в рамках ограничения из versions.tf, а у коллеги версия закреплена. План у вас покажет разные изменения.
Осторожно: Lock-файл не добавляют в .gitignore «потому что мешает»: он как раз и даёт воспроизводимость. Обратное тоже ошибка: коммитить .terraform/ (сотни мегабайт бинарников, которые и так скачаются).
Главное: lock-файл закрепляет версии и хеши провайдеров и лежит в Git;
.terraform/это кэш, его не коммитят; зеркало нужно, когда registry недоступен.
Проверь понимание: что произойдёт, если удалить
.terraform.lock.hclи запуститьinitчерез месяц?
Ответ
Terraform выберет самую новую версию провайдера, подходящую под ограничение в versions.tf (например, ~> 0.232 допускает все 0.x от 0.232 и выше), и напишет новый lock-файл. Версия может оказаться другой, чем у коллег и в CI, и plan покажет неожиданные изменения. Lock-файл нужен именно для того, чтобы версии не «плавали».
Соберём всё вместе: что происходит при одном plan в окружении с модулем и remote state.
Сквозной разбор: что происходит при plan с модулем и remote state
Соберём всё из урока в один запуск, чтобы части встали на места. Ты в envs/dev и выполняешь terraform plan.
1. init уже сделан: известен backend (бакет + ключ) и модуль ../../modules/notes-vm
2. plan берёт блокировку: создаёт notes/dev/terraform.tfstate.tflock в бакете
3. скачивает state: notes/dev/terraform.tfstate
4. применяет moved: если в state старые адреса, а в moved.tf есть блок, то
в плане они отображаются как «has moved to ...»
5. раскрывает module "notes_vm": подставляет значения в variable модуля
(my_ip_cidr, notes_db_password) и вычисляет ресурсы с адресами
module.notes_vm.<тип>.<имя>
6. refresh: спрашивает API облака, как выглядит каждый ресурс сейчас
7. сравнивает код, state и облако, печатает план
8. снимает блокировку
Если на шаге 2 получена ошибка блокировки, дальше Terraform не идёт. Если на шаге 4 нет блока moved, на шаге 7 ты увидишь пары destroy и create. Если на шаге 5 не хватает значения переменной, план остановится с No value for required variable до обращения к облаку. Умение назвать шаг, на котором сломалось, это и есть диагностика.
Прикинь сам: На каком шаге
planостановится, если в модуль не передано значение обязательной переменной, и дойдёт ли он до облака?
На шаге 5, когда раскрывается модуль: будет No value for required variable. До обращения к облаку (шаг 6) дело не дойдёт.
Главное: умение назвать шаг, на котором сломался
plan, и есть диагностика: блокировка, state,moved, модуль, refresh, сравнение.
Практика
Работаем в ~/notes/infra/terraform. Файлы versions.tf, providers.tf, network.tf, variables.tf, vm.tf, cloud-init.yaml.tftpl, outputs.tf и рабочий terraform.tfvars уже есть из уроков 7.1 и 7.2, а ресурсы из них созданы в облаке (сеть notes, подсеть notes, диск notes-data, группа notes-sg, ВМ notes-vm). Нужны Terraform, yc с настроенным профилем (урок 6.1), jq (программа для разбора JSON, урок 1.2) и SSH-ключ из урока 6.2. Если ты удалил ресурсы через terraform destroy после 7.2, сначала повтори apply из 7.2: нам нужно, чтобы в state было что переносить.
Трек без облака: вместо Object Storage поднимается MinIO (бесплатное S3-совместимое хранилище) в контейнере (версия не закреплена, проверь актуальный образ). Backend настраивается так же, меняется только endpoints.s3.
Задание 1. Бакет и ключ для state
Цель: создать бакет с версионированием и отдельный ключ, которым Terraform будет ходить в него.
Предскажи: можно ли создать бакет для state тем же Terraform? Ответ: нет, state бакета должен лежать в бакете, которого ещё нет (курица и яйцо), поэтому его создают вручную.
Шаги:
- Создай бакет.
exportкладёт значение в переменную окружения этого терминала, чтобы не набирать имя снова.yc storage bucket create --name ...просит облако создать бакет с таким именем. Имя уникально на весь Object Storage, поэтому замениCHANGE_MEсвоим суффиксом (например, инициалами и числом):
export TF_BUCKET="notes-tfstate-CHANGE_ME"
yc storage bucket create --name "$TF_BUCKET"
- Включи версионирование: оно хранит прошлые версии объектов, и при порче state есть путь назад.
yc storage bucket update --name "$TF_BUCKET" --versioning versioning-enabled
- Создай сервисный аккаунт для state. Это отдельная «учётка для программы», чтобы ключ Terraform открывал только бакет, а не всё облако:
yc iam service-account create --name notes-tf-state
Выдай ему права на бакет, а не на весь каталог. Флаг --grants задаёт разрешения на бакет, grantee-id это идентификатор аккаунта (поле id из yc iam service-account get). Точный синтаксис сверь с yc storage bucket update --help: он может меняться между версиями yc. Более грубая альтернатива: роль storage.editor на каталог, как в уроке 6.2 (проще, но шире, чем права на один бакет).
SA_ID="$(yc iam service-account get notes-tf-state --format json | jq -r .id)"
yc storage bucket update --name "$TF_BUCKET" \
--grants "grant-type=grant-type-account,grantee-id=$SA_ID,permission=permission-full-control"
Разбор: $(...) подставляет вывод внутренней команды, jq -r .id достаёт поле id из JSON (-r убирает кавычки), обратный слэш \ в конце строки означает «команда продолжается на следующей».
- Выпусти статический ключ доступа и передай его окружению. Значения не печатай и не коммить.
>перенаправляет вывод команды в файл,chmod 600оставляет право чтения только тебе (урок 1.3):
yc iam access-key create --service-account-name notes-tf-state --format json > ~/.notes-tf-key.json
chmod 600 ~/.notes-tf-key.json
export AWS_ACCESS_KEY_ID="$(jq -r .access_key.key_id ~/.notes-tf-key.json)"
export AWS_SECRET_ACCESS_KEY="$(jq -r .secret ~/.notes-tf-key.json)"
Имена AWS_* это историческое наследие: backend s3 читает ключи из них, даже когда хранилище не Amazon.
Что должно получиться:
yc storage bucket get --name "$TF_BUCKET" --format json | jq '{name, versioning}'
{
"name": "notes-tfstate-CHANGE_ME",
"versioning": "VERSIONING_ENABLED"
}
ИИ: если
ycвыдал непонятную ошибку про права или имя бакета, вставь текст ошибки нейросети (без ключей): она подскажет, чего не хватает, но имена командycсверяй сyc storage --help.
Как читать вывод: name это имя твоего бакета. versioning должен быть VERSIONING_ENABLED. Если там VERSIONING_DISABLED, шаг 2 не сработал, и откатывать испорченный state будет не к чему. Полей у yc больше, мы выбрали два через jq '{name, versioning}'.
Объясни себе:
- Почему права выдаются на бакет, а не на весь каталог?
- Чем утечка этого ключа опаснее утечки одного
terraform.tfstateбез ключей?
Типичные ошибки:
ERROR: rpc error: code = AlreadyExists desc = Bucket already exists: имя занято кем-то другим. Добавь уникальный суффикс.jq: command not found: не установленjq. Поставьsudo apt install jq(Ubuntu) илиbrew install jq(macOS).- Ключ попал в git: считай его скомпрометированным, отзови (
yc iam access-key delete) и выпусти новый.
Задание 2. Миграция state в бакет и проверка блокировки
Цель: перенести локальный state в бакет и убедиться, что второй параллельный запуск не пройдёт.
Предскажи: если в бакете по этому ключу уже лежит чужой state, что сделает terraform init -migrate-state?
Ответ
Terraform спросит, перезаписать ли существующий state локальным. Он ничего не склеивает. Ответ yes затрёт чужое состояние, поэтому читай вопрос и при сомнении отвечай no.
Шаги:
- В
infra/terraformсоздайbackend.tf. Каждая строка разобрана в теории («Remote backend»):
terraform {
backend "s3" {
bucket = "notes-tfstate-CHANGE_ME" # то же имя, что в TF_BUCKET
key = "notes/dev/terraform.tfstate"
region = "ru-central1"
endpoints = {
s3 = "https://storage.yandexcloud.net"
}
# блокировка файлом в бакете; полагайся на неё только после проверки ниже
use_lockfile = true
# проверки, специфичные для AWS, в Yandex не нужны
skip_region_validation = true
skip_credentials_validation = true
skip_requesting_account_id = true
skip_s3_checksum = true
}
}
- Перенеси state. Флаг
-migrate-stateговорит: «backend поменялся, скопируй существующий state в новое место». Ответьyesна вопрос о копировании:
terraform init -migrate-state
- Проверь, что state теперь в бакете. Первая команда просит бакет перечислить объекты,
jq -r '.contents[].key'оставляет только имена; вторая показывает, какие ресурсы знает Terraform:
yc storage s3api list-objects --bucket "$TF_BUCKET" --format json | jq -r '.contents[].key'
terraform state list
- Проверь блокировку. Если изменений нет,
applyзавершится сразу, без вопроса, и блокировку удержать не выйдет. Поэтому сначала сделай безопасное изменение: вvm.tfв ресурсyandex_compute_instance.notesдобавь меткуlabels = { env = "dev" }(метка, label, это пометка «ключ = значение» на ресурсе, работе ВМ она не мешает, а Terraform применит её на месте, без пересоздания). Потом в первом терминале запустиapplyи не отвечай на вопросEnter a value(Terraform ждёт ответа, удерживая блокировку):
terraform apply
Во втором терминале выставь те же AWS_* и TF_VAR_notes_db_password (тот же пароль, что в 7.2, иначе plan покажет изменение user-data) и запусти:
terraform plan
- В первом терминале ответь
yes: метка безвредна, а код и state снова совпадут. Повториplanво втором: теперь он проходит и пишетNo changes.
Что должно получиться: после init -migrate-state (вывод сокращён, смотри на вопрос о копировании и последние строки):
Initializing the backend...
Do you want to copy existing state to the new backend?
...
Enter a value: yes
Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.
terraform state list (порядок по алфавиту):
data.yandex_compute_image.ubuntu
yandex_compute_disk.data
yandex_compute_instance.notes
yandex_vpc_network.notes
yandex_vpc_security_group.notes
yandex_vpc_security_group_rule.egress
yandex_vpc_security_group_rule.ingress["http"]
yandex_vpc_security_group_rule.ingress["https"]
yandex_vpc_security_group_rule.ingress["ssh"]
yandex_vpc_subnet.notes
Во втором терминале, пока первый ждёт ответа:
Error: Error acquiring the state lock
Lock Info:
ID: 3d2c6a1e-7f0b-4b5e-9d3a-0c6f7c1e2a44
Path: notes-tfstate-CHANGE_ME/notes/dev/terraform.tfstate
Operation: OperationTypeApply
Who: user@laptop
Точный текст первой строки зависит от версии. Если второй plan прошёл без ошибки, хранилище не поддержало условную запись и use_lockfile тебя не защищает: запускай Terraform только из CI с очередью и не полагайся на флаг.
Как читать вывод: в выводе init главное Successfully configured the backend "s3": с этого момента state живёт в бакете. В list-objects должен быть notes/dev/terraform.tfstate (и, пока идёт apply, рядом notes/dev/terraform.tfstate.tflock: это и есть блокировка). В state list те же 10 строк, что были до переезда: если их меньше, миграция не прошла. В ошибке про блокировку смотри Who и Operation: они показывают, кто держит и что делает.
Объясни себе:
- Где физически лежит блокировка и что будет с ней, если процесс
terraformубить? - Почему
skip_*флаги нужны для Yandex, но их не нужно копировать в конфиг для настоящего AWS?
Типичные ошибки:
Error: Failed to get existing workspaces: ... InvalidAccessKeyId: в этом терминале не выставленыAWS_ACCESS_KEY_IDиAWS_SECRET_ACCESS_KEY. Экспортируй их заново.Error: Unsupported argument ... "endpoint": старый синтаксис. В Terraform 1.10 и новее пишиendpoints = { s3 = "..." }.Error: Backend initialization required, please run "terraform init": после правкиbackend.tfне выполненinit.planпоказывает~ update in-placeу ВМ иuser-data (sensitive value): в терминале другойTF_VAR_notes_db_password, не тот, с которым создавалась ВМ. Верни прежний пароль. Если его уже нет, такойapplyбезвреден для работающей машины (cloud-init повторно не запускается), но state запомнит новое значение.
Задание 3. Модуль notes-vm и окружение dev
Цель: вынести сеть, диск, группу безопасности и ВМ в модуль и вызвать его из envs/dev, не пересоздавая ресурсы.
Предскажи: если просто перенести файлы в модуль и вызвать его на том же state, что покажет plan: No changes или пересоздание? Что произошло с адресами ресурсов?
Ответ
Адреса стали вида module.notes_vm.yandex_vpc_network.notes. Для Terraform это новые ресурсы, а старые исчезли, поэтому plan предложит удалить всё и создать заново. Чтобы этого не случилось, нужны блоки moved (или terraform state mv).
Шаги:
- Сделай страховку и создай структуру.
terraform state pullпечатает текущий state,>пишет его в файл.mkdir -pсоздаёт каталоги (и не ругается, если они есть).git mvпереносит файл и сообщает git, что это перенос. Файлы с секретами (terraform.tfvars) git не отслеживает, для них обычныйmv:
cd ~/notes/infra/terraform
terraform state pull > ~/notes-state-before-module.json # страховка перед переездом
mkdir -p modules/notes-vm envs/dev
git mv network.tf variables.tf vm.tf outputs.tf cloud-init.yaml.tftpl modules/notes-vm/
git mv versions.tf providers.tf backend.tf .terraform.lock.hcl terraform.tfvars.example envs/dev/
mv terraform.tfvars envs/dev/
rm -rf .terraform tfplan terraform.tfstate*
В последней строке удаляются старый кэш .terraform/, файл плана из 7.2 и локальные остатки state: всё нужное уже лежит в бакете. Правила .gitignore для terraform.tfvars и tfplan указывали на старый путь, обнови их:
printf '%s\n' 'infra/terraform/envs/*/terraform.tfvars' 'infra/terraform/envs/*/tfplan' >> ~/notes/.gitignore
Страховочный файл содержит секреты. Когда задание закончено, удали его: rm ~/notes-state-before-module.json.
-
В
modules/notes-vmфайлы остались те же, что в 7.2, и менять в них почти ничего не нужно:variables.tfуже описывает входы (my_ip_cidr,ssh_public_key_path,image_family,vm_cores,data_disk_gb,notes_db_password), аoutputs.tfвыдаётpublic_ipиdata_disk_id. Это и есть интерфейс модуля. Шаблонcloud-init.yaml.tftplлежит рядом сvm.tf, и путь${path.module}/cloud-init.yaml.tftpl(path.moduleэто каталог текущего модуля) продолжает работать. -
Создай вызов модуля, файл
envs/dev/main.tf. Значения для входов модуля берутся из переменных корневого модуля (они объявлены ниже):
# Окружение dev вызывает типовой проект
module "notes_vm" {
source = "../../modules/notes-vm" # путь от этого каталога до модуля
my_ip_cidr = var.my_ip_cidr
notes_db_password = var.notes_db_password
}
Дочернему модулю нужен свой versions.tf: каждый модуль сам говорит, откуда брать провайдера. Без него Terraform решит, что провайдер yandex принадлежит hashicorp (по умолчанию он ищет там), не найдёт его, и init упадёт. Создай modules/notes-vm/versions.tf (версию можно не указывать: её ограничивает корень):
terraform {
required_providers {
yandex = {
source = "yandex-cloud/yandex" # провайдер не из hashicorp, адрес указываем явно
}
}
}
Файл envs/dev/variables.tf (значения придут из terraform.tfvars и TF_VAR_notes_db_password, как в 7.2):
variable "my_ip_cidr" {
type = string
description = "Внешний адрес с /32, откуда разрешён SSH"
}
variable "notes_db_password" {
type = string
sensitive = true
description = "Пароль БД: только через TF_VAR, не в файле"
}
Файл envs/dev/outputs.tf: модуль отдаёт значения наружу только через output, а корневой модуль повторяет их, чтобы terraform output работал как раньше:
output "public_ip" {
value = module.notes_vm.public_ip
}
output "data_disk_id" {
value = module.notes_vm.data_disk_id
}
- Блоки
movedлежат в корневом модуле, где менялись адреса. Файлenvs/dev/moved.tf. Блок нужен для каждого управляемого ресурса (дляdataне нужен: это чтение, а не хранимый объект). Ресурс сfor_eachпереносится одним блоком:
moved {
from = yandex_vpc_network.notes
to = module.notes_vm.yandex_vpc_network.notes
}
moved {
from = yandex_vpc_subnet.notes
to = module.notes_vm.yandex_vpc_subnet.notes
}
moved {
from = yandex_compute_disk.data
to = module.notes_vm.yandex_compute_disk.data
}
moved {
from = yandex_vpc_security_group.notes
to = module.notes_vm.yandex_vpc_security_group.notes
}
moved {
from = yandex_vpc_security_group_rule.ingress
to = module.notes_vm.yandex_vpc_security_group_rule.ingress
}
moved {
from = yandex_vpc_security_group_rule.egress
to = module.notes_vm.yandex_vpc_security_group_rule.egress
}
moved {
from = yandex_compute_instance.notes
to = module.notes_vm.yandex_compute_instance.notes
}
- Установи модуль и проверь план.
terraform initнужен, потому что появился блокmoduleи backend теперь читается из нового каталога (state в бакете тот же, ключ не изменился):
cd envs/dev
export TF_VAR_notes_db_password="<тот же пароль, что в 7.2>"
terraform init
terraform plan
- Примени проверенный план: перемещения адресов записываются в state только командой
apply(planничего не сохраняет). Перемещения не создают и не удаляют ресурсы, поэтомуapplyбезопасен, если в плане нетwill be destroyed:
terraform apply
terraform plan -detailed-exitcode; echo "код выхода: $?"
Должно быть No changes и код 0. Без apply проверка в задании 5 вернула бы код 2.
- Второе окружение делается по образцу: скопируй
envs/devвenvs/stageбез каталога.terraform(там запомнены настройки backend от dev, и после сменыkeyinitначал бы предлагать миграцию; например,cp -r envs/dev envs/stage && rm -rf envs/stage/.terraform), поменяйkeyв backend наnotes/stage/terraform.tfstate, удалиmoved.tf(в stage переносить нечего). Чтобы имена ресурсов не конфликтовали с dev, в модуле добавьvariable "name_suffix"(типstring,default = "") и замени имена в коде:"notes${var.name_suffix}"для сети и подсети,"notes-data${var.name_suffix}"для диска,"notes-sg${var.name_suffix}"для группы,"notes-vm${var.name_suffix}"вlocal.name. Вenvs/stage/main.tfпередайname_suffix = "-stage". У dev значение по умолчанию пустое, поэтому егоplanне изменится: проверь это, вернувшись вenvs/dev. В stage запусти толькоinitиplan;applyне нужен, он создал бы вторую ВМ за твои деньги. Модуль один, состояний два.
Что должно получиться: plan показывает перемещения, а не пересоздание (вывод сокращён до характерных строк):
Terraform will perform the following actions:
# yandex_vpc_network.notes has moved to module.notes_vm.yandex_vpc_network.notes
# yandex_vpc_subnet.notes has moved to module.notes_vm.yandex_vpc_subnet.notes
# yandex_compute_instance.notes has moved to module.notes_vm.yandex_compute_instance.notes
...
Plan: 0 to add, 0 to change, 0 to destroy.
ИИ: нейросеть удобно просить написать
moved.tfпо списку адресов изterraform state list, но результат всегда проверяй командойplan: ждёшь0 to destroy.
Как читать вывод: каждая строка X has moved to Y значит: Terraform понял, что это один и тот же объект, и только перепишет его адрес в state. Смотри на итог: 0 to add, 0 to destroy значит, что ничего не пересоздаётся. Если видишь will be destroyed и will be created для одной и той же ВМ или сети, значит для неё нет блока moved или в нём опечатка в адресе: не применяй, а дополни moved.tf. После apply строки про moved исчезнут, а plan будет показывать No changes.
Объясни себе:
- Почему
movedпишут в корневом модуле, а не внутри вызываемого? - Что будет с
envs/stage, если ты поменяешь код модуля? Как защититься от этого в команде?
Типичные ошибки:
Error: Module not installed: после добавления блокаmoduleне выполненterraform init.Error: Unsupported argument ... An argument named "vm_cores" is not expected here: в вызове модуля есть аргумент, для которого в модуле нетvariable.Error: Reference to undeclared module: имя в ссылке (module.notes_vm) не совпадает с именем блокаmodule.Error: Missing required argument ... "my_ip_cidr" is required: обязательная переменная модуля не передана в блокеmodule.
Задание 4. Зеркало провайдеров и lock-файл
Цель: настроить установку провайдера через зеркало и зафиксировать версии.
Предскажи: какой файл появится после init и почему его коммитят, а каталог .terraform/ нет?
Ответ
Появится .terraform.lock.hcl с версиями и хешами провайдеров. Его коммитят, чтобы у всех и в CI стояли одни и те же версии. Каталог .terraform/ это скачанные бинарники и кэш, он собирается заново и в git не нужен.
Шаги:
- Создай
~/.terraformrc(файл настроек Terraform для всего твоего пользователя). Адрес зеркала в примере условный, проверь актуальный адрес в документации Yandex Cloud:
provider_installation {
network_mirror {
url = "https://terraform-mirror.yandexcloud.net/" # проверь актуальный адрес
include = ["registry.terraform.io/yandex-cloud/*"] # это качаем с зеркала
}
direct {
exclude = ["registry.terraform.io/yandex-cloud/*"] # всё остальное качаем напрямую
}
}
- Пересобери провайдеры и проверь код без применения.
rm -rf .terraformудаляет кэш,init -upgradeзаново выбирает версии в рамках ограничений и обновляет lock-файл,fmt -recursiveприводит оформление всех.tfпод каталогом к стандарту,validateпроверяет синтаксис и ссылки без обращения к облаку:
cd ~/notes/infra/terraform/envs/dev
rm -rf .terraform
terraform init -upgrade
terraform fmt -recursive ../..
terraform validate
Что должно получиться (вывод init сокращён: версия провайдера у тебя может быть новее):
Initializing the backend...
Initializing modules...
Initializing provider plugins...
- Finding yandex-cloud/yandex versions matching "~> 0.232"...
- Installing yandex-cloud/yandex v0.232.0...
...
Terraform has been successfully initialized!
После validate:
Success! The configuration is valid.
В git status .terraform.lock.hcl виден как изменённый или новый файл: его нужно добавить в коммит.
Как читать вывод: строка Installing yandex-cloud/yandex vX показывает выбранную версию, она должна попадать под ограничение из versions.tf. Отдельной строки про зеркало Terraform не печатает: о том, что оно работает, говорит сам факт успешной загрузки. Success! The configuration is valid. значит, что код синтаксически верен, но не гарантирует, что облако его примет (это покажет plan).
Объясни себе:
- Зачем в
directисключён провайдер Yandex, а остальные качаются напрямую? - Что произойдёт, если удалить
.terraform.lock.hclи запуститьinitчерез месяц?
Типичные ошибки:
Error: Failed to query available provider packages ... no available releases match the given constraints: нужной версии провайдера нет на зеркале. Ослабь ограничение вversions.tfили смени зеркало.Error: Failed to install provider ... context deadline exceeded: registry недоступен, а зеркало не подхватилось. Проверь опечатки в~/.terraformrc.
Задание 5. Шаг проекта: инфраструктура «Заметок» в git
Цель: зафиксировать в ~/notes модуль, окружение dev и backend.
Предскажи: какие файлы из infra/terraform должны попасть в коммит, а какие нет?
Ответ
В коммит: modules/notes-vm/* (.tf и шаблон cloud-init), envs/dev/*.tf (включая backend.tf и moved.tf), terraform.tfvars.example и .terraform.lock.hcl. Не в коммит: .terraform/, *.tfstate*, рабочий terraform.tfvars с реальными значениями. Файл ключа ~/.notes-tf-key.json лежит вне репозитория.
Шаги:
- Проверь структуру.
find ... -type fперечисляет файлы,-not -path '*/.terraform/*'пропускает кэш,sortупорядочивает вывод:
cd ~/notes
find infra/terraform -type f -not -path '*/.terraform/*' | sort
- Убедись, что
devне хочет ничего менять. Флаг-detailed-exitcodeделает код выхода информативным: 0 изменений нет, 1 ошибка, 2 изменения есть.$?это код выхода последней команды:
cd infra/terraform/envs/dev
terraform plan -detailed-exitcode
echo "код выхода: $?"
- Закоммить.
git status --shortсначала покажет, что попадёт в коммит: убедись, что там нетtfvarsиtfstate:
cd ~/notes
git status --short infra .gitignore
git add infra/terraform .gitignore
git commit -m "infra: модуль notes-vm, окружение dev, state в Object Storage"
git push
Что должно получиться: список файлов (порядок и набор могут отличаться; terraform.tfvars есть на диске, но .gitignore не пустит его в коммит):
infra/terraform/envs/dev/.terraform.lock.hcl
infra/terraform/envs/dev/backend.tf
infra/terraform/envs/dev/main.tf
infra/terraform/envs/dev/moved.tf
infra/terraform/envs/dev/outputs.tf
infra/terraform/envs/dev/providers.tf
infra/terraform/envs/dev/terraform.tfvars
infra/terraform/envs/dev/terraform.tfvars.example
infra/terraform/envs/dev/variables.tf
infra/terraform/envs/dev/versions.tf
infra/terraform/modules/notes-vm/cloud-init.yaml.tftpl
infra/terraform/modules/notes-vm/network.tf
infra/terraform/modules/notes-vm/outputs.tf
infra/terraform/modules/notes-vm/variables.tf
infra/terraform/modules/notes-vm/versions.tf
infra/terraform/modules/notes-vm/vm.tf
И результат плана:
No changes. Your infrastructure matches the configuration.
код выхода: 0
Код лежит в твоём репозитории ~/notes/infra/terraform.
Как читать вывод: в списке должны быть оба каталога, envs/dev и modules/notes-vm, а в корне infra/terraform никаких .tf не осталось. No changes и код 0 значат, что переезд в модуль ничего не поменял в облаке: так и должно быть при рефакторинге. Код 2 значит, что plan хочет что-то поменять: читай план, не игнорируй. В git status --short строки начинаются с R (перенос) или A (новый файл); ?? напротив terraform.tfvars или tfstate означает, что .gitignore их не закрыл.
Объясни себе:
- Что означает код выхода 2 у
plan -detailed-exitcode? - Почему
backend.tfлежит вenvs/dev, а не в модуле?
Типичные ошибки:
fatal: pathspec 'infra/terraform' did not match any files: ты не в репозитории~/notes.- В
git statusвиденterraform.tfstateилиenvs/dev/terraform.tfvars: в.gitignoreнет нужного правила (шаблон*.tfstate*из урока 3.1, а дляtfvarsпуть, добавленный в задании 3). Добавь шаблон и убери файл из индекса:git rm --cached.
Сломай и почини
Сценарии ручные: ломаешь командами, разбор в <details>. Работай в envs/dev.
Симптом
Выбери сценарий и воспроизведи:
- Конфликт state. В одном терминале запусти
terraform applyи не отвечай, во втором сделай то же самое. Убей первый процесс черезkill -9и повториterraform plan. Error: Backend configuration changed. Поменяй вbackend.tfkeyнаnotes/dev2/terraform.tfstateи запустиterraform plan.- Потеря state. Удали объект state из бакета:
yc storage s3api delete-object --bucket "$TF_BUCKET" --key notes/dev/terraform.tfstate, затемterraform plan.
Гипотезы
- Сценарий 1: блокировка осталась от убитого процесса, либо кто-то ещё реально работает.
- Сценарий 2: state лежит в другом месте, а Terraform помнит старый backend в
.terraform/. - Сценарий 3: Terraform видит пустой state и хочет создать всё заново.
Проверки
# объекты в бакете (файл блокировки, версии state)
yc storage s3api list-objects --bucket "$TF_BUCKET" --format json | jq -r '.contents[].key'
# что Terraform помнит о backend
jq '.backend.config' .terraform/terraform.tfstate
Разбор: первая команда перечисляет объекты бакета (в нём должны быть notes/dev/terraform.tfstate и, пока кто-то работает, .tflock). Вторая читает служебный файл .terraform/terraform.tfstate: это не твой state, а запись о том, какой backend Terraform запомнил при последнем init, и jq '.backend.config' достаёт из неё настройки backend.
Как читать вывод: если в бакете нет notes/dev/terraform.tfstate, state потерян (сценарий 3). Если .tflock есть, а процесса нет, блокировка зависла (сценарий 1). Если key в .backend.config не совпадает с backend.tf, вот причина сценария 2.
Исправление
Разбор сценариев
1. Блокировка от убитого процесса. Убедись, что никто больше не работает (спроси в чате, посмотри Who в сообщении). Затем сними блокировку по ID из ошибки:
terraform force-unlock 3d2c6a1e-7f0b-4b5e-9d3a-0c6f7c1e2a44
Дальше запусти plan: state мог остаться в промежуточном состоянии. force-unlock опасен, потому что разрешает параллельный apply. Хороший процесс: запуски идут только из CI с очередью.
2. Backend configuration changed. Terraform заметил, что описание backend не совпадает с запомненным, и не гадает, что ты хотел. Два честных варианта:
# перенести state в новое место
terraform init -migrate-state
# переключиться на другое место без копирования
terraform init -reconfigure
Ошибка защищает от ситуации, когда plan пойдёт по пустому state и предложит создать всё заново.
3. Потеря state. Сначала попробуй восстановить предыдущую версию объекта из бакета (см. list-object-versions, скачай нужную версию и загрузи её как текущую). Если версий нет, ресурсы живы в облаке, а Terraform о них не знает. Тогда код у тебя уже есть, остаётся импортировать:
terraform import module.notes_vm.yandex_vpc_network.notes "<id сети из yc vpc network list>"
terraform import module.notes_vm.yandex_compute_instance.notes "<id ВМ из yc compute instance list>"
terraform plan
Повторяй импорт для каждого ресурса, пока plan не покажет No changes. Вывод: версионирование бакета включают до аварии, а не после.
ИИ в помощь
Нейросеть быстро разбирает чужой plan и подсказывает блок moved или import, но она не видит твой бакет и state. Общие правила работы с ней: ИИ-помощник.
Задача: составить блок moved для переноса ресурсов в модуль.
Я перенёс ресурсы network.tf и vm.tf в модуль module "notes_vm". Вот список адресов из terraform state list:
<вставь адреса>. Напиши moved.tf, чтобы plan не пересоздавал ресурсы. Для for_each-ресурсов используй один блок на весь ресурс.
Проверь ответ: запусти terraform plan: должно быть 0 to add, 0 to destroy и строки has moved to. Типичная ошибка: нейросеть пишет блок moved в дочернем модуле или путает направление from и to.
Задача: понять, что делать с зависшей блокировкой.
terraform apply упал с Error acquiring the state lock. Вот поля Lock Info: <вставь без секретов>.
Как понять, жив ли процесс, и когда безопасно делать force-unlock?
Проверь ответ: убедись, что она советует сначала выяснить, кто держит блокировку (Who), а после разблокировки сделать plan. Типичная ошибка: совет сразу запускать с -lock=false, это отключает защиту.
Задача: проверить настройки безопасности state.
Вот мой backend.tf и список прав сервисного аккаунта: <вставь без ключей>. Что здесь опасно для state, который содержит секреты?
Проверь ответ: сверь с разделом про права вокруг state: права только на бакет, публичный доступ выключен, версионирование включено. Типичная ошибка: нейросеть считает шифрование хранилища достаточной защитой.
Словарик урока
| Термин | Простыми словами |
|---|---|
| remote backend (удалённое хранилище state) | Общее место (у нас бакет), где Terraform хранит state вместо файла на ноутбуке. |
backend s3 |
Вариант backend для S3-совместимых хранилищ: Yandex Object Storage, AWS S3, MinIO. |
| бакет (bucket) | Хранилище файлов в облаке: «шкаф с ячейками», где объект достаётся по имени-ключу. |
| ключ объекта (key) | Имя файла внутри бакета, например notes/dev/terraform.tfstate; слэши это часть имени, а не настоящие каталоги. |
| versioning (версионирование) | Режим бакета, при котором прошлые версии объекта не стираются: испорченный state можно откатить. |
| state lock (блокировка) | Табличка «занято»: пока один процесс меняет state, другие ждут или получают ошибку. |
use_lockfile |
Параметр backend: блокировка файлом .tflock в том же бакете (Terraform 1.10 и новее). |
force-unlock |
Команда, снимающая «зависшую» блокировку по её ID. Использовать, только убедившись, что владелец не работает. |
-migrate-state |
Флаг init: скопировать существующий state из старого backend в новый. |
-reconfigure |
Флаг init: переключиться на новый backend без копирования state. |
| статический ключ доступа | Пара «идентификатор + секрет» сервисного аккаунта; действует, пока его не отозвали. |
| module (модуль) | Каталог с .tf файлами, который вызывают блоком module: «типовой проект» с входами и выходами. |
| корневой модуль (root module) | Каталог, из которого запускают terraform; у нас envs/dev. |
| дочерний модуль (child module) | Модуль, который вызвали из другого; у нас modules/notes-vm. |
source |
Поле блока module: откуда взять модуль (путь, git, реестр). |
| адрес ресурса | Полное имя ресурса в state: тип.имя, для модуля с префиксом module.<имя>.. |
moved |
Блок кода: «этот ресурс не новый, он просто сменил адрес». Защищает от пересоздания при рефакторинге. |
terraform state mv |
Команда, меняющая адрес ресурса прямо в state (то же, что moved, но без следа в коде). |
| окружение (environment) | Независимый набор инфраструктуры: dev, stage, prod. |
| workspace | Отдельный state внутри одного backend при том же коде. |
terraform import / блок import |
Привязка уже существующего объекта облака к блоку кода; пишет только state, код пишешь сам. |
.terraform.lock.hcl |
Файл с точными версиями и хешами провайдеров; коммитится в git. |
.terraform/ |
Кэш: скачанные провайдеры и запись о backend; в git не кладут. |
| зеркало провайдеров (provider mirror) | Сервер с копией пакетов провайдеров, если основной registry недоступен. |
~/.terraformrc |
Пользовательский файл настроек Terraform (где скачивать провайдеры). |
-detailed-exitcode |
Флаг plan: код выхода 0 (изменений нет), 1 (ошибка), 2 (изменения есть). |
| endpoint | Адрес «окошка» сервиса (API или хранилища), к которому обращается программа. |
Вопросы с собеседований
Раздел для повторения: ответь вслух, потом открой ответ. Короткие вопросы с пометкой [на скорость] тренируй на время: ответ за 30 секунд.
1. [junior] [часто] Зачем хранить state Terraform удалённо и что такое блокировка?
Ответ
Локальный файл нельзя разделить с командой, и в нём лежат секреты. Удалённый state живёт в общем бакете с версионированием и доступом по правам. Блокировка не даёт двум apply работать одновременно: второй ждёт или падает с Error acquiring the state lock.
Что хотят услышать: секреты в state, общий источник правды, версионирование как страховка, lock, S3 плюс DynamoDB или use_lockfile.
Красный флаг: «храним state в git, репозиторий же приватный».
2. [junior] [часто] Как разделить окружения dev и prod: workspaces или каталоги?
Ответ
Каталоги с общими модулями: у каждого окружения свой backend, ключ и права, и видно, где я работаю. Workspaces компактнее, но легко перепутать текущий и применить изменения не туда, а права на бакет общие.
Что хотят услышать: плюсы и минусы обоих подходов, безопасность prod, права на state.
Красный флаг: «workspaces, потому что меньше файлов», без упоминания рисков.
3. [middle] Второй apply упал с Error acquiring the state lock. Что делаешь?
Ответ
Читаю сообщение: Who, Operation, время. Выясняю, работает ли владелец блокировки. Если запуск живой, жду. Если процесс убит или связь оборвалась, снимаю блокировку terraform force-unlock <ID> и сразу делаю plan, потому что state мог остаться в промежуточном состоянии.
Что хотят услышать: сначала выяснить, кто держит, потом force-unlock, после него plan, запуски через CI.
Красный флаг: «сразу -lock=false и применяю».
4. [middle] Кто-то удалил файл state из бакета. Что делаешь?
Ответ
Проверяю версионирование бакета и восстанавливаю предыдущую версию объекта. Если версий нет, ресурсы в облаке живы, а state пуст: сверяю код и делаю terraform import по каждому ресурсу, пока plan не покажет No changes. apply на пустом state не запускаю, иначе получу дубликаты или конфликт имён.
Что хотят услышать: версионирование, import, не запускать apply на пустом state, ограничение прав на запись в бакет.
Красный флаг: «создам всё заново, старые ресурсы удалю руками».
5. [middle] После переноса кода в модуль plan хочет удалить и создать ВМ. Почему и как исправить?
Ответ
Поменялись адреса ресурсов (появился префикс module.<имя>), а Terraform сопоставляет ресурсы по адресу. Исправляю блоками moved в корневом модуле или командой terraform state mv. После этого plan показывает только перемещение.
Что хотят услышать: адрес ресурса, moved, state mv, чтение плана до apply.
Красный флаг: «применил, ВМ пересоздалась, ничего страшного».
6. [middle] После правки backend.tf plan пишет Backend configuration changed. Чем -migrate-state отличается от -reconfigure?
Ответ
Terraform сравнил конфиг backend с запомненным в .terraform/ и требует явного решения. -migrate-state копирует state из старого места в новое. -reconfigure просто переключает на новый backend без копирования, это годится, когда state уже перенесли руками или он не нужен.
Что хотят услышать: защита от plan по пустому state, различие флагов, осторожность с вопросом «перезаписать?».
Красный флаг: «удалю .terraform/ и всё заработает» без понимания последствий.
7. [middle] Когда выносить код в модуль и как версионировать модули?
Ответ
Когда код повторяется и меняется вместе (dev и stage), а не на всякий случай. У модуля есть переменные и outputs, внутренности наружу не торчат. Версию фиксирую: для git ?ref=v1.2.0, для реестра version = "~> 1.2". Локальный source годится, пока модуль живёт в одном репозитории, но тогда правки видят все окружения, и выкатываю их через dev.
Что хотят услышать: интерфейс модуля, версионирование, риск «поправил модуль и сломал все окружения».
Красный флаг: модуль на один ресурс и ссылка на ветку main вместо тега.
8. [middle] Ресурс создан руками в консоли, его просят «взять под Terraform». Как?
Ответ
Пишу описание ресурса в коде, делаю terraform import <адрес> <id> или блок import (Terraform 1.5 и новее), потом plan и правлю код, пока не получу No changes. Применяю только после этого, чтобы не изменить ресурс неожиданно.
Что хотят услышать: import не пишет код, цель No changes, осторожность с apply.
Красный флаг: «удалю и создам заново», если это база с данными.
9. [middle] В CI нужен plan на каждый pull request. Какие подводные камни?
Ответ
Нужен доступ CI к state (для PR только чтение), ключи не хранятся в репозитории (OIDC или секреты CI), для последующего apply план сохраняют через plan -out. Блокировка берётся и на plan, поэтому параллельные PR могут мешать друг другу. Секреты в выводе плана надо маскировать.
Что хотят услышать: -out, права read-only, lock на plan, секреты в state и логах.
Красный флаг: «дадим CI админский ключ и отключим lock».
10. [middle] terraform init из РФ зависает на скачивании провайдера. Что делаешь?
Ответ
Проверяю сеть до registry, затем настраиваю зеркало в ~/.terraformrc: network_mirror для yandex-cloud/*, остальное напрямую. Хеши в lock-файле не дадут зеркалу подложить другую сборку. В CI использую тот же файл или кэширую каталог плагинов.
Что хотят услышать: .terraformrc, зеркало, кэш плагинов в CI, хеши в lock-файле.
Красный флаг: «качаю бинарник провайдера откуда попало и кладу руками».
11. [junior] Что коммитят из Terraform-каталога, а что нет?
Ответ
Коммитят .tf файлы и .terraform.lock.hcl (версии и хеши провайдеров, чтобы у всех и в CI были одни версии). Не коммитят .terraform/, *.tfstate* и *.tfvars с секретами.
Что хотят услышать: воспроизводимость версий, секреты в tfvars и state.
Красный флаг: «lock-файл в .gitignore, он мешает».
12. [junior] Что делают terraform state list, state show, state mv и state rm?
Ответ
Это команды для работы с state без изменения инфраструктуры. terraform state list показывает адреса ресурсов в state, terraform state show АДРЕС печатает атрибуты одного ресурса. terraform state mv меняет адрес ресурса в state, например после переименования или переноса в модуль, чтобы Terraform не пересоздавал его. terraform state rm убирает ресурс из state, сам ресурс в облаке остаётся, а Terraform о нём забывает. Перед mv и rm я делаю копию state (terraform state pull > backup.tfstate) и потом проверяю plan.
Что хотят услышать: работа только со state, mv для рефакторинга, rm не удаляет реальный ресурс, бэкап перед правкой, проверка plan после.
Красный флаг: «state rm удалит ВМ» или правка terraform.tfstate руками в редакторе.
13. [middle] Как передать вывод одного Terraform-проекта в другой и какие у terraform_remote_state минусы?
Ответ
Проект-источник объявляет output, а потребитель читает его через data "terraform_remote_state", указав тот же backend. Минус первый: потребителю нужен доступ на чтение к state источника, а state может хранить чувствительные данные. Минус второй: проекты жёстко связаны структурой outputs, переименовал output и сломал чужой plan. Часто проще взять нужное значение через обычный data source провайдера по имени или тегу, либо опубликовать его в общем хранилище параметров. Если связь всё же нужна, я оставляю в outputs только то, что реально читают соседи.
Что хотят услышать: output плюс remote_state, доступ к чужому state как риск, жёсткая связность, альтернатива через data sources.
Красный флаг: копирование значений руками из одного проекта в другой в tfvars.
14. [junior] Зачем ограничивать версии провайдера и что значит ~> 5.0?
Ответ
Провайдеры выпускают новые версии, и без ограничения завтрашний init может подтянуть версию с другим поведением. В required_providers я задаю ограничение version. Запись ~> 5.0 разрешает любые 5.x не ниже 5.0, но не 6.0, а ~> 5.1.0 разрешает только патчи 5.1.x. Фактически выбранные версии и их хэши фиксирует файл .terraform.lock.hcl, я его коммичу, чтобы у всей команды и CI были одни и те же версии. Обновляю осознанно: terraform init -upgrade, смотрю plan, коммичу новый lock-файл.
Что хотят услышать: ограничение в required_providers, смысл ~>, lock-файл в git, обновление только осознанно через -upgrade.
Красный флаг: не задавать версии вообще или удалять lock-файл, когда он мешает.
Проверено на версиях
- Terraform: 1.16.4 (как в уроках 7.1 и 7.2; для
use_lockfileнужна версия 1.10 или новее) - Провайдер
yandex-cloud/yandex: 0.232.0 (в коде~> 0.232) - Yandex Cloud CLI (
yc): версия не закреплена, проверь актуальную версию на странице проекта - Ubuntu: 26.04 LTS и 24.04 LTS
- Не прогонялось: работа с реальным бакетом Yandex Object Storage (создание, миграция, блокировка) и перенос ВМ в модуль в облаке. Выводы в заданиях 2, 3 и 5 сокращены и показывают формат, на строки которого нужно смотреть. Поддержку
use_lockfileв Yandex Object Storage проверь сам (задание 2). - Формат команд
yc storage bucket update --grants ...сверь сyc storage bucket update --help.
Итог урока: ты умеешь
- умею создать бакет с версионированием и отдельным ключом сервисного аккаунта для state
- умею перенести локальный state в S3-совместимый backend через
terraform init -migrate-state - умею проверить, что блокировка работает, и снять зависшую через
force-unlock - умею вынести сеть и ВМ в модуль и вызвать его из окружения без пересоздания ресурсов (
moved) - умею развести dev и stage по каталогам с отдельными state
- умею восстановить потерянный state из версии бакета или через
terraform import - умею настроить зеркало провайдеров в
~/.terraformrc - умею объяснить на собеседовании разницу между workspaces и каталогами окружений
Дальше: Урок 7.4: Ansible: инвентарь, модули, ad-hoc
Проверь себя
Короткий тест по уроку: 5 вопросов из банка в 30. Засчитывается только полностью правильный ответ, порог 60%. Каждая новая попытка даёт другие вопросы, пока банк не закончится. Ответы видны после проверки.
Тест работает с включённым JavaScript.