Deploy de Agentes — Módulo 11: deploy end-to-end en GCP con Terraform y GitHub Actions
Undécimo módulo del Curso de Deploy de Agentes: desplegar el agente en Google Cloud de punta a punta. Artifact Registry para la imagen, Cloud Run serverless para correrla, Secret Manager para los secretos, y autenticación sin llaves desde GitHub Actions con Workload Identity Federation. Terraform real y el pipeline de deploy que en cada push a main construye, sube y despliega.
☰ Índice del contenido
Undécimo módulo del Curso de Deploy de Agentes, y el que Felipe pidió que fuera el corazón del bloque de nube. En el Módulo 10 aprendiste el lenguaje: provider, resource, state, el ciclo plan/apply. Practicaste con un bucket en AWS para entender el flujo. Hoy lo usamos de verdad, en GCP (Google Cloud Platform), y de punta a punta: tu agente contenerizado (Módulo 5) sube a un registro, corre serverless, lee sus secretos de un gestor, y todo se dispara solo con un push a main — sin una sola llave JSON dando vueltas. Este es el módulo donde el agente deja tu máquina y se vuelve un servicio con URL pública.
La tesis del módulo
Un deploy profesional en la nube no es “subir un archivo”: es cuatro piezas que encajan — registro de imagen, ejecución serverless, gestor de secretos y una identidad sin llaves — todas descritas en Terraform y disparadas por Git. Cuando está bien hecho, mergear a
maines todo el “deploy” que ves.
Las cuatro piezas de GCP
Antes del código, el mapa de servicios. Cada uno resuelve exactamente un problema y conviene glosarlos:
- Artifact Registry: el registro de imágenes de Google. Es donde vive tu imagen Docker después de construirla — un almacén privado y versionado de contenedores. Cloud Run tira las imágenes desde aquí.
- Cloud Run: el servicio que corre tu contenedor sin que administres servidores (serverless). Le das una imagen, él la ejecuta detrás de una URL HTTPS, escala solo con el tráfico y — clave para el costo — escala a cero: sin peticiones, no hay contenedor corriendo y no pagas cómputo.
- Secret Manager: el gestor de secretos. Guarda tu
OPENAI_API_KEY, tu token de base de datos, etc., cifrados y con permisos finos. Cloud Run los inyecta como variables de entorno en el arranque, sin que el secreto toque nunca tu código ni tu imagen. - IAM (Identity and Access Management): el sistema de permisos de GCP. Define quién (qué identidad) puede hacer qué sobre cada recurso. Es lo que permite que Cloud Run lea un secreto, y lo que permite que GitHub Actions despliegue.
| Servicio GCP | Rol en el deploy | Recurso Terraform |
|---|---|---|
| Artifact Registry | Guarda la imagen Docker | google_artifact_registry_repository |
| Cloud Run | Corre el contenedor serverless | google_cloud_run_v2_service |
| Secret Manager | Guarda los secretos cifrados | google_secret_manager_secret |
| IAM | Permisos entre identidades | google_secret_manager_secret_iam_member |
Autenticación sin llaves: Workload Identity Federation
El primer instinto para que GitHub Actions despliegue en GCP es crear una service account (cuenta de servicio: una identidad no-humana), bajar su JSON key (un archivo con credenciales de larga vida) y guardarlo como secreto en GitHub. Funciona. Y es un riesgo permanente: ese JSON es una llave maestra que no expira, y si se filtra — en un log, en un repo, en una captura — alguien tiene tu nube.
Workload Identity Federation (WIF, federación de identidad de cargas de trabajo) elimina ese archivo. La idea: GCP confía directamente en la identidad que GitHub le da a cada workflow. Cuando corre tu Action, GitHub emite un token OIDC de corta vida que dice “soy el repo brisef/agente, rama main”; GCP lo verifica contra una regla que tú configuraste y, si calza, entrega credenciales temporales. Sin JSON. Sin secreto de larga vida. La acción oficial google-github-actions/auth orquesta todo ese intercambio.
GitHub Actions (workflow en main)
│ token OIDC de corta vida
│ "soy repo=brisef/agente, ref=main"
▼
┌──────────────────────────────────────┐
│ GCP: Workload Identity Pool │
│ verifica el token contra la regla │
└───────────────────┬──────────────────┘
▼ credenciales temporales
┌──────────────────────────────────────┐
│ Service Account (impersonada) │
│ con permisos IAM para desplegar │
└──────────────────────────────────────┘
El Terraform de la infraestructura
Este es el HCL real que crea las cuatro piezas. Provider google, recursos reales, encadenados con el patrón tipo.nombre.atributo del módulo anterior. Léelo de arriba a abajo: es una sola historia.
# main.tf
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "~> 6.0"
}
}
# State remoto y cifrado, JAMÁS en git (Módulo 10).
backend "gcs" {
bucket = "brisef-tfstate"
prefix = "agente/deploy"
}
}
provider "google" {
project = var.project_id
region = var.region
}
variable "project_id" { type = string }
variable "region" { type = string, default = "us-central1" }
# 1) El registro donde vive la imagen Docker del agente.
resource "google_artifact_registry_repository" "agente" {
location = var.region
repository_id = "agente"
format = "DOCKER"
}
# 2) El secreto: solo el CONTENEDOR del secreto, no su valor.
# El valor (la versión) se sube aparte, fuera de Terraform,
# para no dejarlo en el state ni en git.
resource "google_secret_manager_secret" "openai_key" {
secret_id = "openai-api-key"
replication {
auto {}
}
}
# 3) El servicio Cloud Run que corre la imagen.
resource "google_cloud_run_v2_service" "agente" {
name = "agente"
location = var.region
deletion_protection = false
template {
scaling {
min_instance_count = 0 # escala a CERO: sin tráfico, no pagas cómputo
max_instance_count = 5
}
containers {
# La imagen vive en Artifact Registry; el tag lo pone el pipeline.
image = "${var.region}-docker.pkg.dev/${var.project_id}/agente/agente:latest"
ports {
container_port = 8080 # Cloud Run inyecta $PORT; escúchalo en tu app
}
# El secreto entra como variable de entorno, leído de Secret Manager.
# El contenedor nunca ve el valor hasta el arranque.
env {
name = "OPENAI_API_KEY"
value_source {
secret_key_ref {
secret = google_secret_manager_secret.openai_key.secret_id
version = "latest"
}
}
}
}
}
}
# 4) Permiso para que la identidad del servicio LEA el secreto.
resource "google_secret_manager_secret_iam_member" "run_lee_secreto" {
secret_id = google_secret_manager_secret.openai_key.secret_id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${google_cloud_run_v2_service.agente.template[0].service_account}"
}
# La URL pública del servicio, como output.
output "url_agente" {
value = google_cloud_run_v2_service.agente.uri
}
Tres cosas que separan esto de un tutorial de juguete:
- El secreto se parte en dos. El
google_secret_manager_secretcrea solo el contenedor; el valor real (la versión) lo subes congcloud secrets versions addfuera de Terraform, para que la llave nunca aterrice en el state ni en el repo. min_instance_count = 0es la perilla del escalado a cero. Es lo que hace a Cloud Run barato para un agente con tráfico intermitente — el paralelo directo delscaledown_windowque viste en GPU Módulo 11 cuando lo que desplegabas era el modelo, no el agente.- El IAM es explícito. Cloud Run no lee el secreto “porque sí”: hay un
secretAccessorque se lo permite. Permisos mínimos, declarados.
El pipeline de deploy
Ahora el otro archivo, el que ata todo a Git: .github/workflows/deploy.yml. En cada push a main se autentica con WIF (sin JSON), construye la imagen, la sube a Artifact Registry y aplica el Terraform para actualizar el servicio.
# .github/workflows/deploy.yml
name: Deploy a GCP
on:
push:
branches: [main]
# WIF necesita este permiso para pedir el token OIDC a GitHub.
permissions:
contents: read
id-token: write
env:
PROJECT_ID: brisef-agente
REGION: us-central1
REPO: agente
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Autenticación SIN llaves: el intercambio OIDC del diagrama de arriba.
- id: auth
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.WIF_PROVIDER }}
service_account: ${{ secrets.WIF_SERVICE_ACCOUNT }}
- uses: google-github-actions/setup-gcloud@v2
# Deja que Docker se autentique contra Artifact Registry.
- name: Configurar Docker para Artifact Registry
run: gcloud auth configure-docker ${{ env.REGION }}-docker.pkg.dev --quiet
# Build y push de la imagen, taggeada con el SHA del commit (trazable).
- name: Build y push de la imagen
run: |
IMG="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/agente:${GITHUB_SHA}"
docker build -t "$IMG" .
docker push "$IMG"
# Actualiza el servicio de Cloud Run con la imagen recién subida.
# (Alternativa a un terraform apply completo: rápido para el día a día.)
- name: Deploy a Cloud Run
run: |
gcloud run deploy agente \
--image "${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO}/agente:${GITHUB_SHA}" \
--region "$REGION" \
--project "$PROJECT_ID"
Un matiz honesto: aquí el gcloud run deploy actualiza la imagen del servicio que Terraform ya creó. Se usa terraform apply cuando cambias la forma de la infra (agregar un secreto, cambiar el escalado); se usa gcloud run deploy cuando solo cambia la imagen, que es el día a día. Muchos equipos hacen ambos: Terraform gobierna la infra, el pipeline empuja la imagen nueva.
El flujo completo, de merge a producción
MERGE A MAIN
│
▼
┌────────────────────────────┐
│ GitHub Actions │
│ auth WIF (sin JSON) │
└─────────────┬──────────────┘
▼
┌────────────────────────────┐
│ build + push de la imagen │
└─────────────┬──────────────┘
▼
┌────────────────────────────┐
│ Artifact Registry │ guarda la imagen
└─────────────┬──────────────┘
▼
┌────────────────────────────┐
│ apply / run deploy │
└─────────────┬──────────────┘
▼
┌────────────────────────────┐ ┌──────────────────┐
│ Cloud Run (serverless) │◄───────┤ Secret Manager │
│ URL pública, escala a 0 │ lee │ OPENAI_API_KEY │
└────────────────────────────┘ └──────────────────┘
Errores comunes
- Bajar una JSON key para GitHub Actions. Es la llave que no expira y que se filtra en un log. Usa Workload Identity Federation: sin archivo, credenciales de corta vida, confianza atada a tu repo y tu rama.
- Poner el valor del secreto en Terraform. El
google_secret_manager_secretcrea el contenedor; el valor se sube aparte congcloud. Si lo escribes en el HCL, termina en el state — y el state no está pensado para guardar llaves. - Olvidar
id-token: writeen los permisos del workflow. Sin ese permiso, GitHub no emite el token OIDC y la autenticación WIF falla con un error críptico. Es el olvido número uno al configurar WIF. - No escuchar el puerto correcto en tu app. Cloud Run inyecta la variable
$PORT(8080 por defecto) y espera que tu servidor escuche ahí. Si tu agente escucha en otro puerto fijo, el deploy “sube” pero el health check falla. - Dejar
deletion_protectionprendido y frustrarte. Es una salvaguarda contra borrados accidentales; en un servicio de práctica que recreas seguido, ponlo enfalsea conciencia — pero en producción real, préndelo.
La frase para llevarse
“Cuando la infra vive en Terraform y el deploy vive en Actions con identidad sin llaves, mergear a main es todo el deploy que necesitas ver — y todo lo demás queda auditado.”
Qué viene
Módulo 12: Railway y la comparación de plataformas. Acabas de ver el camino de máximo control — IaC en GCP, el mundo de la empresa grande. El próximo módulo muestra el camino simple, el que Felipe usa en sus propias apps: desplegar en un PaaS directo desde el repo, sin escribir una línea de Terraform. Y la tabla honesta de cuándo gana cada uno.
Material relacionado
Preguntas sobre este curso
Regístrate gratis para hacer preguntas y guardar tu progreso.
✉ Entérate de lo nuevo
Recibe un aviso cuando publique nuevos cursos, workshops o investigaciones. Sin spam.