Dicas essenciais para manter um sistema em produção

Você ou seu cliente tiveram a ideia, o projeto foi parar em produção e o MVP deu certo. E agora? Sabemos que essa é uma dúvida que persiste até mesmo nos times mais experientes. Afinal, o tempo é curto, mas a quantidade de perrengues que seu produto pode enfrentar em produção, não. Nesse artigo, vou compartilhar alguns conselhos que aprendi construindo produtos que operam no mundo real aqui na ateliware.
Carlos Grell | 11 de setembro de 2020

Sabemos que operar e manter um sistema em produção não é fácil. Você pode ter tido uma ideia que deu certo. Seu produto foi bem projetado, o modelo de negócio fez sentido e o projeto vendeu, ‘deu em algo’ e agora está em produção.

Mas e aí, qual o próximo passo? Deixar o código rodando e rezar para que tudo continue ok? Até o mês passado, sua base de usuários beirava a casa dos singelos 35. Agora entretanto, surgiram 10.500.

Antes de se dar conta, você percebe que terá que lidar com milhares, talvez milhões de requests, disparar e-mails, importar e exportar arquivos, integrar com redes sociais, ter uma página de checkout estável e o mais temido - agora você também tem que lidar com centenas de clientes que depositaram confiança na capacidade do seu sistema de entregar um produto ou serviço satisfatório. Qual o overhead de suporte que seu time ou sua empresa teriam capacidade de fornecer? E se sua equipe ao todo contiver menos de 5 integrantes?

Para fechar com chave de ouro é possível que seu sistema tenha um roadmap já planejado. Caso alguém tenha investido em seu projeto, por exemplo, é bem provável que esses interessados tenham expectativas sobre quais funcionalidades serão - e quando serão - entregues.

Essas e outras variáveis juntas são indispensáveis na hora de fazer os trade-offs, ou seja, balancear os pesos do que se deve ou não investir esforço a fim de garantir que o seu produto ou serviço escale rapidamente, seja confiável e de qualidade. Do outro lado da balança, entregar funcionalidades em um ritmo desejável a fim de manter competitividade contra produtos concorrentes também é de vital importância.

Portanto, neste artigo vou te dar 15 dicas que servem na maioria das situações para que seu você e seu time consigam operar sem dor em produção e, principalmente, evitar que um projeto bem sucedido morra na praia.

1. Ter ao menos um ambiente de testes devidamente isolado, coerente e próximo em funcionalidades

Manter um sistema que ao menos receba pagamentos ou que possua funcionalidades básicas que dependem de jobs, cronjobs, integrações e filas é uma tarefa árdua quando não há ao menos um ambiente seguro para que essas tarefas aconteçam sem interferir nos dados da vida real. Portanto, ter um ambiente de homologação ou staging minimamente ‘verossímil’ é fundamental.

Motivos não faltam. Nem tudo que funciona em localhost:3000 funcionará no seu sistema com domínio próprio rodando em Kubernetes, Heroku, AWS, Godaddy ou qualquer que seja - e isso é apenas 1% da ponta do Iceberg. Muitas integrações não funcionam por natureza em ambiente local. É bem comum que certas funcionalidades no ambiente de desenvolvimento sejam simuladas por dados inventados - ‘mocks’ - ou simplesmente pulando integrações e disparos.

Num mundo ideal semelhante ao que ocorre com projetos que já possuem um nível de maturidade alto, geralmente em empresas grandes ou com times maiores, é comum inclusive a existência de níveis diferentes de staging, cada um com um grau de fidelidade. Um deles, por exemplo, com a finalidade de realizar testes rápidos, sem perder tanto tempo com integrações e mockando o que for tolerável a fim de poupar tempo. O outro ambiente, por sua vez, faria disparos reais de e-mail, integrando com ambientes de homologação com parceiros externos para manter a testabilidade de toda a jornada do sistema.

Para um time que possui recursos limitados e cujo projeto ainda não tem um nível de maturidade que possa requerer um nível de confiança tão grande, a sugestão é criar ao menos o ambiente mais próximo do real, para os testes com ambientes sandbox dos meios de pagamento, disparos reais de e-mail, integrações no ambiente de homologação dos parceiros, sempre recebendo somente dados que façam sentido.

