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
mainsolo 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.
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-vmcontieneazurerm_resource_group,azurerm_virtual_network,azurerm_subnet,azurerm_network_security_group,azurerm_network_interfaceyazurerm_windows_virtual_machine, con sus variables y salidas. Todo llega por variable. - Versiones acotadas (
~> 4.0) y el.terraform.lock.hclen 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 = trueel 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 = truebloquea 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:
- Pull request → el pipeline ejecuta
terraform fmt -check,terraform validateyterraform plan -out=tfplan, y publica el plan como comentario o artefacto. - 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.
- 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).
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-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:
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 bloqueremoved(1.7+). Ambos pasan por revisión, a diferencia deterraform state mvostate 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
.tfvarsdentro del repositorio. La contraseña de administración se genera conrandom_passwordy se guarda en Key Vault, o llega como variablesensitivedesde el almacén de secretos del pipeline. - Ojo:
sensitiveoculta 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.
What you'll get
Infrastructure for a site or a service that comes out of a reviewed repository and not from clicks in the portal. A reusable module, a shared state nobody tramples on, a pipeline where the change is read before it is applied, and an orderly way of dealing with what was changed by hand or already existed before Terraform.
Before you start
- Terraform 1.10 or later and the azurerm 4.x provider.
- A Git repository with protected branches:
mainis reached only through a reviewed pull request (Git guide). - A storage account for state, created separately, with blob versioning and soft delete enabled.
- An identity for the pipeline: managed identity or Entra ID application with workload identity federation (OIDC), with Contributor only on the subscriptions it manages. No stored client secrets.
1. The module: resource group, network and VM
Modules in modules/ and one folder per environment (envs/prod, envs/test) that uses them with its own values.
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/site-vm.tfstate" use_azuread_auth = true }} provider "azurerm" { features {} subscription_id = "00000000-0000-0000-0000-000000000000"} module "site_madrid" { source = "../../modules/site-vm" name = "mad" location = "spaincentral" address_space = "10.50.0.0/16" vm_size = "Standard_B2ms" admin_password = var.admin_password tags = { environment = "prod", owner = "laura.prado" }}- The
site-vmmodule containsazurerm_resource_group,azurerm_virtual_network,azurerm_subnet,azurerm_network_security_group,azurerm_network_interfaceandazurerm_windows_virtual_machine, with their variables and outputs. Everything comes in through variables. - Constrained versions (
~> 4.0) and.terraform.lock.hclcommitted, so everyone uses the same provider.
2. Remote state with locking
State records which real resource corresponds to each block. On someone's laptop it is no use to a team.
- azurerm: state lives in a blob and locking uses the blob lease, with nothing else to configure. With
use_azuread_auth = trueaccess uses an Entra ID identity (Storage Blob Data Contributor role), not the account key. - S3 (on AWS): since Terraform 1.10,
use_lockfile = truelocks with a file in the bucket itself; the DynamoDB table is deprecated. - One state key per environment and component (
prod/site-vm.tfstate,prod/core-network.tfstate). A giant state lets one mistake reach everything.
If an apply is cut off, the lock can stay in place. terraform force-unlock <ID> only after confirming no other apply is running: two processes writing will corrupt the state.
3. Plan and apply in the pipeline, with review
Nobody runs apply from their own machine against production. The flow:
- Pull request → the pipeline runs
terraform fmt -check,terraform validateandterraform plan -out=tfplan, and publishes the plan as a comment or artefact. - A colleague reads the plan, not just the code. What matters is what gets created, what changes and, above all, what gets destroyed.
- Merge to
main→ the pipeline applies that same saved plan (terraform apply tfplan) after the environment approval (Azure DevOps or GitHub environments).
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.site_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-site-mad/providers/Microsoft.Network/networkSecurityGroups/nsg-srv-mad01" name = "nsg-srv-mad01" ~ security_rule = [ + { + name = "allow-https-office" + source_address_prefix = "192.168.10.0/24" + priority = 200 # (10 unchanged attributes hidden) }, ] } Plan: 0 to add, 1 to change, 0 to destroy.A useful review rule: any plan with destroy or -/+ (replace) on a resource holding data (VM, disk, database) stops and gets discussed.
4. "Someone changed it in the portal": drift
On a Friday night someone resized the VM from the portal to get by. The next plan spots it:
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.site_madrid.azurerm_windows_virtual_machine.app has changed ~ resource "azurerm_windows_virtual_machine" "app" { id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-site-mad/providers/Microsoft.Compute/virtualMachines/srv-mad01" name = "srv-mad01" ~ size = "Standard_B2ms" -> "Standard_D4s_v5" tags = { "environment" = "prod" "owner" = "laura.prado" } }There are two ways out, and you decide with the service owner, not alone:
- The change was needed: it goes into the code (
vm_size = "Standard_D4s_v5") through a pull request. The next plan comes out empty. - It wasn't: a normal plan proposes putting it back to
Standard_B2ms, and it is applied in a window (resizing restarts the VM).
A nightly scheduled plan against each environment, alerting if anything other than No changes comes out, turns drift into an alert instead of a surprise. And with no standing Contributor for people in production (read plus PIM), drift is the exception.
5. Importing what already exists
The print server was created by hand years ago. Since Terraform 1.5, import is declared in code and goes through the same reviewed plan:
cat import.tfimport { to = azurerm_windows_virtual_machine.print id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-site-mad/providers/Microsoft.Compute/virtualMachines/srv-print01"}terraform plan -generate-config-out=generated.tfazurerm_windows_virtual_machine.print: Preparing import... [id=/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-site-mad/providers/Microsoft.Compute/virtualMachines/srv-print01] Terraform will perform the following actions: # azurerm_windows_virtual_machine.print will be imported # (config will be generated) Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.generated.tf is a starting point, not final code: clean it up (drop default attributes, move values to variables), repeat the plan until it says 1 to import, 0 to change and apply.
6. State hygiene and secrets
- Move without destroying: when renaming a resource or moving it into a module, a
moved { from = ... to = ... }block stops Terraform destroying and recreating it. To stop managing something without deleting it, aremovedblock (1.7+). Both go through review, unliketerraform state mvorstate rm. - State is not edited by hand. If something breaks, blob versioning lets you go back to the previous version.
- Secrets out of the code: no passwords in
.tfvarsinside the repository. The admin password is generated withrandom_passwordand stored in Key Vault, or arrives as asensitivevariable from the pipeline's secret store. - Careful:
sensitivehides the value in output, but state stores it in clear text. Access to the state container is restricted like Key Vault's; write-only arguments (Terraform 1.11+) keep secrets out of state where the provider supports them.
7. What you review each month
- The nightly drift plan: environments with pending changes and why.
- Who has write access to the state container and to the subscriptions, besides the pipeline.
- Resources in the portal that aren't in any state (Azure Resource Graph by a
managed-bytag): candidates to import or delete.
Next steps
- The Amazon AWS guide: the same discipline with the S3 backend and the
awsprovider. - The Jenkins (CI/CD) guide to build the pipeline if you don't use Azure DevOps or GitHub Actions, and the Ansible guide to configure inside the VMs what Terraform creates.
- The Kubernetes guide to create the AKS cluster with this same flow.
Is something missing, or different in your environment? Get in touch and I'll fix it.
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.Found this useful? Every two weeks I send one practical idea for IT professionals: LinkedIn, projects, networking and hiring processes.
Apúntate y llévate las 33 ideasSign up and get the 33 ideas →