Executando equipes no processo de entrega de uma nova versão do aplicativo ao Kubernetes



Em nossa prática, geralmente enfrentamos a tarefa de adaptar aplicativos clientes para execução no Kubernetes. Ao realizar essas obras, surgem vários problemas típicos. Recentemente, abordamos uma delas no artigo Arquivos locais ao portar um aplicativo no Kubernetes e a outra, que já está conectada aos processos de CI / CD, será descrita neste artigo.

Comandos arbitrários com Helm e werf


Um aplicativo não é apenas lógica e dados de negócios, mas também um conjunto de comandos arbitrários que devem ser executados para uma atualização bem-sucedida. Podem ser, por exemplo, migrações para bancos de dados, “garçons” para a disponibilidade de recursos externos, alguns transcodificadores ou desempacotadores, registradores no Service Discovery externo - você pode realizar tarefas diferentes em diferentes projetos.

O que o Kubernetes oferece para resolver esses problemas? O Kubernetes sabe como executar contêineres como pods, portanto a solução padrão é executar um comando a partir de uma imagem. Para fazer isso, há uma primitiva de tarefa no Kubernetes que permite executar o pod com contêineres de aplicativos e acompanhar a conclusão desse pod.

Helm vai um pouco mais longe e sugere o lançamento de trabalhos em diferentes estágios do processo de implantação. Estamos falando de ganchos Helm com os quais você pode executar o Job antes ou depois da atualização dos manifestos de recursos. Em nossa experiência, esse é um ótimo recurso do Helm que pode ser usado para resolver tarefas de implantação.

No entanto, é impossível obter informações atualizadas sobre o estado dos objetos durante a distribuição no Helm, portanto, usamos o utilitário werf , que permite monitorar o status dos recursos durante a distribuição diretamente do sistema de IC e, em caso de falha, diagnosticar a quebra mais rapidamente.

Como se viu, esses recursos úteis Helm e werf às vezes são mutuamente exclusivos, mas sempre há uma saída. Considere como você pode monitorar o status dos recursos e executar comandos arbitrários no exemplo de migrações.

Executando migrações antes da liberação


Uma parte integrante do release de qualquer aplicativo de banco de dados está atualizando o esquema de dados. A implantação padrão para aplicativos que aplicam migrações executando um comando separado implica as seguintes etapas:

  1. atualização da base de código;
  2. início da migração;
  3. alternando tráfego para a nova versão do aplicativo.

No Kubernetes, o processo deve ser o mesmo, mas ajustado para o que precisamos:

  1. iniciar um contêiner com um novo código, que pode conter um novo conjunto de migrações;
  2. inicie o processo de aplicação de migrações nele, tendo feito isso antes de atualizar a versão do aplicativo.

Considere a opção quando o banco de dados para o aplicativo já estiver em execução e não for necessário implantá-lo como parte da liberação que implementa o aplicativo. Dois ganchos são adequados para aplicar migrações:

  • pre-install - funciona na primeira versão Helm do aplicativo após o processamento de todos os modelos, mas antes da criação de recursos no Kubernetes;
  • pre-upgrade - funciona ao atualizar a versão Helm e é executado, como pre-install , após o processamento dos modelos, mas antes da criação de recursos no Kubernetes.

Exemplo de trabalho usando Helm e os dois ganchos mencionados:

 --- apiVersion: batch/v1 kind: Job metadata: name: {{ .Chart.Name }}-apply-migrations annotations: "helm.sh/hook": pre-install,pre-upgrade spec: activeDeadlineSeconds: 60 backoffLimit: 0 template: metadata: name: {{ .Chart.Name }}-apply-migrations spec: imagePullSecrets: - name: {{ required ".Values.registry.secret_name required" .Values.registry.secret_name }} containers: - name: job command: ["/usr/bin/php7.2", "artisan", "migrate", "--force"] {{ tuple "backend" . | include "werf_container_image" | indent 8 }} env: {{ tuple "backend" . | include "werf_container_env" | indent 8 }} - name: DB_HOST value: postgres restartPolicy: Never 

