Modernização de mais de 60 Cloud Functions no GCP: Gen1 para Gen2, builds pela metade
Cada pipeline de Cloud Functions era um pouco diferente e a maioria era lenta. Padronizei todos eles, reduzi o tempo de checkout em cerca de 80% e o tempo de build pela metade, e estabeleci um caminho seguro do Gen1 para o Gen2.
Uma migração em etapas; o acompanhamento em produção também verificou a service account do runtime.
Contribuição em resumo
- Minha responsabilidade
- Padronização dos pipelines de funções, migração de runtime e acompanhamento de identidade.
- Escopo
- Entrega faseada em desenvolvimento/staging e operação de produção posterior distinta.
- Colaboração e limites
- Trabalhei na infraestrutura e no caminho de deploy; um épico de planejamento não é prova de que toda função em produção concluiu a migração.
- Resultado
- Trabalho de deployment padronizado e um acompanhamento em produção que fixou a identidade do runtime após uma mudança implícita de service account.
Uma frota de Cloud Functions tende a crescer uma função de cada vez, e os pipelines crescem do mesmo jeito: copiados, ajustados e nunca unificados. O resultado eram dezenas de funções espalhadas por vários repositórios, cada uma fazendo deploy por um pipeline Jenkins um pouco diferente, a maioria lenta e nenhuma pronta para o Gen2. Padronizei todo o conjunto e modernizei os runtimes, com o conteúdo sanitizado aqui para destacar apenas a técnica.
Escopo da entrega
Este foi um trabalho de migração faseada e padronização de pipelines. O trabalho em DEV/STG e a operação posterior em produção foram etapas distintas. Um acompanhamento em produção encontrou uma mudança implícita na service account do runtime e fixou a identidade de deployment para evitar recorrência. O épico de planejamento sozinho não estabelece o estado de toda função em produção.
Para onde o tempo estava indo
Duas coisas dominavam cada build. Primeiro, o checkout: cada build fazia um clone completo do repositório de deploy, 30 a 60 segundos de pura espera antes de qualquer trabalho real. Segundo, as dependências: nada ficava em cache, então cada build reinstalava tudo do zero. Multiplique isso por dezenas de funções fazendo deploy várias vezes ao dia e o total chega a horas reais de engenharia e minutos reais de cloud build.
O que eu mudei
- Checkout com clone-ou-atualização. Em vez de um clone completo a cada build, o pipeline faz fetch e reset quando o repositório já está presente. O checkout caiu de 30 a 60 segundos para 5 a 10.
- Cache de dependências. Os builds agora reutilizam dependências em cache em vez de reinstalar, elevando o reaproveitamento de cache de zero para 80 a 90%.
- Pacotes de deploy menores. Reduzi o que é enviado, cortando aproximadamente pela metade o tamanho do pacote de deployment.
- Atualizações de runtime. Atualizei Go, Node.js e Python para runtimes atuais e suportados no Gen1, e adicionei os equivalentes no Gen2.
- Pipelines e exemplos de Gen2. Novos pipelines de deploy para Gen2 mais exemplos funcionais nas três linguagens, de modo que adicionar uma função Gen2 é copiar-e-usar, não pesquisar.
No caminho, corrigi um lote de pequenos incômodos antigos: um caminho de workspace incorreto, uma inconsistência de vendoring causada por blank imports, uma divergência de entry-point entre Gen1 e Gen2 e pacotes de deployment grandes demais.
Antes e depois
| Métrica | Antes | Depois | Mudança |
|---|---|---|---|
| Tempo de checkout | 30 a 60 s | 5 a 10 s | ~80% mais rápido |
| Tempo total de build | 3 a 5 min | 1.5 a 2.5 min | ~50% mais rápido |
| Tamanho do deployment | 100% | 40 a 50% | ~50% menor |
| Reaproveitamento de cache | 0% | 80 a 90% | novo |
Por que Gen2, e como chegar lá com segurança
Gen2 não é apenas um aumento de versão. Ele roda sobre o Cloud Run por baixo dos panos, o que traz concorrência (até 1000 requisições simultâneas por instância em vez de uma), integração nativa com Eventarc, cold starts mais rápidos e cobrança por uso mais granular. A migração é onde as pessoas ficam nervosas, então eu a mantive entediante:
- Começar em dev com funções HTTP nos runtimes atualizados, e validar cada uma individualmente.
- Passar para funções acionadas por Pub/Sub somente depois que as de HTTP estiverem comprovadas.
- Usar cutovers blue-green para que o tráfego só mude quando a nova versão estiver saudável.
- Replicar por staging, depois produção, começando pelas funções não críticas antes das críticas.
O que eu diria a quem está começando isso
- Padronize o pipeline antes de otimizá-lo. Você não consegue acelerar vinte pipelines diferentes; consegue acelerar um e aplicá-lo vinte vezes.
- O ganho de build mais barato costuma ser o checkout. Pare de fazer clone completo a cada execução.
- Faça cache de dependências de forma deliberada. De zero a 80% de reaproveitamento de cache costuma ser a maior queda isolada no tempo de build.
- Migre os runtimes em uma ordem fixa (dev, staging, produção; HTTP antes de Pub/Sub; não crítico antes de crítico) e a migração assustadora vira rotina.
Decisões e próximos passos
Uma visão retrospectiva da abordagem e seus limites. A próxima melhoria é uma proposta, não um resultado entregue.
Alternativa e justificativa
Pipelines por função oferecem flexibilidade local; um caminho de deploy comum reduz a repetição, mas precisa preservar as diferenças de runtime e de gatilho.
Trade-off
Novas gerações de runtime podem mudar identidade e logging, além de computação. O sucesso do deployment sozinho é aceitação insuficiente.
O que eu melhoraria a seguir
Próxima melhoria: manter um inventário de migração por função com verificações de aceitação de gatilho, identidade, logging e rollback.