Cloud Computing

4 abr, 2018

CI com microsserviços

Publicidade

A arquitetura de microsserviços é, atualmente, um dos modelos de desenvolvimento mais utilizados. Como podemos usá-los para orquestrar um CI de forma satisfatória? Neste artigo, vamos ver como usar o Kubernetes e o Wercker para fazer isso.

Kubernetes

O Kubernetes (ou k8s) é um projeto que nasceu dentro da Google como uma alternativa para a gerência de containers LXC. Em 2015, foi doada a Cloud Native Computing Foundation, tornando-se de código aberto. Desde então, o Kubernetes tem sido um dos principais meios de gerenciar grandes quantidades de containers em um ambiente único, ao lado do Docker Swarm e o Mesos.

O que o k8s faz é basicamente organizar os containers em modelos lógicos que podem ser gerenciadas através de uma API REST, tudo em um cluster gerenciado por um nó mestre que controla três ou mais nós filhos que contêm suas estruturas de gerenciamento.

O grande problema dos sistemas de CI antes do k8s era que não havia possibilidade de fazermos a interação “nativa” para seu ambiente de produção, publicando, escalando ou removendo containers de forma eficiente.

Era preciso escrever um script shell que seria executado pela ferramenta, geralmente utilizando a API do provedor cloud, para interagir com o Docker dentro de cada instância individual, o que implicava que o CI precisava saber onde todos os serviços estavam para poder interagir com cada um deles. Com o k8s, podemos fazer a integração através de simples chamadas REST para o nó master sem necessidade de passar por tudo isso.

Veremos como fazer isso.

Wercker

Agora vamos falar um pouco sobre o Wercker, um CI comprado pela Oracle que possui uma integração nativa com o k8s, focado em microsserviços e feito para suprir os problemas que citamos anteriormente. Com ele, fica fácil criar uma integração com o cluster e gerenciar seus deploys, além de todas as funções que um CI tradicional já tem. Para entender as próximas instruções, é importante que você seja familiarizado com o Git, containers e Kubernetes. Também é necessário que você tenha um cluster k8s rodando e uma conta no Docker Hub e no GitHub.

Para começar, baixe este repositório https://github.com/khaosdoctor/imasters-node-http-server. Nele, há toda a estrutura necessária para o exemplo. Vamos explicá-lo passo a passo.

Nele, você encontrará o package.json, com algumas informações relevantes, e o index.js, que contém o código que rodará um simples serviço para nossos testes. Para isso, estamos usando o StandardJS.

No index.js, você também notará que estamos utilizando uma variável de ambiente chamada PORT. Para rodarmos um teste localmente, precisamos exportar essa variável utilizando o comando export PORT=2324 && npm start e depois podemos entrar no endereço localhost para verificar o resultado:

