Hacer clic manualmente a través de los paneles de proveedores cloud para crear servidores, configurar registros DNS y establecer firewalls no escala. Cuando tu infraestructura crece más allá de unos pocos recursos, la gestión manual se vuelve propensa a errores, inconsistente e imposible de reproducir. La Infraestructura como Código (IaC) resuelve esto definiendo toda tu infraestructura en archivos de configuración controlados por versiones. Terraform, creado por HashiCorp, es la herramienta IaC más ampliamente adoptada, soportando cientos de proveedores cloud con un único flujo de trabajo.

¿Qué Es la Infraestructura como Código?

La Infraestructura como Código (IaC) es la práctica de gestionar infraestructura — servidores, redes, DNS, bases de datos, firewalls — usando archivos de configuración declarativos en lugar de procesos manuales. Describes el estado deseado de tu infraestructura y la herramienta determina cómo hacerlo realidad.

Beneficios clave:

  • Reproducibilidad — Despliega entornos idénticos para desarrollo, staging y producción
  • Control de versiones — Rastrea cada cambio de infraestructura en Git con historial completo
  • Colaboración — Revisa cambios de infraestructura a través de pull requests igual que el código de aplicación
  • Automatización — Integra con pipelines CI/CD para despliegues automatizados
  • Documentación — El código mismo documenta cómo se ve tu infraestructura

Terraform vs Otras Herramientas IaC

CaracterísticaTerraformAnsiblePulumiCloudFormation
EnfoqueDeclarativoImperativo/DeclarativoImperativoDeclarativo
LenguajeHCLYAMLPython, TypeScript, GoJSON/YAML
Multi-cloudSolo AWS
Gestión de estadoArchivo de estadoSin estadoArchivo de estadoGestionado por AWS
Mejor paraAprovisionamiento de infraestructuraGestión de configuraciónDesarrolladores que prefieren códigoEntornos solo AWS

Terraform destaca en el aprovisionamiento de infraestructura a través de múltiples proveedores cloud. Usa Ansible para la gestión de configuración (instalar software en servidores) y Terraform para crear los servidores en sí. Muchos equipos usan ambos juntos.

Instalar Terraform

macOS

brew tap hashicorp/tap
brew install hashicorp/tap/terraform

Ubuntu/Debian

wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform

Windows

choco install terraform

Verifica la instalación:

terraform version

Fundamentos de la Sintaxis HCL

Terraform usa HashiCorp Configuration Language (HCL), un lenguaje declarativo diseñado para ser legible por humanos. Estos son los constructos principales:

Bloques

Todo en Terraform se define en bloques:

# Bloque de recurso
resource "type" "name" {
  argument1 = "value1"
  argument2 = "value2"

  nested_block {
    key = "value"
  }
}

Tipos de Datos

# Cadena de texto
name = "web-server"

# Número
count = 3

# Booleano
enable_monitoring = true

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

# Mapa
tags = {
  Environment = "production"
  Team        = "platform"
}

Referencias e Interpolación

# Referenciar el atributo de otro recurso
subnet_id = aws_subnet.main.id

# Interpolación de cadenas
name = "app-${var.environment}-${count.index}"

# Expresión condicional
instance_type = var.environment == "production" ? "t3.large" : "t3.micro"

El Flujo de Trabajo Principal de Terraform

Cada proyecto de Terraform sigue cuatro comandos:

1. terraform init

Inicializa el directorio de trabajo, descarga los plugins de proveedores y configura el backend:

terraform init

Ejecuta esto cuando configures un proyecto por primera vez o añadas un nuevo proveedor.

2. terraform plan

Previsualiza los cambios que Terraform realizará sin aplicarlos realmente:

terraform plan

Esta es tu red de seguridad. Siempre revisa el plan antes de aplicar. La salida muestra recursos a crear (+), modificar (~) o destruir (-).

3. terraform apply

Ejecuta los cambios mostrados en el plan:

terraform apply

Terraform mostrará el plan nuevamente y pedirá confirmación. Usa -auto-approve solo en pipelines automatizados, nunca de forma interactiva.

4. terraform destroy

Elimina todos los recursos gestionados por esta configuración:

terraform destroy

Advertencia: terraform destroy es irreversible. Elimina infraestructura real. Siempre verifica que estés apuntando al workspace y entorno correctos antes de ejecutar este comando.

Proveedores

Los proveedores son plugins que interactúan con plataformas cloud, herramientas SaaS y otras APIs. Debes declarar qué proveedores usa tu configuración:

