Lab práctico · Semana 10: Entrega e implementación — CI/CD, infraestructura como código, APIs y herramientas
Infraestructura como código con Terraform y estado remoto
Qué vas a construir
Desde Cloud Shell, con Terraform, desplegarás una VPC con una subred, una regla de firewall para SSH vía IAP, una VM e2-micro sin IP pública y un bucket de Cloud Storage. El estado de Terraform vivirá en un bucket remoto con versionado, como en un equipo real. Practicarás el ciclo init → plan → apply, un cambio incremental, la detección de desviaciones (drift) y destroy.
flowchart LR
CS["Cloud Shell: ficheros .tf"] -->|"terraform plan / apply"| API["APIs de Google Cloud"]
CS -->|"lee y escribe el estado (con bloqueo)"| ST["gs://PROYECTO-tfstate (versionado)"]
API --> VPC["vpc-lab20 + subred"]
API --> FW["firewall: SSH desde IAP"]
API --> VM["vm-lab20 (e2-micro, sin IP pública)"]
API --> B["bucket de datos"]
Antes de empezar
- Proyecto dedicado (por ejemplo
pca-lab-20) con facturación. - Terraform viene instalado en Cloud Shell. Comprueba la versión:
terraform -version
- Variables y APIs:
export PROJECT_ID=$(gcloud config get-value project)
export REGION=europe-southwest1
export BUCKET_ESTADO=${PROJECT_ID}-tfstate
gcloud services enable compute.googleapis.com storage.googleapis.com
Terraform en Cloud Shell usa tus credenciales de usuario a través de Application Default Credentials, así que no necesitas claves.
Paso 1: bucket para el estado remoto
El fichero de estado es el «inventario» de Terraform. En local se perdería al cerrar Cloud Shell o no lo podrían usar tus compañeros. Lo guardamos en Cloud Storage siguiendo las recomendaciones de Google: versionado (para recuperar estados anteriores), uniform bucket-level access y public access prevention.
gcloud storage buckets create gs://$BUCKET_ESTADO \
--location=$REGION \
--uniform-bucket-level-access \
--public-access-prevention
gcloud storage buckets update gs://$BUCKET_ESTADO --versioning
Este bucket se crea fuera de Terraform a propósito: es el «huevo» del que nace todo lo demás.
Paso 2: escribe la configuración
mkdir -p ~/lab20 && cd ~/lab20
versions.tf: proveedor y backend. El bloque backend no admite variables, así que dejamos el nombre del bucket fuera y lo pasamos en init (partial configuration).
cat > versions.tf <<'EOF'
terraform {
required_version = ">= 1.5"
required_providers {
google = {
source = "hashicorp/google"
version = "~> 8.0"
}
}
backend "gcs" {
prefix = "lab20"
}
}
provider "google" {
project = var.project_id
region = var.region
}
EOF
variables.tf:
cat > variables.tf <<'EOF'
variable "project_id" {
type = string
description = "ID del proyecto"
}
variable "region" {
type = string
default = "europe-southwest1"
}
variable "zone" {
type = string
default = "europe-southwest1-a"
}
EOF
main.tf: los recursos.
cat > main.tf <<'EOF'
resource "google_compute_network" "vpc" {
name = "vpc-lab20"
auto_create_subnetworks = false
}
resource "google_compute_subnetwork" "subred" {
name = "subred-lab20"
ip_cidr_range = "10.20.0.0/24"
region = var.region
network = google_compute_network.vpc.id
private_ip_google_access = true
}
# SSH solo desde el rango de IAP TCP forwarding
resource "google_compute_firewall" "ssh_iap" {
name = "permitir-ssh-iap"
network = google_compute_network.vpc.id
source_ranges = ["35.235.240.0/20"]
target_tags = ["ssh"]
allow {
protocol = "tcp"
ports = ["22"]
}
}
resource "google_compute_instance" "vm" {
name = "vm-lab20"
machine_type = "e2-micro"
zone = var.zone
tags = ["ssh"]
boot_disk {
initialize_params {
image = "debian-cloud/debian-12"
}
}
# Sin bloque access_config: la VM no tiene IP pública
network_interface {
subnetwork = google_compute_subnetwork.subred.id
}
labels = {
entorno = "lab"
}
}
resource "google_storage_bucket" "datos" {
name = "${var.project_id}-datos-lab20"
location = var.region
uniform_bucket_level_access = true
public_access_prevention = "enforced"
force_destroy = true # solo para el lab: permite borrar con objetos dentro
}
EOF
outputs.tf y terraform.tfvars:
cat > outputs.tf <<'EOF'
output "vm_ip_interna" {
value = google_compute_instance.vm.network_interface[0].network_ip
}
output "bucket_datos" {
value = google_storage_bucket.datos.url
}
EOF
echo "project_id = \"$PROJECT_ID\"" > terraform.tfvars
Fíjate en las referencias entre recursos (google_compute_network.vpc.id): Terraform deduce de ellas el orden de creación (primero la VPC, después la subred y la VM).
Paso 3: init, fmt y validate
terraform init -backend-config="bucket=$BUCKET_ESTADO"
terraform fmt
terraform validate
init descarga el proveedor google y configura el backend gcs. fmt normaliza el formato y validate comprueba la sintaxis; en un pipeline real ambos se ejecutan en cada pull request.
Paso 4: plan y apply
terraform plan -out=tfplan
Lee la salida: debe decir Plan: 5 to add, 0 to change, 0 to destroy. Los + son recursos nuevos y (known after apply) son valores que solo se sabrán al crearlos.
Guardar el plan con -out garantiza que apply hace exactamente lo que revisaste:
terraform apply tfplan
Mira el estado remoto y la lista de recursos gestionados:
gcloud storage ls gs://$BUCKET_ESTADO/lab20/
terraform state list
terraform output
Verás default.tfstate en el bucket. Mientras un apply está en curso aparece también un fichero .tflock: es el bloqueo que impide que dos personas modifiquen el estado a la vez.
Paso 5: cambio incremental
Añade una etiqueta a la VM. Edita main.tf (con nano main.tf o con el Cloud Shell Editor) y cambia el bloque labels:
labels = {
entorno = "lab"
equipo = "plataforma"
}
terraform plan
Ahora el plan muestra ~ (modificación en el sitio, 1 to change). Hay cambios que Terraform no puede aplicar en el sitio y obligan a recrear el recurso (-/+, por ejemplo cambiar la imagen de arranque): léelo siempre antes de aplicar, porque recrear una VM significa perder su disco. Aplica:
terraform apply
(Sin plan guardado, apply calcula el plan y te pide confirmación: escribe yes.)
Paso 6: detecta una desviación (drift)
Alguien cambia la VM «a mano», fuera de Terraform:
gcloud compute instances update vm-lab20 --zone=europe-southwest1-a \
--update-labels=equipo=manual
terraform plan
El plan detecta la diferencia y propone devolver la etiqueta al valor del código (plataforma). Es la razón por la que, con IaC, todos los cambios pasan por el código: lo manual se sobrescribe en el siguiente apply.
Comprueba que funciona
terraform outputmuestra la IP interna de la VM y la URL del bucket.- La VM no tiene IP externa:
gcloud compute instances describe vm-lab20 --zone=europe-southwest1-a --format="value(networkInterfaces[0].accessConfigs)"sale vacío. - Puedes entrar por IAP:
gcloud compute ssh vm-lab20 --zone=europe-southwest1-a --tunnel-through-iap(si te lo pide, habilita la API de IAP congcloud services enable iap.googleapis.com). - En el bucket de estado,
gcloud storage ls -a gs://$BUCKET_ESTADO/lab20/muestra varias generaciones dedefault.tfstategracias al versionado.
Limpieza
Primero destruye lo gestionado por Terraform y después el bucket del estado:
cd ~/lab20
terraform destroy
gcloud storage rm -r gs://$BUCKET_ESTADO
destroy muestra un plan con 5 to destroy y pide confirmación. El segundo comando borra el bucket del estado con todas sus versiones. Comprueba que no queda nada con gcloud compute instances list y gcloud storage ls.
Preguntas para pensar como arquitecto
¿Por qué no se debe guardar el fichero de estado en Git?
Porque puede contener secretos en claro (contraseñas generadas, claves), porque Git no ofrece bloqueo (dos apply simultáneos lo corromperían) y porque es fácil trabajar con una copia desactualizada. El backend gcs resuelve las tres cosas: control de acceso con IAM (y cifrado con CMEK si hace falta), bloqueo y una única fuente de verdad, con versionado para recuperarse de errores.
Tu organización tiene 40 equipos que necesitan redes con la misma configuración de seguridad. ¿Cómo lo organizas?
Con un módulo de Terraform versionado (por ejemplo, basado en los módulos del Cloud Foundation Toolkit) que encapsula las buenas prácticas, publicado en un repositorio interno o en un catálogo (Application Design Center). Cada equipo lo usa con sus variables, en estados separados por equipo y entorno. Las políticas de organización y la validación con gcloud beta terraform vet en el pipeline impiden desviarse de lo permitido.
¿Cuándo recomendarías Infrastructure Manager en lugar de ejecutar Terraform en Cloud Shell o en un pipeline propio?
Cuando quieres Terraform sin gestionar los ejecutores ni el estado: Infrastructure Manager ejecuta Terraform en Cloud Build, guarda el estado y las revisiones en Cloud Storage, ofrece previews y deja registro de cada despliegue con IAM de Google Cloud. Un pipeline propio (Cloud Build o GitHub Actions con Workload Identity Federation) tiene sentido si ya tienes ese flujo y necesitas más control.
En el paso 6, ¿qué habría pasado si el cambio manual hubiera sido crítico (un parche urgente durante un incidente)?
El siguiente apply lo habría revertido sin avisar a nadie. La buena práctica es llevar cualquier cambio de emergencia al código lo antes posible (o hacerlo directamente vía pipeline) y revisar el plan en busca de desviaciones antes de aplicar. Algunas organizaciones ejecutan terraform plan periódicamente solo para detectar drift y alertar.