Ambientes por Branch com OpenShift Next Gen

10 minute read Published:

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 podem 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ó 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 no mesmo.

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 esta 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.

Sobre uma introdução ao Kubernetes/OpenShift pode ver aqui:

Um ambiente simples usando Kubernetes e OpenShift Next Gen - Parte 1

O cliente de linha de comando pode ser baixado em:

openshift/origin origin - Enterprise Kubernetes for Developers

GitLab: Integrations, CI, Registry e Environments

A primeira experiencia 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.

retorno do serviço helloworld

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

.gitlab-ci.yml

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.

Fonte completo até aqui:

Files · v1 · Lucas dos Santos Abreu / k8s-pr-envs


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:  

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:  
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ões, 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

.gitlab-ci.yml (v2)

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 é 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 a 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, 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"

k8s/deploy

Vale ressaltar que é importante marcar os componentes do ambiente com app=$CI_ENVIRONMENT_SLUG, pois é assim que o GitLab consegue encontrar eles 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__

k8s/full.yml

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.

Fonte completo até agora:

Files · v2 · Lucas dos Santos Abreu / k8s-pr-envs


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 uma 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

[...]

.gitlab-ci.yml (v3)

Basicamente adicionei as duas novas etapas, 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>

public/index.html (pr)

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:

Fontes com essas alterações em:

Files · v3 · Lucas dos Santos Abreu / k8s-pr-envs


Após essas alterações podemos implementar a regra de merge apenas após testes pela equipe de QA, sem interferência de outras atividades que foram aplicadas no meio do caminho e permitindo um controle melhor sobre o que esta pronto para ir para a produção.


A postagem acabou ficando bem grande apenas para falar do processo no GitLab, por isso vou criar um segundo post sobre como fazer isso no GitHub, abaixo esta o link para ele:

Ambientes por Branch com OpenShift Next Gen usando GitHub

comments powered by Disqus