Решение придумано давно: вынести terraform state в общее удалённое хранилище. В мире инфраструктуры как код (iac) это называется remote backend. Для Yandex Cloud самый удобный вариант - положить состояние в Object Storage, потому что он S3-совместимый, а у Terraform из коробки есть бэкенд "s3". В этом уроке разберём, как настроить terraform yandex cloud так, чтобы state жил в бакете, был защищён блокировкой и переживал любые катастрофы благодаря версионированию. Заодно глянем на новый механизм 2026 года - нативную блокировку без всякой DynamoDB.
Зачем вообще удалённый бэкенд
Удалённый бэкенд решает сразу три проблемы.
- Общий источник правды. State лежит в одном месте, все берут его оттуда. Никаких "а у меня локально по-другому".
- Блокировка (state locking). Пока один человек делает terraform plan apply, бэкенд ставит замок. Второй, кто попытается применить изменения параллельно, получит вежливый отказ и не испортит файл состояния гонкой.
- Безопасность и бэкапы. Бакет можно зашифровать, ограничить по IAM, включить версионирование. State перестаёт валяться на чьём-то рабочем столе.

Что нужно подготовить в облаке
Object Storage не пускает по обычному IAM-токену для S3-протокола. Ему нужна пара статических ключей - access key и secret key, привязанных к сервисному аккаунту. Цепочка такая: создаём сервисный аккаунт, выдаём ему роль storage.editor на каталог, генерим статический ключ доступа. В Terraform это три ресурса, имена которых я взял прямо из документации провайдера, не выдумывая.
Код: Выделить всё
locals {
folder_id = "b1gxxxxxxxxxxxxxxxxx"
}
# Сервисный аккаунт под управление состоянием
resource "yandex_iam_service_account" "tf_state" {
folder_id = local.folder_id
name = "tf-state-sa"
}
# Роль на каталог: права писать в Object Storage
resource "yandex_resourcemanager_folder_iam_member" "tf_state_editor" {
folder_id = local.folder_id
role = "storage.editor"
member = "serviceAccount:${yandex_iam_service_account.tf_state.id}"
}
# Статический ключ доступа сервисного аккаунта
resource "yandex_iam_service_account_static_access_key" "tf_state_key" {
service_account_id = yandex_iam_service_account.tf_state.id
description = "static access key for terraform state bucket"
}
# Сам бакет под state, с версионированием
resource "yandex_storage_bucket" "tf_state" {
access_key = yandex_iam_service_account_static_access_key.tf_state_key.access_key
secret_key = yandex_iam_service_account_static_access_key.tf_state_key.secret_key
bucket = "cyberlake-tf-state"
versioning {
enabled = true
}
}
Тонкий момент про курицу и яйцо. Бакет под state создаётся самим Terraform, но бэкенд должен существовать до первого apply. Поэтому схема обычно двухшаговая: сначала поднимаешь бакет с локальным state (или вообще руками через консоль), потом подключаешь бэкенд и мигрируешь состояние в этот бакет. Не пытайся в одном проекте одновременно создавать бакет и хранить в нём же его собственный state - получишь логический парадокс.
Блок backend "s3" для Yandex Cloud
Теперь самое главное - как сказать Terraform, чтобы он хранил state в нашем бакете. Бэкенд описывается внутри блока terraform. Object Storage прикидывается AWS S3, но это не настоящий AWS, поэтому половину "ауэсных" проверок надо отключить, иначе Terraform полезет в несуществующие STS и metadata API и упадёт.
Код: Выделить всё
terraform {
backend "s3" {
bucket = "cyberlake-tf-state"
key = "prod/infra.tfstate"
region = "ru-central1"
endpoints = {
s3 = "https://storage.yandexcloud.net"
}
# Native state locking, Terraform 1.11+ / OpenTofu
use_lockfile = true
# Без этих флагов S3-бэкенд пытается ходить в реальный AWS
skip_region_validation = true
skip_credentials_validation = true
skip_requesting_account_id = true
skip_s3_checksum = true
}
}
- bucket - имя бакета, которое мы создали выше.
- key - путь к файлу состояния внутри бакета. Удобно класть разные окружения по разным ключам: prod/infra.tfstate, stage/infra.tfstate.
- region - подойдёт ru-central1. Object Storage не особо смотрит на регион, но поле обязательное, поэтому валидацию мы и выключаем.
- endpoints.s3 - адрес Object Storage. Раньше писали плоский endpoint = "storage.yandexcloud.net", но начиная с Terraform 1.6 этот плоский атрибут объявлен устаревшим, и правильно использовать блок endpoints с ключом s3. Если у тебя старые гайды с одиночным endpoint - они ещё работают, но кидают deprecation warning.
- skip_* флаги - отключают проверки, специфичные для настоящего AWS. Для любого S3-совместимого хранилища это стандартный набор.
Блокировка: нативный lockfile вместо DynamoDB
Историческая боль S3-бэкенда: сам по себе S3 не умеет атомарной блокировки, поэтому раньше для state locking приходилось поднимать отдельную таблицу - в AWS это DynamoDB, в Yandex Cloud её роль играла YDB в DynamoDB-совместимом режиме. Лишний ресурс, лишние права, лишняя точка отказа.
В 2026 году это уже архаизм. Начиная с Terraform 1.11 (экспериментально появилось в 1.10) бэкенд s3 умеет нативную блокировку через простой lock-файл прямо в бакете. Включается одной строкой: use_lockfile = true. Никакой таблицы не нужно. OpenTofu поддерживает то же самое. DynamoDB-локинг при этом объявлен устаревшим и в одной из будущих минорных версий уедет на пенсию, так что для новых проектов выбор очевиден - use_lockfile.
Как это работает: перед apply Terraform кладёт в бакет маленький объект рядом с твоим state (по ключу infra.tfstate.tflock), а после завершения - удаляет. Если объект уже есть, значит кто-то держит замок, и второй процесс ждёт или падает с понятной ошибкой. Если apply внезапно прервался и замок завис, его снимают вручную командой terraform force-unlock с ID из сообщения об ошибке - но без фанатизма, сначала убедись, что параллельного процесса реально нет.
Миграция состояния: init -migrate-state
Допустим, у тебя уже есть проект с локальным state, и ты только что дописал блок backend "s3". Просто так Terraform на новый бэкенд не переедет - надо инициализироваться и явно разрешить миграцию.
Код: Выделить всё
export AWS_ACCESS_KEY_ID="<access_key сервисного аккаунта>"
export AWS_SECRET_ACCESS_KEY="<secret_key сервисного аккаунта>"
terraform init -migrate-state
Команда terraform init -reconfigure - это другое: она не переносит state, а просто пересобирает настройки бэкенда, игнорируя сохранённые. Её используют, когда меняешь, например, ключ или бакет и не хочешь тащить старое состояние. Не путай -migrate-state и -reconfigure, это частый источник недоразумений.
Типичные грабли
- Забыл skip-флаги. Самая популярная ошибка: пишешь endpoint, но не выключаешь валидацию - и init виснет или ругается на credentials, пытаясь достучаться до настоящего AWS. Лечится набором skip_*.
- Ключи в коде. Зашил access_key/secret_key прямо в блок backend и закоммитил. Поздравляю, секрет утёк. Только переменные окружения или -backend-config.
- Нет версионирования на бакете. State побился, а откатиться некуда. Включай versioning сразу, это бесплатная страховка.
- Курица и яйцо. Пытаешься создать бакет и хранить в нём же его state в одном apply. Раздели на два шага.
- Старый одиночный endpoint. Работает, но устарел. Используй блок endpoints = { s3 = ... }, чтобы не ловить warning и не переписывать конфиг через год.
- Тянешь DynamoDB по привычке. В 2026 для блокировки достаточно use_lockfile. Таблица YDB/DynamoDB - лишняя сущность, если только тебе не нужна совместимость со старыми версиями Terraform.
Повтори руками, без этого в голове не уляжется:
- Создай сервисный аккаунт, выдай роль storage.editor и сгенерь статический ключ (через Terraform с локальным state или через консоль).
- Подними бакет yandex_storage_bucket с включённым versioning. Имя бакета глобально уникальное, придумай своё.
- Допиши блок terraform { backend "s3" { ... } } с endpoints.s3, skip-флагами и use_lockfile = true.
- Прогони terraform init -migrate-state и убедись, что state уехал: открой бакет в консоли и найди там свой .tfstate.
- Запусти terraform plan из двух терминалов одновременно (или поставь долгий apply и параллельно дёрни plan) - поймай сообщение о блокировке. Это и есть state locking в действии.
- Затри объект state в бакете и восстанови его из предыдущей версии - проверь, что версионирование реально спасает.
- Почему Object Storage требует статический ключ доступа, а не обычный IAM-токен, для работы по S3-протоколу?
- Чем отличается terraform init -migrate-state от terraform init -reconfigure?
- Что делает use_lockfile = true и почему в 2026 году это лучше, чем отдельная таблица DynamoDB/YDB?
- Зачем включать versioning на бакете со state и как это помогает при аварии?
Удалённый бэкенд - первое, что нужно настроить на любом командном проекте, иначе terraform state рано или поздно превратится в источник боли. В Yandex Cloud это бакет Object Storage плюс блок backend "s3" с адресом storage.yandexcloud.net в endpoints.s3 и обязательным набором skip-флагов. Доступ - через статический ключ сервисного аккаунта, который кладём в переменные окружения, а не в код. Блокировку в 2026 делаем нативным use_lockfile = true без всякой DynamoDB. Версионирование бакета держим включённым - это твой бэкап на случай, когда state всё-таки побьётся. И помни про два шага: сначала бакет, потом миграция через init -migrate-state.