open atlas
↑ К треку
AWS на практике AWS · 06 · 03

Операции с состоянием Terraform: import, move и ловушка пересоздания

Состояние сопоставляет конфиг реальным ID ресурсов и авторитетно: рефакторинг, переименовавший ресурс без блока moved, велит Terraform удалить прод и пересоздать его пустым. Освой import, state mv, state rm и дробление состояния, и относись к состоянию как к секрету.

AWS Senior ◷ 19 min
Уровень
ОсновыJuniorMiddleSenior

Ты открываешь аккуратный PR-рефакторинг: он переименовывает aws_db_instance.db в aws_db_instance.primary — замена в одно слово ради читаемости, больше ничего не тронуто. Ревью одобряет за тридцать секунд; переименование «очевидно» ничего не может сделать. CI запускает terraform apply при мердже. План, который никто не открыл, пишет -/+ destroy and then create replacement для продового инстанса RDS. Terraform видит, что старый адрес исчез из конфига, а новый адрес отсутствует в состоянии, и делает ровно то, что ты ему велел: удаляет живую базу и поднимает свежую пустую. Через сорок секунд приложение сыплет ошибками соединения на пустую схему, последнему автоматическому снапшоту шесть часов, а ты восстанавливаешь прод из бэкапа, пока клиенты смотрят на страницу обслуживания. Это сделало переименование — потому что источник истины это состояние, а не твой конфиг, и никто не сказал ему, что ресурс просто переехал.

Состояние — это сопоставление, и сопоставление авторитетно

В прошлом уроке ты уже настроил удалённый бэкенд S3 с блокировкой. Теперь опасная часть: эксплуатировать это состояние, не теряя данные. Файл состояния — это карта. Для каждого блока ресурса в конфиге он хранит привязку к реальному облачному объекту — адрес aws_db_instance.primary ↔ фактический идентификатор RDS db-prod-7f3a, плюс каждый атрибут и ребро зависимости. terraform plan — это трёхсторонний diff: он сравнивает твой конфиг, состояние и реальность (обновлённую из живого API), затем планирует минимальный набор вызовов API, чтобы их согласовать.

Ловушка в том, что именно состояние, а не конфиг, решает, чем является каждый ресурс. Правило согласования жестоко буквально:

  • Ресурс в конфиге, но не в состоянии → Terraform считает его новым → CREATE.
  • Ресурс в состоянии, но не в конфиге → Terraform считает, что ты его удалил → DESTROY.
  • Ресурс в обоих, с изменёнными неизменяемыми атрибутами → REPLACE (-/+, удалить затем создать).

Все три правила объясняют, почему каждая катастрофа с состоянием начинается одинаково: кто-то меняет конфиг, не обновив карту. Без правила 2 удаление неотличимо от переименования.

Так что когда ты переименовываешь aws_db_instance.db в .primary в конфиге, не сказав состоянию, Terraform видит два независимых факта: адрес .db пропал из конфига (→ удалить реальную БД, на которую он всё ещё указывает) и .primary отсутствует в состоянии (→ создать новую пустую БД). Буквальный вывод — две операции, одно удаление и одно создание, на том, что ты хотел оставить единым ресурсом, сохраняющим свою идентичность. Проблемой было не переименование, а то, что сопоставление не переместили.

import, state mv и декларативный блок moved

Три операции держат сопоставление честным при изменениях. terraform import усыновляет существующий неуправляемый ресурс — созданный руками в консоли или другим инструментом — в состояние, чтобы Terraform управлял им, а не пытался пересоздать дубликат, который столкнётся на уникальном имени:

# Усынови созданный руками бакет в управление — не дай TF пересоздать его.
terraform import aws_s3_bucket.assets acme-prod-assets
# Теперь `plan` показывает no-op (или только дрейф конфига), а не CREATE.

terraform state mv переименовывает адрес в состоянии, не трогая реальный ресурс — императивное исправление для Hook:

terraform state mv aws_db_instance.db aws_db_instance.primary
# Состояние теперь сопоставляет .primary → живой db-prod-7f3a; plan — no-op.

Современная, обозреваемая форма — декларативный блок moved {}, закоммиченный рядом с переименованием, чтобы перемещение ехало вместе с PR и выполнялось на apply у всех, включая CI — без внеполосного CLI-шага, который товарищ забудет:

