Migração do EC2 para o Kubernetes

Fernando
DogHero Brasil
Published in
9 min readAug 12, 2019

Normalmente quando uma startup começa seu projeto, uma das grandes preocupações é o seu rápido crescimento. Visando isso no desenvolvimento de software, algumas das preocupações passam a não ter uma visibilidade tão grande durante esse período, como a infraestrutura ou segurança.

Esse é um cenário perfeitamente normal, inclusive no meu ponto de vista um dos melhores contextos para o desenvolvimento de software tendo como foco o rápido crescimento. Porém, eventualmente, esse software irá crescer e essas preocupações deverão ser consideradas com um peso maior.

Na Doghero, tínhamos um cenário semelhante. Precisávamos criar uma nova estrutura confíavel, escalável e versátil.

Photo by Elias Castillo on Unsplash

Os primeiros passos

Antes de começar, precisávamos analisar o nosso estado atual e pensar aonde gostaríamos de chegar. Nossos dois principais sistemas, o frontend em angular e o backend em rails, estavam em duas stacks do OpsWorks, da AWS. Também, existiam serviços rodando em diferentes instâncias do EC2 e ECS.

Pensando não só em uma estrutura escalável, mas também em sua confiabilidade para deploys, testes e segurança, utilizar o Docker para todos os nossos serviços era uma opção de grande agrado. Porém, pensando um pouco mais adiante, o Kubernetes poderia nos proporcionar além das vantagens do Docker, uma grande confiabilidade em ter uma estrutura saudável, tanto na facilidade para a realização de deploys quanto a orquestração de nossos serviços.

Logo, o Kubernetes se tornou um excelente candidato para ser a base da nossa infraestrutura em cloud.

Photo by Marek Szturc on Unsplash

Criando imagens de docker para os nossos serviços

Para conseguir utilizar o Kubernetes, precisávamos fazer com que os nosso serviços tivessem uma estrutura com imagens do Docker.

Mas, o que são imagens do docker? Como cria-las?

A princípio, pode parecer que para ter essa estrutura é necessário diversas mudanças, mas a simplicidade do Docker para a criação dessas imagens é resumida em apenas um arquivo chamado Dockerfile. Esse arquivo contém etapas, em que uma determinada imagem base segue até chegar no estado em que o seu software estará rodando de forma isolada e indepentente dentro de um sistema operacional. Porém, vale lembrar que não necessáriamente isso deve ser resumido em apenas um Dockerfile, como vários, para atingirmos um determinado nível de abstração. Então, basicamente esse primeiro passo de migração era escrever Dockerfiles para cada um de nossos serviços.

É possível utilizar qualquer imagem base para a criação de suas imagens customizadas, desde sistemas operacionais e suas distribuições como Debian, Centos e Ubuntu, quanto linguagens em específico como Python, Ruby e Node. Todas essas imagens possuem suas determinadas versões e além disso versões com softwares extras ou de forma mais crua possível.

A vantagem de usar uma imagem grande com softwares extras é no contexto de quando o funcionamento de um software é um tanto quanto obscuro. Assim torna-se mais fácil ter tudo rodando de maneira lisa. Porém, você pode ter vulnerabilidades de softwares, que, talvez, nem usem só por tê-los em seu sistema e é aí que entram as vantagens de uma imagem mais crua.

Tendo todos esses pontos em vista, para cada um de nosso serviços utilizamos imagens de suas determinadas linguagens como base em suas versões Alpine. Então, basicamente nossos Dockerfiles consistem em: copiar nosso código para dentro de uma imagem, instalar suas depêndencias, em alguns casos compilar o código e logo em seguida iniciar nosso serviço. E depois de tudo isso, utilizar o comando build do Docker para criar nossas imagens.

$ docker build -t $IMAGE_NAME:$IMAGE_TAG --build-arg ENV_LANG=$ENV_LANG -f $DOCKERFILE_FILE .

Nos Dockerfiles, vale lembrar a importância de utilizar argumentos para a separação de diferentes ambientes, como produção ou desenvolvimento, com a função ARG e a utlização de CMD para seu start point e ENTRYPOINT para o seu start de dependências. Todos podem ser checados em sua documentação oficial.

E caso queira testar:

$ docker run -v $VOLUMES -p $PORTS -e CUSTOM_ENV=$CUSTOM_ENV -d $IMAGE_NAME:$IMAGE_TAG

Preparados para o Kubernetes?

