Infrastructure as Code with Terraform / OpenTofu / Pulumi / CDK / Crossplane. Shows declarative provisioning, plan-apply lifecycle, S3+DynamoDB remote state with locking, multi-cloud providers, modules and workspaces, drift detection, and an ADR comparing Terraform vs Pulumi vs CDK vs Crossplane.
Click-ops в cloud console — это catastrophe. Через год никто не помнит, почему этот security group разрешает 0.0.0.0/0, и кто его создал. DR-план — «надеемся, что AWS не упадёт». Staging и prod дрейфуют независимо, баги воспроизводятся только в проде.
Infrastructure as Code превращает инфраструктуру в код: HCL/TypeScript/Python-файлы, лежащие в git, проходящие code review, применяемые тулом. Что получаешь:
terraform plan в комментарии, два +1 — мержtfvarsterraform apply в чужом регионе восстановит всёЭто та же эволюция, что server config: ручной SSH → Ansible playbooks. Только этажом выше — управляем не пакетами на хосте, а самим хостом.
Терраформ — это declarative state reconciler с DAG-планировщиком. Ты описываешь «как должно быть», движок сравнивает с «как есть» и придумывает diff. Внутри три кита:
terraform.tfstate) — JSON-файл, текущее представление мира с точки зрения Terraform. Маппинг «логический resource → реальный cloud-ID + атрибуты». Без state нет diff.-parallelism=10 по умолчанию).Ключевая инвариант — state == reality. Любой ручной клик в консоли её ломает (это drift). Чтобы не сломать самого себя в параллельных запусках, используется lock (DynamoDB conditional put, terraform.tfstate.lock.info).
Provider — отдельный бинарь (terraform-provider-aws v5.40), который умеет говорить с конкретным API. Terraform-движок ничего не знает про AWS; он умеет только обходить DAG и звать provider.Create/Read/Update/Delete. Это и есть точка расширения: 4000+ провайдеров покрывают всё от GitHub teams до Datadog monitors.
Пять групп, читаются слева направо, сверху вниз:
terraform CLI как dispatcher, под ним plan (DAG-билдер), apply (executor), modules registry (загрузчик переиспользуемых блоков) и workspaces (изоляция state по env).terraform.tfstate (versioned + SSE), DynamoDB держит lock, KMS шифрует. Это и есть «единственный источник правды» в команде.Edges — это вызовы (commit, webhook, read state, CreateVpc), а не направление данных. Ответы идут по тем же edges в reverse.
Plan → Apply. Канонический happy path. Инженер редактирует aws_instance.web, коммит, PR. Atlantis (CI-бот для Terraform) ловит вебхук, запускает terraform init && terraform plan. CLI скачивает модуль из registry, берёт lock в DynamoDB (conditional put на LockID=aws-prod), читает текущий state из S3 (расшифровывая через KMS), строит DAG, выводит diff в PR-комментарий. Ревьюер видит «5 add, 3 change, 0 destroy» — пишет atlantis apply. CLI вызывает terraform apply -auto-approve plan.out, walker обходит DAG: сначала IAM role (на неё ссылаются все), потом VPC, потом параллельно EC2 + S3 (между ними нет зависимости), в конце RDS (нужна VPC subnet group). Каждый ресурс — вызов провайдера, провайдер дёргает AWS API. После — новый state пишется обратно в S3, lock отпускается, статус «12 added» возвращается в PR.
Drift detection. Реальность ушла от state. On-call инженер посреди ночи зашёл в консоль и сменил instance_type с t3.medium на m5.xlarge — нагрузка скакнула. Terraform об этом не знает. Утром инженер запускает terraform plan -refresh-only: CLI берёт lock, читает state, идёт в провайдер с DescribeInstances на каждый managed-ресурс. Ответ от AWS говорит m5.xlarge — diff! Терраформ выводит «1 resource has drifted».
Два пути:
terraform apply → откатит обратно к t3.medium (state — закон, реальность подчиняется).m5.xlarge, закоммитить, apply → state догонит реальность (legitimize the drift).Если ресурс создан вообще вне Terraform (например, S3-бакет из 2019-го): terraform import aws_s3_bucket.legacy mybucket-2019 втащит его в state без пересоздания. После — пишешь матчащий HCL-блок, plan покажет 0 changes.
Modules + workspaces. Один модуль modules/vpc/main.tf, три окружения, разные scale. terraform workspace select dev → state-путь становится env/dev/terraform.tfstate, переменные подставляются из dev.tfvars (cidr 10.0.0.0/16, 1 NAT GW — дёшево). Тот же модуль для staging (10.1.0.0/16, 2 NAT) и prod (10.2.0.0/16, 3 NAT, multi-AZ RDS).
Когда два инженера запускают terraform apply на prod одновременно — второй ловит Error: ConditionalCheckFailedException на DynamoDB-локе. CLI ждёт с backoff (по умолчанию 30s × 9 попыток), потом либо берёт lock когда первый отпустит, либо падает с ошибкой. Это спасает state от corruption — две конкурентных записи в S3 без сериализации создают резистент-стейт, из которого только terraform force-unlock + ручной merge.
ADR-014 walkthrough. Почему Terraform, а не альтернативы. Pulumi — настоящий код (TS/Go/Python), можно for-loop по списку, юнит-тесты на инфру; минус — community меньше, runtime-debugging тяжелее (стек-трейс через JIT). CDK — типизированные AWS-constructs, компилируется в CloudFormation; минус — AWS-only, CFN-роллбеки медленные и непрозрачные. Crossplane — CRD в Kubernetes, инфра как kubectl apply; минус — нужен K8s как пререкизит, каталог меньше. Финальный выбор — Terraform: standard de-facto, S3+DynamoDB-бэкенд, Atlantis для PR-флоу. Escape hatch — переехать на Pulumi, если хот-патч станет цикл по runtime-данным.
Контекст: команда из 8 человек, мультиоблако (AWS primary + GCP для ML), GitOps-флоу через GitHub PR.
| Опция | Pros | Cons | Когда брать |
|---|---|---|---|
| Terraform / OpenTofu | HCL читаем не-программистами; 4000+ провайдеров; огромное community; стабильный state-формат | HCL ≠ настоящий язык (циклы через for_each костыли); state-merge при конфликте — боль; HashiCorp BSL-licensing → OpenTofu fork | Multi-cloud, mid-large команды, стандартный путь |
| Pulumi | Реальный TS/Python/Go: типы, тесты, абстракции, IDE-навигация; одинаковый стейт-движок что у TF | Меньше комьюнити; debugging через JIT тяжелее; vendor-lock на Pulumi Cloud (или self-hosted Postgres) | Команды разработчиков, динамическая инфра (loops over API responses) |
| AWS CDK | Типизация AWS L2/L3 constructs; высокий уровень («один объект = VPC + subnets + NAT»); генерит CFN | AWS-only; CFN-стек хрупкий: rollback одного ресурса блокирует весь стек; lock через CFN | AWS-only команда, любящая высокоуровневые абстракции |
| Crossplane | K8s-native: инфра как CRD, GitOps через ArgoCD; control-plane reconciliation постоянно (не одноразовый apply) | Нужен K8s; catalog меньше; debugging через kubectl describe непривычен инфра-инженерам | Уже K8s-shop, хотите единый control plane для apps + infra |
| Ansible | Императивный, отлично для config-management поверх IaC; agentless через SSH | Не для provisioning (нет state); идемпотентность модулей нестабильна | Конфиг внутри VM (после того как Terraform её создал) |
Решение: Terraform + S3/DynamoDB backend + Atlantis. Триггер пересмотра — если объём dynamic infra (например, генерация per-tenant Kafka-топиков из API-ответа) станет регулярной задачей → Pulumi выигрывает.
terraform.tfstate. State — это не текстовый файл, это transactional truth. vim tfstate → потерянные ресурсы, дубли при apply, corruption. Для миграций — terraform state mv / terraform state rm / terraform import.plan 8 минут, lock держится пока CI крутится, два PR не сольются. Решение — root-модули по доменам (network/, data/, apps/) с remote_state data sources для связи.terraform import или коммит TODO с тикетом.password = "hunter2" → попадает в state (S3, plain JSON), в plan-output, в git. Используй AWS Secrets Manager / Vault + sensitive = true на output.providers = { aws = aws.tenant } извне; иначе нельзя переиспользовать для multi-region/multi-account.terraform destroy в prod. Один человек, один тип-о — снёс всё. Защита: prevent_destroy = true на критичных ресурсах + отдельный break-glass workflow (не CI bot).terraform plan -refresh-only + alert.terraform init && state setup > время на ручной aws ec2 run-instances. Если знаешь, что снесёшь — снеси скриптом.kubectl.terraform plan/apply сидят в pipeline, и почему apply нужен отдельный manual-approve.