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/lessons/tf-intermediate-09-moved-block

lesson ── terraform-intermediate ── ~14 мин ── 5 шагов

moved блок: рефакторинг без пересоздания

Переименовать ресурс или перенести в модуль, рутина. Без декларативной поддержки Terraform увидит «logs больше нет, log_storage появился» и сделает destroy+create. Бакет с данными, уничтожится.

moved блок (TF 1.1+) объясняет terraform'у: «это тот же ресурс, просто адрес поменялся». Plan показывает «has moved», ничего не пересоздаётся. Это декларативная замена terraform state mv, с git-историей и PR-ревью.

▶ интерактивный sandbox

Поднимется пара контейнеров: terraform 1.9 и localstack 3.8 в одной сети. В браузере откроется терминал, можно сразу terraform init. Каждый шаг проверяется автоматически. TTL 45 минут, без регистрации.

запустить sandbox →

stack ── terraform · localstack · 1 GB RAM · самоуничтожается через 45 мин простоя

Шаги

  1. 01

    Создай ресурс под старым именем

    bash
    cd /home/student/tf-moved
    cat > main.tf <<'EOF'
    resource "random_id" "suffix" {
      byte_length = 4
    }
    resource "aws_s3_bucket" "logs" {
      bucket = "linuxlab-moved-${random_id.suffix.hex}"
      tags = {
        Project = "moved-demo"
      }
    }
    EOF
    terraform init
    terraform apply -auto-approve
    terraform state list

    Должно вывести:

    aws_s3_bucket.logs
    random_id.suffix

    ✓ Бакет создан под адресом aws_s3_bucket.logs.

  2. 02

    Попробуй переименовать БЕЗ moved: увидишь destroy

    Просто переименуй адрес в HCL:

    bash
    sed -i 's/"aws_s3_bucket" "logs"/"aws_s3_bucket" "log_storage"/' main.tf
    grep "aws_s3_bucket" main.tf
    bash
    terraform plan

    В выводе:

    Terraform will perform the following actions:
      # aws_s3_bucket.logs will be destroyed
      # ...
      - resource "aws_s3_bucket" "logs" { ... }
      # aws_s3_bucket.log_storage will be created
      + resource "aws_s3_bucket" "log_storage" { ... }
    Plan: 1 to add, 0 to change, 1 to destroy.

    Это destroy + create. Если был бы реальный бакет с данными, данные пропадут (S3 destroy = remove). НЕ ДЕЛАЙ APPLY сейчас.

    Это и есть проблема, которую решает moved.

    ✓ Видишь destroy+create. Это плохо. Сейчас добавим moved.

  3. 03

    Добавь moved блок

    В main.tf, рядом с resource:

    hcl
    moved {
      from = aws_s3_bucket.logs
      to   = aws_s3_bucket.log_storage
    }

    Финальный файл:

    hcl
    resource "random_id" "suffix" {
      byte_length = 4
    }
    resource "aws_s3_bucket" "log_storage" {
      bucket = "linuxlab-moved-${random_id.suffix.hex}"
      tags = {
        Project = "moved-demo"
      }
    }
    moved {
      from = aws_s3_bucket.logs
      to   = aws_s3_bucket.log_storage
    }
    bash
    terraform plan

    Теперь вывод:

    Terraform will perform the following actions:
      # aws_s3_bucket.logs has moved to aws_s3_bucket.log_storage
          bucket = "linuxlab-moved-..."
    Plan: 0 to add, 0 to change, 0 to destroy.

    0 to destroy. Это и есть magic. См. tf-moved-block.

    ✓ moved блок понят. Apply переименует в state без destroy.

  4. 04

    Apply: адрес меняется, бакет цел

    bash
    terraform apply -auto-approve

    Apply ничего не делает в облаке, только обновляет state.

    Проверь:

    bash
    terraform state list

    aws_s3_bucket.log_storage теперь в state, aws_s3_bucket.logs больше нет. Адрес изменился.

    В облаке бакет тот же самый:

    bash
    aws --endpoint-url=http://localstack:4566 s3 ls | grep linuxlab-moved
    aws --endpoint-url=http://localstack:4566 s3api get-bucket-tagging --bucket "$(aws --endpoint-url=http://localstack:4566 s3 ls | grep linuxlab-moved | awk '{print $3}')"

    Tags на месте, бакет не пересоздавался, те же creation_date и bucket-id (в LocalStack они генерятся при первом create).

    ✓ Переименование завершено, ресурс в облаке цел. Так выглядит безопасный рефакторинг state'а.

    То же самое на OpenTofu

    OpenTofu держит CLI и state совместимыми с Terraform по командам этого шага: миграция обычно проходит через mv .terraform .terraform.bak; tofu init -upgrade. Но при первом переходе сделай backup state и прогон на feature-branch - расхождения концентрируются в новых фичах (variables в backend, state-encryption, OCI registry-backed модули). См. tf-opentofu-parity для полной матрицы.

    • → OpenTofu parity
  5. 05

    moved блок можно оставить или удалить

    После успешного apply moved блок свою роль сыграл. Варианты:

    Удалить, освободит код от «архивных» блоков. terraform plan будет чистым, потому что HCL согласован со state'ом.

    Оставить, будет служить документацией: «когда-то этот ресурс назывался иначе». В большом проекте бывает полезно.

    Поэкспериментируй: удали блок и проверь plan.

    bash
    # удалить moved блок
    sed -i '/^moved {/,/^}/d' main.tf
    grep -c "moved" main.tf || echo "no moved block"
    terraform plan

    Plan: No changes. Блок можно удалять, рефакторинг закончился.

    В команде договоритесь о policy: «удаляем сразу» или «оставляем пока не сделаем cleanup-PR через 2 спринта». Главное, единый подход.

    ✓ Plan чистый и без moved блока. Рефакторинг полный.

    moved для count → for_each

    Самый частый non-rename use case. Был:

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

    Адреса: 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   = "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"] }

    Plan: 0 to destroy. Адреса меняются с индексных на ключевые, ресурсы в облаке те же.

    Важно: имена бакетов меняются (logs-0 → logs-debug). Для S3 имя, immutable, plan покажет destroy на bucket. Нужно либо оставить старые имена (через bucket = "logs-${each.key == "debug" ? 0 : each.key == "info" ? 1 : 2}", уродливо), либо принять пересоздание.

    Обычно count → for_each делается до того как ресурсы созданы. Если уже созданы, оцениваешь стоимость пересоздания против стоимости жить с count.

    • → moved block целиком
    • → Паттерны рефакторинга

Что ты узнал

moved { from = ADDR_OLD, to = ADDR_NEW } в HCL, декларативное переименование адреса в state. Plan показывает «has moved», облако не трогается. После apply блок можно удалить (или оставить как документацию). Работает для rename, переноса в модуль, count↔for_each.

команды

  • terraform planс moved: увидишь 'has moved' вместо destroy+create
  • terraform state listпосле apply адрес сменился

концепции

  • · moved: это state-операция, ресурс в облаке не меняется
  • · Тип ресурса в from и to должен совпадать: moved не для смены типа
  • · PR с moved блоком виден в diff: рефакторинг прозрачен для команды

← предыдущий

lifecycle: запреты, игнор и пересоздание

следующий →

OIDC, IAM-роль для CI без access-keys

Footer
linuxlab-
Copyright © 2026 LinuxLab. Все права защищены.
Учебники
Цены
О платформе
Конфиденциальность и куки