Introducción

Amazon Web Services (AWS) es el proveedor de nube más utilizado del mundo, y Terraform ofrece un soporte excepcional para sus cientos de servicios. En este módulo aprenderás a configurar el provider de AWS, autenticarte de forma segura y crear infraestructura real: desde una VPC básica hasta una arquitectura web completa con EC2, Security Groups y S3.

Al final de este módulo serás capaz de diseñar y aprovisionar infraestructura AWS profesional utilizando Terraform.


Configuración del Provider AWS

Antes de crear cualquier recurso en AWS, debes configurar el provider. El provider es el plugin que Terraform utiliza para comunicarse con la API de AWS.

Configuración básica

# versions.tf
terraform {
  required_version = ">= 1.5.0"

  required_providers {
    aws = {
      # Fuente oficial del provider de AWS en el registro de HashiCorp
      source  = "hashicorp/aws"
      # El operador ~> permite actualizaciones menores pero no mayores
      # ~> 5.0 permite 5.1, 5.2, etc. pero no 6.0
      version = "~> 5.0"
    }
  }
}

# Configuración del provider de AWS
provider "aws" {
  # Región donde se crearán los recursos por defecto
  region = "us-east-1"

  # (Opcional) Etiquetas que se añadirán automáticamente a todos los recursos
  default_tags {
    tags = {
      ManagedBy   = "terraform"
      Environment = var.environment
      Project     = var.project_name
    }
  }
}

Configuración multi-región

Si necesitas recursos en varias regiones, puedes definir múltiples configuraciones del provider:

# Provider para us-east-1 (por defecto, sin alias)
provider "aws" {
  region = "us-east-1"
}

# Provider para eu-west-1 (con alias para diferenciarlo)
provider "aws" {
  alias  = "europa"
  region = "eu-west-1"
}

# Usar el provider de Europa en un recurso específico
resource "aws_s3_bucket" "backup_europa" {
  # Con provider = se especifica qué configuración del provider usar
  provider = aws.europa
  bucket   = "mis-backups-europa"
}

Autenticación: IAM, Variables de Entorno y AWS Profiles

La autenticación es uno de los aspectos más críticos. Terraform buscará credenciales de AWS en este orden:

Método 1: Variables de Entorno (Recomendado para CI/CD)

# Configurar credenciales mediante variables de entorno
export AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
export AWS_DEFAULT_REGION="us-east-1"

# Para roles temporales (STS AssumeRole), también se necesita:
export AWS_SESSION_TOKEN="FwoGZXIvYXdzEJr..."

Seguridad: Nunca escribas credenciales de AWS directamente en archivos .tf. Las variables de entorno son más seguras porque no se commitean al repositorio.

Método 2: AWS CLI Profiles

# Configurar un perfil con la CLI de AWS
aws configure --profile mi-proyecto

# Se pedirá:
# AWS Access Key ID: AKIAIOSFODNN7EXAMPLE
# AWS Secret Access Key: wJalrXUtnFEMI/...
# Default region name: us-east-1
# Default output format: json

Luego en Terraform:

provider "aws" {
  region  = "us-east-1"
  # Usar el perfil configurado en ~/.aws/credentials
  profile = "mi-proyecto"
}

O mediante variable de entorno:

export AWS_PROFILE="mi-proyecto"

Método 3: IAM Role (Recomendado para EC2 y ECS)

Cuando Terraform se ejecuta desde una instancia EC2 o contenedor ECS, puede asumir automáticamente el rol IAM asignado al recurso:

provider "aws" {
  region = "us-east-1"
  # No se necesitan credenciales explícitas cuando se usa un IAM Role
  # Terraform las obtiene del metadata service de la instancia
}

Método 4: Asumir un Rol (AssumeRole)

Para gestionar múltiples cuentas o separar responsabilidades:

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

  assume_role {
    # ARN del rol que Terraform asumirá para ejecutar las operaciones
    role_arn     = "arn:aws:iam::123456789012:role/TerraformDeployRole"
    session_name = "terraform-session"
    # Duración máxima de la sesión temporal
    duration     = "1h"
  }
}

