Translate

Mostrando las entradas con la etiqueta Terraform. Mostrar todas las entradas
Mostrando las entradas con la etiqueta Terraform. Mostrar todas las entradas

miércoles, 7 de octubre de 2026

Terraform avanzado: count, for_each y expresiones


Hasta ahora vimos cómo describir infraestructura con Terraform.


Aprendimos a crear resources:


resource "local_file" "hello" {

  filename = "hello.txt"

  content  = "Hola!"

}


También vimos variables:


variable "environment" {

  type = string

}



Y módulos:


module "application" {

  source = "./modules/application"

}


Pero hay un problema.

En infraestructura real, muchas veces no queremos crear un único recurso.


Queremos crear:

  • 3 máquinas
  • 5 buckets
  • 10 reglas
  • 2 redes


Y probablemente tampoco queramos escribir diez veces el mismo bloque.

Ahí Terraform empieza a mostrar otra de sus características interesantes: podemos utilizar expresiones para construir infraestructura de forma dinámica.


Empecemos con algo sencillo. Supongamos que queremos crear tres archivos.

Sin count tendríamos que escribir:


resource "local_file" "file1" {

  filename = "file1.txt"

  content  = "Archivo 1"

}


resource "local_file" "file2" {

  filename = "file2.txt"

  content  = "Archivo 2"

}


resource "local_file" "file3" {

  filename = "file3.txt"

  content  = "Archivo 3"

}


Funciona.

Pero estamos repitiendo código.

Terraform nos permite utilizar count:


resource "local_file" "file" {

  count = 3


  filename = "file-${count.index}.txt"

  content  = "Archivo ${count.index}"

}


Ahora Terraform crea tres instancias del resource.


local_file.file[0]

local_file.file[1]

local_file.file[2]


El valor: count.index representa el índice de la instancia actual.


Por lo tanto:

count.index = 0


genera:

file-0.txt


Después:

count.index = 1


genera:

file-1.txt


Y finalmente:

count.index = 2


genera:

file-2.txt


Lo interesante es que count puede depender de una variable.


Por ejemplo:


variable "file_count" {

  type    = number

  default = 3

}


Nuestro resource:


resource "local_file" "file" {

  count = var.file_count


  filename = "file-${count.index}.txt"

  content  = "Archivo ${count.index}"

}


Ahora podemos cambiar:


file_count = 10


y Terraform creará diez recursos.


Tenemos entonces:

variable

    │

    ▼

file_count = 10

    │

    ▼

  count

    │

    ▼

10 resources


Esto ya resulta bastante útil para infraestructura real.


Pero count tiene una limitación


Supongamos que tenemos:

count = 3


Terraform identifica los recursos por posición:

resource[0]

resource[1]

resource[2]


Ahora imaginemos que queremos eliminar el segundo elemento.

Por ejemplo, teníamos:

[ "web", "api", "database" ]


y queremos:

[ "web", "database" ]


Si utilizamos count, las posiciones cambian:


Antes:

0 → web

1 → api

2 → database


Después:

0 → web

1 → database


Terraform puede interpretar que:

resource[1]


cambió de api a database.

Y eso puede provocar cambios que no esperábamos.

Cuando los elementos tienen una identidad propia, count muchas veces no es la mejor opción.

Ahí aparece: for_each


for_each permite crear múltiples instancias utilizando una colección.


Por ejemplo:


variable "files" {

  type = set(string)


  default = [

    "hello",

    "goodbye",

    "config"

  ]

}


Podemos hacer:


resource "local_file" "file" {

  for_each = var.files


  filename = "${each.key}.txt"

  content  = "Archivo ${each.key}"

}



Terraform creará:

hello.txt

goodbye.txt

config.txt


Pero ahora los recursos tienen una identidad basada en su clave:


local_file.file["hello"]

local_file.file["goodbye"]

local_file.file["config"]



Esto es diferente de:


local_file.file[0]

local_file.file[1]

local_file.file[2]


Podemos resumir la diferencia:

  • count : instancias identificadas por índice
  • for_each : instancias identificadas por clave

Por ejemplo:

count:

server[0]

server[1]

server[2]


Mientras que:

for_each:


server["web"]

server["api"]

server["database"]


Cuando cada elemento tiene una identidad propia, for_each suele expresar mejor nuestra intención.

for_each resulta especialmente interesante con mapas.


Por ejemplo:

variable "servers" {

  type = map(string)


  default = {

    web      = "small"

    api      = "medium"

    database = "large"

  }

}


Podemos escribir:


resource "local_file" "server" {

  for_each = var.servers


  filename = "${each.key}.txt"

  content  = "Server type: ${each.value}"

}


Ahora:

each.key


representa:

web

api

database


y:

each.value


representa:

small

medium

large


Por ejemplo, para web:

each.key   = "web"

each.value = "small"


Hasta ahora usamos for_each para crear recursos.

Pero Terraform también tiene expresiones for que permiten transformar colecciones.


Supongamos:


variable "names" {

  default = [

    "juan",

    "pedro",

    "ana"

  ]

}


Podemos crear una nueva lista:


locals {

  upper_names = [

    for name in var.names : upper(name)

  ]

}


El resultado será:

[

  "JUAN",

  "PEDRO",

  "ANA"

]


La sintaxis básica es:


[

  for elemento in coleccion : expresion

]


Es bastante parecida a un map en otros lenguajes o listas por comprensión.


Podemos pensarlo como:


colección

    │

    ▼

   for

    │

    ▼

transformación

    │

    ▼

