Terraform state без боли: backends, locking и миграция со старой схемы
Terraform state — это та папка, куда смотрят со страхом и редко. Лежит где-то в S3, кто-то его настраивал три года назад, никто не помнит, как делать backup, и каждый apply начинается с молитвы. Я через это прошёл несколько раз, в том числе мигрируя со старого local state в S3+DynamoDB и потом в Terraform Cloud. Ниже — что и как настраивать в 2026, чтобы спать спокойно.
Версия — terraform 1.9. Опенть, OpenTofu 1.8 ведёт себя идентично, все команды и backends те же.
Что такое state и почему без него никак
State — это JSON-файл, в котором terraform хранит маппинг между ресурсами в коде и их реальными ID в облаке. Когда ты пишешь:
resource "aws_s3_bucket" "data" {
bucket = "my-app-data"
}...terraform создаёт bucket в AWS и записывает в state: «aws_s3_bucket.data → arn:aws:s3:::my-app-data». В следующий terraform plan он смотрит state, идёт в AWS, читает реальное состояние bucket и сравнивает с кодом. Без state он не знает, какие ресурсы он создавал.
State содержит секреты — пароли БД, ключи, любые sensitive-output. Это первая причина не хранить его в git.
Backends: где state лежит
local — только для локальной игры
По умолчанию terraform пишет state в terraform.tfstate в текущей папке. Никаких блокировок, никакой шары. Если два инженера в команде запустят apply одновременно — последний выиграет, остальные потеряют изменения.
Local — это для одиночных экспериментов и tutorials. В команде сразу настраиваешь remote backend.
S3 + DynamoDB — классика для AWS
Самый распространённый remote backend. State хранится в S3, locking — в DynamoDB.
terraform {
backend "s3" {
bucket = "company-tf-state"
key = "prod/network/terraform.tfstate"
region = "eu-central-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}В новых версиях terraform появился флаг use_lockfile = true и нативная поддержка S3-native locking без DynamoDB (через S3 object locks). Если стартуешь с нуля в 2026 — стоит присмотреться, минус одна сущность. Я пока консервативен и держу DynamoDB, потому что в проде уже работает.
Bucket для state нужно настраивать так:
resource "aws_s3_bucket" "tf_state" {
bucket = "company-tf-state"
}
resource "aws_s3_bucket_versioning" "tf_state" {
bucket = aws_s3_bucket.tf_state.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "tf_state" {
bucket = aws_s3_bucket.tf_state.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
resource "aws_s3_bucket_public_access_block" "tf_state" {
bucket = aws_s3_bucket.tf_state.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}Версионирование — обязательно. Если state случайно повредится, можно восстановить предыдущую версию.
GCS, Azure Blob — для своих облаков
Аналог S3 в Google Cloud и Azure. Конфиги те же, синтаксис чуть другой:
terraform {
backend "gcs" {
bucket = "company-tf-state"
prefix = "prod/network"
}
}GCS поддерживает object versioning и встроенный locking без DynamoDB-аналога.
Terraform Cloud / Terraform Enterprise
Hashicorp-managed backend плюс много обвязки: workspace UI, audit log, RBAC, run triggers. Если у тебя 30+ инженеров и нужен governance — это хороший выбор. Но это уже подписка и $$, и lock-in.
OpenTofu state encryption
OpenTofu добавил встроенное шифрование state на стороне клиента. Это решает проблему «у меня в state пароль БД, и кто-то с readonly access к S3 его прочитает». Если на тебя смотрит compliance — посмотри в эту сторону.
Locking: чтобы apply не убивал apply
Locking — это механизм, который не даёт двум terraform-сессиям менять state одновременно. Без него гонки гарантированы.
В S3 backend lock реализуется через DynamoDB-таблицу с одной строкой на ключ state-файла. Один apply берёт lock, другой ждёт.
resource "aws_dynamodb_table" "tf_locks" {
name = "terraform-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}Если apply упал в середине (terraform убит SIGKILL, инженер закрыл терминал) — lock остаётся в таблице. Снять руками:
terraform force-unlock <lock-id>ID берётся из сообщения об ошибке при попытке нового apply. Делать force-unlock без понимания, кто держит lock — опасно: можно потерять чужие изменения.
Структура state-файлов
Главный вопрос — один state на всё или много маленьких? Я придерживаюсь правила: один state на одну изолированную область, которая меняется вместе.
infra/
network/ # VPC, subnets, route tables
backend.tf # key = "prod/network/terraform.tfstate"
cluster/ # EKS
backend.tf # key = "prod/cluster/terraform.tfstate"
rds/ # БД
backend.tf # key = "prod/rds/terraform.tfstate"
apps/ # отдельные приложения, может быть свой state на app
api/
worker/Между state-файлами нужны зависимости — это делается через terraform_remote_state:
data "terraform_remote_state" "network" {
backend = "s3"
config = {
bucket = "company-tf-state"
key = "prod/network/terraform.tfstate"
region = "eu-central-1"
}
}
resource "aws_eks_cluster" "main" {
name = "prod"
role_arn = aws_iam_role.eks.arn
vpc_config {
subnet_ids = data.terraform_remote_state.network.outputs.private_subnet_ids
}
}Альтернатива — data sources, которые читают AWS напрямую (например, aws_vpc по тегу). Это менее coupled, но и менее explicit.
Миграция со старой схемы
Часто приходишь в проект, где state лежит в одном гигантском файле, или вообще в local. Что делать:
Шаг 1: бэкап
terraform state pull > backup-$(date +%Y%m%d-%H%M).tfstateЭто руками. Делай каждый раз перед любой миграцией. Серьёзно. Я однажды потерял неделю работы команды, потому что не сделал бэкап перед split.
Шаг 2: настрой новый backend
Допустим, у тебя был local state, переходишь на S3. Сначала создаёшь bucket и DynamoDB-таблицу (отдельным small-проектом, который потом сам в себя в state добавишь, или вручную через AWS CLI).
Потом в основном проекте добавляешь backend-блок:
terraform {
backend "s3" {
bucket = "company-tf-state"
key = "prod/main/terraform.tfstate"
region = "eu-central-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}И запускаешь:
terraform init -migrate-stateterraform спросит «копировать local state в S3?» — отвечаешь yes. После этого terraform.tfstate в локальной папке можно удалить (а лучше переименовать на пару дней, пока не убедишься, что всё работает).
Шаг 3: split одного огромного state на несколько
Это самая болезненная процедура. Если у тебя в одном state-файле 2000 ресурсов — split их по логическим областям.
# из старого state переносим ресурсы в новый
terraform state mv -state-out=../network/terraform.tfstate \
aws_vpc.main aws_vpc.main
terraform state mv -state-out=../network/terraform.tfstate \
'aws_subnet.private[0]' 'aws_subnet.private[0]'После каждой партии переноса делай terraform plan в обоих местах — должно показывать «No changes». Если показывает diff — что-то перенесено не туда, откатывайся из бэкапа.
Реалистично: для проекта с 500 ресурсами split занимает день-два, не пробуй сделать вечером перед уходом домой.
Что не делать никогда
- Не править state.json руками. Только через
terraform state ...команды. - Не коммитить .tfstate в git. Там секреты.
- Не запускать apply из двух мест без lock. Сломаешь state.
- Не использовать один state на десять окружений. dev, staging, prod — отдельные state-файлы или отдельные workspace.
Что запомнить
State — это сердце terraform, относись к нему как к БД с продакшен-данными. Backend в S3 (или GCS) с шифрованием и версионированием. Locking через DynamoDB или нативный S3 lock. Один state на одну логически изолированную область. Бэкап перед любой миграцией. Никогда не правь руками.
Куда копать: официальная документация по state, и про OpenTofu — у них хорошие гайды по encryption и миграции с terraform. Если планируешь модульную инфраструктуру с кучей stacks — присмотрись к Terragrunt, он автоматизирует backend-конфиги и зависимости между state-файлами.