Mesclagem de 3 vias em werf: implantação no Kubernetes com Helm "turbinado"

Aconteceu o que estávamos (e não só nós) esperando há muito tempo: werf, nossa ferramenta Open Source para construção de aplicações e sua entrega em Kubernetes, agora suporta a aplicação de mudanças através de patches de 3 vias! Além disso, surgiu a possibilidade de adoção de recursos existentes do K8s em releases do Helm sem re-criação desses recursos.

Mesclagem de 3 vias em werf: implantação no Kubernetes com Helm "turbinado"

Em termos simples, basta definir WERF_THREE_WAY_MERGE=enabled — e obtemos um deploy 'como em kubectl apply', compatível com instalações existentes no Helm 2 e até um pouco mais.

Mas vamos começar pela teoria: o que são patches de 3 vias, como as pessoas chegaram ao conceito de sua geração e por que eles são importantes nos processos de CI/CD com infraestrutura baseada em Kubernetes? Em seguida, veremos o que realmente é 3-way-merge no werf, quais modos são usados por padrão e como gerenciá-los.

O que é um patch de 3 vias?

Então, vamos começar com a tarefa de implantar recursos descritos em manifests YAML no Kubernetes.

Para trabalhar com recursos, a API do Kubernetes oferece as seguintes operações principais: create, patch, replace e delete. Supõe-se que com elas seja possível construir um roll-out contínuo de recursos no cluster. Como?

Comandos imperativos do kubectl

A primeira abordagem para gerenciar objetos no Kubernetes é usar comandos imperativos do kubectl para criar, modificar e deletar esses objetos. Em termos simples:

  • com o comando kubectl run pode iniciar um Deployment ou Job:
    kubectl run --generator=deployment/apps.v1 NOME_DO_DEPLOYMENT --image=IMAGEM
  • com o comando kubectl scale — altera o número de réplicas:
    kubectl scale --replicas=3 deployment/mysql
  • etc.

Essa abordagem pode parecer conveniente à primeira vista. No entanto, existem problemas:

  1. É difícil automatizar.
  2. Como refletir a configuração no Git? Como fazer uma revisão das mudanças que ocorrem no cluster?
  3. Como garantir a reprodutibilidade da configuração ao reiniciar?
  4. …

É claro que essa abordagem combina mal com o armazenamento junto ao código do aplicativo e a infraestrutura como código (IaC; ou até mesmo GitOps como uma variante mais moderna que está ganhando popularidade no ecossistema Kubernetes). Portanto, essas equipes não tiveram mais desenvolvimento no kubectl.

Operações create, get, replace e delete

Com a criação primária é simples: enviamos o manifest para a operação do kube API e o recurso é criado. A representação YAML do manifest pode ser armazenada no Git, e para criar - utilizamos o comando create kubectl create -f manifest.yaml a remoção.

Com também é simples: inserimos o mesmo manifest.yaml do Git no comando kubectl delete -f manifest.yaml replace.

Operação replace permite substituir completamente a configuração do recurso por uma nova, sem a necessidade de recriar o recurso. Isso significa que, antes de fazer uma alteração no recurso, é lógico solicitar a versão atual por meio da operação get, alterá-la e atualizá-la com a operação replace. O kube apiserver possui bloqueio otimista , e, se após a operação get o objeto tiver mudado, a operação replace não será bem-sucedida.

Para armazenar a configuração no Git e atualizar usando replace, é necessário realizar a operação get, mesclar a configuração do Git com o que recebemos e executar replace. Normalmente, o kubectl permite apenas usar o comando kubectl replace -f manifest.yaml, onde do Git no comando — um manifesto já completamente preparado (neste caso, mesclado) que deve ser instalado. Portanto, o usuário precisa implementar a mesclagem de manifestos, o que não é uma tarefa trivial…

Vale ressaltar que, embora do Git no comando seja armazenado no Git, não podemos saber de antemão se devemos criar um objeto ou atualizá-lo — isso deve ser feito pelo software do usuário.