Depende. Inicialmente, apenas com nossas imagens, já é possível ter nossos serviços rodando no Kubernetes. Mas aonde rodar o Kubernetes? E como rodar?

Hoje existem diversas opções para ter um ambiente com kubernetes. Construir o seu próprio instalando e configurando o Kubernetes ou utilizar algum serviço de cloud como o EKS na AWS, o Kubernetes Engine no Google e o AKS na Azure.

Como toda nossa estrutura já estava em cloud, optamos por escolher um dos seus determinados serviços. Onde durante a época da migração, o serviço que aparentava ser o mais maduro era o do Google (hoje todos os três são boas opções). Então, além da migração de estrutura, também teríamos uma migração de cloud para esses serviços.

Após diversos estudos e criações de ambientes de teste, estávamos preparados para começar a utilizar nossos serviços em produção. Porém, é claro, migrando eles em pequenas parcelas.

Alguns conceitos do Kubernetes

Antes de falar sobre a migração, é importante conhecer alguns termos e conceitos do Kubernetes.

Os clusters, são o conjunto de máquinas de um ambiente.
Os workloads, estão dentro do cluster e são os nossos deployments. Basicamente são as nossas imagens rodando.
Os pods estão relacionados aos deployments e são as unidades de nossa imagem rodando, podendo ser de um a múltiplos pods.
Os services são os serviços responsáveis para a comunicação com os nossos deployments, gerando neles por exemplo nossos IPs.
Os storages são os nossos volumes persistentes.
As configurations são basicamente variáveis de ambiente.
E por fim o kubectl, que é a CLI do Kubernetes.

Os pods possuem vida útil. Eles nascem e morrem. Então, não é aconselhável alterações em real time dentro de seus pods, pois elas serão perdidas.
Apesar desse comportamento, eles não são instáveis. E o Kubernetes lida com isso de forma natural, subindo imediatamente o número mínimo de pods que foi estabelecido no seu deploy.

Os services não são necessáriamente ligados aos deployments, mas sim ligados ao label que são atribuídos, relacionando-se com deployments com o mesmo label.

Agora sim! Kubernetes.

Photo by Delaney Dawson on Unsplash

Como primeira ação, criamos clusters para cada uma dos nossos ambientes. Produção, para tudo que for em produção, e Staging, onde teria tanto o ambiente de staging como o de development.

Hoje acredito que essa não seja a melhor prática, já que problemas no cluster pode causar downtime em todos os seus serviços. Mas, também deve ser levado em consideração do tamanho dos seus serviços e o quanto eles vão consumir, para não se ter um cluster inteiro para um serviço que não consome 10% de tudo.

Usando o Container Registry para manter nossas imagens privadas, começamos a criar os nossos respectivos workloads. Utlizando arquivos yamls para realizar o seu primeiro deployment e subindo eles com o kubectl.

$ kubectl create -f deployment.yml

É claro que é possível subir tudo pela UI do Google, mas ter yamls salvos é uma boa prática. Já que em casos extremos, você consegue recuperar a sua estrutura muito mais fácil, tendo em vista essa ser uma prática de infrastructure as code. Mas, também é possível ter esse yaml com um deployment já criado com o seguinte comando:

$ kubectl get deployment $DEPLOYMENT -o yaml >> meu_deployment.yml

Vale lembrar que os yamls não servem apenas para deployments, como também para storages e configurations, por exemplo.

Após os deployments, tanto em seu yaml quando pela UI de sua estrutura do Kubernetes na Cloud, você pode configurar a quantidade de pods que seu serviço irá utilizar e também colocar um autoscale. É legal ter pelo menos três pods para seus serviços, tendo assim mais segurança contra falhas ou o deploy de imagens quebradas. Já que no Kubernetes, os deploys de imagens são feitos de um pod por vez.

E a sua última parte para ter um deployment rodando e no ar é criar um service para o seu deployment.
Os cinco principais tipos de services que podem ser criados são:

Cluster IP, que é o IP do seu cluster e a porta do seu deployment, liberando acesso para deployments no mesmo cluster.
NodePort, que funciona de maneira parecida com o Cluster IP, porém também libera acesso para deployments fora do seu cluster.
Load Balancer, que cria um IP externo para seu deployment, abrindo ele para o mundo e fazendo o balance dos seus pods.
Headless, que cria um serviço que não precisa necessariamente de um acesso direto.
Ingress, que trabalha com toda as regras de redirecionamento e SSL para específicos DNS.

