Entrega Continua com Kubernetes e Jenkins

Uma das principais vantagens de utilizar um orquestrador de containers é a facilidade de criar fluxos para colocar automaticamente a sua aplicação em produção sem intervenção humana.

O grande problema é que são necessários muitos estágios até que a sua aplicação possa ir para produção, geralmente consiste em:

  • Build -> Teste Unitarios -> Deploy em Homologação -> Testes Integrados -> Deploy em Produção

E em cada um desses estágios se dividem em múltiplas tarefas, como por exemplo o Deploy em Produção, é geralmente assim:

  • tirar um servidor do balanceamento -> instalar a versão nova da aplicação -> reiniciar o serviço -> testar -> colocar o servidor novamente no balanceamento -> fazer isso pelo numero de servidores que você tiver e rezar para tudo funcionar!

Alguns dos projetos aqui do Terra conseguimos automatizar boa parte desse fluxo, e é o que eu pretendo demostrar aqui, vou mostrar uma versão reduzida em que vou criar os estágios Build, Push e Deploy usando o Jenkins.

Pré-requisitos

Vou criar esse ambiente todo na minha maquina, apesar de aqui na empresa (Terra) usamos um ambiente muito semelhante em produção. Os procedimentos aqui foram feitos em um macOS.

Install Docker

Basta entrar na página Get Started do Docker e fazer o donwload para a sua versão de sistema operacional.

Install VirtualBox

Baixe e instale a ultima versão do VirtualBox que você pode achar no site de Downloads deles.

Install Minikube

Procure a ultima versão do Minikube na pagina de Releases.

curl -Lo minikube https://github.com/kubernetes/minikube/releases/download/v0.25.2/minikube-darwin-amd64 && chmod +x minikube && sudo mv minikube /usr/local/bin/

Install Kubectl

A última parte que falta é instalar o kubectl aplicativo que vamos utilizar para se comunicar com o cluster do Kubernetes.

curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/darwin/amd64/kubectl && chmod +x kubectl && sudo mv kubectl /usr/local/bin/

Repositórios

Vamos usar dois repositórios no git para trabalhar nesse ambiente.

Jenkins

O primeiro repositório pode ser visto aqui https://github.com/cirolini/jenkins-docker-kubectl e tem uma versão do Jenkins em Docker que vai rodar dentro do nosso ambiente com o Kubernetes e vai ter acesso a executar comandos dentro do Kubernetes para executar as etapas necessarias para colocar a nossa aplicação no ar.

Docker-Flask-Uwsgi

É uma aplicação de teste em python usando o Flask e o uWSGI no Docker, e é uma aplicação onde eu comentei mais aqui e o repositório pode ser visto aqui https://github.com/cirolini/Docker-Flask-uWSGI

Criando o ambiente

A primeira coisa é iniciar o Minikube:

MacBook-Air-de-Rafael:~ rafaelcirolini$ minikube start
Starting local Kubernetes v1.10.0 cluster...
Starting VM...
Getting VM IP address...
Moving files into cluster...
Setting up certs...
Connecting to cluster...
Setting up kubeconfig...
Starting cluster components...
Kubectl is now configured to use the cluster.
Loading cached images from config file.

Depois vamos iniciar a nossa própria instancia de Jenkins e do Registry, que vai ser o nosso repositório de containers.

MacBook-Air-de-Rafael:~ rafaelcirolini$ kubectl apply -f https://raw.githubusercontent.com/cirolini/jenkins-docker-kubectl/master/k8s_jenkins.yaml
clusterrolebinding "jenkins-rbac" created
persistentvolume "jenkins" created
persistentvolumeclaim "jenkins-claim" created
service "jenkins" created
deployment "jenkins" created

Agora criando o Registry:

MacBook-Air-de-Rafael:~ rafaelcirolini$ kubectl apply -f https://raw.githubusercontent.com/cirolini/jenkins-docker-kubectl/master/k8s_registry.yaml
persistentvolume "registry" created
persistentvolumeclaim "registry-claim" created
service "registry" created
service "registry-ui" created
deployment "registry" created

Verificando a criação dos containers:

MacBook-Air-de-Rafael:~ rafaelcirolini$ kubectl get pods
NAME READY STATUS RESTARTS AGE
jenkins-5ccb4f9498-56svc 0/1 ContainerCreating 0 44s
registry-95c457bdb-9frmt 0/2 ContainerCreating 0 11s

Na minha maquina demorou uns 5 minutos até os containers estarem em running:

MacBook-Air-de-Rafael:~ rafaelcirolini$ kubectl get pods
NAME READY STATUS RESTARTS AGE
jenkins-5ccb4f9498-56svc 1/1 Running 0 5m
registry-95c457bdb-9frmt 2/2 Running 0 5m

Agora vamos abrir a interface do Jenkins:

MacBook-Air-de-Rafael:~ rafaelcirolini$ minikube service jenkins
Opening kubernetes service default/jenkins in default browser...

Para pegar a chave de ativação do seu Jenkins para continuar a instalação, basta rodar o comando abaixo trocando o id do pod pelo da sua estrutura:

MacBook-Air-de-Rafael:~ rafaelcirolini$ kubectl exec jenkins-5ccb4f9498-56svc cat /var/jenkins_home/secrets/initialAdminPassword
33c7f2604a274647acb327b87dba6427