resource "aws_db_instance" "primary" {  # переименован из "db"
  identifier = "db-prod-7f3a"
  # ...без изменений...
}

moved {
  from = aws_db_instance.db
  to   = aws_db_instance.primary
}

С блоком moved план гласит aws_db_instance.db has moved to aws_db_instance.primary и показывает ноль destroy/create — сопоставление обновлено, живая база не тронута. Блок moved работает и когда ты затягиваешь ресурс в модуль: to = module.data.aws_db_instance.primary. Режим отказа — чисто отсутствие этого: переименуй без него, и ловушка пересоздания сработает на следующем apply.

Почему это работает

Почему предпочесть moved {}, а не terraform state mv, если оба чинят одно и то же? Потому что state mv — императивная внеполосная команда: один инженер запускает её на своём локальном CLI, и если соответствующее переименование конфига смерджится раньше, чем все запустят тот же mv — или если CI применит первым — те, у кого его нет, получат план destroy/create. Состояние и конфиг расходятся по принципу кто-что-запустил. Блок moved декларативен и под версионным контролем: он живёт в .tf-файлах, мерджится атомарно с переименованием в том же коммите, виден в ревью, и каждый apply (ноутбук или CI) выполняет перемещение идемпотентно. Через релиз-другой его можно удалить. Прибереги state mv для хирургии, которую нельзя выразить в конфиге — разбиение одного адреса на for_each или аварийное восстановление — а moved сделай выбором по умолчанию для рутинных рефакторингов.

Состояние хранит секреты открытым текстом, а state rm осиротит намеренно

Вот свойство, делающее состояние обузой, а не просто индексом: оно хранит атрибуты ресурсов открытым текстом, включая сгенерированные секреты. У aws_db_instance с password, сгенерированный tls_private_key, секрет aws_iam_access_key — всё попадает в файл состояния как читаемый JSON. Terraform помечает их sensitive, чтобы они не печатались в выводе plan, но в самом файле состояния они открытым текстом. Последствия конкретны: файл состояния, закоммиченный в git, утекает каждый секрет в нём любому, кто клонирует репозиторий, навсегда в истории; файл состояния в нешифрованном, открытом миру S3-бакете — та же утечка по HTTP. Поэтому состояние — это секрет: шифруй его на покое (S3 SSE, который ты задал на бэкенде), запри доступ к бакету только для CI-роли и платформенной команды, никогда не коммить *.tfstate в git и ротируй любой credential, что когда-либо касался открытого состояния. Один утёкший файл состояния = каждый пароль, ключ и токен внутри скомпрометированы.

Контрапарт import — это terraform state rm, который удаляет ресурс из состояния не уничтожая реальную вещь. Ты намеренно его осиротяешь — Terraform забывает, что он существует, но живой ресурс продолжает работать и тарифицироваться:

# Перестань управлять этой БД здесь, не удаляя её — передай другому стеку.
terraform state rm aws_db_instance.primary
# `plan` больше её не упоминает; живой db-prod-7f3a не тронут.
# В ДРУГОМ стеке: terraform import aws_db_instance.primary db-prod-7f3a

Senior-применение — передача ресурса между стеками при разбиении: state rm в источнике (осиротить, не уничтожать), затем import в целевой. Режим отказа — путаница с destroy: state rm, а затем apply в конфиге, который всё ещё ссылается на ресурс, пересоздаст или продублирует его, а terraform destroy на ресурсе, который ты хотел осиротить, удалит живую вещь. rm забывает; destroy убивает.

Радиус поражения: одно гигантское состояние против дроблёного

Последний senior-выбор — насколько многим управляет один файл состояния. Монолитное состояние на всю организацию ставит каждый ресурс — сеть, базы, все приложения — за одну блокировку и один план. Издержки накапливаются: каждый apply обновляет и рискует всем (опечатка в плане модуля приложения теперь перечисляет 500 ресурсов, любой из которых неосторожный approve может тронуть), планы занимают минуты, потому что обновляют сотни ресурсов против API, а единственный файл состояния — единая точка катастрофы: испорти или потеряй его, и весь парк разом неуправляем. Есть и состязание за блокировку: одна общая блокировка означает, что один застрявший apply (упавшая CI-задача, не отпустившая блокировку) блокирует всю организацию от применений, пока кто-то не запустит terraform force-unlock <LOCK_ID> — что ты делаешь только убедившись, что ни один apply на самом деле не в полёте, ведь force-unlock живого apply портит состояние.

