Día 98 - Launch EC2 in Private VPC Subnet (multi-file + variables + outputs + red privada)
Problema / Desafío
El equipo Nautilus necesita una VPC privada con una subnet, y una EC2 dentro, accesible solo desde dentro de la VPC. Requisitos:
- VPC
datacenter-priv-vpc, CIDR10.0.0.0/16 - Subnet
datacenter-priv-subneten la VPC, CIDR10.0.1.0/24, con auto-assign IP deshabilitado - EC2
datacenter-priv-ec2dentro de la subnet, tipot2.micro - El security group debe permitir acceso solo desde el CIDR de la VPC
main.tfpara VPC + subnet + EC2 (no otro.tfpara los recursos)variables.tfcon:KKE_VPC_CIDR,KKE_SUBNET_CIDRoutputs.tfcon:KKE_vpc_name,KKE_subnet_name,KKE_ec2_privateterraform plandebe devolver "No changes" antes de entregar
Directorio /home/bob/terraform.
Día 5 de Terraform y el más completo: primer proyecto multi-archivo (main + variables + outputs), 4 recursos encadenados, variables de entrada y outputs, más el requisito explícito de idempotencia (
plansin cambios).
Conceptos clave
Multi-archivo — Terraform carga TODOS los .tf del directorio
El requisito separa el código en tres archivos. No hay que "importar" ni enlazarlos: Terraform concatena automáticamente todos los .tf del directorio de trabajo como si fueran uno solo.
| Archivo | Convención (qué suele ir) |
|---|---|
main.tf |
Los resource y data (la infra) |
variables.tf |
Las declaraciones variable (entradas) |
outputs.tf |
Las declaraciones output (salidas) |
providers.tf |
El bloque provider / terraform { required_providers } |
Es solo organización — Terraform no distingue por nombre de archivo. Se podría poner todo en main.tf; separar mejora la legibilidad y es la convención de la comunidad. El orden entre archivos no importa (el grafo de dependencias decide el orden real, Día 96).
Variables de entrada — parametrizar el código
Un bloque variable declara una entrada del módulo, con tipo, descripción y valor por defecto:
variable "KKE_VPC_CIDR" {
description = "CIDR block for the VPC"
type = string
default = "10.0.0.0/16"
}
| Campo | Función |
|---|---|
type |
Tipo esperado (string, number, bool, list, map, object) |
default |
Valor si no se pasa otro (opcional; sin default, es obligatoria) |
description |
Documentación de la variable |
Se referencian con var.<nombre>:
Ventaja: el CIDR se define una vez y se reutiliza (en la VPC y en el ingress del SG). Cambiarlo en un solo lugar afecta todo. Sin default, Terraform lo pediría interactivo o por -var/terraform.tfvars.
Outputs — exponer valores después del apply
Un bloque output publica un valor al terminar el apply (y queda consultable con terraform output):
output "KKE_vpc_name" {
description = "vpc name"
value = aws_vpc.this.tags.Name # lee el tag Name del recurso
}
Para qué sirven los outputs:
- Mostrar datos útiles tras aplicar (IPs, IDs, nombres) sin buscarlos en la consola.
- Pasar valores entre módulos: el output de un módulo es la entrada de otro.
- Consumo por scripts/CI:
terraform output -raw KKE_vpc_name.
Acá los tres outputs leen el tags.Name de cada recurso, que es lo que pide el enunciado ("name of the VPC/subnet/EC2").
La arquitectura: VPC → subnet → EC2 (red privada)
Los 4 recursos se encadenan por referencias (grafo del Día 96):
aws_vpc.this (10.0.0.0/16)
│
├── aws_subnet.private_subnet (10.0.1.0/24, dentro del CIDR de la VPC)
│ │
│ └── aws_instance.this (subnet_id → nace en esta subnet)
│
└── aws_security_group.priv_sg (vpc_id → ingress solo desde 10.0.0.0/16)
│
└── aws_instance.this (vpc_security_group_ids)
| Recurso | Referencia clave | Efecto |
|---|---|---|
| subnet | vpc_id = aws_vpc.this.id |
la subnet vive en la VPC |
| instance | subnet_id = aws_subnet...id |
la instancia nace en esa subnet |
| instance | vpc_security_group_ids = [sg.id] |
aplica el SG privado |
El CIDR de la subnet vive dentro del CIDR de la VPC
10.0.1.0/24 está contenido en 10.0.0.0/16 (Día 94: /16 fija 10.0.x.x, /24 fija 10.0.1.x). Una subnet debe ser un sub-rango de su VPC — si el CIDR de la subnet cae fuera del de la VPC, AWS rechaza la creación.
VPC 10.0.0.0/16 → 10.0.0.0 – 10.0.255.255 (65 536 IPs)
subnet 10.0.1.0/24 → 10.0.1.0 – 10.0.1.255 (256 IPs, dentro del rango VPC)
map_public_ip_on_launch = false — qué hace "privada" a la subnet
resource "aws_subnet" "private_subnet" {
map_public_ip_on_launch = false # ← "auto-assign IP must not be enabled"
}
Este es el punto 2 del lab. Cuando es false, las instancias que nacen en la subnet no reciben una IP pública automática → no son alcanzables desde internet. Es lo que la consola de AWS llama "Auto-assign public IPv4 address".
En una subnet custom el default ya es
false, pero dejarlo explícito lo hace inequívoco ante el validador. Una subnet con esto enfalse(y sin route a un Internet Gateway) es, de hecho, una subnet privada.
El SG "solo desde dentro de la VPC"
El punto 4 se cumple poniendo el CIDR de la VPC como único origen del ingress:
ingress {
from_port = 0
to_port = 0
protocol = "-1" # -1 = todo el tráfico (todos los protocolos)
cidr_blocks = [var.KKE_VPC_CIDR] # 10.0.0.0/16 → solo IPs internas de la VPC
}
protocol = "-1"+from_port/to_port = 0: todo el tráfico (no se limita a un puerto).cidr_blocks = [10.0.0.0/16]: solo responde a IPs dentro de la VPC. Nada de0.0.0.0/0en entrada → no se expone a internet.
Contraste con el Día 95 (SG con 0.0.0.0/0 en HTTP/SSH, abierto al mundo): hoy el SG es restringido por diseño. Reutilizar var.KKE_VPC_CIDR garantiza que el SG y la VPC siempre coincidan, aunque cambie el CIDR.
★ El requisito de idempotencia: plan sin cambios
El lab exige que, tras aplicar, terraform plan devuelva:
Esto es la garantía central de la IaC: el código describe el estado deseado, y si la realidad ya coincide, no hay nada que hacer. Un plan "limpio" confirma que:
- No hay drift (nadie cambió la infra por fuera).
- El código no tiene cambios pendientes sin aplicar.
- Volver a aplicar es seguro (no recrea ni modifica nada).
Si
planmostrara cambios tras unapply, sería señal de un problema: un atributo que AWS normaliza distinto a como se escribió, un valor(known after apply)que no se estabiliza, o drift real. Para este lab, los 4 recursos quedan estables →planlimpio.
★ (known after apply) ambiguo — referencia vs no-seteado
En el plan de la instancia aparece:
Acá (known after apply) no significa "no lo seté" (como sí pasaba en el Día 96 con key_name). Significa que el valor referencia el .id de un recurso que aún no existe (aws_subnet.private_subnet.id) — Terraform no conoce ese id hasta crear la subnet, por eso lo difiere.
(known after apply) aparece porque… |
¿Es problema? |
|---|---|
| El argumento no se seteó (Día 96) | Sí — falta el valor |
Referencia un atributo computado (.id, .arn) de un recurso que se crea en el mismo apply |
No — es normal |
La lección: (known after apply) es ambiguo. Para distinguir, mirar el código: si el argumento está en el .tf y referencia un .id, es normal; si no está, es un olvido.
Pasos
- Abrir terminal en
/home/bob/terraform - Crear
variables.tf(las dosKKE_*_CIDR) - Crear
main.tf(VPC + subnet + SG + EC2) - Crear
outputs.tf(los tresKKE_*) terraform init→terraform plan→terraform apply→yes- Re-correr
terraform plany confirmar "No changes"
Comandos / Código
variables.tf
variable "KKE_VPC_CIDR" {
description = "CIDR block for the VPC"
type = string
default = "10.0.0.0/16"
}
variable "KKE_SUBNET_CIDR" {
description = "CIDR block for subnet"
type = string
default = "10.0.1.0/24"
}
main.tf
provider "aws" {
region = "us-east-1"
}
resource "aws_vpc" "this" {
cidr_block = var.KKE_VPC_CIDR
tags = { Name = "datacenter-priv-vpc" }
}
resource "aws_subnet" "private_subnet" {
cidr_block = var.KKE_SUBNET_CIDR
vpc_id = aws_vpc.this.id
map_public_ip_on_launch = false # auto-assign IP deshabilitado
tags = { Name = "datacenter-priv-subnet" }
}
resource "aws_security_group" "priv_sg" {
name = "datacenter-priv-sg"
vpc_id = aws_vpc.this.id
description = "allow only within vpc resources"
ingress {
description = "allow only inside vpc"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = [var.KKE_VPC_CIDR] # solo desde dentro de la VPC
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = { Name = "datacenter-priv-sg" }
}
resource "aws_instance" "this" {
ami = "ami-0c101f26f147fa7fd"
instance_type = "t2.micro"
subnet_id = aws_subnet.private_subnet.id
vpc_security_group_ids = [aws_security_group.priv_sg.id]
tags = { Name = "datacenter-priv-ec2" }
}
outputs.tf
output "KKE_vpc_name" {
description = "vpc name"
value = aws_vpc.this.tags.Name
}
output "KKE_subnet_name" {
description = "subnet name"
value = aws_subnet.private_subnet.tags.Name
}
output "KKE_ec2_private" {
description = "ec2 name" # (el valor es el Name, como pide el lab)
value = aws_instance.this.tags.Name
}
Ejecutar y verificar idempotencia
Output real del apply (recortado):
Plan: 4 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ KKE_ec2_private = "datacenter-priv-ec2"
+ KKE_subnet_name = "datacenter-priv-subnet"
+ KKE_vpc_name = "datacenter-priv-vpc"
aws_vpc.this: Creation complete after 1s [id=vpc-17e69d33...]
aws_subnet.private_subnet: Creation complete after 0s [id=subnet-e9b05e8f...]
aws_security_group.priv_sg: Creation complete after 0s [id=sg-866c6997...]
aws_instance.this: Creation complete after 10s [id=i-c81231af...]
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.
Outputs:
KKE_ec2_private = "datacenter-priv-ec2"
KKE_subnet_name = "datacenter-priv-subnet"
KKE_vpc_name = "datacenter-priv-vpc"
Verificación de la subnet privada y el state:
terraform state list
# aws_instance.this
# aws_security_group.priv_sg
# aws_subnet.private_subnet
# aws_vpc.this
terraform state show aws_subnet.private_subnet | grep map_public_ip_on_launch
# map_public_ip_on_launch = false ← auto-assign IP deshabilitado ✓
terraform output # los tres KKE_*
Variantes (referencia)
Pasar variables sin default (tfvars)
terraform apply # toma terraform.tfvars automáticamente
terraform apply -var="KKE_VPC_CIDR=10.5.0.0/16" # o por flag
Output del ID/IP real (no solo el nombre)
output "ec2_private_ip" {
value = aws_instance.this.private_ip # la IP privada real, no el tag
}
output "vpc_id" {
value = aws_vpc.this.id
}
Validación de variables
variable "KKE_VPC_CIDR" {
type = string
validation {
condition = can(cidrhost(var.KKE_VPC_CIDR, 0))
error_message = "Debe ser un CIDR IPv4 válido."
}
}
Subnet en una AZ específica
resource "aws_subnet" "private_subnet" {
cidr_block = var.KKE_SUBNET_CIDR
vpc_id = aws_vpc.this.id
availability_zone = "us-east-1a"
}
Troubleshooting
| Problema | Causa | Solución |
|---|---|---|
plan muestra cambios después del apply |
Drift, o un atributo que AWS normaliza distinto | Investigar el diff; re-aplicar; el requisito pide plan limpio |
InvalidSubnet.Range: ... not within VPC CIDR |
El CIDR de la subnet cae fuera del de la VPC | La subnet debe ser sub-rango de la VPC (10.0.1.0/24 ⊂ 10.0.0.0/16) |
| La instancia recibe IP pública igual | map_public_ip_on_launch = true o default heredado |
Ponerlo en false explícitamente |
Error: ... security group ... another VPC |
El SG es de otra VPC distinta a la de la subnet | SG e instancia deben estar en la misma VPC (referenciar aws_vpc.this.id) |
Reference to undeclared input variable |
Se usó var.X sin declararlo en variables.tf |
Declarar la variable (Terraform lee todos los .tf, pero debe existir) |
Error: Output refers to ... undeclared |
El output referencia un recurso/atributo que no existe | Revisar el path (aws_vpc.this.tags.Name) |
(known after apply) en subnet_id y parece error |
Normal: referencia el .id de un recurso que se crea en el mismo apply |
No es problema; se resuelve al crear la subnet |
| La variable pide valor interactivo | No tiene default ni se pasó por tfvars/-var |
Agregar default, o un terraform.tfvars |
Conexión con días anteriores
- Día 94 (CIDR): hoy se aplica el "contención de rangos" — la subnet
10.0.1.0/24debe estar dentro de la VPC10.0.0.0/16. El cálculo de rangos del 94 se vuelve un requisito de AWS. - Día 95 (SG): contraste directo — ahí el SG abría
0.0.0.0/0(público); hoy restringe al CIDR de la VPC (privado). Mismo recurso, intención opuesta. - Día 96 (grafo,
(known after apply)): hoy el grafo encadena 4 recursos; y(known after apply)aparece de nuevo, pero esta vez como referencia válida (.id), no como olvido — el contraste aclara qué significa cada caso. - Variables vs Ansible: las
variable/var.Xde Terraform son el equivalente de las variables de inventario /host_varsde Ansible (Día 85) — parametrizar el código en vez de hardcodear.