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: jsonLuego en Terraform:
provider "aws" {
region = "us-east-1"
# Usar el perfil configurado en ~/.aws/credentials
profile = "mi-proyecto"
}O mediante variable de entorno:
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:
- Una VPC con CIDR
10.0.0.0/16 - Dos subredes públicas en diferentes Availability Zones
- Dos subredes privadas en diferentes Availability Zones
- Un Internet Gateway para las subredes públicas
- Un NAT Gateway para que las subredes privadas accedan a internet
- Las Route Tables apropiadas para cada tipo de subred
- Un Security Group que permita HTTP, HTTPS y SSH
- 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ónSolució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 destroyErrores Comunes con AWS
Error 1: Credenciales no encontradas
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
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
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
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 destroycuando termines de practicar para evitar cargos inesperados.
Curso de Terraform
Módulo 1: Introducción a Terraform
- ¿Qué es Terraform?
- Instalando Terraform
- Conceptos Básicos de Terraform
- Primera Configuración de Terraform
Módulo 2: Lenguaje de Configuración de Terraform
Módulo 3: Gestión del Estado
Módulo 4: Módulos de Terraform
Módulo 5: Aprovisionamiento de Recursos
- Conceptos Básicos de Aprovisionamiento
- Aprovisionamiento de Recursos AWS
- Aprovisionamiento de Recursos Azure
- Aprovisionamiento de Recursos GCP
Módulo 6: Funcionalidades Avanzadas de Terraform
Módulo 7: Mejores Prácticas de Terraform
- Organización del Código
- Control de Versiones
- Pruebas del Código de Terraform
- Mejores Prácticas de Seguridad
Módulo 8: Terraform en CI/CD
- Integración de Terraform con CI/CD
- Automatización de Terraform con Jenkins
- Uso de Terraform con GitHub Actions
- Terraform Cloud y Enterprise