nueva colección


Las expresiones for también pueden filtrar elementos.


Por ejemplo:


variable "numbers" {

  default = [1, 2, 3, 4, 5, 6]

}


Podemos quedarnos solamente con los números pares:


locals {

  even_numbers = [

    for number in var.numbers : number

    if number % 2 == 0

  ]

}


El resultado: [2, 4, 6]


La parte:

if number % 2 == 0

funciona como un filtro.


También podemos producir mapas.

Por ejemplo:


variable "names" {

  default = [

    "juan",

    "pedro",

    "ana"

  ]

}


Podemos construir:


locals {

  name_lengths = {

    for name in var.names :

    name => length(name)

  }

}


El resultado sería:

{

  juan  = 4

  pedro = 5

  ana   = 3

}



La expresión:

name => length(name)


define:

clave => valor


Esto es particularmente útil cuando necesitamos transformar datos antes de pasarlos a un resource o módulo.

Terraform también permite utilizar expresiones condicionales.

La sintaxis es:

condicion ? valor_si_true : valor_si_false


Por ejemplo:

variable "environment" {

  type = string

}


Podemos definir:

locals {

  instance_size = var.environment == "production"

    ? "large"

    : "small"

}


Esto permite adaptar nuestra infraestructura dependiendo del ambiente.


En los ejemplos anteriores apareció algo nuevo:


locals {

  instance_size = ...

}


Los locals permiten definir valores calculados que queremos reutilizar dentro de nuestra configuración.


Por ejemplo:


locals {

  environment = var.environment


  name_prefix = "myapp-${var.environment}"


  instance_size = var.environment == "production"

    ? "large"

    : "small"

}


Después podemos utilizarlos:


resource "..." "..." {

  name = local.name_prefix

}


La diferencia conceptual es:


variable

   ↓

valor que recibimos


local

   ↓

valor que calculamos


Las variables forman parte de la interfaz de nuestra configuración.

Los locals son principalmente una herramienta para organizar valores derivados.


Ahora podemos hacer algo un poco más interesante.

Supongamos que tenemos:


variable "environment" {

  type    = string

  default = "development"

}


variable "servers" {

  type = map(string)


  default = {

    web = "small"

    api = "small"

  }

}


Podemos definir:


locals {

  name_prefix = "myapp-${var.environment}"

}


Y utilizar for_each:


resource "local_file" "server" {

  for_each = var.servers


  filename = "${local.name_prefix}-${each.key}.txt"


  content = <<EOF

Environment: ${var.environment}

Server: ${each.key}

Size: ${each.value}

EOF

}


En development tendremos:

myapp-development-web.txt

myapp-development-api.txt


Y el contenido de cada archivo será diferente.


¿Cuándo utilizar count?

Una regla práctica puede ser:

Utilizá count cuando las instancias son esencialmente iguales y su identidad puede estar determinada por una posición o simplemente necesitás activar/desactivar un recurso.


Por ejemplo:

count = var.create_monitoring ? 1 : 0


Esto es muy común.


Si:

create_monitoring = true

tenemos:


monitoring[0]

Si es false:

ningún recurso


Este patrón es bastante útil para recursos opcionales.


¿Cuándo utilizar for_each?

Utilizá for_each cuando cada instancia tiene una identidad propia.


Por ejemplo:

  • web
  • api
  • database


es mucho más expresivo que:

  • 0
  • 1
  • 2


Con:

for_each = var.servers


podemos identificar los recursos mediante:

server["web"]

server["api"]

server["database"]


Esto además hace que los cambios en la colección sean más predecibles.


Cuando venimos de lenguajes como Java, C#, C++ o Python, es tentador pensar:

for_each es un for que ejecuta varias veces el resource.

No exactamente.

Terraform es declarativo.

No estamos escribiendo un programa que ejecuta:


for

    crear recurso

    crear recurso

    crear recurso


Estamos describiendo:"Quiero una instancia de este resource por cada elemento de esta colección."

Terraform después determina qué necesita hacer para conseguir ese resultado.

Esta diferencia de mentalidad se vuelve cada vez más importante a medida que las configuraciones crecen.


Entonces, tenemos varias herramientas para trabajar con colecciones:

count: Crea varias instancias indexadas:


resource "..." "server" {

  count = 3

}


Resultado:

server[0]

server[1]

server[2]


for_each: Crea instancias identificadas por claves:


resource "..." "server" {

  for_each = var.servers

}


Resultado:

server["web"]

server["api"]


for: Transforma colecciones:


[

  for item in var.items : transform(item)

]


condicional: Permite seleccionar valores:


condition ? value1 : value2


locals: Permite definir valores calculados reutilizables:


locals {

  name = "myapp-${var.environment}"

}


Hasta ahora utilizábamos Terraform principalmente para entender sus conceptos.

Con count, for_each, for, condicionales y locals empezamos a tener herramientas para construir configuraciones que se adapten a diferentes situaciones.



martes, 6 de octubre de 2026

Terraform: módulos para reutilizar infraestructura


En los artículos anteriores fuimos construyendo las piezas fundamentales de Terraform.


Ya vimos:

  • Resources.
  • Providers.
  • Variables.
  • Outputs.
  • State.


Hasta ahora nuestros ejemplos fueron pequeños.

Pero imaginemos un proyecto real.


Podríamos tener:

  • network
  • database
  • application
  • load balancer
  • monitoring
  • security


Y cada una de esas partes puede tener varios recursos.

Nuestro main.tf podría empezar a crecer. Y además podemos terminar repitiendo configuraciones.

