Saltar al contenido principal
60 min de lectura1 de septiembre de 2026
TerraformIaCInfraestructuraHCLDevOps

Requisitos previos

  • docker
  • ci-cd

Lo que aprenderás

  • Entender IaC y por qué declarativo supera a imperativo
  • Escribir configuraciones Terraform con HCL: providers, recursos y data sources
  • Gestionar variables, outputs y secrets de forma segura
  • Crear módulos reutilizables para infraestructura repeatable
  • Configurar remote state y workspaces para colaboración en equipo
Tutorial60 min

Infraestructura como código con Terraform

Terraform permite definir, versionar y ejecutar infraestructura completa mediante archivos declarativos. Aprende a convertir servidores, redes y servicios en código que se puede revisar, probar y ejecutar de forma repetible.


¿Qué es IaC?

Infraestructura como código (IaC) es el práctica de gestionar y aprovisionar recursos de infraestructura mediante archivos de configuración en lugar de procesos manuales. En vez de hacer clic en una consola web o ejecutar comandos ssh uno por uno, describes el estado deseado en archivos que se pueden versionar, auditar y ejecutar automáticamente.

Declarativo vs imperativo

El enfoque declarativo (Terraform, CloudFormation) dice "quiero que exista X con estas propiedades" y el motor se encarga de calcular los pasos necesarios. El enfoque imperativo (scripts Bash, Pulumi en modo procedural) describe paso a paso cómo llegar al resultado. Terraform es declarativo por naturaleza: defines el estado final y él decide qué crear, modificar o eliminar.

Beneficios de IaC

Reproducibilidad — la misma configuración produce el mismo entorno en desarrollo, staging y producción. Versionado — cada cambio queda registrado en Git con un diff legible. Auditabilidad — puedes revisar quién cambió qué y cuándo. Colaboración — el equipo trabaja sobre los mismos archivos en vez de sobre conocimiento tácito.

IaC no reemplaza la experimentación — primero prueba en un entorno temporal, luego codifica lo que funciona. Terraform tiene plan justamente para eso: ver qué cambiaría antes de ejecutar.

Primeros pasos con Terraform

Instalación

En macOS instala con Homebrew. En Linux descarga el binario o usa el gestor de paquetes. En Windows usa winget o Chocolatey.

# macOS
brew install hashicorp/tap/terraform

# Linux (Debian/Ubuntu)
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

# Verificar instalación
terraform version

Sintaxis HCL

Terraform usa HCL (HashiCorp Configuration Language). Un archivo .tf contiene bloques con atributos, comentarios con # o //, y strings entre comillas.

# Esto es un comentario en línea
terraform {
  required_version = ">= 1.5"

  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "~> 3.0"
    }
  }
}

# Bloque provider: configura DÓNDE ejecutar
provider "docker" {
  host = "unix:///var/run/docker.sock"
}

# Bloque resource: define QUÉ crear
resource "docker_container" "web" {
  name  = "mi-nginx"
  image = docker_image.nginx.image_id

  ports {
    internal = 80
    external = 8080
  }
}

# Bloque data: consulta recursos existentes
data "docker_image" "nginx" {
  name = "nginx:alpine"
}

Providers

Los providers son plugins que conectan Terraform con APIs externas: AWS, Azure, GCP, Docker, Kubernetes, GitHub, etc. Cada provider expone recursos y data sources que puedes usar en tu configuración.

Init / Plan / Apply / Destroy

El ciclo de vida de Terraform tiene cuatro comandos fundamentales:

# 1. init: descarga providers y configura el backend
terraform init

# 2. plan: muestra qué cambiaría (dry run)
terraform plan

# 3. apply: ejecuta los cambios
terraform apply

# 4. destroy: elimina todo lo gestionado
terraform destroy

El state file

Terraform guarda el estado actual de la infraestructura en un archivo terraform.tfstate. Este archivo mapea los recursos definidos en código con los recursos reales en el proveedor.Nunca lo edites a mano — usa terraform state para manipularlo.

El state file contiene datos sensibles (contraseñas, tokens). No lo subas a Git. Usa un backend remoto como S3 con bloqueo DynamoDB para producción.

Ejemplo práctico: provisionar un contenedor Docker con Terraform:

# main.tf
terraform {
  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "~> 3.0"
    }
  }
}

provider "docker" {}

