Módulo 9 · Despliegue y operaciones

Lección 44 — Infraestructura como código (Terraform)

La infraestructura se versiona, se revisa y se destruye sin llorar.

Publicada
En esta lección
  1. Objetivos
  2. 1. El clickops y sus graves consecuencias
  3. 2. El Terraform de TicketFlow: la infra mínima real
  4. 3. El flujo: write → plan → apply
  5. 4. El state: la memoria y su candado
  6. 5. Secretos, destrucción y el ADR final
  7. Autoevaluación

Stack: Terraform · Proyecto: TicketFlow Estado: Publicada — cierre del módulo de despliegue Prerrequisito: Lección 43 — Kubernetes y servicios gestionados


Objetivos

  1. Declarar la infraestructura de TicketFlow en Terraform: la VPC, el Postgres gestionado, el Redis, el service del app y sus IAM.
  2. Internalizar el flujo write→plan→apply con el state y los riesgos del apply a mano (el terraform plan es la revisión de código de la infra).
  3. Aplicar las reglas del proyecto: el state remoto con lock, los secretos fuera del código (27) y la destrucción sin drama.

1. El clickops y sus graves consecuencias

Sin IaC, la infra vive en la consola del proveedor: nadie sabe QUÉ existe ni POR QUÉ (¿ese NAT del 42 fue deliberado o quedó de un experimento?), el staging diverge del prod (la 27 rota a nivel de infraestructura), y el disaster recovery (47) es "intentar recordar cómo se montó todo". El IaC declara el estado deseado en ficheros versionados: git ES la fuente de verdad, el PR ES la revisión, el plan ES la prueba. La regla del proyecto: si el clickops lo creó, no existe de verdad — todo recurso lleva su fichero.tf o se declara huérfano (el hunting del 42 formalizado).

2. El Terraform de TicketFlow: la infra mínima real

hcl
# infra/main.tf (extracto; providers y backends en sus ficheros)
variable "env" { type = string }                 # dev | staging | prod (27)

module "network" {
  source      = "./modules/network"
  env         = var.env
  cidr        = "10.0.0.0/16"
}

resource "google_sql_database_instance" "pg" {
  name             = "ticketflow-${var.env}"
  database_version = "POSTGRES_16"
  settings {
    tier              = var.env == "prod" ? "db-custom-2-7680" : "db-f1-micro"
    availability_type = var.env == "prod" ? "REGIONAL" : "ZONAL"     # el SLO del 46 decide el costo
    ip_configuration {
      ipv4_enabled    = false
      private_network = module.network.id          # el 42: solo VPC privada
    }
  }
  deletion_protection = var.env == "prod"          # el `destroy` del prod pide doble ceremonia
}

resource "google_redis_instance" "cache" {
  name           = "ticketflow-${var.env}"
  memory_size_gb = 1
  connect_mode   = "PRIVATE_SERVICE_ACCESS"
}

resource "google_cloud_run_service" "web" {
  name     = "ticketflow-web-${var.env}"
  location = "europe-west1"
  template {
    spec {
      containers {
        image = var.image_digest            # el artefacto del 41, JAMÁS :latest
        env {
          name  = "DATABASE_URL"
          value = "postgres://...${google_sql_database_instance.pg.private_ip_address}..."
        }
      }
    }
  }
  metadata { annotations = { "autoscaling.knative.dev/maxScale" = "8" } }   # el HPA del 43 en el presupuesto del 39
}

La pieza crítica: var.image_digest llega del pipeline (41) — el IaC despliega EL artefacto testeado, no reconstruye nada. Y las variables por entorno (var.env): el MISMO código despliega los tres entornos con distinto tier/HA (la 27 llevada a la infraestructura: la imagen del 40 era "una imagen, tres entornos"; esto es "un módulo, tres entornos").

3. El flujo: write → plan → apply

El ciclo obligatorio: (1) terraform plan (el diff de la infra: "+1 instancia, ~30 €/mes" — se lee como el diff de un PR); (2) el plan se guarda como artefacto y el apply aplica EXACTAMENTE ese plan (no un plan re-calculado a medianoche); (3) terraform apply con aprobación (en CI: el PR muestra el plan como comment; el merge dispara el apply). La lectura del plan es una habilidad: los forces replacement (el recurso que se DESTRUYE y recrea: una BD con name cambiado = datos fuera) y los cambios in-place seguros — el plan de la BD borrada accidental empieza con -/+ resource "google_sql_database_instance" y 3 segundos de lectura lo evitan.

bash
terraform plan -out=tfplan -var="env=prod" -var="image_digest=sha256:9f2c..."
terraform apply tfplan        # aplica EXACTAMENTE lo revisado

4. El state: la memoria y su candado

El state de Terraform es el mapa recurso→ID real: sin él, Terraform no sabe qué existe (y el apply de un state perdido puede CREAR duplicados). Las reglas del proyecto: (1) backend remoto (GCS/S3 con lock nativo): el state NUNCA local ni commiteado (contiene los secretos de los recursos en claro — el bucket del state con acceso de proyecto solamente); (2) el lock del state (el backend lo ofrece): dos applies simultáneos (el colega y tú, o dos pipelines) corrompen — el lock serializa; (3) el state por entorno (dev/staging/prod en paths distintos) con la menor blast radius posible: el error del prod-state no toca el staging. El drift (alguien hizo clickops): terraform plan lo revela como diff inesperado — la revisión mensual del 42 lo corre y reconcilia (import del recurso huérfano o destrucción).

5. Secretos, destrucción y el ADR final

Los secretos NO viven en el.tf (el plan y el state los expondrían): el.tf referencia el gestor (Secret Manager) y el runtime los lee (27/42):

hcl
resource "google_secret_manager_secret" "django_key" { secret_id = "django-secret-key" }
# el valor lo carga el despliegue del 41 desde el gestor; el .tf solo declara que EXISTE

La destrucción (terraform destroy): legítima en entornos efímeros (el staging nocturno que se apaga: la factura del 42 lo agradece), con candados en prod (deletion_protection, el prevent_destroy en los módulos de datos, y la ceremonia del doble apply). El ADR de cierre del módulo: "La infra de TicketFlow es código (44): el state en GCS con lock por entorno, el plan en el PR, los secretos en el gestor, el digest del 41 como único input de despliegue. La reconstrucción completa del entorno desde cero debe tardar <30 min — el disaster recovery (47) es un terraform apply, no una semana de memoria".


Autoevaluación

  1. ¿Qué tres problemas del clickops resuelve el IaC y qué significa "si el clickops lo creó, no existe de verdad"?
  2. En el.tf del §2: ¿de dónde sale var.image_digest y por qué JAMÁS :latest? ¿Qué decide availability_type por entorno?
  3. El plan: ¿qué es un forces replacement y qué diferencia hay entre apply directo y apply tfplan guardado?
  4. ¿Qué es el state, por qué va remoto con lock y cómo se detecta el drift del clickops?
  5. ¿Por qué los secretos no viven en el.tf ni en el state local, y qué ceremonia protege al prod del destroy?

Continúa con los ejercicios. Las solutions.md solo tras intentarlo.