No total: podemos construir uma implantação contínua apenas com create, replace e delete, garantindo o armazenamento da configuração da infraestrutura no Git junto com o código e um CI/CD conveniente?

Em princípio, podemos… Para isso será necessário implementar a operação de mesclagem de manifestos e algum tipo de envoltório que:

  • verifica a presença do objeto no cluster,
  • realiza a criação inicial do recurso,
  • atualiza ou o remove.

Ao atualizar, é necessário levar em conta que o recurso pode ter mudado desde a última get e deve tratar automaticamente o caso de bloqueio otimista — fazendo novas tentativas de atualização.

No entanto, por que reinventar a roda, quando o kube-apiserver oferece uma maneira alternativa de atualizar recursos: a operação patch, que remove parte dos problemas descritos do usuário?

Patch

Chegamos aos patches.

Patches são a principal forma de aplicar alterações a objetos existentes no Kubernetes. A operação patch funciona da seguinte forma:

  • o usuário do kube-apiserver precisa enviar um patch em formato JSON e especificar o objeto,
  • e o apiserver irá entender o estado atual do objeto e trazê-lo para a forma desejada.

O bloqueio otimista não é necessário neste caso. Esta operação é mais declarativa em comparação ao replace, embora possa parecer o contrário à primeira vista.

Assim:

  • com a operação create nós criamos um objeto a partir do manifesto do Git,
  • com delete — removemos se o objeto não for mais necessário,
  • com patch — alteramos o objeto, moldando-o da maneira descrita no Git.

No entanto, para fazer isso, é necessário criar um patch correto!

Como os patches funcionam no Helm 2: 2-way-merge

Na primeira instalação do release, o Helm executa uma operação create para os recursos do chart.

Ao atualizar o release do Helm para cada recurso:

  • ele calcula o patch entre a versão do recurso do chart anterior e a versão atual do chart,
  • aplica esse patch.

Esse patch chamamos de 2-way-merge patch, porque na sua criação participam 2 manifestos:

  • o manifesto do recurso da versão anterior do release,
  • o manifesto do recurso da versão atual.

Ao excluir, a operação delete é invocada no kube apiserver para recursos que foram declarados no release anterior, mas não declarados no atual.

A abordagem com 2 way merge patch tem um problema: ela leva a desincronizações entre o estado real do recurso no cluster e o manifesto no Git.

Ilustração do problema com um exemplo

  • No Git, no chart, há um manifesto onde o campo image no Deployment tem o valor ubuntu:18.04.
  • O usuário por meio de kubectl edit mudou o valor desse campo para ubuntu:19.04.
  • Ao reaplicar o chart do Helm não é gerado um patch,porque o campo image na versão anterior do release e no chart atual são iguais.
  • Após a reaplicação image permanece ubuntu:19.04, embora no chart esteja escrito ubuntu:18.04.

A desincronização ocorreu e perdemos a declaratividade.

O que é um recurso sincronizado?

De modo geral, não é possível obter a correspondência total entre o manifesto do recurso no cluster em funcionamento e o manifesto no Git. Porque no manifesto real podem haver anotações/etiquetas de serviço, contêineres adicionais e outros dados, que são adicionados e removidos do recurso dinamicamente por controladores. Esses dados não podemos e não queremos manter no Git. No entanto, queremos que, ao aplicar, os campos que indicamos claramente no Git assumam os valores correspondentes.

Assim, surge uma regra geral para um recurso sincronizado: ao aplicar um recurso, só se pode alterar ou remover aqueles campos que estão claramente especificados no manifesto do Git (ou que foram especificados na versão anterior e agora foram removidos).

3-way-merge patch