E por fim, para todos os nossos serviços, utilizamos NodePort, headless e o Ingress. Fazendo com que tenhámos um IP externo, colocando nossas regras A de DNS nesse IP. E claro, todos criados com yamls utilizando o comando create do kubectl.

Ingress, Helm, SSL e Controllers

O ingress, facilita muito os controles de URLs e subdomínios, por exemplo. Já que, podemos redirecionar para múltiplos serviços o mesmo DNS e ter toda a nossa configuração de SSL dentro dele. Porém, ele pode ter diferentes controllers com vantagens e desvantagens.

Optamos por utilizar a Nginx Controller por já ter familiaridade com o Nginx, a forma para escrever suas regras de redirecionamentos e também, outra vantagem que ela trazia na época era forçar o redirecionamento para HTTPS de nossas aplicações.

Para a configuração e instalação do Nginx Controller, tivémos uma grande ajuda do helm, um package manager para o Kubernetes. E de forma simples, podemos instalar e configurar a controller e ter ela pronta para ser utilizada.

$ helm install stable/nginx-ingress

E assim, a última peça do quebra cabeça era ter os SSL dentro do nosso ingress. E com isso, desenvolvemos um serviço (um deploymente rodando em cada um dos clusters) para manipular os nossos certificados SSL com o certbot. Onde basicamente ele gera o certificado e faz suas devidas verificações. E assim, salvando em um volume persistente esses dados dentro do path:

$ cd /etc/letsencrypt/

E para guardar o certificado dentro do cluster:

$ export CERT_PATH=/etc/letsencrypt/live/$DOMAIN
$ kubectl create secret tls $CERT_NAME --cert $CERT_PATH/fullchain.pem --key $CERT_PATH/privkey.pem

E claro, para gerar o certificado:

$ certbot --manual -d $SUBDOMAIN.doghero.com.br --preferred-challenge dns certonly

Você também pode guardar seus certificados do certbot em custom paths, basta checar sua documentação.

Com tudo isso em mão, conseguimos fazer uma grande migração de serviços em OpsWorks, EC2 e ECS para o Kubernetes.

Photo by Jason Hogan on Unsplash

Alguns problemas no caminho

É claro que tivémos problemas e desafios durante esse caminho e estarei livre para perguntas, críticas e sugestões. Mas, um problema que foi o mais marcante nesse processo foi a migração do nosso backend em Rails.

No processo, quando chegavámos a uma determinada quantidade de conexões, o serviço ficava extremamente lento. E a príncipio, imaginávamos que o problema era no Docker ou kubernetes. Mas, talvez um pouco fora do contexto, o problema não tinha tanta relação com eles e sim com o unicorn, que não trabalhava com threads. A solução que tivémos foi a migração do unicorn para o puma, e assim trabalhando com threads de uma forma mais fácil.

Pipelines, monitorações e algumas mudanças pós produção

Uma grande vantagem do Kubernetes é ser muito fácil o processo de continuous deployment e rollback, já que você tem o histórico de imagens geradas com o Docker e mais uma vez, podendo fazer essas mudanças com o kubectl:

$ kubectl set image deployment $DEPLOYMENT $DEPLOYMENT_IMAGE=$IMAGE_NAME:$IMAGE_VERSION

E tudo isso já integrado com o Jenkins e pipelines, que terão o seu respectivo artigo com mais detalhes.

Para monitoração, existem diversas alternativas e seria extenso falar delas por aqui. Mas, na Doghero utilizamos a Elastic Stack que também terá um artigo com mais detalhes.
Mas, algumas alternativas rápidas e fáceis de implementar são os próprios serviços de log do seu cloud provider, o prometheus e o Netdata, que também utilizamos, porém todos ligado a nossa Elastic Stack.
Porém, lembre-se de filtrar seus logs em seu cloud provider para que os seus gastos com logs não aumentem de forma exponencial.

E também, uma recente mudança que tivémos foi de colocar um Nginx na frente dos nossos serviços para termos dados de acessos ligados a nossa Elastic Stack com filebeat.

Obrigado pela leitura!

E aí, gostou do texto? Adoraríamos saber sua opinião sobre o que foi escrito por aqui. Deixe seu comentário e siga a DogHero no Medium para acompanhar as próximas publicações! 🐶

Gostaria de trabalhar com a gente? Confira nossas vagas abertas.

--

--