К блогу
Apr 20, 2026
8 min read

Шпаргалка по Terraform: команды и приёмы, которые реально используются

Подборка команд и конструкций Terraform, которые регулярно нужны: plan-трюки, рефакторинг через moved/removed/import, state-операции, отладка через console и graph, lock-файл для нескольких платформ.

#terraform#infrastructure as code#cheatsheet#operations

Введение

В прошлых статьях про 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 — замена deprecated taint, пересоздание одного ресурса без правки кода.
  • Блоки moved / import / removed — рефакторинг, импорт и удаление из state декларативно, через PR, а не императивными командами.
  • terraform console и console -plan — REPL для отладки выражений и просмотра known after apply значений.
  • providers lock -platforms — стабильный lock-файл для команды с разными ОС.
  • sensitive, try(), terraform_data, precondition — короткие конструкции, которые убирают костыли из модулей.

Мой боевой домашний конфиг с модулями под Proxmox и Talos лежит тут.