Tabla de Recursos AWS más Usados con sus Tipos en Terraform

Servicio AWS Recurso Tipo en Terraform
EC2 Instancia virtual aws_instance
VPC Red privada virtual aws_vpc
VPC Subred aws_subnet
VPC Internet Gateway aws_internet_gateway
VPC Route Table aws_route_table
VPC Security Group aws_security_group
S3 Bucket de almacenamiento aws_s3_bucket
RDS Instancia de base de datos aws_db_instance
RDS Subnet Group aws_db_subnet_group
IAM Rol aws_iam_role
IAM Política aws_iam_policy
IAM Usuario aws_iam_user
ELB Application Load Balancer aws_lb
ELB Target Group aws_lb_target_group
Route53 Zona DNS aws_route53_zone
Route53 Registro DNS aws_route53_record
CloudFront Distribución CDN aws_cloudfront_distribution
Lambda Función serverless aws_lambda_function
ECS Cluster de contenedores aws_ecs_cluster
EKS Cluster de Kubernetes aws_eks_cluster
ElastiCache Cluster de caché aws_elasticache_cluster

Recursos AWS más Comunes con Terraform

VPC y Subredes

# variables.tf
variable "vpc_cidr" {
  description = "CIDR block para la VPC principal"
  type        = string
  default     = "10.0.0.0/16"
}

variable "environment" {
  description = "Nombre del entorno (dev, staging, prod)"
  type        = string
  default     = "dev"
}
# vpc.tf

# La VPC es la red privada que contendrá todos nuestros recursos
resource "aws_vpc" "principal" {
  cidr_block = var.vpc_cidr

  # Necesario para que las instancias dentro de la VPC
  # puedan resolver nombres DNS de AWS (ej: RDS endpoints)
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name = "${var.environment}-vpc"
  }
}

# Internet Gateway: permite que la VPC se comunique con Internet
# Sin esto, los recursos en subredes públicas no tienen acceso a Internet
resource "aws_internet_gateway" "igw" {
  # Adjuntamos el IGW a nuestra VPC
  vpc_id = aws_vpc.principal.id

  tags = {
    Name = "${var.environment}-igw"
  }
}

# Subred pública: los recursos aquí pueden tener IPs públicas
resource "aws_subnet" "publica" {
  vpc_id = aws_vpc.principal.id

  # Un subconjunto del rango CIDR de la VPC
  cidr_block = "10.0.1.0/24"

  # Los recursos en esta subred obtendrán automáticamente una IP pública
  map_public_ip_on_launch = true

  # Zona de disponibilidad donde se creará la subred
  availability_zone = "us-east-1a"

  tags = {
    Name = "${var.environment}-subnet-publica"
    Tier = "public"
  }
}

# Subred privada: los recursos aquí NO tienen IPs públicas directas
resource "aws_subnet" "privada" {
  vpc_id            = aws_vpc.principal.id
  cidr_block        = "10.0.2.0/24"
  availability_zone = "us-east-1b"

  # NO asignar IPs públicas (false es el valor por defecto)
  map_public_ip_on_launch = false

  tags = {
    Name = "${var.environment}-subnet-privada"
    Tier = "private"
  }
}

# Route Table para la subred pública
# Define por dónde sale el tráfico de red
resource "aws_route_table" "publica" {
  vpc_id = aws_vpc.principal.id

  # Ruta por defecto: todo el tráfico (0.0.0.0/0) sale por el Internet Gateway
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.igw.id
  }

  tags = {
    Name = "${var.environment}-rt-publica"
  }
}

# Asociar la route table con la subred pública
# Sin esta asociación, la subred usa la route table por defecto de la VPC
resource "aws_route_table_association" "publica" {
  subnet_id      = aws_subnet.publica.id
  route_table_id = aws_route_table.publica.id
}

Security Groups

# security_groups.tf

