werf - nossa ferramenta para CI/CD no Kubernetes (visão geral e relatório em vídeo)

27 de maio, no salão principal da conferência DevOpsConf 2019, realizada como parte do festival RIT++ 2019, como parte da seção “Entrega Contínua”, foi fornecido um relatório “werf - nossa ferramenta para CI/CD em Kubernetes”. Ele fala sobre aqueles problemas e desafios que todos enfrentam ao implantar no Kubernetes, bem como sobre nuances que podem não ser imediatamente perceptíveis. Analisando possíveis soluções, mostramos como isso é implementado em uma ferramenta Open Source bem.

Desde a apresentação, nosso utilitário (anteriormente conhecido como dapp) atingiu um marco histórico de 1000 estrelas no GitHub — esperamos que sua crescente comunidade de usuários facilite a vida de muitos engenheiros de DevOps.

werf - nossa ferramenta para CI/CD no Kubernetes (visão geral e relatório em vídeo)

Então, nós apresentamos vídeo da reportagem (~47 minutos, muito mais informativo que o artigo) e o trecho principal dele em forma de texto. Ir!

Entregando código para Kubernetes

A conversa não será mais sobre werf, mas sobre CI/CD no Kubernetes, o que implica que nosso software é empacotado em contêineres Docker (falei sobre isso em Relatório de 2016), e K8s serão usados ​​para executá-lo em produção (mais sobre isso em 2017 ano).

Como é a entrega no Kubernetes?

  • Existe um repositório Git com o código e instruções para construí-lo. O aplicativo é integrado a uma imagem Docker e publicado no Docker Registry.
  • O mesmo repositório também contém instruções sobre como implantar e executar o aplicativo. Na fase de implantação, essas instruções são enviadas ao Kubernetes, que recebe a imagem desejada do registro e a inicia.
  • Além disso, geralmente há testes. Algumas delas podem ser feitas ao publicar uma imagem. Você também pode (seguindo as mesmas instruções) implantar uma cópia do aplicativo (em um namespace K8s separado ou em um cluster separado) e executar testes lá.
  • Finalmente, você precisa de um sistema de CI que receba eventos do Git (ou cliques em botões) e chame todos os estágios designados: construir, publicar, implantar, testar.

werf - nossa ferramenta para CI/CD no Kubernetes (visão geral e relatório em vídeo)

Existem algumas notas importantes aqui:

  1. Porque temos uma infraestrutura imutável (infraestrutura imutável), a imagem do aplicativo usada em todas as etapas (preparação, produção, etc.), deve haver um. Falei sobre isso com mais detalhes e com exemplos. aqui.
  2. Porque seguimos a abordagem de infraestrutura como código (IAC), o código do aplicativo, instruções para montagem e lançamento devem ser exatamente em um repositório. Para obter mais informações sobre isso, consulte o mesmo relatório.
  3. Cadeia de entrega (entrega) normalmente vemos assim: o aplicativo foi montado, testado, lançado (fase de lançamento) e é isso - a entrega ocorreu. Mas, na realidade, o usuário recebe o que você implementou, não aí quando você entregou para produção, e quando ele conseguiu ir lá e essa produção funcionou. Então acredito que a cadeia de entrega termina apenas na fase operacional (corre), ou mais precisamente, ainda no momento em que o código foi retirado da produção (substituindo-o por um novo).

Voltemos ao esquema de entrega acima no Kubernetes: ele foi inventado não apenas por nós, mas literalmente por todos que lidaram com esse problema. Na verdade, esse padrão agora é chamado de GitOps (você pode ler mais sobre o termo e as ideias por trás dele aqui). Vejamos as etapas do esquema.

Estágio de construção

Parece que podemos falar sobre a construção de imagens Docker em 2019, quando todos souberem como escrever Dockerfiles e executá-los docker build?.. Aqui estão as nuances que gostaria de prestar atenção:

  1. Peso da imagem importa, então use em várias fasesdeixar na imagem apenas o aplicativo realmente necessário para a operação.
  2. Número de camadas deve ser minimizado pela combinação de cadeias de RUN-comandos de acordo com o significado.
  3. No entanto, isso acrescenta problemas depuração, porque quando o assembly trava, você precisa encontrar o comando correto na cadeia que causou o problema.
  4. Velocidade de montagem importante porque queremos implementar mudanças rapidamente e ver os resultados. Por exemplo, você não deseja reconstruir dependências em bibliotecas de linguagem toda vez que cria um aplicativo.
  5. Muitas vezes, de um repositório Git você precisa muitas imagens, que pode ser resolvido por um conjunto de Dockerfiles (ou estágios nomeados em um arquivo) e um script Bash com sua montagem sequencial.

