В этом уроке разберём, как изолировать terraform state по окружениям, чтобы такого не случалось. Сравним два подхода - рабочие области (terraform workspaces) и раскладку по отдельным папкам - и я честно скажу, почему опытные команды в большинстве случаев выбирают второй. Спойлер: дело не в моде, а в том, что папки тяжелее прострелить себе ногой.
Почему состояние вообще надо изолировать
Напомню механику. Terraform хранит карту реального мира в файле состояния - terraform state. Это связь между тем, что написано в коде (инфраструктура как код, она же iac), и тем, что реально создано в облаке: id виртуалок, сетей, дисков. Когда ты делаешь terraform plan, инструмент сравнивает желаемое (код) с текущим (state) и показывает разницу. terraform apply эту разницу применяет.
Теперь главное. Если dev и prod пишут в один и тот же state, у Terraform в голове каша. Он видит ресурсы обоих окружений в одной карте. Меняешь параметр для теста - а план затрагивает боевые ресурсы, потому что они лежат рядом. Плюс блокировки: пока кто-то катит dev, прод-деплой ждёт лок. И самое опасное - terraform destroy, нацеленный не туда, выносит всё разом.
Вывод простой: у каждого окружения должно быть своё, отдельное состояние. Физически отдельный файл (или ключ в бэкенде), который ничего не знает про соседей. Вопрос только в том, как это организовать. Вот тут и расходятся два пути.

Способ 1: рабочие области (terraform workspaces)
Workspaces - встроенный механизм. Идея: один набор кода, один бэкенд, но несколько именованных состояний внутри него. По умолчанию ты всегда в области default. Команды простые:
Код: Выделить всё
terraform workspace new dev
terraform workspace new prod
terraform workspace list
terraform workspace select prod
В коде к текущей области обращаются через terraform.workspace. Типичный приём - подмешивать имя области в параметры:
Код: Выделить всё
locals {
env = terraform.workspace
vm_count = terraform.workspace == "prod" ? 3 : 1
disk_size = terraform.workspace == "prod" ? 50 : 20
}
resource "yandex_compute_instance" "app" {
count = local.vm_count
name = "app-${local.env}-${count.index}"
zone = "ru-central1-a"
resources {
cores = 2
memory = 2
}
boot_disk {
initialize_params {
image_id = "fd8vmcue7aajpmeo39kk"
size = local.disk_size
}
}
network_interface {
subnet_id = yandex_vpc_subnet.this.id
nat = true
}
}
Что ещё плохо. Все окружения делят один бэкенд и один набор прав - изолировать доступ к боевому состоянию на уровне IAM или отдельного бакета уже не выйдет. Конфигурация различается через тернарники в коде, и когда условий становится много, читать это больно. И провайдеры тоже общие - нельзя, например, держать dev и prod в разных каталогах Yandex Cloud аккуратно и явно.
Где workspaces реально уместны? Когда нужно быстро поднять несколько почти одинаковых временных копий: ветка фичи, краткоживущий стенд для ревью, эфемерное окружение в CI. Вот там это удобно. А для постоянных dev/stage/prod - нет.
Способ 2: раскладка по папкам (рекомендуемый)
Подход, который советуют почти все книги и зрелые команды: для каждого окружения - своя директория со своим backend-ключом. Никакой магии скрытого состояния. Какую папку открыл - в том окружении и работаешь. Это видно глазами, это видно в git diff, это сложно перепутать.
Структура проекта обычно такая:
Код: Выделить всё
.
├── modules/
│ ├── network/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── app/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
└── environments/
├── dev/
│ ├── main.tf
│ ├── backend.tf
│ └── terraform.tfvars
├── stage/
│ ├── main.tf
│ ├── backend.tf
│ └── terraform.tfvars
└── prod/
├── main.tf
├── backend.tf
└── terraform.tfvars
Код: Выделить всё
terraform {
backend "s3" {
endpoints = {
s3 = "https://storage.yandexcloud.net"
}
bucket = "tfstate-cyberlake"
key = "prod/terraform.tfstate"
region = "ru-central1"
skip_region_validation = true
skip_credentials_validation = true
skip_requesting_account_id = true
skip_s3_checksum = true
}
}
Код: Выделить всё
module "app" {
source = "../../modules/app"
env = "prod"
vm_count = 3
disk_size = 50
subnet_id = module.network.subnet_id
}
module "network" {
source = "../../modules/network"
env = "prod"
}
И да, всё сказанное один в один работает с OpenTofu - это форк Terraform, команды и формат бэкенда те же, так что выбор подхода от инструмента не зависит.
Типичные грабли
- Забыл terraform workspace select. Классика жанра при первом способе. Сидишь в default, думаешь, что в dev, катишь в прод. Лечится только дисциплиной или баннером в promt - что ненадёжно.
- Копипаста папок целиком. При раскладке по папкам люди копируют всю логику из dev в prod вместо выноса в модуль. В итоге dev и prod расходятся, фиксы доезжают не везде. Общее - в modules, в папках только параметры.
- Одинаковый backend key в двух папках. Скопировал backend.tf и забыл поменять key. Два окружения начинают топтать одно состояние - тот самый ад, от которого ты бежал. Проверяй key после каждого копирования.
- tfvars с секретами в git. terraform.tfvars удобно класть рядом, но пароли и токены туда совать нельзя - утекут в историю репозитория. Секреты - через переменные окружения или Lockbox.
Повтори руками, это закрепляет лучше любого текста:
- Создай структуру: папку modules/app с простейшим yandex_compute_instance и папки environments/dev и environments/prod.
- В каждое окружение положи backend.tf с разными key (dev/terraform.tfstate и prod/terraform.tfstate), указывающими на один бакет в Object Storage.
- Зайди в environments/dev, выполни terraform init и terraform plan. Затем то же в prod. Убедись, что планы независимы.
- Теперь для сравнения создай terraform workspace new test в одной из папок и посмотри через terraform workspace show, как состояние стало невидимым в коде. Прочувствуй разницу.
- Что физически произойдёт, если dev и prod пишут в один файл состояния, и чем это грозит при terraform destroy?
- Почему имя текущей рабочей области называют "невидимым состоянием" и в чём тут опасность?
- Что должно лежать в папке окружения, а что выносится в модуль при раскладке по папкам?
- Какой ровно один атрибут в backend-конфигурации отвечает за изоляцию состояний разных окружений в одном бакете?
Запомни главное. Каждое окружение - отдельный terraform state, это не обсуждается. Workspaces дают изоляцию дёшево, но прячут окружение в невидимое состояние терминала, и на этом легко обжечься - оставь их для эфемерных стендов. Раскладка по папкам с отдельным backend-ключом на окружение многословнее, зато окружение видно в коде, права изолируются на уровне бакета, а прострелить ногу заметно труднее. Для постоянных dev/stage/prod бери папки. Это и есть зрелый terraform yandex cloud в проде.