# Security Group para el servidor web
resource "aws_security_group" "web_sg" {
  name        = "${var.environment}-web-sg"
  description = "Security Group para servidores web"

  # El SG debe pertenecer a una VPC específica
  vpc_id = aws_vpc.principal.id

  # INBOUND RULES: tráfico que puede ENTRAR al recurso
  ingress {
    description = "HTTPS desde cualquier lugar"
    from_port   = 443   # Puerto de inicio del rango
    to_port     = 443   # Puerto de fin del rango
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"] # Desde cualquier IP (internet)
  }

  ingress {
    description = "HTTP desde cualquier lugar (redirección a HTTPS)"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    description = "SSH solo desde IPs corporativas"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    # Lista de CIDRs permitidos para SSH
    cidr_blocks = ["203.0.113.0/24"] # Reemplaza con tu IP corporativa
  }

  # OUTBOUND RULES: tráfico que puede SALIR del recurso
  egress {
    description = "Todo el tráfico de salida está permitido"
    from_port   = 0     # 0 = todos los puertos
    to_port     = 0
    protocol    = "-1"  # -1 = todos los protocolos
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "${var.environment}-web-sg"
  }
}

# Security Group para la base de datos
# Solo permite tráfico desde el SG del servidor web
resource "aws_security_group" "db_sg" {
  name        = "${var.environment}-db-sg"
  description = "Security Group para la base de datos"
  vpc_id      = aws_vpc.principal.id

  ingress {
    description = "MySQL solo desde el SG del servidor web"
    from_port   = 3306
    to_port     = 3306
    protocol    = "tcp"
    # En lugar de un CIDR, referenciamos otro Security Group
    # Esto es más seguro que abrir a un rango de IPs
    security_groups = [aws_security_group.web_sg.id]
  }

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

  tags = {
    Name = "${var.environment}-db-sg"
  }
}

EC2 Instancias

# ec2.tf