terraform {
  required_version = ">= 1.7.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
    cloudflare = {
      source  = "cloudflare/cloudflare"
      version = "~> 4.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

provider "cloudflare" {
  api_token = var.cloudflare_api_token
}

La restricción de versión ~> 5.0 significa “cualquier versión >= 5.0.0 y < 6.0.0”, permitiendo actualizaciones de parche y menores mientras previene cambios incompatibles.

Tu Primer Recurso: Registro DNS en Cloudflare

Vamos a crear un primer recurso práctico — un registro DNS en Cloudflare:

# main.tf
terraform {
  required_providers {
    cloudflare = {
      source  = "cloudflare/cloudflare"
      version = "~> 4.0"
    }
  }
}

provider "cloudflare" {
  api_token = var.cloudflare_api_token
}

resource "cloudflare_record" "www" {
  zone_id = var.cloudflare_zone_id
  name    = "www"
  content = "203.0.113.50"
  type    = "A"
  ttl     = 300
  proxied = true
}

resource "cloudflare_record" "mail" {
  zone_id  = var.cloudflare_zone_id
  name     = "@"
  content  = "mail.knowledgexchange.xyz"
  type     = "MX"
  priority = 10
  ttl      = 3600
}

Ejecuta el flujo de trabajo:

terraform init
terraform plan
terraform apply

Terraform crea ambos registros DNS y los rastrea en su archivo de estado.

Variables

Las variables hacen tus configuraciones reutilizables y específicas por entorno:

Definir Variables

# variables.tf
variable "cloudflare_api_token" {
  description = "Token API de Cloudflare con permisos de edición DNS"
  type        = string
  sensitive   = true
}

variable "cloudflare_zone_id" {
  description = "ID de zona de Cloudflare para el dominio"
  type        = string
}

variable "environment" {
  description = "Entorno de despliegue"
  type        = string
  default     = "staging"

  validation {
    condition     = contains(["development", "staging", "production"], var.environment)
    error_message = "Environment must be development, staging, or production."
  }
}

variable "server_count" {
  description = "Número de servidores a crear"
  type        = number
  default     = 2
}

variable "allowed_ips" {
  description = "Lista de direcciones IP con acceso permitido al servidor"
  type        = list(string)
  default     = []
}

variable "tags" {
  description = "Etiquetas de recursos"
  type        = map(string)
  default = {
    ManagedBy = "terraform"
  }
}

Establecer Valores de Variables

Crea un archivo terraform.tfvars (cargado automáticamente):

# terraform.tfvars
cloudflare_zone_id = "abc123def456"
environment        = "production"
server_count       = 3
allowed_ips        = ["203.0.113.50", "198.51.100.25"]
tags = {
  ManagedBy   = "terraform"
  Environment = "production"
  Team        = "platform"
}

Pasa valores sensibles a través de variables de entorno:

export TF_VAR_cloudflare_api_token="your-api-token-here"
terraform apply

Importante: Nunca subas archivos terraform.tfvars que contengan secretos al control de versiones. Usa variables de entorno o un gestor de secretos para valores sensibles.

Outputs

Los outputs exponen valores de tu configuración, haciéndolos disponibles para otras configuraciones o scripts:

# outputs.tf
output "dns_record_hostname" {
  description = "El FQDN del registro DNS creado"
  value       = cloudflare_record.www.hostname
}

output "server_ip" {
  description = "La dirección IP pública del servidor"
  value       = aws_instance.web.public_ip
}

output "database_connection_string" {
  description = "Cadena de conexión a la base de datos"
  value       = aws_db_instance.main.endpoint
  sensitive   = true
}

Visualiza los outputs después de aplicar:

terraform output
terraform output dns_record_hostname

Gestión del Estado

Terraform rastrea cada recurso que gestiona en un archivo de estado (terraform.tfstate). Este archivo mapea tu configuración a recursos del mundo real.

Por Qué el Estado Importa

  • Terraform usa el estado para determinar qué cambios necesitan hacerse
  • El estado contiene información sensible (IDs de recursos, direcciones IP, a veces contraseñas)
  • Sin estado, Terraform no puede gestionar recursos existentes

Backends Remotos

Nunca almacenes archivos de estado localmente para proyectos de equipo. Usa un backend remoto:

# backend.tf
terraform {
  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "production/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"
  }
}

El backend S3 almacena el estado en un bucket S3 con cifrado del lado del servidor. La tabla DynamoDB proporciona bloqueo de estado, previniendo que dos personas apliquen cambios simultáneamente.

Otras opciones de backend incluyen:

  • Azure Blob Storage — Para equipos centrados en Azure
  • Google Cloud Storage — Para usuarios de GCP
  • Terraform Cloud — El servicio gestionado de HashiCorp con un nivel gratuito
  • Consul — Para despliegues on-premises

Fuentes de Datos

Las fuentes de datos te permiten leer información de infraestructura existente que Terraform no gestiona:

# Buscar una VPC existente
data "aws_vpc" "existing" {
  filter {
    name   = "tag:Name"
    values = ["production-vpc"]
  }
}

# Buscar la AMI de Ubuntu más reciente
data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]  # Canonical

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-*-24.04-amd64-server-*"]
  }
}