resource "docker_image" "nginx" {
  name = "nginx:alpine"
}

resource "docker_container" "web" {
  name  = "terraform-nginx"
  image = docker_image.nginx.image_id

  ports {
    internal = 80
    external = 8080
  }
}

# Ejecutar:
# terraform init
# terraform apply -auto-approve
# curl http://localhost:8080  -> funciona
# terraform destroy -auto-approve

Recursos, variables y outputs

Resources

Los resources son los bloques que definen componentes de infraestructura. Cada resource tiene un tipo (aws_instance, docker_container) y un nombre lógico. Terraform compara el estado actual con el deseado y ejecuta los cambios necesarios.

Data Sources

Los data sources permiten consultar información de recursos que ya existen sin gestionarlos. Útiles para obtener AMIs 最新, IPs de VPCs existentes o IDs de subnets.

data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]

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

Variables

Las variables parametrizan tu configuración. Terraform soporta strings, números, booleanos, listas y maps. Defínelas en variables.tf y asigna valores en terraform.tfvars.

# variables.tf
variable "container_name" {
  description = "Nombre del contenedor"
  type        = string
  default     = "mi-app"
}

variable "replicas" {
  description = "Número de réplicas"
  type        = number
  default     = 1
}

variable "enable_logging" {
  description = "Activar logging"
  type        = bool
  default     = true
}

variable "ports" {
  description = "Puertos a exponer"
  type        = list(number)
  default     = [80, 443]
}

variable "environment_vars" {
  description = "Variables de entorno"
  type        = map(string)
  default = {
    NODE_ENV = "production"
    LOG_LEVEL = "info"
  }
}

# terraform.tfvars
container_name  = "web-production"
replicas        = 3
enable_logging  = true
ports           = [80, 443, 8080]
environment_vars = {
  NODE_ENV   = "production"
  LOG_LEVEL  = "warn"
}

Outputs

Los outputs exponen información útil tras el apply: IPs públicas, IDs de recursos, URLs de endpoints. Marca valores sensibles con sensitive = true para que no se muestren en la CLI.

# outputs.tf
output "container_id" {
  description = "ID del contenedor creado"
  value       = docker_container.web.id
}

output "app_url" {
  description = "URL de la aplicación"
  value       = "http://localhost:8080"
}

output "db_password" {
  description = "Contraseña de la base de datos"
  value       = var.db_password
  sensitive   = true
}

Ejemplo completo: crear un security group parametrizado con variables:

# variables.tf
variable "allowed_ports" {
  type    = list(number)
  default = [22, 80, 443]
}

variable "allowed_cidrs" {
  type    = list(string)
  default = ["0.0.0.0/0"]
}

# main.tf
resource "aws_security_group" "web" {
  name        = "web-sg"
  description = "Security group para servidores web"

  dynamic "ingress" {
    for_each = var.allowed_ports
    content {
      from_port   = ingress.value
      to_port     = ingress.value
      protocol    = "tcp"
      cidr_blocks = var.allowed_cidrs
    }
  }

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

Módulos reutilizables

Un módulo es un paquete de configuración Terraform que agrupa recursos relacionados y expone inputs (variables) y outputs. Los módulos promueven la reutilización, el testing y la separación de responsabilidades.

Estructura de directorios

modules/
  docker-app/
    main.tf         # recursos principales
    variables.tf    # inputs del módulo
    outputs.tf      # outputs del módulo
    README.md       # documentación

Módulos del registry

Terraform tiene un registry público en registry.terraform.io con módulos oficiales para AWS, GCP, Azure y terceros. Usa módulos verificados para infraestructura de producción.

# Usar un módulo del registry
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.1.0"

  name = "mi-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["eu-west-1a", "eu-west-1b"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24"]
}

# Acceder a outputs del módulo
output "vpc_id" {
  value = module.vpc.vpc_id
}

Ejemplo: módulo para deploy de una app con Docker provider que acepta imagen, puerto y replicas:

# modules/docker-app/main.tf
variable "image" {
  type = string
}

variable "port" {
  type    = number
  default = 80
}

variable "replicas" {
  type    = number
  default = 1
}

resource "docker_image" "app" {
  name = var.image
}

resource "docker_container" "app" {
  count = var.replicas
  name  = "app-${count.index + 1}"
  image = docker_image.app.image_id

  ports {
    internal = var.port
    external = var.port + count.index
  }
}