Dessa forma, seu ambiente de staging ficará mais coerente e próximo da realidade, enquanto que testes rápidos podem ser feitos no dia-a-dia do desenvolvimento utilizando o container local de preferência do projeto, como Docker ou afins.

2. Suíte de testes automatizados

Esta é talvez a mais óbvia, porém aquela cuja ausência causa mais problemas no futuro. Partindo do princípio de que quase qualquer produto está sempre se desenvolvendo e se aprimorando, é necessário ter uma suíte de testes automatizados que ao menos faça o double-check das regras de negócio do seu sistema e que possa, inclusive, explicar casos de uso e comportamentos de forma mais fácil e de simples manutenção.

Acima de tudo, uma suíte de testes automatizados também serve como um porto seguro para garantir a testabilidade de regressão do seu sistema. Nada garante que o merge de um código que tenha contido conflitos não possa retirar por acidente uma lógica que havia sido definida no passado.

É necessário, portanto, uma ‘trava’ para explicar que aquela lógica ou regra de negócio definida não deve ser quebrada até ‘segunda ordem’. Não é necessário que a maioria dos testes seja complexa ou mirabolante para conseguir que um sistema esteja bem coberto. Esses testes devem sim existir, principalmente, à medida em que a maturidade do sistema cresce. Porém, grande parte dos testes pode e deve ser simples, como testes unitários com regras curtas e enxutas, e testes com disparos e integrações nas formas mais simples.

Se pararmos para pensar, os maiores problemas em aplicações que estão rodando em produção ocorrem não porque uma animação ou um widget que contém javascript para de funcionar - algo que seria coberto em um teste ao estilo ‘Selenium’ ou ‘PhantomJS’, isto é, testes complexos e demorados utilizando headless browsers. Em vez disso, os problemas mais sérios costumam ocorrer quando regras de negócio são ignoradas, modificadas sem controle ou mal-interpretadas, gerando efeitos colaterais envolvendo valores monetários, permissionamento indevido e outras falhas que podem abalar gravemente a operação da sua empresa.

3. Faça o Logging de tudo o que for preciso, desde que não seja informação sensível

Logar informações é sempre importante. Quando um sistema já está em produção, é fundamental para que bugs sejam resolvidos rapidamente. A dica aqui é logar de forma inteligente. Quase toda plataforma de IaaS (Infrastructure as a Service) entre elas GCP, Amazon AWS, Azure e outras, possuem algum sistema de agregação de logs de acordo com o que o seu sistema já joga por padrão no stdout. Para melhorar esses logs, você pode investir em alguma biblioteca focada nisso e dividir seus logs em níveis de criticidade, entre outras melhorias.

O cuidado aqui tem que ser sempre em obscurecer informações sensíveis. Claro que senhas e derivados já não deveriam nunca estar em texto plano por natureza. Porém, ainda existe a chance de expor acidentalmente dados sobre seus usuários que não deveriam ficar em qualquer log por aí. Imagens de conversas privadas são um exemplo clássico de algo que deveria ser obscurecido em um log.

4. Não atropelar o essencial para entregar mais rápido

Pode até parecer tudo bem modelar algo no seu sistema e depois ter que voltar atrás logo no começo, porém, se você já tem uma base de clientes ou usuários em produção, esse tipo de fenômeno deve passar a ser exceção. Afinal de contas, ninguém vai gostar de entrar no seu sistema caso ele passe um dia inteiro fora do ar, ‘voltando no tempo’ antes de um erro de modelagem ou de uma perda ou poluição de dados causada acidentalmente.

Para ter um produto admirável o suficiente pelo usuário final, é importante que esse tipo de situação seja prevenida através da suíte de testes que comentamos e também através de um período de validação mínimo em ambiente staging (homologação) também citado.

Portanto, deve haver, no mínimo, uma parte do seu processo de desenvolvimento que conte com esse período de validação, principalmente do que é mais sensível em termos de regras de negócio. Não esqueça que um tempo suficiente para projetar e modelar uma nova funcionalidade também é fundamental, para que não sejam feitos ‘puxadinhos’ dos quais você com certeza vai se arrepender no futuro.

5. Tenha um roadmap