A ideia principal 3-way-merge patch: gera-se um patch entre a última versão aplicada do manifesto do Git e a versão-alvo do manifesto do Git, levando em conta a versão atual do manifesto do cluster em funcionamento. O patch final deve estar em conformidade com a regra do recurso sincronizado:

  • Novos campos adicionados na versão de destino são incluídos por meio de um patch;
  • Os campos existentes na última versão aplicada e que não estão presentes na versão de destino são zerados por meio do patch;
  • Os campos na versão atual do objeto que diferem da versão de destino do manifesto são atualizados por meio de um patch.

É exatamente assim que os patches são gerados kubectl apply:

  • A última versão aplicada do manifesto é salva na anotação do próprio objeto,
  • o alvo é retirado do arquivo YAML especificado,
  • o atual vem do cluster em funcionamento.

Agora que entendemos a teoria, é hora de contar o que fizemos no werf.

Aplicação de alterações no werf

Anteriormente, o werf, assim como o Helm 2, usava patches de mesclagem de 2 vias.

Repair patch

Para migrar para o novo tipo de patches — 3-way-merge — o primeiro passo foi a introdução dos chamados repair-patches.

Durante o deployment, é utilizado um patch padrão de mesclagem de 2 vias, mas o werf também gera um patch adicional que sincroniza o estado real do recurso com o que está escrito no Git (um patch é criado utilizando a mesma regra de recurso sincronizado descrita anteriormente).

Em caso de desvio, no final do deployment o usuário recebe um WARNING com a mensagem correspondente e um patch que deve ser aplicado para trazer o recurso ao estado sincronizado. Esse patch também é registrado em uma anotação especial werf.io/repair-patch. Espera-se que o usuário aplique-o manualmente; o werf não aplicará esse patch de forma alguma.

A geração de repair-patches é uma medida temporária que permite testar a criação de patches com base no princípio de 3-way-merge, mas esses patches não são aplicados automaticamente. No momento, esse modo de operação está habilitado por padrão.

3-way-merge patch apenas para novos lançamentos

A partir de 1º de dezembro de 2019, versões beta e alpha do werf começam padrão a usar patches completos de 3-way-merge para aplicar alterações apenas para novos lançamentos do Helm, implementados por meio do werf. Lançamentos já existentes continuarão a utilizar a abordagem de 2-way-merge + repair-patches.

Esse modo de operação pode ser explicitamente habilitado configurando WERF_THREE_WAY_MERGE_MODE=onlyNewReleases já agora.

Nota: a funcionalidade apareceu no werf ao longo de vários lançamentos: no canal alpha, ela se tornou estável na versão v1.0.5-alpha.19, e no canal beta — a partir de v1.0.4-beta.20.

3-way-merge patch para todos os lançamentos

A partir de 15 de dezembro de 2019, as versões beta e alpha do werf começarão a usar, por padrão, patches de mesclagem 3-way completos para aplicar alterações em todos os lançamentos.

Esse modo de operação pode ser explicitamente habilitado configurando WERF_THREE_WAY_MERGE_MODE=enabled já agora.

Como lidar com o dimensionamento automático de recursos?

No Kubernetes, existem 2 tipos de dimensionamento automático: HPA (horizontal) e VPA (vertical).

O horizontal seleciona automaticamente o número de réplicas, enquanto o vertical determina a quantidade de recursos. Tanto o número de réplicas quanto os requisitos de recursos são especificados no manifesto do recurso (consulte spec.replicas ou spec.containers[].resources.limits.cpu, spec.containers[].resources.limits.memory e outros).

Problema: se o usuário configurar um recurso no chart de forma que valores específicos de recursos ou réplicas sejam indicados e se escaladores automáticos forem ativados para esse recurso, a cada implantação o werf irá redefinir esses valores para o que está registrado no manifesto do chart.

Há duas soluções para o problema. Primeiro, é melhor evitar especificar explicitamente valores escaláveis no manifesto do chart. No entanto, se essa opção não for viável por algum motivo (por exemplo, porque é conveniente definir limites iniciais de recursos e o número de réplicas no chart), o werf oferece as seguintes anotações:

  • werf.io/set-replicas-only-on-creation=true
  • werf.io/set-resources-only-on-creation=true

