Terraform и OpenTofu хотят пересоздать ресурс: как разобраться с планом

В конфигурации изменили одно имя, а plan предлагает удалить сервер и создать новый. Число строк в Git diff плохо предсказывает масштаб изменения инфраструктуры. Решение зависит от адреса ресурса в state, свойств провайдера и того, какое отличие обнаружено при построении плана.

Начните с точного вопроса: заменяется один существующий объект или план показывает независимые delete и create по двум разным адресам? В первом случае инструмент узнал ресурс, но не может обновить его указанным способом. Во втором он мог потерять связь после переименования блока, изменения ключа for_each или структуры модуля.

Сохраним план, который будем обсуждать

Работайте с тем же backend, workspace, набором переменных и lock-файлом провайдеров, что использует команда. Иначе можно исследовать другое окружение. Выберите команды своего инструмента; не запускайте оба набора подряд в одном рабочем каталоге.

terraform workspace show
terraform plan -out=review.tfplan
terraform show -no-color review.tfplan
terraform show -json review.tfplan > review.json
tofu workspace show
tofu plan -out=review.tfplan
tofu show -no-color review.tfplan
tofu show -json review.tfplan > review.json

Plan читает удалённые объекты через провайдеры и может потребовать рабочие credentials, но не применяет предложенные изменения. Сохранённый план позволяет обсуждать один результат, а не разные пересчёты на меняющемся окружении. Файлы плана и JSON могут содержать чувствительные значения: храните их как служебные данные с ограниченным доступом.

Выделим действия с заменой

Скрипт ниже читает review.json и выводит только те записи, у которых в actions одновременно есть create и delete. Он не делает вывод о допустимости замены: помогает сократить большой план до конкретных адресов. Сохраните его как replacements.py рядом с JSON.

import json
from pathlib import Path

plan = json.loads(Path("review.json").read_text(encoding="utf-8-sig"))
for resource in plan.get("resource_changes", []):
    change = resource["change"]
    actions = change["actions"]
    if "create" in actions and "delete" in actions:
        print(resource["address"])
        print("actions:", " -> ".join(actions))
        print("reason:", resource.get("action_reason", "not provided"))
        print("replace_paths:", change.get("replace_paths", []))

Порядок delete → create означает удаление перед созданием, create → delete — обратный порядок. replace_paths, когда они доступны, указывают поля, участвующие в необходимости замены. Пустой список не доказывает отсутствие причины: замену могли запросить явно или вызвать другим механизмом. Формат описан отдельно для Terraform и OpenTofu.

Теперь объясним найденное отличие

Откройте текстовый блок нужного адреса и найдите пометку forces replacement. Сравните старое и новое значение поля с конфигурацией и входными переменными. Если поменялся идентификатор образа или другой неизменяемый атрибут, провайдер может требовать новый объект. Проверять это нужно по конкретному типу и версии провайдера: универсального списка заменяемых полей для всех ресурсов нет.

Если конфигурация не менялась, посмотрите фактическое состояние удалённого объекта и изменения вне Terraform или OpenTofu. Plan обычно обновляет представление о текущих объектах, поэтому ручная правка в облаке тоже влияет на результат. Отключение refresh ради исчезновения неудобного diff оставит вас с устаревшей картиной.

Проверьте команду CI и историю: флаг -replace явно запрашивает замену адреса. Ещё одна возможная причина — объект, отмеченный как tainted после проблемного запуска. Исправление в этих случаях отличается от изменения свойства: надо выяснить, почему экземпляр сочли повреждённым и сохранилось ли это основание.

Когда нужен moved, а когда изменение архитектуры

Если после переименования блока старый адрес удаляется, а новый создаётся с теми же параметрами, рассмотрите moved. Он описывает новую привязку существующего объекта. Но moved не отменяет замену из-за изменённого неизменяемого свойства. Отделите рефакторинг адресов от функционального изменения и постройте планы по отдельности: тогда причина каждого действия видна.

create_before_destroy меняет порядок замены, но не гарантирует непрерывную доступность. У ресурса могут быть уникальное имя, лимит квоты, зависимый том или ограничения сети. Прежде чем использовать такой режим, выясните, могут ли старый и новый объекты существовать одновременно и как переключается трафик.

После исправления пересчитайте plan с теми же входами и повторно проверьте адреса. Если замена необходима, планируйте перенос данных и переключение отдельно. Если это было переименование, ожидайте сообщение о переносе без лишних destroy/create. Для согласования сохраните причину решения рядом с конкретным планом; одно итоговое число ресурсов не объясняет риск изменения.

Ещё по теме

Продолжить изучение

Хотите управлять инфраструктурой через код и понимать последствия изменений? Plan, state, модули и работа с существующими ресурсами — темы программы «Terraform и OpenTofu: инфраструктура как код».

Продолжить на практике

  • Terraform и OpenTofu: инфраструктура как код ↗

    Описывайте инфраструктуру в коде, проверяйте изменения до применения и работайте со state без сюрпризов. HCL, модули, remote state, импорт, CI и различия Terraform/OpenTofu — на воспроизводимом учебном окружении.