linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
Intro
Lessons
Footer
linuxlab-УчебникиЦеныО платформеКонфиденциальность и куки
Copyright © 2026 LinuxLab. Все права защищены.
linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
  • Введение
  • Уроки
  • How it works
  • База знаний
  • Шпаргалка
  • Capstone
  • Собеседование
home/terraform/kb/Рефакторинг/tf-moved-block

kb/refactoring ── Рефакторинг ── intermediate

moved блок: переименование без destroy

`moved { from = ..., to = ... }` в HCL декларативно говорит Terraform: «этот ресурс раньше был по одному адресу, теперь по другому, в облаке тот же». Plan покажет «move», не «destroy + create». Появился в TF 1.1. Замена ручному `terraform state mv`, оставляет след в git, повторяется у всех в команде, видно в diff.

view as markdownaka: terraform-moved, terraform-moved-block

Зачем moved

До TF 1.1 переименование ресурса = terraform state mv руками. Это работает, но:

  • Только один человек запустил, у остальных state'ы разъехались (если local backend) или они должны быстро pull.
  • В git нет следа: HCL поменялся, а почему не упал в destroy+create, знает только тот, кто crash'нул терминал командой.
  • В PR-ревью невозможно увидеть «это рефакторинг» vs «это новый ресурс».

С TF 1.1, есть moved блок. Декларативный, в HCL, видно в diff, plan показывает как move явно.

Минимальный пример: переименование

Было:

hcl
resource "aws_s3_bucket" "logs" {
  bucket = "linuxlab-logs"
}

Хочешь:

hcl
resource "aws_s3_bucket" "log_storage" {
  bucket = "linuxlab-logs"
}
moved {
  from = aws_s3_bucket.logs
  to   = aws_s3_bucket.log_storage
}

Запускаешь plan:

# aws_s3_bucket.logs has moved to aws_s3_bucket.log_storage
      bucket = "linuxlab-logs"
      ...
Plan: 0 to add, 0 to change, 0 to destroy.

Apply, ничего в облаке не меняется. State обновлён.

Без moved блока plan показал бы:

- aws_s3_bucket.logs will be destroyed
+ aws_s3_bucket.log_storage will be created

Это destroy+create, потеря данных. Moved предотвращает.

Кейсы

Переименование

Самый частый. Описан выше. После апплая moved блок можно удалить (но многие команды оставляют в HCL как документ, «когда-то это переименовали»).

Перенос в модуль

Был ресурс в root, вынес в модуль:

hcl
# root
module "buckets" {
  source = "./modules/buckets"
}
moved {
  from = aws_s3_bucket.logs
  to   = module.buckets.aws_s3_bucket.logs
}

Внутри модуля:

hcl
# modules/buckets/main.tf
resource "aws_s3_bucket" "logs" {
  bucket = "linuxlab-logs"
}

terraform plan → 0 to add, 0 to change, 0 to destroy. Ресурс переместился в state из root-уровня в module.buckets.

Из модуля наружу

Обратное: разбираешь модуль, ресурс возвращается в root:

hcl
moved {
  from = module.buckets.aws_s3_bucket.logs
  to   = aws_s3_bucket.logs
}

count → for_each

Один из самых полезных кейсов. Раньше:

hcl
resource "aws_s3_bucket" "logs" {
  count  = 3
  bucket = "linuxlab-logs-${count.index}"
}

Адреса в state: aws_s3_bucket.logs[0], [1], [2].

Хочешь for_each для стабильности ключей:

hcl
variable "log_levels" {
  type    = set(string)
  default = ["debug", "info", "error"]
}
resource "aws_s3_bucket" "logs" {
  for_each = var.log_levels
  bucket   = "linuxlab-logs-${each.key}"
}
moved {
  from = aws_s3_bucket.logs[0]
  to   = aws_s3_bucket.logs["debug"]
}
moved {
  from = aws_s3_bucket.logs[1]
  to   = aws_s3_bucket.logs["info"]
}
moved {
  from = aws_s3_bucket.logs[2]
  to   = aws_s3_bucket.logs["error"]
}