# Usar la fuente de datos en un recurso
resource "aws_instance" "web" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t3.micro"
  subnet_id     = data.aws_vpc.existing.id

  tags = {
    Name = "web-server"
  }
}

Módulos

Los módulos son paquetes reutilizables de configuración Terraform. Encapsulan recursos relacionados en una única unidad:

# modules/web-server/main.tf
variable "instance_type" {
  type    = string
  default = "t3.micro"
}

variable "server_name" {
  type = string
}

resource "aws_instance" "server" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = var.instance_type

  tags = {
    Name = var.server_name
  }
}

output "public_ip" {
  value = aws_instance.server.public_ip
}

Usa el módulo:

# main.tf
module "web" {
  source        = "./modules/web-server"
  server_name   = "web-production"
  instance_type = "t3.large"
}

module "staging" {
  source        = "./modules/web-server"
  server_name   = "web-staging"
  instance_type = "t3.micro"
}

output "web_ip" {
  value = module.web.public_ip
}

El Terraform Registry aloja miles de módulos de la comunidad. Por ejemplo, el popular módulo terraform-aws-modules/vpc/aws crea una VPC completa con subredes, tablas de rutas y NAT gateways en pocas líneas.

Ejemplo Práctico: Stack Web Completo

Aquí tienes un ejemplo del mundo real que despliega registros DNS, un servidor y reglas de firewall:

# main.tf
terraform {
  required_version = ">= 1.7.0"

  required_providers {
    cloudflare = {
      source  = "cloudflare/cloudflare"
      version = "~> 4.0"
    }
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }

  backend "s3" {
    bucket         = "myapp-terraform-state"
    key            = "web-stack/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"
  }
}

provider "aws" {
  region = var.aws_region
}

provider "cloudflare" {
  api_token = var.cloudflare_api_token
}

# --- Grupo de Seguridad (Firewall) ---
resource "aws_security_group" "web" {
  name        = "${var.project}-web-sg"
  description = "Allow HTTP, HTTPS, and SSH"
  vpc_id      = var.vpc_id

  ingress {
    description = "HTTPS"
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    description = "HTTP"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    description = "SSH"
    from_port   = 2222
    to_port     = 2222
    protocol    = "tcp"
    cidr_blocks = var.ssh_allowed_ips
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = merge(var.tags, {
    Name = "${var.project}-web-sg"
  })
}

# --- Instancia EC2 ---
data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-*-24.04-amd64-server-*"]
  }
}

resource "aws_instance" "web" {
  ami                    = data.aws_ami.ubuntu.id
  instance_type          = var.instance_type
  key_name               = var.ssh_key_name
  vpc_security_group_ids = [aws_security_group.web.id]
  subnet_id              = var.subnet_id

  root_block_device {
    volume_size = 30
    volume_type = "gp3"
    encrypted   = true
  }

  user_data = <<-EOF
    #!/bin/bash
    apt-get update
    apt-get install -y nginx
    systemctl enable nginx
    systemctl start nginx
  EOF

  tags = merge(var.tags, {
    Name = "${var.project}-web-server"
  })
}

# --- Registros DNS ---
resource "cloudflare_record" "root" {
  zone_id = var.cloudflare_zone_id
  name    = "@"
  content = aws_instance.web.public_ip
  type    = "A"
  ttl     = 300
  proxied = true
}

resource "cloudflare_record" "www" {
  zone_id = var.cloudflare_zone_id
  name    = "www"
  content = var.domain
  type    = "CNAME"
  ttl     = 300
  proxied = true
}
# variables.tf
variable "aws_region" {
  type    = string
  default = "us-east-1"
}

variable "cloudflare_api_token" {
  type      = string
  sensitive = true
}

variable "cloudflare_zone_id" {
  type = string
}

variable "domain" {
  type = string
}