Nota : o modelo YAML acima foi criado levando em consideração as especificidades do werf. Para adaptá-lo a um leme "limpo", basta:

  • substitua {{ tuple "backend" . | include "werf_container_image" | indent 8 }} {{ tuple "backend" . | include "werf_container_image" | indent 8 }} {{ tuple "backend" . | include "werf_container_image" | indent 8 }} para a imagem do contêiner que você precisa;
  • exclua a linha {{ tuple "backend" . | include "werf_container_env" | indent 8 }} {{ tuple "backend" . | include "werf_container_env" | indent 8 }} {{ tuple "backend" . | include "werf_container_env" | indent 8 }} , especificado na chave env .

Portanto, esse modelo do Helm precisará ser adicionado ao diretório .helm/templates , que já contém o restante dos recursos da versão. Quando o werf deploy --stages-storage :local chamado, todos os modelos serão processados ​​primeiro e depois serão carregados no cluster Kubernetes.

Iniciando migrações durante o processo de liberação


A opção acima implica o uso de migrações para o caso em que o banco de dados já está em execução. Mas e se precisarmos lançar a revisão de ramificação para o aplicativo e o banco de dados ser lançado com o aplicativo em uma versão?

Nota : você pode encontrar um problema semelhante ao implantar no ambiente de produção se usar o Serviço com um terminal que contenha o endereço IP do banco de dados para conectar-se ao banco de dados.

Nesse caso, os ganchos de pre-install e pre-upgrade não são adequados para nós, pois o aplicativo tentará aplicar migrações ao banco de dados que ainda não existe . Portanto, é necessário fazer migrações após o lançamento.

Ao usar o Helm, essa tarefa é possível, pois não monitora o status dos aplicativos. Depois de carregar recursos no Kubernetes, os ganchos de post sempre disparam:

  • post-install - após carregar todos os recursos no K8s no primeiro lançamento;
  • post-upgrade - após atualizar todos os recursos no K8s ao atualizar o release.

No entanto, como mencionamos acima, o werf possui um sistema de rastreamento de recursos durante o lançamento. Vou abordar isso um pouco mais detalhadamente:

  • Para rastreamento, o werf usa os recursos da biblioteca kubedog , sobre os quais já falamos no blog.
  • Esse recurso no werf nos permite determinar exclusivamente o status do release e exibir informações sobre a conclusão bem-sucedida ou malsucedida da implantação na interface do sistema de CI / CD.
  • Sem receber essas informações, não se pode falar de nenhuma automação do processo de liberação, pois a criação bem-sucedida de recursos no cluster Kubernetes é apenas um dos estágios. Por exemplo, o aplicativo pode não iniciar devido a uma configuração incorreta ou devido a um problema de rede, mas para ver isso após a helm upgrade , você precisará executar etapas adicionais.

Agora, de volta ao aplicativo de migrações nos ganchos pós-gancho do Helm. Os problemas que encontramos:

  • Muitos aplicativos antes de iniciar de uma maneira ou de outra verificam o estado do circuito no banco de dados. Portanto, sem novas migrações, o aplicativo pode não iniciar.
  • Como o werf, por padrão, garante que todos os objetos estejam no estado Ready , os ganchos de postagem não funcionarão e as migrações falharão.
  • Os objetos de rastreamento podem ser desativados por meio de anotações adicionais, mas é impossível obter informações confiáveis ​​sobre os resultados da implantação.

Como resultado, chegamos ao seguinte:

  • Os trabalhos são criados antes dos principais recursos, portanto, não há necessidade de usar ganchos de capacete para migrações .
  • No entanto, um trabalho com migrações deve ser executado em todas as implantações. Para que isso ocorra, Job deve ter um nome exclusivo (aleatório): nesse caso, para Helm, é sempre que um novo objeto no release será criado no Kubernetes.
  • Com esse lançamento, não faz sentido se preocupar que o Job acumule com migrações, pois todos terão nomes exclusivos, e o Job anterior será excluído com uma nova liberação.
  • Um trabalho com migrações deve ter um contêiner init que verifique a disponibilidade do banco de dados - caso contrário, obtemos uma implantação descartada (o trabalho cairá no contêiner init).

