Lab práctico · Semana 2: Cómputo: Compute Engine, MIG, GKE, Cloud Run y cuándo usar cada uno
Cloud Run: servicio, revisiones con reparto de tráfico, job y función
Qué vas a construir
Un servicio de Cloud Run desplegado desde código fuente, con una cuenta de servicio propia, dos revisiones con reparto de tráfico (canary), ajustes de concurrencia y escalado, un job que se ejecuta hasta terminar y una Cloud Run function HTTP.
flowchart LR
DEV["Código en Cloud Shell"] -->|"gcloud run deploy --source"| CB["Cloud Build + Artifact Registry"]
CB --> R1["Revisión 1: 90 % del tráfico"]
CB --> R2["Revisión 2: 10 % del tráfico"]
U["Usuarios"] --> S["Servicio hola, europe-southwest1"]
S --> R1
S --> R2
J["Job informe: 3 tareas"] --> X["Se ejecuta y termina"]
F["Función saludo"] --> S2["Servicio de Cloud Run"]
Antes de empezar
- Labs 01 y 02 hechos.
- Proyecto nuevo y APIs:
export PROJECT_ID="pca-lab-04-$RANDOM"
export REGION="europe-southwest1"
export BILLING_ID="$(gcloud billing accounts list --format='value(ACCOUNT_ID)' --limit=1)"
gcloud projects create "$PROJECT_ID" --name="pca-lab-04"
gcloud billing projects link "$PROJECT_ID" --billing-account="$BILLING_ID"
gcloud config set project "$PROJECT_ID"
gcloud config set run/region "$REGION"
gcloud services enable run.googleapis.com cloudbuild.googleapis.com \
artifactregistry.googleapis.com compute.googleapis.com
Paso 1: Crea una cuenta de servicio para el servicio
Qué y por qué: por defecto, Cloud Run usaría la cuenta predeterminada de Compute Engine, que suele tener demasiados permisos. Buena práctica: una cuenta por servicio, aquí sin ningún rol porque la aplicación no llama a otras APIs.
gcloud iam service-accounts create hola-sa --display-name="Servicio hola"
export SA="hola-sa@${PROJECT_ID}.iam.gserviceaccount.com"
Además, el despliegue desde código usa Cloud Build, que por defecto compila con la cuenta de servicio predeterminada de Compute Engine (por eso habilitaste también la API de Compute Engine, que la crea). La documentación indica concederle el rol Cloud Run Builder (roles/run.builder); tarda un par de minutos en propagarse:
export PROJECT_NUMBER="$(gcloud projects describe "$PROJECT_ID" --format='value(projectNumber)')"
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="serviceAccount:${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" \
--role="roles/run.builder" --condition=None
Paso 2: Despliega un servicio desde código fuente
Qué y por qué: con --source, Cloud Run construye la imagen con buildpacks en Cloud Build, la guarda en Artifact Registry y la despliega. No escribes ningún Dockerfile.
mkdir -p ~/hola && cd ~/hola
cat > main.py <<'EOF'
import os
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hola():
version = os.environ.get("VERSION", "v1")
revision = os.environ.get("K_REVISION", "local")
return f"Hola desde {version} (revisión {revision})\n"
if __name__ == "__main__":
app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))
EOF
cat > requirements.txt <<'EOF'
flask
gunicorn
EOF
cat > Procfile <<'EOF'
web: gunicorn --bind :$PORT --workers 1 --threads 8 main:app
EOF
gcloud run deploy hola --source=. \
--service-account="$SA" \
--set-env-vars=VERSION=v1 \
--allow-unauthenticated
Si te pregunta si quieres crear un repositorio de Artifact Registry, responde Y. Al terminar obtendrás una URL https://hola-....run.app:
export URL="$(gcloud run services describe hola --format='value(status.url)')"
curl "$URL"
Por consola: Cloud Run → hola: pestañas Métricas, Revisiones, Registros y YAML.
Paso 3: Ajusta concurrencia y escalado
Qué y por qué: la concurrencia decide cuántas peticiones atiende cada instancia a la vez; max-instances protege el coste y los sistemas de detrás; min-instances=0 permite escalar a cero.
gcloud run services update hola \
--concurrency=50 --min-instances=0 --max-instances=5 \
--cpu=1 --memory=512Mi --timeout=60
gcloud run services describe hola --format="yaml(spec.template.metadata.annotations, spec.template.spec.containerConcurrency)"
Cada actualización de configuración crea una nueva revisión. Compruébalo:
gcloud run revisions list --service=hola
Deja el servicio sin tráfico unos minutos y mira en Métricas → Número de instancias cómo baja a cero. La siguiente petición tardará algo más (arranque en frío).
Paso 4: Canary con reparto de tráfico
Qué y por qué: publicas una versión nueva sin enviarle tráfico, la pruebas por una URL con etiqueta y luego le das un 10 %. Si falla, vuelves atrás al instante, porque las revisiones son inmutables.
gcloud run deploy hola --source=. \
--service-account="$SA" \
--set-env-vars=VERSION=v2 \
--no-traffic --tag=verde
gcloud run services describe hola --format="value(status.traffic)"
La revisión nueva tiene su propia URL https://verde---hola-....run.app (aparece en la salida del despliegue). Pruébala:
export URL_VERDE="$(gcloud run services describe hola --format=json | python3 -c 'import sys,json; t=json.load(sys.stdin)["status"]["traffic"]; print([x["url"] for x in t if x.get("tag")=="verde"][0])')"
curl "$URL_VERDE"
Envía el 10 % del tráfico a la revisión etiquetada y comprueba el reparto:
gcloud run services update-traffic hola --to-tags=verde=10
for i in $(seq 1 20); do curl -s "$URL"; done | sort | uniq -c
Verás aproximadamente 2 respuestas «v2» de cada 20. Para completar el despliegue o para volver atrás:
gcloud run services update-traffic hola --to-latest # todo a v2
# gcloud run services update-traffic hola --to-revisions=NOMBRE_REVISION_V1=100 # rollback
Paso 5: Un Cloud Run job
Qué y por qué: un job ejecuta tareas que terminan, sin escuchar peticiones. Usarás la imagen de ejemplo de Google, que imprime información de cada tarea y termina.
gcloud run jobs create informe \
--image=us-docker.pkg.dev/cloudrun/container/job:latest \
--tasks=3 --max-retries=1 --task-timeout=5m \
--service-account="$SA"
gcloud run jobs execute informe --wait
gcloud run jobs executions list --job=informe
Cada tarea recibe CLOUD_RUN_TASK_INDEX (0, 1, 2) y CLOUD_RUN_TASK_COUNT (3), lo que permite repartir el trabajo (por ejemplo, un tercio de los ficheros cada una). Mira los registros en Cloud Run → Jobs → informe → Registros. Para programarlo, usarías Cloud Scheduler (no lo haces aquí).
Paso 6: Una Cloud Run function
Qué y por qué: para una pieza pequeña de lógica, escribes solo la función. Se despliega como un servicio de Cloud Run.
mkdir -p ~/saludo && cd ~/saludo
cat > main.py <<'EOF'
import functions_framework
@functions_framework.http
def saludo(request):
nombre = request.args.get("nombre", "mundo")
return f"Hola, {nombre}\n"
EOF
cat > requirements.txt <<'EOF'
functions-framework
EOF
gcloud run deploy saludo --source=. \
--function=saludo --base-image=python312 \
--service-account="$SA" --allow-unauthenticated
curl "$(gcloud run services describe saludo --format='value(status.url)')?nombre=Borja"
Si la imagen base python312 no estuviera disponible, consulta la lista de runtimes en la documentación de Cloud Run functions y usa otra versión de Python soportada.
Comprueba que funciona
curl "$URL"responde.gcloud run revisions list --service=holamuestra varias revisiones y el reparto 90/10 antes de--to-latest.- La ejecución del job aparece como completada con 3 tareas correctas.
- La función responde «Hola, Borja».
Limpieza
gcloud projects delete "$PROJECT_ID"
O, recurso a recurso:
gcloud run services delete hola --quiet
gcloud run services delete saludo --quiet
gcloud run jobs delete informe --quiet
gcloud artifacts repositories delete cloud-run-source-deploy --location="$REGION" --quiet
gcloud iam service-accounts delete "$SA" --quiet
Preguntas para pensar como arquitecto
1. La primera petición de la mañana tarda 4 segundos y el cliente se queja. ¿Qué opciones tienes y qué cuestan?
Configurar --min-instances=1 (o más) para mantener instancias calientes, que se pagan aunque no haya tráfico; reducir el tiempo de arranque del contenedor (imagen más pequeña, menos inicialización); o activar startup CPU boost, que da más CPU durante el arranque. Es un trade-off latencia frente a coste.
2. El servicio se conecta a una base de datos que admite como máximo 100 conexiones. ¿Qué parámetros de Cloud Run ajustas?
--max-instances y la concurrencia, junto con el tamaño del pool de conexiones por instancia, para que instancias máximas × conexiones por instancia no supere 100. Si hace falta más escala, un proxy o pool de conexiones intermedio.
3. El servicio debe llamar a una VM con IP privada en la VPC. ¿Qué configuras?
Direct VPC egress con --network, --subnet y --vpc-egress=private-ranges-only, y reglas de firewall que permitan el tráfico desde la subred. Un Serverless VPC Access connector también funcionaría, pero es la opción anterior y tiene coste fijo.
4. ¿Cuándo convertirías el job en un servicio o en un MIG?
Si tiene que responder a peticiones, en un servicio. Si necesita horas de ejecución continua con hardware especial, control del SO o GPUs muy concretas, podría encajar mejor en Compute Engine (quizá con Spot VMs) o en GKE. Para tareas que terminan en menos de 7 días por tarea, Cloud Run jobs suele ser la opción más simple.