# Data source: obtiene la AMI más reciente de Amazon Linux 2
# sin necesidad de hardcodear el ID de la AMI (que varía por región)
data "aws_ami" "amazon_linux" {
  # Devuelve la AMI más reciente si hay varias coincidencias
  most_recent = true

  # Solo AMIs publicadas por Amazon (ID de cuenta de Amazon)
  owners = ["amazon"]

  filter {
    name   = "name"
    # El asterisco es un wildcard: cualquier AMI cuyo nombre empiece así
    values = ["amzn2-ami-hvm-*-x86_64-gp2"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

resource "aws_instance" "servidor_web" {
  # Referenciamos el ID de la AMI obtenido del data source
  # Así el código es portable entre regiones
  ami = data.aws_ami.amazon_linux.id

  # Tipo de instancia: determina CPU, RAM y capacidad de red
  instance_type = "t3.micro" # 2 vCPU, 1 GB RAM (elegible para free tier)

  # En qué subred lanzar la instancia
  subnet_id = aws_subnet.publica.id

  # Lista de Security Groups a aplicar
  vpc_security_group_ids = [aws_security_group.web_sg.id]

  # Par de claves SSH para acceso remoto
  # Debes crear el key pair previamente en AWS o con el recurso aws_key_pair
  key_name = "mi-clave-ssh"

  # Script que se ejecuta al inicio de la instancia (User Data)
  # Útil para instalar software y configurar el servidor
  user_data = <<-EOF
    #!/bin/bash
    # Actualizar paquetes del sistema
    yum update -y

    # Instalar el servidor web Apache
    yum install -y httpd

    # Iniciar Apache y habilitarlo para que arranque con el sistema
    systemctl start httpd
    systemctl enable httpd

    # Crear una página web simple
    echo "<h1>Servidor desplegado con Terraform</h1>" > /var/www/html/index.html
  EOF

  # Tipo y tamaño del volumen raíz (disco duro de la instancia)
  root_block_device {
    volume_type = "gp3"   # General Purpose SSD v3 (mejor rendimiento que gp2)
    volume_size = 20       # 20 GB de almacenamiento
    encrypted   = true     # Encriptar el disco en reposo (buena práctica)
  }

  tags = {
    Name = "${var.environment}-servidor-web"
    Role = "webserver"
  }
}

S3 Buckets

# s3.tf

resource "aws_s3_bucket" "almacenamiento" {
  # El nombre del bucket debe ser globalmente único en toda AWS
  bucket = "${var.environment}-mi-app-almacenamiento-${random_id.sufijo.hex}"

  tags = {
    Name    = "${var.environment}-almacenamiento"
    Purpose = "application-storage"
  }
}

# Generador de ID aleatorio para hacer el nombre del bucket único
resource "random_id" "sufijo" {
  byte_length = 4
}

# Configuración de versionado del bucket
resource "aws_s3_bucket_versioning" "almacenamiento" {
  bucket = aws_s3_bucket.almacenamiento.id

  versioning_configuration {
    # ENABLED: guarda todas las versiones de cada objeto
    status = "Enabled"
  }
}

# Bloquear acceso público al bucket (importante para seguridad)
resource "aws_s3_bucket_public_access_block" "almacenamiento" {
  bucket = aws_s3_bucket.almacenamiento.id

  block_public_acls       = true  # Bloquear ACLs públicas
  block_public_policy     = true  # Bloquear políticas de bucket públicas
  ignore_public_acls      = true  # Ignorar ACLs públicas existentes
  restrict_public_buckets = true  # Restringir buckets públicos
}

# Encriptar los objetos del bucket por defecto
resource "aws_s3_bucket_server_side_encryption_configuration" "almacenamiento" {
  bucket = aws_s3_bucket.almacenamiento.id

  rule {
    apply_server_side_encryption_by_default {
      # SSE-S3: encripción gestionada por AWS (opción más simple)
      sse_algorithm = "AES256"
    }
  }
}

RDS Instances

# rds.tf

# Subnet Group: define en qué subredes puede vivir la BD
# RDS necesita al menos 2 subredes en diferentes AZs para alta disponibilidad
resource "aws_db_subnet_group" "principal" {
  name        = "${var.environment}-db-subnet-group"
  description = "Subnet group para la base de datos principal"

  # Lista de subredes donde RDS puede lanzar instancias
  subnet_ids = [
    aws_subnet.privada.id,
    # En producción, necesitarías una segunda subred privada en otra AZ
    # aws_subnet.privada_2.id
  ]

  tags = {
    Name = "${var.environment}-db-subnet-group"
  }
}

resource "aws_db_instance" "base_de_datos" {
  # Identificador único de la instancia RDS dentro de tu cuenta
  identifier = "${var.environment}-mysql-db"

  # Motor de base de datos y versión
  engine         = "mysql"
  engine_version = "8.0"

  # Clase de instancia: determina CPU y RAM
  # db.t3.micro es elegible para el free tier
  instance_class = "db.t3.micro"

  # Almacenamiento
  allocated_storage     = 20       # GB de almacenamiento inicial
  max_allocated_storage = 100      # GB máximo (auto scaling de storage)
  storage_type          = "gp2"    # General Purpose SSD

  # Credenciales de la base de datos
  # En producción, usa AWS Secrets Manager en lugar de variables directas
  db_name  = "miapp"
  username = "admin"
  password = var.db_password  # Variable sensible, nunca hardcodear

  # Red y seguridad
  db_subnet_group_name   = aws_db_subnet_group.principal.name
  vpc_security_group_ids = [aws_security_group.db_sg.id]

  # La BD NO debe ser accesible desde internet
  publicly_accessible = false

  # Backups automáticos: retener por 7 días
  backup_retention_period = 7
  backup_window           = "03:00-04:00"  # Ventana de backup en UTC

  # Ventana de mantenimiento (actualizaciones del motor, etc.)
  maintenance_window = "Mon:04:00-Mon:05:00"

  # Si es true, se aplican actualizaciones de versiones menores automáticamente
  auto_minor_version_upgrade = true

  # Encriptar los datos en reposo
  storage_encrypted = true

  # Si true, Terraform pedirá confirmación explícita antes de eliminar la BD
  # Protección contra eliminaciones accidentales
  deletion_protection = false  # En producción, pon esto en true

  tags = {
    Name = "${var.environment}-base-datos"
  }
}

IAM Roles y Policies

# iam.tf

# Documento de política que define quién puede asumir este rol
data "aws_iam_policy_document" "ec2_assume_role" {
  statement {
    # Efecto: Allow (permitir) o Deny (denegar)
    effect = "Allow"

    # Quién puede asumir este rol (en este caso, el servicio EC2)
    principals {
      type        = "Service"
      identifiers = ["ec2.amazonaws.com"]
    }

    # La acción de "asumir un rol"
    actions = ["sts:AssumeRole"]
  }
}

# El rol IAM que será asignado a las instancias EC2
resource "aws_iam_role" "rol_ec2" {
  name               = "${var.environment}-ec2-role"
  description        = "Rol IAM para instancias EC2 de la aplicación"

  # La política de confianza: quién puede usar este rol
  assume_role_policy = data.aws_iam_policy_document.ec2_assume_role.json

  tags = {
    Name = "${var.environment}-ec2-role"
  }
}

# Documento de política: qué puede hacer el rol
data "aws_iam_policy_document" "permisos_s3" {
  statement {
    effect = "Allow"

    actions = [
      "s3:GetObject",    # Leer objetos del bucket
      "s3:PutObject",    # Escribir objetos en el bucket
      "s3:DeleteObject", # Eliminar objetos del bucket
      "s3:ListBucket",   # Listar el contenido del bucket
    ]

    # A qué recursos aplica esta política
    resources = [
      aws_s3_bucket.almacenamiento.arn,        # El bucket mismo
      "${aws_s3_bucket.almacenamiento.arn}/*", # Los objetos dentro del bucket
    ]
  }
}

# La política IAM como recurso independiente
resource "aws_iam_policy" "politica_s3" {
  name        = "${var.environment}-s3-access-policy"
  description = "Permite a EC2 acceder al bucket S3 de la aplicación"
  policy      = data.aws_iam_policy_document.permisos_s3.json
}

# Adjuntar la política al rol
resource "aws_iam_role_policy_attachment" "ec2_s3" {
  role       = aws_iam_role.rol_ec2.name
  policy_arn = aws_iam_policy.politica_s3.arn
}

# Instance Profile: envuelve el rol para que EC2 pueda usarlo
resource "aws_iam_instance_profile" "perfil_ec2" {
  name = "${var.environment}-ec2-instance-profile"
  role = aws_iam_role.rol_ec2.name
}

Data Sources de AWS: aws_ami y aws_availability_zones

Los data sources te permiten consultar información de AWS sin crear recursos:

# Obtener las Availability Zones disponibles en la región actual
data "aws_availability_zones" "disponibles" {
  # Solo las AZs en estado activo (no las que están en mantenimiento)
  state = "available"
}

# Obtener información de la cuenta actual
data "aws_caller_identity" "actual" {}

# Obtener información de la región actual
data "aws_region" "actual" {}

# Uso de los data sources
resource "aws_subnet" "publica_az1" {
  vpc_id = aws_vpc.principal.id
  # Usar la primera AZ disponible de forma dinámica
  availability_zone = data.aws_availability_zones.disponibles.names[0]
  cidr_block        = "10.0.1.0/24"
}

resource "aws_subnet" "publica_az2" {
  vpc_id = aws_vpc.principal.id
  # Usar la segunda AZ disponible de forma dinámica
  availability_zone = data.aws_availability_zones.disponibles.names[1]
  cidr_block        = "10.0.2.0/24"
}

# Ejemplo: construir un ARN usando los datos de la cuenta
locals {
  account_id = data.aws_caller_identity.actual.account_id
  region     = data.aws_region.actual.name

  bucket_arn = "arn:aws:s3:::${var.environment}-app-${local.account_id}"
}

Tags y Convenciones de Nomenclatura en AWS

Establecer una estrategia de tags consistente es fundamental para la gestión de costos y recursos:

# locals.tf

locals {
  # Tags comunes que se aplicarán a todos los recursos
  tags_comunes = {
    # Quién gestiona este recurso (no modificar manualmente)
    ManagedBy   = "terraform"
    # Nombre del repositorio donde está el código
    Repository  = "github.com/mi-empresa/infraestructura"
    # Entorno del recurso
    Environment = var.environment
    # Equipo responsable
    Team        = var.team
    # Nombre del proyecto
    Project     = var.project_name
    # Fecha de creación (útil para auditorías)
    CreatedDate = formatdate("YYYY-MM-DD", timestamp())
  }
}

# Convenciones de nomenclatura
# Formato recomendado: {environment}-{proyecto}-{recurso}-{sufijo}
# Ejemplos:
#   prod-webapp-vpc
#   staging-webapp-sg-web
#   dev-webapp-ec2-bastion

resource "aws_vpc" "principal" {
  cidr_block = "10.0.0.0/16"

  # Merge combina los tags comunes con los específicos del recurso
  # Si hay un tag con el mismo nombre, el específico sobrescribe al común
  tags = merge(local.tags_comunes, {
    Name = "${var.environment}-${var.project_name}-vpc"
    Tier = "network"
  })
}

Outputs Útiles para AWS

# outputs.tf

output "vpc_id" {
  description = "ID de la VPC principal"
  value       = aws_vpc.principal.id
}

output "instancia_ip_publica" {
  description = "IP pública del servidor web"
  value       = aws_instance.servidor_web.public_ip
}

output "instancia_ip_privada" {
  description = "IP privada del servidor web"
  value       = aws_instance.servidor_web.private_ip
}

output "rds_endpoint" {
  description = "Endpoint de conexión a la base de datos"
  value       = aws_db_instance.base_de_datos.endpoint
  # El endpoint contiene información sensible de conexión
  sensitive   = true
}

output "s3_bucket_nombre" {
  description = "Nombre del bucket S3 creado"
  value       = aws_s3_bucket.almacenamiento.bucket
}

output "url_aplicacion" {
  description = "URL para acceder a la aplicación web"
  value       = "http://${aws_instance.servidor_web.public_ip}"
}

output "comandos_utiles" {
  description = "Comandos para conectarse a la infraestructura"
  value = {
    ssh    = "ssh -i ~/.ssh/mi-clave.pem ec2-user@${aws_instance.servidor_web.public_ip}"
    mysql  = "mysql -h ${aws_db_instance.base_de_datos.address} -u admin -p miapp"
  }
}

Ejercicio Práctico: Crear una VPC con Subredes Públicas y Privadas

Descripción del ejercicio

Crea una infraestructura AWS completa que incluya:

  1. Una VPC con CIDR 10.0.0.0/16
  2. Dos subredes públicas en diferentes Availability Zones
  3. Dos subredes privadas en diferentes Availability Zones
  4. Un Internet Gateway para las subredes públicas
  5. Un NAT Gateway para que las subredes privadas accedan a internet
  6. Las Route Tables apropiadas para cada tipo de subred
  7. Un Security Group que permita HTTP, HTTPS y SSH
  8. Una instancia EC2 en la subred pública

Plantilla de inicio

# Crea los archivos main.tf, variables.tf y outputs.tf
# Intenta completar el código antes de ver la solución

Solución Completa con Código

# variables.tf
variable "region" {
  description = "Región de AWS donde se desplegará la infraestructura"
  type        = string
  default     = "us-east-1"
}

variable "environment" {
  description = "Nombre del entorno"
  type        = string
  default     = "ejercicio"
}

variable "vpc_cidr" {
  description = "CIDR block de la VPC"
  type        = string
  default     = "10.0.0.0/16"
}
# main.tf

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

provider "aws" {
  region = var.region
}

# ============================================
# DATA SOURCES
# ============================================

# Obtener las AZs disponibles dinámicamente
data "aws_availability_zones" "disponibles" {
  state = "available"
}

# AMI de Amazon Linux 2 más reciente
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*-x86_64-gp2"]
  }
}

# ============================================
# VPC
# ============================================

resource "aws_vpc" "principal" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name        = "${var.environment}-vpc"
    Environment = var.environment
  }
}