A configuração resultante é mais ou menos assim:

 --- apiVersion: batch/v1 kind: Job metadata: name: {{ printf "%s-apply-migrations-%s" .Chart.Name (now | date "2006-01-02-15-04-05") }} spec: activeDeadlineSeconds: 60 backoffLimit: 0 template: metadata: name: {{ printf "%s-apply-migrations-%s" .Chart.Name (now | date "2006-01-02-15-04-05") }} spec: imagePullSecrets: - name: {{ required ".Values.registry.secret_name required" .Values.registry.secret_name }} initContainers: - name: wait-db image: alpine:3.6 ommand: ["/bin/sh", "-c", "while ! nc -z postgres 5432; do sleep 1; done;"] containers: - name: job command: ["/usr/bin/php7.2", "artisan", "migrate", "--force"] {{ tuple "backend" . | include "werf_container_image" | indent 8 }} env: {{ tuple "backend" . | include "werf_container_env" | indent 8 }} - name: DB_HOST value: postgres restartPolicy: Never 

NB : A rigor, os contêineres init para verificar a disponibilidade do banco de dados são melhor utilizados.

Um exemplo de modelo universal para todas as operações de implantação


No entanto, as operações que precisam ser executadas durante a liberação podem ser mais do que o lançamento das migrações já mencionadas. Você pode controlar a ordem de execução do trabalho não apenas pelos tipos de ganchos, mas também atribuindo peso a cada um deles - por meio da anotação helm.sh/hook-weight . Os ganchos são classificados por peso em ordem crescente e, se o peso for o mesmo, por nome do recurso.

Com um grande número de trabalhos, é conveniente criar um modelo universal para trabalhos e colocar a configuração em values.yaml . O último pode ser assim:

 deploy_jobs: - name: migrate command: '["/usr/bin/php7.2", "artisan", "migrate", "--force"]' activeDeadlineSeconds: 120 when: production: 'pre-install,pre-upgrade' staging: 'pre-install,pre-upgrade' _default: '' - name: cache-clear command: '["/usr/bin/php7.2", "artisan", "responsecache:clear"]' activeDeadlineSeconds: 60 when: _default: 'post-install,post-upgrade' 

... e o próprio modelo é assim:

 {{- range $index, $job := .Values.deploy_jobs }} --- apiVersion: batch/v1 kind: Job metadata: name: {{ $.Chart.Name }}-{{ $job.name }} annotations: "helm.sh/hook": {{ pluck $.Values.global.env $job.when | first | default $job.when._default }} "helm.sh/hook-weight": "1{{ $index }}" spec: activeDeadlineSeconds: {{ $job.activeDeadlineSeconds }} backoffLimit: 0 template: metadata: name: {{ $.Chart.Name }}-{{ $job.name }} spec: imagePullSecrets: - name: {{ required "$.Values.registry.secret_name required" $.Values.registry.secret_name }} initContainers: - name: wait-db image: alpine:3.6 ommand: ["/bin/sh", "-c", "while ! nc -z postgres 5432; do sleep 1; done;"] containers: - name: job command: {{ $job.command }} {{ tuple "backend" $ | include "werf_container_image" | indent 8 }} env: {{ tuple "backend" $ | include "werf_container_env" | indent 8 }} - name: DB_HOST value: postgres restartPolicy: Never {{- end }} 

Essa abordagem permite adicionar rapidamente novos comandos ao processo de liberação e torna a lista de comandos executáveis ​​mais visual.

Conclusão


O artigo fornece exemplos de modelos que permitem descrever operações comuns que você precisa executar no processo de liberação de uma nova versão do aplicativo. Embora tenham sido o resultado da experiência na implementação de processos de IC / CD em dezenas de projetos, não insistimos que haja apenas uma solução certa para todas as tarefas. Se os exemplos descritos no artigo não atenderem às necessidades do seu projeto, teremos prazer em ver situações nos comentários que ajudariam a complementar esse material.

Comentário dos desenvolvedores do werf:
No futuro, a werf planeja introduzir estágios de recursos configuráveis ​​pelo usuário. Com a ajuda de tais estágios, será possível descrever os dois casos e não apenas.

PS


Leia também em nosso blog:

Source: https://habr.com/ru/post/pt476320/


All Articles