Введение
В прошлых статьях про Terraform мы подняли инфраструктуру в Proxmox, написали свои модули и развернули поверх Kubernetes на Talos. Команды terraform при этом растыканы по тексту кусками.
Эта статья — шпаргалка, в которую можно возвращаться за конкретной командой или флагом. Базовых init / plan / apply здесь нет, предполагаем, что вы их и так делаете каждый день.
Акцент на том, что в доках легко пропустить: блоки moved / import / removed, флаги -refresh-only и -detailed-exitcode, -replace вместо taint, кэш провайдеров и несколько приёмов отладки.
Переменные окружения и .terraformrc
Самое полезное из того, что настраивается один раз и работает всегда.
Переменные в окружение:
export TF_VAR_proxmox_password="..." # значение variable "proxmox_password"
export TF_LOG=DEBUG # или TRACE, если совсем грустно
TF_VAR_<name> перекрывает *.tfvars и особенно полезен для секретов: значение не попадает в репозиторий и в shell history, если пробрасывать через direnv или vault.
В ~/.terraformrc имеет смысл включить общий кэш провайдеров:
plugin_cache_dir = "$HOME/.terraform.d/plugin-cache"
disable_checkpoint = true
Один bpg/proxmox весит под 80 MB. Если у вас десяток TF-проектов, каждый terraform init тащит провайдер заново в .terraform/providers. С plugin_cache_dir провайдер скачивается один раз, дальше все проекты линкуют его из кэша.
💡 Внимательно! Кэш-каталог должен существовать до первого
init, иначе Terraform молча игнорирует настройку. Создаём руками:mkdir -p ~/.terraform.d/plugin-cache.
Осмотреться в проекте
Команды, которые ничего не ломают.
terraform version # версия TF и провайдеров
terraform providers # дерево провайдеров по модулям
terraform fmt -recursive -diff # форматирование с показом diff
terraform fmt -recursive -check # exit code != 0, если не отформатировано, для pre-commit
terraform validate # синтаксис и ссылки, без обращения к провайдеру
Что реально лежит в state:
terraform state list # все адреса ресурсов
terraform state show 'module.vms.proxmox_virtual_environment_vm.this["talos-cp-01"]'
Outputs в пайплайн:
terraform output -json | jq '.kubeconfig.value' -r > kubeconfig
-json снимает человекочитаемое форматирование и markdown, дальше jq парсит как обычный JSON.
Plan-трюки
Сохраняем план в файл и применяем ровно его:
terraform plan -out=plan.tfplan
terraform apply plan.tfplan
Между plan и apply state мог измениться (коллега применил, дрейфнуло в облаке), и без -out мы применяем не то, что видели на ревью.
Проверка на дрейф без изменения конфигурации:
terraform plan -refresh-only -detailed-exitcode
-refresh-only подтягивает реальное состояние и сравнивает со state, ничего не применяя. -detailed-exitcode даёт три кода: 0 нет изменений, 1 ошибка, 2 изменения есть. В CI это прямой ответ на вопрос «разъехалось ли что-то за ночь».
Пересоздать один конкретный ресурс:
terraform apply -replace='module.vms.proxmox_virtual_environment_vm.this["talos-cp-01"]'
Это современная замена terraform taint, который давно помечен deprecated. Флаг живёт внутри обычного apply, никакого отдельного состояния «помеченности» в state не создаётся.
Точечное применение:
terraform apply -target=module.talos_configs
Полезно, когда нужно срочно применить один кусок, не дожидаясь ребилда всего кластера. Злоупотреблять не стоит, -target маскирует граф зависимостей, и дальше легко получить drift.
Когда провайдер захлёбывается по rate-limit (Proxmox API любит затыкаться на массовом создании ВМ):
terraform apply -parallelism=3
По умолчанию параллелизм 10, для домашнего Proxmox это много.
💡 Внимательно!
-targetне для повседневной работы, а для аварийных случаев. После такого применения обязательно прогоняем обычныйplanбез-target, чтобы убедиться, что кластер сошёлся.
Рефакторинг без пересоздания
Самое приятное из того, что появилось в TF за последние версии.
Переименовали модуль или ресурс, не хотим destroy + create:
moved {
from = module.cloud_images.proxmox_virtual_environment_download_file.cloud_images
to = module.images.proxmox_virtual_environment_download_file.this
}
На следующем apply Terraform сам переедет адреса в state. Блок можно убрать после успешного применения (или оставить как документацию).
Импорт существующего ресурса декларативно:
import {
to = proxmox_virtual_environment_vm.legacy_vm
id = "pve1/401"
}
resource "proxmox_virtual_environment_vm" "legacy_vm" {
# ...
}
Работает с 1.5, удобнее императивного terraform import: импорт описан в коде, попадает в PR, проходит ревью.
Убрать ресурс из state, не трогая его в облаке (с 1.7):
removed {
from = proxmox_virtual_environment_vm.old_vm
lifecycle {
destroy = false
}
}
Нужно, например, когда ВМ теперь управляется другой командой или мигрирует в другой state.
Императивный fallback, когда декларативных блоков мало:
terraform state mv 'proxmox_virtual_environment_vm.a' 'proxmox_virtual_environment_vm.b'
terraform state rm 'proxmox_virtual_environment_vm.legacy'
💡 Внимательно!
state rmубирает ресурс из state, но оставляет в облаке. Если забыли проremovedсdestroy = falseи дропнули модуль целиком — TF увидит «лишний» ресурс в облаке при следующемrefreshтолько если он попал вimport. Держите бэкап state-файла перед такими операциями.
Отладка выражений
Главный недооценённый инструмент — terraform console. Это REPL с доступом к переменным, локалам и state:
terraform console
> var.images_config.images
> [for k, v in var.images_config.images : k if v.enabled]
> jsondecode(file("./configs/images.yaml"))
Сложные for-выражения и try() гораздо быстрее гонять в консоли, чем через plan с правками кода.
Тот же REPL, но против сохранённого плана:
terraform console -plan=plan.tfplan
Так можно смотреть значения вычисленных count, for_each, атрибутов, которые в обычном console ещё (known after apply).
Посмотреть, что именно поедет на удаление в сохранённом плане:
terraform show -json plan.tfplan | jq '.resource_changes[] | select(.change.actions[] == "delete") | .address'
Визуализация графа зависимостей, когда TF ругается на cycle:
terraform graph | dot -Tsvg > graph.svg
Lock-файл и провайдеры
Если в команде кто-то на macOS, а CI на Linux, .terraform.lock.hcl будет драться: хеши провайдера в нём платформо-зависимые. Решение:
terraform providers lock \
-platforms=linux_amd64 \
-platforms=darwin_arm64 \
-platforms=darwin_amd64
Команда ходит в registry и записывает хеши для всех платформ сразу. Lock-файл становится стабильным, init на чужой платформе не дописывает новую строчку.
Обновить провайдеры до последних допустимых версий:
terraform init -upgrade
Без -upgrade TF держится за то, что уже зафиксировано в lock. С -upgrade поднимается в пределах constraints из required_providers.
Секретики в коде
Не команды, а короткие конструкции, которые часто забывают.
Маскирование в выводе:
variable "proxmox_password" {
type = string
sensitive = true
}
output "kubeconfig" {
value = module.talos.kubeconfig
sensitive = true
}
Всё, что помечено sensitive, Terraform не печатает в plan и apply. Если значение нужно всё-таки достать — terraform output -raw kubeconfig или функция nonsensitive() в выражении.
Опциональные поля в модулях:
checksum = try(each.value.checksum, null)
try() возвращает первый аргумент, который не кинул ошибку. Удобнее, чем тернарники с contains(keys(...)). Пара к нему — can(), возвращает bool.
Современная замена null_resource:
resource "terraform_data" "bootstrap_trigger" {
triggers_replace = [var.cluster_endpoint]
}
terraform_data встроен в TF, не требует провайдера hashicorp/null и поддерживает triggers_replace без костылей с keepers.
Ассерты прямо на ресурсе:
resource "proxmox_virtual_environment_vm" "this" {
# ...
lifecycle {
precondition {
condition = var.address != ""
error_message = "VM address must be set."
}
}
}
precondition / postcondition в lifecycle проверяют не только вход модуля (как validation), а именно то, что ресурс получил или отдал. Ловят ошибки до apply, а не в момент падения провайдера.
Деструктивные операции
Дальше пошло то, на чём можно реально сломать.
terraform destroy # снести всё
terraform destroy -target=module.vms # снести кусок
terraform apply -replace=<addr> # пересоздать ресурс
terraform state rm <addr> # забыть про ресурс, оставить в облаке
terraform force-unlock <LOCK_ID> # снять залипший лок state
force-unlock нужен, когда apply прервался нештатно (VPN отвалился, процесс убили) и state остался залочен. Перед этим обязательно убедиться, что никто другой не применяет в этот же state, иначе получим одновременные записи и кашу.
Правка terraform.tfstate руками — крайний случай. Бывает нужно, когда state mv и removed не справляются (битый формат, корявый импорт). Всегда делаем копию файла перед правкой, state пишется как JSON, валидность после правки проверяется через terraform state list.
💡 Внимательно!
terraform state rmлюбимая ошибка новичков: ресурс исчезает из плана, кажется, что «всё почистилось», а на самом деле он живёт в облаке и тянет деньги. Для чистого удаления из управления — блокremoved { lifecycle { destroy = false } }, там хотя бы видно в коде, что именно выкинули.
Что безопасно, что деструктивно
Короткий свод, чтобы не думать каждый раз.
- Safe (read-only):
version,providers,validate,fmt -check,state list/show,output,console,show,plan -refresh-only,graph. - Меняет state, не трогает облако:
state mv,import(команда и блок), блокmoved, блокremovedсdestroy = false. - Меняет облако:
apply,apply -replace,apply -target,destroy,destroy -target. - Опасно без понимания:
state rm,force-unlock, ручная правкаterraform.tfstate.
Заключение
Что полезного собрали в шпаргалке:
plugin_cache_dirв.terraformrc— один кэш провайдеров на все проекты,initперестаёт тянуть одно и то же по сто раз.plan -refresh-only -detailed-exitcode— drift-детект для CI без применения изменений.apply -replace— замена deprecatedtaint, пересоздание одного ресурса без правки кода.- Блоки
moved/import/removed— рефакторинг, импорт и удаление из state декларативно, через PR, а не императивными командами. terraform consoleиconsole -plan— REPL для отладки выражений и просмотраknown after applyзначений.providers lock -platforms— стабильный lock-файл для команды с разными ОС.sensitive,try(),terraform_data,precondition— короткие конструкции, которые убирают костыли из модулей.
Мой боевой домашний конфиг с модулями под Proxmox и Talos лежит тут.