# ============================================
# INTERNET GATEWAY
# ============================================

resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.principal.id

  tags = {
    Name        = "${var.environment}-igw"
    Environment = var.environment
  }
}

# ============================================
# SUBREDES PÚBLICAS
# ============================================

resource "aws_subnet" "publica_1" {
  vpc_id                  = aws_vpc.principal.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = data.aws_availability_zones.disponibles.names[0]
  map_public_ip_on_launch = true

  tags = {
    Name        = "${var.environment}-subnet-pub-1"
    Tier        = "public"
    Environment = var.environment
  }
}

resource "aws_subnet" "publica_2" {
  vpc_id                  = aws_vpc.principal.id
  cidr_block              = "10.0.2.0/24"
  availability_zone       = data.aws_availability_zones.disponibles.names[1]
  map_public_ip_on_launch = true

  tags = {
    Name        = "${var.environment}-subnet-pub-2"
    Tier        = "public"
    Environment = var.environment
  }
}

# ============================================
# SUBREDES PRIVADAS
# ============================================

resource "aws_subnet" "privada_1" {
  vpc_id            = aws_vpc.principal.id
  cidr_block        = "10.0.10.0/24"
  availability_zone = data.aws_availability_zones.disponibles.names[0]

  tags = {
    Name        = "${var.environment}-subnet-priv-1"
    Tier        = "private"
    Environment = var.environment
  }
}