variable "project" {
  type    = string
  default = "myapp"
}

variable "instance_type" {
  type    = string
  default = "t3.micro"
}

variable "vpc_id" {
  type = string
}

variable "subnet_id" {
  type = string
}

variable "ssh_key_name" {
  type = string
}

variable "ssh_allowed_ips" {
  type    = list(string)
  default = []
}

variable "tags" {
  type = map(string)
  default = {
    ManagedBy = "terraform"
  }
}
# outputs.tf
output "server_public_ip" {
  description = "IP pública del servidor web"
  value       = aws_instance.web.public_ip
}

output "website_url" {
  description = "URL del sitio web"
  value       = "https://${var.domain}"
}

output "ssh_command" {
  description = "Comando SSH para conectarse al servidor"
  value       = "ssh -p 2222 ubuntu@${aws_instance.web.public_ip}"
}

Despliega el stack completo:

terraform init
terraform plan -out=plan.tfplan
terraform apply plan.tfplan

Mejores Prácticas

Fijación de Versiones

Siempre fija las versiones de proveedores y de Terraform para evitar cambios inesperados:

terraform {
  required_version = ">= 1.7.0, < 2.0.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.30"
    }
  }
}

.gitignore para Terraform

Añade esto a tu .gitignore:

# Terraform
*.tfstate
*.tfstate.*
*.tfvars
.terraform/
.terraform.lock.hcl
crash.log
override.tf
override.tf.json
*_override.tf
*_override.tf.json

Nota: Algunos equipos eligen subir .terraform.lock.hcl al repositorio para asegurar versiones consistentes de proveedores en todo el equipo. Este es un enfoque válido, especialmente para configuraciones de producción.

Workspaces

Usa workspaces para gestionar múltiples entornos con la misma configuración:

# Crear workspaces
terraform workspace new staging
terraform workspace new production

# Cambiar de workspace
terraform workspace select staging

# Listar workspaces
terraform workspace list

Referencia el workspace en tu configuración:

resource "aws_instance" "web" {
  instance_type = terraform.workspace == "production" ? "t3.large" : "t3.micro"

  tags = {
    Environment = terraform.workspace
  }
}

Estructura de Archivos

Organiza tu proyecto Terraform con una estructura clara:

project/
  main.tf          # Recursos principales
  variables.tf     # Declaraciones de variables
  outputs.tf       # Declaraciones de outputs
  providers.tf     # Configuración de proveedores
  backend.tf       # Configuración del backend
  terraform.tfvars # Valores de variables (no se sube al repositorio)
  modules/
    web-server/
      main.tf
      variables.tf
      outputs.tf

Errores Comunes a Evitar

  1. Almacenar el estado localmente — Siempre usa un backend remoto para proyectos de equipo. Los archivos de estado locales se desincronizan y causan conflictos.

  2. Codificar valores directamente — Usa variables para cualquier cosa que pueda cambiar entre entornos. Los valores codificados hacen las configuraciones frágiles.

  3. No usar terraform plan — Siempre revisa el plan antes de aplicar. Un recurso mal configurado puede eliminar datos de producción.

  4. Subir secretos al repositorio — Nunca subas tokens API, contraseñas o archivos terraform.tfvars con valores sensibles. Usa variables de entorno o un gestor de secretos.

  5. Ignorar el bloqueo de estado — Sin bloqueo de estado (DynamoDB para S3, o Terraform Cloud), dos personas pueden modificar la infraestructura simultáneamente, causando corrupción.

  6. Crear recursos manualmente — Una vez que empiezas a usar Terraform, crea todo a través de Terraform. Los cambios manuales crean divergencias entre tu código y la infraestructura real.

  7. Configuraciones monolíticas masivas — Divide configuraciones grandes en módulos y archivos de estado separados. Un único archivo de estado para toda tu infraestructura es frágil y lento.

Resumen

Terraform transforma la gestión de infraestructura de un proceso manual y propenso a errores en un flujo de trabajo repetible y controlado por versiones. Comienza con un recurso simple como un registro DNS, luego construye gradualmente hasta gestionar stacks de aplicaciones completos. Los principios clave son: define todo como código, siempre revisa los planes antes de aplicar, usa estado remoto con bloqueo y divide configuraciones grandes en módulos reutilizables.

Para más contenido sobre cloud y DevOps, explora nuestros artículos de Cloud y guías de DevOps. Si estás desplegando en plataformas cloud específicas, consulta nuestras guías sobre proveedores de hosting cloud.