Pode se tornar complicado evoluir um produto que já está em produção com um time pequeno. Afinal, seu time agora também tem que eventualmente tratar incidentes e talvez fornecer suporte técnico. Isso é ainda mais difícil se você não tiver um plano. Por isso, recomendo que seja feito sempre um roadmap, que pode ser através de listas com bullets ou mapas mentais, ou outra forma semelhante de ao menos mapear o que será (ou pelo menos o que é desejável que seja) desenvolvido nos próximos 2, 3 e até 6 meses de projeto.

Aprendemos com a experiência aqui na ateliware que desenvolver olhando para o escuro é a receita perfeita para um sistema que pode ficar estagnado no tempo ou perder a chance que tinha de conquistar um grande mercado. Logo, tenha sempre um objetivo, mesmo que resumido, e itere sobre esse objetivo ao longo do tempo (exemplo: mensalmente).

6. Cuide dos débitos técnicos, refatorações e atualizações de bibliotecas com frequência

Não é preciso desenvolver no modo go-horse para deixar para trás algum débito técnico. É natural que mesmo um sistema desenvolvido com planejamento tenha seus débitos técnicos ou refatorações ainda não feitas, que por conta do ritmo rápido de crescimento tiveram de ser toleradas e postergadas.

Entretanto, é recomendado que se itere sobre tais débitos, como códigos repetidos, a fim de evitar que em um futuro próximo sua base de código seja insustentável de manter.

Outro tipo de débito técnico pode ser também causado por features que funcionam para um sistema com poucos usuários. Por exemplo: é natural que um sistema com poucos usuários possua algum tipo de listagem sem paginação. Isso pode inclusive ganhar tempo de desenvolvimento inicialmente, afinal, para que fazer todas as funcionalidades do seu sistema terem de lidar com big data caso ele não tenha dados ‘ainda tão big’ assim.

Entretanto, é fundamental que se acompanhe esse tipo de funcionalidade mais ‘precária’ para que, ao subir rapidamente em número de usuários, seu sistema não engasgue por conta de algo simples de resolver.

Do ponto de vista das bibliotecas e frameworks, atualizá-los sempre e pouco a pouco também é parte de um processo constante, que ao ser comprometido, pode prejudicar o avanço do projeto depois.

Pare para pensar: até que é rápido atualizar a versão do seu projeto em Ruby on Rails da versão 5.2 para a 6, não? Porém, ao fazer o mesmo processo da versão 3 para a 6, por exemplo, em um sistema que já existe em produção, isso poderia levar meses.

Além do framework, cada lib de suporte tem suas próprias dependências. Portanto, a dica é atualizar sempre para atualizar facilmente. Tire um tempo do projeto, em um intervalo que pode ser de 2 em 2 meses, ou até mais longo, mas que seja dedicado a iterar sobre as dependências das bibliotecas, versões de frameworks e linguagens. E, claro, teste isso tudo no staging antes de subir para produção.

7. Respeite os dados de seus usuários

Um debate constante tem se intensificado, principalmente no Brasil, sobre as formas corretas de se lidar com dados dos usuários. Com a divulgação da LGPD (Lei Geral da Proteção de Dados), esse cuidado se tornou ainda mais importante e fundamental para que qualquer sistema em produção tenha uma política clara sobre a retenção de dados dos usuários, e até recursos para limpeza, exportação ou remoção desses dados a pedido do cliente.

Vale lembrar que guardar senhas e outras informações sensíveis -  como número de cartões de crédito - em texto plano nunca foi aceitável! Prefira cartões tokenizados, se for guardar, e senhas somente em hash.

8. Pace e deploy constante

Para que seu time mantenha as entregas e um pace (ritmo) de deploy confiável e constante é fundamental que se otimize esse processo. Portanto, é importante confiar em ferramentas de Continuous Delivery/Continuous Deployment (CD) e Continuous Integration (CI).

No caso do CI, a principal função dessas ferramentas é fazer com que o código sempre seja re-testado e re-integrado com a funcionalidade que já estava lá anteriormente. É possível integrar, por exemplo, para que seu container rode a suíte de testes sempre que um pull request for aberto. Caso não passe nessa suíte de testes, significa que o código não está de fato preparado para aquele pull request, e ferramentas como o Github têm integrações disponíveis para travar com que esse merge do pull request seja feito em caso de reprovação automaticamente. É possível testar também se a cobertura de testes está completa, se a estrutura de workers para filas funciona adequadamente, entre outros.

