Cloud Run vs GKE Autopilot: cómo decidimos dónde desplegar un microservicio nuevo
Cada vez que arrancamos un microservicio nuevo en GCP, la primera pregunta técnica que resolvemos no es de lenguaje ni de framework, es de destino: ¿Cloud Run o GKE Autopilot? Las dos plataformas prometen quitar de en medio la gestión de infraestructura -ningún nodo que parchear, ningún cluster que dimensionar a mano- pero parten de un modelo mental distinto, y confundirlas lleva a decisiones que se pagan meses después, cuando el servicio ya tiene tráfico real y cambiar de plataforma deja de ser trivial.
Este artículo recoge los criterios que usamos en la práctica para decidir, no una comparación de features en abstracto.
El modelo mental: contenedor sin estado vs cluster de Kubernetes real
Cloud Run ejecuta contenedores individuales sin que el equipo tenga que pensar en Kubernetes en ningún momento. Se define un contenedor, cuántos recursos necesita, un rango de instancias, y la plataforma se encarga del resto: enrutamiento, TLS, escalado por request, reintentos de despliegue. No hay kubectl, no hay manifiestos de Deployment ni de Service, no hay que decidir un nodeSelector ni una PodDisruptionBudget.
gcloud run deploy pedidos-api \
--image gcr.io/mi-proyecto/pedidos-api:latest \
--region us-central1 \
--min-instances 0 \
--max-instances 20 \
--memory 512Mi \
--cpu 1 \
--allow-unauthenticated
GKE Autopilot sigue siendo Kubernetes de verdad: los mismos manifiestos YAML, la misma API, los mismos objetos (Deployment, Service, HPA, Ingress) que un cluster GKE Standard, solo que Google administra los nodos y factura por los recursos que cada pod solicita en lugar de por la capacidad del nodo completo. Si el equipo ya conoce Kubernetes, migrar cargas existentes hacia Autopilot es casi mecánico. Si no lo conoce, Autopilot no elimina esa curva de aprendizaje, solo elimina la parte de operar los nodos.
apiVersion: apps/v1
kind: Deployment
metadata:
name: pedidos-api
spec:
replicas: 2
selector:
matchLabels:
app: pedidos-api
template:
metadata:
labels:
app: pedidos-api
spec:
containers:
- name: pedidos-api
image: gcr.io/mi-proyecto/pedidos-api:latest
resources:
requests:
cpu: "500m"
memory: "512Mi"
Esta diferencia de modelo mental es la que más pesa al principio: Cloud Run está pensado para que un equipo pequeño despliegue rápido sin adoptar Kubernetes; Autopilot está pensado para que un equipo que ya opera en Kubernetes deje de operar nodos.
Cold starts y el rango min/max de instancias
En Cloud Run, cuando una revisión escala a cero, la siguiente request paga el costo de arrancar un contenedor nuevo: descomprimir la imagen, inicializar el runtime, ejecutar cualquier código de arranque de la aplicación. Ese costo varía según el lenguaje y el tamaño de la imagen -un binario de Go compilado arranca de forma distinta a una aplicación de JVM con un classpath grande- y es la razón por la que fijar --min-instances en un valor mayor a cero es la primera palanca que tocamos cuando un servicio tiene tráfico constante y la latencia de la primera request importa. Con min-instances en 1 o más, Cloud Run mantiene esa cantidad de instancias corriendo permanentemente y el escalado a cero deja de aplicar para ese piso.
En GKE Autopilot, el equivalente de "cold start" es distinto: no es arrancar un contenedor desde cero en cada request, es el tiempo que tarda el scheduler en programar un pod nuevo cuando el Horizontal Pod Autoscaler decide escalar hacia arriba, y ese tiempo depende de si Autopilot ya tiene capacidad de cómputo lista o si necesita aprovisionarla. Para un Deployment con un número mínimo de réplicas fijo, ese costo no aparece en el camino de cada request como en Cloud Run: los pods ya están corriendo y el HPA solo entra en juego cuando el tráfico sube por encima de lo que las réplicas actuales pueden absorber.
Dicho de otro modo: en Cloud Run el escalado a cero es la opción por defecto y hay que decidir explícitamente no usarla; en Autopilot mantener réplicas mínimas corriendo es la opción por defecto y escalar a cero requiere configuración adicional (por ejemplo con KEDA) que en la práctica usamos poco porque la mayoría de las cargas que llevamos a Kubernetes ya tienen tráfico constante.
Control sobre networking y contenedores sidecar
Cloud Run soporta despliegues multi-contenedor -un contenedor principal más uno o varios sidecars que comparten el mismo ciclo de vida y el mismo espacio de red- lo cual cubre casos como un proxy de autenticación o un agente de métricas corriendo junto al servicio. Pero el control de red sigue siendo el de una plataforma administrada: para hablar con recursos dentro de una VPC (una instancia de Cloud SQL por IP privada, un servicio interno) hace falta un conector de VPC Access, y no hay acceso a primitivas de red de bajo nivel como NetworkPolicies o control fino sobre el CNI.
GKE Autopilot da acceso a los objetos de networking nativos de Kubernetes -Service, Ingress, NetworkPolicy, Gateway API- y a un espacio de nombres real donde correr un service mesh como Istio o Cloud Service Mesh si el proyecto lo necesita. La contrapartida es que Autopilot impone restricciones deliberadas sobre lo que un pod puede hacer: no permite contenedores privilegiados, restringe el uso de hostNetwork y hostPort, y no admite DaemonSets de uso general de la misma forma que un cluster Standard, precisamente porque Google necesita mantener garantías de aislamiento entre las cargas de distintos clientes que comparten el mismo pool de nodos administrado. Un equipo que necesita una DaemonSet real -por ejemplo para un agente de logging que corre en cada nodo con acceso directo al filesystem del host- se topa con esa restricción rápido, y ahí GKE Standard vuelve a la conversación.
El punto en el que un cluster empieza a justificarse
Para un microservicio nuevo, sin estado, con tráfico HTTP y sin dependencias exóticas de red, casi siempre empezamos en Cloud Run: el tiempo hasta el primer despliegue es menor, no hay YAML que mantener, y el costo en tráfico bajo o intermitente es difícil de igualar porque se paga por uso real, no por capacidad reservada.
La conversación cambia cuando aparece alguna de estas condiciones, no una sola sino que suelen aparecer juntas: el número de servicios relacionados crece lo suficiente como para que compartir un mismo espacio de red y un mismo service mesh entre ellos valga más que desplegarlos aislados; el equipo necesita primitivas de Kubernetes que Cloud Run no expone, como Jobs con dependencias entre pasos, StatefulSets para cargas con estado, o CRDs de un operador específico; o el equipo ya tiene experiencia operando Kubernetes en otros proyectos y estandarizar reduce carga cognitiva más de lo que cuesta operar el cluster adicional. Ninguna de esas condiciones aparece el primer día de un microservicio nuevo, y por eso rara vez arrancamos ahí directamente: es más barato migrar de Cloud Run a Autopilot cuando el proyecto lo pide -las imágenes de contenedor no cambian, solo cambia el manifiesto de despliegue- que operar un cluster desde el día uno para un servicio que todavía no sabe cuánto tráfico va a tener.
El criterio que aplicamos
La pregunta que nos hacemos antes de elegir no es "¿cuál plataforma es mejor?" sino "¿qué necesita este servicio que la otra plataforma no puede darle de forma nativa?". Si la respuesta es "nada todavía", Cloud Run. Si la respuesta involucra networking compartido entre varios servicios, primitivas de Kubernetes específicas, o un equipo que ya opera clusters y quiere un solo modelo operativo para todo, Autopilot. Elegir Autopilot por adelantado para un servicio que todavía no lo necesita es la forma más común en la que un equipo termina manteniendo YAML de Kubernetes para un problema que un solo comando de gcloud run deploy hubiera resuelto igual de bien.