linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
Intro
Lessons
Footer
linuxlab-УчебникиЦеныО платформеКонфиденциальность и куки
Copyright © 2026 LinuxLab. Все права защищены.
linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
  • Введение
  • Уроки
  • How it works
  • База знаний
  • Шпаргалка
  • Capstone
  • Собеседование
home/terraform/kb/Рефакторинг/tf-refactor-patterns

kb/refactoring ── Рефакторинг ── intermediate

Паттерны рефакторинга: count→for_each, split files, extract module

Большие конфиги превращаются в спагетти. Базовые рефакторинги: count→for_each (стабильные ключи), разделение на файлы по доменам (network/compute/storage), вынос повторяющегося блока в модуль, объединение мелких ресурсов в составной, удаление мёртвых импортов. Каждый, пошагово, с проверкой `plan` на каждом шагу.

view as markdownaka: terraform-refactoring, terraform-refactor-patterns

Правила безопасного рефакторинга

Любой рефакторинг = плотная работа со state'ом. Правила:

  1. Бекап state перед началом: terraform state pull > backup.json.
  2. Plan после каждого шага. Если plan показывает destroy/create, стоп, значит шаг не «рефакторинг», а смена ресурса.
  3. Один атомарный рефакторинг = один PR. Не мешай rename с добавлением фичи. Иначе ревью теряет смысл.
  4. Тестовая среда первой. Применить refactor в dev, посмотреть что получилось, потом stage, потом prod.

Дальше, конкретные паттерны.

Паттерн 1: count → for_each

Проблема

hcl
variable "envs" {
  type    = list(string)
  default = ["dev", "stage", "prod"]
}
resource "aws_s3_bucket" "logs" {
  count  = length(var.envs)
  bucket = "logs-${var.envs[count.index]}"
}

Адреса: aws_s3_bucket.logs[0] (dev), [1] (stage), [2] (prod).

Удалили dev из списка, prod стал индексом 1, stage индексом 0. Plan покажет destroy на [2], рекод на [1] и [0]. Это массовое пересоздание, не удаление одного.

Решение: for_each

hcl
variable "envs" {
  type    = set(string)
  default = ["dev", "stage", "prod"]
}
resource "aws_s3_bucket" "logs" {
  for_each = var.envs
  bucket   = "logs-${each.key}"
}
moved { from = aws_s3_bucket.logs[0], to = aws_s3_bucket.logs["dev"] }
moved { from = aws_s3_bucket.logs[1], to = aws_s3_bucket.logs["stage"] }
moved { from = aws_s3_bucket.logs[2], to = aws_s3_bucket.logs["prod"] }

Адреса: aws_s3_bucket.logs["dev"], ["stage"], ["prod"]. Удаление dev теперь, точечное destroy ["dev"], остальные не трогает.

Pull request показывает:

  • HCL diff: count → for_each, list → set, count.index → each.key.
  • moved-блоки, явно объясняющие миграцию.

После apply moved-блоки можно удалить.

Паттерн 2: split files по доменам

Проблема

main.tf на 800 строк. Сетка, compute, storage, IAM, всё в одном файле. Diff в PR трогает 200 строк, ревьюер тонет.

Решение

network.tf   # vpc, subnets, route tables, NAT, IGW
compute.tf   # ec2, launch templates, autoscaling
storage.tf   # s3, ebs, efs
iam.tf       # roles, policies, instance profiles
variables.tf # все variable
outputs.tf   # все output
versions.tf  # terraform + providers + required_providers
locals.tf    # все locals

Это переименование файлов, не изменение HCL. Terraform читает все .tf в директории, склеивает в один граф. Какой ресурс в каком файле, ему всё равно.

Применение:

bash
# вырезать и вставить блоки в новые файлы
# plan
terraform plan
# должно быть No changes

Без всякого moved. Никакой migration. Только git-diff показывает что происходит.

Анти-паттерн: один файл на ресурс

s3.tf, iam.tf, s3_logs.tf, s3_data.tf, слишком мелко. Хорошее правило: на крупный домен (network/compute): один файл. Файл больше 500 строк, расщепляй. Меньше 50, объединяй.

Паттерн 3: extract module

Проблема

Три раза в HCL встречается одинаковая связка: бакет + bucket policy + versioning + logging. Копи-паста, изменение одного из трёх нужно делать втроём.

Решение

  1. Создай modules/audited-bucket/ с тремя .tf:

    hcl
    # modules/audited-bucket/main.tf
    resource "aws_s3_bucket" "this" {
      bucket = var.name
    }
    resource "aws_s3_bucket_versioning" "this" {
      bucket = aws_s3_bucket.this.id
      versioning_configuration { status = "Enabled" }
    }
    # ...
  2. В root замени блоки на module-вызов:

    hcl
    module "logs" {
      source = "./modules/audited-bucket"
      name   = "linuxlab-logs"
    }
  3. moved-блоки для каждого перенесённого ресурса:

    hcl
    moved { from = aws_s3_bucket.logs,            to = module.logs.aws_s3_bucket.this }
    moved { from = aws_s3_bucket_versioning.logs, to = module.logs.aws_s3_bucket_versioning.this }
    # ...
  4. Plan → 0 to add, 0 to change, 0 to destroy. Если что-то «to add», это новый ресурс в модуле, проверь diff.

  5. Apply. После, moved-блоки можно удалить.

Когда не extract

Если ресурсы похожи, но не идентичны, модуль придётся параметризовать через 10 переменных. К третьему параметру обычно понимаешь: проще оставить копи-пасту, разница реальная.

Правило большого пальца: 3+ повторений с минимальными различиями ⇒ модуль. < 3 ⇒ оставь как есть.