No caso do CD, a ideia é parecida, porém com o intuito de focar na facilidade de se deployar algo novo. Afinal de contas, ter um processo de deploy que dura 2 dias faz com que você e seu time percam muito mais tempo com o processo em si do que talvez com o próprio desenvolvimento. Daí a necessidade do CD. Assim que um código entrar na branch destino (master, staging ou outras, por exemplo), é possível fazer com que esse código tenha seu deploy facilitado e em grande parte automatizado. Assim, você garante que pequenas features vão entrar sempre que algo novo estiver pronto.

Dessa forma, a dor de cabeça é bem menor, pois é bem mais simples garantir que uma funcionalidade nova pequena esteja intacta, do que algo que muda completamente o comportamento do sistema.

9. Tenha ferramentas para prover certas funcionalidades de forma assíncrona

É quase impossível que um sistema grande em produção (monolítico ou não) não dependa de nenhuma estrutura de organização de tarefas assíncronas. O próprio disparo de e-mails, por exemplo, já pode se tornar um gargalo caso a aplicação cresça e tenha de ser feito em tempo real. Importações, exportações, machine learning, cron jobs (com data e hora para acontecer), e muitos outros processos são exemplos claros de que seu sistema em produção teria que comer muito arroz e feijão (falando de recursos) para dar conta de tudo isso.

Portanto, desde que features como essas são pensadas, já se pode escolher ferramentas para lidar com isso. Exemplos não faltam em qualquer linguagem: Sidekiq, RabbitMQ, quaisquer libs que utilizam Redis ou Memcached por baixo para gestão mais elaborada de filas e jobs assíncronos ajudam nesse aspecto. Escolha sempre as ferramentas mais bem avaliadas (open source ou não), e mais focadas naquilo que seu projeto precisa.

10. Audits, Versionamento e Soft Delete dos dados

Essas 3 atividades se completam e são indispensáveis para fornecer a rastreabilidade de tudo o que é feito no seu sistema em produção.

Auditioning é a ideia de gerar uma espécie de log permanente para cada modificação relevante que é feita em uma entidade, ou em um grupo de entidades no sistema.  A ideia aqui é que audits não devem ser deletados depois de um tempo, como ocorre geralmente com logs comuns. Se alguém der algum acesso importante a outro usuário ou alterar algum valor que tenha impacto relevante na sua operação, é fundamental deixar esse rastro.

Não faltam ferramentas open source para lidar com audits e, inclusive, vários frameworks atuais para desenvolvimento de aplicações full stack já vêm com algumas ferramentas básicas para auditar seu sistema desde o início.

Versionamento das entidades é a ideia de criar uma cópia rasa de entidades do seu sistema toda vez que ela for modificada. Dessa forma, é sempre possível ‘memorizar’ alterações nas configurações dos usuários, saber quem alterou tais campos e outras informações, se isso tiver sido provocado por um operador do sistema. Nas bibliotecas que lidam com isso é comum que seja um complemento da funcionalidade que o audit já está monitorando.

Isso pode economizar tempo correndo atrás de logs caso os usuários do seu backoffice tenham, por exemplo, um histórico de alterações e quem as realizou. Como temos trabalhado bastante com Ruby on Rails, um exemplo rápido de biblioteca capaz de lidar com isso seria o Papertrail, em Rails. Mas bibliotecas como essas são bem diversas e, de modo geral, presentes em todas as linguagens.

11. Bancos de dados e outros serviços autogerenciáveis

Pode ser até bem mais barato à primeira vista ter um banco de dados hospedado em uma máquina física ou em um container simples no Kubernetes em cloud. Porém, apesar de o segundo parecer mais seguro e atraente, em ambos os casos, as rotinas de backups e otimizações, bem como prevenção de falhas, multi-sharding, dynamic pools e outras funções podem se tornar um pesadelo caso seu time não seja grande e sua empresa não tinha uma equipe dedicada de infra e DBAs para lidarem com isso.

Portanto, a dica aqui,é de usar algum serviço mais auto-gerenciável, como Google Cloud SQL, Amazon RDS, Heroku DB Addons (como Heroku Postgres) e Azure Database, especialmente se os recursos do seu time forem limitados. Esses serviços têm programas de backups automáticos diários, data-clips e outros benefícios que podem facilitar muito sua vida e prevenir dor de cabeça no futuro.