Важно: имена бакетов меняются! linuxlab-logs-0 → linuxlab-logs-debug. Если бакеты уже созданы с числовыми именами. Нужно либо оставить старые имена в HCL, либо принять bucket = изменение (которое для S3 = destroy+create, потому что имя, immutable).

Чаще count → for_each делают до того как ресурсы созданы. Если уже созданы, оставь имена в HCL такими как в облаке.

Что moved умеет, что нет

МожетНе может
Переименовать ресурс в stateИзменить тип ресурса (aws_s3_bucket → aws_s3_bucket_v2)
Переместить в модуль / из модуляСменить provider
Переключить count ↔ for_eachИзменить provider configuration
Перенести между разными root'амиОбъединить несколько ресурсов в один

Для смены типа, destroy+create или import. Для смены provider, обычно тоже destroy+create, потому что под капотом это другой API.

С чем сочетается

  • С removed блоком (tf-removed-block). moved переносит, removed забирает из state. Часто рядом в одном PR.
  • С import блоком (tf-state-import): иногда меняешь HCL и одновременно захватываешь существующий ресурс.
  • С lifecycle.ignore_changes, moved не отменяет ignore_changes, они продолжают применяться после переноса.

Подводные камни

  • moved не удаляется автоматически. После apply блок остаётся в HCL. Можно удалить вручную; некоторые команды оставляют в архивных файлах (refactoring.tf) для аудита. Безопасно удалять через 1-2 спринта после miграции.

  • moved не переносит данные. Это state-only операция. Если у тебя aws_s3_bucket.logs со 100GB данных, и ты делаешь moved, данные в том же бакете. Меняется только адрес в state.

  • Цепочки moved работают. A → B, B → C в одном HCL. Terraform разрулит. Но это плохой стиль: при возможности, один прямой A → C.

  • moved между провайдерами не работает. aws.us и aws.eu, разные provider instances. Move между ними = destroy+create (бакет в одном регионе не «переезжает» в другой).

  • moved не работает с разными типами. aws_s3_bucket.x → aws_s3_bucket_acl.x , нет. Тип ресурса должен совпадать в from и to.

  • moved блок проверяется при init. Опечатка в имени ресурса, terraform plan упадёт ещё до plan'а.

  • Идеален для PR-ревью. Видно в diff: «вот ресурс был тут, теперь тут, это рефакторинг, не destroy». Объясняет интент. Поэтому предпочтительнее ручного state mv.

§ команды

bash
terraform plan

С moved блоком: покажет 'has moved' вместо destroy+create.

bash
terraform apply

Применяет move. В облаке ничего не меняется. State обновляется.

bash
terraform state list

Проверка после apply: адрес сменился.

bash
git log -p main.tf

След рефакторинга остаётся в git. Кто-нибудь через год спросит 'почему было logs стал log_storage': ответ в diff.

§ см. также

  • tf-state-manipulationstate mv, state rm, state pull/push: ручные операции`terraform state mv` переименовывает адрес ресурса в state (без destroy/recreate). `terraform state rm` убирает ресурс из state (но не из облака). `terraform state pull/push`, скачать/залить state как файл. Все четыре, резкие операции, делать через backup и понимая зачем. Для декларативных альтернатив есть [[tf-moved-block]] и [[tf-removed-block]].
  • tf-refactor-patternsПаттерны рефакторинга: count→for_each, split files, extract moduleБольшие конфиги превращаются в спагетти. Базовые рефакторинги: count→for_each (стабильные ключи), разделение на файлы по доменам (network/compute/storage), вынос повторяющегося блока в модуль, объединение мелких ресурсов в составной, удаление мёртвых импортов. Каждый, пошагово, с проверкой `plan` на каждом шагу.
  • tf-count-for-eachcount и for_each: несколько ресурсов из одного блокаcount создаёт N одинаковых ресурсов по индексам 0..N-1. for_each: ресурсы по ключам из set или map. Правило: count для одинаковых, for_each когда у каждого свои настройки. Если сомневаешься, бери for_each.
Footer
linuxlab-
Copyright © 2026 LinuxLab. Все права защищены.
Учебники
Цены
О платформе
Конфиденциальность и куки