Terraform IaC
GuΓa de Infrastructure as Code con Terraform. Cubre desde los conceptos fundamentales hasta patrones avanzados de mΓ³dulos, workspaces, estado remoto y pipelines de CI/CD para infraestructura.
Fundamentos
Terraform es una herramienta de Infrastructure as Code declarativa que permite definir, planificar y aplicar cambios de infraestructura de forma reproducible y versionable.
Flujo de trabajo Terraform
Developer Terraform Cloud Provider
β β β
β terraform init β β
ββββββββββββββββββββββββββββΆβ Descarga providers β
β β Inicializa backend β
β β β
β terraform plan β β
ββββββββββββββββββββββββββββΆβ Compara estado vs config β
βββββββββββββββββββββββββββββ€ Muestra cambios β
β "Plan: 3 to add, β β
β 1 to change, 0 destroy" β β
β β β
β terraform apply β β
ββββββββββββββββββββββββββββΆβ Ejecuta cambios β
β βββββββββββββββββββββββββββββββΆβ
β β API calls β
β ββββββββββββββββββββββββββββββββ€
βββββββββββββββββββββββββββββ€ Actualiza state β
β "Apply complete! β β
β 3 added, 1 changed" β β
Archivos clave:
main.tf -> Recursos principales
variables.tf -> Variables de input
outputs.tf -> Valores de salida
providers.tf -> Configuracion de providers
terraform.tfvars -> Valores de variables (no commitear secrets) # providers.tf β Configuracion del provider
terraform {
required_version = ">= 1.7"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
# Estado remoto en S3
backend "s3" {
bucket = "my-terraform-state"
key = "production/terraform.tfstate"
region = "eu-west-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Environment = var.environment
ManagedBy = "terraform"
Project = var.project_name
}
}
} State remoto obligatorio
Nunca uses state local en equipos. El state contiene informacion sensible y debe estar en un backend compartido (S3, GCS, Terraform Cloud) con locking para evitar race conditions.
Resources & Data Sources
Los resources crean infraestructura real. Los data sources leen infraestructura existente sin modificarla.
# main.tf β Infraestructura de una aplicacion web
# -- VPC --
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
tags = { Name = "${var.project_name}-vpc" }
}
# -- Subnets (publico + privado) --
resource "aws_subnet" "public" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${count.index}.0/24"
availability_zone = data.aws_availability_zones.available.names[count.index]
map_public_ip_on_launch = true
tags = { Name = "public-${count.index}" }
}
# -- Security Group --
resource "aws_security_group" "web" {
name = "web-sg"
vpc_id = aws_vpc.main.id
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
# -- Data source: leer zonas disponibles --
data "aws_availability_zones" "available" {
state = "available"
}
# -- Data source: AMI mas reciente --
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-24.04-amd64-*"]
}
} Modulos
Los modulos son paquetes reutilizables de configuracion Terraform. Encapsulan recursos relacionados con una interfaz de variables/outputs limpia.
# modules/ecs-service/main.tf β Modulo reutilizable
variable "service_name" { type = string }
variable "image" { type = string }
variable "cpu" { type = number; default = 256 }
variable "memory" { type = number; default = 512 }
variable "desired_count" { type = number; default = 2 }
variable "port" { type = number; default = 8080 }
resource "aws_ecs_task_definition" "this" {
family = var.service_name
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = var.cpu
memory = var.memory
container_definitions = jsonencode([{
name = var.service_name
image = var.image
essential = true
portMappings = [{ containerPort = var.port }]
}])
}
output "task_definition_arn" {
value = aws_ecs_task_definition.this.arn
}
# -- Uso del modulo --
# root/main.tf
module "backend" {
source = "./modules/ecs-service"
service_name = "backend-api"
image = "ghcr.io/org/backend:latest"
cpu = 512
memory = 1024
desired_count = 3
}
module "worker" {
source = "./modules/ecs-service"
service_name = "worker"
image = "ghcr.io/org/worker:latest"
cpu = 1024
memory = 2048
} Terraform Registry
Antes de crear un modulo propio, busca en el Terraform Registry (registry.terraform.io). Hay modulos verificados para VPC, EKS, RDS, y casi cualquier servicio de AWS/GCP/Azure.
Workspaces & Environments
Los workspaces permiten gestionar multiples entornos (dev, staging, prod) con el mismo codigo Terraform usando variables diferentes por entorno.
# variables.tf
variable "environment" {
type = string
validation {
condition = contains(["dev", "staging", "production"], var.environment)
error_message = "Environment must be dev, staging, or production."
}
}
# Configuracion por entorno
locals {
env_config = {
dev = {
instance_type = "t3.small"
min_size = 1
max_size = 2
multi_az = false
}
staging = {
instance_type = "t3.medium"
min_size = 2
max_size = 4
multi_az = true
}
production = {
instance_type = "m5.large"
min_size = 3
max_size = 10
multi_az = true
}
}
config = local.env_config[var.environment]
} # Gestion de workspaces
terraform workspace new dev
terraform workspace new staging
terraform workspace new production
# Seleccionar workspace
terraform workspace select staging
# Aplicar con variables de entorno
terraform apply -var-file="envs/staging.tfvars" CI/CD para IaC
La infraestructura debe pasar por el mismo rigor de CI/CD que el codigo: linting, plan en PR, apply automatico en merge.
# .github/workflows/terraform.yml
name: Terraform CI/CD
on:
pull_request:
paths: ['infra/**']
push:
branches: [main]
paths: ['infra/**']
jobs:
plan:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.7.0"
- name: Terraform Init
run: terraform init -backend=false
working-directory: infra/
- name: Terraform Format Check
run: terraform fmt -check -recursive
- name: Terraform Validate
run: terraform validate
- name: Terraform Plan
run: terraform plan -var-file=envs/staging.tfvars -no-color
working-directory: infra/
apply:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment: production # Requiere aprobacion manual
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- run: terraform init
- run: terraform apply -auto-approve -var-file=envs/production.tfvars Nunca auto-approve en produccion sin proteccion
Configura GitHub Environment protection rules para que el apply en produccion requiera aprobacion manual de al menos un reviewer antes de ejecutarse.
State Management
El state de Terraform es el archivo que mapea la configuracion HCL a los recursos reales en el proveedor cloud. Gestionarlo correctamente es critico: un state corrupto o desincronizado puede causar la destruccion accidental de infraestructura.
Backends remotos
Comparacion de backends para state remoto
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Remote State Backends β
β β
β βββββββββββββββ βββββββββββββββ ββββββββββββββββββββββββ β
β β AWS S3 β β GCP GCS β β Terraform Cloud β β
β β + DynamoDB β β + GCS Lockβ β (HCP) β β
β β β β β β β β
β β Locking: β β β Locking: β β β Locking: β β β
β β Encrypt: β β β Encrypt: β β β Encrypt: β β β
β β Cost: bajo β β Cost: bajo β β Cost: free/paid β β
β β Setup: DIY β β Setup: DIY β β Setup: managed β β
β βββββββββββββββ βββββββββββββββ ββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | Backend | Locking | Cifrado | Costo | Mejor para |
|---|---|---|---|---|
| S3 + DynamoDB | DynamoDB table | SSE-S3 / SSE-KMS | ~$1/mes | Equipos en AWS |
| GCS | Nativo en GCS | Google-managed / CMEK | ~$1/mes | Equipos en GCP |
| Azure Blob | Blob lease | SSE con Azure Key Vault | ~$1/mes | Equipos en Azure |
| Terraform Cloud | Nativo | Gestionado por HashiCorp | Gratis hasta 500 resources | Equipos pequenos, gestion simplificada |
| Consul | Nativo | Configurable | Self-hosted | On-premise con HashiCorp stack |
Configuracion de S3 backend con locking
# backend.tf β Estado remoto en S3 con DynamoDB locking
terraform {
backend "s3" {
bucket = "mycompany-terraform-state"
key = "services/backend/terraform.tfstate"
region = "eu-west-1"
encrypt = true
dynamodb_table = "terraform-locks"
# Opcional: cifrado con KMS
kms_key_id = "arn:aws:kms:eu-west-1:123456789:key/abcd-1234"
}
}
# -- Crear el bucket y tabla con un proyecto bootstrap --
# bootstrap/main.tf (se aplica una sola vez con state local)
resource "aws_s3_bucket" "terraform_state" {
bucket = "mycompany-terraform-state"
lifecycle {
prevent_destroy = true # Evitar borrado accidental
}
}
resource "aws_s3_bucket_versioning" "state_versioning" {
bucket = aws_s3_bucket.terraform_state.id
versioning_configuration {
status = "Enabled" # Poder recuperar states anteriores
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "state_encryption" {
bucket = aws_s3_bucket.terraform_state.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
}
}
}
resource "aws_s3_bucket_public_access_block" "state_public_access" {
bucket = aws_s3_bucket.terraform_state.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
} Siempre habilitar versionado en el bucket
El versionado del bucket S3 permite recuperar un state anterior si se corrompe
o si un terraform apply destructivo actualiza el state incorrectamente.
Sin versionado, un state perdido significa tener que reimportar toda la infraestructura.
Comandos de terraform state
# ββ Listar todos los recursos en el state ββ
terraform state list
# aws_vpc.main
# aws_subnet.public[0]
# aws_subnet.public[1]
# module.backend.aws_ecs_task_definition.this
# ββ Ver detalles de un recurso especifico ββ
terraform state show aws_vpc.main
# resource "aws_vpc" "main" {
# arn = "arn:aws:ec2:eu-west-1:123456:vpc/vpc-abc123"
# cidr_block = "10.0.0.0/16"
# enable_dns_hostnames = true
# ...
# }
# ββ Mover recurso (renombrar en el state sin recrear) ββ
terraform state mv aws_vpc.main aws_vpc.primary
# Util al refactorizar: renombrar recursos sin destruirlos
# ββ Mover recurso a otro modulo ββ
terraform state mv aws_subnet.public module.networking.aws_subnet.public
# ββ Eliminar recurso del state (sin destruir en cloud) ββ
terraform state rm aws_security_group.legacy
# El recurso sigue existiendo en AWS, pero Terraform deja de gestionarlo
# ββ Descargar state remoto a archivo local ββ
terraform state pull > terraform.tfstate.backup
# ββ Subir state local al backend remoto (PELIGROSO) ββ
terraform state push terraform.tfstate.backup
# ββ Forzar desbloqueo del state (si un apply quedo colgado) ββ
terraform force-unlock LOCK_ID Cheatsheet de state
state list β listar recursos
state show ADDR β detalles de un recurso
state mv SRC DST β renombrar/mover sin recrear
state rm ADDR β dejar de gestionar un recurso
state pull β descargar state
state push β subir state (usar con precaucion)
Importar recursos existentes
Cuando tienes infraestructura creada manualmente (ClickOps) o por otra
herramienta, puedes incorporarla al state de Terraform con el bloque import.
# imports.tf β Importar recursos existentes (Terraform >= 1.5)
# Importar una VPC existente
import {
to = aws_vpc.main
id = "vpc-0abc123def456789"
}
# Importar un Security Group
import {
to = aws_security_group.web
id = "sg-0abc123def456789"
}
# Importar un RDS instance
import {
to = aws_db_instance.primary
id = "my-production-database"
}
# Importar un bucket S3
import {
to = aws_s3_bucket.uploads
id = "my-company-uploads-bucket"
} # Flujo de importacion
# 1. Escribir el bloque import + el recurso HCL
# 2. Ejecutar plan para ver que Terraform detecta el recurso
terraform plan -generate-config-out=generated.tf # Genera HCL automatico (experimental)
# 3. Revisar y ajustar el HCL generado
# 4. Aplicar para sincronizar el state
terraform apply
# Metodo clasico (CLI, sin bloque import)
terraform import aws_vpc.main vpc-0abc123def456789 Moved blocks para refactoring
Al reorganizar modulos o renombrar recursos, usa el bloque moved
para que Terraform entienda que el recurso cambio de nombre pero
no debe destruirse y recrearse.
# moved.tf β Refactoring sin destruir recursos
# Renombrar un recurso
moved {
from = aws_vpc.main
to = aws_vpc.primary
}
# Mover recurso a un modulo
moved {
from = aws_subnet.public
to = module.networking.aws_subnet.public
}
# Mover entre modulos
moved {
from = module.old_network.aws_vpc.this
to = module.networking.aws_vpc.this
}
# Renombrar un modulo completo
moved {
from = module.backend_service
to = module.api_service
} moved vs state mv
moved blocks son declarativos y se commitean en Git: todo el equipo
recibe la refactorizacion al hacer terraform plan. state mv
es imperativo y modifica el state directamente β solo lo ve quien lo ejecuta.
Siempre prefiere moved en equipos colaborativos.