resource "aws_subnet" "privada_2" {
  vpc_id            = aws_vpc.principal.id
  cidr_block        = "10.0.11.0/24"
  availability_zone = data.aws_availability_zones.disponibles.names[1]

  tags = {
    Name        = "${var.environment}-subnet-priv-2"
    Tier        = "private"
    Environment = var.environment
  }
}

# ============================================
# NAT GATEWAY (para que subredes privadas salgan a internet)
# ============================================

# El NAT Gateway necesita una IP elástica (IP pública fija)
resource "aws_eip" "nat" {
  domain = "vpc"

  # El EIP depende del IGW
  depends_on = [aws_internet_gateway.igw]

  tags = {
    Name = "${var.environment}-nat-eip"
  }
}

resource "aws_nat_gateway" "nat" {
  # El NAT Gateway se coloca en una subred pública
  subnet_id     = aws_subnet.publica_1.id
  allocation_id = aws_eip.nat.id

  tags = {
    Name = "${var.environment}-nat-gw"
  }

  depends_on = [aws_internet_gateway.igw]
}

# ============================================
# ROUTE TABLES
# ============================================

# Route table pública: sale por el Internet Gateway
resource "aws_route_table" "publica" {
  vpc_id = aws_vpc.principal.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.igw.id
  }

  tags = {
    Name = "${var.environment}-rt-publica"
  }
}

