Crie um pipeline de CI/CD para microsserviços no Kubernetes com Azure DevOps e Helm

AKS (Serviço de Kubernetes do Azure)
Registro de Contêiner do Azure
Azure DevOps

Pode ser um desafio criar um processo confiável de integração contínua/entrega contínua (CI/CD) para uma arquitetura de microsserviços. As equipes individuais devem ser capazes de lançar serviços de forma rápida e confiável, sem interromper outras equipes ou desestabilizar o aplicativo como um todo.

Este artigo descreve um exemplo de pipeline de CI/CD para implantar microsserviços no Serviço de Kubernetes do Azure (AKS). Cada equipe e projeto é diferente, sendo assim, não presuma que este artigo contém um conjunto de regras rígidas. Ele destina-se a ser um ponto de partida para projetar seu próprio processo de CI/CD.

Os objetivos de um pipeline de CI/CD para microsserviços hospedados no Kubernetes podem ser resumidos da seguinte maneira:

  • As equipes podem criar e implantar os serviços de forma independente.
  • As alterações de código transmitidas no processo de CI são implantadas automaticamente em um ambiente semelhante à produção.
  • Os portões de qualidade são aplicadas em cada etapa do pipeline.
  • Uma nova versão de um serviço pode ser implantada lado a lado com a versão anterior.

Para obter mais contexto, consulte CI/CD para arquiteturas de microsserviços.

Suposições

Para fins deste exemplo, aqui estão algumas suposições sobre a equipe de desenvolvimento e a base de código:

  • O repositório de código é único, com pastas organizadas por microsserviço.
  • A estratégia de ramificação da equipe se baseia no desenvolvimento com base em troncos.
  • A equipe usa ramificações de lançamento para gerenciar versões. Versões separadas são criadas para cada microsserviço.
  • O processo de CI/CD usa o Azure Pipelines para criar, testar e implantar os microsserviços no AKS.
  • As imagens de contêiner para cada microsserviço são armazenadas no Registro de Contêiner do Azure.
  • A equipe usa gráficos Helm para empacotar cada microsserviço.
  • Um modelo de implantação por push é usado, em que os Pipelines do Azure e agentes associados executam implantações conectando-se diretamente ao cluster AKS.

Essas suposições orientam muitos dos detalhes específicos do pipeline de CI/CD. No entanto, a abordagem básica descrita aqui deve ser adaptada para outros processos, ferramentas e serviços, como Jenkins ou Docker Hub.

Alternativas

Veja a seguir alternativas comuns que os clientes podem usar ao escolher uma estratégia de CI/CD com o Serviço de Kubernetes do Azure:

  • Como uma alternativa ao uso do Helm como uma ferramenta de gerenciamento e implantação de pacotes, o Kustomize é uma ferramenta de gerenciamento de configuração nativa do Kubernetes que introduz uma maneira livre de modelos para personalizar e parametrizar a configuração do aplicativo.
  • Como alternativa ao uso do Azure DevOps para repositórios e pipelines do Git, os Repositórios do GitHub podem ser usados para repositórios Git privados e públicos, e as Ações do GitHub podem ser usadas para pipelines de CI/CD.
  • Como alternativa ao uso de um modelo de implantação por push, o gerenciamento da configuração do Kubernetes em grande escala pode ser feito usando GitOps (modelo de implantação pull), em que um operador Kubernetes no cluster sincroniza o estado do cluster com base na configuração armazenada em um repositório Git.

Compilações de validação

