> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bbrands.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Infraestructura como código (Terraform)

> Cada recurso de Better Stack está definido como código bajo infra/betterstack. Entiende el layout, el estado por entorno, y el significado exacto de plan, apply e import.

<Info>
  Toda la configuración de Better Stack — páginas de estado, monitores, heartbeats, labels
  de logs, dashboards y alertas — se declara como código bajo
  `infra/betterstack/`. La UI nunca es la fuente de verdad; Terraform lo es.
</Info>

## Por qué infraestructura como código

Configurar docenas de monitores, secciones y alertas a mano no escala y
deriva silenciosamente entre entornos. Terraform nos da:

| UI manual                          | Terraform                                                  |
| ---------------------------------- | ---------------------------------------------------------- |
| Docenas de objetos creados una vez | `terraform apply` por entorno                              |
| Drift entre dev / cert / prod      | Mismos módulos, distintos `*.tfvars`                       |
| Sin rastro de revisión             | Archivos `.tf` revisados en PR                             |
| "¿Quién cambió esto?"              | Historial de Git + estado, reproducible en cualquier lugar |

## Cómo piensa Terraform

Terraform reconcilia continuamente **tres** imágenes del mundo. Entender
esto es la clave para todo lo demás en esta página:

```mermaid theme={null}
flowchart LR
    code["Declared config<br/>.tf + adopted/*.json"]
    state["Tracked state<br/>terraform.tfstate"]
    real["Real world<br/>Better Stack API"]

    code -->|"desired"| engine((Terraform))
    state -->|"known"| engine
    real -->|"actual"| engine
    engine -->|"reconcile"| result["plan / apply"]
```

* **Config declarada** — lo que los archivos `.tf` y los manifiestos de adopción dicen que debería
  existir.
* **Estado rastreado** (`terraform.tfstate`) — el inventario de Terraform de lo que
  ya gestiona, incluyendo el id de Better Stack de cada recurso.
* **Mundo real** — lo que la API de Better Stack devuelve en este momento.

## `plan` vs `apply` — la distinción central

Este es el concepto que cada operador debe internalizar. La analogía más simple:
**`plan` es el diff, `apply` es el commit.**

<CardGroup cols={2}>
  <Card title="terraform plan — dry run, solo lectura" icon="magnifying-glass">
    Refresca el estado desde la API de Better Stack, lo compara con la config
    declarada, e imprime las acciones que *tomaría*: `+ create`, `~ update`,
    `- destroy`. No cambia **nada**. Un plan que muestra
    `0 to add, 0 to change, 0 to destroy` prueba que no hay drift.
  </Card>

  <Card title="terraform apply — ejecuta el cambio" icon="play">
    Toma ese mismo diff y ejecuta las llamadas reales a la API (POST / PATCH / DELETE),
    luego escribe los ids resultantes de vuelta en el tfstate. Esta es la **única**
    operación que muta la infraestructura.
  </Card>
</CardGroup>

<Note>
  Nunca haces `apply` sin leer primero el `plan`. En CI el plan se imprime
  en el log del job y `apply` requiere adicionalmente escribir `APPLY` — consulta
  [CI/CD para el monitoreo](/es/monitoring/github-actions).
</Note>

### Un tercer verbo: `import` (adopción)

Cuando un recurso ya existe en Better Stack (creado a mano antes de que Terraform
lo gestionara), `import` lo adjunta al estado **sin recrearlo**. Así
es como se adoptaron los monitores existentes: el primer `apply` en development
importó **68** recursos, y el `plan` de seguimiento reportó **0 cambios**.

### Idempotencia — por qué reejecutar es seguro

Terraform es declarativo: aplicar la misma config dos veces no duplica
nada, siempre que el estado se preserve.

| Ejecución                       | Qué sucede                                              |
| ------------------------------- | ------------------------------------------------------- |
| Primer `apply`                  | Crea los recursos; almacena sus ids en el tfstate       |
| Segundo `apply` (sin cambios)   | `0 to add, 0 to change, 0 to destroy` — nada se duplica |
| `apply` tras editar un `.tf`    | Actualiza solo los atributos cambiados en su lugar      |
| `apply` tras agregar un monitor | Crea solo ese; los recursos existentes quedan intactos  |

<Warning>
  Los duplicados solo ocurren si el estado se **pierde o se omite** — por ejemplo,
  aplicando contra un estado local vacío mientras los objetos ya existen en la UI,
  o borrando `terraform.tfstate`. Esto es exactamente por lo que CI se niega a ejecutarse sin
  un backend remoto, y por lo que los recursos existentes se importan antes del primer
  apply.
</Warning>

## Layout del repositorio

```text theme={null}
infra/betterstack/
  main.tf, providers.tf, variables.tf, outputs.tf, locals.tf, versions.tf
  environments/
    development.tfvars      # feature flags per environment (no secrets)
    certification.tfvars
    production.tfvars
  adopted/
    development.json        # desired state (ids + attributes)
    development.imports.tf  # one-time import blocks
    certification.json
    production.json
  modules/
    uptime-environment/     # status page, sections, monitors, heartbeats
    notification-policies/  # políticas de escalamiento a Slack + urgencias solo-Slack
    api-log-alerts/         # labels de logs, métrica duration_ms, dashboard, gráficos, alertas
    idp-oauth-alerts/       # monitor JWKS del IdP + alertas de logs OAuth
    infrastructure-charts/  # gráficos PostgreSQL/Redis (cron metrics-push)
    platform-log-alerts/    # platform-wide log alerts
  backend_override.tf.example  # template for the remote state backend
```

