Exemplo de fluxo conectando o versionamento, a integração e a entrega contínua

O que uma aplicação precisa para ter DevOps?

É possível implementar os conceitos do DevOps em qualquer aplicação? Ou apenas nos que possuem tecnologias modernas, como sistemas web na nuvem pública ou aplicativos mobile? E os sistemas legados, como ficam?

Para responder essas perguntas, vamos revisar as práticas básicas de engenharia voltadas ao ciclo de vida da aplicação – desenvolvimento, entrega e sustentação. Também vamos falar sobre a importância da portabilidade na construção de serviços resilientes, portáveis e “prontos para a nuvem”. Por último, vamos comparar alguns cenários e seu grau de complexidade, sugerindo estratégias para que a implementação seja bem sucedida.

Versionamento de Código

Versionamento de código é mais do que manter o código em um repositório centralizado com histórico de versões. É permitir que um ou mais times colaborem com a mesma base de código sem que o trabalho de uma pessoa entre em conflito com o da outra, de uma forma em que as funcionalidades possam ser descartadas, revertidas, entregues numa ordem diferente da planejada, ou mesmo deixadas em stand-by temporariamente, tudo isso com eficiência e sem complicações técnicas.

Para isso a base de código precisa no mínimo íntegra, isto é, ter ao menos uma ramificação estável para que o time possa desenvolver a partir dela, e ser portável – poder ser baixada, modificada e executada em uma estação de trabalho compatível sem muita complexidade, como dificuldade durante a configuração inicial do ambiente de trabalho ou necessidade de modificar o código para que ele funcione.

Versionamento de código vai muito além disso, mas vamos nos aprofundar nesses dois aspectos: portabilidade e integridade do código.

Todo sistema possui pré requisitos de software e até mesmo de hardware para ser codificado. Hoje em dia o desenvolvimento está cada vez mais multi-plataforma – o mesmo código pode ser desenvolvido em Linux, macOS ou Windows, por exemplo, enquanto tecnologias mais antigas funcionam apenas em ambientes com características específicas. A maioria dos sistemas exige algum tipo de SDK (Software Development Kit), por exemplo o JDK (Java), Node ou .Net Framework. Alguns vão precisar de bibliotecas e drivers instalados na máquina, por exemplo um driver do Oracle ou SQL Server ou serviços compartilhados (COM+, GAC, etc). Outros serão capazes de buscar e instalar as dependências da aplicação no diretório em que ela está usando, sem configuração prévia na máquina (npm install, nuget restore). De qualquer forma, algum tipo de setup será feito no ambiente de trabalho para que a pessoa possa começar a desenvolver.

Para tornar esse setup mais simples, pode-se criar um script responsável por verificar a compatibilidade do ambiente (sistema operacional, requisitos mínimos de hardware) e baixar, instalar e configurar as dependências de software, se já não estiverem disponíveis. Isso pode ser feito com qualquer linguagem de script (bash, python), ou com algum utilitário desenvolvido para isso, por exemplo o PSake, em powershell, ou Cake, em C#.

