← Todo el material
Curso Avanzado 23 de agosto de 2026

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 main es 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 GCPRol en el deployRecurso Terraform
Artifact RegistryGuarda la imagen Dockergoogle_artifact_registry_repository
Cloud RunCorre el contenedor serverlessgoogle_cloud_run_v2_service
Secret ManagerGuarda los secretos cifradosgoogle_secret_manager_secret
IAMPermisos entre identidadesgoogle_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_secret crea solo el contenedor; el valor real (la versión) lo subes con gcloud secrets versions add fuera de Terraform, para que la llave nunca aterrice en el state ni en el repo.
  • min_instance_count = 0 es la perilla del escalado a cero. Es lo que hace a Cloud Run barato para un agente con tráfico intermitente — el paralelo directo del scaledown_window que 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 secretAccessor que 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_secret crea el contenedor; el valor se sube aparte con gcloud. Si lo escribes en el HCL, termina en el state — y el state no está pensado para guardar llaves.
  • Olvidar id-token: write en 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_protection prendido y frustrarte. Es una salvaguarda contra borrados accidentales; en un servicio de práctica que recreas seguido, ponlo en false a 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

DeployGCPTerraformCloud RunGitHub ActionsIaC

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.