Suponha que um desenvolvedor esteja trabalhando em um microsserviço chamado Serviço de Entrega. Ao desenvolver um novo recurso, o desenvolvedor coloca código em um branch de recursos. Por convenção, os branches de recursos recebem o nome de feature/*.

CI/CD workflow

O arquivo de definição de compilação inclui um gatilho que filtra por nome do branch e por caminho de origem:

trigger:
  batch: true
  branches:
    include:
    # for new release to production: release flow strategy
    - release/delivery/v*
    - refs/release/delivery/v*
    - master
    - feature/delivery/*
    - topic/delivery/*
  paths:
    include:
    - /src/shipping/delivery/

Usando essa abordagem, cada equipe pode ter seu próprio pipeline de build. Somente o código que é verificado na pasta /src/shipping/delivery aciona uma compilação do Serviço de Entrega. O envio por push confirma para uma ramificação que corresponde ao filtro aciona uma compilação de CI. Agora, no fluxo de trabalho, a compilação de CI executa uma verificação mínima no código:

  1. Compile o código.
  2. Execute os testes de unidade.

O objetivo é manter os tempos de compilação curtos para que o desenvolvedor possa receber comentários rapidamente. Depois que o recurso está pronto para ser mesclado com o mestre, o desenvolvedor abre uma solicitação de pull. Essa operação dispara outra compilação de CI que executa algumas verificações adicionais:

  1. Compile o código.
  2. Execute os testes de unidade.
  3. Crie a imagem de contêiner do runtime.
  4. Execute exames de vulnerabilidade na imagem.

Diagram showing ci-delivery-full in the Build pipeline.

Observação

No Azure DevOps Repos, você pode definir políticas para proteger ramificações. Por exemplo, a política poderia exigir uma compilação de CI bem-sucedida e a aprovação de uma pessoa pertinente para fazer a mesclagem com o mestre.

Compilação completa de CI/CD

Em algum momento, a equipe estará pronta para implantar uma nova versão do Serviço de Entrega. O gerente de versão cria uma ramificação da ramificação principal com este padrão de nomenclatura: release/<microservice name>/<semver>. Por exemplo, release/delivery/v1.0.2.

Diagram showing ci-delivery-full in the Build pipeline and cd-delivery in the Release pipeline.

A criação dessa ramificação aciona uma compilação de CI completa que executa todas as etapas anteriores, além de:

  1. Envie por push a imagem de contêiner para o Registro de Contêiner do Azure. A imagem é marcada com o número de versão obtido do nome do branch.
  2. Executar helm package para empacotar o gráfico Helm para o serviço. O gráfico também é marcado com um número de versão.
  3. Efetuar push do pacote do Helm para o Registro de Contêiner.

Supondo que essa compilação seja bem-sucedida, ela disparará um processo de implantação (CD) usando um pipeline de lançamento do Azure Pipelines. Este pipeline tem as seguintes etapas:

  1. Implante o gráfico do Helm em um ambiente de QA.
  2. Um aprovador confirma a aprovação antes que o pacote seja movido para a produção. Confira Controle de implantação de versão usando aprovações.
  3. Marque novamente a imagem do Docker com o namespace de produção no Registro de Contêiner do Azure. Por exemplo, se a marca atual for myrepo.azurecr.io/delivery:v1.0.2, a marca de produção será myrepo.azurecr.io/prod/delivery:v1.0.2.
  4. Implante o gráfico do Helm no ambiente de produção.

Mesmo em um repositório único, o escopo dessas tarefas pode ser definido para microsserviços individuais, para que as equipes possam fazer a implantação rapidamente. O processo tem algumas etapas manuais: aprovação de PRs, criação de ramificações de liberação e aprovação de implantações no cluster de produção. Essas etapas são manuais: elas poderão ser automatizadas se a organização preferir.

Isolamento de ambientes

Você terá vários ambientes com serviços implantados, incluindo ambientes de desenvolvimento, smoke test, testes de integração, testes de carga e, finalmente, produção. Esses ambientes precisam de algum nível de isolamento. No Kubernetes, você pode escolher entre o isolamento físico e o isolamento lógico. Isolamento físico significa implantar em clusters separados. O isolamento lógico usa namespaces e as políticas, conforme descrito anteriormente.

Nossa recomendação é criar um cluster de produção dedicado juntamente com um cluster separado para seus ambientes de desenvolvimento/teste. Use o isolamento lógico para separar ambientes dentro do cluster de desenvolvimento/teste. Os serviços implantados no cluster de desenvolvimento/teste nunca devem ter acesso a armazenamentos de dados que contêm dados de negócios.

Processo de compilação

Quando possível, empacote o processo de compilação em um contêiner do Docker. Essa configuração permite criar artefatos de código usando o Docker sem configurar um ambiente de compilação em cada máquina de compilação. Um processo de compilação em contêineres facilita a expansão do pipeline de CI adicionando novos agentes de compilação. Além disso, qualquer desenvolvedor da equipe pode criar o código simplesmente executando o contêiner de compilação.

Usando compilações de vários estágios no Docker, você pode definir o ambiente de compilação e a imagem de runtime em um único Dockerfile. Por exemplo, aqui está um Dockerfile que cria um aplicativo .NET:

FROM mcr.microsoft.com/dotnet/core/runtime:3.1 AS base
WORKDIR /app

FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build
WORKDIR /src/Fabrikam.Workflow.Service

COPY Fabrikam.Workflow.Service/Fabrikam.Workflow.Service.csproj .
RUN dotnet restore Fabrikam.Workflow.Service.csproj

COPY Fabrikam.Workflow.Service/. .
RUN dotnet build Fabrikam.Workflow.Service.csproj -c release -o /app --no-restore

FROM build AS testrunner
WORKDIR /src/tests

COPY Fabrikam.Workflow.Service.Tests/*.csproj .
RUN dotnet restore Fabrikam.Workflow.Service.Tests.csproj

COPY Fabrikam.Workflow.Service.Tests/. .
ENTRYPOINT ["dotnet", "test", "--logger:trx"]

FROM build AS publish
RUN dotnet publish Fabrikam.Workflow.Service.csproj -c Release -o /app

FROM base AS final
WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "Fabrikam.Workflow.Service.dll"]

Este Dockerfile define vários estágios de compilação. Observe que o estágio chamado base usa o runtime do .NET, enquanto o estágio chamado build usa o SDK completo do .NET. O estágio build é usado para criar o projeto .NET. No entanto, o contêiner de tempo de execução final é criado a partir do base, que contém apenas o runtime e é significativamente menor do que a imagem completa do SDK.

Criação de um executor de teste

Outra boa prática é executar testes de unidade no contêiner. Por exemplo, aqui está parte de um arquivo do Docker que cria um executor de teste:

FROM build AS testrunner
WORKDIR /src/tests

COPY Fabrikam.Workflow.Service.Tests/*.csproj .
RUN dotnet restore Fabrikam.Workflow.Service.Tests.csproj

COPY Fabrikam.Workflow.Service.Tests/. .
ENTRYPOINT ["dotnet", "test", "--logger:trx"]

Um desenvolvedor pode usar esse arquivo do Docker para executar os testes localmente:

docker build . -t delivery-test:1 --target=testrunner
docker run delivery-test:1

O pipeline de CI também deve executar os testes como parte da etapa de verificação de compilação.

Esse arquivo usa o comando ENTRYPOINT do Docker para executar os testes, não o comando RUN do Docker.

  • Se você usar o comando RUN, os testes serão executados toda vez que você criar a imagem. Ao usar ENTRYPOINT, os testes são ativados. Eles são executados somente quando você direciona explicitamente o estágio testrunner.
  • Um teste com falha não faz com que o comando build do Docker falhe. Dessa forma, você pode distinguir falhas de compilação de contêiner de falhas de teste.
  • Os resultados do teste podem ser salvos em um volume montado.

Práticas recomendadas de contêiner

Aqui estão algumas outras práticas recomendadas a serem consideradas para contêineres:

  • Defina as convenções de toda a organização para marcações de contêiner, controle de versão e convenções de nomenclatura de recursos implantados para o cluster (pods, serviços e assim por diante). Isso pode facilitar o diagnóstico de problemas de implantação.

  • Durante o ciclo de desenvolvimento e teste, o processo de CI/CD cria muitas imagens de contêiner. Somente algumas imagens são candidatas a lançamento, e apenas alguns desses candidatos a lançamento serão promovidos para produção. Tenha uma estratégia de controle de versão clara para que você saiba quais imagens estão implantadas atualmente para produção e ajudar a reverter para uma versão anterior, se necessário.

  • Sempre implante tags de versão de contêiner específicas, não latest.

  • Use namespaces no Registro de Contêiner do Azure para isolar as imagens que foram aprovadas para a produção das imagens que ainda estão sendo testadas. Não mova uma imagem por push para o namespace de produção até que você esteja pronto para implantá-lo em produção. Se você combinar essa prática com controle de versão semântico de imagens de contêiner, isso poderá reduzir a chance de acidentalmente implantar uma versão não aprovada para lançamento.

  • Siga o princípio de privilégio mínimo executando contêineres como um usuário sem privilégios. No Kubernetes, você pode criar uma política de segurança de pod que impeça que os contêineres sejam executados como raiz.

Gráficos Helm

Considere usar o Helm para gerenciar, criar e implantar serviços. Veja alguns dos recursos do Helm que ajudam com CI/CD:

  • Muitas vezes, um único microsserviço é definido por vários objetos Kubernetes. O Helm permite que esses objetos sejam compilados em um único gráfico Helm.
  • Um gráfico pode ser implantado com um único comando do Helm em vez de uma série de comandos kubectl.
  • Os gráficos são explicitamente versionados. Use o Helm para lançar uma versão, exibir versões e reverter para uma versão anterior. Acompanhar atualizações e revisões, usando controle de versão semântico, além da capacidade de reverter para uma versão anterior.
  • Os gráficos do Helm usam modelos para evitar a duplicação de informações, como rótulos e seletores, em vários arquivos.
  • O Helm pode gerenciar dependências entre gráficos.
  • Os gráficos podem ser armazenados em um repositório do Helm, como um Registro de Contêiner do Azure, e integrados no pipeline de compilação.

Para obter mais informações sobre como usar o Registro de Contêiner como um repositório do Helm, confira Usar o Registro de Contêiner do Azure como um repositório do Helm para os gráficos do aplicativo.

Um único microsserviço pode envolver vários arquivos de configuração do Kubernetes. Atualizar um serviço pode significar tocar em todos esses arquivos para atualizar seletores, rótulos e tags de imagem. O Helm os trata como um único pacote chamado gráfico e permite que você atualize facilmente os arquivos YAML usando variáveis. O Helm usa uma linguagem de modelo (baseada em modelos Go) para permitir que você crie arquivos de configuração YAML parametrizados.

Por exemplo, veja abaixo parte de um arquivo YAML que define uma implantação:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "package.fullname" . | replace "." "" }}
  labels:
    app.kubernetes.io/name: {{ include "package.name" . }}
    app.kubernetes.io/instance: {{ .Release.Name }}
  annotations:
    kubernetes.io/change-cause: {{ .Values.reason }}

...

  spec:
      containers:
      - name: &package-container_name fabrikam-package
        image: {{ .Values.dockerregistry }}/{{ .Values.image.repository }}:{{ .Values.image.tag }}
        imagePullPolicy: {{ .Values.image.pullPolicy }}
        env:
        - name: LOG_LEVEL
          value: {{ .Values.log.level }}

Você pode ver que o nome da implantação, os rótulos e a especificação do contêiner usam parâmetros de modelo, que são fornecidos no momento da implantação. Por exemplo, na linha de comando:

helm install $HELM_CHARTS/package/ \
     --set image.tag=0.1.0 \
     --set image.repository=package \
     --set dockerregistry=$ACR_SERVER \
     --namespace backend \
     --name package-v0.1.0

Embora o pipeline de CI/CD possa instalar um gráfico diretamente no Kubernetes, recomendamos criar um arquivo de gráfico (arquivo .tgz) e enviar o gráfico para um repositório do Helm, como o Registro de Contêiner do Azure. Para obter mais informações, consulte Empacotar aplicativos baseados em Docker em gráficos Helm no Azure Pipelines.

Revisões

Os gráficos Helm sempre têm um número de versão, que deve usar controle de versão semântico. Um gráfico também pode ter um appVersion. Esse campo é opcional e não precisa estar relacionado à versão do gráfico. Algumas equipes podem querer aplicar versões separadamente das atualizações dos gráficos. Mas uma abordagem mais simples é usar um número de versão, portanto, há uma relação de 1:1 entre a versão do gráfico e a versão do aplicativo. Dessa forma, você pode armazenar um gráfico por versão e implantar facilmente a versão desejada:

helm install <package-chart-name> --version <desiredVersion>

Outra boa prática é fornecer uma anotação de causa de alteração no modelo de implantação:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "delivery.fullname" . | replace "." "" }}
  labels:
     ...
  annotations:
    kubernetes.io/change-cause: {{ .Values.reason }}

Isso permite que você exiba o campo de causa de alteração para cada revisão, usando o comando kubectl rollout history. No exemplo anterior, a causa da alteração é fornecida como um parâmetro de gráfico Helm.

kubectl rollout history deployments/delivery-v010 -n backend
deployment.extensions/delivery-v010
REVISION  CHANGE-CAUSE
1         Initial deployment

Você também pode usar o comando helm list para exibir o histórico de revisões:

helm list
NAME            REVISION    UPDATED                     STATUS        CHART            APP VERSION     NAMESPACE
delivery-v0.1.0 1           Sun Apr  7 00:25:30 2020    DEPLOYED      delivery-v0.1.0  v0.1.0          backend

Pipeline do Azure DevOps

No Azure Pipelines, os pipelines são divididos em pipelines de compilação e pipelines de lançamento. O pipeline de compilação executa o processo de CI e cria artefatos de compilação. Para uma arquitetura de microsserviços no Kubernetes, esses artefatos são as imagens de contêiner e os gráficos Helm que definem cada microsserviço. O pipeline de lançamento executa o processo de CD que implanta um microsserviço em um cluster.

Com base no fluxo de CI descrito anteriormente neste artigo, um pipeline de compilação pode consistir nas seguintes tarefas:

  1. Crie o contêiner do executor de teste.

    - task: Docker@1
      inputs:
        azureSubscriptionEndpoint: $(AzureSubscription)
        azureContainerRegistry: $(AzureContainerRegistry)
        arguments: '--pull --target testrunner'
        dockerFile: $(System.DefaultWorkingDirectory)/$(dockerFileName)
        imageName: '$(imageName)-test'
    
  2. Execute os testes, invocando a execução do docker no contêiner do executor de teste.

    - task: Docker@1
      inputs:
        azureSubscriptionEndpoint: $(AzureSubscription)
        azureContainerRegistry: $(AzureContainerRegistry)
        command: 'run'
        containerName: testrunner
        volumes: '$(System.DefaultWorkingDirectory)/TestResults:/app/tests/TestResults'
        imageName: '$(imageName)-test'
        runInBackground: false
    
  3. Publique os resultados de teste. Consulte Criar uma imagem.

    - task: PublishTestResults@2
      inputs:
        testResultsFormat: 'VSTest'
        testResultsFiles: 'TestResults/*.trx'
        searchFolder: '$(System.DefaultWorkingDirectory)'
        publishRunAttachments: true
    
  4. Crie o contêiner do runtime.

    - task: Docker@1
      inputs:
        azureSubscriptionEndpoint: $(AzureSubscription)
        azureContainerRegistry: $(AzureContainerRegistry)
        dockerFile: $(System.DefaultWorkingDirectory)/$(dockerFileName)
        includeLatestTag: false
        imageName: '$(imageName)'
    
  5. Envie a imagem de contêiner para o Registro de Contêiner do Azure (ou outro registro de contêiner).

    - task: Docker@1
      inputs:
        azureSubscriptionEndpoint: $(AzureSubscription)
        azureContainerRegistry: $(AzureContainerRegistry)
        command: 'Push an image'
        imageName: '$(imageName)'
        includeSourceTags: false
    
  6. Crie o pacote do gráfico do Helm.

    - task: HelmDeploy@0
      inputs:
        command: package
        chartPath: $(chartPath)
        chartVersion: $(Build.SourceBranchName)
        arguments: '--app-version $(Build.SourceBranchName)'
    
  7. Envie o pacote Helm para o Registro de Contêiner do Azure (ou outro repositório do Helm).

    task: AzureCLI@1
      inputs:
        azureSubscription: $(AzureSubscription)
        scriptLocation: inlineScript
        inlineScript: |
        az acr helm push $(System.ArtifactsDirectory)/$(repositoryName)-$(Build.SourceBranchName).tgz --name $(AzureContainerRegistry);
    

A saída do pipeline de CI é uma imagem de contêiner pronta para produção e um gráfico Helm atualizado para o microsserviço. Nesse ponto, o pipeline de lançamento pode assumir o controle. Haverá um pipeline de lançamento exclusivo para cada microsserviço. O pipeline de lançamento será configurado para ter uma fonte de gatilho definida para o pipeline de CI que publicou o artefato. Esse pipeline permite ter implantações independentes de cada microsserviço. O pipeline de lançamento executa as seguintes etapas:

  • Implante o gráfico Helm em ambientes de desenvolvimento/QA/preparo. O comando Helm upgrade pode ser usado com o sinalizador --install para oferecer suporte à primeira instalação e às atualizações subsequentes.
  • Aguarde até que um aprovador aprove ou rejeite a implantação.
  • Remarcar a imagem do contêiner para lançamento
  • Envie a tag de lançamento para o registro do contêiner.
  • Implante o gráfico Helm no cluster de produção.

Para obter mais informações sobre como criar um pipeline de lançamento, consulte Pipelines de lançamento, versões de rascunho e opções de versão.

O diagrama a seguir mostra o processo de CI/CD de ponta a ponta descrito neste artigo:

CD/CD pipeline

Colaboradores

Esse artigo é mantido pela Microsoft. Ele foi originalmente escrito pelos colaboradores a seguir.

Autor principal:

  • John Poole | Arquiteto de Soluções na Nuvem Sênior

Para ver perfis não públicos do LinkedIn, entre no LinkedIn.

Próximas etapas