DevOps desde Cero: Infraestructura como Codigo con Terraform
Apoya este blog
Si te resulta util este contenido, considera apoyar el blog.
Introduccion
Bienvenido al septimo articulo de la serie DevOps from Zero to Hero. En el articulo anterior exploramos networking en AWS: VPCs, subnets, tablas de rutas y security groups. Ahora es momento de dejar de hacer click en la consola de AWS y empezar a definir infraestructura como definimos codigo de aplicaciones: en archivos, bajo control de versiones, con resultados repetibles.
Esto es Infraestructura como Codigo (IaC), y es una de las practicas mas importantes en DevOps moderno. Si alguna vez creaste una instancia EC2 manualmente, te diste cuenta de que te olvidaste un tag, creaste otra diferente, y despues no sabias cual era “la correcta,” ya entendes el problema que IaC resuelve.
Vamos a cubrir que es IaC, el workflow de Terraform, gestion de estado segura, y construir una VPC real con subnets publicas y privadas usando archivos HCL. Si queres profundizar despues, mira Getting started with Terraform modules y Brief introduction to Terratest.
Vamos a meternos de lleno.
Que es Infraestructura como Codigo?
IaC significa definir tu infraestructura (servidores, redes, bases de datos, balanceadores, registros DNS) en archivos de configuracion declarativos en lugar de crearlos manualmente por una consola web.
- Reproducibilidad: Podes recrear toda tu infraestructura desde cero con un solo comando.
- Control de versiones: Cada cambio queda registrado en Git. Sabes quien cambio que, cuando y por que.
- Colaboracion: Los cambios de infraestructura pasan por pull requests igual que los de codigo.
- Deteccion de drift: Las herramientas detectan cuando el estado real se desvio del declarado y lo corrigen.
- Documentacion: Tu codigo ES tu documentacion. Siempre actualizada porque es la fuente de verdad.
IaC vs ClickOps
“ClickOps” es gestionar infraestructura haciendo click en la consola. Esta bien para aprender pero se cae en equipos:
- Sin auditoria: Alguien cambia una regla de security group. Tres meses despues, nadie se acuerda quien ni por que.
- Servidores snowflake: Cada entorno es diferente porque distintas personas los configuraron en distintos momentos.
- Sin reproducibilidad: Podrias recrear tu entorno de produccion desde cero? Cuanto tardarias?
- Error humano: A las 2 AM borraste accidentalmente una base de datos de produccion porque estabas en la solapa equivocada.
- Silos de conocimiento: Solo una persona sabe como esta la red porque la armo manualmente.
IaC elimina todos estos problemas. Infraestructura definida en codigo, revisada por el equipo, registrada en Git, reproducible en cualquier momento.
Por que Terraform?
Varias herramientas de IaC existen:
- CloudFormation: Nativo de AWS, JSON/YAML. Solo AWS, verboso, pero integracion profunda.
- Pulumi: Infraestructura en lenguajes reales (TypeScript, Python, Go). Gran DX, comunidad mas chica.
- AWS CDK: Genera CloudFormation con TypeScript o Python. Solo AWS, mejor que CloudFormation crudo.
- Terraform: Herramienta de HashiCorp con HCL. Funciona con AWS, GCP, Azure, Kubernetes y cientos de providers.
Usamos Terraform porque funciona entre clouds, tiene el ecosistema mas grande, y es lo que la mayoria de los equipos usa. Los conceptos (estado, planes, config declarativa) se transfieren a cualquier herramienta de IaC.
Conceptos basicos de Terraform
Terraform usa HCL (HashiCorp Configuration Language), un lenguaje declarativo para describir infraestructura.
Providers son plugins para hablar con un cloud o servicio:
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
Resources describen una pieza de infraestructura:
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
tags = { Name = "web-server" }
}
Data sources leen informacion sin crear nada:
data "aws_ami" "ubuntu" {
most_recent = true
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
owners = ["099720109477"]
}
Variables parametrizan tu configuracion:
variable "environment" {
description = "Deployment environment"
type = string
default = "dev"
}
Outputs extraen valores despues de la creacion:
output "instance_public_ip" {
value = aws_instance.web.public_ip
}
El workflow de Terraform: init, plan, apply, destroy
terraform init descarga providers y configura el backend:
$ terraform init
Initializing provider plugins...
- Installing hashicorp/aws v5.82.1...
Terraform has been successfully initialized!
terraform plan muestra que cambiaria sin cambiar nada:
$ terraform plan
# aws_instance.web will be created
+ resource "aws_instance" "web" {
+ ami = "ami-0c55b159cbfafe1f0"
+ instance_type = "t3.micro"
}
Plan: 1 to add, 0 to change, 0 to destroy.
Los simbolos: + crear, ~ modificar, - destruir, -/+ reemplazar. Siempre lee el plan antes
de aplicar.
terraform apply hace los cambios reales (pide confirmacion):
$ terraform apply
aws_instance.web: Creating...
aws_instance.web: Creation complete after 32s [id=i-0abc123def456789]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
terraform destroy tira todo abajo cuando ya no lo necesitas.
Gestion de estado
Terraform registra lo que creo en un archivo de estado. Por defecto es local (terraform.tfstate),
lo cual falla en equipos:
- Sin compartir: Tus companeros no pueden correr Terraform sin el archivo de estado.
- Sin locking: Dos applies concurrentes pueden corromper el estado o crear duplicados.
- Riesgo de perdida: Se muere tu compu, perdiste el estado, Terraform se olvida de tu infra.
La solucion es estado remoto con S3 + DynamoDB locking:
# Crear bucket S3 para estado (una sola vez)
aws s3api create-bucket --bucket my-terraform-state --region us-east-1
aws s3api put-bucket-versioning --bucket my-terraform-state \
--versioning-configuration Status=Enabled
# Crear tabla DynamoDB para locking
aws dynamodb create-table --table-name terraform-lock \
--attribute-definitions AttributeName=LockID,AttributeType=S \
--key-schema AttributeName=LockID,KeyType=HASH \
--billing-mode PAY_PER_REQUEST --region us-east-1
Despues configura el backend:
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/network/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-lock"
encrypt = true
}
}
Ahora el estado es compartido, versionado, encriptado y bloqueado durante applies.
Ejemplo practico: aprovisionando una VPC
Armemos una VPC con subnets publicas y privadas, internet gateway, tablas de rutas y security group. La misma arquitectura del articulo de networking, pero como codigo.
variables.tf
variable "aws_region" {
description = "AWS region"
type = string
default = "us-east-1"
}
variable "environment" {
description = "Environment name"
type = string
default = "dev"
}
variable "vpc_cidr" {
description = "CIDR block for the VPC"
type = string
default = "10.0.0.0/16"
}
variable "public_subnet_cidrs" {
type = list(string)
default = ["10.0.1.0/24", "10.0.2.0/24"]
}
variable "private_subnet_cidrs" {
type = list(string)
default = ["10.0.10.0/24", "10.0.11.0/24"]
}
variable "allowed_ssh_cidr" {
type = string
default = "0.0.0.0/0"
}
main.tf
data "aws_availability_zones" "available" {
state = "available"
}
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "${var.environment}-vpc"
Environment = var.environment
ManagedBy = "terraform"
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = { Name = "${var.environment}-igw" }
}
resource "aws_subnet" "public" {
count = length(var.public_subnet_cidrs)
vpc_id = aws_vpc.main.id
cidr_block = var.public_subnet_cidrs[count.index]
availability_zone = data.aws_availability_zones.available.names[count.index]
map_public_ip_on_launch = true
tags = { Name = "${var.environment}-public-${count.index + 1}", Tier = "public" }
}
resource "aws_subnet" "private" {
count = length(var.private_subnet_cidrs)
vpc_id = aws_vpc.main.id
cidr_block = var.private_subnet_cidrs[count.index]
availability_zone = data.aws_availability_zones.available.names[count.index]
tags = { Name = "${var.environment}-private-${count.index + 1}", Tier = "private" }
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = { Name = "${var.environment}-public-rt" }
}
resource "aws_route_table_association" "public" {
count = length(var.public_subnet_cidrs)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table" "private" {
vpc_id = aws_vpc.main.id
tags = { Name = "${var.environment}-private-rt" }
}
resource "aws_route_table_association" "private" {
count = length(var.private_subnet_cidrs)
subnet_id = aws_subnet.private[count.index].id
route_table_id = aws_route_table.private.id
}
resource "aws_security_group" "web" {
name = "${var.environment}-web-sg"
description = "Allow HTTP, HTTPS, and SSH"
vpc_id = aws_vpc.main.id
tags = { Name = "${var.environment}-web-sg" }
}
resource "aws_vpc_security_group_ingress_rule" "http" {
security_group_id = aws_security_group.web.id
cidr_ipv4 = "0.0.0.0/0"
from_port = 80
to_port = 80
ip_protocol = "tcp"
}
resource "aws_vpc_security_group_ingress_rule" "https" {
security_group_id = aws_security_group.web.id
cidr_ipv4 = "0.0.0.0/0"
from_port = 443
to_port = 443
ip_protocol = "tcp"
}
resource "aws_vpc_security_group_ingress_rule" "ssh" {
security_group_id = aws_security_group.web.id
cidr_ipv4 = var.allowed_ssh_cidr
from_port = 22
to_port = 22
ip_protocol = "tcp"
}
resource "aws_vpc_security_group_egress_rule" "all_outbound" {
security_group_id = aws_security_group.web.id
cidr_ipv4 = "0.0.0.0/0"
ip_protocol = "-1"
}
outputs.tf
output "vpc_id" {
value = aws_vpc.main.id
}
output "public_subnet_ids" {
value = aws_subnet.public[*].id
}
output "private_subnet_ids" {
value = aws_subnet.private[*].id
}
output "security_group_id" {
value = aws_security_group.web.id
}
Que crea esto:
- VPC con soporte DNS usando el CIDR configurado
- Internet Gateway adjunto a la VPC para acceso a internet publico
- Subnets publicas en distintas zonas de disponibilidad con IP publica automatica
- Subnets privadas sin ruta a internet, manteniendo los recursos aislados
- Tablas de rutas dirigiendo trafico publico a traves del gateway
- Security group permitiendo HTTP, HTTPS, SSH entrante y todo el saliente
Variables y tfvars
Usa archivos .tfvars para valores especificos por entorno:
# terraform.tfvars (dev por defecto)
aws_region = "us-east-1"
environment = "dev"
vpc_cidr = "10.0.0.0/16"
# prod.tfvars
# aws_region = "us-east-1"
# environment = "prod"
# vpc_cidr = "10.1.0.0/16"
# public_subnet_cidrs = ["10.1.1.0/24", "10.1.2.0/24", "10.1.3.0/24"]
# private_subnet_cidrs = ["10.1.10.0/24", "10.1.11.0/24", "10.1.12.0/24"]
# allowed_ssh_cidr = "203.0.113.0/24"
# Usa terraform.tfvars automaticamente
terraform plan
# Usa un archivo especifico
terraform plan -var-file="prod.tfvars"
# O pasa directamente
terraform plan -var="environment=staging"
# O usa variables de entorno
export TF_VAR_environment="staging"
Precedencia (menor a mayor): defaults, terraform.tfvars, *.auto.tfvars, -var-file, -var,
TF_VAR_ env vars.
Ejecutando el ejemplo
terraform init # Descargar providers
terraform fmt # Formatear codigo
terraform validate # Verificar sintaxis
terraform plan # Preview de cambios
terraform apply # Crear infraestructura
terraform output # Mostrar outputs
terraform state list # Listar recursos gestionados
terraform destroy # Limpiar cuando termines
Buenas practicas
Algunas cosas a tener en cuenta:
- Nunca commitees archivos de estado a Git. Contienen datos sensibles. Usa estado remoto.
- Si commitea
.terraform.lock.hcl. Fija versiones de providers comopackage-lock.json.- Cuidado con
.tfvars. Si tienen secretos, usa variables de entorno o un gestor de secretos.- Taggealo todo con
ManagedBy = "terraform"para distinguir recursos gestionados por IaC de los manuales.- Usa
plan -out=tfplanen CI/CD para guardar un plan y aplicar exactamente lo revisado.
Notas finales
La Infraestructura como Codigo cambia como pensas sobre la infraestructura. En lugar de entornos fragiles configurados a mano, tenes definiciones reproducibles, versionadas y revisables que cualquiera del equipo puede leer y modificar.
Terraform no es la unica herramienta, pero es un gran punto de partida. El enfoque declarativo (describis lo que queres, Terraform se encarga de como llegar) lo hace accesible, y el workflow de plan-antes-de-apply te da una red de seguridad que hacer click en una consola nunca podria darte.
Empeza de a poco. Un recurso, un plan, un apply. Despues agrega mas. Antes de que te des cuenta, toda tu infraestructura va a estar en un punado de archivos y te vas a preguntar como hiciste sin esto.
Espero que te haya resultado util y lo hayas disfrutado! Hasta la proxima!
Errata
Si encontras algun error o tenes alguna sugerencia, por favor mandame un mensaje para que se corrija.
Tambien podes revisar el codigo fuente y los cambios en las fuentes aca
$ Comentarios
Online: 0Por favor inicie sesión para poder escribir comentarios.