Modernizar más de 60 GCP Cloud Functions: de Gen1 a Gen2, builds a la mitad
Cada pipeline de Cloud Functions era un poco diferente y la mayoría eran lentos. Los estandaricé todos, reduje el tiempo de checkout en cerca de 80% y el tiempo de build a la mitad, y establecí una ruta segura de Gen1 a Gen2.
Una migración por fases; el seguimiento en producción también revisó la service account del runtime.
Contribución de un vistazo
- Mi responsabilidad
- Estandarización de pipelines de funciones, migración de runtime y seguimiento de identidad.
- Alcance
- Entrega por fases en desarrollo/staging y una operación posterior en producción como etapa distinta.
- Colaboración y límites
- Trabajé en la infraestructura y la ruta de despliegue; una épica de planificación no es prueba de que cada función en producción haya completado la migración.
- Resultado
- Trabajo de despliegue estandarizado y un seguimiento en producción que fijó la identidad del runtime tras un cambio implícito de service account.
Una flota de Cloud Functions tiende a crecer una función a la vez, y los pipelines crecen de la misma forma: copiados, ajustados y nunca unificados. El resultado eran decenas de funciones repartidas en varios repositorios, cada una desplegando a través de un pipeline de Jenkins un poco distinto, la mayoría lentos y ninguno listo para Gen2. Estandaricé todo el conjunto y modernicé los runtimes; aquí todo está generalizado a la técnica.
Alcance de la entrega
Fue un esfuerzo de migración por fases y estandarización de pipelines. El trabajo en DEV/STG y la operación posterior en producción fueron etapas distintas. Un seguimiento en producción detectó un cambio implícito en la service account del runtime y fijó la identidad de despliegue para evitar que se repitiera. La épica de planificación por sí sola no determina el estado de cada función en producción.
A dónde se iba el tiempo
Dos cosas dominaban cada build. Primero, el checkout: cada build hacía un clon completo del repositorio de despliegue, de 30 a 60 segundos de pura espera antes de cualquier trabajo real. Segundo, las dependencias: no se guardaba nada en caché, así que cada build reinstalaba todo desde cero. Multiplica eso por decenas de funciones que se despliegan muchas veces al día y se acumula en horas reales de ingeniería y minutos reales de build en la nube.
Lo que cambié
- Checkout de clonar o actualizar. En lugar de un clon completo en cada build, el pipeline hace fetch y reset cuando el repositorio ya está presente. El checkout bajó de entre 30 y 60 segundos a entre 5 y 10 segundos.
- Caché de dependencias. Ahora los builds reutilizan dependencias en caché en lugar de reinstalarlas, llevando la reutilización de caché de cero a entre 80 y 90%.
- Paquetes de despliegue más pequeños. Recorté lo que se envía, reduciendo aproximadamente a la mitad el tamaño del paquete de despliegue.
- Actualizaciones de runtime. Llevé Go, Node.js y Python a runtimes actuales y soportados en Gen1, y agregué los equivalentes de Gen2.
- Pipelines y ejemplos de Gen2. Nuevos pipelines de despliegue de Gen2 más ejemplos funcionales en los tres lenguajes, de modo que agregar una función Gen2 es copiar y listo, no investigar.
En el camino corregí un conjunto de molestias de larga data: una ruta de workspace incorrecta, una inconsistencia de vendoring por imports en blanco, un desajuste de entry-point entre Gen1 y Gen2 y paquetes de despliegue de tamaño excesivo.
Antes y después
| Métrica | Antes | Después | Cambio |
|---|---|---|---|
| Tiempo de checkout | 30 a 60 s | 5 a 10 s | ~80% más rápido |
| Tiempo total de build | 3 a 5 min | 1.5 a 2.5 min | ~50% más rápido |
| Tamaño del despliegue | 100% | 40 a 50% | ~50% más pequeño |
| Reutilización de caché | 0% | 80 a 90% | nuevo |
Por qué Gen2 y cómo llegar ahí de forma segura
Gen2 no es solo un cambio de versión. Por debajo corre sobre Cloud Run, lo que aporta concurrencia (hasta 1000 solicitudes simultáneas por instancia en lugar de una), integración nativa con Eventarc, cold starts más rápidos y un pago por uso más granular. La migración es donde la gente se pone nerviosa, así que la mantuve aburrida:
- Empieza en dev con funciones HTTP sobre los runtimes actualizados, y valida cada una por separado.
- Pasa a las funciones activadas por Pub/Sub solo después de comprobar las de HTTP.
- Usa conmutaciones blue-green para que el tráfico cambie solo cuando la nueva versión esté sana.
- Replica a través de staging, luego a producción, empezando por las funciones no críticas antes que las críticas.
Lo que le diría a alguien que empieza con esto
- Estandariza el pipeline antes de optimizarlo. No puedes acelerar veinte pipelines distintos; puedes acelerar uno y aplicarlo veinte veces.
- La mejora de build más barata suele ser el checkout. Deja de hacer un clon completo en cada ejecución.
- Guarda en caché las dependencias de forma deliberada. Pasar de cero a 80% de reutilización de caché suele ser la mayor reducción individual del tiempo de build.
- Migra los runtimes en un orden fijo (dev, staging, producción; HTTP antes que Pub/Sub; no críticas antes que críticas) y la migración aterradora se vuelve rutina.
Decisiones y próximos pasos
Una mirada retrospectiva al enfoque y sus límites. La próxima mejora es una propuesta, no un resultado entregado.
Alternativa y justificación
Los pipelines por función ofrecen flexibilidad local; una ruta de despliegue común reduce la repetición, pero debe preservar las diferencias de runtime y de triggers.
Contrapartida
Las nuevas generaciones de runtime pueden cambiar la identidad y el logging además del cómputo. El éxito del despliegue por sí solo no basta como aceptación.
Lo que mejoraría a continuación
Próxima mejora: mantener un inventario de migración por función con comprobaciones de aceptación de trigger, identidad, logging y rollback.