O investimento financeiro, apesar de maior, tem muito mais retorno nesse tipo de serviço do que caso fosse utilizado um banco de dados gerenciado ‘na marra’, o que seria uma situação clássica daquela velha expressão de que às vezes o barato sai caro.

12. Ambiente de deploy flexível e conteinerizado, sem downtime

Nem é preciso entrar a fundo nas vantagens de um ambiente de deploy conteinerizado, já que a conteinerização virou, nos últimos 3 anos, um dos termos mais discutidos no mundo do desenvolvimento. O raciocínio é simples: com ela, você pode reproduzir o mesmo ambiente em qualquer lugar, quantas vezes for necessário, com o mesmo comportamento.

Além disso, um ambiente de deploy flexível deve possibilitar que sua topologia, isto é, a estrutura do seu cluster de serviços, seja reproduzida facilmente caso um dia seja necessário trocar de ambiente.

Seu projeto pode ter que mudar de requisito e não ser mais hospedado na Amazon. E se, em poucos dias, você tiver que migrar para o Azure? Com um ambiente de deploy flexível, escalável e descritivo como o Kubernetes e vários outros é possível aproveitar uma topologia já definida em um novo hospedeiro.

Para um sistema com muitos usuários em produção, os cinco minutos necessários para subir seu deploy podem ser bem longos para que seu serviço tenha que ficar offline toda vez que acontecer um deploy. Portanto, é fundamental que haja uma maneira de deployar sem downtime, isto é, sem que a aplicação fique indisponível.

Isso é possível em quase todas as ferramentas de orquestração de clusters e serviços em cloud, como o Kubernetes comentado anteriormente. Por meio de um conceito conhecido como awareness probe, ele consegue saber quais réplicas do seu sistema já estão preparadas para atender requests e quais não estão, impedindo que um novo deploy derrube cópias antigas do seu servidor até haver alguma cópia nova que já esteja em operação.

Outros serviços, especialmente de PaaS  - Platform as a Service, como o Heroku provê em contraste ao tradicional Infrastructure as a Service - conseguem entregar o deploy sem downtime de forma até mesmo mais simples, apesar de mais alienada e com menos controle sobre clusterização, detalhes do deploy e dos balanceamentos de carga. Tais fatores devem pesar na escolha de qual serviço é mais satisfatório para o nível de maturidade do sistema.

13. Analytics, monitoramento, BI, SEO

Para que seu sistema tenha um futuro promissor e bem pensado, é fundamental que essas palavrinhas acima estejam presentes em produção. Passarei de forma simplificada por cada um desses pontos, já que seria impossível falar de todos com detalhes em um único artigo.

Analytics servem principalmente para que você tenha informações sobre o comportamento dos usuários e também do seu sistema. Será que o seu cliente final consegue chegar ao final do checkout com facilidade?

Qual o tempo médio que o usuário passa utilizando seu aplicativo? Seus usuários acessam outras páginas ou passam todo o tempo na home? Será que o tempo médio que suas exportações e integrações demoram é satisfatório? Esse é o tipo de pergunta, que entre muitas outras, pode ser respondida por meio de Analytics e outras métricas.

Monitoramento, também conhecido como APM (Application Performance Monitoring), permite que você entenda quais são os gargalos - quais as dores físicas ou lógicas do seu sistema. Saber que partes do seu sistema retornam quantidades de erros maiores que o normal, quais consomem mais recursos do servidor, quais partes passam por mais estabilidades e várias outras respostas.

Por meio do APM, é possível saber até mesmo quais navegadores apresentam mais erros de javascript no seu sistema, qual sistema operacional os usuários que tiveram uma certa falha estavam utilizando.

Exemplos de soluções bem conhecidas de APM: Data Dog, New Relic, entre outros.

BI (Business Intelligence): É importante que a equipe de gestão ou também stakeholders do projeto tenham a visão do que está acontecendo. Seu projeto, afinal, está trazendo resultados financeiros? Quantos usuários o sistema ganha por semana? Ou será que ele não está mais perdendo usuários do que ganhando?