Дробление состояния по компоненту и окружению — network/, data/, app/ на окружение, каждое со своим состоянием и блокировкой — сужает радиус поражения: деплой приложения никогда не тронет VPC, планы падают до секунд, потому что каждый обновляет десятки, а не сотни ресурсов, а испорченное состояние повреждает один слой, а не все. Цена — обвязка: слои, которым нужны выводы друг друга, читают их между состояниями через data-источник terraform_remote_state, что добавляет явный порядок (применять network до app) и связность для управления:

# В стеке app: читаем выводы VPC из состояния стека network.
data "terraform_remote_state" "network" {
  backend = "s3"
  config = {
    bucket = "acme-tf-state"
    key    = "prod/network/terraform.tfstate"
    region = "eu-central-1"
  }
}

resource "aws_instance" "api" {
  subnet_id = data.terraform_remote_state.network.outputs.private_subnet_id
}

Senior-эвристика: дроби по жизненному циклу и радиусу поражения. То, что меняется с разной скоростью и было бы катастрофой вместе, относится к разным состояниям — VPC (редко меняется, ломает всё) отдельно от приложения (деплоится ежечасно), данные отдельно от вычислений. Не дроби так мелко, чтобы каждое изменение стало оркестрацией из пяти состояний; дроби на швах, где радиус поражения и частота изменений реально различаются.

Выбери лучший вариант

Нужно переименовать адрес ресурса управляемого продового RDS в конфиге (aws_db_instance.db → aws_db_instance.primary) ради читаемости, с нулевой потерей данных и нулевым простоем, и изменение должно применяться корректно для каждого товарища и в CI. Выбери подход.

Викторина

Товарищ создал S3-бакет руками в консоли. Ты хочешь, чтобы Terraform управлял им дальше, не пересоздавая. Какая команда берёт его под управление?

Вспомните перед уходом
  1. 01
    Почему переименование ресурса в конфиге без блока moved удаляет и пересоздаёт его, и каковы два безопасных исправления?
  2. 02
    Почему файл состояния — секрет, и как import, state rm и дробление состояния связаны с безопасными операциями состояния?
Итог

Файл состояния — это авторитетная карта от каждого адреса ресурса конфига к реальному ID облачного ресурса, а terraform plan — это трёхсторонний diff конфига, состояния и реальности, согласующий их буквально: адрес в конфиге, но не в состоянии планирует CREATE, а адрес в состоянии, но не в конфиге планирует DESTROY. Это буквальное правило — корень большинства катастроф состояния: переименуй ресурс в конфиге, не сказав состоянию, и Terraform прочтёт это как удаление старого адреса и создание нового, поэтому косметическое переименование продового инстанса RDS планирует -/+ (удалить затем пересоздать пустым) и теряет базу на apply. Три операции держат сопоставление честным: terraform import усыновляет существующий неуправляемый ресурс в состояние, чтобы он не пересоздавался как сталкивающийся дубликат; terraform state mv (или, предпочтительно, декларативный блок moved , закоммиченный с переименованием) переименовывает адрес в состоянии, не трогая реальный ресурс, что и есть исправление ловушки пересоздания; а terraform state rm удаляет ресурс из состояния, не уничтожая его, для намеренного осиротения ресурса перед передачей другому стеку. Состояние ещё и секрет — оно хранит атрибуты, включая сгенерированные пароли и приватные ключи, открытым текстом — поэтому оно должно быть зашифровано на покое, с запертым доступом, и никогда не закоммичено в git, а любой credential в утёкшем состоянии скомпрометирован. Наконец, радиус поражения — это проектное решение: монолитное состояние делает так, что каждый apply рискует всем, занимает минуты на план, является единой точкой катастрофы и сериализует всю организацию за одной блокировкой (восстановимо через force-unlock, осторожно), тогда как дробление состояния по жизненному циклу и радиусу поражения — network, data, app на окружение, соединённые data-источниками terraform_remote_state — сужает риск и урезает время плана до секунд ценой порядка и кросс-состоянной обвязки. Теперь, когда увидишь PR с переименованием ресурса в .tf-файле, первым делом спросишь: где блок moved?

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 5 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.