Por ejemplo, quizás necesitamos crear la misma infraestructura para:

  • development
  • staging
  • production



¿Cómo evitamos copiar y pegar cientos de líneas?

La respuesta son los modules.


¿Qué es un módulo?

Un módulo de Terraform es, básicamente, un conjunto de archivos Terraform que encapsula una determinada funcionalidad o parte de nuestra infraestructura.

Podemos pensar en un módulo como una función.


Una función recibe parámetros:


entrada

   ↓

 función

   ↓

 resultado


Un módulo hace algo conceptualmente parecido:


variables

    ↓

  módulo

    ↓

resources

    ↓

outputs


Por ejemplo, podríamos crear un módulo llamado: database. Que se encargue de crear una base de datos completa.

Desde afuera solamente necesitamos decir:

  • Quiero una database
  • con este nombre
  • con este tamaño
  • en este ambiente


El módulo se ocupa del resto.


Vamos a crear una estructura sencilla:


terraform/

│

├── main.tf

│

└── modules/

    │

    └── file/

        │

        ├── main.tf

        ├── variables.tf

        └── outputs.tf


Nuestro módulo estará en:

modules/file


Y tendrá tres archivos.

En:

modules/file/main.tf


podemos tener:


resource "local_file" "file" {

  filename = var.filename

  content  = var.content

}


El módulo necesita dos valores:

  • filename
  • content


Por eso los definimos como variables.


En:

modules/file/variables.tf


tenemos:


variable "filename" {

  type = string

}


variable "content" {

  type = string

}


Ahora nuestro módulo tiene una interfaz. Desde afuera no necesitamos saber cómo está implementado.

Solamente necesitamos conocer sus entradas.

Podemos devolver información desde el módulo.


En:

modules/file/outputs.tf


escribimos:


output "filename" {

  value = local_file.file.filename

}


Ahora nuestro módulo tiene:


Entradas

   │

   ├── filename

   └── content

        │

        ▼

      módulo

        │

        ▼

     resource

        │

        ▼

     Outputs

        │

        └── filename


Ya tenemos algo parecido a una función.

Ahora podemos utilizar nuestro módulo desde el main.tf principal:


terraform {

  required_providers {

    local = {

      source = "hashicorp/local"

    }

  }

}


provider "local" {}


module "hello" {

  source = "./modules/file"

  filename = "hello.txt"

  content  = "Hola desde un módulo!"

}


La parte importante es:


module "hello" {

  source = "./modules/file"

  filename = "hello.txt"

  content  = "Hola desde un módulo!"

}


Estamos diciendo: Utilizá el módulo que está en `./modules/file`.

Y le pasamos sus parámetros.

Nuestro main.tf ya no necesita conocer los detalles internos del resource. Y esto nos permite separar responsabilidades. El módulo sabe cómo crear el recurso.

El código principal sabe qué recurso quiere utilizar.

Podemos utilizar el mismo módulo varias veces.


Por ejemplo:


module "hello" {

  source = "./modules/file"


  filename = "hello.txt"

  content  = "Hola!"

}


module "goodbye" {

  source = "./modules/file"


  filename = "goodbye.txt"

  content  = "Chau!"

}


Tenemos dos instancias del mismo módulo.

No copiamos la implementación. La reutilizamos.


Recordemos que nuestro módulo tiene:


output "filename" {

  value = local_file.file.filename

}


Desde el módulo principal podemos acceder al output:


output "hello_file" {

  value = module.hello.filename

}


La sintaxis:

module.hello.filename


significa:

module

  │

  └── instancia "hello"

           │

           └── output "filename"


Esto es muy parecido a acceder al resultado de una función.

Este concepto es muy importante.

Nuestro módulo tiene una especie de API.


Entradas:

  • filename
  • content


Salidas:

  • filename


Internamente podría tener:

  • 10 resources
  • 20 resources
  • 100 resources


Pero quien lo utiliza no necesita conocer todos esos detalles. Esto permite encapsular complejidad.

Y en infraestructura real esto es extremadamente útil.


Supongamos que nuestra aplicación necesita una máquina virtual.

Podríamos tener un módulo:


modules/

└── application/

    ├── main.tf

    ├── variables.tf

    └── outputs.tf


Internamente podría crear:

  • Virtual Machine
  • Network Interface
  • Security Group
  • Disk


Desde nuestro proyecto principal podríamos utilizarlo así:


module "application" {

  source = "./modules/application"


  environment = "production"

  instance_type = "small"

}


El módulo podría encargarse de todos los detalles.

El código principal queda mucho más limpio:


main.tf


  application

      │

      ├── network

      ├── machine

      ├── security

      └── disk


En lugar de tener cientos de recursos directamente en el main.tf.


Ahora podemos combinar lo que vimos en artículos anteriores.


Supongamos que queremos:

  • development
  • staging
  • production


Podemos tener diferentes configuraciones que utilizan los mismos módulos.


Por ejemplo:


terraform/

│

├── modules/

│   └── application/

│

├── environments/

│   ├── development/

│   │   └── main.tf

│   │

│   ├── staging/

│   │   └── main.tf

│   │

│   └── production/

│       └── main.tf


En development:


module "application" {

  source = "../../modules/application"


  environment   = "development"

  instance_type = "small"

}


En producción:


module "application" {

  source = "../../modules/application"


  environment   = "production"

  instance_type = "large"

}


El módulo es el mismo.

Lo que cambia es la configuración que le pasamos.

Esto nos permite evitar una práctica peligrosa:


development/

    copia de todo Terraform


production/

    otra copia de todo Terraform


Si copiamos infraestructura, tarde o temprano las copias empiezan a divergir.

Con módulos podemos reutilizar la implementación.

Hasta ahora utilizamos:


source = "./modules/application"


Es decir, un módulo local.

Pero Terraform también permite utilizar módulos almacenados en otros lugares.


Por ejemplo, podemos obtener módulos desde:

  • Git;
  • registros de módulos;
  • repositorios privados;
  • otros sistemas compatibles con Terraform.


Un ejemplo podría ser:

module "application" {

  source = "git::https://example.com/infrastructure.git"

}


La idea es que un módulo puede convertirse en una pieza reutilizable entre diferentes proyectos.


Los módulos también pueden tener versiones. Cuando una infraestructura se vuelve importante, queremos evitar que una actualización de un módulo rompa todos nuestros ambientes.

Por eso es habitual trabajar con versiones.

Un proyecto puede utilizar una versión determinada.

Esto es especialmente importante cuando los módulos son compartidos por diferentes equipos.


Este es un error bastante común cuando empezamos a utilizar módulos.

Podemos terminar haciendo:

module "resource1"

module "resource2"

module "resource3"

module "resource4"


para absolutamente todo.

Y terminamos con una infraestructura más complicada que la original.

Un módulo tiene sentido cuando encapsula una unidad reutilizable o conceptualmente coherente.


Por ejemplo:

  • network
  • database
  • application
  • monitoring


pueden ser buenos candidatos.

Crear un módulo solamente porque tenemos un resource:

module "s3_bucket"

no necesariamente aporta valor.


Depende del proyecto y de cuánto comportamiento adicional encapsule.


Una forma interesante de pensar Terraform es como si estuviéramos construyendo una aplicación.

En una aplicación tenemos:

  • clases
  • funciones
  • componentes
  • librerías


En Terraform podemos tener:

  • resources
  • modules
  • providers


Un módulo puede encapsular una parte de nuestra infraestructura de la misma manera que una librería encapsula funcionalidad de una aplicación.


Por ejemplo:


Aplicación

│

├── UserService

├── PaymentService

└── NotificationService


Terraform:


Infraestructura

│

├── Network

├── Database

└── Application


La comparación no es perfecta, pero ayuda a entender la idea.


Un módulo no es solamente una forma de ordenar archivos. Este punto es importante.

Podríamos pensar:"Entonces los módulos sirven solamente para tener el código más ordenado."

No.

Su verdadero valor aparece cuando conseguimos:

  • reutilizar infraestructura;
  • encapsular detalles;
  • definir interfaces mediante variables;
  • devolver resultados mediante outputs;
  • versionar componentes;
  • compartir componentes entre proyectos.


Por eso un módulo puede convertirse en una verdadera unidad de diseño de infraestructura.


Terraform State: qué es y por qué es tan importante


En los artículos anteriores vimos cómo Terraform permite definir infraestructura como código.

También vimos que, después de ejecutar Terraform, aparece un archivo llamado:


terraform.tfstate



¿Qué es exactamente?

¿Por qué Terraform necesita guardar información sobre los recursos que creó?

Y quizás la pregunta más importante: ¿Por qué no alcanza con mirar nuestra configuración .tf?

La respuesta está en uno de los conceptos fundamentales de Terraform: el State.


Supongamos que tenemos:


resource "local_file" "hello" {

  filename = "hello.txt"

  content  = "Hola!"

}


Ejecutamos:

terraform apply


Terraform crea:

hello.txt


Hasta acá todo parece sencillo.


Pero imaginemos que mañana ejecutamos nuevamente:

terraform apply


Terraform tiene que decidir: "¿Tengo que crear el archivo otra vez?"

La respuesta es no.

Terraform necesita saber que ese recurso ya existe y está siendo administrado.

Ahí aparece el State.

El State es información que Terraform mantiene sobre la infraestructura que administra.

Por defecto se almacena en:

terraform.tfstate


Podemos pensar que Terraform tiene tres fuentes de información:

  • La configuración describe lo que queremos.
  • La infraestructura representa lo que existe realmente.
  • El State ayuda a Terraform a saber qué recursos está administrando y cómo se relacionan con la configuración.

Podríamos pensar: "Entonces terraform.tfstate es simplemente otra versión de main.tf."

No.


Son cosas diferentes.

Nuestro código podría decir:


resource "local_file" "hello" {

  filename = "hello.txt"

  content  = "Hola!"

}


Mientras que el State contiene información sobre el recurso que Terraform está administrando.

Por ejemplo, puede almacenar:

  • el tipo de recurso;
  • su identificador;
  • atributos conocidos;
  • información del provider;
  • relaciones entre recursos;
  • valores que Terraform necesita para gestionar ese recurso.


El State funciona como una especie de mapa entre nuestra configuración y la infraestructura real.

Podemos crear un ejemplo pequeño:


terraform {

  required_providers {

    local = {

      source = "hashicorp/local"

    }

  }

}


provider "local" {}


resource "local_file" "hello" {

  filename = "hello.txt"

  content  = "Hola Terraform!"

}


Inicializamos:

terraform init


Y aplicamos:

terraform apply


Ahora tendremos:

hello.txt

terraform.tfstate


Podemos consultar el State mediante:

terraform show


Terraform nos muestra información sobre los recursos que administra.

También podemos listar los recursos que forman parte del State:

terraform state list


Obtendremos algo parecido a:

local_file.hello


Esto nos dice que Terraform está administrando ese recurso.

¿Qué pasa si ejecutamos apply otra vez?

Terraform no debería intentar crear otro archivo.

En lugar de eso veremos algo similar a:

No changes. Your infrastructure matches the configuration.


¿Por qué?

Porque Terraform sabe que:

  1. nuestra configuración declara local_file.hello;
  2. ese recurso ya está registrado en el State;
  3. el recurso existe;
  4. no hay cambios que aplicar.


Por eso Terraform es idempotente en este sentido:


apply

  ↓

crear recurso


apply nuevamente

  ↓

no hay cambios


¿Y si modificamos la configuración?

Supongamos que cambiamos:


content = "Hola Terraform!"


por:


content = "Hola mundo!"



Ahora ejecutamos:

terraform plan


Terraform detecta que nuestra configuración cambió.

Podríamos ver algo parecido a:

~ local_file.hello

  content: "Hola Terraform!" -> "Hola mundo!"


El símbolo: ~ indica que Terraform planea modificar un recurso existente.


Después:

terraform apply


Terraform actualiza el recurso.

¿De dónde sabe Terraform qué cambió?

Esta es justamente una de las razones por las que existe el State.

Terraform puede comparar diferentes piezas de información:


               configuración

                         │

                        ▼

                  Terraform

                         │

        ┌─────┴─────┐

        ▼                             ▼

      State                    infraestructura


A partir de esa información Terraform puede determinar qué acciones necesita realizar.


Por ejemplo:

crear

modificar

destruir

no hacer nada


Esto se conoce como planificación de cambios.

Ahora viene una situación muy interesante.

Supongamos que Terraform creó una infraestructura y todo está funcionando correctamente.

Pero alguien entra directamente a la consola del proveedor cloud y modifica algo.


Por ejemplo:

Terraform:

instance_type = "small"


Pero alguien entra manualmente y cambia la máquina a:

instance_type = "large"


Ahora tenemos una diferencia: La infraestructura real ya no coincide con lo que Terraform espera.

A esto normalmente se lo llama drift.


Es decir: La infraestructura se alejó del estado que Terraform espera administrar.


¿Qué puede hacer Terraform?

Cuando ejecutamos:

terraform plan


Terraform consulta información del provider y puede detectar diferencias entre lo que conoce y lo que existe.


Por ejemplo, podría mostrarnos que una configuración cambió.

Entonces Terraform puede proponer una corrección.


La idea es volver a:

Configuración

      │

      ▼

   Terraform

      │

      ▼

Infraestructura deseada


En otras palabras: Terraform puede utilizarse para mantener la infraestructura alineada con una configuración declarativa.

¿Terraform detecta absolutamente todo?

No. Esto es importante.

Terraform depende de lo que el provider pueda consultar y representar.

Además, no todos los cambios externos necesariamente tienen el mismo comportamiento.

Por eso no deberíamos pensar: "Terraform mágicamente sabe todo lo que ocurrió."

Terraform trabaja con la información que obtiene de los providers y con el State que mantiene.


¿Qué ocurre si eliminamos terraform.tfstate? Esta es una prueba interesante.

Supongamos que tenemos:

terraform.tfstate

hello.txt


Si eliminamos:

terraform.tfstate


pero dejamos:

hello.txt


la infraestructura sigue existiendo.

Pero Terraform perdió su información sobre ese recurso.


Si volvemos a ejecutar:

terraform plan


Terraform puede considerar que el recurso declarado en la configuración no está registrado en el State.

Es decir, tenemos:


Configuración

      │

      ▼

local_file.hello


   State

      │

      ▼

     vacío


Infraestructura

      │

      ▼

  hello.txt


Esto muestra algo importante: El State no es la infraestructura.

Si borramos el State, no necesariamente borramos la infraestructura.

Hemos perdido la información que Terraform utilizaba para administrarla.


Entonces, ¿podemos borrar el State?

No deberíamos hacerlo arbitrariamente.

En un proyecto real, perder el State puede ser un problema serio.


Por eso es habitual:

  • almacenarlo de forma segura;
  • realizar backups;
  • restringir el acceso;
  • utilizar un backend apropiado;
  • evitar modificarlo manualmente.


Y esto nos lleva a otro problema.

¿Qué pasa cuando trabajamos en equipo?

Supongamos que tenemos:


Emanuel

   │

   └── terraform apply


Juan

   │

   └── terraform apply


Si ambos tienen una copia local de:

terraform.tfstate

tenemos un problema.


Cada uno podría tener un State diferente.


Por ejemplo:

Emanuel

terraform.tfstate

      │

      └── versión A


Juan

terraform.tfstate

      │

      └── versión B


¿Cuál es el correcto?

Por eso, en equipos, normalmente no queremos guardar el State únicamente en el disco local de cada desarrollador.


Necesitamos un backend remoto.

Terraform permite almacenar el State en diferentes backends.


Por ejemplo:


Terraform

    │

    ▼

Backend remoto

    │

    ├── State

    └── Locking


Algunos proveedores cloud ofrecen almacenamiento adecuado para esto.

Por ejemplo, un equipo que trabaja con AWS puede utilizar un backend basado en S3.

La configuración puede tener una forma similar a:


terraform {

  backend "s3" {

    bucket = "terraform-state"

    key    = "production/terraform.tfstate"

    region = "us-east-1"

  }

}


La configuración exacta depende de la versión de Terraform y del backend que utilicemos, pero la idea fundamental es: El State deja de estar únicamente en la máquina de un desarrollador y pasa a estar almacenado en un lugar compartido.


¿Y si dos personas ejecutan Terraform al mismo tiempo?

Este problema es todavía más interesante.

Imaginemos:


Emanuel

   │

   ├── terraform apply

   │

   ▼

   State


Juan

   │

   ├── terraform apply

   │

   ▼

   State


Si ambos intentan modificar la infraestructura simultáneamente, podemos terminar con operaciones que interfieren entre sí.


Por eso aparece otro concepto importante: State locking

Algunos backends permiten bloquear el State mientras una operación está en curso.

Conceptualmente:


Emanuel

   │

   └── apply

         │

         ▼

       LOCK 🔒

         │

         ▼

       State

         

Juan

   │

   └── apply

         │

         ▼

    espera/rechazo


De esta manera evitamos que dos operaciones modifiquen simultáneamente el mismo State.


¿Podemos guardar terraform.tfstate en Git?


En general, no es una buena práctica para proyectos reales.

Hay varias razones.

La primera es que el State puede contener información sensible.


Por ejemplo:

  • identificadores;
  • atributos de infraestructura;
  • información de configuración;
  • y, dependiendo del provider y de los recursos, valores que no queremos distribuir libremente.


Además, Git no es un backend diseñado para coordinar el acceso concurrente al State.

Por eso normalmente tenemos:


Código Terraform

      │

      ▼

     Git


pero:


Terraform State

      │

      ▼

Backend remoto


Son responsabilidades diferentes.

Después de conocer el State, podemos entender mejor qué ocurre cuando ejecutamos:

terraform plan


No es simplemente: "Leé los `.tf` y ejecutá los comandos."

Terraform tiene que construir un plan considerando:


                         Configuración

                                  │

                                 ▼

                           Terraform

                                 │

        ┌────────┴────────┐

        ▼                                              ▼

      State                                  infraestructura

        │                                                │

        └────────┬────────┘

                                 ▼

                               Plan

                                 │

                                ▼

                     cambios necesarios


Por ejemplo:

+ crear

~ modificar

- destruir


Y recién después:

terraform apply


se aplican esos cambios.


Podemos resumir todo esto con una frase: La configuración dice lo que queremos, la infraestructura dice lo que existe y el State permite a Terraform relacionar ambas cosas.

Por eso el State es tan importante.

No es un simple archivo temporal.

Es una parte fundamental del funcionamiento de Terraform.

sábado, 3 de octubre de 2026

Terraform desde cero: variables, outputs y recursos


En el artículo anterior vimos qué problema intenta resolver Terraform y qué significa Infrastructure as Code.

La idea fundamental era: En lugar de configurar nuestra infraestructura manualmente, podemos describirla mediante código.

Ahora vamos a dar el siguiente paso.

En este artículo vamos a conocer tres conceptos que aparecen prácticamente en cualquier proyecto de Terraform:

  • Resources
  • Variables
  • Outputs

Y vamos a terminar viendo dónde aparece un concepto fundamental: el state.


Resources: los recursos que queremos crear

El concepto más importante de Terraform probablemente sea el de resource.

Un resource representa algo que queremos administrar.


Por ejemplo:

  • una máquina virtual;
  • una base de datos;
  • un bucket;
  • una red;
  • una regla de firewall;
  • un DNS;
  • un archivo local.


En el artículo anterior utilizamos:


resource "local_file" "hello" {

  filename = "hello.txt"

  content  = "Hola desde Terraform!"

}


La estructura general es:


resource "TIPO" "NOMBRE" {

  ...

}


Por ejemplo:


resource "local_file" "hello" {

  filename = "hello.txt"

  content  = "Hola desde Terraform!"

}


Tenemos dos nombres importantes:


local_file

    │

    └── tipo de recurso


hello

    │

    └── nombre del recurso


Por lo tanto, dentro de Terraform podemos referirnos a este recurso como:

local_file.hello


Es importante entender que local_file no es algo que Terraform conozca mágicamente.

Lo proporciona un provider.


Providers

Terraform utiliza providers para comunicarse con diferentes plataformas y servicios.

Por ejemplo, existen providers para trabajar con:

  • AWS
  • Azure
  • Google Cloud
  • Kubernetes
  • GitHub
  • Docker
  • bases de datos
  • recursos locales


Por ejemplo, para nuestro ejemplo local:


terraform {

  required_providers {

    local = {

      source = "hashicorp/local"

    }

  }

}


provider "local" {}


Y entonces podemos utilizar:


resource "local_file" "hello" {

  filename = "hello.txt"

  content  = "Hola desde Terraform!"

}


Podemos pensar en el provider como el componente que sabe cómo comunicarse con el sistema que estamos administrando.


Terraform sabe interpretar nuestra configuración.

El provider sabe cómo convertir esa configuración en operaciones sobre un servicio concreto.

Supongamos ahora que queremos crear dos archivos:


resource "local_file" "hello" {

  filename = "hello.txt"

  content  = "Hola desde Terraform!"

}


resource "local_file" "goodbye" {

  filename = "goodbye.txt"

  content  = "Chau desde Terraform!"

}


Funciona perfectamente.

Pero imaginemos que el nombre de nuestro ambiente aparece en varios lugares:


resource "local_file" "config" {
  filename = "production-config.txt"
  content  = "Configuración de production"
}


¿Qué pasa si mañana queremos utilizar exactamente la misma configuración para development?

Podríamos copiar el archivo y modificar los valores.

Pero eso rápidamente empieza a convertirse en un problema.

