Esse post não é diretamente relacionado a desenvolvimento com Python, mas conta a história de uma das muitas experiências que passamos desenvolvendo e mostra como a filosofia e o mindset Python podem nos influenciar a tomar decisões melhores.
Contexto geral
Atualmente trabalho remotamente pela Toptal, uma empresa de consultoria em software com foco em trabalho remoto e que tem um processo seletivo bastante rígido para garantir uma qualidade acima da média para seus clientes (saiba mais sobre a Toptal aqui).
No time em que faço parte os papéis são bem definidos entre desenvolvedores front-end e back-end e faço parte da equipe de back-end, que usa principalmente Django nas aplicações. À medida que evoluímos e nos tornamos mais maduros como time, buscamos soluções que pudessem otimizar nosso processo de desenvolvimento.
Atualmente utilizamos CircleCI -- uma plataforma para integração e entrega contínuas -- para tarefas como rodar nossa suíte de testes, fazer a integração de nosso código, instanciar uma nova versão de nossos sistemas em um ambiente de staging e criar imagens Docker posteriormente colocadas em produção.
Melhorias
Nosso time constantemente reavalia processos, ferramentas e o resultado são discussões interessantes sobre como tornar nosso trabalho mais rápido e produtivo.
Recentemente começamos a utilizar um servidor NPM -- um dos mais usados gerenciadores de pacotes para Javascript -- privado para uma melhor separação de pacotes front-end, otimizando o tempo de build de assets de 47 para 25 segundos.
Na raiz do nosso projeto temos um package.json com o seguinte conteúdo:
{
// [ ... ]
"dependencies": {
"cat": "^1.0.0",
"front": "^1.0.0",
"core": "^1.0.0",
},
// [ ... ]
}
Sendo que cat, front e core (renomeados para exemplificar) são pacotes mantidos por nós mesmos no NPM privado. Por padrão, se você lista o pacote com “^”
(como por exemplo acima “^1.0.0”
), o npm considera apenas o número que representa a major version, no caso o número 1, e fará o download da última versão que começa com 1.
Essa abordagem tem quatro pontos fracos:
- Ela pode quebrar seu código. Se pacote de terceiro atualizar, seu código pode não estar preparado para lidar com as novas funcionalidades adicionadas, principalmente porque libs evoluem tão rapidamente que se torna fácil acontecer uma atualização sem backwards compatibility.
- Você não sabe exatamente qual versão do pacote seu sistema está usando em produção. Para saber, você teria que acessar os servidores remotamente e executar o comando
npm list
, por exemplo (poderia fazer localmente também mas existe a possibilidade de que no momento em que ocorreu o deploy, aquele pacote estava em uma versão anterior à sua versão local). - Você perde o controle de quando quer que seu sistema utilize a nova versão do pacote.
- Se você precisar fazer um rollback ou usar uma imagem antiga de seu sistema em produção, ainda assim ela vai utilizar a última versão do pacote, o que pode levar a mais dores de cabeça.
Problema
Recentemente tivemos um bug em produção, e uma mudança no pacote core resolveria. O que fazer com o sistema principal? Nada, não era necessária nenhuma alteração. Só precisaríamos gerar uma nova imagem Docker que ela seria montada do zero e no momento de instalar os pacotes npm, baixaria a última versão.
Bastava realizar rebuild na branch master no CircleCI, que assim que terminado ele trataria de enviar um webhook para o nossa ferramenta que cria imagens Docker. Nós utilizamos o seguinte padrão de nomenclatura dessas imagens:
myapp-production-<branch>-<sha[:7]>
Como não fizemos nenhuma alteração no sistema principal, o branch e o sha continuaram os mesmos.
Resumindo, nosso Docker recebeu um pedido de build para aquela branch e sha e, por padrão, primeiro procurou em seu cache de imagens se já existia alguma imagem pronta com aquele nome. O resultado foi que a mesma imagem, sem o hotfix, foi para produção (pois ela havia sido criada antes e no momento em que baixou os pacotes npm ainda não havia alterações no core).
Demoramos um pouco para perceber o problema, mas o suficiente para resolvê-lo sem que stakeholders percebessem.
Solução
Algum tempo depois discutimos e nós desenvolvedores back-end sugerimos a seguinte solução:
{
// [ ... ]
"dependencies": {
"cat": "1.0.5",
"front": "1.0.7",
"core": "1.0.10",
},
// [ ... ]
}
Com essa abordagem:
- Você pode fazer rollback do seu código sem problemas pois o código antigo vai usar a versão antiga do pacote.
- Você tem controle sobre quando quer que seu sistema utilize a nova versão do pacote.
- Você sabe exatamente quais versões de pacotes seu sistema está utilizando, bastando abrir o packages.json.
- Caso uma nova versão quebre seu código, você pode voltar uma versão rapidamente até que o problema seja resolvido.
O problema que tivemos em produção não aconteceria caso tivéssemos utilizado a abordagem acima. Assim que os pacotes fossem atualizados, criaríamos uma pull request no repositório do sistema principal com as seguintes alterações:
diff --git i/package.json w/package.json
index eaae10d..5aa773b 100644
--- i/package.json
+++ w/package.json
@@ -9,7 +9,7 @@
"dependencies": {
"cat": "1.0.5",
"front": "1.0.7",
- "core": "1.0.10",
+ "core": "1.0.11",
},
Após o merge, um novo build aconteceria no CircleCI, e um novo sha seria enviado via webhook. O Docker não encontraria nenhuma imagem com essa combinação de branch e sha e criaria uma nova do zero. Produção teria o hotfix e não haveria constrangimento.
Os desenvolvedores front-end não gostaram da ideia de ter que atualizar o arquivo toda vez que alguma dependência subisse de versão. Discutimos bastante e a última coisa que eu disse foi: “from the Zen of Python: explicit is better than implicit”.
Lição aprendida.
"Explicit is better than implicit" de "Ivan Neto" está licenciado com uma Licença Creative Commons - Atribuição-NãoComercial-SemDerivações 4.0 Internacional.