Automatize a implementação de dependências de pod no Kubernetes

8 min de leitura
Patrocinado
Imagem de: Automatize a implementação de dependências de pod no Kubernetes
Avatar do autor

Equipe TecMundo

Você geralmente precisa sincronizar a implementação de um novo documento de design ou uma atualização de API com o código da aplicação relacionada? Este artigo é uma inspiração para times de DevOps que querem automatizar suas dependências pod no Kubernetes.

Requisitos

Para aproveitar este artigo ao máximo, você precisa ter conhecimento geral sobre Kubernetes, containers, implementação e integração contínuas, bem como design de documentos do Cloudant.

Tempo estimado

O artigo requer cerca de 15 minutos para leitura completa.

Até onde podemos automatizar a parte Ops do DevOps?

Como desenvolvedor, quero ter controle total. Quero definir alterações nas dependências externas diretamente no código da minha aplicação. Desejo implementar atualizações na minha aplicação Node.js ou Java e vê-la sendo lançada juntamente com a versão antiga. Quero evitar o tempo de inatividade e os conflitos entre as visualizações de banco de dados de que meu serviço web precisava antes e as visualizações de que ele precisa agora.

Afinal, a tendência da automatização em cloud permite que as equipes de DevOps sejam donas da implementação end-to-end.

Felizmente, o Kubernetes está tornando as atualizações um processo muito direto. Com as ferramentas de deploy e integração contínuos, como Travis CI ou a ferramenta de desenvolvedor Toolchain do IBM Cloud, o processo fica cada vez mais fácil de configurar. Várias implementações diárias se tornaram rotina. No entanto, nem sempre é tão fácil sincronizar bancos de dados personalizados ou atualizações de API com o código da aplicação. Normalmente, é necessário orquestrar as implementações.

Este artigo descreve uma das maneiras de sincronizar dependências de pod usando Kubernetes Init Containers.

Escopo de automatização

Aplicações como microsserviços geralmente precisam de um conjunto limitado de dependências, como seu próprio armazenamento e definição de API. As atualizações relacionadas ao armazenamento ou à API geralmente evoluem para uma rotina e exigem automatização. A atualização do documento de design Cloudant é um ótimo exemplo. A parte mais importante do deploy contínuo em aplicações de alta disponibilidade é a garantia de que a visualização atualizada esteja disponível ao mesmo tempo que o serviço é atualizado.

Um processo de deploy automatizado é adequado quando você tem os seguintes tipos de dependências:

  • Bancos de dados (Cloudant, armazenamento de objetos)

  • Documentos de bancos de dados (índices, visualizações)

  • Documentos seed

  • APIs (swaggers)

  • API changelogs

  • Testes de integração automatizados

As configurações para todas essas dependências podem ser armazenadas no repositório do código da aplicação e empacotadas na imagem do docker.

Observação: por “configuração”, quero dizer código, não credenciais. As credenciais devem ser injetadas como variáveis de ambiente ou fornecidas pelos serviços de configuração.

Analise a seguinte estrutura de um exemplo de um diretório de configuração:

config

¦ cloudant

¦ ¦ {databaseSuffixOrFullName}

¦ ¦ ¦ designs

¦ ¦ ¦ ¦ {designName}.json

¦ ¦ ¦ seeds

¦ ¦ ¦ ¦ {seedName}.json

¦ ¦ parameters.json (optional)

¦ apis

¦ ¦ {apiName}

¦ ¦ ¦ {version}.yaml

¦ ¦ ¦ {version}.md

¦ ¦ ¦ {version}.test.json

¦ ¦ ¦ {version}.test.js

Realmente precisamos armazenar todas essas informações com o código da aplicação? Não, mas um controle completo de todas as partes dá liberdade para os desenvolvedores. Serviços totalmente independentes oferecem tranquilidade de que nenhuma das dependências pode criar conflitos entre diferentes versões do serviço.

Init containers: a resposta do Kubernetes para as dependências das aplicações

A maneira recomendada de instalar dependências de aplicações no Kubernetes é através de Init Containers. Os Init Containers são definidos na implementação do pod e bloqueiam o início da aplicação até que sejam executados com êxito.

No exemplo simples a seguir, o Init Container cria um banco de dados Cloudant:

apiVersion: v1

kind: Pod

metadata:

  name: app