Com essa anotação, o werf não redefinirá os valores correspondentes em cada implantação, mas apenas os definirá na criação inicial do recurso.

Para mais informações, consulte a documentação do projeto sobre HPA e VPA.

Proibir o uso de patch 3-way-merge

Por enquanto, o usuário pode proibir o uso de novos patches no werf através da variável de ambiente WERF_THREE_WAY_MERGE_MODE=disabled. No entanto, a partir de 1 de março de 2020, essa proibição deixará de funcionar e apenas patches de mesclagem 3-way poderão ser utilizados.

Adoção de recursos no werf

A implementação do método de aplicação de alterações utilizando patches de mesclagem 3-way nos permitiu imediatamente implementar um recurso como a adoção de recursos existentes no cluster em lançamentos do Helm.

O Helm 2 tem um problema: não é possível adicionar um recurso que já existe no cluster aos manifestos do chart sem recriar esse recurso do zero (consulte #6031, #3275). Nós ensinamos o werf a aceitar recursos existentes no lançamento. Para isso, é necessário definir uma anotação na versão atual do recurso do cluster ativo (por exemplo, usando kubectl edit):

"werf.io/allow-adoption-by-release": RELEASE_NAME

Agora, o recurso precisa ser descrito no gráfico e, na próxima implantação do lançamento com o nome correspondente, o recurso existente será aceito nesse lançamento e permanecerá sob sua gestão. Além disso, durante o processo de aceitação do recurso no lançamento, o werf levará o estado atual do recurso do cluster ativo para o estado descrito no gráfico, usando os mesmos patches de 3-way-merge e a regra de recurso sincronizado.

Nota: configuração WERF_THREE_WAY_MERGE_MODE não afeta a adoção de recursos — em caso de adoção, sempre é utilizado um patch de 3-way-merge.

Detalhes podem ser encontrados em documentação.

Conclusões e planos futuros

Espero que, após este artigo, tenha ficado mais claro o que são patches de 3-way-merge e por que chegamos a isso. Do ponto de vista prático do desenvolvimento do projeto werf, sua implementação foi mais um passo na direção da melhoria das implantações semelhantes ao Helm. Agora podemos esquecer os problemas de sincronização de configuração que frequentemente surgiam ao usar o Helm 2. Ao mesmo tempo, uma nova e útil funcionalidade de adoção de recursos Kubernetes já baixados para o lançamento do Helm foi adicionada.

Na implantação semelhante ao Helm, ainda permanecem alguns problemas e dificuldades, como o uso de templates Go, e continuaremos a resolvê-los.

Informações sobre métodos de atualização de recursos e adoção também podem ser encontradas em esta página de documentação.

Helm 3

Um ponto separado que merece atenção é o lançamento recentemente da nova versão maior do Helm — v3, que também utiliza patches de 3-way-merge e elimina o Tiller. A nova versão do Helm requer migração que as instalações já existentes sejam convertidas para o novo formato de armazenamento de lançamentos.

O werf, por sua vez, já se livrou do uso do Tiller, mudou para 3-way-merge e adicionou muito mais, permanecendo compatível com as instalações existentes do Helm 2 (nenhum script de migração precisa ser executado). Portanto, enquanto o werf não migrar para o Helm 3, os usuários do werf não perdem as principais vantagens do Helm 3 sobre o Helm 2 (elas também estão presentes no werf).

No entanto, a migração do werf para a base de código do Helm 3 é inevitável e acontecerá em um futuro próximo. Presumivelmente, será no werf 1.1 ou 1.2 (no momento, a versão principal do werf é 1.0; mais detalhes sobre a estrutura de versionamento do werf podem ser vistos em aqui). Nesse meio tempo, o Helm 3 deve se estabilizar.

P.S.

Confira também em nosso blog:

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster