Translate

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.


No hay comentarios.:

Publicar un comentario