Guías · Nivel 4

Terraform: un módulo, estado remoto, el plan en el pipeline y alguien que lo cambió en el portal

Infraestructura como código para un equipo de sistemas: un módulo pequeño en Azure (grupo de recursos, red y VM), estado remoto con bloqueo, plan y apply en un pipeline con revisión, qué hacer con la deriva, cómo importar lo que ya existe y cómo mantener el estado limpio y los secretos fuera del repositorio.

Qué vas a conseguir

Que la infraestructura de una sede o de un servicio salga de un repositorio revisado y no de clics en el portal. Un módulo reutilizable, un estado compartido que nadie pisa, un pipeline donde el cambio se lee antes de aplicarse y una forma ordenada de tratar lo que se cambió a mano o lo que ya existía antes de Terraform.

Antes de empezar

  • Terraform 1.10 o posterior y el proveedor azurerm 4.x.
  • Un repositorio Git con ramas protegidas: a main solo se llega por pull request revisado (guía de Git).
  • Una cuenta de almacenamiento para el estado, creada aparte, con versionado de blobs y eliminación temporal activados.
  • Una identidad para el pipeline: identidad administrada o aplicación de Entra ID con federación de identidades de carga de trabajo (OIDC), con rol Contributor solo sobre las suscripciones que gestiona. Sin secretos de cliente guardados.

1. El módulo: grupo de recursos, red y VM

Módulos en modules/ y una carpeta por entorno (envs/prod, envs/pruebas) que los usa con sus valores.

repo infra-azure
cat envs/prod/main.tfterraform {  required_version = ">= 1.10"  required_providers {    azurerm = { source = "hashicorp/azurerm", version = "~> 4.0" }  }  backend "azurerm" {    resource_group_name  = "rg-tfstate"    storage_account_name = "sttfstatecontoso01"    container_name       = "tfstate"    key                  = "prod/sede-vm.tfstate"    use_azuread_auth     = true  }} provider "azurerm" {  features {}  subscription_id = "00000000-0000-0000-0000-000000000000"} module "sede_madrid" {  source         = "../../modules/sede-vm"  nombre         = "mad"  ubicacion      = "spaincentral"  rango_red      = "10.50.0.0/16"  tamano_vm      = "Standard_B2ms"  admin_password = var.admin_password  etiquetas      = { entorno = "prod", responsable = "laura.prado" }}
  • El módulo sede-vm contiene azurerm_resource_group, azurerm_virtual_network, azurerm_subnet, azurerm_network_security_group, azurerm_network_interface y azurerm_windows_virtual_machine, con sus variables y salidas. Todo llega por variable.
  • Versiones acotadas (~> 4.0) y el .terraform.lock.hcl en el repositorio, para que todos usen el mismo proveedor.

2. Estado remoto con bloqueo

El estado dice qué recurso real corresponde a cada bloque. En el portátil de alguien no sirve para un equipo.

  • azurerm: el estado vive en un blob y el bloqueo usa el lease del blob, sin nada más que configurar. Con use_azuread_auth = true el acceso es con identidad de Entra ID (rol Storage Blob Data Contributor), no con la clave de la cuenta.
  • S3 (en AWS): desde Terraform 1.10, use_lockfile = true bloquea con un fichero en el propio bucket; la tabla de DynamoDB queda obsoleta.
  • Una clave de estado por entorno y componente (prod/sede-vm.tfstate, prod/red-core.tfstate). Un estado gigante hace que un error alcance a todo.

Si un apply se corta, el bloqueo puede quedarse puesto. terraform force-unlock <ID> solo tras confirmar que no hay otro apply en marcha: dos procesos escribiendo corrompen el estado.

3. Plan y apply en el pipeline, con revisión

Nadie ejecuta apply desde su equipo contra producción. El flujo:

  1. Pull request → el pipeline ejecuta terraform fmt -check, terraform validate y terraform plan -out=tfplan, y publica el plan como comentario o artefacto.
  2. Un compañero lee el plan, no solo el código. Lo que importa es qué se crea, qué cambia y, sobre todo, qué se destruye.
  3. Merge a main → el pipeline aplica ese mismo plan guardado (terraform apply tfplan) tras la aprobación del entorno (environments de Azure DevOps o de GitHub).
Pipeline · etapa plan (PR 214)
terraform init -input=falseterraform plan -input=false -out=tfplanTerraform used the selected providers to generate the following executionplan. Resource actions are indicated with the following symbols:  ~ update in-place Terraform will perform the following actions:   # module.sede_madrid.azurerm_network_security_group.vm will be updated in-place  ~ resource "azurerm_network_security_group" "vm" {        id   = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-sede-mad/providers/Microsoft.Network/networkSecurityGroups/nsg-srv-mad01"        name = "nsg-srv-mad01"      ~ security_rule = [          + {              + name                       = "allow-https-oficina"              + source_address_prefix      = "192.168.10.0/24"              + priority                   = 200                # (10 unchanged attributes hidden)            },        ]    } Plan: 0 to add, 1 to change, 0 to destroy.