Паттерн 4: объединение мелких ресурсов

Проблема

В HCL, пять aws_security_group_rule ресурсов в SG. Provider 4.x поддерживает только их. Provider 5.x ввёл aws_vpc_security_group_ingress_rule (новые, рекомендуемые).

Миграция = смена типа ресурса. moved не работает между типами. Это destroy+create.

Решение: import-блок

  1. Узнай ID существующих rules (aws ec2 describe-security-group-rules).

  2. Объяви новые ресурсы в HCL:

    hcl
    resource "aws_vpc_security_group_ingress_rule" "web_from_alb" {
      security_group_id = aws_security_group.web.id
      ip_protocol       = "tcp"
      from_port         = 80
      to_port           = 80
      referenced_security_group_id = aws_security_group.alb.id
    }
    import {
      to = aws_vpc_security_group_ingress_rule.web_from_alb
      id = "sgr-0abc123..."
    }
  3. Удали старый aws_security_group_rule блок.

  4. removed { from = aws_security_group_rule.web_from_alb, lifecycle { destroy = false } }.

  5. Plan: видишь import + removed без destroy. Apply.

Облако не трогается. State перешёл с одного типа на другой.

Паттерн 5: вынос окружения в отдельный root

Проблема

Один root управляет dev + stage + prod через count/for_each+var.env. Любой apply затрагивает все три. Lock мешает параллельной работе. Когда dev ломается, stage и prod тоже не получается обновить.

Решение

environments/
├── dev/
│   ├── main.tf       # module "app" с env=dev
│   └── backend.tf
├── stage/
│   ├── main.tf
│   └── backend.tf
└── prod/
    ├── main.tf
    └── backend.tf
modules/
└── app/              # общий модуль с реальными ресурсами

Миграция:

  1. Создай новый root environments/dev/, импортируй существующие ресурсы.
  2. removed { destroy = false } в старом root.
  3. Apply нового → захватил. Apply старого → state почистился.
  4. Повтори для stage, prod.

Это большой рефакторинг. Делай по одному env в раз, не разом.

Паттерн 6: dead code cleanup

Проблема

В state остались ресурсы, которых уже нет в облаке (удалили через Console, забыли в HCL). plan показывает destroy на ресурс который и так не существует. AWS отвечает 404.

Решение

bash
terraform refresh

Это сверяет state с облаком и убирает несуществующие. Безопасно, это read-only к облаку.

Или для конкретного ресурса:

bash
terraform state rm aws_s3_bucket.deleted_already

И обязательно удали блок из HCL, иначе следующий apply попытается создать.

Чек-лист перед PR с рефакторингом

  • Бэкап state сделан (terraform state pull > backup.json)
  • plan чистый (0 to add, 0 to change, 0 to destroy)
  • Все moved/removed/import блоки объяснены в PR-описании
  • Применён в dev, протестирован
  • В commit message: «refactor: …», отличается от «feat:» / «fix:»
  • PR не смешивает рефакторинг с новой фичей
  • Описано почему: «consolidate buckets into audited-bucket module»

Подводные камни

  • Рефакторинг, это не «улучшить код». Это сохранить поведение, изменить структуру. Если plan показывает destroy/create, ты не рефакторишь, ты перепи_сываешь. Стоп, разбирайся.

  • Бекап state НЕ заменяет S3-versioning. Бекап в файл, твой собственный. S3-versioning, автоматический. Обе нужны для рефакторинга.

  • moved/removed не работают между разными типами ресурсов. Если «refactoring» = смена типа, это import + removed, не moved.

  • CI должен поймать destroy в рефакторинге. В pipeline ставь terraform plan -detailed-exitcode, exit 2 (есть изменения) на PR с тегом «refactor» = красный флаг для ревьюера.

  • Применять в dev → stage → prod, не одновременно. Между средами жди подтверждения «всё ок». Слишком много историй когда «вроде же проверили».

§ команды

bash
terraform state pull > backup-$(date +%s).json

Бекап state. ОБЯЗАТЕЛЬНО перед любым рефакторингом.

bash
terraform plan -detailed-exitcode

После каждого шага. Exit 0 = чисто, 2 = есть изменения. На refactor ожидаешь 0.

bash
terraform refresh

Сверить state с облаком. Убирает state-«призраков». Read-only.

bash
terraform state list

Перед/после рефакторинга: что в state.

bash
git diff --stat

Размер refactor PR. Если 1000+ строк в diff: слишком большой, дроби.

§ см. также

  • tf-moved-blockmoved блок: переименование без destroy`moved { from = ..., to = ... }` в HCL декларативно говорит Terraform: «этот ресурс раньше был по одному адресу, теперь по другому, в облаке тот же». Plan покажет «move», не «destroy + create». Появился в TF 1.1. Замена ручному `terraform state mv`, оставляет след в git, повторяется у всех в команде, видно в diff.
  • tf-state-manipulationstate mv, state rm, state pull/push: ручные операции`terraform state mv` переименовывает адрес ресурса в state (без destroy/recreate). `terraform state rm` убирает ресурс из state (но не из облака). `terraform state pull/push`, скачать/залить state как файл. Все четыре, резкие операции, делать через backup и понимая зачем. Для декларативных альтернатив есть [[tf-moved-block]] и [[tf-removed-block]].
  • tf-count-for-eachcount и for_each: несколько ресурсов из одного блокаcount создаёт N одинаковых ресурсов по индексам 0..N-1. for_each: ресурсы по ключам из set или map. Правило: count для одинаковых, for_each когда у каждого свои настройки. Если сомневаешься, бери for_each.
Footer
linuxlab-
Copyright © 2026 LinuxLab. Все права защищены.
Учебники
Цены
О платформе
Конфиденциальность и куки