Это и есть боль, которую решает сегодняшний урок. Дефолтное поведение Terraform при замене ресурса - destroy, потом create. Логично для диска в шкафу, катастрофично для живого трафика. К счастью, в инфраструктуре как код (а это и есть iac, ради которого мы тут собрались) есть аккуратный набор инструментов, чтобы менять ресурсы без секунды простоя. Разберём их по порядку и доведём до полноценной zero-downtime стратегии для группы виртуалок в Yandex Cloud.
Почему apply вообще что-то ломает
Сначала про механику. Когда ты меняешь атрибут, который провайдер помечает как ForceNew (например image_id у диска), Terraform не может изменить ресурс на месте. Он обязан его пересоздать. В terraform plan ты увидишь страшное:
Код: Выделить всё
-/+ resource "yandex_compute_disk" "data" {
~ image_id = "fd8..." -> "fd9..." # forces replacement
}
Управляет порядком и защитой специальный мета-блок lifecycle. Он не относится к конкретному провайдеру - это часть ядра Terraform, и работает одинаково в terraform и opentofu. Внутри живут три ключевые ручки: create_before_destroy, prevent_destroy и ignore_changes.

create_before_destroy: поменять местами создание и удаление
Идея простая до гениальности. Разверни порядок: сначала подними новый ресурс, дождись, что он готов, переключи на него ссылки, и только потом снеси старый. Один флаг:
Код: Выделить всё
resource "yandex_compute_instance" "app" {
name = "app-vm"
platform_id = "standard-v3"
resources {
cores = 2
memory = 4
}
boot_disk {
initialize_params {
image_id = var.image_id
size = 20
}
}
network_interface {
subnet_id = yandex_vpc_subnet.app.id
nat = true
}
lifecycle {
create_before_destroy = true
}
}
Подвох, о который спотыкаются все: ресурсы с уникальными именами. Если у тебя name = "app-vm" жёстко прописан, то новый инстанс не создастся - имя занято старым, который ещё жив. Решение - давать имена с суффиксом, который меняется вместе с образом, либо отдавать генерацию имени самому облаку. На одиночной виртуалке create_before_destroy полезен, но настоящий zero-downtime начинается, когда за дело берётся группа экземпляров и балансировщик. К этому и идём.
prevent_destroy и ignore_changes: предохранители
Пока мы тут, закроем две соседние ручки lifecycle - они спасают нервы.
prevent_destroy = true превращает ресурс в неуязвимый: любой план, который пытается его удалить, падает с ошибкой ещё до apply. Ставь на то, что больно терять - продакшен-базу, бакет с бэкапами, managed-кластер.
Код: Выделить всё
resource "yandex_mdb_postgresql_cluster" "main" {
name = "prod-pg"
# ... остальная конфигурация ...
lifecycle {
prevent_destroy = true
}
}
ignore_changes - про мир, а не про порядок. Бывает, что какой-то атрибут крутит снаружи кто-то ещё: автоскейлер меняет размер группы, оператор правит метку руками, облако само дописывает поле. Terraform на каждом plan будет упорно возвращать "как в коде". Скажи ему игнорировать конкретные поля:
Код: Выделить всё
resource "yandex_compute_instance" "app" {
# ...
lifecycle {
ignore_changes = [
metadata,
boot_disk[0].initialize_params[0].image_id,
]
}
}
Zero-downtime для группы: deploy_policy и health checks
А вот теперь главное блюдо. Для реального продакшена одну виртуалку обычно не держат - берут yandex_compute_instance_group, группу одинаковых машин за балансировщиком. И самое приятное: для плавной выкатки новой версии тут create_before_destroy уже почти не нужен. Группа умеет катить обновление сама, а правила задаёт блок deploy_policy.
Когда ты меняешь instance_template (например тот самый image_id), группа не сносит все машины разом. Она идёт по политике развёртывания. Ключевые поля:
- max_unavailable - сколько работающих инстансов можно одновременно вывести из строя во время обновления. Поставь 0 (или близко к нулю), если простой недопустим вообще.
- max_expansion - сколько машин можно временно поднять сверх целевого размера группы. Это и есть аналог create_before_destroy на уровне группы: сначала создаём лишние, потом гасим старые.
- max_creating и max_deleting - потолки на одновременное создание и удаление, чтобы не перегружать квоты.
- startup_duration - сколько секунд дать машине на старт; пока не прошло это время и не прошли health-чеки, трафик на неё не пускают.
- strategy - proactive (по умолчанию, группа может сама принудительно гасить инстанс) или opportunistic (ждёт, пока инстанс освободится сам).
Код: Выделить всё
resource "yandex_compute_instance_group" "app" {
name = "app-ig"
folder_id = var.folder_id
service_account_id = yandex_iam_service_account.ig.id
instance_template {
platform_id = "standard-v3"
resources {
cores = 2
memory = 4
}
boot_disk {
mode = "READ_WRITE"
initialize_params {
image_id = var.image_id
size = 20
}
}
network_interface {
network_id = yandex_vpc_network.app.id
subnet_ids = [yandex_vpc_subnet.app.id]
}
}
scale_policy {
fixed_scale {
size = 4
}
}
allocation_policy {
zones = ["ru-central1-a"]
}
deploy_policy {
max_unavailable = 0
max_expansion = 2
max_creating = 2
max_deleting = 2
startup_duration = 30
strategy = "proactive"
}
health_check {
interval = 5
timeout = 2
healthy_threshold = 2
unhealthy_threshold = 3
http_options {
port = 80
path = "/health"
}
}
load_balancer {
target_group_name = "app-tg"
}
}
Ещё две полезные ручки рядом: max_checking_health_duration на самой группе (таймаут ожидания, пока VM станет healthy, иначе её прибьют по политике) и внутри load_balancer - max_opening_traffic_duration (сколько ждать, пока балансировщик проверит машину).
Типичные грабли
- Жёсткое name и create_before_destroy на одиночной VM. Имя занято старым ресурсом - новый не создаётся. Либо суффикс в имени, либо отдай нейминг группе.
- Цепная реакция замены. Флаг create_before_destroy "заразен": если ресурс с ним зависит от другого, тот тоже начнёт создаваться раньше удаления. Иногда это неожиданно перестраивает половину графа. Читай plan внимательно.
- max_unavailable не равен нулю там, где нужен ноль. Дефолты могут позволить гасить машины раньше времени. Для строгого zero-downtime ставь явно 0 и давай группе место через max_expansion.
- Нет /health эндпоинта. Health check бьёт в путь, которого нет, машина вечно unhealthy, выкатка стоит колом или машины гасятся как битые. Сначала ручка здоровья в приложении, потом health_check.
- prevent_destroy мешает осознанному пересозданию. Когда ресурс реально надо заменить, флаг придётся снять в коде - terraform destroy его не обойдёт. Это не баг, это смысл.
- Подними одиночный yandex_compute_instance без lifecycle. Поменяй image_id, посмотри plan - убедись, что порядок -/+ (destroy-before-create).
- Добавь lifecycle { create_before_destroy = true }. Снова смени image_id, сравни plan - порядок должен стать +/-.
- Навесь prevent_destroy = true на этот инстанс и попробуй terraform destroy. Полюбуйся на ошибку, потом убери флаг.
- Собери yandex_compute_instance_group с deploy_policy (max_unavailable = 0, max_expansion = 2) и health_check. Поменяй образ, запусти apply и параллельно дёргай сервис curl-ом в цикле - убедись, что 5xx не прилетает.
- Какой порядок операций по умолчанию у Terraform при замене ресурса и почему он опасен для живого трафика?
- Зачем нужен max_expansion в deploy_policy и как он связан с идеей create_before_destroy?
- Чем отличаются prevent_destroy и ignore_changes - что каждый из них защищает?
- Что произойдёт при выкатке, если у приложения нет рабочего health-эндпоинта, на который смотрит health_check?
Дефолт Terraform - удалить, потом создать, и это рвёт трафик. Флаг lifecycle { create_before_destroy = true } разворачивает порядок для одиночных ресурсов. prevent_destroy - предохранитель от случайного удаления критичного (в OpenTofu 1.12 его наконец можно задавать переменной), ignore_changes - чтобы Terraform не воевал с тем, что меняют снаружи. Но настоящий zero-downtime в Yandex Cloud живёт в yandex_compute_instance_group: deploy_policy с max_unavailable = 0 и max_expansion, плюс health_check и связка с балансировщиком через target group дают плавную выкатку без единой секунды простоя. Меняй образ смело - инфраструктура как код больше не значит "сломать продакшен одной командой".