Строка «must be replaced» в плане базы данных — повод остановиться, а не нажать yes
lifecycleУчебный сервис этого модуля — образ, том с данными и контейнер, который его монтирует:
resource "docker_image" "nginx" {
name = "nginx:1.28-alpine"
keep_locally = true
}
# Данные сервиса: удалять случайно нельзя
resource "docker_volume" "data" {
name = "price-data"
lifecycle {
prevent_destroy = true
}
}
resource "docker_container" "web" {
name = "price-web"
image = docker_image.nginx.image_id
restart = "no"
ports {
internal = 80
external = 8080
ip = "127.0.0.1"
}
volumes {
volume_name = docker_volume.data.name
container_path = "/usr/share/nginx/html"
}
}
Перед любым действием Terraform строит граф: ресурсы — вершины, ссылки — рёбра. terraform graph выводит его в формате Graphviz (картинку рисует dot -Tsvg из пакета graphviz):
$ terraform graph
digraph G {
rankdir = "RL";
node [shape = rect, fontname = "sans-serif"];
"docker_container.web" [label="docker_container.web"];
"docker_image.nginx" [label="docker_image.nginx"];
"docker_volume.data" [label="docker_volume.data"];
"docker_container.web" -> "docker_image.nginx";
"docker_container.web" -> "docker_volume.data";
}
Контейнер зависит от образа и тома, образ и том друг от друга не зависят. Поэтому Terraform создаёт образ и том параллельно (по умолчанию до 10 операций одновременно, ключ -parallelism), контейнер — после них, а удаляет в обратном порядке. В облаке, где ресурс создаётся минутами, параллельность экономит много времени.
| Символ | Действие | На что смотреть |
|---|---|---|
+ | Создать | Ожидали ли столько новых ресурсов |
~ | Изменить на месте | Какие именно атрибуты: стрелка старое -> новое |
-/+ | Удалить и создать заново | Простой и потеря данных; причина — пометка # forces replacement |
+/- | Создать новый, потом удалить старый | То же, но без простоя (create_before_destroy) |
- | Удалить | Каждую строку — особенно базы, тома, бакеты, DNS |
<= | Прочитать источник данных | Модуль 07 |
Начинают с последней строки — Plan: N to add, M to change, K to destroy. Если вы поменяли порт, а план собирается удалить пять ресурсов, — что-то не так, и разбираться надо до apply. Из-за таких планов в работе с настоящей инфраструктурой не пишут -auto-approve.
Обычный apply строит план заново, и между вашим plan и apply мир мог измениться. plan -out сохраняет план в файл, show показывает его, а apply с именем файла выполняет ровно его, без вопроса:
$ terraform plan -out=tfplan | tail -4
Saved the plan to: tfplan
To perform exactly these actions, run the following command to apply:
terraform apply "tfplan"
$ terraform show tfplan -no-color | grep -E "^ # |Plan:"
# docker_container.web will be created
# docker_image.nginx will be created
# docker_volume.data will be created
Plan: 3 to add, 0 to change, 0 to destroy.
$ terraform apply tfplan | grep -E "Creat|complete"
docker_volume.data: Creating...
docker_image.nginx: Creating...
docker_volume.data: Creation complete after 0s [id=price-data]
docker_image.nginx: Creation complete after 0s [id=sha256:dc73b49f5124cf2ee538dfbdfbd121f0b4ccdcb20fea30f3a81bd477c02e2bb5nginx:1.28-alpine]
docker_container.web: Creating...
docker_container.web: Creation complete after 0s [id=1dbd8dc4a3c21eaf78eedd6d92c65f74bd1cee9de786e84052c0fc3f121ab93e]
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
Так работают в CI: задание plan сохраняет файл, человек читает план в merge request, задание apply применяет тот же файл (модуль 08). Файл плана — двоичный, в нём есть значения всех атрибутов, включая пароли; в Git его не кладут.
Меняем политику перезапуска контейнера — Docker умеет менять её у работающего контейнера (docker update), и провайдер это знает:
$ sed -i "s/restart = \"no\"/restart = \"unless-stopped\"/" main.tf
$ terraform plan
docker_volume.data: Refreshing state... [id=price-data]
docker_image.nginx: Refreshing state... [id=sha256:dc73b49f5124cf2ee538dfbdfbd121f0b4ccdcb20fea30f3a81bd477c02e2bb5nginx:1.28-alpine]
docker_container.web: Refreshing state... [id=1dbd8dc4a3c21eaf78eedd6d92c65f74bd1cee9de786e84052c0fc3f121ab93e]
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
~ update in-place
Terraform will perform the following actions:
# docker_container.web will be updated in-place
~ resource "docker_container" "web" {
id = "1dbd8dc4a3c21eaf78eedd6d92c65f74bd1cee9de786e84052c0fc3f121ab93e"
name = "price-web"
~ restart = "no" -> "unless-stopped"
# (50 unchanged attributes hidden)
# (2 unchanged blocks hidden)
}
Plan: 0 to add, 1 to change, 0 to destroy.
─────────────────────────────────────────────────────────────────────────────
Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.
$ terraform apply -auto-approve | grep -E "Modif|complete"
docker_container.web: Modifying... [id=1dbd8dc4a3c21eaf78eedd6d92c65f74bd1cee9de786e84052c0fc3f121ab93e]
docker_container.web: Modifications complete after 0s [id=1dbd8dc4a3c21eaf78eedd6d92c65f74bd1cee9de786e84052c0fc3f121ab93e]
Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
Знак ~ и одна строка с изменением; остальные атрибуты скрыты (50 unchanged attributes hidden). Идентификатор контейнера тот же — контейнер не пересоздавался.
Опубликованный порт у существующего контейнера не поменять — только создать новый. Провайдер помечает такие аргументы, и план превращается в -/+ (вывод отфильтрован grep; ключ -no-color убирает цветовые коды, чтобы фильтровать было удобно):
$ sed -i "s/external = 8080/external = 8081/" main.tf
$ terraform plan -out=tfplan -no-color | grep -E "^ # |replacement|forces|Plan:"
-/+ destroy and then create replacement
# docker_container.web must be replaced
~ external = 8080 -> 8081 # forces replacement
Plan: 1 to add, 0 to change, 1 to destroy.
Для контейнера без состояния это несколько секунд простоя. Для виртуальной машины — потеря всего, что было на её диске, для управляемой базы — потеря данных. Что пересоздаёт ресурс, написано в документации провайдера и видно в плане по # forces replacement. Частые случаи в облаках: зона доступности, образ загрузочного диска, имя.
План выше сохранён в tfplan. Пока его читали, коллега выполнил apply сам. Применить сохранённый план теперь нельзя:
$ terraform apply -auto-approve | grep -E "complete"
docker_container.web: Destruction complete after 1s
docker_container.web: Creation complete after 0s [id=7bd96b0c47e4ff63003f854bedb36ec60990592b3ff1d601aa99c1a45495139b]
Apply complete! Resources: 1 added, 0 changed, 1 destroyed.
$ terraform apply tfplan
╷
│ Error: Saved plan is stale
│
│ The given plan file can no longer be applied because the state was changed
│ by another operation after the plan was created.
╵
Terraform помнит, к какому состоянию построен план, и отказывается применять его к другому. Это защищает от гонок, но не заменяет блокировку: двое, запустивших apply одновременно, испортят состояние. Блокировки — в модуле 05.
Перед каждым планом Terraform обновляет (refresh) состояние: спрашивает провайдер, как ресурсы выглядят сейчас (строки Refreshing state…). Удалим контейнер мимо Terraform:
$ docker rm -f price-web
price-web
$ terraform plan -no-color | grep -E "^ # |Plan:"
# docker_container.web will be created
Plan: 1 to add, 0 to change, 0 to destroy.
Terraform заметил пропажу и планирует создать контейнер снова. Иногда изменение снаружи правильное — например, ресурс удалили вручную намеренно, и вы хотите только, чтобы Terraform «узнал» об этом. Для этого есть -refresh-only: показать, что изменилось снаружи, и предложить записать это в состояние, ничего не трогая в инфраструктуре:
$ terraform plan -refresh-only -no-color | grep -E "^ # |Objects|refresh-only plan"
Note: Objects have changed outside of Terraform
# docker_container.web has been deleted
This is a refresh-only plan, so Terraform will not take any actions to undo
$ terraform apply -auto-approve | grep complete
docker_container.web: Creation complete after 0s [id=6c73b976c3143f774d9d43c70f65a3ddde16e09ab0aa4fa5c7a96ad8fe88b386]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
apply -refresh-only приняло бы изменения в состояние; обычный apply, как здесь, возвращает инфраструктуру к коду. Регулярный plan по расписанию в CI — простой детектор дрейфа: непустой план ночью означает, что кто-то правил руками.
Бывают атрибуты, которыми управляет не Terraform: число машин в группе меняет автомасштабирование, метки ставит система учёта. Изобразим это: политику перезапуска поменяли снаружи, и каждый план собирается её вернуть:
$ docker update --restart=always price-web
price-web
$ terraform plan -no-color | grep -E "^ # |restart |Plan:"
# docker_container.web will be updated in-place
~ restart = "always" -> "unless-stopped"
Plan: 0 to add, 1 to change, 0 to destroy.
Если так и задумано, атрибут исключают из сравнения блоком lifecycle внутри ресурса:
resource "docker_container" "web" {
# … аргументы как раньше …
lifecycle {
ignore_changes = [restart]
}
}
$ terraform plan -no-color | grep -E "^ # |No changes"
No changes. Your infrastructure matches the configuration.
ignore_changes прячет дрейф, поэтому перечисляют конкретные атрибуты и пишут комментарий «кто ими управляет». ignore_changes = all превращает Terraform в «создал и забыл» — так делают только осознанно.
По умолчанию пересоздание идёт как -/+: сначала удалить, потом создать — между ними сервис недоступен. create_before_destroy = true меняет порядок на +/-. Добавим его и снова сменим порт:
lifecycle {
ignore_changes = [restart]
create_before_destroy = true
}
$ terraform apply -auto-approve -no-color 2>&1 | grep -E "^ # |replacement|Plan:|Creat|Error"
+/- create replacement and then destroy
# docker_container.web must be replaced
~ external = 8081 -> 8082 # forces replacement
Plan: 1 to add, 0 to change, 1 to destroy.
docker_container.web: Creating...
Error: Unable to create container: Error response from daemon: Conflict. The container name "/price-web" is already in use by container "6c73b976c3143f774d9d43c70f65a3ddde16e09ab0aa4fa5c7a96ad8fe88b386". You have to remove (or rename) that container to be able to reuse that name.
$ terraform apply -auto-approve | grep -E "complete"
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
Новый контейнер не создался: старый ещё жив, а имя price-web занято. То же произойдёт с любым ресурсом, имя которого должно быть уникальным, — виртуальной машиной в каталоге облака, бакетом, DNS-записью. С create_before_destroy имя делают изменяемым — например, добавляют случайный суффикс (random_id, модуль 07) — или не задают вовсе, если провайдер умеет генерировать его сам. Старый контейнер ошибка не тронула: Terraform не удаляет старое, пока не создано новое. Возвращаем порт и lifecycle без create_before_destroy.
Код не менялся, но ресурс «испорчен»: машина зависла, в контейнере что-то правили руками. -replace=АДРЕС пересоздаёт один ресурс:
$ terraform plan -replace=docker_container.web -no-color | grep -E "^ # |Plan:"
# docker_container.web will be replaced, as requested
Plan: 1 to add, 0 to change, 1 to destroy.
Раньше для этого была команда terraform taint; она устарела — -replace сначала показывает план, а taint молча помечал ресурс в состоянии.
Том price-data защищён prevent_destroy = true (код в начале модуля). Любой план, в котором он удаляется, — и destroy, и замена из-за правки, — завершается ошибкой до каких-либо действий:
$ terraform destroy -auto-approve 2>&1 | sed -n "/Error/,\$p"
│ Error: Instance cannot be destroyed
│
│ on main.tf line 7:
│ 7: resource "docker_volume" "data" {
│
│ Resource docker_volume.data has lifecycle.prevent_destroy set, but the plan
│ calls for this resource to be destroyed. To avoid this error and continue
│ with the plan, either disable lifecycle.prevent_destroy or reduce the scope
│ of the plan using the -target option.
╵
Чтобы удалить ресурс, защиту снимают в коде — это отдельное изменение, которое пройдёт ревью. После этого destroy удаляет всё, вместе с данными тома:
$ terraform destroy -auto-approve | grep -E "Destr|complete"
docker_container.web: Destroying... [id=6c73b976c3143f774d9d43c70f65a3ddde16e09ab0aa4fa5c7a96ad8fe88b386]
docker_container.web: Destruction complete after 0s
docker_image.nginx: Destroying... [id=sha256:dc73b49f5124cf2ee538dfbdfbd121f0b4ccdcb20fea30f3a81bd477c02e2bb5nginx:1.28-alpine]
docker_volume.data: Destroying... [id=price-data]
docker_image.nginx: Destruction complete after 0s
docker_volume.data: Destruction complete after 2s
Destroy complete! Resources: 3 destroyed.
prevent_destroy не спасёт, если удалить из кода блок ресурса целиком: защиты в коде больше нет, и следующий apply удалит ресурс. Для данных нужна защита и на стороне облака — запрет удаления у базы и диска, резервные копии, — а план с destroy для таких ресурсов читают особенно внимательно.Ключ -target=АДРЕС ограничивает план одним ресурсом и его зависимостями. Он нужен для починки в аварии, но не для работы: план с -target показывает только часть картины, и Terraform сам предупреждает об этом.
-/+ и - для данных — повод остановитьсяplan -out + apply файл — применяется ровно прочитанный план; устаревший план не применится~ — изменение на месте, -/+ — пересоздание, причина — # forces replacement-refresh-only — принять изменения в состояние, не трогая инфраструктуруignore_changes — для атрибутов, которыми управляет кто-то другой, перечисляя их явноcreate_before_destroy убирает простой, но требует изменяемых уникальных имён-replace вместо устаревшего taint; prevent_destroy не защищает от удаления блока из кода