Sari la conținut
← Perspective

7 min readDe Kicked Team

Terraform la scară: cum administrezi infrastructura multi-environment fără să-ți pierzi mințile

Cu Terraform e ușor să începi și greu să scalezi. Iată cum structurăm noi Terraform pentru 50+ environment-uri — gestionarea state-ului, designul modulelor, integrarea CI/CD și pattern-urile care ne țin întregi la minte.

  • DevOps
  • Terraform
  • Infrastructure
  • Automation

Primul tău proiect Terraform este magic. Scrii puțin HCL, rulezi terraform apply și infrastructura apare. Al 50-lea proiect este un coșmar de module copiate cu copy-paste, conflicte pe fișierele de state și un pipeline de CI care durează 40 de minute.

Administrăm Terraform în zeci de environment-uri ale clienților. Iată toate pattern-urile pe care le-am învățat pe pielea noastră.

Decizia monorepo vs. polyrepo

Este prima bifurcație a drumului, și majoritatea echipelor aleg greșit.

Monorepo (toată infrastructura într-un singur repo):

  • ✅ Ușor de căutat și de refactorizat între environment-uri
  • ✅ Un singur pipeline de CI de întreținut
  • ❌ Blast radius — un merge greșit poate afecta totul
  • ❌ Plan-uri lente pe măsură ce codebase-ul crește

Polyrepo (un repo per environment/proiect):

  • ✅ Izolare — schimbările nu se pot scurge între environment-uri
  • ✅ Cicluri de release independente
  • ❌ Versionarea modulelor devine critică
  • ❌ Mai greu de menținut consistența

Noi folosim o abordare hibridă: un repo per graniță logică (de exemplu, per client sau per produs), cu un registry de module partajat. Asta ne dă izolare acolo unde contează și reutilizare acolo unde ajută.

Structura repository-ului

După ani de iterație, acesta este layout-ul nostru standard:

infrastructure/
├── environments/
│   ├── production/
│   │   ├── networking/
│   │   │   ├── main.tf
│   │   │   ├── variables.tf
│   │   │   ├── outputs.tf
│   │   │   └── terraform.tfvars
│   │   ├── compute/
│   │   ├── database/
│   │   └── dns/
│   ├── staging/
│   │   └── ... (mirrors production)
│   └── shared/
│       ├── iam/
│       └── monitoring/
├── modules/
│   ├── vpc/
│   ├── server/
│   ├── database/
│   └── monitoring-stack/
└── terragrunt.hcl

Principii-cheie:

  1. Un fișier de state per grup de resursenetworking, compute, database au fiecare propriul state. Un terraform apply greșit în compute nu-ți poate distruge rețeaua.
  2. Environment-urile se oglindesc — Staging este structural identic cu production. Dacă merge în staging, merge în production.
  3. Modulele sunt versionate — Release-uri cu tag, semantic versioning. Production se leagă de versiuni stabile.

Gestionarea state-ului

State-ul remote nu este negociabil. Fișierele de state locale sunt o bombă cu ceas.

# backend.tf
terraform {
  backend "s3" {
    bucket         = "kicked-terraform-state"
    key            = "production/networking/terraform.tfstate"
    region         = "eu-central-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"
  }
}

Reguli pe care le impunem:

  • State locking — DynamoDB (AWS) sau echivalent. Doi ingineri care rulează apply simultan îți vor corupe state-ul.
  • Criptare la repaus — Fișierele de state conțin secrete (parole de baze de date, chei API). Criptează întotdeauna.
  • Fără intervenții manuale pe stateterraform state mv și terraform import doar prin CI, cu audit trail.
  • Fișier de state per grup de resurse — Nu per environment. Un singur fișier de state pentru tot production-ul este rețeta pentru plan-uri de 20 de minute.

Terragrunt: stratul DRY

Terraform-ul brut devine repetitiv rapid. Fiecare environment are nevoie de aceeași configurație de backend, același setup de provider, aceleași pattern-uri de variabile. Terragrunt elimină duplicarea:

# environments/production/networking/terragrunt.hcl
terraform {
  source = "../../../modules/vpc"
}

include "root" {
  path = find_in_parent_folders()
}

inputs = {
  environment = "production"
  cidr_block  = "10.0.0.0/16"
  az_count    = 3
}
# root terragrunt.hcl
remote_state {
  backend = "s3"
  generate = {
    path      = "backend.tf"
    if_exists = "overwrite_terragrunt"
  }
  config = {
    bucket         = "kicked-terraform-state"
    key            = "${path_relative_to_include()}/terraform.tfstate"
    region         = "eu-central-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"
  }
}

