Día 94 - Create VPC Using Terraform (primer día de IaC: providers, resources, state, plan/apply)
Problema / Desafío
El equipo Nautilus está migrando parte de su infraestructura a AWS de forma incremental, en pasos pequeños y controlados. Primer paso:
- Crear una VPC llamada
xfusion-vpcen la regiónus-east-1con cualquier bloque CIDR IPv4, usando Terraform. - Directorio de trabajo:
/home/bob/terraform. Crear el archivomain.tf(no crear otro.tf).
Cambio de sección: termina la racha de Ansible (Días 82-93) y arranca Terraform / Infrastructure as Code. Si Ansible es configuration management (configura servidores que ya existen), Terraform es provisioning (crea la infraestructura misma — VPCs, subnets, instancias).
Conceptos clave
Qué problema resuelve la IaC (Infrastructure as Code)
Crear infraestructura "a mano" (clickeando en la consola de AWS) no escala: no queda registro de qué se creó ni por qué, no es reproducible, y no hay forma de versionar o revisar cambios. La IaC resuelve esto: se escribe la infraestructura como código declarativo, lo que permite:
- Versionar la infra en git (revisar diffs, hacer PRs, rollback)
- Reproducir entornos idénticos (dev/staging/prod)
- Automatizar cambios y testearlos antes de aplicarlos
- Gestionar el ciclo de vida completo (crear → modificar → destruir)
Herramientas: Terraform, Pulumi, AWS CloudFormation, OpenTofu. Hoy se usa Terraform.
Declarativo vs imperativo — el mismo salto mental que Ansible
| Enfoque | Se describe… | Ejemplo |
|---|---|---|
| Imperativo | Los pasos para llegar al estado | aws ec2 create-vpc --cidr ... |
| Declarativo | El estado deseado final | resource "aws_vpc" { ... } |
Terraform es declarativo: se declara "quiero una VPC con este CIDR" y Terraform calcula qué acciones ejecutar (crear, modificar o destruir) para que la realidad coincida con lo declarado. Mismo principio que state: present en Ansible, pero para infraestructura completa.
Los conceptos núcleo de Terraform
| Concepto | Qué es |
|---|---|
| HCL | HashiCorp Configuration Language — el lenguaje declarativo de los .tf |
| Provider | Plugin que traduce el código a llamadas a la API de una nube (aws, gcp, azure) |
| Resource | Un objeto de infraestructura a gestionar (aws_vpc, aws_instance, …) |
| State | Archivo (terraform.tfstate) que mapea el código a los objetos reales creados |
| Plan | El diff calculado: qué se va a crear/cambiar/destruir antes de tocar nada |
| Apply | Ejecuta el plan contra la API del provider |
El flujo: se escribe HCL → el provider traduce a llamadas API → se crean resources → Terraform guarda el resultado en el state.
Anatomía de un resource
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
tags = {
Name = "xfusion-vpc"
}
}
# ▲ ▲
# │ └── nombre LOCAL (referencia interna en Terraform: aws_vpc.main)
# └── TIPO de resource (lo define el provider aws)
Dos identificadores distintos que conviene no confundir:
- Nombre local (
main): cómo se referencia el resource dentro del código Terraform (aws_vpc.main.id). No existe en AWS. - Tag
Name(xfusion-vpc): el nombre que aparece en la consola de AWS. Es lo que la validación chequea.
★ El gotcha del lab: la validación busca el tag
Name = "xfusion-vpc". PonerName = "nautilus-vpc"(u otro) crea una VPC válida que falla la validación — elapplytiene éxito pero el requisito no se cumple. Es el mismo patrón "traducir el requisito literal" de los días 65/67/92/93.
Rangos de IP y CIDR — cómo elegir el bloque de la VPC
El lab dice "any IPv4 CIDR", pero en la práctica el rango no es arbitrario. Un bloque CIDR se escribe red/prefijo, ej. 10.0.0.0/16.
Qué significa el /N: es la cantidad de bits fijos (la parte de red). Los bits restantes (32 - N) son para hosts. A mayor /N, menos IPs.
El cálculo — una IPv4 tiene 32 bits en total (4 octetos × 8). El número de IPs sale de restar el prefijo a 32 y elevar 2 a esa potencia:
| CIDR | 32 − N (bits de host) |
2^(32−N) |
IPs totales |
|---|---|---|---|
/16 |
32 − 16 = 16 |
2^16 |
65 536 |
/20 |
32 − 20 = 12 |
2^12 |
4 096 |
/24 |
32 − 24 = 8 |
2^8 |
256 |
/28 |
32 − 28 = 4 |
2^4 |
16 |
Tabla con las IPs ya calculadas y lo usable en AWS:
| CIDR | Bits de host | IPs totales | IPs usables en AWS* | Uso típico |
|---|---|---|---|---|
/16 |
16 | 65 536 | 65 531 | VPC grande (lo más común) |
/20 |
12 | 4 096 | 4 091 | VPC mediana / subnet grande |
/24 |
8 | 256 | 251 | Subnet típica |
/28 |
4 | 16 | 11 | Subnet mínima permitida en AWS |
* AWS reserva 5 IPs por subnet (red, gateway, DNS, futuro, broadcast), por eso "usables" = totales − 5.
Regla mental rápida: cada +4 en el prefijo divide las IPs por 16. /16 = 65 536 → /20 = 4 096 → /24 = 256.
Restricciones de AWS para el CIDR de una VPC:
- El prefijo debe estar entre
/16(máx, 65 536 IPs) y/28(mín, 16 IPs). No se puede/8ni/30. - AWS recomienda usar rangos privados RFC 1918 (no enrutables en internet):
| Rango RFC 1918 | CIDR | Tamaño |
|---|---|---|
10.0.0.0 |
10.0.0.0/8 |
~16,7M IPs |
172.16.0.0 |
172.16.0.0/12 |
~1M IPs |
192.168.0.0 |
192.168.0.0/16 |
65 536 IPs |
- Técnicamente AWS acepta rangos públicos (como el
100.0.0.0/16del lab), pero es mala práctica: si esas IPs públicas pertenecen a un servicio real de internet, las instancias de la VPC no podrían alcanzarlo (el tráfico se enruta dentro de la VPC). Por eso en producción siempre RFC 1918.
Cómo elegir el rango en la práctica:
- Tamaño: estimar cuántas IPs hará falta (instancias + subnets + crecimiento).
/16da margen amplio; es el default seguro. - No solaparse: el CIDR no debe chocar con otras VPCs, on-premise, o redes con las que se hará peering/VPN. Dos redes con CIDRs solapados no pueden enrutar entre sí. Por eso se planifica un esquema (ej.
10.0.0.0/16para prod,10.1.0.0/16para staging). - Dejar espacio para subnets: la VPC se subdivide luego en subnets (
/24,/20, etc.), una por AZ. El/16de la VPC se reparte entre ellas.
Solapamiento de CIDRs — cómo saber si dos rangos chocan
El requisito de "no solaparse" (peering/VPN) exige poder calcular qué rango real cubre un CIDR y comparar dos bloques. El rango va de la primera IP (network address, bits de host en 0) a la última (bits de host en 1):
| CIDR | Primera IP | Última IP |
|---|---|---|
10.0.0.0/16 |
10.0.0.0 |
10.0.255.255 |
10.1.0.0/16 |
10.1.0.0 |
10.1.255.255 |
10.0.0.0/24 |
10.0.0.0 |
10.0.0.255 |
El truco rápido por octetos: el prefijo dice cuántos octetos quedan "fijos":
/8→ fija 1 octeto:10.x.x.x(todo lo que empiece con10.)/16→ fija 2 octetos:10.0.x.x/24→ fija 3 octetos:10.0.0.x
Por eso 10.0.0.0/16 y 10.1.0.0/16 no se solapan (difieren en el 2º octeto), pero 10.0.0.0/16 contiene a 10.0.5.0/24 (este último cae dentro de 10.0.x.x) → sí se solapan.
La regla de oro del solapamiento — dos rangos A y B chocan si:
Si uno empieza antes de que el otro termine, y viceversa, hay intersección. Ejemplo: 10.0.0.0/16 (10.0.0.0–10.0.255.255) vs 10.0.128.0/17 (10.0.128.0–10.0.255.255) → solapan (el segundo está enterito dentro del primero).
El caso no obvio — prefijos que no caen en borde de octeto (/20, /12):
Un /20 fija 2 octetos + 4 bits del tercero, así que los bloques avanzan de 16 en 16 en el 3er octeto:
10.0.0.0/20 → 10.0.0.0 – 10.0.15.255
10.0.16.0/20 → 10.0.16.0 – 10.0.31.255 ← siguiente bloque, NO 10.0.20.0
10.0.32.0/20 → 10.0.32.0 – 10.0.47.255
Acá es fácil equivocarse: 10.0.10.0/20 y 10.0.12.0/20 apuntan al mismo bloque (10.0.0.0/20) y solaparían — porque ambos caen en el primer tramo de 16. Por eso, salvo que se domine el cálculo, conviene:
- Alinear a bordes de octeto (
/8,/16,/24) — el solapamiento se ve "a ojo". - Usar una herramienta:
ipcalc 10.0.0.0/20muestra red, broadcast y rango; o calculadoras CIDR online. - AWS ayuda: no deja crear subnets que se solapen dentro de una misma VPC, ni peering entre VPCs con CIDRs solapados — falla con error explícito.
El provider — fijar la región en código
El provider aws necesita saber región y credenciales. En los labs de KodeKloud las credenciales ya vienen por variables de entorno (AWS_ACCESS_KEY_ID, etc.) y la región puede venir de AWS_DEFAULT_REGION. Por eso un main.tf sin provider block igual funciona — la región la toma del entorno.
Pero el requisito pide us-east-1 explícitamente. Fijarla en el provider block hace el código autocontenido y reproducible: no depende de qué variable de entorno tenga la máquina que corre terraform. Y como va en main.tf, respeta el "no crear otro .tf".
El state — el corazón de Terraform
Tras apply, Terraform escribe terraform.tfstate: un JSON que mapea cada resource del código al objeto real creado:
Para qué sirve el state:
- Saber qué gestiona Terraform: sin el state, Terraform no sabría que esa VPC es "suya".
- Calcular diffs: el próximo
plancompara código vs state vs realidad, y muestra solo lo que cambió (en vez de recrear todo). - Detectar drift: si alguien cambia algo en la consola de AWS por fuera de Terraform, el
planlo detecta como drift (deriva). Ahí se decide: incorporar el cambio al código, o dejar que Terraform sobrescriba y vuelva al estado declarado.
El state es sensible (puede contener secretos) y crítico. En equipos se guarda en un backend remoto (S3 + DynamoDB lock) en vez del archivo local, para compartirlo y evitar corrupción por escrituras concurrentes.
El ciclo de trabajo: init → plan → apply
| Comando | Qué hace |
|---|---|
terraform init |
Descarga el provider (aws) e inicializa el directorio. Obligatorio primero. |
terraform plan |
Muestra el diff (qué se crearía/cambiaría/destruiría) sin aplicar nada |
terraform apply |
Ejecuta el plan (pide confirmación yes); crea/modifica la infra real |
terraform destroy |
Destruye toda la infra gestionada por el state |
terraform apply corre un plan internamente y lo muestra antes de pedir confirmación — por eso en el output del lab se ve el plan completo seguido del prompt Enter a value: yes.
Pasos
- Abrir terminal en
/home/bob/terraform(click derecho en EXPLORER → Open in Integrated Terminal) - Crear
main.tfcon el provider (us-east-1) y el resourceaws_vpc(tagName = xfusion-vpc) terraform init— inicializar y descargar el provider awsterraform plan— revisar el diffterraform apply→ confirmar conyes- Verificar el
idde la VPC creada
Comandos / Código
main.tf (solución)
# /home/bob/terraform/main.tf
provider "aws" {
region = "us-east-1"
}
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
tags = {
Name = "xfusion-vpc"
}
}
El CIDR es libre ("any IPv4 CIDR").
10.0.0.0/16es un rango privado RFC 1918 convencional para VPCs (da 65 536 IPs). En el lab se usó100.0.0.0/16, que también es válido para una VPC aunque no sea un rango privado clásico.
Ejecutar el ciclo
terraform init # descarga el provider aws (primera vez)
terraform plan # revisar el diff
terraform apply # confirmar con 'yes'
Output real del primer apply (recortado):
# aws_vpc.main will be created
+ resource "aws_vpc" "main" {
+ arn = (known after apply)
+ cidr_block = "100.0.0.0/16"
+ id = (known after apply)
+ tags = {
+ "Name" = "nautilus-vpc" ← ⚠ debería ser "xfusion-vpc"
}
...
}
Plan: 1 to add, 0 to change, 0 to destroy.
Enter a value: yes
aws_vpc.main: Creating...
aws_vpc.main: Creation complete after 0s [id=vpc-741c865ee3603da53]
(known after apply): atributos que AWS asigna al crear (comoid,arn). Terraform no los conoce hasta que el recurso existe — por eso no aparecen en el plan.
★ Corregir el tag — update in-place, NO recrea
El primer apply creó la VPC con Name = "nautilus-vpc", pero el requisito pide xfusion-vpc. Al corregir el tag en main.tf y volver a apply, Terraform no destruye ni recrea la VPC: detecta en el state que el recurso ya existe y solo actualiza el tag en sitio (~ update in-place):
# main.tf corregido
resource "aws_vpc" "main" {
cidr_block = "100.0.0.0/16"
tags = {
Name = "xfusion-vpc" # ← era "nautilus-vpc"
}
}
# aws_vpc.main will be updated in-place
~ resource "aws_vpc" "main" {
id = "vpc-741c865ee3603da53" ← mismo id: NO se recrea
~ tags = {
~ "Name" = "nautilus-vpc" -> "xfusion-vpc"
}
}
Plan: 0 to add, 1 to change, 0 to destroy. ← 1 to change, no add/destroy
Enter a value: yes
aws_vpc.main: Modifying... [id=vpc-741c865ee3603da53]
aws_vpc.main: Modifications complete after 1s
Esto es exactamente el poder del state: como Terraform sabe que
aws_vpc.main↔vpc-741c..., un cambio de tag es un1 to change(no1 to add+ huérfana). Elidse mantiene. Si se hubiera cambiado elcidr_blocken cambio, sí sería destroy+create (el CIDR no es modificable en sitio) — por eso conviene revisar siempre elplanantes delyes.
Verificación
# Ver el id y atributos desde el state
terraform show
# O con el CLI de AWS
aws ec2 describe-vpcs --region us-east-1 \
--filters "Name=tag:Name,Values=xfusion-vpc" \
--query "Vpcs[].VpcId" --output text
Variantes (referencia)
VPC con DNS habilitado (común en producción)
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = { Name = "xfusion-vpc" }
}
Parametrizar con variables
variable "vpc_cidr" { default = "10.0.0.0/16" }
variable "vpc_name" { default = "xfusion-vpc" }
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
tags = { Name = var.vpc_name }
}
Exponer el id como output
Fijar versión del provider (buena práctica)
Troubleshooting
| Problema | Causa | Solución |
|---|---|---|
| La validación falla aunque la VPC se creó | El tag Name no es xfusion-vpc (ej. quedó nautilus-vpc) |
Corregir el tag Name y volver a apply (Terraform actualiza el tag) |
Error: configuring Terraform AWS Provider: no valid credential |
No hay credenciales en el entorno | Verificar AWS_ACCESS_KEY_ID/SECRET; en el lab ya vienen seteadas |
Error: Invalid AWS Region |
Región mal escrita o ausente | region = "us-east-1" en el provider block |
terraform: command ... Required plugins are not installed |
No se corrió terraform init |
Ejecutar terraform init antes de plan/apply |
Error: CIDR ... is not a valid CIDR block |
Bloque CIDR mal formado | Usar formato válido, ej. 10.0.0.0/16 |
El apply recrea la VPC en vez de actualizarla |
Se borró/corrompió terraform.tfstate |
No borrar el state; usar terraform import si se perdió el tracking |
| Quedó una VPC "huérfana" con el nombre viejo | Se creó con un nombre, luego se cambió el local name del resource | terraform destroy lo viejo o limpiar a mano en la consola AWS |
Error: error creating EC2 VPC: VpcLimitExceeded |
Límite de VPCs por región alcanzado | Borrar VPCs sin uso o pedir aumento de límite |
Conexión con días anteriores
- Toda la sección Ansible (82-93): Ansible configura servidores existentes (paquetes, servicios, archivos). Terraform crea la infraestructura donde esos servidores viven. Se complementan: Terraform provisiona la VPC + EC2, Ansible las configura.
- Declarativo (Días 87, 89):
state: presenten Ansible yresourceen Terraform son el mismo principio — declarar el estado deseado y dejar que la herramienta calcule las acciones. Elplande Terraform es como el--check/dry-run de Ansible. - Idempotencia (Día 84, 87): correr
applydos veces sin cambios →0 to add, 0 to change, 0 to destroy. El state es lo que hace posible esa idempotencia. - Patrón "traducir requisito literal" (Días 65, 67, 92, 93): el tag
Namedebe ser exactamentexfusion-vpc— unapplyexitoso con otro nombre no cumple el requisito.