Esta foi apenas a ponta do iceberg que todos enfrentam. Mas existem outros problemas, em particular:

  1. Muitas vezes, na fase de montagem, precisamos de algo montar (por exemplo, armazenar em cache o resultado de um comando como apt em um diretório de terceiros).
  2. Nós queremos Ansible em vez de escrever em shell.
  3. Nós queremos construir sem Docker (por que precisamos de uma máquina virtual adicional na qual precisamos configurar tudo para isso, quando já temos um cluster Kubernetes no qual podemos executar contêineres?).
  4. Montagem paralela, que pode ser entendido de diferentes maneiras: diferentes comandos do Dockerfile (se for usado multi-estágio), vários commits do mesmo repositório, vários Dockerfiles.
  5. Montagem distribuída: Queremos coletar coisas em cápsulas que são "efêmeras" porque seu cache desaparece, o que significa que ele precisa ser armazenado em algum lugar separado.
  6. Finalmente, nomeei o auge dos desejos magia automática: O ideal seria ir ao repositório, digitar algum comando e obter uma imagem pronta, montada com o entendimento de como e o que fazer corretamente. No entanto, pessoalmente não tenho a certeza de que todas as nuances possam ser previstas desta forma.

E aqui estão os projetos:

  • moby/kit de construção — um construtor da Docker Inc (já integrado nas versões atuais do Docker), que está tentando resolver todos esses problemas;
  • Kaniko — um construtor do Google que permite construir sem Docker;
  • Buildpacks.io — a tentativa do CNCF de fazer mágica automática e, em particular, uma solução interessante com rebase para camadas;
  • e vários outros utilitários, como construir, ferramentas genuínas/img...

...e veja quantas estrelas eles têm no GitHub. Ou seja, por um lado, docker build existe e pode fazer alguma coisa, mas na realidade o problema não está completamente resolvido - prova disso é o desenvolvimento paralelo de coletores alternativos, cada um dos quais resolve alguma parte dos problemas.

Montagem em werf

Então nós temos que bem (mais cedo famoso como dapp) — Um utilitário de código aberto da empresa Flant, que fabricamos há muitos anos. Tudo começou há 5 anos com scripts Bash otimizando a montagem de Dockerfiles e, nos últimos 3 anos, o desenvolvimento completo foi realizado no âmbito de um projeto com seu próprio repositório Git (primeiro em Ruby, e depois reescrever to Go, e ao mesmo tempo renomeado). Quais problemas de montagem são resolvidos no werf?

werf - nossa ferramenta para CI/CD no Kubernetes (visão geral e relatório em vídeo)

Os problemas sombreados em azul já foram implementados, a construção paralela foi feita no mesmo host e os problemas destacados em amarelo estão planejados para serem concluídos até o final do verão.

Estágio de publicação em registro (publicar)

Nós discamos docker push... - o que poderia ser difícil em enviar uma imagem para o registro? E aí surge a pergunta: “Que tag devo colocar na imagem?” Surge porque temos GitflowGenericName (ou outra estratégia Git) e Kubernetes, e a indústria está tentando garantir que o que acontece no Kubernetes siga o que acontece no Git. Afinal, Git é nossa única fonte de verdade.

O que há de tão difícil nisso? Garanta a reprodutibilidade: de um commit no Git, que é de natureza imutável (imutável), para uma imagem Docker, que deve ser mantida igual.

Também é importante para nós determinar origem, porque queremos entender a partir de qual commit o aplicativo em execução no Kubernetes foi construído (então podemos fazer diffs e coisas semelhantes).

Estratégias de marcação

O primeiro é simples dia de git. Temos um registro com uma imagem marcada como 1.0. O Kubernetes possui palco e produção, onde esta imagem é carregada. No Git fazemos commits e em algum momento marcamos 2.0. Coletamos de acordo com as instruções do repositório e colocamos no registro com a tag 2.0. Nós o lançamos no palco e, se tudo estiver bem, na produção.

werf - nossa ferramenta para CI/CD no Kubernetes (visão geral e relatório em vídeo)