# Asociaciones de la route table pública con las subredes públicas
resource "aws_route_table_association" "publica_1" {
  subnet_id      = aws_subnet.publica_1.id
  route_table_id = aws_route_table.publica.id
}

resource "aws_route_table_association" "publica_2" {
  subnet_id      = aws_subnet.publica_2.id
  route_table_id = aws_route_table.publica.id
}

# Route table privada: sale por el NAT Gateway
resource "aws_route_table" "privada" {
  vpc_id = aws_vpc.principal.id

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.nat.id
  }

  tags = {
    Name = "${var.environment}-rt-privada"
  }
}

# Asociaciones de la route table privada con las subredes privadas
resource "aws_route_table_association" "privada_1" {
  subnet_id      = aws_subnet.privada_1.id
  route_table_id = aws_route_table.privada.id
}

resource "aws_route_table_association" "privada_2" {
  subnet_id      = aws_subnet.privada_2.id
  route_table_id = aws_route_table.privada.id
}

# ============================================
# SECURITY GROUP
# ============================================

resource "aws_security_group" "web" {
  name        = "${var.environment}-web-sg"
  description = "Security Group para el servidor web"
  vpc_id      = aws_vpc.principal.id

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

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

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"] # En producción, limitar a tu IP
    description = "SSH"
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
    description = "Todo el tráfico de salida"
  }

  tags = {
    Name        = "${var.environment}-web-sg"
    Environment = var.environment
  }
}

# ============================================
# EC2 INSTANCE
# ============================================