Esse é o tipo de pergunta que você pode responder com um dashboard de BI, muitas vezes ligado de forma quase ‘automágica’ ao banco de dados. Mas, não se esqueça: acessos somente para leitura. Alternativamente, você pode disponibilizar uma API para que a ferramenta de BI colete esses dados de tempos em tempos, o que pode ser uma opção mais segura.

Opções: Power BI, Big Query BI Engine + Datastudio, entre outros.

SEO (Search Engine Optimization): Se o seu sistema compete com concorrentes em produção é provável que você queira aparecer antes deles no Google e em outras ferramentas de pesquisa. Portanto, leia sobre e siga no mínimo algumas diretivas básicas de SEO para que seu produto não fique para trás nessa hora. Exemplos clássicos incluem expor títulos importantes em h1 e h2, conter favicons, tags meta description, entre outros.

Vale a pena uma leitura mais detalhada sobre cada aspecto de SEO, pois com uma página SEO-ready, seu produto não precisará ser sponsored pelo Google para que seu site apareça em uma posição decente. Ferramentas como o WebPage Test, que rodam inclusive a engine do Google Lighthouse automaticamente por baixo do capô, são bem úteis na hora de entender se o seu sistema é otimizado para dispositivos móveis, conexões lentas e SEO em geral.

  • Leia também: Data driven culture: a importância das métricas para tomada de decisões
  • 14. Tenha código e ferramentas que cumpram com a escalabilidade. Sim, você não leu errado, seu código também precisa ser escalável.

    Ter estruturas assíncronas, ambientes escaláveis e até um CDN para distribuição de assets é realmente super importante quando se quer operar em produção. Mas e o código? Ouço muitas pessoas dizerem que não utilizam Rails, por exemplo, porque ‘não é escalável’. Se ferramentas gigantes e com milhões de usuários como Github são escaláveis, como fica essa história do que é ou não é escalável?

    Sei que o raciocínio é um pouco mais complexo do que isso, mas, de certa forma, código ruim quase nunca é escalável. Não adianta ter um serviço desenvolvido na linguagem mais rápida possível se após fazer uma query você iterar por essa lista de resultados 5 vezes diferentes, cada uma utilizando uma função ‘map’ diferente.

    É impossível também, por exemplo, emitir uma lista de um milhão de usuários em um único request comum, não paginado, sem qualquer tipo de tratamento. Usar uma linguagem mais rápida pode favorecer, em partes, operações como essa, caso o breaking point ainda esteja sendo atingido em cheio. Mas mesmo nessas linguagens, ao se deparar com situações não otimizadas em queries, relacionamentos, ausências de paginação ou de caches, o resultado será o mesmo.

    Afinal de contas, você não achou que o Facebook, que tem alguns bilhões de usuários, assim como outras redes sociais gigantes, usassem apenas ‘linguagens rápidas’, não é mesmo? Por trás de operações como essas estão caches, CDN, multi-sharding, indexações complexas, sistemas de pool virtuais nos bancos e muitos outros mecanismos.

    15. Esperar que algumas coisas quebrem

    A última dica é mais relacionada ao aspecto psicológico da equipe do que qualquer outra coisa, pois vez ou outra, alguma coisa em produção certamente vai quebrar. Afinal, ainda não há APM, planejamento ou suíte de testes capaz de evitar que absolutamente todos os bugs sejam prevenidos.

    A dica aqui consiste em 2 partes. A primeira é tentar fazer com que os bugs não afetem tanto as lógicas de negócio, o que vai de encontro à filosofia de ter testes automatizados que cobrem de forma bem rígida sobre tais cumprimentos de regras.

    Se for para investir mais tempo garantindo que não haja falhas, que esse tempo seja perdido testando as regras de negócio mais críticas à operação, em vez de testar apenas se o sistema tem certos botões na tela ou derivados.

    A segunda parte consiste em trabalhar a maturidade da equipe a fim de entender que quebras em produção são riscos inerentes ao processo. Partindo desse ponto, vale a pena reforçar que bugs e incidentes críticos devem sempre ser tratados com urgência maior do que a sprint convencional e, eventualmente, até dedicar ou rotacionar parte da equipe para ser a linha de frente responsável por lidar com incidentes.

    Carlos Grell
    Software Engineer | Fascinado por tudo que envolve ciência, lógica e conhecimento. Criptografia, user interfaces, eletrônica digital, Internet das Coisas. Fã de código limpo e reutilizável.