# Паттерны рефакторинга: count→for_each, split files, extract module _Рефакторинг · TerraformLab Knowledge Base_ **TL;DR:** Большие конфиги превращаются в спагетти. Базовые рефакторинги: count→for_each (стабильные ключи), разделение на файлы по доменам (network/compute/storage), вынос повторяющегося блока в модуль, объединение мелких ресурсов в составной, удаление мёртвых импортов. Каждый, пошагово, с проверкой `plan` на каждом шагу. ## Правила безопасного рефакторинга Любой рефакторинг = плотная работа со 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: слишком большой, дроби. ## См. также - [moved блок: переименование без destroy](/terraform/kb/tf-moved-block.md) - [state mv, state rm, state pull/push: ручные операции](/terraform/kb/tf-state-manipulation.md) - [count и for_each: несколько ресурсов из одного блока](/terraform/kb/tf-count-for-each.md)