# modules/docker-app/outputs.tf
output "container_ids" {
  value = docker_container.app[*].id
}

output "endpoints" {
  value = [for c in docker_container.app :
    "http://localhost:${c.ports[0].external}"
  ]
}

# --- Uso del módulo ---
module "web_app" {
  source   = "./modules/docker-app"
  image    = "nginx:alpine"
  port     = 80
  replicas = 3
}

output "urls" {
  value = module.web_app.endpoints
}
Los módulos locales se referencian con source = "./ruta". Los del registry usan source = "organizacion/modulo-provider". Siempre fija la versión con version = "~> X.Y" para evitar breaking changes.

Buenas prácticas y CI/CD para IaC

Remote state

En producción nunca uses el state file local. Configura un backend remoto como S3 con bloqueo DynamoDB para evitar que dos personas ejecuten apply simultáneamente.

# backend.tf
terraform {
  backend "s3" {
    bucket         = "mi-terraform-state"
    key            = "produccion/terraform.tfstate"
    region         = "eu-west-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

# Crear el bucket y tabla DynamoDB primero (one-time setup)
# aws s3api create-bucket --bucket mi-terraform-state --region eu-west-1
# aws dynamodb create-table --table-name terraform-locks \
#   --attribute-definitions AttributeName=LockID,AttributeType=S \
#   --key-schema AttributeName=LockID,KeyType=HASH \
#   --billing-mode PAY_PER_REQUEST

Workspaces

Los workspaces permiten tener múltiples estados en el mismo directorio. Usa uno por entorno: dev, staging, prod. Cada workspace tiene su propio terraform.tfstate.

terraform workspace new dev
terraform workspace new staging
terraform workspace new prod

terraform workspace list
terraform workspace select dev

# Ejecutar apply solo en el workspace activo
terraform apply

Drift detection

El drift ocurre cuando alguien modifica la infraestructura manualmente (fuera de Terraform). Detecta drift ejecutando terraform plan en un pipeline periódico: si hay cambios no esperados, falla la build y avisa.

tfsec para seguridad

tfsec (o trivy config) escanea tu código Terraform en busca de malas prácticas de seguridad: puertos abiertos, blobs sin cifrado, IAM overly permissive, etc. Ejecútalo como paso previo al plan.

# Instalar tfsec
brew install aquasecurity/tap/tfsec

# Ejecutar sobre el directorio actual
tfsec .

# Output en formato JSON para CI
tfsec . --format json --out tfsec-report.json

Pipeline CI/CD con GitHub Actions

El flujo estándar: PR ejecuta terraform plan y comenta el resultado. Tras el merge a main, ejecuta terraform apply con aprobación manual.

# .github/workflows/terraform.yml
name: Terraform CI/CD

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions:
  contents: read
  pull-requests: write
  id-token: write

jobs:
  terraform:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: "1.7.0"

      - name: Terraform fmt
        run: terraform fmt -check

      - name: Terraform Init
        run: terraform init
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

      - name: Terraform Validate
        run: terraform validate

      - name: Terraform Plan
        id: plan
        run: terraform plan -no-color
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

      - name: Comentar plan en PR
        if: github.event_name == 'pull_request'
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: \`### Terraform Plan
            \`\`\`
            \${{ steps.plan.outputs.stdout }}
            \`\`\`
            \`
            body: \`### Terraform Plan
            \`\`\`
            \${{ steps.plan.outputs.stdout }}
            \`\`\`
            \`

      - name: Terraform Apply
        if: github.ref == 'refs/heads/main' && github.event_name == 'push'
        run: terraform apply -auto-approve
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
Nunca hagas terraform apply sin terraform plan primero en producción. El plan es tu seguro: muestra exactamente qué se va a crear, modificar o destruir antes de tocar infraestructura real.
Usa aprobación manual en GitHub Actions para el apply en producción. Configura environment: production en el job para que requiera aprobación de un maintainer antes de ejecutar.

Con Terraform tienes el ciclo completo de infraestructura como código: definir declarativamente, planificar cambios, ejecutar de forma repetible y gestionar estado remoto. El siguiente paso natural es integrar módulos con CI/CD, agregar testing con terratest y explorar herramientas como OpenTofu como alternativa open-source.