| Módulo                  | Gestiona                                                                                         | Habilitado por                                       |
| ----------------------- | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------- |
| `uptime-environment`    | Página de estado, secciones, 31 monitores, 3 heartbeats                                          | `enable_uptime_monitors = true`                      |
| `notification-policies` | Políticas de escalamiento a Slack + urgencias solo-Slack (sin email/SMS)                         | `slack_integration_ids` no vacío                     |
| `api-log-alerts`        | Labels de logs, métrica `duration_ms`, dashboard, gráficos 5xx + latencia, alertas por categoría | `enable_api_log_alerts` + `logtail_enabled` + source |
| `idp-oauth-alerts`      | Monitor JWKS del IdP + alertas de logs de errores OAuth                                          | `enable_idp_oauth_alerts` / `idp_issuer_url`         |
| `infrastructure-charts` | Gráficos de PostgreSQL/Redis en el dashboard                                                     | `enable_infrastructure_charts` + `logtail_enabled`   |
| `platform-log-alerts`   | Alertas de logs a nivel de plataforma                                                            | `enable_platform_log_alerts` + `logtail_enabled`     |

### Providers

| Provider  | Paquete                       | Gestiona                                           |
| --------- | ----------------------------- | -------------------------------------------------- |
| Uptime    | `BetterStackHQ/better-uptime` | Monitores, heartbeats, páginas de estado, recursos |
| Telemetry | `BetterStackHQ/logtail`       | Fuentes, métricas/labels, dashboards, alertas      |

## Manifiestos de adopción

Cada entorno converge al mismo catálogo — **31 monitores** (12 API,
incluyendo el journey transversal Digital Contract, + 9 integraciones +
10 plataformas), **4 secciones** y **3 heartbeats** — cada uno
apuntando a sus propios hosts. El manifiesto `adopted/<env>.json` es el estado
deseado; después de la adopción lo **editas vía PR**. El generador sintetiza entradas
que aún no existen (con `monitor_id = null`) para que Terraform las **cree** en el
apply en lugar de importarlas.

| Entorno       | Id de página de estado | Importados (ya existen)                     | Creados en el apply                             |
| ------------- | ---------------------- | ------------------------------------------- | ----------------------------------------------- |
| development   | `252747`               | los 30 + 4 + 3                              | —                                               |
| certification | `252750`               | 10 plataformas + 3 secciones + 3 heartbeats | 20 monitores de API/integración + sección Infra |
| production    | `251528`               | 10 plataformas + 3 secciones + 3 heartbeats | 20 monitores de API/integración + sección Infra |

Regenera solo para auditar el drift, y revisa siempre el diff antes de hacer commit:

```bash theme={null}
node scripts/status-monitoring/export-betterstack-inventory.mjs development
node scripts/status-monitoring/generate-adoption-manifest.mjs --all
git diff infra/betterstack/adopted/
```

## Estado por entorno (workspaces)

El estado se aísla por entorno usando **workspaces** de Terraform: el nombre del workspace
es igual al nombre del entorno, así que development, certification y production
mantienen cada uno un estado independiente bajo el prefijo `env:/` del backend.

<Warning>
  El estado local está en gitignore e incrusta el valor de `x-health-token`. Perderlo
  causa creaciones duplicadas en el próximo apply. Antes de cualquier uso por el equipo o CI, migra
  a un **backend remoto compatible con S3** compartido.
</Warning>

```bash theme={null}
cd infra/betterstack
cp backend_override.tf.example backend_override.tf   # fill in bucket/endpoint
terraform init -migrate-state
```

El mismo bloque se almacena en el secret de GitHub `TF_BACKEND_OVERRIDE` para que el
workflow lo escriba antes de `terraform init`.

## Ejecutarlo localmente

`terraform-apply.sh` envuelve `init` + selección de workspace + `plan`/`apply`, y
carga los tokens automáticamente desde `backend/horizon-api/.env.<env>` (mapeando
`BETTERSTACK_UPTIME_API_TOKEN` al `BETTERUPTIME_API_TOKEN` del provider).

```bash theme={null}
# Dry run, then apply, then confirm no drift
./scripts/status-monitoring/terraform-apply.sh plan development
./scripts/status-monitoring/terraform-apply.sh apply development
./scripts/status-monitoring/terraform-apply.sh plan development   # expect: No changes

# Shortcuts
pnpm tf:betterstack:plan
pnpm tf:betterstack:apply
```

<Note>
  `destroy` está deliberadamente protegido: requiere `CONFIRM_DESTROY=<env>` para que
  nunca pueda ejecutarse por accidente.
</Note>

## Outputs

Después del `apply`, Terraform expone:

* `status_page_resources_json` — el mapa scope de componente → id de recurso de la página de
  estado usado para llenar `BETTERSTACK_STATUS_PAGE_RESOURCES`.
* `uptime_heartbeat_ids` — los tres ids de heartbeat de cron.

## Código relacionado

* Análisis profundo de IaC: `docs/initiatives/betterstack/06-terraform-iac.md`
* README del módulo: `infra/betterstack/README.md`
* Script del runner: `scripts/status-monitoring/terraform-apply.sh`