Queremos parametrizar nuestra infraestructura.

Ahí aparecen las variables.


Variables

Una variable permite definir un valor que puede cambiar sin modificar directamente nuestro resource.

Por ejemplo:

variable "environment" {
  type    = string
  default = "development"
}

Ahora podemos utilizarla:

resource "local_file" "config" {
  filename = "${var.environment}-config.txt"

  content = "Configuración de ${var.environment}"
}


Terraform reemplazará:

${var.environment}

por el valor correspondiente.


Con el valor por defecto:

development

terminaremos teniendo:

development-config.txt

y su contenido será:

Configuración de development

No siempre queremos definir un valor por defecto.

Por ejemplo:

variable "environment" {
  type = string
}

Ahora Terraform necesita que le proporcionemos un valor.

Podemos hacerlo mediante un archivo:

terraform.tfvars

con:

environment = "production"

Y nuestro resource puede seguir siendo exactamente el mismo:

resource "local_file" "config" {
  filename = "${var.environment}-config.txt"

  content = "Configuración de ${var.environment}"
}

Esto es interesante porque podemos utilizar la misma infraestructura para diferentes ambientes.


Por ejemplo:

development
staging
production

cambiando solamente los valores de entrada.

¿Por qué no poner todo directamente en el resource?

Podríamos escribir:

resource "local_file" "config" {
  filename = "production-config.txt"
  content  = "Configuración de production"
}

Pero entonces nuestro código está acoplado a un ambiente concreto.

Con variables podemos separar:

Infraestructura
      +
Configuración


Por ejemplo:

main.tf
    ↓
define cómo funciona la infraestructura

terraform.tfvars
    ↓
define los valores para este ambiente


Esta separación se vuelve especialmente importante cuando trabajamos con infraestructura real.

Las variables pueden tener diferentes tipos.

Por ejemplo:

variable "environment" {
  type = string
}

Una cadena.

También podemos tener números:

variable "instance_count" {
  type = number
}

Booleanos:

variable "enabled" {
  type = bool
}

Y estructuras más interesantes.

Por ejemplo, una lista:

variable "availability_zones" {
  type = list(string)
}

Podemos asignarle:

availability_zones = [
  "us-east-1a",
  "us-east-1b"
]

Más adelante, cuando empecemos a trabajar con configuraciones más complejas, estos tipos van a ser muy importantes.


Hasta ahora vimos cómo introducir información en Terraform mediante variables.

Pero muchas veces queremos hacer lo contrario.

Queremos que Terraform nos muestre información sobre lo que acaba de crear.

Para eso tenemos los outputs.

Por ejemplo:

output "environment" {
  value = var.environment
}

Después de ejecutar:

terraform apply

Terraform puede mostrar:

Outputs:

environment = "development"


Los outputs sirven para exponer información que resulta útil después de crear nuestros recursos.

Los outputs se vuelven mucho más interesantes cuando apuntan a recursos.


Por ejemplo:

resource "local_file" "config" {
  filename = "${var.environment}-config.txt"

  content = "Configuración de ${var.environment}"
}


Podemos definir:

output "config_file" {
  value = local_file.config.filename
}


Ahora Terraform conoce el nombre del archivo creado y podemos mostrarlo como output.


La referencia:

local_file.config.filename


significa:

resource
   │
   └── local_file.config
            │
            └── atributo filename


Esta forma de referenciar recursos es fundamental en Terraform.

Terraform entiende las dependencias

Supongamos que tenemos:

resource "local_file" "config" {
  filename = "${var.environment}-config.txt"
  content  = "Configuración de ${var.environment}"
}

y:

output "config_file" {
  value = local_file.config.filename
}


El output depende del resource.

Terraform puede detectar esa relación.

Podemos imaginarlo así:

local_file.config
        │
        ▼
output.config_file


En configuraciones más grandes tendremos muchos recursos relacionados:


network
   │
   ├── subnet
   │      │
   │      └── virtual machine
   │
   └── database


Terraform construye y utiliza estas dependencias para determinar el orden en el que debe realizar las operaciones.

Veamos un ejemplo completo

Nuestro main.tf puede quedar así:

terraform {
  required_providers {
    local = {
      source = "hashicorp/local"
    }
  }
}

provider "local" {}

variable "environment" {
  type    = string
  default = "development"
}

resource "local_file" "config" {
  filename = "${var.environment}-config.txt"

  content = <<EOF
Environment: ${var.environment}
Application: MyApplication
EOF
}

output "config_file" {
  value = local_file.config.filename
}


Ahora ejecutamos:

terraform init


Después:

terraform plan


Y finalmente:

terraform apply


Terraform crea:

development-config.txt

con:

Environment: development
Application: MyApplication

Y además podemos obtener:

Outputs:

config_file = "development-config.txt"

Tenemos entonces las tres piezas:

                                       Terraform
                                             │
        ┌────────────┼────────────┐
        │                                  │                                  │
        ▼                                ▼                                 ▼
     Variable                     Resource                       Output
        │                                  │                                  │
        │                                  │                                  │
        └────────────┼────────────┘
                                             │
                                            ▼
                                     infraestructura


¿Dónde entra el State?

Hay un concepto que todavía no explicamos en profundidad, pero que ya aparece cuando ejecutamos Terraform:

terraform apply

Terraform necesita saber qué recursos administra y cuál es el estado conocido de esos recursos.

Para eso utiliza el state.

Normalmente, después de ejecutar nuestro ejemplo veremos un archivo:

terraform.tfstate