O problema dessa abordagem é que primeiro colocamos a tag e só depois a testamos e implementamos. Por que? Em primeiro lugar, é simplesmente ilógico: estamos lançando uma versão de software que ainda nem testamos (não podemos fazer de outra forma, porque para verificar precisamos colocar uma etiqueta). Em segundo lugar, este caminho não é compatível com Gitflow.

A segunda opção - git commit + tag. O branch master tem uma tag 1.0; para isso no registro - uma imagem implantada na produção. Além disso, o cluster Kubernetes possui contornos de visualização e preparação. A seguir seguimos o Gitflow: no branch principal para desenvolvimento (develop) criamos novos recursos, resultando em um commit com o identificador #c1. Nós o coletamos e publicamos no registro usando este identificador (#c1). Com o mesmo identificador, lançamos para visualização. Fazemos o mesmo com commits #c2 и #c3.

Quando percebemos que existem recursos suficientes, começamos a estabilizar tudo. Crie uma ramificação no Git release_1.1 (na base #c3 de develop). Não há necessidade de coletar esta liberação, porque... isso foi feito na etapa anterior. Portanto, podemos simplesmente implementá-lo para teste. Corrigimos bugs em #c4 e, da mesma forma, lançar para teste. Ao mesmo tempo, o desenvolvimento está em andamento em develop, onde as alterações são retiradas periodicamente release_1.1. Em algum momento, compilamos e enviamos um commit para o teste, o que nos deixa satisfeitos (#c25).

Em seguida, mesclamos (com avanço rápido) o branch de lançamento (release_1.1) no mestre. Colocamos uma tag com a nova versão neste commit (1.1). Mas esta imagem já está coletada no registro, então para não coletá-la novamente, basta adicionar uma segunda tag à imagem existente (agora ela possui tags no registro #c25 и 1.1). Depois disso, colocamos em produção.

Há uma desvantagem de que apenas uma imagem é carregada para teste (#c25), e na produção é meio diferente (1.1), mas sabemos que “fisicamente” são a mesma imagem do registro.

werf - nossa ferramenta para CI/CD no Kubernetes (visão geral e relatório em vídeo)

A verdadeira desvantagem é que não há suporte para commits de mesclagem, você precisa avançar rapidamente.

Podemos ir mais longe e fazer um truque... Vejamos um exemplo de Dockerfile simples:

FROM ruby:2.3 as assets
RUN mkdir -p /app
WORKDIR /app
COPY . ./
RUN gem install bundler && bundle install
RUN bundle exec rake assets:precompile
CMD bundle exec puma -C config/puma.rb

FROM nginx:alpine
COPY --from=assets /app/public /usr/share/nginx/www/public

Vamos construir um arquivo a partir dele de acordo com o seguinte princípio:

  • SHA256 dos identificadores das imagens utilizadas (ruby:2.3 и nginx:alpine), que são somas de verificação de seu conteúdo;
  • todas as equipes (RUN, CMD e assim por diante.);
  • SHA256 dos arquivos que foram adicionados.

... e pegue a soma de verificação (novamente SHA256) desse arquivo. Esse assinatura tudo o que define o conteúdo da imagem Docker.

werf - nossa ferramenta para CI/CD no Kubernetes (visão geral e relatório em vídeo)

Vamos voltar ao diagrama e em vez de commits usaremos essas assinaturas, ou seja marque imagens com assinaturas.

werf - nossa ferramenta para CI/CD no Kubernetes (visão geral e relatório em vídeo)

Agora, quando for necessário, por exemplo, mesclar alterações de um release para o master, podemos fazer um verdadeiro merge commit: ele terá um identificador diferente, mas a mesma assinatura. Com o mesmo identificador iremos lançar a imagem para produção.

A desvantagem é que agora não será possível determinar que tipo de commit foi enviado para produção - as somas de verificação funcionam apenas em uma direção. Este problema é resolvido por uma camada adicional com metadados - contarei mais tarde.

Marcação no werf

No werf fomos ainda mais longe e estamos nos preparando para fazer uma construção distribuída com um cache que não fica armazenado em uma máquina... Então, estamos construindo dois tipos de imagens Docker, nós as chamamos etapa и imagem.

O repositório werf Git armazena instruções específicas de construção que descrevem os diferentes estágios da construção (antes de instalar, instalar, antes da configuração, instalação). Coletamos a imagem do primeiro estágio com uma assinatura definida como o checksum dos primeiros passos. Em seguida, adicionamos o código-fonte, para a nova imagem do estágio calculamos sua soma de verificação... Essas operações são repetidas para todas as etapas, como resultado obtemos um conjunto de imagens de palco. Depois fazemos a imagem final, que também contém metadados sobre sua origem. E marcamos esta imagem de diferentes maneiras (detalhes mais tarde).

werf - nossa ferramenta para CI/CD no Kubernetes (visão geral e relatório em vídeo)

Suponha que depois disso apareça um novo commit no qual apenas o código do aplicativo foi alterado. O que vai acontecer? Para alterações no código, um patch será criado e uma nova imagem do palco será preparada. Sua assinatura será determinada como a soma de verificação da imagem antiga do palco e do novo patch. Uma nova imagem final será formada a partir desta imagem. Comportamento semelhante ocorrerá com mudanças em outros estágios.

Assim, as imagens de palco são um cache que pode ser armazenado de forma distribuída, e as imagens já criadas a partir dele são carregadas no Docker Registry.

werf - nossa ferramenta para CI/CD no Kubernetes (visão geral e relatório em vídeo)

Limpando o registro

Não estamos falando sobre a exclusão de camadas que permaneceram suspensas após as tags excluídas - esse é um recurso padrão do próprio Docker Registry. Estamos falando de uma situação em que muitas tags Docker se acumulam e entendemos que não precisamos mais de algumas delas, mas elas ocupam espaço (e/ou pagamos por isso).

Quais são as estratégias de limpeza?

  1. Você simplesmente não pode fazer nada não limpe. Às vezes é realmente mais fácil pagar um pouco por espaço extra do que desvendar um enorme emaranhado de tags. Mas isso só funciona até certo ponto.
  2. Reinicialização completa. Se você excluir todas as imagens e reconstruir apenas as atuais no sistema de CI, poderá surgir um problema. Se o contêiner for reiniciado em produção, uma nova imagem será carregada para ele - uma que ainda não foi testada por ninguém. Isso mata a ideia de infraestrutura imutável.
  3. Azul verde. Um registro começou a transbordar - carregamos imagens para outro. O mesmo problema do método anterior: em que ponto você pode limpar o registro que começou a transbordar?
  4. Pelo tempo. Excluir todas as imagens com mais de 1 mês? Mas com certeza haverá um serviço que não é atualizado há um mês...
  5. manualmente determinar o que já pode ser excluído.

Existem duas opções verdadeiramente viáveis: não limpar ou uma combinação de azul esverdeado + manualmente. Neste último caso, estamos falando do seguinte: quando você entende que é hora de limpar o registro, você cria um novo e adiciona todas as novas imagens a ele ao longo de, por exemplo, um mês. E depois de um mês, veja quais pods no Kubernetes ainda estão usando o registro antigo e transfira-os também para o novo registro.

A que chegamos bem? Nós coletamos:

  1. Cabeçalho do Git: todas as tags, todas as ramificações - supondo que precisamos de tudo o que está marcado no Git nas imagens (e se não, precisamos excluí-lo no próprio Git);
  2. todos os pods atualmente enviados para o Kubernetes;
  3. ReplicaSets antigos (o que foi lançado recentemente) e também planejamos digitalizar as versões do Helm e selecionar as imagens mais recentes lá.

... e faça uma lista de permissões deste conjunto - uma lista de imagens que não iremos excluir. Limpamos todo o resto, depois encontramos imagens órfãs do palco e as excluímos também.

Estágio de implantação

Declaratividade confiável

O primeiro ponto que gostaria de chamar a atenção na implantação é a implementação da configuração atualizada dos recursos, declarada declarativamente. O documento YAML original que descreve os recursos do Kubernetes é sempre muito diferente do resultado realmente executado no cluster. Porque o Kubernetes adiciona à configuração:

  1. identificadores;
  2. Serviço de informação;
  3. muitos valores padrão;
  4. seção com status atual;
  5. alterações feitas como parte do webhook de admissão;
  6. o resultado do trabalho de vários controladores (e do agendador).

Portanto, quando uma nova configuração de recurso aparece (novo), não podemos simplesmente substituir a configuração atual e “ativa” por ele (viver). Para fazer isso teremos que comparar novo com a última configuração aplicada (última aplicação) e role para viver patch recebido.

Essa abordagem é chamada Mesclagem bidirecional. É usado, por exemplo, no Helm.

Há também Mesclagem bidirecional, que difere nisso:

  • comparando última aplicação и novo, olhamos o que foi excluído;
  • comparando novo и viver, olhamos o que foi adicionado ou alterado;
  • o patch somado é aplicado a viver.

Implementamos mais de 1000 aplicativos com Helm, portanto, vivemos com mesclagem bidirecional. Porém, ele tem uma série de problemas que resolvemos com nossos patches, que ajudam o Helm a funcionar normalmente.

Status de implementação real

Depois que nosso sistema CI gera uma nova configuração para Kubernetes com base no próximo evento, ele a transmite para uso (aplicar) para um cluster - usando Helm ou kubectl apply. Em seguida, ocorre a já descrita fusão N-way, à qual a API Kubernetes responde com aprovação ao sistema CI e ao seu usuário.

werf - nossa ferramenta para CI/CD no Kubernetes (visão geral e relatório em vídeo)

Contudo, há um enorme problema: afinal aplicação bem-sucedida não significa implementação bem-sucedida. Se o Kubernetes entender quais mudanças precisam ser aplicadas e aplicá-las, ainda não sabemos qual será o resultado. Por exemplo, atualizar e reiniciar pods no front-end pode ser bem-sucedido, mas não no back-end, e obteremos versões diferentes das imagens do aplicativo em execução.

Para fazer tudo corretamente, este esquema requer um link adicional - um rastreador especial que receberá informações de status da API do Kubernetes e as transmitirá para análise posterior do estado real das coisas. Criamos uma biblioteca Open Source em Go - cachorro-cubo (veja seu anúncio aqui), que resolve esse problema e está integrado ao werf.

O comportamento deste rastreador no nível werf é configurado usando anotações colocadas em Deployments ou StatefulSets. Anotação principal - fail-mode - compreende os seguintes significados:

  • IgnoreAndContinueDeployProcess — ignoramos os problemas de implantação desta componente e continuamos a implantação;
  • FailWholeDeployProcessImmediately — um erro neste componente interrompe o processo de implantação;
  • HopeUntilEndOfDeployProcess — esperamos que este componente funcione até o final da implantação.

Por exemplo, esta combinação de recursos e valores de anotação fail-mode:

werf - nossa ferramenta para CI/CD no Kubernetes (visão geral e relatório em vídeo)

Quando implantamos pela primeira vez, o banco de dados (MongoDB) pode ainda não estar pronto – as implantações falharão. Mas você pode esperar o momento de começar, e a implantação ainda ocorrerá.

Existem mais duas anotações para kubedog no werf:

  • failures-allowed-per-replica — o número de quedas permitidas para cada réplica;
  • show-logs-until — regula o momento até o qual o werf mostra (em stdout) logs de todos os pods implementados. O padrão é PodIsReady (para ignorar mensagens que provavelmente não queremos quando o tráfego começar a chegar ao pod), mas os valores também são válidos: ControllerIsReady и EndOfDeploy.

O que mais queremos da implantação?

Além dos dois pontos já descritos, gostaríamos:

  • ver Histórico - e apenas os necessários, e não todos seguidos;
  • acompanhar progresso, porque se o trabalho travar “silenciosamente” por vários minutos, é importante entender o que está acontecendo ali;
  • ter reversão automática caso algo dê errado (e portanto é fundamental saber o real estado da implantação). O rollout deve ser atômico: ou vai até o fim ou tudo volta ao estado anterior.

Resultados de

Para nós, como empresa, para implementar todas as nuances descritas em diferentes estágios de entrega (construir, publicar, implantar), um sistema e utilitário de CI são suficientes bem.

Em vez de uma conclusão:

werf - nossa ferramenta para CI/CD no Kubernetes (visão geral e relatório em vídeo)

Com a ajuda do werf, fizemos um bom progresso na solução de um grande número de problemas para engenheiros de DevOps e ficaríamos felizes se a comunidade em geral pelo menos experimentasse esse utilitário em ação. Será mais fácil alcançar um bom resultado juntos.

Vídeos e slides

Vídeo da performance (~47 minutos):

Apresentação do relatório:

PS

Outros relatórios sobre Kubernetes em nosso blog:

Fonte: habr.com

Adicionar um comentário