Agora que temos nosso servidor funcionando, podemos partir para a configuração do CI, que começa entrando no site do Wercker (https://wercker.com) e criando sua conta. É importante se conectar com a mesma do seu GitHub – assim, ele pode buscar os repositórios automaticamente quando formos criar a aplicação no sistema.

Você precisará criar um novo repositório no Docker Hub para poder armazenar sua imagem – basta entrar em https://hub.docker.com, fazer seu login e criar o repositório.

Com isso feito, criaremos o nosso repositório no Wercker. Para isso, vá ao botão “+” ao lado da sua foto de usuário e clique em Add Application. Na próxima tela, selecione o SCM – nesse caso, o GitHub. Continue seguindo os passos selecionando o repositório criado anteriormente e as configurações recomendadas de acesso, que é a opção Wercker will check out the code without using an SSH key.

Feito isso, seremos redirecionados para o dashboard – aqui, temos informações importantes a serem notadas. No topo, vamos ter várias abas. Runs é onde teremos o resumo das nossas builds. As abas Workflows e Environment serão as mais importantes para nós.

Em Workflows, vamos ter os pipelines. Será ali que vamos definir os passos que queremos que cada build execute e quando eles devem ser iniciados. Em Environment, será onde vamos colocar nossas variáveis de ambiente, como a variável PORT que definimos antes. As variáveis são definidas em níveis:

  • Organização: Se você tem uma empresa, podemos organizar os times em uma organização. Essas variáveis abrangem todas as aplicações que estão criadas sob ela.
    Projeto: Na aba Environment, estamos criando variáveis de projeto; elas serão visíveis para todos os pipelines da aba Workflows, independentemente da build em que estiverem.
    Workflow: É o tipo mais específico; criamos uma variável de ambiente dentro do passo que vamos executar, e ela só estará disponível ali.

A ordem das variáveis sempre é da mais para a menos específica, ou seja, o Wercker vai procurar uma variável a partir do último item para o primeiro, e a primeira que for encontrada será utilizada. Além disso, uma variável de organização não vai sobrescrever as variáveis de projeto se tiverem o mesmo nome.

Criaremos nossa variável PORT na aba Environment e dar a ela o valor que quisermos na nossa porta:

Para que possamos configurar nossa integração, vamos precisar criar um arquivo wercker.yml na raiz do projeto.

Nesse arquivo, definiremos os processos e cada etapa do build que utilizamos. Em uma visão geral, realizaremos quatro passos:

  1. Vamos instalar todas as dependências e gerar a tag da nossa imagem para vrsionarmos no Docker Hub;
  2. Criar nosso passo de testes;
  3. Fazer o push para o Docker;
  4. Criar o passo de publicação para o k8s.

Primeiramente, vamos criar o arquivo e dizer qual é a imagem base que vamos utilizar, que será a node na versão 8.4. Esta deve ser a primeira linha do arquivo:

# Qual é a imagem base que vamos utilizar
box: node:8.4

Então, vamos definir o primeiro passo, que deve se chamar “build”:

# Definição do primeiro step
build:
  steps:
        - npm-install
        - script:
          name: gerar tag de imagem
          code: |
            export PACKAGE_VERSION=$(node -p "require('./package.json').version")
            export IMAGE_TAG=$PACKAGE_VERSION
            echo "$IMAGE_TAG" >> $WERCKER_SOURCE_DIR/.image_tag

Aqui temos algumas coisas acontecendo. O Wercker já tem um passo predefinido para o comando de instalação, então apenas o chamamos na execução. Depois, definimos um script personalizado para podermos pegar a versão da nossa aplicação do package.json usando o comando node -p, salvando o resultado em uma variável e depois em um arquivo. Precisamos salvar o arquivo, pois reutilizaremos essa variável em outros passos. O Wercker também já tem algumas variáveis pré-configuradas, como o WERCKER_SOURCE_DIR.

Após esse step, vamos criar o passo de testes, apenas executando o comando de testes:

# Definição do step de testes
test:
  steps:
- npm-test

Então, criaremos nosso step mais importante, realizar o push para o hub:

# Definição do step de push
docker-push:
  steps:
        - script:
          name: recupera tag de imagem
          code: |
            export IMAGE_TAG=$(cat $WERCKER_SOURCE_DIR/.image_tag)
        - internal/docker-push:
            username: $DOCKER_USER
            password: $DOCKER_PASS
            repository: $DOCKER_REPO
            tag: $IMAGE_TAG
            working-dir: $WERCKER_SOURCE_DIR
            entrypoint: npm start
registry: https://hub.docker.com

Aqui vamos ter que criar algumas variáveis quando estivermos no nosso pipeline, porque estaremos utilizando-as apenas neste step. Veja que recuperamos o valor da tag que salvamos no arquivo anteriormente. Então, utilizamos um passo interno chamado internal/docker-push para poder realizar o push para o registry.

Por fim, vamos realizar o push para o k8s:

# Definição do step do kubernetes
deploy:
  steps:
        - script:
          name: parsear templates
          code: |
            export IMAGE_TAG=$(cat $WERCKER_SOURCE_DIR/.image_tag)
            mv $WERCKER_SOURCE_DIR/kub/kub_*.template $WERCKER_SOURCE_DIR

        - bash-template

        - script:
          name: movimentar templates
          code: |
            mv $WERCKER_SOURCE_DIR/kub_*.* $WERCKER_SOURCE_DIR/kub
        - riceo/kubectl:
            server: $GCE_KUBERNETES_MASTER
            gcloud-key-json: $GCP_KEY_JSON
            gke-cluster-name: $GCE_CLUSTER_NAME
            gke-cluster-zone: $GCE_CLUSTER_ZONE
            gke-cluster-project: $GCE_CLUSTER_PROJECT
command: apply -f $WERCKER_SOURCE_DIR/kub/

Aqui fazemos várias coisas. Primeiro, convertemos arquivos de templates do Wercker para arquivos válidos do k8s, substituindo as variáveis dentro desses arquivos para que possamos criá-las dinamicamente. Esses arquivos são as definições dos objetos do k8s – no caso, um pod e um service.

Outro ponto importante é que usamos um fork do step padrão kubectl do Wercker, isso é possível graças a uma extensibilidade que o CI tem. Podemos clonar qualquer um dos steps disponíveis no repositório do Wercker e modificá-los para as nossas necessidades; depois, basta que o chamemos aqui, passando os parâmetros – como fizemos com riceo/ kubectl (um repositório do GitHub do usuário riceo).

Para criarmos os arquivos do k8s, abriremos uma nova pasta “kub” e nela criaremos dois arquivos, kub_pod.yml.template e kub_service.yml.template, e também vamos criar outra variável no Wercker chamada SERVICE_NAME, à qual vamos dar um valor de até 15 caracteres para ser o nome de serviço.

Nosso arquivo de pod ficará assim:

apiVersion: v1
kind: Pod
metadata:
  name: ${SERVICE_NAME}-pod
  namespace: default
  labels:
        app: ${SERVICE_NAME}-pod
spec:
  containers:
        - name: ${SERVICE_NAME}
          image: khaosdoctor/imasters-node-http-server:${IMAGE_TAG}
          env:
          - name: PORT
            value: "${PORT}"
          ports:
- containerPort: ${PORT}

Estamos colocando as variáveis do Wercker no estilo ${VAR}, porque quando rodarmos o comando bash-template definido no arquivo wercker.yml no step deploy, essa sintaxe será substituída pelo valor da variável. Faremos o mesmo com o serviço:

apiVersion: v1
kind: Service
metadata:
  name: ${SERVICE_NAME}-svc
  namespace: default
  labels:
        app: ${SERVICE_NAME}-svc
spec:
  type: LoadBalancer
  ports:
  - port: 80
        targetPort: ${PORT}
        protocol: TCP
        name: http
  selector:
app: ${SERVICE_NAME}-pod

Estamos selecionando a porta 80 para entrada, logo, vamos acessar através do endereço padrão, mas o serviço vai rotear para a porta que definimos na nossa aplicação.

Com os dois arquivos criados, criaremos as variáveis definidas no Wercker.yml:

  • DOCKER_USER e DOCKER_PASS: Seu usuário e senha do Docker Hub; lembre-se de que eles devem ser protegidos! O Wercker possui uma opção de transformar uma variável em uma variável protegida, basta selecionar o botão protected antes de salvar.
  • DOCKER_REPO: Será o <seuusuario>/<seuapp> no Docker Hub.

Para podermos criar essas variáveis, precisamos criar o nosso workflow. Para isso, vamos na aba Workflows e criaremos um pipeline para cada step que definimos no nosso arquivo YAML: Build (já vai existir), Teste, Push e Deploy, clicando no botão “Add new Pipeline”.

Podemos colocar um nome qualquer campo da tela, porém o “step name” deve ser exatamente o nome do step que definimos (que é o nível mais alto da hierarquia do YAML):

As opções seguintes definem se esse pipeline irá ser executado ao receber um push, ou após o anterior. Vamos manter a forma padrão, afinal, queremos um pipeline sequencial. Ao criarmos, você perceberá que um campo de variáveis se abrirá no topo, é lá que vamos criar as variáveis de pipeline, o nível mais específico do Wercker.

Precisamos repetir o processo para cada pipeline nova, então teremos três pipelines definidas por nós e uma (build) definida pelo wercker.

Marque a caixa “protected” ao lado da senha para que ela se torne obscura. Note também os nomes que demos para o pipeline. Seguiremos fazendo isto para o deploy:

Estou usando o Google Cloud Platform (GCP) para hospedar meu cluster k8s, porque é a solução mais simples de hoje, já que o próprio sistema de containers do GCP é baseado no k8s, então a integração é instantânea. Isso implica que o step de deploy para o k8s é específico para essa plataforma. Se pretende usar outro serviço de cloud, considere ler a documentação do Wercker para informações sobre como é possível integrar com esses serviços, ou construir o seu próprio step se necessário.

Por usar o GCP, precisaremos de outras variáveis que só teremos através dele:

  • GCP_KEY_JSON: É a chave de acesso que pode ser obtida através do painel de Service Accounts. Ao gerar uma chave, você poderá baixá-la no formato JSON; quando criar essa variável de ambiente, utilize o comando tr -d ‘\n’ < suachave.json para poder remover todas as quebras de linha do arquivo e, só aí, colar o conteúdo no Wercker.
  • GCE_KUBERNETES_MASTER: É o IP do seu master node do k8s.
  • GCE_CLUSTER_NAME: É o nome do seu cluster no GCP.
  • GCE_CLUSTER_ZONE: É a zona na qual seu cluster foi criado.
  • GCE_CLUSTER_PROJECT: É o ID do seu projeto no GCP (no topo da tela).

Não se esqueça de proteger sua chave:

Com tudo criado, basta criarmos a sequência de pipelines; no momento, deixaremos a execução em todas as branches, em um projeto real em que deveríamos restringir as pipelines de produção para as branches de produção:

Não coloquei o step de deploy ali, porque quero fazer o deploy manualmente – o CI apenas deixará tudo pronto. Agora, vamos rodar a build; na guia Runs, vamos ter o status detalhado. Ao finalizarmos, você poderá conferir que o Dockerhub mostrará a tag correta da imagem enviada:

A cada alteração de versão, teremos uma nova imagem gerada em uma nova tag – podemos passá-la também para o k8s como uma label. Agora que temos a imagem, vamos realizar o deploy entrando no último passo da última build (push-para-docker) a partir da guia Runs e clicando em “More Actions”. De lá, podemos selecionar qualquer outra pipeline para execução, vamos selecionar o deploy.

Uma nova linha será adicionada na guia Runs e nosso deploy começará. Ao final do processo, veremos no k8s os artefatos criados, o pod:

E nosso service:

Agora, se acessarmos o IP determinado, veremos:

E assim publicamos o microserviço de maneira totalmente automatizada! A partir daí, podemos estender o CI para fazer qualquer parte da build e automaticamente realizar o deploy para o k8s.

A grande vantagem é que o processo é facilmente replicável, pois ao configurar o primeiro serviço, todos os demais são quase idênticos, basta copiar os registros de um para o outro e mover as variáveis globais entre eles para um escopo de organização.

***

Artigo publicado na revista iMasters, edição #25: https://issuu.com/imasters/docs/25