Terraform в реальной команде
Уроки на YouTube заканчиваются на «вот ваш VPC». Реальная команда — это десятки модулей, многоразовые structures, drift, миграции и code review. Здесь — как это работает.
Структура проекта
Минимум для команды
infra/
├── modules/ # переиспользуемые модули
│ ├── vpc/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
│ │ ├── versions.tf
│ │ ├── README.md
│ │ └── examples/
│ ├── eks/
│ ├── rds/
│ ├── alb/
│ └── irsa/ # IAM roles for service accounts
├── envs/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── backend.tf
│ │ └── terraform.tfvars
│ ├── staging/
│ └── prod/
├── .pre-commit-config.yaml
├── .github/workflows/
│ ├── plan.yml
│ └── apply.yml
└── README.md
Когда переходить на Terragrunt
5+ environments / regions / accounts. Иначе boilerplate growing exponentially.
State — продакшен-настройка
S3 + DynamoDB backend
terraform {
backend "s3" {
bucket = "company-tf-state-prod"
key = "envs/prod/eks/terraform.tfstate"
region = "eu-west-1"
dynamodb_table = "tf-lock"
encrypt = true
kms_key_id = "alias/tf-state-key"
}
}
S3 bucket должен иметь:
- Versioning ON (для rollback state)
- SSE-KMS encryption
- Block Public Access
- MFA Delete для prod
- Lifecycle rules для старых версий
- Replication в другой регион (DR)
State per layer
Не один state на всё. Разбивай:
foundation/— VPC, IAM (редко меняется)data/— RDS, ElastiCachecompute/— EKS, ECSapps/— Helm releases (если через Terraform)
Между state'ами — terraform_remote_state data source или явные variables.
Модуль — структура
# modules/vpc/main.tf
resource "aws_vpc" "this" {
cidr_block = var.cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = merge(var.tags, { Name = "${var.name}-vpc" })
}
resource "aws_subnet" "private" {
for_each = { for idx, az in var.azs : idx => az }
vpc_id = aws_vpc.this.id
availability_zone = each.value
cidr_block = cidrsubnet(var.cidr, 4, each.key + 8)
tags = merge(var.tags, {
Name = "${var.name}-private-${each.value}"
"kubernetes.io/role/internal-elb" = "1"
})
}
# modules/vpc/variables.tf
variable "name" {
description = "Name prefix"
type = string
}
variable "cidr" {
description = "VPC CIDR"
type = string
validation {
condition = can(cidrhost(var.cidr, 0))
error_message = "Must be valid CIDR."
}
}
variable "azs" {
description = "Availability zones"
type = list(string)
}
variable "tags" {
type = map(string)
default = {}
}
# modules/vpc/outputs.tf
output "vpc_id" { value = aws_vpc.this.id }
output "private_subnet_ids" { value = [for s in aws_subnet.private : s.id] }
output "vpc_cidr" { value = aws_vpc.this.cidr_block }
for_each vs count
Почти всегда for_each. Почему: при удалении элемента из середины count сдвигает всё, Terraform пересоздаёт лишнее.
# Плохо
resource "aws_iam_user" "users" {
count = length(var.usernames)
name = var.usernames[count.index]
}
# Хорошо
resource "aws_iam_user" "users" {
for_each = toset(var.usernames)
name = each.key
}
CI/CD пайплайн
На PR
terraform fmt -check— форматированиеterraform validate— синтаксисtflint— линтерtfsec/Checkov— security scanterraform plan— комментарий в PR с diff- Review + approve
После merge
terraform applyдля dev — авто- Для staging — авто после успешного dev
- Для prod — manual approval
Инструменты
- Atlantis — бот в PR:
atlantis plan,atlantis apply - Terraform Cloud / HCP — managed с UI
- Spacelift / env0 — enterprise managed
- Свой пайплайн на GitHub Actions / GitLab CI
Импорт существующих ресурсов
Унаследованная инфра — частая реальность. Не пересоздавай — импортируй.
# Terraform 1.5+ — декларативный import (в коде)
import {
to = aws_s3_bucket.legacy
id = "existing-bucket-name"
}
resource "aws_s3_bucket" "legacy" {
bucket = "existing-bucket-name"
# ... остальные параметры
}
# Старый способ — CLI
terraform import aws_s3_bucket.legacy existing-bucket-name
# Потом terraform plan — увидеть diff и подогнать код
Drift detection
Drift — расхождение state и реальности (кто-то правил руками).
- Регулярный
terraform planв CI (без apply) на cron - driftctl — open source инструмент
- AWS Config — параллельный мониторинг
- Spacelift / env0 — встроенный drift detection
Тесты
Уровни
- Static: fmt, validate, tflint, tfsec
- Unit:
terraform test(с TF 1.6+) - Integration: Terratest (Go) — поднимает реальную инфру, ассертит, сносит
- Policy: OPA / Conftest на Rego
# tests/vpc.tftest.hcl
run "vpc_creates_correctly" {
command = plan
variables {
name = "test"
cidr = "10.99.0.0/16"
azs = ["us-east-1a"]
}
assert {
condition = aws_vpc.this.cidr_block == "10.99.0.0/16"
error_message = "CIDR mismatch"
}
}
Best practices — собрано
- Никогда не правь руками после
apply - Pin версии провайдеров и модулей
- Один state — одна логическая единица
planв CI на каждый PRapplyчерез CI с approval для prod- Секреты — Vault/SSM/
TF_VAR_*, не в коде for_eachвместоcount- Единый naming convention
- Все ресурсы с тегами (Environment, Owner, ManagedBy)
- Code review с plan-выводом
- Pre-commit hooks: fmt + tflint + tfsec
- Modules в отдельном репо с tagged releases