${path_relative_to_include()} generează automat chei de state unice pe baza căii directorului. Gata cu copy-paste-ul configurațiilor de backend.

Principii de design pentru module

Modulele proaste sunt mai rele decât lipsa modulelor. Iată ce face un modul bun:

1. Default-uri cu opinie, override complet

variable "instance_type" {
  description = "EC2 instance type"
  type        = string
  default     = "t3.medium"  # Sensible default
}

variable "monitoring_enabled" {
  description = "Enable detailed monitoring"
  type        = bool
  default     = true  # Safe default
}

Un modul ar trebui să funcționeze cu zero variabile opționale. Dar fiecare default ar trebui să poată fi suprascris.

2. Output-urile sunt API-ul tău

# Good: outputs everything downstream might need
output "vpc_id" { value = aws_vpc.main.id }
output "private_subnet_ids" { value = aws_subnet.private[*].id }
output "public_subnet_ids" { value = aws_subnet.public[*].id }
output "nat_gateway_ips" { value = aws_eip.nat[*].public_ip }

# Bad: "just look at the state file"

3. Validează devreme

variable "environment" {
  type = string
  validation {
    condition     = contains(["production", "staging", "development"], var.environment)
    error_message = "Environment must be production, staging, or development."
  }
}

Prinde greșelile la plan, nu la apply.

Pipeline-ul CI/CD

Nimeni nu rulează terraform apply local. Pipeline-ul nostru:

# Simplified CI flow
on:
  pull_request:
    paths: ["environments/**"]

jobs:
  plan:
    steps:
      - name: Detect changed environments
        # Only plan what changed
      - name: terraform init
      - name: terraform validate
      - name: terraform plan
        # Output saved as artifact
      - name: Post plan as PR comment
        # Reviewers see exact changes

  apply:
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    steps:
      - name: terraform apply
        # Uses the saved plan — no drift between plan and apply

Reguli critice:

  • Plan pe PR — Fiecare reviewer vede exact schimbările înainte de merge
  • Apply la merge — Merge-ul în main declanșează apply-ul. Fără pași manuali.
  • Plan-uri salvate — Artefactul de plan din PR este ceea ce se aplică. Fără surprize.
  • Doar ce s-a schimbat — Nu face plan pe toate cele 50 de environment-uri când s-a schimbat un singur fișier

Detectarea drift-ului

Infrastructura derivă. Cineva dă click într-o consolă, echipa de securitate adaugă manual o regulă, un auto-scaler schimbă numărul de instanțe. Trebuie să detectezi asta:

# Run nightly via cron
terragrunt run-all plan -detailed-exitcode

# Exit code 0 = no changes (in sync)
# Exit code 1 = error
# Exit code 2 = changes detected (DRIFT!)

Rulăm detectarea drift-ului în fiecare noapte. Orice drift declanșează o alertă pe Slack și un tichet. Rezolvarea este întotdeauna aceeași: fie actualizezi Terraform-ul ca să reflecte realitatea, fie re-aplici ca să aduci realitatea la ce spune Terraform.

Gestionarea secretelor

Nu pune niciodată secrete în fișiere .tfvars. Opțiuni:

  1. Variabile de mediuTF_VAR_db_password setat în CI
  2. Provider-ul Vault — Citește secretele din HashiCorp Vault la momentul plan-ului
  3. SOPS — Criptează fișierele .tfvars cu secrete folosind chei age/KMS
  4. 1Password/AWS SSM — Referențiază secretele după cale, rezolvă-le la apply

Noi preferăm provider-ul Vault pentru producție și SOPS pentru setup-uri mai mici.

Greșeli frecvente pe care le reparăm

Greșeală Remediu
Un singur fișier de state uriaș Împarte pe grupuri de resurse
Copy-paste între environment-uri Folosește Terragrunt sau workspaces
Apply rulat local Doar CI/CD, cu plan-uri salvate
Fără state locking DynamoDB/echivalent, întotdeauna
Valori hardcodate în module Variabile cu validare
Fără detectare de drift Plan nocturn din cron
Secrete în state, necriptate Backend criptat + output-uri sensitive

Cum începi

Dacă Terraform-ul tău este deja o harababură, nu încerca să repari totul deodată:

  1. Săptămâna 1 — Mută state-ul pe un backend remote criptat, cu locking
  2. Săptămâna 2 — Împarte state-ul monolitic pe grupuri de resurse
  3. Săptămâna 3 — Extrage primul modul partajat
  4. Săptămâna 4 — Configurează CI/CD cu plan-on-PR

Sau lasă-ne pe noi. Am migrat echipe de la „un main.tf mare” la setup-uri multi-environment scalabile în mai puțin de o lună.