Depois clique para instalar os plugins sugeridos, crie seu usuário e senha e reinicie seu Jenkins pela interface mesmo.

Agora com o Jenkins funcionando vamos criar o nosso primeiro pipeline.

O Pipeline

Duas coisas são muito interessantes aqui, a criação de um pipeline para a entrega continua da nossa aplicação e o arquivo do Jenkinsfile.

O Jenkinsfile é uma solução muito elegante para codificar e manter a sua lógica de integração e deploy continua "scriptada" em conjunto com todo o código da sua aplicação, quando quiser alterar algo no fluxo basta alterar no arquivo que na próxima execução ja vai respeitar essas alterações. E é nele que vão ficar todos os estágios do nosso pipeline.

node {
    checkout scm
    // Pega o commit id para ser usado de tag (versionamento) na imagem
sh "git rev-parse --short HEAD > commit-id"
tag = readFile('commit-id').replace("\n", "").replace("\r", "")

// configura o nome da aplicação, o endereço do repositório e o nome da imagem com a versão
appName = "app"
registryHost = "127.0.0.1:30400/"
imageName = "${registryHost}${appName}:${tag}"

// Configuramos os estágios

stage "Build"
        def customImage = docker.build("${imageName}")
    stage "Push"
        customImage.push() 
    stage "Deploy PROD"
        input "Deploy to PROD?"
customImage.push('latest')
sh "kubectl apply -f https://raw.githubusercontent.com/cirolini/Docker-Flask-uWSGI/master/k8s_app.yaml"
sh "kubectl set image deployment app app=${imageName} --record"
sh "kubectl rollout status deployment/app"
}

No nosso Jenkinsfile a primeira coisa que fazemos é pegar o commit id do git para usar de versionamento da aplicação, isso é uma boa pratica pois ajuda a pessoa a não esquecer de incrementar a versão, e fica fácil de rastrear aquela alteração no git.

Depois definimos algumas variáveis que vamos usar mais adiante e logo os três estágios que vamos usar.

Criando o Job no Jenkins

Na tela de boas vindas do Jenkins, clique em "Create New Jobs", escolha um nome e selecione o tipo Pipeline.

Na parte de configuração escolha "Pipeline script from SCM", e no "Repository URL" coloque o endereço completo do Git, por exemplo: https://github.com/cirolini/Docker-Flask-uWSGI

Clique em salvar e depois clique em Build Now, seguem alguns prints abaixo:

Pronto, no nosso Stage View ja aparecem os nossos três estágios, que funcionaram corretamente.

Agora para vermos a aplicação em produção é só usar:

MacBook-Air-de-Rafael:~ rafaelcirolini$ minikube service app
Opening kubernetes service default/app in default browser...

E o seu navegador deve abrir algo bem parecido com isso:

Se você ficar dando uns F5 na tela deve ver o Hostname mudar pq o nosso arquivo de configuração yaml da aplicação pede para que dois pods sejam levantados no Kubernetes.

MacBook-Air-de-Rafael:~ rafaelcirolini$ kubectl get pods | grep app
app-7957d8f69c-mmbb7 1/1 Running 0 3h
app-7957d8f69c-rjmg5 1/1 Running 0 3h

Isso tudo aconteceu pq no estágio de “Deploy PROD” nós usamos dentro do Jenkins um comando do kubectl que manda aplicar o arquivo yaml de dentro do projeto que cria e configura um "service" e um "deployment" dentro do Kubernetes. Logo depois garantimos que a imagem foi atualizada e acompanhamos a execução do update para ver se tudo foi feito conforme esperado.

Criando uma nova versão

Se por exemplo, agora no meu código eu trocar o Hello Dave! por Hello World! Basta eu alterar o código no meu Git, e clicar em Build Now novamente.

Agora aparecem os dois jobs que executamos, se você olhar os pods vai ver que eles foram recriados a 1m:

MacBook-Air-de-Rafael:~ rafaelcirolini$ kubectl get pods | grep app
app-7f77cf489d-9c64n 1/1 Running 0 1m
app-7f77cf489d-frr62 1/1 Running 0 1m

Tudo isso sem interrupção do serviço graças a camada de service do Kubernetes. E se rodar um "minikube service app", ou simplesmente dar um F5 na pagina que ficou aberta da aplicação vai ver que mudou para Hello World!

O Registry

Estamos usando um repositório local chamado Registry para guardar as nossas imagens Docker da aplicação. Podemos ver as imagens geradas acessando a interface grafica:

MacBook-Air-de-Rafael:~ rafaelcirolini$ minikube service registry-ui
Opening kubernetes service default/registry-ui in default browser...

E então vamos poder ver todas as versões da aplicação versionadas pelo commit id do git.

Conclusão

A ideia aqui era mostrar como é possível criar um pipeline de entrega continua usando o Jenkins e o Kubernetes. Criei 3 estágios básicos, mas poderia ter feito muitos mais, com certeza ficou faltando coisas como testes unitários, testes integrados, deploys em múltiplos ambientes, tipos de deploy em prod como configurações do Rolling Update, Canary ou Blue Green.

O principal é poder adaptar isso a cada tipo de projeto ou necessidade que você utilize.