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.tfvars

Tabla 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" { }         # Impreciso

Convenciones 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íptico

Uso 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"
}
# locals.tf
locals {
  common_tags = {
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}
# 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
}
# terraform.tfvars
region      = "us-east-1"
environment = "dev"
vpc_cidr    = "10.0.0.0/16"

Errores Comunes de Organización

  1. Poner outputs en main.tf: Los outputs deben ir siempre en outputs.tf para que sean fáciles de encontrar.

  2. Mezclar variables con recursos: Las variables van en variables.tf, nunca mezcladas con los recursos.

  3. No usar locals.tf: Repetir el mismo string o expresión en múltiples recursos es un error de mantenimiento.

  4. 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.

  5. Nombres de archivo sin convenio: Usar nombres como stuff.tf, misc.tf o temp.tf no aporta claridad.

  6. No documentar módulos: Cada módulo debería tener un README.md que 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.

© Copyright 2026. Todos los derechos reservados