Hoje, na Coderockr, utilizamos Pull Requests e Code Reviews como uma ferramenta de qualidade nos nossos desenvolvimentos, e tem garantido resultados nesse sentido.
Mas mesmo com esse processo, eventualmente temos de lidar com alguns problemas como, por exemplo, funções que interferem umas nas outras depois de aprovadas, permitir que os Testers possam avaliar as melhorias, e garantir que todos as mudanças feitas na branch principal possam ser enviadas para produção.
Esses problemas podem ser reduzidos, ou até eliminados, se, mesmo antes de aprovar os PRs, os Testers conseguissem trabalhar sobre essas melhorias e só serem repassadas para a branch principal após a aprovação deles.
Desse modo, o fonte principal não só passou pelo Review de outros desenvolvedores, como foi testado pela equipe de QA, dando ainda mais confiança a ele. Mas subir ambientes de homologação para cada um dos PRs, automaticamente ou sobre demanda, não é um problema trivial, envolve subir máquinas, garantir que esteja rodando a versão atualizada, liberar portas etc.
Uma forma que encontramos para resolver esse problema é utilizando um cluster Kubernetes (ou a versão da Red Hat, o OpenShift), pois essas ações são bem simples de realizar com ele e ainda mais fáceis se forem automatizadas.
Agora, vou explicar como montar um exemplo simples, um para o GitLab e outro para o GitHub, integrando com o OpenShift, da Getup Cloud.
Obs: sobre uma introdução ao Kubernetes/OpenShift pode ver aqui. O cliente de linha de comando pode ser baixado neste link.
GitLab: Integrations, CI, Registry e Environments
A primeira experiência que fizemos foi com o GitLab, principalmente pela integração que ele traz com o Kubernetes, e as outras ferramentas que ele oferece que acabaram cobrindo todo o escopo do problema.
O que queremos montar é um ambiente por branch/PR que deve ser facilmente criado e destruído. Para demonstrar, criei um repositório no GitLab com uma aplicação bem simples que apenas retorna uma página estática, mas é o suficiente para o objetivo.
Primeiramente, criei a base da aplicação usando Docker, a mesma gera uma página com o conteúdo acima. O que vale destacar nesse primeiro momento é que já configurei um processo de CI simples:
build: image: docker:latest services: - docker:dind stage: build script: - docker login -u "gitlab-ci-token" -p "$CI_JOB_TOKEN" $CI_REGISTRY - docker build --pull -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME" . - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME" - echo "Pushing image $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME" only: - branches
Nesse CI, eu construo o contêiner da aplicação para cada commit feito e guardo no registro do próprio GitLab por branch. Dessa forma, tenho uma versão do meu contêiner para cada uma das branchs que forem criadas e vou atualizando essas versões automaticamente a cada alteração.
Obs: Fonte completo até aqui.
Nesse momento, não tenho nenhum deploy, seja de ambiente de teste, produção ou por branch. Então, vamos adicionar um processo de deploy no OpenShift para o ambiente de produção e testes, sendo que o ambiente de testes é atualizado automaticamente para os commits na master e o de produção apenas quando um usuário disparar o deploy.
Para fazer isso, primeiramente, temos de configurar a integração entre o OpenShift e o GitLab. Para isso, vamos em Settings > Integrations e procuramos Kubernetes nas opções. O GitLab irá solicitar algumas informações sobre o ambiente, qual o Namespace, o URL da API do Kubernetes e uma forma de autenticação, que pode ser um Service Token, ou um CA Bundle.
Dessa forma, vou criar um novo Namespace, como fazer isso vai depender do seu vendor de Kubernetes, no caso da Getup Cloud, basta ir em https://portal.getupcloud.com/projects e criar um novo projeto, o nome do projeto será o Namespace.
Uma vez com o Namespace, podemos criar um novo Service Token para ser usado no CI do GitLab. No caso, para criar um Service Token é necessário criar uma ServiceAccount e dar permissões a mesma, e então, pegar o Service Token dela. O script abaixo realiza essas operações:
$ oc login https://api.getupcloud.com:443 Authentication required for https://api.getupcloud.com:443 ... Username: lucas.s.abreu@gmail.com Password: Login successful. ... $ oc project gitlab-k8s-pr-envs #usar o seu projeto Now using project "gitlab-k8s-pr-envs" on server ... $ oc create serviceaccount gitlab serviceaccount "gitlab" created $ oc policy add-role-to-user admin \ system:serviceaccount:gitlab-k8s-pr-envs:gitlab $ oc describe serviceaccount gitlab Name: gitlab Namespace: gitlab-k8s-pr-envs Labels: <none> Image pull secrets: gitlab-dockercfg-qj9o9 Mountable secrets: gitlab-token-6ael2 gitlab-dockercfg-qj9o9 Tokens: gitlab-token-6ael2 gitlab-token-zkk6u $ oc describe secret gitlab-token-6ael2 Name: gitlab-token-6ael2 Namespace: gitlab-k8s-pr-envs Labels: <none> Annotations: kubernetes.io/service-account.name=gitlab kubernetes.io/service-account.uid=zzz Type: kubernetes.io/service-account-token Data ==== ca.crt: 1066 bytes namespace: 18 bytes service-ca.crt: 2182 bytes token: token-do-openshift-que-estou-ocultando
Agora que temos o token gerado, basta adicionar essas informações no GitLab.
Você pode confirmar se passou os dados corretos com o botão de teste no GitLab.
Certo, agora o GitLab consegue conversar com o OpenShift. Podemos, então, alterar nossas regras de CI para criar duas novas etapas: staging e production, que irão realizar o deploy dos nossos ambientes padrão, sendo que staging será disparada automaticamente por commits na master e production ficará como manual.
O .gitlab-ci.yml ficou como abaixo (já usando a integração com OpenShift):
stages: - build - staging - production variables: KUBE_DOMAIN: getup.io build: stage: build image: docker:latest services: - docker:dind script: - docker login -u "gitlab-ci-token" -p "$CI_JOB_TOKEN" $CI_REGISTRY - docker build --pull -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME" . - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME" - echo "Pushing image $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME" only: - branches staging: stage: staging image: lucassabreu/openshift-k8s-cli:latest variables: CI_ENVIRONMENT_URL: http://$CI_PROJECT_NAME-staging.$KUBE_DOMAIN environment: name: staging url: http://$CI_PROJECT_NAME-staging.$KUBE_DOMAIN script: - k8s/deploy only: - master production: stage: production image: lucassabreu/openshift-k8s-cli:latest variables: CI_ENVIRONMENT_URL: http://$CI_PROJECT_NAME.$KUBE_DOMAIN environment: name: production url: http://$CI_PROJECT_NAME.$KUBE_DOMAIN when: manual script: - k8s/deploy only: - master
As mudança são os novos stages staging e production; as variáveis novas KUBE_DOMAIN e CI_ENVIRONMENT_URL; e o script k8s/deploy. Vamos por partes.
A variável KUBE_DOMAIN vai ajudar a deixar o nosso processo de deploy mais simples. Basicamente, nós colocamos nela o domínio base que o OpenShift usa para expor as rotas dele. No caso da Getup, seria “getup.io”. A CI_ENVIRONMENT_URL vai completar a KUBE_DOMAIN e serve para informar o k8s/deploy qual endereço ele deve expor o ambiente, ele deve sempre terminar com o KUBE_DOMAIN e deve ser igual à URL da chave environment, pois é por essa chave que o GitLab sabe onde os ambientes estão expostos.
As etapas de staging e production irão fazer o deploy dos nossos ambientes e, como comentei antes, o ambiente de staging terá deploy automático para todo commit na master, enquanto production irá esperar uma ação do usuário. No mais, as duas etapas são iguais, mudando apenas a URL que estão sendo expostas. Estou usando a imagem lucassabreu/openshift-k8s-cli que é basicamente um ubuntu com o oc instalado.
O script k8s/deploy está abaixo e ele basicamente se autentica contra a API do OpenShift usando o Service Token que criamos antes. Ele destrói a aplicação antiga e executa o deploy de uma nova.
#!/bin/bash oc login "$KUBE_URL" --token "$KUBE_TOKEN" oc project "$KUBE_NAMESPACE" HOSTNAME="$CI_ENVIRONMENT_URL" # remove protocol from URL HOSTNAME="${HOSTNAME/\http:\/\//}" HOSTNAME="${HOSTNAME/\http:\/\//}" IMAGE_TAG="$CI_REGISTRY_IMAGE:$CI_BUILD_REF_NAME" ENV="$CI_ENVIRONMENT_SLUG" echo ">> Deleting old application..." oc delete all -l "app=$CI_ENVIRONMENT_SLUG" echo ">> Deploying image $IMAGE_TAG to env $ENV at $HOSTNAME..." sed " s|__HOSTNAME__|$HOSTNAME|; s|__ENV__|$ENV|; s|__IMAGE_TAG__|$IMAGE_TAG|; " k8s/full.yml | oc apply -f - if [ $? != 0 ]; then exit 1 fi echo ">> Deployed to $CI_ENVIRONMENT_URL"
Vale ressaltar que é importante marcar os componentes do ambiente com app=$CI_ENVIRONMENT_SLUG, pois é assim que o GitLab consegue encontrá-los e lhe retornar status sobre eles.
Também estou usando um truque de “templating” com o YAML, que define os ambientes para poder inserir as variáveis de cada ambiente nele. Existem outras ferramentas mais avançadas, como o Helm, mas para o meu exemplo, templating com sed é o suficiente.
apiVersion: extensions/v1beta1 kind: Deployment metadata: name: hw-dpl-__ENV__ labels: app: __ENV__ spec: replicas: 1 template: metadata: labels: app: __ENV__ name: hw-pod spec: containers: - name: hw-container image: __IMAGE_TAG__ imagePullPolicy: Always ports: - name: web-port containerPort: 8080 --- apiVersion: "v1" kind: Service metadata: name: hw-src-__ENV__ labels: app: __ENV__ spec: ports: - port: 80 targetPort: "web-port" protocol: TCP selector: name: hw-pod app: __ENV__ --- apiVersion: v1 kind: Route metadata: name: __ENV__ labels: app: __ENV__ spec: host: __HOSTNAME__ to: kind: Service name: hw-src-__ENV__
Agora, depois que do commit das alterações, o GitLab faz o build, o deploy da staging e production (manual); podemos ver na área Environments, do GitLab, que os ambientes estão rodando. Ele inclusive traz alguns comandos para facilitar a vida: link para a URL do ambiente, terminal dentro do Pod e opção de Re-deploy.
Obs: Fonte completo até agora.
Agora que temos o build da nossa aplicação e um deploy automatizado, vamos adicionar a função de deploy por branch.
Basicamente precisamos de duas novas etapas no nosso CI: uma para subir o ambiente para um branch e outro para destruir esse ambiente para evitar consumir recursos sem necessidade.
Para isso, fiz as seguintes alterações nos .gitlab-ci.yml:
stages: - build - review - staging - production - cleanup review: stage: review image: lucassabreu/openshift-k8s-cli:latest variables: CI_ENVIRONMENT_URL: http://$CI_PROJECT_NAME-$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN environment: name: r/$CI_COMMIT_REF_NAME url: http://$CI_PROJECT_NAME-$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN on_stop: stop_review script: - k8s/deploy only: - branches except: - master stop_review: stage: cleanup image: lucassabreu/openshift-k8s-cli:latest environment: name: r/$CI_COMMIT_REF_NAME action: stop when: manual variables: GIT_STRATEGY: none script: - oc login "$KUBE_URL" --token "$KUBE_TOKEN" - oc project "$KUBE_NAMESPACE" - oc delete deployments -l "app=$CI_ENVIRONMENT_SLUG" - oc delete all -l "app=$CI_ENVIRONMENT_SLUG" only: - branches except: - master [...]
Basicamente, adicionei as duas novas etapas. O review basicamente faz a mesma coisa que staging, mas usa um nome de ambiente dinâmico baseado na branch; e tem um enviroment:on_stop que basicamente indica o que fazer quando a branch for removida.
Na etapa stop_review, executo alguns comandos para eliminar o ambiente quando for chamada, é importante deixar essa como manual para que ela não apague sozinha o ambiente quando terminar as outras etapas.
Os comandos da etapa stop_review precisam estar definidos diretamente no .gitlab-ci.yml, pois quando essa etapa for executada é possível que a branch e commits dela não existam mais, é também por esse motivo que informamos a variável GIT_STRATEGY como NO, evitando que sequer seja checado se a branch/commit de origem existem.
Agora quando crio uma nova branch, automaticamente é criado um novo ambiente para a mesma no OpenShift.
Para testar, criei a branch a-change e fiz a seguinte alteração:
<img id="logo" src="logo.svg" alt="CodeRocker" title="CodeRocker" /> <h1>Hello World !</h1> + <h2>(with a change)</h2> </body> </html>
Assim que dei o git push, começou o deploy do novo ambiente r/a-change, logo que terminou pude verificar na área de ambientes do GitLab que estava rodando, e tem as mesmas operações disponíveis que os outros, mais a opção de parada (stop_review):
Já rodando as alterações:
Obs: Fontes com essas alterações.
Após essas alterações, podemos implementar a regra de merge apenas após testes da equipe de QA, sem interferência de outras atividades que foram aplicadas no meio do caminho e permitindo um controle melhor sobre o que está pronto para ir para a produção.
O artigo acabou ficando bem grande apenas para falar do processo no GitLab, por isso vou criar um segundo sobre como fazer isso no GitHub.