Agora considere que o desenvolvedor já está com todos os pré-requisitos de ambiente devidamente configurados, baixou a última versão do código mas teve problemas de compilação ou execução. A razão, provavelmente, foi uma das abaixo:

  • Haviam apontamentos de dependências que podiam funcionar na máquina da última pessoa que desenvolveu, mas que estão em locais diferentes na máquina atual. Por exemplo, uma referência que está em um diretório na partição “D:” na máquina do outro, e na partição “C:” na máquina atual. Para evitar esse tipo de problema, é importante que todas as dependências sejam obtidas a partir de um repositório centralizado próprio pra isso, como um Nuget Server ou Nexus, por exemplo.
  • Haviam configurações específicas de usuário que não são compatíveis na máquina atual, como preferências locais, como opções da IDE (Integrated Development Environment) ou opções de execução que não necessariamente serão compatíveis com todos os ambientes. Normalmente os versionadores possuem um arquivo de controle do que deve e do que não deve ser versionado, como o .gitignore ou .vsignore. Fazendo bom uso desses arquivos é possível evitar esse problema.
  • O código possuía algum bug. É importante que cada pessoa valide seu código para garantir que ele pelo menos compile, antes de enviar as alterações para o servidor. Ao mesmo tempo, é importante que as alterações sejam enviadas frequentemente ao servidor mesmo que a funcionalidade não esteja completa, porque algo pode ocorrer com a estação de trabalho, ou a pessoa pode querer continuar o desenvolvimento a partir de outra máquina. Criar ramificações para cada pessoa trabalhando no código permite que a pessoa publique seu código com frequência sem comprometer a integridade da ramificação principal, e quando estiver mais estável, daí então essas alterações serão combinadas com a ramificação principal. Ainda assim, esse processo de combinar o que está sendo desenvolvido pelo time com a ramificação principal precisa ser validado. Daí entra o processo de automação de build (construção).

Automação de build

A automação de build permite garantir a integridade do código automaticamente. É um serviço executa procedimentos de validação, que pode ser executado de maneira agendada e periódica (toda noite, por exemplo) ou continuamente toda vez que houver uma alteração na base de código, nesse caso sendo também chamado de Integração Contínua, ou Continuous Integration (CI). Normalmente os procedimentos de validação seguem um fluxo semelhante ao descrito abaixo:

  1. Obter o código fonte em um ambiente “limpo”;
  2. Configurar os pré-requisitos de software;
  3. Baixar as dependências da aplicação;
  4. Compilar o código;
  5. Executar testes automatizados (unitários, integração, aceitação etc);
  6. Efetuar a análise estática do código (verificar se o código possui os padrões de qualidade definidos pelo time);
  7. Empacotar a saída da compilação (normalmente arquivos binários e estáticos gerados a partir da compilação);
  8. Assinalar o pacote de forma que possa ser rastreado com o processo de integração e com a versão do código;
  9. Aplicar um checksum ou outra forma de garantir que o pacote não foi adulterado ou corrompido;
  10. Armazenar o pacote em um repositório de artefatos apropriado.

Existem diversas plataformas que permitem a automação de build, por exemplo, o Azure Pipelines, Jenkings e Bamboo. Nas plataformas é configurado o procedimento, e podem haver diversos serviços conectados respondendo à plataforma para executar o procedimento. Esses serviços são instalados em servidores cujo ambiente é mais adequado para sua execução, ou seja, que atendam os pré requisitos de hardware e software para a compilação da solução (Sistema Operacional, SDK’s, etc).

Por isso mesmo, é importante que as recomendações mencionadas no tópico anterior sejam seguidas – para configurar um processo de automação de build bem sucedido, será muito mais simples se a base de código estiver portável, livre de apontamentos específicos de ambiente e arquivos desnecessários, e caso dependa de um setup mais complexo, tenha um script que o faça automaticamente.

Com um procedimento de automação de build bem configurado, podemos ir mais além e configurar um procedimento para automatizar a entrega (deploy). Preferencialmente, a cada pacote novo no repositório de artefatos, execute a entrega  em determinado ambiente. Esse procedimento é chamado de Entrega Contínua.

Automação de deploy

É um procedimento de automação de entrega configurado em uma plataforma (Azure Pipelines, Jenkings, Bamboo) conectado ao repositório de artefatos. É chamado de Entrega Contínua, ou Continuous Delivery (CD), quando, a cada pacote novo no repositório de artefatos, executa a entrega em determinado ambiente.

Esse procedimento também possui serviços que respondem a ele, responsáveis ou por obter o pacote e enviar para os ambientes destino, ou rodar no próprio ambiente destino e buscar o pacote. Normalmente o fluxo será composto por

  1. Obter o pacote;
  2. Instalar/Publicar no local de destino;
  3. Validar a instalação;

