Contextualizando a migração e avaliando o lift and shift
Perfeito, pessoal. Dando continuidade, nós vimos que a migração que a Amaral Seguros fez para a nuvem da plataforma de comércio eletrônico não saiu bem. Os problemas foram mantidos e ainda surgiram outros, relacionados a custos e despesas de infraestrutura em nuvem.
Um ponto importante: isso não significa que o lift and shift (migração direta) nunca deva ser utilizado. Nós já trabalhamos em cenários em que a infraestrutura que o cliente tinha em on-premises (ambiente local) havia atingido um limite e, para provisionar mais servidores, era necessário comprá-los, entregá-los no data center (centro de dados) e passar por um processo que levava semanas ou até meses. Assim, a simples velocidade de provisionar infraestrutura — por exemplo, quando temos um servidor com capacidade X e precisamos do dobro ou do triplo — já pode ser um benefício.
O ponto central é: executar uma aplicação na nuvem não a torna, por si só, cloud native (nativa em nuvem). O lift and shift pode ser interessante e gerar valor para a empresa em alguns casos, mas isso não transforma a aplicação em uma aplicação cloud native (nativa em nuvem).
Introduzindo a transformação para cloud native
A questão, então, é como transformar uma aplicação para adotar boas práticas e ser, de fato, cloud native (nativa em nuvem). Existem caminhos, boas práticas e estratégias que podemos adotar.
Um conjunto muito relevante é o da 12-Factor App (aplicativo de 12 fatores), que consolida boas práticas. Foi criado pela Heroku, empresa que oferece uma plataforma em nuvem para provisionamento de recursos computacionais, bancos de dados, servidores, entre outros. O time da Heroku, a partir de ampla experiência operacional, listou o que considera as 12 principais estratégias para que uma aplicação se beneficie de ser cloud native (nativa em nuvem). Isso é extremamente relevante, principalmente para estratégias e desenhos de arquitetura baseados em microserviços, embora não seja exclusivo a esse tipo de arquitetura.
Outro ponto importante: nem toda aplicação precisa adotar, necessariamente, os 12 fatores.
Para termos uma visão geral dos 12 fatores, sem aprofundar em cada um deles — há outros cursos na Alura que exploram esse conteúdo em profundidade — destacamos os seguintes princípios e implicações práticas:
Apresentando princípios iniciais dos 12 fatores
Base de código: podemos ter N ambientes de implantação, mas todo o código-fonte deve estar em um único repositório. Não devemos criar variações ou gerir código por ambiente. Tudo parte da mesma origem e é gerido no mesmo repositório, sendo então distribuído para N ambientes.
Dependências: todas as dependências da aplicação devem ser gerenciadas por um gerenciador de pacotes, como o NPM do Node ou o Maven do Java. Cada linguagem tem várias opções; o ponto não é qual usar, mas garantir que tudo o que a aplicação precisa para executar esteja declarado em um arquivo de dependências. A aplicação nunca deve presumir que o ambiente computacional fornecerá um recurso por padrão — por exemplo, disponibilizar a biblioteca XPTO no momento do deploy (implantação). Isso não é uma boa prática.
Configurações: não devemos manter configurações hardcoded (fixadas no código). Senhas, endpoints (pontos de acesso) a consumir, API keys (chaves de API) e similares devem ficar em arquivos de configuração ou em secret managers (gerenciadores de segredos), externos à aplicação, e ser considerados conforme o ambiente em que a aplicação está em execução. Em desenvolvimento, usamos os valores de desenvolvimento; em produção, os valores de produção.
Serviços de apoio: a aplicação deve conseguir alternar, por exemplo, de um banco de dados de desenvolvimento que está executando localmente em um contêiner com MySQL para o banco de dados do servidor de desenvolvimento ou para o de produção apenas mudando parâmetros ou variáveis de ambiente. Não deve haver acoplamento com a instância A ou B do recurso computacional — seja banco de dados, fila ou cache. Tudo precisa ser configurável.
Automação e entrega: deve existir uma pipeline (fluxo automatizado) de DevOps (práticas de desenvolvimento e operações) para realizar todo o processo de build (compilação/empacotamento), release (liberação) e execução da aplicação, com automação em cada etapa.
Processos sem estado e armazenamento: o ideal é que a aplicação seja sem estado. Podemos ter a necessidade de persistir dados — o que é comum —, mas não devemos depender do armazenamento do recurso de computação no qual a aplicação está executando. Quando trabalhamos com contêineres isso fica evidente. Podemos atribuir um volume, mas isso precisa ser feito de forma explícita, indicando onde estará o armazenamento. Em suma, não devemos contar com o armazenamento local do servidor ou do recurso de computação onde a aplicação está rodando.
Detalhando práticas de estado, servidor e escalabilidade
Precisamos persistir arquivos e registros em um recurso externo — em uma base de dados, em um bucket no Cloud Storage ou em um bucket no S3 —, isto é, fora da infraestrutura computacional onde a aplicação está em execução.
A aplicação não deve depender de implantação em nenhum tipo de servidor externo. Ela deve incluir um servidor embutido. Por exemplo, quando trabalhamos com Spring Boot e geramos o fat.jar, ele inclui automaticamente um Tomcat ou um Jetty para inicializar a aplicação. Assim, o servidor web já vem embutido junto com o fat.jar.
Em concorrência, o ideal é que a aplicação esteja preparada para escalar horizontalmente. Escalar horizontalmente é atender a um fluxo maior de solicitações de pessoas usuárias por meio de cópias, isto é, instâncias paralelas da infraestrutura. Não foi o que aconteceu com a estratégia adotada por Amaral Seguros: fizeram um lift and shift (migração sem refatoração), migrando a aplicação para um Compute Engine, uma VM no Google Cloud, e a aplicação atingiu o limite de recursos. Em seguida, precisaram migrá-la para outro servidor, para um número maior de servidores, com mais recursos. Isso é o que chamamos de escalabilidade vertical: escalar aumentando a quantidade de recursos computacionais do servidor. Esse processo pode consumir bastante tempo.
A ideia é trabalhar de forma horizontal. Criamos mais instâncias para atender a um fluxo maior e reduzimos o número de instâncias conforme a necessidade. Isso ajuda a obter um custo mais atrativo e uma escalabilidade mais elástica. Podemos criar e eliminar instâncias. Preparar a aplicação para trabalhar de forma horizontal e atender picos de demanda é essencial.
A aplicação deve estar preparada para ser desligada e ligada sem problemas. Se precisarmos eliminar uma instância que está executando a aplicação, ela deve ser capaz de tomar os dados que estão em cache, persistir em algum lugar e realizar verificações para que a interrupção não gere inconsistências de dados. Isso é o que chamamos de graceful shutdown (desligamento ordenado).
Garantindo paridade e observabilidade entre ambientes
Paridade entre ambientes: precisamos garantir que os ambientes de desenvolvimento, homologação e produção sejam equivalentes. Não necessariamente em recursos computacionais, mas em estruturas e tecnologias utilizadas. Quando trabalhamos com Docker, isso se simplifica bastante. Independentemente de estarmos executando a aplicação no notebook pessoal, em um servidor local (on-premises) ou no servidor de produção, na nuvem A, B ou C, a aplicação está sendo executada em um contêiner autocontido. Esse é o ecossistema que o software percebe. Onde o recurso computacional de fato está, onde o contêiner roda, é indiferente para a aplicação. A paridade torna-se muito mais simples quando trabalhamos com aplicações conteinerizadas.
O item 11 trata de logs. Nossa aplicação deve ser projetada para simplesmente gerar um stream (fluxo) de logs continuamente. Outra ferramenta ficará responsável pela ingestão, tratamento e suporte à observabilidade da aplicação, sem adicionar complexidade à aplicação principal. A aplicação apenas precisa gerar os logs — por dia, por hora ou em um arquivo único —, e uma ferramenta externa processará esses logs, gerará métricas, apoiará a monitoração e permitirá a adoção inteligente de uma estratégia de observabilidade.
Tratando processos administrativos e planejando a adoção por etapas
Por fim, processos administrativos pontuais devem ser tratáveis de forma replicável. Por exemplo, ao criarmos uma nova versão da aplicação que implica criar ou alterar tabelas, incluir colunas ou renomear uma tabela, ao replicar isso para um ambiente de desenvolvimento ou produção, não deve ser necessário que uma pessoa desenvolvedora acesse o ambiente A, B ou C para executar scripts manualmente. Isso é suscetível a erro e não é boa prática. A boa prática é ter um script que simplesmente precise ser executado: um script é criado e executado em desenvolvimento, homologação e produção para replicar as mudanças. Usamos base de dados como exemplo, mas poderia ser qualquer outro tipo de configuração. Isso não deve fazer parte da aplicação; não devemos criar um endpoint apenas para executar esse tipo de tarefa. Em vez disso, devemos ter um processo pontual que se executa, faz o que precisa ser feito e não será executado novamente.
Essa é a visão geral dos 12 fatores. Reforçando: não é esperado que todas as aplicações adotem necessariamente todas essas estratégias. Adotaremos muitas delas quando já tivermos pavimentado o caminho de como nossa empresa trabalha, com uma arquitetura de referência definida, e não precisaremos nos preocupar com isso para cada aplicação. Em alguns casos, teremos aplicações que precisarão escalar verticalmente, pois dependem de mais recursos computacionais. Ainda assim, esse panorama indica o que precisamos adotar para alcançar o cenário de uma aplicação Cloud Native (nativa em nuvem).
Por último, não precisamos adotar tudo de uma vez; podemos avançar em ondas, incorporando essas boas práticas quando partimos de um cenário em que nada disso está em execução.
Perfeito, pessoal. Até o próximo vídeo. [♪]