Правила безопасного рефакторинга
Любой рефакторинг = плотная работа со state'ом. Правила:
- Бекап state перед началом:
terraform state pull > backup.json. - Plan после каждого шага. Если plan показывает destroy/create, стоп, значит шаг не «рефакторинг», а смена ресурса.
- Один атомарный рефакторинг = один PR. Не мешай rename с добавлением фичи. Иначе ревью теряет смысл.
- Тестовая среда первой. Применить refactor в dev, посмотреть что получилось, потом stage, потом prod.
Дальше, конкретные паттерны.
Паттерн 1: count → for_each
Проблема
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
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 в директории, склеивает в один граф. Какой ресурс в каком файле,
ему всё равно.
Применение:
# вырезать и вставить блоки в новые файлы
# 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. Копи-паста, изменение одного из трёх нужно делать втроём.
Решение
-
Создай
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" }}
# ...
-
В root замени блоки на module-вызов:
hclmodule "logs" {source = "./modules/audited-bucket"
name = "linuxlab-logs"
}
-
moved-блоки для каждого перенесённого ресурса:
hclmoved { 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 }# ...
-
Plan →
0 to add, 0 to change, 0 to destroy. Если что-то «to add», это новый ресурс в модуле, проверь diff. -
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-блок
-
Узнай ID существующих rules (
aws ec2 describe-security-group-rules). -
Объяви новые ресурсы в HCL:
hclresource "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..."
}
-
Удали старый
aws_security_group_ruleблок. -
removed { from = aws_security_group_rule.web_from_alb, lifecycle { destroy = false } }. -
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/ # общий модуль с реальными ресурсами
Миграция:
- Создай новый root
environments/dev/, импортируй существующие ресурсы. removed { destroy = false }в старом root.- Apply нового → захватил. Apply старого → state почистился.
- Повтори для stage, prod.
Это большой рефакторинг. Делай по одному env в раз, не разом.
Паттерн 6: dead code cleanup
Проблема
В state остались ресурсы, которых уже нет в облаке (удалили через
Console, забыли в HCL). plan показывает destroy на ресурс который
и так не существует. AWS отвечает 404.
Решение
terraform refresh
Это сверяет state с облаком и убирает несуществующие. Безопасно, это read-only к облаку.
Или для конкретного ресурса:
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, не одновременно. Между средами жди подтверждения «всё ок». Слишком много историй когда «вроде же проверили».