lesson ── terraform-intermediate ── ~14 мин ── 5 шагов
Переименовать ресурс или перенести в модуль, рутина. Без декларативной поддержки 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 минут, без регистрации.
stack ── terraform · localstack · 1 GB RAM · самоуничтожается через 45 мин простоя
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.
Просто переименуй адрес в HCL:
sed -i 's/"aws_s3_bucket" "logs"/"aws_s3_bucket" "log_storage"/' main.tf
grep "aws_s3_bucket" main.tf
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.
В main.tf, рядом с resource:
moved {from = aws_s3_bucket.logs
to = aws_s3_bucket.log_storage
}
Финальный файл:
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
}
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.
terraform apply -auto-approve
Apply ничего не делает в облаке, только обновляет state.
Проверь:
terraform state list
aws_s3_bucket.log_storage теперь в state, aws_s3_bucket.logs
больше нет. Адрес изменился.
В облаке бакет тот же самый:
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 держит CLI и state совместимыми с Terraform по командам
этого шага: миграция обычно проходит через mv .terraform .terraform.bak; tofu init -upgrade. Но при первом переходе
сделай backup state и прогон на feature-branch - расхождения
концентрируются в новых фичах (variables в backend,
state-encryption, OCI registry-backed модули). См.
tf-opentofu-parity для полной матрицы.
После успешного apply moved блок свою роль сыграл. Варианты:
Удалить, освободит код от «архивных» блоков. terraform plan
будет чистым, потому что HCL согласован со state'ом.
Оставить, будет служить документацией: «когда-то этот ресурс назывался иначе». В большом проекте бывает полезно.
Поэкспериментируй: удали блок и проверь plan.
# удалить moved блок
sed -i '/^moved {/,/^}/d' main.tfgrep -c "moved" main.tf || echo "no moved block"
terraform plan
Plan: No changes. Блок можно удалять, рефакторинг закончился.
В команде договоритесь о policy: «удаляем сразу» или «оставляем пока не сделаем cleanup-PR через 2 спринта». Главное, единый подход.
✓ Plan чистый и без moved блока. Рефакторинг полный.
Самый частый non-rename use case. Был:
resource "aws_s3_bucket" "logs" {count = 3
bucket = "logs-${count.index}"}
Адреса: aws_s3_bucket.logs[0], [1], [2].
Хочется for_each ради стабильности ключей при удалении:
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 { from = ADDR_OLD, to = ADDR_NEW } в HCL, декларативное
переименование адреса в state. Plan показывает «has moved», облако не
трогается. После apply блок можно удалить (или оставить как документацию).
Работает для rename, переноса в модуль, count↔for_each.
команды
terraform planс moved: увидишь 'has moved' вместо destroy+createterraform state listпосле apply адрес сменилсяконцепции