В этом уроке разберём подводные камни, на которых спотыкаются все - и новички, и те, кто "уже год пишет на терраформе". А потом возьмём главное лекарство 2026 года - блок moved, который превращает опасный рефакторинг в безопасную рутину.
Грабли первая: значения, известные только после apply
Terraform строит граф зависимостей и хочет знать структуру ресурсов ДО того, как что-то создаст. Поэтому есть жёсткое правило: аргументы count и for_each должны быть вычислимы на этапе plan. Если ты подсунешь туда значение, которое появится только после apply (например, id ещё не созданного ресурса или длину списка, зависящего от вывода другого ресурса), Terraform упрётся и выдаст что-то вроде "The count value depends on resource attributes that cannot be determined until apply".
Пример, как НЕ надо:
Код: Выделить всё
# Так делать нельзя: count зависит от id, который появится только после apply
resource "yandex_compute_instance" "vm" {
count = length(yandex_vpc_subnet.this.*.id) # ловушка
# ...
}
Код: Выделить всё
variable "subnet_count" {
type = number
default = 3
}
resource "yandex_vpc_subnet" "this" {
count = var.subnet_count
name = "subnet-${count.index}"
zone = "ru-central1-a"
network_id = yandex_vpc_network.this.id
v4_cidr_blocks = ["10.1.${count.index}.0/24"]
}

Грабли вторая: "без простоя" - это с оговорками
Многие думают, что Terraform умеет менять ресурсы вообще без даунтайма. Не всегда. Есть атрибуты, которые провайдер помечает как "ForceNew" - их нельзя изменить на лету, поэтому ресурс пересоздаётся: destroy, потом create. По умолчанию сначала старый удаляется, и в этот промежуток сервис лежит.
Частично спасает lifecycle с create_before_destroy - тогда новый ресурс поднимется ДО удаления старого:
Код: Выделить всё
resource "yandex_compute_instance" "web" {
name = "web-1"
# ...
lifecycle {
create_before_destroy = true
}
}
Грабли третья: даже идеальный план может упасть
terraform plan показал ровно то, что ты хотел. Ты делаешь apply - и получаешь ошибку. Так бывает, и это нормально. Причины:
- Гонки (race conditions). Между plan и apply что-то поменялось - кто-то руками удалил ресурс, другой пайплайн дёрнул тот же объект.
- Лимиты и квоты. В Yandex Cloud есть квоты на число виртуалок, ядра, внешние адреса. План про них не знает, а API при apply скажет "quota exceeded".
- Eventual consistency. Облачные API не всегда мгновенно согласованы. Ресурс создан, но ещё "не виден" другому вызову - и зависимый ресурс падает с "not found".
Главное блюдо: рефакторинг и блок moved
Теперь к той самой боли из вступления. Любое переименование ресурса или перенос его в модуль для Terraform выглядит так: старого адреса в конфиге больше нет (значит, надо удалить), появился новый адрес (значит, надо создать). Итог - destroy + create. Для stateful-ресурсов это катастрофа.
Исторически лечилось это командой terraform state mv. Она работает, но у неё три беды: её надо помнить и запускать руками, она не попадает в git (коллега подтянет твой код и снова получит destroy), и ошибиться в ней легко. Императивщина в мире, который весь про декларативность.
В Terraform 1.1 (а полноценно с 1.3) появился блок moved - и это, на мой взгляд, одна из лучших фич за всю историю. Ты прямо в коде объявляешь: "ресурс переехал с адреса А на адрес Б". Terraform при plan/apply просто обновляет state, НЕ трогая саму инфраструктуру. Никакого пересоздания.
Переименование ресурса выглядит так:
Код: Выделить всё
# Было: resource "yandex_compute_instance" "web"
# Стало: resource "yandex_compute_instance" "frontend"
resource "yandex_compute_instance" "frontend" {
name = "web-1"
# ... те же аргументы
}
moved {
from = yandex_compute_instance.web
to = yandex_compute_instance.frontend
}
Блок moved умеет больше, чем простое переименование:
- Перенос ресурса внутрь модуля: from = yandex_compute_instance.db, to = module.database.yandex_compute_instance.db.
- Переезд между индексами count: from = yandex_vpc_subnet.this[0], to = yandex_vpc_subnet.this["a"] (это же и помогает мигрировать с count на for_each).
- Переименование самого модуля: from = module.old_name, to = module.new_name.
Код: Выделить всё
resource "yandex_vpc_subnet" "this" {
for_each = toset(["a", "b", "c"])
name = "subnet-${each.key}"
zone = "ru-central1-${each.key}"
network_id = yandex_vpc_network.this.id
v4_cidr_blocks = ["10.1.0.0/24"]
}
moved {
from = yandex_vpc_subnet.this[0]
to = yandex_vpc_subnet.this["a"]
}
moved {
from = yandex_vpc_subnet.this[1]
to = yandex_vpc_subnet.this["b"]
}
moved {
from = yandex_vpc_subnet.this[2]
to = yandex_vpc_subnet.this["c"]
}
Пара слов про экосистему 2026. Всё это работает не только в самом Terraform (актуальные ветки 1.x), но и в OpenTofu - открытом форке, который к лету 2026 дошёл до версии 1.12.x. moved там тоже поддерживается, так что если ты сидишь на opentofu, ничего нового учить не надо. А ещё в новых версиях есть императивные команды terraform plan -generate-config и блок import {} для обратной операции - затягивания существующих ресурсов в state декларативно. Но это уже тема отдельного урока.
Мини-лаба: проверь руками
Подними тестовый стенд в Yandex Cloud (сеть, подсеть, одна виртуалка). Затем по шагам:
- Переименуй ресурс виртуалки (поменяй local name) БЕЗ блока moved и сделай terraform plan. Убедись, что Terraform хочет destroy + create. Не применяй.
- Добавь блок moved с from старого адреса на to нового. Снова terraform plan - теперь должно быть "moved" и ноль реальных изменений. Сделай apply.
- Заверни виртуалку в модуль module.compute и пропиши moved на module.compute.yandex_compute_instance... - снова проверь, что пересоздания нет.
- Бонус: переделай подсеть с count на for_each и пропиши moved по индексам, как в примере выше.
Контрольные вопросы
- Почему count и for_each не принимают значения, известные только после apply, и как это обойти?
- Что реально делает Terraform при переименовании ресурса без блока moved и чем это опасно для stateful-ресурсов?
- Чем блок moved лучше команды terraform state mv - назови минимум два преимущества.
- Назови три причины, по которым корректный plan всё равно может упасть на apply.
- count и for_each должны быть вычислимы на этапе plan - не вешай на них значения "после apply".
- Развёртывание без простоя возможно не всегда; create_before_destroy помогает, но не для всех ресурсов.
- plan - это прогноз. apply может упасть из-за гонок, квот и eventual consistency - повторный прогон часто спасает.
- Переименование ресурса по умолчанию = destroy + create. Это лечит блок moved.
- moved переносит ресурс в terraform state без пересоздания, живёт в git и применяется автоматически у всех - забудь про ручной state mv. Работает и в Terraform 1.x, и в OpenTofu 1.12.