Terraform es una herramienta de Infrastructure as Code (IaC) desarrollada por HashiCorp. Permite gestionar recursos de proveedores cloud como AWS mediante codigo declarativo. En este articulo, aprenderemos desde los fundamentos hasta el nivel de produccion a traves de la construccion practica de infraestructura AWS.
Lo que aprenderas en este articulo
- Conceptos basicos de Terraform y sintaxis HCL
- Construccion de recursos AWS (VPC, EC2, RDS, S3)
- Diseno de modulos y mejores practicas
- Gestion de estado y trabajo en equipo
- Integracion con pipelines CI/CD
Configuracion del entorno
Instalacion
# macOS (Homebrew)
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
# Linux
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 version
terraform version
Configuracion de autenticacion AWS
# Configuracion AWS CLI
aws configure
# AWS Access Key ID: AKIAIOSFODNN7EXAMPLE
# AWS Secret Access Key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
# Default region name: ap-northeast-1
# O mediante variables de entorno
export AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
export AWS_DEFAULT_REGION="ap-northeast-1"
Estructura del proyecto
terraform-project/
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
│ │ └── terraform.tfvars
│ ├── staging/
│ └── prod/
├── modules/
│ ├── networking/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── compute/
│ ├── database/
│ └── storage/
├── .terraform-version
└── .gitignore
Sintaxis basica de HCL
Configuracion del proveedor
# main.tf
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
# Backend remoto (se explica mas adelante)
backend "s3" {
bucket = "my-terraform-state"
key = "dev/terraform.tfstate"
region = "ap-northeast-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Environment = var.environment
Project = var.project_name
ManagedBy = "Terraform"
}
}
}
Definicion de variables
# variables.tf
variable "aws_region" {
description = "AWS region"
type = string
default = "ap-northeast-1"
}
variable "environment" {
description = "Environment name"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
variable "project_name" {
description = "Project name"
type = string
}
variable "vpc_cidr" {
description = "VPC CIDR block"
type = string
default = "10.0.0.0/16"
}
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.micro"
}
variable "db_config" {
description = "Database configuration"
type = object({
instance_class = string
allocated_storage = number
engine_version = string
})
default = {
instance_class = "db.t3.micro"
allocated_storage = 20
engine_version = "16.1"
}
}
Archivo de valores de variables
# terraform.tfvars
aws_region = "ap-northeast-1"
environment = "dev"
project_name = "my-app"
vpc_cidr = "10.0.0.0/16"
instance_type = "t3.small"
db_config = {
instance_class = "db.t3.small"
allocated_storage = 50
engine_version = "16.1"
}
Construccion de red VPC
Diseno de modulos
# modules/networking/main.tf
locals {
azs = slice(data.aws_availability_zones.available.names, 0, 3)
}
data "aws_availability_zones" "available" {
state = "available"
}
# VPC
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.project_name}-${var.environment}-vpc"
}
}
# Internet Gateway
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.project_name}-${var.environment}-igw"
}
}
# Subredes publicas
resource "aws_subnet" "public" {
count = length(local.azs)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 4, count.index)
availability_zone = local.azs[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.project_name}-${var.environment}-public-${local.azs[count.index]}"
Tier = "Public"
}
}
# Subredes privadas
resource "aws_subnet" "private" {
count = length(local.azs)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 4, count.index + length(local.azs))
availability_zone = local.azs[count.index]
tags = {
Name = "${var.project_name}-${var.environment}-private-${local.azs[count.index]}"
Tier = "Private"
}
}
# NAT Gateway (para entornos de produccion)
resource "aws_eip" "nat" {
count = var.enable_nat_gateway ? length(local.azs) : 0
domain = "vpc"
tags = {
Name = "${var.project_name}-${var.environment}-nat-eip-${count.index}"
}
}
resource "aws_nat_gateway" "main" {
count = var.enable_nat_gateway ? length(local.azs) : 0
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
tags = {
Name = "${var.project_name}-${var.environment}-nat-${count.index}"
}
depends_on = [aws_internet_gateway.main]
}
# Tabla de rutas (publica)
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.project_name}-${var.environment}-public-rt"
}
}
resource "aws_route_table_association" "public" {
count = length(local.azs)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
# Tabla de rutas (privada)
resource "aws_route_table" "private" {
count = length(local.azs)
vpc_id = aws_vpc.main.id
dynamic "route" {
for_each = var.enable_nat_gateway ? [1] : []
content {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main[count.index].id
}
}
tags = {
Name = "${var.project_name}-${var.environment}-private-rt-${count.index}"
}
}
resource "aws_route_table_association" "private" {
count = length(local.azs)
subnet_id = aws_subnet.private[count.index].id
route_table_id = aws_route_table.private[count.index].id
}
Variables y salidas del modulo
# modules/networking/variables.tf
variable "project_name" {
type = string
}
variable "environment" {
type = string
}
variable "vpc_cidr" {
type = string
default = "10.0.0.0/16"
}
variable "enable_nat_gateway" {
type = bool
default = false
}
# modules/networking/outputs.tf
output "vpc_id" {
description = "VPC ID"
value = aws_vpc.main.id
}
output "public_subnet_ids" {
description = "Public subnet IDs"
value = aws_subnet.public[*].id
}
output "private_subnet_ids" {
description = "Private subnet IDs"
value = aws_subnet.private[*].id
}
Construccion de instancias EC2
Grupo de seguridad
# modules/compute/main.tf
resource "aws_security_group" "web" {
name = "${var.project_name}-${var.environment}-web-sg"
description = "Security group for web servers"
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"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.project_name}-${var.environment}-web-sg"
}
}
Launch Template y Auto Scaling
# Data source para AMI
data "aws_ami" "amazon_linux_2023" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
# Launch Template
resource "aws_launch_template" "web" {
name_prefix = "${var.project_name}-${var.environment}-web-"
image_id = data.aws_ami.amazon_linux_2023.id
instance_type = var.instance_type
network_interfaces {
associate_public_ip_address = false
security_groups = [aws_security_group.web.id]
}
iam_instance_profile {
name = aws_iam_instance_profile.web.name
}
user_data = base64encode(templatefile("${path.module}/templates/user_data.sh", {
environment = var.environment
region = var.aws_region
}))
monitoring {
enabled = true
}
tag_specifications {
resource_type = "instance"
tags = {
Name = "${var.project_name}-${var.environment}-web"
}
}
lifecycle {
create_before_destroy = true
}
}
# Auto Scaling Group
resource "aws_autoscaling_group" "web" {
name = "${var.project_name}-${var.environment}-web-asg"
vpc_zone_identifier = var.private_subnet_ids
target_group_arns = [aws_lb_target_group.web.arn]
health_check_type = "ELB"
min_size = var.asg_min_size
max_size = var.asg_max_size
desired_capacity = var.asg_desired_capacity
launch_template {
id = aws_launch_template.web.id
version = "$Latest"
}
instance_refresh {
strategy = "Rolling"
preferences {
min_healthy_percentage = 50
}
}
tag {
key = "Name"
value = "${var.project_name}-${var.environment}-web"
propagate_at_launch = true
}
}
# Politicas de escalado
resource "aws_autoscaling_policy" "web_scale_up" {
name = "${var.project_name}-${var.environment}-web-scale-up"
autoscaling_group_name = aws_autoscaling_group.web.name
adjustment_type = "ChangeInCapacity"
scaling_adjustment = 1
cooldown = 300
}
resource "aws_autoscaling_policy" "web_scale_down" {
name = "${var.project_name}-${var.environment}-web-scale-down"
autoscaling_group_name = aws_autoscaling_group.web.name
adjustment_type = "ChangeInCapacity"
scaling_adjustment = -1
cooldown = 300
}
Construccion de base de datos RDS
# modules/database/main.tf
# Grupo de subredes
resource "aws_db_subnet_group" "main" {
name = "${var.project_name}-${var.environment}-db-subnet"
subnet_ids = var.private_subnet_ids
tags = {
Name = "${var.project_name}-${var.environment}-db-subnet"
}
}
# Grupo de seguridad
resource "aws_security_group" "db" {
name = "${var.project_name}-${var.environment}-db-sg"
description = "Security group for RDS"
vpc_id = var.vpc_id
ingress {
description = "PostgreSQL from web servers"
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [var.web_security_group_id]
}
tags = {
Name = "${var.project_name}-${var.environment}-db-sg"
}
}
# Grupo de parametros
resource "aws_db_parameter_group" "main" {
family = "postgres16"
name = "${var.project_name}-${var.environment}-pg16"
parameter {
name = "log_min_duration_statement"
value = "1000" # Registrar queries de mas de 1 segundo
}
parameter {
name = "shared_preload_libraries"
value = "pg_stat_statements"
}
}
# Instancia RDS
resource "aws_db_instance" "main" {
identifier = "${var.project_name}-${var.environment}-db"
engine = "postgres"
engine_version = var.db_config.engine_version
instance_class = var.db_config.instance_class
allocated_storage = var.db_config.allocated_storage
max_allocated_storage = var.db_config.allocated_storage * 2
storage_type = "gp3"
storage_encrypted = true
db_name = var.db_name
username = var.db_username
password = var.db_password # Se recomienda usar Secrets Manager
multi_az = var.environment == "prod"
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [aws_security_group.db.id]
parameter_group_name = aws_db_parameter_group.main.name
backup_retention_period = var.environment == "prod" ? 7 : 1
backup_window = "03:00-04:00"
maintenance_window = "mon:04:00-mon:05:00"
skip_final_snapshot = var.environment != "prod"
final_snapshot_identifier = var.environment == "prod" ? "${var.project_name}-${var.environment}-final" : null
deletion_protection = var.environment == "prod"
performance_insights_enabled = true
monitoring_interval = 60
monitoring_role_arn = aws_iam_role.rds_monitoring.arn
tags = {
Name = "${var.project_name}-${var.environment}-db"
}
}
Gestion de estado
Configuracion del backend remoto
# backend-setup/main.tf
# Crear bucket S3 y tabla DynamoDB para gestion de estado
resource "aws_s3_bucket" "terraform_state" {
bucket = "my-terraform-state-${data.aws_caller_identity.current.account_id}"
lifecycle {
prevent_destroy = true
}
}
resource "aws_s3_bucket_versioning" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
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" "terraform_state" {
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"
}
}
Integracion CI/CD
GitHub Actions
# .github/workflows/terraform.yml
name: Terraform
on:
push:
branches: [main]
paths:
- 'terraform/**'
pull_request:
branches: [main]
paths:
- 'terraform/**'
env:
TF_VERSION: '1.6.0'
AWS_REGION: 'ap-northeast-1'
jobs:
terraform:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions
aws-region: ${{ env.AWS_REGION }}
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Terraform Format
id: fmt
run: terraform fmt -check -recursive
continue-on-error: true
- name: Terraform Init
id: init
run: terraform init
working-directory: terraform/environments/dev
- name: Terraform Validate
id: validate
run: terraform validate -no-color
- name: Terraform Plan
id: plan
if: github.event_name == 'pull_request'
run: terraform plan -no-color -input=false
working-directory: terraform/environments/dev
continue-on-error: true
- name: Update Pull Request
uses: actions/github-script@v7
if: github.event_name == 'pull_request'
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const output = `#### Terraform Format 🖌 \`${{ steps.fmt.outcome }}\`
#### Terraform Init ⚙️ \`${{ steps.init.outcome }}\`
#### Terraform Validate 🤖 \`${{ steps.validate.outcome }}\`
#### Terraform Plan 📖 \`${{ steps.plan.outcome }}\`
<details><summary>Show Plan</summary>
\`\`\`terraform
${{ steps.plan.outputs.stdout }}
\`\`\`
</details>`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
})
- name: Terraform Apply
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: terraform apply -auto-approve -input=false
working-directory: terraform/environments/dev
Mejores practicas
Convencion de nombres
# Convencion de nombres consistente
locals {
name_prefix = "${var.project_name}-${var.environment}"
}
resource "aws_vpc" "main" {
tags = {
Name = "${local.name_prefix}-vpc"
}
}
resource "aws_subnet" "public" {
tags = {
Name = "${local.name_prefix}-public-subnet-${count.index + 1}"
}
}
Uso de data sources
# Referencia a recursos existentes
data "aws_vpc" "existing" {
filter {
name = "tag:Name"
values = ["existing-vpc"]
}
}
data "aws_subnets" "private" {
filter {
name = "vpc-id"
values = [data.aws_vpc.existing.id]
}
filter {
name = "tag:Tier"
values = ["Private"]
}
}
Condicionales
# Condicionales segun el entorno
resource "aws_nat_gateway" "main" {
count = var.environment == "prod" ? length(local.azs) : 1
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
}
# Bloques dinamicos
resource "aws_security_group" "example" {
dynamic "ingress" {
for_each = var.allowed_ports
content {
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
}
Referencia de comandos
# Inicializacion
terraform init
# Formateo
terraform fmt -recursive
# Validacion
terraform validate
# Ver plan
terraform plan -out=plan.tfplan
# Aplicar
terraform apply plan.tfplan
# Destruir
terraform destroy
# Ver estado
terraform state list
terraform state show aws_vpc.main
# Importar
terraform import aws_vpc.main vpc-12345678
# Ver salidas
terraform output
terraform output -json
Resumen
Resumimos los puntos clave de la construccion de infraestructura con Terraform.
Principios de diseno
- Modularizacion: Diseno de componentes reutilizables
- Separacion de entornos: Gestion de estado independiente por entorno
- Convencion de nombres: Nomenclatura consistente
- Documentacion: Enriquecer las descripciones de variables
Puntos operativos
- Backend remoto: Gestion de estado con S3 + DynamoDB
- Integracion CI/CD: Automatizacion con GitHub Actions
- Revision del plan: Siempre verificar antes de aplicar
- Control de versiones: Fijar versiones del proveedor y Terraform
La infraestructura como codigo permite lograr reproducibilidad, trazabilidad y automatizacion.