spec:

  containers:

  - name: app

    image: registry.ng.bluemix.net/services/app:v1.0.1

    ports:

    - containerPort: 8080

  initContainers:

  - name: deploy

    image: appropriate/curl

    command: [ "sh" ]

    args: [ "-c", "curl -X PUT $URL/dbname" ]

    env:

    - name: URL

      valueFrom:

        configMapKeyRef:

          name: config

          key: cloudantApiKeyUrl

No entanto, a verdadeira lógica de dependência é muito mais complexa e requer uma nova aplicação. Vamos chamá-la de “Gerenciador de Implementação”. O script initContainer pode chamar o Gerenciador de Implementação em execução como um serviço, mas uma abordagem muito mais independente é a criação do Gerenciador de Implementação como outro docker no registro e integrar sua imagem nos scripts de implementação ou gráficos helm.

O arquivo de implementação ficará assim:

apiVersion: v1

kind: Pod

metadata:

  name: app

spec:

  containers:

  - name: app

    image: registry.ng.bluemix.net/services/app:v1.0.1

    ports:

    - containerPort: 8080

  initContainers:

  - name: deploy

    image: registry.ng.bluemix.net/utilities/deployment_manager:v0.0.1

    command: [ "sh" ]

    args: [ "-c", "cd /usr/src/app/;npm run start-as-init-container" ]

    env:

    - name: config

      valueFrom:

        configMapKeyRef:

          name: config

          key: config

Como manter todas as entradas de dependências no código da aplicação

No exemplo anterior, o Gerenciador de Implementação precisa obter todas as entradas da variável de ambiente. Contudo, o objetivo é obter as entradas do código da aplicação, o que significa extrair as entradas (todas, exceto credenciais) do container da aplicação.

O Init Container do Gerenciador de Implementação não detecta o container da aplicação no pod e não pode se comunicar diretamente com ele. O principal truque para permitir essa abordagem é carregar o container da aplicação antecipadamente. Em seguida, extraia a entrada necessária em um volume compartilhado para o Gerenciador de Implementação a utilizar posteriormente.

Os seguintes exemplos mostram o uso de outro Init Container com essa finalidade:

apiVersion: v1

kind: Pod

metadata:

  name: app

spec:

  volumes:

    - name: deployment-volume

      emptyDir: {}

  containers:

  - name: app

    image: registry.ng.bluemix.net/services/app:v1.0.1

    ports:

    - containerPort: 8080

    volumeMounts:

      - name: deployment-volume

        mountPath: "/init"

  initContainers:

  - name: copy

    image: registry.ng.bluemix.net/services/app:v1.0.1

    command: [ "sh" ]

    args: [ "-c", "set -e;cp -v -r /usr/src/app/config/* /init/" ]

    volumeMounts:

      - name: deployment-volume

        mountPath: "/init"

  - name: deploy

    image: registry.ng.bluemix.net/utilities/deployment_manager:v0.0.1

    command: [ "sh" ]

    args: [ "-c", "cd /usr/src/app/;npm run start-as-init-container" ]

    env:

    - name: config

      valueFrom:

        configMapKeyRef:

          name: config

          key: config

    volumeMounts:

      - name: deployment-volume

        mountPath: "/init"

Por que isso funciona

Os containers se iniciam sequencialmente:

  • O primeiro Init Container denominado ‘copy’ carrega a imagem da aplicação, mas substitui o método de inicialização usando um comando de cópia personalizado. Contanto que a imagem do docker da aplicação suporte o script sh, o comando copy extrai todos os arquivos de configuração para o novo volume compartilhado do deployment-volume no caminho /init e sai. Qualquer falha nesse ponto produz um erro e bloqueia a execução das próximas etapas.

  • O próximo Init Container é sua aplicação do Gerenciador de Implementação. Ela usa o mesmo volume compartilhado e localiza todas as entradas de dependência necessárias da aplicação deixadas pelo primeiro container. O container de deploy pode levar o tempo necessário para instalar as dependências. A implementação do pod aguarda até que o processo termine com sucesso.

  • Por fim, o container principal da aplicação carrega o mesmo volume compartilhado. Essa etapa é necessária porque o Gerenciador de Implementação gera um arquivo init.json como saída da inicialização. O arquivo init contém todos os detalhes sobre qual versão de um recurso específico (por exemplo, o documento de design Cloudant) a aplicação deve usar.

Para tornar o mesmo processo reutilizável em ambientes poliglotas, use a nomenclatura padronizada e a estrutura do diretório de configuração. JSON é um dos formatos ideais para entrada e saída, mas você também pode utilizar outros.

O exemplo a seguir mostra a saída do init.json:

