Удалённый бэкенд и блокировки

Локальный 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.

Проверьте себя
1. Зачем нужна блокировка state (state locking)?
AДля шифрования state
BЧтобы два apply не записывали в state одновременно и не повредили его
CДля ускорения plan
DЧтобы запретить destroy
2. Что изменилось в блокировке S3-бэкенда в новых версиях Terraform?
AБлокировка стала платной
BПоявилась нативная блокировка через S3 (use_lockfile), DynamoDB больше не обязательна
CБлокировку убрали
DТеперь нужна только DynamoDB