Este archivo contiene información que Terraform utiliza para relacionar nuestra configuración con los recursos que administra.

Por ahora no necesitamos entrar en todos sus detalles.

Lo importante es entender que Terraform trabaja con tres cosas:

  Configuración
       │
       │
       ▼
   Terraform
       ▲
       │
       │
     State
       │
      ▼
Infraestructura real


La configuración dice lo que queremos.

El state ayuda a Terraform a saber qué está administrando.

Y la infraestructura real es lo que existe efectivamente.

Este concepto merece otro post... 

viernes, 2 de octubre de 2026

Terraform: ¿qué problema resuelve? Una introducción a Infrastructure as Code


Cuando hablamos de Terraform, muchas veces la primera reacción es: ¿Otra herramienta más para configurar servidores?

No exactamente.

Terraform intenta resolver un problema bastante más interesante: ¿cómo podemos definir, versionar y reproducir nuestra infraestructura de la misma manera que hacemos con nuestro código?


Imaginemos que tenemos que desplegar una aplicación.

Necesitamos:

  • una máquina virtual;
  • una base de datos;
  • una red;
  • algunos permisos;
  • un almacenamiento;
  • quizás un balanceador.


Una forma de hacerlo es entrar a la consola de nuestro proveedor cloud y empezar a crear recursos.

  1. Hacemos clic.
  2. Configuramos una opción.
  3. Creamos una red.
  4. Después una máquina virtual.
  5. Después cambiamos una configuración.
  6. Y finalmente tenemos nuestra infraestructura funcionando.


El problema aparece unos meses después.

Queremos crear exactamente el mismo ambiente para testing.

¿Recordamos todos los pasos?

¿Todas las opciones?

¿Todas las reglas de firewall?

¿Todas las dependencias?

Probablemente no.


Y aunque recordemos todo, aparece otro problema: ¿cómo sabemos exactamente qué cambió entre producción y testing?


Ahí aparece una idea importante: Infrastructure as Code

Infrastructure as Code, o IaC, consiste en tratar la infraestructura como código.


En lugar de decir: Entrá a esta consola y creá una máquina con estas opciones, podemos escribir algo que describa nuestra infraestructura:


resource "..." "..." {

    ...

}


Ese archivo puede:

  • guardarse en Git;
  • revisarse mediante Pull Requests;
  • versionarse;
  • reutilizarse;
  • automatizarse;
  • compartirlo con otros desarrolladores.


Nuestra infraestructura deja de estar solamente en una consola y pasa a estar también descrita en código.

HashiCorp Terraform es una herramienta de Infrastructure as Code desarrollada originalmente por HashiCorp.


La idea fundamental es bastante sencilla: Describimos el estado que queremos y Terraform se encarga de realizar los cambios necesarios para llegar a ese estado.

Esto es importante porque Terraform utiliza un enfoque declarativo.

No necesitamos describir paso por paso cómo crear la infraestructura.

Describimos qué queremos tener.


Por ejemplo:

resource "local_file" "hello" {

  filename = "hello.txt"

  content  = "Hola desde Terraform!"

}


Estamos diciendo: Quiero un recurso local_file llamado hello cuyo archivo sea hello.txt y cuyo contenido sea Hola desde Terraform!.

Terraform se ocupa del resto.


Para empezar no necesitamos AWS, Azure ni Google Cloud.

Podemos utilizar un provider que trabaja con archivos locales.

Nuestro primer ejemplo completo puede ser:


terraform {

  required_providers {

    local = {

      source = "hashicorp/local"

    }

  }

}


provider "local" {}


resource "local_file" "hello" {

  filename = "hello.txt"

  content  = "Hola desde Terraform!"

}


Guardamos el archivo como:

main.tf


Y ejecutamos:

terraform init


Terraform descarga y configura el provider que necesitamos.

Después podemos preguntarle qué va a hacer:


terraform plan


Terraform nos muestra un plan de ejecución.


Finalmente:

terraform apply


Terraform aplica los cambios.


Y ahora tenemos:

hello.txt


con el contenido:


Hola desde Terraform!


¿Por qué plan es interesante?

Este es uno de los conceptos más importantes de Terraform.

Antes de modificar nuestra infraestructura podemos ejecutar:


terraform plan


Terraform analiza la configuración y el estado actual y nos muestra qué cambios pretende realizar.


Por ejemplo:

Plan: 1 to add, 0 to change, 0 to destroy.


Esto nos permite revisar los cambios antes de aplicarlos.

En una infraestructura real esto es extremadamente importante.


Imaginemos que alguien modifica un archivo Terraform y, sin revisar nada, ejecuta directamente la aplicación de los cambios.

Podría terminar eliminando un recurso que no debía eliminarse.

Con plan, podemos ver qué piensa hacer Terraform antes de hacerlo.


Terraform no es un script. Esta diferencia merece una explicación.


miércoles, 9 de noviembre de 2022

Free eBook: Declarative Cloud Infrastructure Management with Terraform (New Edition)

 

Download Now

 

Free eBook: Declarative Cloud Infrastructure Management with Terraform (New Edition)


Understand the underlying concepts of Terraform, declarative cloud infrastructure management, and key basics of the Hashicorp Configuration Language.

Also covered: why and when you should use Linode Kubernetes Engine (LKE) and Terraform together, Packer explained, and links to more educational content.

Get the PDF instantly (2.6 MB) — no registration required
Download Now
Your Suggestions
Any ideas or suggestions? Shoot us an email at newsletter@javacodegeeks.com