🧩 Variables básicas en Terraform¶
1. ¿Qué es una variable en Terraform?¶
Una variable de entrada es un parámetro de la configuración. Se declara una vez en un archivo .tf, se usa en tantos sitios como haga falta con var.nombre, y su valor lo decide quien ejecuta Terraform: en un archivo .tfvars, en la línea de comandos, en una variable de entorno o, si no lo recibe de ninguna parte, respondiendo a una pregunta interactiva.
¿Por qué usar variables?
- Evitar duplicar código: un solo main.tf para todos los entornos.
- Separar código y configuración: el código se revisa y versiona; los valores de cada entorno viven en su archivo.
- Reutilizar módulos: un módulo se invoca muchas veces con valores distintos.
- Proteger secretos: con sensitive = true el valor no se imprime en la consola.
- Validar antes de desplegar: un valor incorrecto falla en el plan, con tu mensaje, no en la API.
No confundas la variable con el local: la variable la fija quien ejecuta; el local (locals { ... }) lo calcula el código a partir de otros valores. Los verás juntos en el ejemplo.
2. Declaración de variables¶
Se usa el bloque variable, por convención en un archivo variables.tf (Terraform carga todos los .tf del directorio, así que el nombre del archivo es solo orden).
variable "entorno" {
description = "Nombre del entorno (dev, test, prod)"
type = string
default = "dev" # valor si nadie indica otro
}
| Argumento | Función | ¿Obligatorio? |
|---|---|---|
description |
Para qué sirve. Se muestra si Terraform pide el valor de forma interactiva. | No, pero ponlo siempre |
type |
Tipo de dato. Terraform rechaza en el plan un valor que no encaje. |
No, pero ponlo siempre |
default |
Valor por defecto. Debe ser un literal: sin funciones ni referencias. Sin él, la variable es obligatoria. | No |
Hay tres argumentos más (validation, sensitive, nullable). El primero lo verás al final de esta página; los otros dos, en la siguiente.
3. Tipos de datos¶
Los tres primeros son primitivos; los dos últimos, colecciones, y llevan entre paréntesis el tipo de sus elementos (list(string), no list).
# string: texto (nombres, regiones, SKUs)
variable "nombre_proyecto" {
type = string
description = "Nombre corto del proyecto, en minúsculas"
default = "demo"
}
# number: entero o decimal
variable "numero_subredes" {
type = number
description = "Cuántas subredes crear en la red virtual"
default = 2
}
# bool: verdadero o falso
variable "crear_storage" {
type = bool
description = "Crear o no la cuenta de almacenamiento"
default = true
}
# list(string): secuencia ordenada, se indexa desde 0
variable "address_space" {
type = list(string)
description = "Rangos de la red virtual"
default = ["10.0.0.0/16"]
}
# map(string): pares clave-valor
variable "tags" {
type = map(string)
description = "Etiquetas para los recursos"
default = {
entorno = "dev"
propietario = "equipo-devops"
}
}
| Tipo | Cómo se usa | En el ejemplo decide... |
|---|---|---|
string |
var.nombre_proyecto, o dentro de texto "rg-${var.nombre_proyecto}" |
Los nombres de todo |
number |
count = var.numero_subredes |
Cuántas subredes existen |
bool |
count = var.crear_storage ? 1 : 0 |
Si existe la cuenta de almacenamiento |
list(string) |
var.address_space entero, o var.address_space[0] |
El rango de la red |
map(string) |
var.tags entero, o var.tags["entorno"] |
Las etiquetas |
4. Ejemplo práctico en Topaz¶
Un grupo de recursos, una red virtual con N subredes y una cuenta de almacenamiento opcional. Los cinco tipos de la sección anterior tienen aquí un efecto que podrás ver en el plan.
🔷 En Topaz. El emulador no incluye el proveedor
Microsoft.Compute, así que no se pueden crear máquinas virtuales; por eso el ejemplo usa red y almacenamiento, que sí están soportados. Las máquinas virtuales (azurerm_linux_virtual_machinecon su interfaz de red, clave SSH e imagen0001-com-ubuntu-server-jammy) se ven en el módulo de Azure real.
Paso 1. Directorio y providers.tf¶
# providers.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
}
}
provider "azurerm" {
features {}
metadata_host = "topaz.local.dev:8899"
resource_provider_registrations = "none"
subscription_id = "00000000-0000-0000-0000-000000000001"
}
Paso 2. variables.tf¶
Las cinco variables de la sección 3 más la región:
variable "nombre_proyecto" {
type = string
description = "Nombre corto del proyecto, en minúsculas y sin guiones"
default = "demo"
}
variable "ubicacion" {
type = string
description = "Región de Azure (nombre corto)"
default = "eastus"
}
variable "numero_subredes" {
type = number
description = "Cuántas subredes crear"
default = 2
}
variable "crear_storage" {
type = bool
description = "Crear o no la cuenta de almacenamiento"
default = true
}
variable "address_space" {
type = list(string)
description = "Rangos de la red virtual"
default = ["10.0.0.0/16"]
}
variable "tags" {
type = map(string)
description = "Etiquetas para los recursos"
default = {
entorno = "dev"
propietario = "equipo-devops"
}
}
Paso 3. main.tf¶
locals {
# Calculado a partir de variables: por eso es un local y no un default
tags = merge(var.tags, { gestion = "terraform" })
}
resource "azurerm_resource_group" "lab" {
name = "rg-${var.nombre_proyecto}-001"
location = var.ubicacion
tags = local.tags
lifecycle {
ignore_changes = [tags] # Topaz no devuelve las tags del grupo
}
}
resource "azurerm_virtual_network" "lab" {
name = "vnet-${var.nombre_proyecto}"
location = azurerm_resource_group.lab.location
resource_group_name = azurerm_resource_group.lab.name
address_space = var.address_space # list(string) completa
tags = local.tags
}
resource "azurerm_subnet" "lab" {
count = var.numero_subredes # number → cuántas copias
name = "snet-${count.index + 1}"
resource_group_name = azurerm_resource_group.lab.name
virtual_network_name = azurerm_virtual_network.lab.name
# Divide el primer rango en /24: 10.0.0.0/24, 10.0.1.0/24, ...
address_prefixes = [cidrsubnet(var.address_space[0], 8, count.index)]
}
resource "azurerm_storage_account" "lab" {
count = var.crear_storage ? 1 : 0 # bool → existe o no
name = "st${var.nombre_proyecto}001"
resource_group_name = azurerm_resource_group.lab.name
location = azurerm_resource_group.lab.location
account_tier = "Standard"
account_replication_type = "LRS"
tags = local.tags
}
Paso 4. outputs.tf¶
output "subredes" {
description = "Nombre y prefijo de cada subred"
value = { for s in azurerm_subnet.lab : s.name => s.address_prefixes[0] }
}
output "storage" {
description = "Endpoint de blobs, o null si no se creó la cuenta"
value = var.crear_storage ? azurerm_storage_account.lab[0].primary_blob_endpoint : null
}
Paso 5. Desplegar con los valores por defecto¶
# azurerm_resource_group.lab will be created
+ name = "rg-demo-001"
# azurerm_virtual_network.lab will be created
+ address_space = [ + "10.0.0.0/16" ]
# azurerm_subnet.lab[0] will be created
+ address_prefixes = [ + "10.0.0.0/24" ]
# azurerm_subnet.lab[1] will be created
+ address_prefixes = [ + "10.0.1.0/24" ]
# azurerm_storage_account.lab[0] will be created
+ name = "stdemo001"
Plan: 5 to add, 0 to change, 0 to destroy.
terraform apply -auto-approve
terraform output subredes
# { "snet-1" = "10.0.0.0/24", "snet-2" = "10.0.1.0/24" }
az resource list -g rg-demo-001 -o table
Ningún archivo .tfvars, ningún -var: todo ha salido de los default. Ahora vas a cambiar valores sin tocar el código.
Paso 6. Cambiar un number y un bool¶
terraform plan -var numero_subredes=3
# # azurerm_subnet.lab[2] will be created (10.0.2.0/24)
# Plan: 1 to add, 0 to change, 0 to destroy.
terraform plan -var crear_storage=false
# # azurerm_storage_account.lab[0] will be destroyed
# Plan: 0 to add, 0 to change, 1 to destroy.
Un número más, una subred más; un false, una cuenta menos. Eso es lo que significa que las variables "personalizan la configuración": no cambian texto, cambian infraestructura.
5. Cómo pasar valores¶
Hay cuatro formas habituales, y todas las probarás sobre el proyecto que ya tienes aplicado.
5.1. Archivo terraform.tfvars¶
Terraform lo carga automáticamente si existe en el directorio. Es el sitio para los valores del proyecto:
# terraform.tfvars
nombre_proyecto = "demo"
numero_subredes = 3
address_space = ["10.0.0.0/16"]
tags = {
entorno = "dev"
propietario = "tu-nombre"
coste = "CC-LAB"
}
🔷 En Topaz. El grupo no aparece entre los "3 to change" gracias al
ignore_changes; la red, las subredes no (no tienen tags) y la cuenta de almacenamiento sí. El emulador acepta y devuelve correctamente las etiquetas de la red y del storage.
5.2. Archivos *.auto.tfvars¶
También se cargan solos, en orden alfabético, después de terraform.tfvars. Útiles para ajustes personales que no se suben a Git:
echo 'nombre_proyecto = "demoana"' > personal.auto.tfvars
echo '*.auto.tfvars' >> .gitignore
terraform plan # los nombres cambian → replace de todo: no apliques, solo observa
rm personal.auto.tfvars
5.3. Línea de comandos: -var y -var-file¶
terraform plan -var numero_subredes=4
terraform plan -var 'tags={entorno="test",propietario="qa"}' # mapas y listas: sintaxis HCL entre comillas simples
terraform plan -var-file=prod.tfvars # un archivo que NO se carga solo (no acaba en .auto.tfvars)
5.4. Variables de entorno TF_VAR_¶
export TF_VAR_numero_subredes=1
terraform plan # Plan: 0 to add, 0 to change, 2 to destroy (¡gana a terraform.tfvars? No: ver abajo)
unset TF_VAR_numero_subredes
Ese último plan te sorprenderá: no propone destruir nada, porque terraform.tfvars (con numero_subredes = 3) tiene más prioridad que la variable de entorno. Este es el orden completo, de menor a mayor prioridad:
Si quieres comprobarlo, comenta la línea numero_subredes de terraform.tfvars y repite el export: entonces sí verás 2 to destroy.
⚠️ Secretos. Nunca escribas contraseñas ni claves en un
.tf(como hacía el ejemplo original conadmin_password = "P@$$w0rd1234!"). Tampoco con-var, que queda en el historial del shell. UsaTF_VAR_leído de un gestor de secretos, o unsecretos.auto.tfvarsen.gitignore, y marca la variablesensitive = true. Para secretos reales, Azure Key Vault.
6. Validación básica¶
El bloque validation añade reglas que Terraform comprueba en el plan, antes de hablar con la API. Añade estas dos al variables.tf del ejemplo:
variable "nombre_proyecto" {
type = string
description = "Nombre corto del proyecto, en minúsculas y sin guiones"
default = "demo"
validation {
condition = can(regex("^[a-z0-9]{2,12}$", var.nombre_proyecto))
error_message = "Solo minúsculas y dígitos, de 2 a 12 caracteres: se usa en el nombre de la cuenta de almacenamiento."
}
}
variable "numero_subredes" {
type = number
description = "Cuántas subredes crear"
default = 2
validation {
condition = var.numero_subredes >= 1 && var.numero_subredes <= 8
error_message = "Entre 1 y 8 subredes."
}
}
│ Error: Invalid value for variable
│
│ on variables.tf line 1:
│ 1: variable "nombre_proyecto" {
│ │ var.nombre_proyecto is "Mi-Proyecto"
│
│ Solo minúsculas y dígitos, de 2 a 12 caracteres: se usa en el nombre de la cuenta de almacenamiento.
🔷 En Topaz. Las validaciones importan más que en Azure real: el emulador no siempre devuelve los mismos mensajes que Azure ante un nombre inválido, así que un error claro de Terraform ahorra tiempo de depuración. El patrón
contains([...], var.x)del original sigue siendo válido para listas cerradas de valores (regiones permitidas, SKUs, entornos). La siguiente página amplía la validación consensitive,nullabley tiposobject.
Limpieza¶
7. Errores comunes¶
⚠️ Solución de problemas
Mensaje Causa Solución Functions may not be called here / Variables not allowed Función o referencia en un defaultMover el cálculo a localsNo value for required variable Sin defaulty sin valor, con-input=falsePasar -var,-var-fileoTF_VAR_Invalid value for input variable ... a number is required numero_subredes = "dos"Respetar el type;"3"sí se convierte,"dos"noValue for undeclared variable (aviso) Errata en el nombre dentro del .tfvarsComparar con variables.tfMissing resource instance key Referenciar azurerm_storage_account.lab.nameen un recurso concountIndexar: azurerm_storage_account.lab[0].nameEl valor de TF_VAR_"no hace nada"terraform.tfvarstiene más prioridadRevisar el orden de precedencia de la sección 5 StorageAccountAlreadyTaken Otro alumno usa el mismo nombre_proyectoen TopazCambiarlo en tu terraform.tfvars
8. Autoevaluación¶
- ¿Qué tres argumentos deberías poner siempre al declarar una variable?
description,typey, si existe un valor razonable,default. Sindefaultla variable es obligatoria. - ¿Por qué
default = "rg-${timestamp()}"no es válido? Undefaultsolo admite literales. Los cálculos van enlocals. - ¿Qué diferencia hay entre
listylist(string)?lista secas es sintaxis heredada equivalente alist(any): no comprueba el tipo de los elementos. - Con
numero_subredes = 3enterraform.tfvarsyTF_VAR_numero_subredes=1, ¿cuántas subredes se crean? Tres. El archivoterraform.tfvarstiene más prioridad que la variable de entorno; solo*.auto.tfvars,-var-filey-varestán por encima. - ¿Cómo consigues que un recurso exista o no según una variable?
Con
count = var.crear_x ? 1 : 0, y referenciándolo después comorecurso.nombre[0]. - ¿Por qué el ejemplo original con una máquina virtual no funciona en Topaz? El emulador no incluye el proveedor Compute. Además, el código referenciaba una interfaz de red no declarada, usaba contraseña con la autenticación por contraseña desactivada por defecto y una imagen retirada.
- ¿Dónde debería vivir una contraseña que Terraform necesita?
Fuera del
.tf: enTF_VAR_o en un.auto.tfvarsignorado por Git, con la variable marcadasensitive = true; idealmente en Key Vault.