Una regla útil en la revisión: cualquier plan con destroy o con -/+ (replace) en un recurso con datos (VM, disco, base de datos) se para y se habla.

4. "Alguien lo cambió en el portal": la deriva

Un viernes por la noche alguien amplió la VM desde el portal para salir del paso. El siguiente plan lo detecta:

terraform plan -refresh-only
terraform plan -refresh-onlyNote: Objects have changed outside of Terraform Terraform detected the following changes made outside of Terraform since thelast "terraform apply" which may have affected this plan:   # module.sede_madrid.azurerm_windows_virtual_machine.app has changed  ~ resource "azurerm_windows_virtual_machine" "app" {        id   = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-sede-mad/providers/Microsoft.Compute/virtualMachines/srv-mad01"        name = "srv-mad01"      ~ size = "Standard_B2ms" -> "Standard_D4s_v5"        tags = {            "entorno"     = "prod"            "responsable" = "laura.prado"        }    }

Hay dos salidas, y se decide con el responsable del servicio, no en solitario:

  • El cambio era necesario: se lleva al código (tamano_vm = "Standard_D4s_v5") por pull request. El plan siguiente queda vacío.
  • No lo era: un plan normal propone devolverla a Standard_B2ms, y se aplica en una ventana (cambiar el tamaño reinicia la VM).

Un plan programado cada noche contra cada entorno, que avisa si sale algo distinto de No changes, convierte la deriva en un aviso en lugar de una sorpresa. Y sin rol Contributor permanente para las personas en producción (lectura más PIM), la deriva es la excepción.

5. Importar lo que ya existe

El servidor de impresión se creó a mano hace años. Desde Terraform 1.5, la importación se declara en el código y pasa por el mismo plan revisado:

import.tf + plan
cat import.tfimport {  to = azurerm_windows_virtual_machine.impresion  id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-sede-mad/providers/Microsoft.Compute/virtualMachines/srv-print01"}terraform plan -generate-config-out=generado.tfazurerm_windows_virtual_machine.impresion: Preparing import... [id=/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-sede-mad/providers/Microsoft.Compute/virtualMachines/srv-print01] Terraform will perform the following actions:   # azurerm_windows_virtual_machine.impresion will be imported  # (config will be generated) Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.

generado.tf es un punto de partida, no código final: se limpia (fuera atributos por defecto, valores a variables), se repite el plan hasta que diga 1 to import, 0 to change y se aplica.

6. Higiene del estado y secretos

  • Mover sin destruir: al renombrar un recurso o meterlo en un módulo, un bloque moved { from = ... to = ... } evita que Terraform lo destruya y lo cree de nuevo. Para dejar de gestionar algo sin borrarlo, un bloque removed (1.7+). Ambos pasan por revisión, a diferencia de terraform state mv o state rm.
  • El estado no se edita a mano. Si algo se rompe, el versionado del blob permite volver a la versión anterior.
  • Secretos fuera del código: nada de contraseñas en .tfvars dentro del repositorio. La contraseña de administración se genera con random_password y se guarda en Key Vault, o llega como variable sensitive desde el almacén de secretos del pipeline.
  • Ojo: sensitive oculta el valor en la salida, pero el estado lo guarda en claro. El acceso al contenedor del estado se restringe como el de Key Vault; los argumentos write-only (Terraform 1.11+) evitan que lleguen al estado cuando el proveedor los admite.

7. Lo que revisas cada mes

  • El plan nocturno de deriva: entornos con cambios pendientes y por qué.
  • Quién tiene permisos de escritura sobre el contenedor del estado y sobre las suscripciones, además del pipeline.
  • Recursos en el portal que no están en ningún estado (Azure Resource Graph por etiqueta gestionado-por): candidatos a importar o a borrar.

Siguientes pasos

  • La guía de Amazon AWS: la misma disciplina con el backend S3 y el proveedor aws.
  • La guía de Jenkins (CI/CD) para montar el pipeline si no usáis Azure DevOps o GitHub Actions, y la de Ansible para configurar dentro de las VM lo que Terraform crea.
  • La guía de Kubernetes para crear el clúster AKS con este mismo flujo.

¿Falta algo o en tu entorno es distinto? Escríbeme y lo corrijo.

Tech Career Lab · newsletter

¿Te ha servido? Cada dos semanas envío una idea práctica para perfiles IT: LinkedIn, proyectos, networking y procesos de selección.

Apúntate y llévate las 33 ideas →