Normalmente o fluxo contempla a instalação em diversos ambientes de validação até chegar no ambiente final (produção). É comum que o deploy seja feito continuamente no ambiente de desenvolvimento (DEV), depois sob aprovação para o ambiente de homologação (HML) e depois sob aprovação para o ambiente de produção (PRD), conforme a figura abaixo:

Exemplo de fluxo conectando o versionamento, a integração e a entrega contínua
Exemplo de fluxo conectando o versionamento, a integração e a entrega contínua

Observem que o mesmo pacote (#3) é promovido para cada um dos ambientes. Não é gerado um novo pacote para o ambiente de homologação e outro para produção.

Para que isso ocorra, o pacote deve ser capaz de obter as configurações do ambiente sem precisar ser modificado. Uma das formas mais simples é preparar a aplicação para ler os apontamentos a partir de variáveis de ambiente, ou seja, valores pré-configurados no ambiente de execução, por exemplo, conexões com banco de dados ou serviços que a aplicação precisa para rodar. Por exemplo, o endereço do banco de dados de desenvolvimento provavelmente será diferente dos endereços de homologação e produção. Se o valor estiver fixo no código, e por consequência no pacote, o pacote deverá ser modificado para que funcione em ambientes diferentes, e isso faz com que ele perca a integridade – se for modificado, não há a garantia de que o que as funcionalidades validadas e aprovadas em desenvolvimento continuarão a funcionar em homologação, por exemplo. Uma alternativa é manter as configurações dos ambientes individualmente (DEV, HML e PRD) dentro do versionamento do código, mas (1) isso não resolve se houverem dados confidenciais, que não podem ser versionados de maneira nenhuma, e (2) caso alguma configuração mude, o código deve ser atualizado, um novo processo de build deve ser disparado, um novo pacote gerado e um novo deploy deve ser feito para atualizar essa configuração. Se o dado não for alterado com frequência isso não é um problema tão grande, mas endereços, usuários e configurações no geral costumam mudar. Configurar as aplicações para que o pacote seja portável implica em codificar de uma forma em que essas configurações possa ser obtidas a partir do ambiente, de um serviço ou de um repositório de configurações adequado.

É importante que a aplicação forneça alguma maneira de indicar que a instalação foi feita com sucesso. Esse procedimento costuma ser chamado de healthcheck. Para uma API, imagine uma rota de acesso (ex: /ready) indicando que a aplicação está no ar, cumprindo com todos os pré-requisitos para o seu funcionamento (conectividade e permissões com os serviços que ela depende, por exemplo). Isso auxilia muito o processo de validação. Quando um pacote funciona em DEV, e não funciona em HML, é preciso identificar rapidamente se a falha está relacionada a um bug de desenvolvimento ou é alguma configuração incorreta, ou mesmo um serviço que a aplicação depende e está fora do ar ou sem permissões. Com esse tipo de verificação na própria aplicação, é possível descartar alguns tipos de problema de infraestrutura, contribuindo para um diagnóstico mais rápido. Esse tipo de verificação pode ser integrada ao fluxo de automação de deploy.

Sustentação

Com as recomendações dos tópicos anteriores, conseguimos garantir o versionamento, integridade e entrega. Mas o que precisamos ter em mente para que a aplicação seja sustentável?

Portabilidade

O objetivo da maioria das soluções de software é que cresçam, ou seja, atendam um maior número de usuários.

No caso de aplicações web, para que se possa suportar uma quantidade maior de requisições, é necessário escalar a aplicação. Quando ela não é portável, ou seja, não é facilmente replicável em diversas instâncias com balanceamento de carga, a forma de escala costuma ser vertical. Isso significa um aumento de recursos de hardware, como memória e CPU, no servidor que hospeda a aplicação.

A escala vertical é muito comum para aplicações legadas, que costumam possuir uma configuração de ambiente bem complexa, com dependências compartilhadas, pré-requisitos específicos e outros complicadores, tornando a configuração de um novo ambiente um verdadeiro desafio. Às vezes, o design da aplicação não permite que mais de uma instância seja executada no mesmo servidor, então a escala implica, necessariamente, em ou aumentar a capacidade do servidor atual, ou configurar um novo ambiente.

Executar mais instâncias da mesma aplicação, seja lado-a-lado ou em ambientes isolados, é a escala horizontal. É uma abordagem mais interessante porque permite a elasticidade, isto é, disponibilizar mais instâncias em um período de alta demanda, e menos instâncias quando a demanda for baixa. Se as grandes plataformas de nuvem pública forem utilizadas, é possível configurar isso automaticamente, e pagar pelo uso – apenas os recursos que forem utilizados serão cobrados. A escala vertical, por outro lado, tende a ser permanente. Os recursos de infra que você adquire serão cobrados quer você use ou não, e muitas vezes não podem ser aproveitados de maneira eficiente.

Para viabilizar a escala horizontal, algumas premissas devem ser cumpridas. A aplicação deve estar preparada para que a carga seja distribuída – isto é, se utilizar dados de sessão, esses dados precisam ser gerenciados de maneira adequada, porque uma transação que inicia na instância A pode não conseguir ser finalizada na instância B, se os dados de usuário estiverem atrelados à instância A, por exemplo. A aplicação, preferencialmente, não deve utilizar dependências compartilhadas com outras no mesmo ambiente, para evitar conflitos. Algum nível de isolamento deve ser garantido mesmo que as instâncias sejam executadas no mesmo servidor.

Diversas tecnologias hoje em dia facilitam a configuração de self-hosted apps. Nesses cenários, o web server é uma dependência da própria aplicação, que roda como um processo, ao invés do web server conter a aplicação. Executando a aplicação como um processo facilita a gestão de múltiplas instâncias lado a lado.

Outra tecnologia que favorece a escala horizontal e elasticidade são Containers. Eles funcionam de maneira semelhante a máquinas virtuais, mais leves, e promovem o isolamento de dependências, execução via processo e distribuição de carga por definição.

Resumindo, um dos aspectos da sustentação eficiente de uma aplicação é a escala. A escala vertical tende a ser permanente, e é uma alternativa quando a configuração de novas instâncias da aplicação representam um desafio. A escala horizontal funciona bem quando a aplicação é portável – quando o aumento, diminuição de instâncias e distribuição de carga é facilmente configurável. A portabilidade permite a elasticidade – o aumento ou diminuição de recursos e instâncias conforme a demanda, viabilizando uma utilização mais eficiente dos recursos, e até mesmo a cobrança conforme o uso.

Diagnóstico eficiente

Outro aspecto importantíssimo na sustentação de aplicações são logs. As aplicações precisam fornecer logs de qualidade durante todo seu ciclo de vida, de forma que possam ser coletados, centralizados e disponibilizados para o time na plataforma mais adequada. Muitas vezes, quando as áreas de desenvolvimento e infraestrutura são separadas, os desenvolvedores não têm acesso direto a produção, porém pelo menos ao logs eles deveriam ter.

Quando a aplicação não fornece logs da sua própria execução, ou as plataformas de monitoramento são mais rudimentares (logs salvos em disco no ambiente de execução, por exemplo) o diagnóstico fica comprometido. Provavelmente, um operador com privilégios no ambiente de execução vai precisar logar remotamente e obter os logs, acessar o terminal, observar os eventos do servidor e repassar essas informações ao time de desenvolvimento. Isso leva tempo, e no caso de um bug crítico em produção, tempo pode significar um prejuízo financeiro.

Qualquer tarefa relacionada a administração da aplicação em execução deve estar contida na própria aplicação. O healthcheck (mencionado no tópico de Automação de deploy), ou outras rotinas, como limpeza de cache e reinicialização de serviços, deveriam poder ser acessadas sem que um operador precise conectar remotamente ao servidor em que ela está sendo executada. Isso agrega velocidade à manutenção de uma forma em que os administradores da aplicação não necessariamente precisem ser administradores dos servidores, diminuindo uma das barreiras entre desenvolvimento e operações.

Por que essas práticas são importantes

Até o momento, citamos uma série de recomendações sobre versionamento de código, automações de build, deploy e sustentação de aplicações. Mas por que elas são tão importantes?

Basicamente, a maioria das falhas de design de aplicação são compensadas com recursos de infraestrutura e/ou esforço operacional. Alguns exemplos:

  • Quando uma base de código depende de um setup complexo de ambiente para sua compilação, o procedimento de automação de build é dificultado. Sem a automação de build, o empacotamento da solução a ser entregue é feito manualmente. Pode ser feito na máquina do desenvolvedor, que já está preparada com os pré-requisitos, e disponibilizada em um diretório de rede. Esse procedimento dificilmente garantirá a integridade do pacote e seu rastreamento com a versão de código.
  • Quando o procedimento de entrega não é automatizado, um operador será responsável por copiar o pacote e instalá-lo no ambiente de destino. Todo procedimento manual está sujeito a falha humana, e costuma ser mais lento que o automatizado;
  • Quando a aplicação não é portável, a escala costuma ser vertical, o que nem sempre tem um custo efetivo;
  • Se a aplicação não fornece logs de qualidade, healthchecks e outras tarefas administrativas disponíveis remotamente, é necessário acessar o ambiente em que a aplicação está sendo executada para efetuar o diagnóstico. Nem sempre o time de desenvolvimento tem acesso direto a isso.

Isso implica diretamente em tempo, custo ou ambos. Tudo que não estiver ao alcance direto do time de desenvolvimento precisa ser solicitado ao time de operações. Isso pode envolver a abertura de um ticket, sujeito a fila de atendimento, e ser algo que pode ser resolvido apenas com a aquisição de mais recursos de infra estrutura. Se a empresa utilizar estrutura On Premises (local), a aquisição de novos servidores e plataformas pode levar até meses.

O problema é que muitas vezes, essas preocupações não são priorizadas. As aplicações legadas são mantidas como estão porque “um dia serão substituídas”. Os débitos técnicos de aplicações novas não são priorizados porque “não agregam valor direto ao usuário”. As aplicações novas não contemplam esses requisitos porque possuem uma “arquitetura emergente”.

O problema é que todos esses argumentos são falácias. As aplicações legadas, normalmente, são aplicações críticas e vitais no ecossistema de soluções da empresa, e muitas vezes demoram anos para serem migrados, isso SE forem migrados. Alguns débitos técnicos atrapalham a manutenção e sustentação do produto, e isso é percebido pelo usuário com demora na solução de bugs e indisponibilidade. E “arquitetura emergente” não é desculpa para deixar de se preocupar com portabilidade, resiliência e monitoramento já nas primeiras versões. O que os gerentes e product owners não priorizam é revertido em custo, demora no atendimento, desgaste entre as áreas desenvolvimento e operações e até mesmo pelo usuário final.

12 Factor Apps

Um meio de orientar o time a desenvolver aplicações portáveis e sustentáveis desde suas primeiras versões é estudar os 12 Fator Apps, uma metodologia de construção de serviços resilientes, portáveis e “prontos para a nuvem”, criada pelo time da plataforma Heroku, com base em lições aprendidas na sustentação de sua plataforma, que consiste em um conjunto de direcionamentos para garantir que os serviços sejam minimamente portáveis, elásticos, resilientes e sustentáveis, independente da tecnologia.

A metodologia dos 12 fatores é amplamente utilizada no mundo para representar os conceitos do DevOps. Com ela, é possível cumprir com todas as recomendações mencionadas até aqui, e mais. Além disso, a maioria das tecnologias modernas, como o .net core e containers, estimula o cumprimento dos fatores by design.

Estratégias de implementação

Para aplicações novas serem desenhadas de forma portátil, basta que os desenvolvedores estejam cientes desses conceitos e se preocupem com os 12 fatores desde o início do desenvolvimento.
Quando as aplicações já estão engessadas, esse débito técnico deve ser entendido pelos gestores ou product owners como uma prioridade, já que isso reflete em custos também, se não no desenvolvimento, então para operações e recursos de infraestrutura. Se isso for priorizado, com pouco esforço as aplicações podem ser adaptadas, até porque as tecnologias modernas já favorecem o cumprimento dos 12 fatores.

As aplicações legadas não só costumam conter configurações espalhadas no código, mas também dependências compartilhadas. Por costumarem ser soluções “frágeis” e ao mesmo tempo vitais para a empresa, recomenda-se começar por ações pontuais que viabilizem o build e o deploy automático com o menor impacto no código possível. Algumas ações consistem que a base de código contemple:

  • Configurações centralizadas, mapeadas por um arquivo que obtenha os valores do ambiente em que está sendo executado.
  • Dependências bem mapeadas com suas versões. Ainda que usem dependências compartilhadas no mesmo servidor, se houver um versionamento apropriado não haverá conflito.
  • Scripts que configurem os pré-requisitos de software, para que possa ser usado tanto pelos desenvolvedores quanto pelos processos de build

Aplicações locais e plataformas fechadas, como mainframes, podem ser um desafio à parte.
Quando eu digo plataformas, imagine que o código da plataforma não é aberto para versionamento como um todo, porém você pode alterar áreas específicas, por exemplo SAP, Sharepoint, CRM Dynamics, ou mesmo Mainframes. Nesses casos, toda alteração que puder ser feita via script deve ter esse script versionado de forma parametrizada, ou seja, que possa ser executado em instâncias diferentes das plataformas. O deploy automatizado pode ser viável em alguns cenários, quando a plataforma permite integração, em outros casos não.

Aplicações locais podem ter um versionamento e uma gestão de configurações adequada, porém sua distribuição dificilmente fica a cargo de um processo de deploy automatizado centralizado, e sim no fluxo oposto – ao invés do deploy ser feito de maneira centralizada, a aplicação é responsável por se atualizar. A aplicação irá verificar periodicamente se possui alguma atualização disponível no servidor, irá solicitar a permissão para o usuário e fará o download e atualização dela mesma. O monitoramento ocorre com a coleta de dados nos ambientes de execução e envio periódico para o sistema de gestão, caso o usuário permita.

Considerações finais

  • Da mesma forma que empresas podem implementar uma ou mais disciplinas do DevOps dependendo dos limites da organização, as aplicações podem ter uma ou mais disciplinas.
  • A chave para que o versionamento, build e deploy ocorram é a portabilidade e gestão adequada de configurações.
  • Outros fatores como fornecimento de logs via stream e tarefas administrativas embutidas na própria aplicação facilitam a sustentação da aplicação
  • A metodologia dos 12 fatores é um bom guia inicial para que as aplicações permitam um mínimo de portabilidade, gestão operacional eficiente e resiliência
  • Aplicações modernas podem cumprir facilmente os 12 fatores até porque as tecnologias atuais favorecem isso, como containers e PAAS.

Conteúdo recomendado

PSake

Linguagem de automação de builds em powershell.

Cake

Linguagem de automação de builds em C#.

12 Factor Apps

Esse é o site oficial da metodologia 12 Factor Apps. Nela vocês podem entender a forma como o time da Heroku resolveu todos os problemas apresentados neste vídeo.

12 Factor Apps & .Net Core

Nesse artigo o Ben Morris mostra como construir serviços cumprindo os 12 fatores com recursos do .net core.

E se quiserem conferir a “versão vídeo” deste conteúdo, cliquem no link abaixo!

 

Publicado por

Grazi Bonizi

Coordeno a trilha de Arquitetura .Net no The Developers Conference, compartilho código no GitHub, escrevo no Medium e no Blog da Lambda3, e participo de Meetups e PodCasts normalmente sobre DevOps, Azure, .Net, Docker/Kubernetes e DDD

Deixe uma resposta