Introducción
Una de las habilidades más importantes al trabajar con Terraform en entornos profesionales no es simplemente escribir código que funcione, sino escribir código que sea mantenible, escalable y comprensible para cualquier miembro del equipo. La organización del código determina qué tan fácil será añadir nuevas funcionalidades, corregir errores y colaborar con otros ingenieros.
En este módulo aprenderás las convenciones y estructuras recomendadas para proyectos de cualquier tamaño, desde un entorno de pruebas personal hasta infraestructuras de producción complejas con múltiples equipos.
Por qué la Organización del Código Importa
Cuando alguien comienza con Terraform, es común poner todo el código en un único archivo main.tf. Esto funciona para proyectos muy pequeños, pero rápidamente se convierte en un problema:
- Dificultad para encontrar recursos: Un archivo de 2.000 líneas es difícil de navegar
- Conflictos en el control de versiones: Varios colaboradores modificando el mismo archivo simultáneamente
- Falta de claridad: No queda claro qué hace cada parte del código
- Reutilización imposible: El código no está estructurado para ser compartido o reutilizado
- Revisiones de código largas: Los pull requests se vuelven enormes y difíciles de revisar
Una buena organización del código resuelve todos estos problemas y hace que el equipo sea más productivo.
Estructura de Archivos para Proyectos Pequeños
Para un proyecto pequeño (por ejemplo, un entorno de desarrollo con pocos recursos), la estructura más recomendada es:
proyecto-terraform/ ├── main.tf ├── variables.tf ├── outputs.tf ├── versions.tf └── terraform.tfvars
Descripción de cada archivo
main.tf — Contiene los recursos principales del proyecto. Es el punto de entrada de la configuración.
variables.tf — Define todas las variables de entrada del módulo o proyecto. Incluye descripción, tipo y valor por defecto de cada variable.
outputs.tf — Declara los valores que se exportan del módulo o que se muestran al finalizar terraform apply.
versions.tf — Especifica la versión requerida de Terraform y de los providers utilizados.
terraform.tfvars — Asigna valores concretos a las variables. Este archivo suele ser específico del entorno (no se sube al repositorio en proyectos con datos sensibles).
Ejemplo de estructura mínima
# versions.tf
# Este archivo ancla las versiones de Terraform y de los providers.
# Anclar versiones evita que actualizaciones inesperadas rompan la infraestructura.
terraform {
required_version = ">= 1.6.0" # Versión mínima de Terraform requerida
required_providers {
aws = {
source = "hashicorp/aws" # Origen oficial del provider de AWS
version = "~> 5.0" # Permite versiones 5.x pero no 6.x
}
}
}# variables.tf
# Centralizar las variables aquí permite ver de un vistazo todos los parámetros configurables.
variable "region" {
description = "Región de AWS donde se crearán los recursos"
type = string
default = "us-east-1" # Valor por defecto: se usa si no se especifica otro
}
variable "environment" {
description = "Nombre del entorno (dev, staging, prod)"
type = string
# Sin default: obliga al usuario a especificarlo explícitamente
}
variable "instance_type" {
description = "Tipo de instancia EC2"
type = string
default = "t3.micro"
}# main.tf
# El bloque provider configura la conexión con AWS usando la variable de región.
provider "aws" {
region = var.region # Usa la variable definida en variables.tf
}
# Recurso: instancia EC2
resource "aws_instance" "web_server" {
ami = "ami-0c55b159cbfafe1f0" # AMI de Amazon Linux 2
instance_type = var.instance_type # Tipo tomado de la variable
tags = {
Name = "web-server-${var.environment}" # Nombre dinámico según entorno
Environment = var.environment
ManagedBy = "terraform" # Buena práctica: indicar que Terraform gestiona este recurso
}
}# outputs.tf
# Los outputs permiten compartir información entre módulos o mostrarla al operador.
output "instance_id" {
description = "ID de la instancia EC2 creada"
value = aws_instance.web_server.id # Referencia al atributo del recurso
}
output "instance_public_ip" {
description = "Dirección IP pública de la instancia"
value = aws_instance.web_server.public_ip
}Estructura para Proyectos Medianos y Grandes
A medida que el proyecto crece, es necesario dividir el código en más archivos especializados:
proyecto-terraform/ ├── main.tf # Recursos principales y llamadas a módulos ├── variables.tf # Definición de todas las variables ├── outputs.tf # Valores exportados ├── locals.tf # Valores calculados internamente ├── versions.tf # Versiones de Terraform y providers ├── data.tf # Fuentes de datos (data sources) ├── networking.tf # Recursos de red (VPC, subnets, etc.) ├── compute.tf # Recursos de cómputo (EC2, ECS, etc.) ├── storage.tf # Recursos de almacenamiento (S3, EBS, etc.) ├── security.tf # Grupos de seguridad, IAM roles, políticas ├── terraform.tfvars # Valores de variables para este entorno └── README.md # Documentación del módulo
Separación por Ambiente vs Separación por Capa
Existen dos filosofías principales para organizar múltiples entornos:
Separación por Ambiente (directory-per-environment)
Cada entorno tiene su propio directorio con su propia configuración de estado:
infraestructura/
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── terraform.tfvars
│ ├── staging/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── terraform.tfvars
│ └── prod/
│ ├── main.tf
│ ├── variables.tf
│ └── terraform.tfvars
└── modules/
├── networking/
└── compute/Separación por Capa (layer-based)
Cada capa de la infraestructura está en su propio directorio:
infraestructura/
├── 01-networking/
│ ├── main.tf
│ └── terraform.tfvars
├── 02-security/
│ ├── main.tf
│ └── terraform.tfvars
├── 03-compute/
│ ├── main.tf
│ └── terraform.tfvars
└── 04-applications/
├── main.tf
└── terraform.tfvarsTabla Comparativa: Estructuras de Proyecto
| Estructura | Ventajas | Desventajas | Mejor para |
|---|---|---|---|
| Archivo único (monolito) | Simple al inicio, fácil de empezar | Difícil de mantener, conflictos en Git | Pruebas personales, demos |
| Por tipo de archivo (main, variables, outputs) | Organización clara, estándar de la industria | Puede volverse grande en proyectos grandes | Proyectos pequeños/medianos |
| Por tipo de recurso (networking.tf, compute.tf) | Fácil de encontrar recursos específicos | Puede haber acoplamiento entre archivos | Proyectos medianos |
| Por entorno (environments/dev, prod) | Aislamiento completo entre entornos | Duplicación de código si no se usan módulos | Proyectos medianos/grandes |
| Por capa (networking, compute, apps) | Despliegue incremental, menor radio de impacto | Más complejo de gestionar, dependencias entre capas | Proyectos grandes y equipos múltiples |
| Módulos + entornos | Alta reutilización, máxima flexibilidad | Mayor complejidad inicial | Proyectos enterprise |
El Archivo locals.tf y el Bloque locals {}
El bloque locals permite definir valores calculados que se reutilizan en múltiples lugares del código. A diferencia de las variables, los locals no pueden ser sobreescritos desde fuera del módulo.
# locals.tf
# Los locals son valores calculados a partir de otras variables o expresiones.
# Su ventaja es que se calculan una sola vez y se reutilizan sin repetición.
locals {
# Prefijo estándar para nombrar todos los recursos del proyecto
# En lugar de repetir "${var.project}-${var.environment}" en cada recurso,
# se define una sola vez aquí.
name_prefix = "${var.project}-${var.environment}"
# Etiquetas comunes que se aplican a TODOS los recursos
# Esto garantiza consistencia y facilita la búsqueda por etiquetas en la consola
common_tags = {
Project = var.project
Environment = var.environment
ManagedBy = "terraform"
Owner = var.team_name
CreatedAt = timestamp() # Fecha de creación (solo al primer apply)
}
# Calcular el CIDR de subnets a partir del CIDR principal
# cidrsubnet() divide una red en subredes más pequeñas
public_subnet_cidr = cidrsubnet(var.vpc_cidr, 8, 1) # Ej: 10.0.1.0/24
private_subnet_cidr = cidrsubnet(var.vpc_cidr, 8, 2) # Ej: 10.0.2.0/24
}Uso del local en recursos:
# networking.tf
# Nota cómo se usa local.name_prefix y local.common_tags
# para evitar repetición y garantizar consistencia.
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
tags = merge(
local.common_tags, # Aplicar etiquetas comunes
{
Name = "${local.name_prefix}-vpc" # Nombre específico de este recurso
}
)
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = local.public_subnet_cidr # Usar el CIDR calculado en locals
tags = merge(
local.common_tags,
{
Name = "${local.name_prefix}-public-subnet"
Type = "public"
}
)
}El Archivo data.tf
Los data sources permiten consultar información de recursos existentes (no gestionados por el estado actual). Centralizarlos en data.tf facilita saber qué información se importa externamente.
# data.tf
# Los data sources obtienen información de recursos ya existentes
# o de APIs del provider, sin crear ni modificar nada.
# Obtener la AMI más reciente de Amazon Linux 2
data "aws_ami" "amazon_linux_2" {
most_recent = true # Obtener la versión más reciente
owners = ["amazon"] # Solo AMIs oficiales de Amazon
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"] # Patrón del nombre de la AMI
}
filter {
name = "virtualization-type"
values = ["hvm"] # Tipo de virtualización hardware
}
}
# Obtener información sobre la cuenta de AWS actual
data "aws_caller_identity" "current" {}
# Uso: data.aws_caller_identity.current.account_id
# Obtener las zonas de disponibilidad de la región actual
data "aws_availability_zones" "available" {
state = "available" # Solo zonas activas
}
# Uso: data.aws_availability_zones.available.names[0]Estructura de Repositorio para Múltiples Entornos
Un repositorio real de empresa suele tener esta estructura:
infraestructura-empresa/ │ ├── modules/ # Módulos reutilizables (internos) │ ├── networking/ │ │ ├── main.tf │ │ ├── variables.tf │ │ ├── outputs.tf │ │ └── README.md │ ├── compute/ │ │ ├── main.tf │ │ ├── variables.tf │ │ ├── outputs.tf │ │ └── README.md │ └── database/ │ ├── main.tf │ ├── variables.tf │ └── outputs.tf │ ├── environments/ │ ├── dev/ │ │ ├── main.tf # Llama a los módulos con valores de dev │ │ ├── variables.tf │ │ ├── locals.tf │ │ ├── outputs.tf │ │ ├── versions.tf │ │ ├── backend.tf # Configuración del estado remoto para dev │ │ └── terraform.tfvars # Valores específicos de dev │ │ │ ├── staging/ │ │ ├── main.tf │ │ ├── variables.tf │ │ ├── locals.tf │ │ ├── outputs.tf │ │ ├── versions.tf │ │ ├── backend.tf │ │ └── terraform.tfvars │ │ │ └── prod/ │ ├── main.tf │ ├── variables.tf │ ├── locals.tf │ ├── outputs.tf │ ├── versions.tf │ ├── backend.tf │ └── terraform.tfvars │ ├── .gitignore ├── .terraform.lock.hcl └── README.md
Convenciones de Nomenclatura de Recursos
La consistencia en los nombres de recursos es fundamental para la mantenibilidad:
# CONVENCION RECOMENDADA: snake_case para nombres de recursos en Terraform
# Formato: <tipo_recurso>_<descripción>_<sufijo_opcional>
# BUENA nomenclatura:
resource "aws_security_group" "web_server" { } # Descriptivo y claro
resource "aws_s3_bucket" "logs_storage" { } # Indica función
resource "aws_iam_role" "lambda_execution" { } # Indica qué usa este rol
# MALA nomenclatura (evitar):
resource "aws_security_group" "sg1" { } # Sin significado
resource "aws_s3_bucket" "bucket" { } # Demasiado genérico
resource "aws_iam_role" "role_for_stuff" { } # ImprecisoConvenciones para nombres de variables
# variables.tf
# Convenciones de nomenclatura para variables:
# Usar snake_case (palabras separadas por guiones bajos)
variable "vpc_cidr_block" { } # BIEN: snake_case descriptivo
variable "max_instance_count" { } # BIEN: indica el propósito
# Evitar:
variable "VpcCidrBlock" { } # MAL: PascalCase no es convención de Terraform
variable "vpc-cidr-block" { } # MAL: los guiones causan problemas con algunas referencias
variable "x" { } # MAL: completamente crípticoUso de Locals para Evitar Repetición (DRY)
El principio DRY (Don't Repeat Yourself) es especialmente importante en infraestructura:
# MAL EJEMPLO: repetición de valores en múltiples recursos
resource "aws_instance" "app_server" {
tags = {
Project = "mi-proyecto" # Repetido
Environment = "produccion" # Repetido
ManagedBy = "terraform" # Repetido
}
}
resource "aws_s3_bucket" "data_bucket" {
tags = {
Project = "mi-proyecto" # Repetido otra vez
Environment = "produccion" # Si cambia el entorno, hay que cambiar en todos lados
ManagedBy = "terraform" # Propenso a errores por inconsistencia
}
}
# BUEN EJEMPLO: usando locals para centralizar valores comunes
locals {
common_tags = {
Project = var.project_name
Environment = var.environment
ManagedBy = "terraform"
}
}
resource "aws_instance" "app_server" {
tags = local.common_tags # Una sola línea, consistente siempre
}
resource "aws_s3_bucket" "data_bucket" {
tags = local.common_tags # Cambiar en un lugar afecta a todos los recursos
}
# Si necesitas tags adicionales para un recurso específico, usa merge():
resource "aws_rds_instance" "database" {
tags = merge(
local.common_tags, # Tags comunes de todos los recursos
{
BackupEnabled = "true" # Tags adicionales específicos de este recurso
DataClass = "confidential"
}
)
}Ejercicio: Reorganizar una Configuración Monolítica
Situación inicial
El siguiente código está en un único archivo main.tf y necesita ser reorganizado:
# main.tf (CÓDIGO MONOLÍTICO - a reorganizar)
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
variable "region" {
default = "us-east-1"
}
variable "environment" {
description = "Entorno de despliegue"
}
variable "vpc_cidr" {
default = "10.0.0.0/16"
}
provider "aws" {
region = var.region
}
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
tags = { Name = "vpc-${var.environment}", Environment = var.environment, ManagedBy = "terraform" }
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
tags = { Name = "subnet-public-${var.environment}", Environment = var.environment, ManagedBy = "terraform" }
}
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
subnet_id = aws_subnet.public.id
tags = { Name = "web-${var.environment}", Environment = var.environment, ManagedBy = "terraform" }
}
output "vpc_id" {
value = aws_vpc.main.id
}
output "instance_ip" {
value = aws_instance.web.public_ip
}Tu tarea: Reorganiza este código en la estructura de archivos correcta.
Solución del Ejercicio
Estructura resultante:
proyecto-reorganizado/ ├── versions.tf ├── variables.tf ├── locals.tf ├── data.tf ├── networking.tf ├── compute.tf ├── outputs.tf └── terraform.tfvars
# versions.tf
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.region
}# variables.tf
variable "region" {
description = "Región de AWS"
type = string
default = "us-east-1"
}
variable "environment" {
description = "Entorno de despliegue (dev, staging, prod)"
type = string
}
variable "vpc_cidr" {
description = "Bloque CIDR para la VPC"
type = string
default = "10.0.0.0/16"
}# networking.tf
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
tags = merge(local.common_tags, { Name = "vpc-${var.environment}" })
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
tags = merge(local.common_tags, { Name = "subnet-public-${var.environment}" })
}# compute.tf
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
subnet_id = aws_subnet.public.id
tags = merge(local.common_tags, { Name = "web-${var.environment}" })
}# outputs.tf
output "vpc_id" {
description = "ID de la VPC creada"
value = aws_vpc.main.id
}
output "instance_ip" {
description = "IP pública de la instancia web"
value = aws_instance.web.public_ip
}Errores Comunes de Organización
-
Poner outputs en
main.tf: Los outputs deben ir siempre enoutputs.tfpara que sean fáciles de encontrar. -
Mezclar variables con recursos: Las variables van en
variables.tf, nunca mezcladas con los recursos. -
No usar
locals.tf: Repetir el mismo string o expresión en múltiples recursos es un error de mantenimiento. -
Archivos demasiado granulares: Crear un archivo por recurso (
aws_vpc.tf,aws_subnet.tf) produce demasiados archivos pequeños. La granularidad ideal es por tipo o capa funcional. -
Nombres de archivo sin convenio: Usar nombres como
stuff.tf,misc.tfotemp.tfno aporta claridad. -
No documentar módulos: Cada módulo debería tener un
README.mdque explique qué hace, qué variables acepta y qué outputs produce.
Resumen
En este módulo has aprendido:
- Por qué la organización del código es crítica para proyectos de infraestructura mantenibles
- La estructura de archivos estándar:
main.tf,variables.tf,outputs.tf,locals.tf,versions.tf,data.tf - Cómo separar recursos por tipo funcional (
networking.tf,compute.tf, etc.) - Las diferencias entre separación por entorno y separación por capa
- Cómo usar
locals {}para aplicar el principio DRY - Convenciones de nomenclatura para recursos y variables
- Cómo reorganizar código monolítico en una estructura mantenible
En el siguiente módulo aprenderás a gestionar estos archivos con control de versiones (Git), incluyendo qué archivos deben estar en el repositorio y cómo trabajar con ramas para gestionar cambios de infraestructura de forma segura.
Curso de Terraform
Módulo 1: Introducción a Terraform
- ¿Qué es Terraform?
- Instalando Terraform
- Conceptos Básicos de Terraform
- Primera Configuración de Terraform
Módulo 2: Lenguaje de Configuración de Terraform
Módulo 3: Gestión del Estado
Módulo 4: Módulos de Terraform
Módulo 5: Aprovisionamiento de Recursos
- Conceptos Básicos de Aprovisionamiento
- Aprovisionamiento de Recursos AWS
- Aprovisionamiento de Recursos Azure
- Aprovisionamiento de Recursos GCP
Módulo 6: Funcionalidades Avanzadas de Terraform
Módulo 7: Mejores Prácticas de Terraform
- Organización del Código
- Control de Versiones
- Pruebas del Código de Terraform
- Mejores Prácticas de Seguridad
Módulo 8: Terraform en CI/CD
- Integración de Terraform con CI/CD
- Automatización de Terraform con Jenkins
- Uso de Terraform con GitHub Actions
- Terraform Cloud y Enterprise