resource "aws_instance" "web" {
  ami                    = data.aws_ami.amazon_linux.id
  instance_type          = "t3.micro"
  subnet_id              = aws_subnet.publica_1.id
  vpc_security_group_ids = [aws_security_group.web.id]

  user_data = <<-EOF
    #!/bin/bash
    yum update -y
    yum install -y httpd
    systemctl start httpd
    systemctl enable httpd
    echo "<h1>VPC con Terraform - Ejercicio Completado</h1>" > /var/www/html/index.html
  EOF

  tags = {
    Name        = "${var.environment}-servidor-web"
    Environment = var.environment
  }
}
# outputs.tf
output "vpc_id" {
  value       = aws_vpc.principal.id
  description = "ID de la VPC creada"
}

output "subredes_publicas" {
  value       = [aws_subnet.publica_1.id, aws_subnet.publica_2.id]
  description = "IDs de las subredes públicas"
}

output "subredes_privadas" {
  value       = [aws_subnet.privada_1.id, aws_subnet.privada_2.id]
  description = "IDs de las subredes privadas"
}

output "servidor_web_ip" {
  value       = aws_instance.web.public_ip
  description = "IP pública del servidor web"
}

output "url_web" {
  value       = "http://${aws_instance.web.public_ip}"
  description = "URL para acceder al servidor web"
}
# Comandos para ejecutar el ejercicio
terraform init
terraform plan
terraform apply

# Verificar que el servidor web responde
curl http://$(terraform output -raw servidor_web_ip)

# Limpiar recursos al terminar (¡el NAT Gateway genera costos!)
terraform destroy

Errores Comunes con AWS

Error 1: Credenciales no encontradas

Error: No valid credential sources found

Solución:

# Verificar que las credenciales están configuradas
aws sts get-caller-identity

# Si falla, configurar las credenciales
aws configure
# O exportar como variables de entorno
export AWS_ACCESS_KEY_ID="tu-access-key"
export AWS_SECRET_ACCESS_KEY="tu-secret-key"

Error 2: Límites de servicio (Service Quotas)

Error: Error launching source instance: InstanceLimitExceeded:
You have requested more instances (11) than your current instance limit of 10 allows

Solución: Solicitar aumento del límite en la consola de AWS Service Quotas o terminar instancias no utilizadas.

Error 3: AMI no encontrada en la región

Error: Your query returned no results. Please change your search criteria and try again.

Solución: Las AMIs son específicas de cada región. Verifica que el filtro del data source aws_ami devuelve resultados en tu región:

aws ec2 describe-images --owners amazon --filters "Name=name,Values=amzn2-ami-hvm-*-x86_64-gp2" --region us-east-1 --query 'Images[0].ImageId'

Error 4: Bucket S3 ya existe

Error: creating S3 Bucket: BucketAlreadyOwnedByYou

Solución: Los nombres de S3 son globalmente únicos. Añade un sufijo único (cuenta, fecha, ID aleatorio).

Error 5: Error de dependencia al destruir

Error: deleting VPC: DependencyViolation: The vpc cannot be deleted. It has dependencies.

Solución: Terraform debe destruir los recursos en el orden correcto. Si esto falla, verifica si hay recursos creados manualmente dentro de la VPC que no están en el estado de Terraform.


Resumen

En este módulo has aprendido a trabajar con AWS y Terraform de forma profesional:

  • Configurar el provider AWS con soporte multi-región y multi-cuenta
  • Los diferentes métodos de autenticación (variables de entorno, profiles, IAM roles)
  • Crear los recursos AWS más importantes: VPC, subredes, Security Groups, EC2, S3, RDS e IAM
  • Usar data sources para consultar información dinámica de AWS
  • Implementar convenciones de nomenclatura y tagging consistentes
  • Definir outputs útiles para compartir información entre módulos y equipos

La arquitectura que construiste en el ejercicio es la base de cualquier aplicación web en AWS: una VPC con subredes públicas y privadas, con alta disponibilidad en dos Availability Zones.

En el siguiente módulo aplicarás estos mismos conceptos pero en Microsoft Azure, conociendo las diferencias fundamentales entre los dos proveedores y cómo Terraform las abstrae.

Nota de costos: El NAT Gateway es uno de los recursos más costosos del ejercicio (~$32/mes). Recuerda ejecutar terraform destroy cuando termines de practicar para evitar cargos inesperados.

© Copyright 2026. Todos los derechos reservados