{

  "init": {

    "cloudant": {

      "channels": {

        "designs": {

          "search": "search~2019-06-04T13:19:49.745Z"

        },

        "seeds": {

          "taxonomy": "taxonomy"

        }

      }

    },

    "apis": {

      "search": {

        "v3": {

          "api": "API~channels~v3~2019-06-01T23:15:18.609Z"

        }

      }

    }

  }

}

Lançando uma nova aplicação

Suponha que um documento de design Cloudant atualizado seja necessário na versão (v1.0.2) da aplicação que está em execução.

Você quer evitar qualquer tempo de inatividade durante a atualização de vários pods. Portanto, você deve garantir que as duas versões da aplicação possam ser executadas ao mesmo tempo.

Essa situação significa que você não pode simplesmente remover bancos de dados e antigas visualizações. Em vez disso, primeiramente é necessário criar outros e aguardar até que a alteração seja lançada em todos os pods da aplicação na cópia. Então, somente após o sucesso da implementação, você deve garantir que as dependências antigas e não usadas sejam removidas.

Na prática, você precisa escolher um nome diferente para cada versão, como usar um sufixo de data/hora adicionado ao nome do documento de design.

Exemplo: um documento de design Cloudant chamado “search” requerido pela versão v1.0.1 é substituído por uma versão diferente na v1.0.2. O documento inicial, chamado “search ~2019-06-04T13:19:49.745Z” está instalado no banco de dados. No momento da instalação da v1.0.2, a aplicação Gerenciador de Implementação compara a visualização usando um diff profundo. Se achar que a visualização não corresponde à visualização do código da aplicação, ele instala a segunda versão denominada “search ~2019-06-05T08:12:33:123Z”.

Durante o lançamento, os pods antigos ainda usam o primeiro documento de design e os novos pods começam a utilizar o novo. Não há conflito entre os pods, e a transição pode acontecer sem nenhum tempo de inatividade. Além disso, se a aplicação precisar reverter, o mesmo processo pode ser aplicado. A cada momento, cada pod usa exatamente o código de visualização contido nele — aquele com o qual foi testado.

Limpando

Até agora tudo bem. Você tem uma ótima separação de dependências. Mas o que fazer com as dependências que não são mais necessárias?

Como o objetivo é criar um sistema totalmente automatizado, você precisa conhecer todas as dependências atualmente em uso por todos os pods. Considere as seguintes maneiras de rastrear as dependências em uso:

  • Manter o log de dependências usadas pelo Gerenciador de Implementação

  • Expor as dependências usadas pela aplicação mediante solicitação

  • Definir uma a expiração para cada recurso e exigir renovação regular deles

Você pode usar qualquer uma dessas abordagens, desde que elas consigam se recuperar facilmente de falhas. Entretanto, na maioria dos casos, o processo de limpeza precisa ler a lista de todos os pods ativos para determinar se as dependências implementadas ainda são necessárias.

Se necessário, a equipe de DevOps pode investir em uma interface para o administrador supervisionar o processo de limpeza, como no exemplo a seguir:

Soluções alternativas

Você pode usar scripts de implementação e integração contínuos para extrair configurações de dependência do código da aplicação. Um gerente de desenvolvimento customizado pode garantir que as dependências sejam disponibilizadas antes da inicialização da aplicação.

Contudo, o uso de Init Containers geralmente é mais eficiente. A implementação completa pode levar apenas alguns segundos, porque o trabalho acontece diretamente no cluster Kubernetes com todas as imagens já disponíveis. Ademais, uma grande vantagem do Init Containers é que nenhum pod será iniciado sem uma verificação completa de todas as suas dependências.

Resumo

A inclusão de dependências de pod na imagem da aplicação pode simplificar drasticamente o gerenciamento do ciclo de vida de uma aplicação. Ela simplesmente não inicia até que as dependências corretas estejam disponíveis e as atualizações e os downgrades sejam contínuos. Essa abordagem sugerida pode ajudar o time de DevOps a se concentrar mais no desenvolvimento e no valor comercial, em vez de operações.

Para saber mais, confira Kubernetes Init Containers.

...

Quer ler mais conteúdo especializado de programação? Conheça a IBM Blue Profile e tenha acesso a matérias exclusivas, novas jornadas de conhecimento e testes personalizados. Confira agora mesmo, consiga as badges e dê um upgrade na sua carreira!

Você sabia que o TecMundo está no Facebook, Instagram, Telegram, TikTok, Twitter e no Whatsapp? Siga-nos por lá.