Удалённый бэкенд и блокировки
Локальный state на ноутбуке работает только для соло-экспериментов. В команде нужен удалённый бэкенд: общий, защищённый и с блокировкой, чтобы двое не применяли одновременно.
Два инженера, два
apply, один state — это рецепт катастрофы. State locking превращает гонку в очередь: пока один применяет, второй ждёт.
Бэкенд (backend) определяет, где хранится state. По умолчанию он локальный (terraform.tfstate рядом с кодом). В команде это недопустимо: state нужен общий, зашифрованный и с защитой от одновременной записи. Популярные удалённые бэкенды — AWS S3, Terraform Cloud, Azure Blob, GCS.
Настройка S3-бэкенда
terraform {
backend "s3" {
bucket = "my-tf-states"
key = "prod/network/terraform.tfstate"
region = "eu-west-1"
encrypt = true
use_lockfile = true # нативная S3-блокировка (новые версии)
}
}
Раньше для блокировки требовалась отдельная таблица DynamoDB. В современных версиях Terraform появилась нативная блокировка через сам S3 (флаг use_lockfile = true) — DynamoDB больше не нужна. Перед операцией Terraform ставит lock, после — снимает.
Инженер A STATE LOCK (S3) Инженер B
terraform apply -> [ЗАХВАЧЕН A] <- terraform apply
... работает | ЖДЁТ (Lock!)
завершил -> [СВОБОДЕН] -> теперь захватывает B
работает
Как работает под капотом
Блокировка — это простой механизм: перед записью проверь, свободен ли замок; если да — захвати, сделай дело, освободи. Если занят — жди или падай с ошибкой. Смоделируем менеджер блокировок:
class StateLock:
def __init__(self):
self.holder = None
def acquire(self, who):
if self.holder is not None:
return f"ОТКАЗ: state заблокирован пользователем {self.holder}"
self.holder = who
return f"OK: {who} захватил lock"
def release(self, who):
if self.holder == who:
self.holder = None
return f"OK: {who} освободил lock"
return "ОТКАЗ: не ваш lock"
lock = StateLock()
print(lock.acquire("alice")) # alice начинает apply
print(lock.acquire("bob")) # bob пытается параллельно -> отказ
print(lock.release("alice")) # alice закончила
print(lock.acquire("bob")) # теперь bob может
«Попробуй сам ▶» — пока alice держит lock, bob получает отказ. Именно это спасает state от одновременной записи.
Частые ошибки
- Локальный state в команде. Каждый держит свою копию — конфликты и потеря изменений неизбежны.
- Отключить блокировку
-lock=false. «Чтобы не ждать» — и получить повреждённый state при гонке. - Завис lock после краша. Если apply упал, lock может остаться; снимают через
terraform force-unlock ID— но осторожно.
Best practices
- Всегда удалённый бэкенд с шифрованием (
encrypt = true) и блокировкой для любой командной работы. - Разные окружения — разные ключи state (
prod/...,dev/...), чтобы изолировать blast radius. - Включите версионирование бакета — это бесплатный бэкап state на случай беды.
Разбор глубже
Стоит понимать разницу между standard и enhanced бэкендами. Большинство (S3, GCS, Azure Blob) — это standard: они только хранят state. А enhanced-бэкенды (Terraform Cloud, ранее «remote») умеют ещё и выполнять операции — то есть plan и apply запускаются на их серверах, а не на вашей машине. Это даёт централизованные логи, очередь запусков, управление доступом и хранение переменных в одном месте. Для команд, которым нужна полноценная платформа вокруг Terraform, enhanced-бэкенд снимает много инфраструктурной работы.
Настройка бэкенда имеет важное ограничение: блок backend не может использовать переменные — он должен содержать только литералы. Это следствие курицы и яйца: чтобы прочитать переменные, Terraform должен инициализироваться, а чтобы инициализироваться, ему нужен бэкенд. Решают это через partial configuration: часть параметров оставляют в коде, а остальные передают при инициализации флагом terraform init -backend-config=... или отдельным файлом. Так один и тот же код может указывать на разные бакеты state для разных окружений, не нарушая запрет на переменные в блоке backend.
Итог: удалённый бэкенд даёт общий, зашифрованный state, а блокировка превращает параллельные apply в безопасную очередь. Современный S3 умеет блокировать сам. Дальше — что делать, когда реальность разошлась с state.