Creare una pipeline CI/CD per i microservizi in Kubernetes con Azure DevOps e Helm

Servizio Azure Kubernetes
Registro Azure Container
Azure DevOps

Può essere difficile creare un processo di integrazione continua/recapito continuo affidabile (CI/CD) per un'architettura di microservizi. I singoli team devono essere in grado di rilasciare i servizi in modo rapido e affidabile, senza interrompere altri team o destabilizzare l'applicazione nel suo complesso.

Questo articolo descrive una pipeline CI/CD di esempio per la distribuzione di microservizi in servizio Azure Kubernetes (servizio Azure Kubernetes). Ogni team e progetto è diverso, quindi non accettare questo articolo come set di regole hard-and-fast. È invece un punto di partenza per progettare un processo CI/CD personalizzato.

Gli obiettivi di una pipeline CI/CD per i microservizi ospitati in Kubernetes possono essere riepilogati nel modo seguente:

  • Teams può creare e distribuire i propri servizi in modo indipendente.
  • Le modifiche al codice che passano il processo di integrazione continua vengono distribuite automaticamente in un ambiente simile a quello di produzione.
  • I controlli di qualità vengono applicati in ogni fase della pipeline.
  • Una nuova versione di un servizio può essere distribuita side-by-side con la versione precedente.

Per altre informazioni, vedere CI/CD per le architetture di microservizi.

Presupposti

Ai fini di questo esempio, ecco alcuni presupposti relativi al team di sviluppo e alla codebase:

  • Il repository di codice è un monorepo, con cartelle organizzate per microservizio.
  • La strategia di creazione dei rami del team è basata sullo sviluppo basato su trunk.
  • Il team usa i rami di rilascio per gestire le versioni. Vengono create versioni separate per ogni microservizio.
  • Il processo CI/CD usa Azure Pipelines per compilare, testare e distribuire i microservizi nel servizio Azure Kubernetes.
  • Le immagini del contenitore per ogni microservizio vengono archiviate in Registro Azure Container.
  • Il team usa grafici Helm per creare un pacchetto di ogni microservizio.
  • Viene usato un modello di distribuzione push, in cui Azure Pipelines e gli agenti associati eseguono distribuzioni connettendosi direttamente al cluster del servizio Azure Kubernetes.

Questi presupposti determinano molti dei dettagli specifici della pipeline CI/CD. Tuttavia, l'approccio di base descritto qui può essere adattato per altri processi, strumenti e servizi, ad esempio Jenkins o Docker Hub.

Alternative

Di seguito sono riportate le alternative comuni che i clienti possono usare quando si sceglie una strategia CI/CD con servizio Azure Kubernetes:

  • In alternativa all'uso di Helm come strumento di gestione e distribuzione dei pacchetti, Kustomize è uno strumento di gestione della configurazione nativa kubernetes che introduce un modo senza modello per personalizzare e parametrizzare la configurazione dell'applicazione.
  • In alternativa all'uso di Azure DevOps per repository e pipeline Git, è possibile usare repository GitHub per repository Git privati e pubblici e GitHub Actions può essere usato per le pipeline CI/CD.
  • In alternativa all'uso di un modello di distribuzione push, è possibile gestire la configurazione di Kubernetes su larga scala usando GitOps (modello di distribuzione pull), in cui un operatore Kubernetes nel cluster sincronizza lo stato del cluster, in base alla configurazione archiviata in un repository Git.

Compilazioni di convalida

Si supponga che uno sviluppatore stia lavorando a un microservizio denominato Servizio di distribuzione. Durante lo sviluppo di una nuova funzionalità, lo sviluppatore archivia il codice in un ramo di funzionalità. Per convenzione, i rami di funzionalità sono denominati feature/*.

CI/CD workflow

Il file di definizione di compilazione include un trigger che filtra in base al nome del ramo e al percorso di origine:

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/

Con questo approccio, ogni team può avere una propria pipeline di compilazione. Solo il codice archiviato nella /src/shipping/delivery cartella attiva una compilazione del servizio di recapito. Il push dei commit in un ramo che corrisponde al filtro attiva una compilazione CI. A questo punto nel flusso di lavoro, la compilazione CI esegue alcune verifiche minime del codice:

  1. Compilare il codice.
  2. Eseguire gli unit test.

L'obiettivo è mantenere brevi i tempi di compilazione in modo che lo sviluppatore possa ottenere feedback rapido. Quando la funzionalità è pronta per l'unione nel master, lo sviluppatore apre una richiesta pull. Questa operazione attiva un'altra compilazione CI che esegue alcuni controlli aggiuntivi:

  1. Compilare il codice.
  2. Eseguire gli unit test.
  3. Compilare l'immagine del contenitore di runtime.
  4. Eseguire analisi della vulnerabilità nell'immagine.

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

Nota

In Azure DevOps Repos è possibile definire criteri per proteggere i rami. Ad esempio, i criteri potrebbero richiedere una compilazione CI riuscita oltre all'approvazione da un responsabile per poter eseguire il merge nel master.

Compilazione CI/CD completa

A un certo punto, il team è pronto per distribuire una nuova versione del servizio di recapito. Il gestore delle versioni crea un ramo dal ramo principale con questo modello di denominazione: release/<microservice name>/<semver>. Ad esempio, release/delivery/v1.0.2.

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

La creazione di questo ramo attiva una compilazione CI completa che esegue tutti i passaggi precedenti e:

  1. Eseguire il push dell'immagine del contenitore in Registro Azure Container. L'immagine viene contrassegnata con il numero di versione ottenuto dal nome del ramo.
  2. Eseguire helm package per creare il pacchetto del grafico Helm per il servizio. Il grafico viene anche contrassegnato con un numero di versione.
  3. Eseguire il push del pacchetto Helm in Registro Contenitori.

Supponendo che questa compilazione abbia esito positivo, attiva un processo di distribuzione (CD) usando una pipeline di versione di Azure Pipelines. Questa pipeline prevede i passaggi seguenti:

  1. Distribuire il grafico Helm in un ambiente qa.
  2. Un responsabile dà l'approvazione prima che il pacchetto venga spostato nell'ambiente di produzione. Vedere Release deployment control using approvals (Controllo della distribuzione della versione tramite approvazioni).
  3. Ripetere il tag dell'immagine Docker per lo spazio dei nomi di produzione in Registro Azure Container. Ad esempio, se il tag corrente è myrepo.azurecr.io/delivery:v1.0.2, il tag di produzione è myrepo.azurecr.io/prod/delivery:v1.0.2.
  4. Distribuire il grafico Helm nell'ambiente di produzione.

Anche in un monorepo, queste attività possono essere incluse in singoli microservizi in modo che i team possano eseguire la distribuzione con velocità elevata. Il processo prevede alcuni passaggi manuali: approvazione delle richieste pull, creazione di rami di rilascio e approvazione delle distribuzioni nel cluster di produzione. Questi passaggi sono manuali; potrebbero essere automatizzati se l'organizzazione preferisce.

Isolamento degli ambienti

Si avranno più ambienti in cui si distribuiscono i servizi, inclusi gli ambienti per lo sviluppo, i test di fumo, i test di integrazione, i test di carico e infine la produzione. Per questi ambienti è necessario un certo livello di isolamento. In Kubernetes, è possibile scegliere tra isolamento fisico e isolamento logico. Per isolamento fisico si intende la distribuzione atta a separare i cluster. L'isolamento logico usa spazi dei nomi e criteri, come descritto in precedenza.

Si consiglia di creare un cluster di produzione dedicato insieme a un cluster separato per gli ambienti di sviluppo/prova. Usare l'isolamento logico per separare gli ambienti all'interno del cluster di sviluppo/prova. I servizi distribuiti nel cluster di sviluppo/prova non avranno accesso agli archivi dati che contengono dati aziendali.

Processo di compilazione

Quando possibile, creare un pacchetto del processo di compilazione in un contenitore Docker. Questa configurazione consente di compilare artefatti di codice usando Docker e senza configurare un ambiente di compilazione in ogni computer di compilazione. Un processo di compilazione in contenitori semplifica l'aumento del numero di istanze della pipeline di integrazione continua aggiungendo nuovi agenti di compilazione. Inoltre, qualsiasi sviluppatore del team può compilare il codice semplicemente eseguendo il contenitore di compilazione.

Usando compilazioni a più fasi in Docker, è possibile definire l'ambiente di compilazione e l'immagine di runtime in un singolo Dockerfile. Ad esempio, ecco un Dockerfile che compila un'applicazione .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"]

Questo Dockerfile definisce diverse fasi di compilazione. Si noti che la fase denominata base usa il runtime .NET, mentre la fase denominata build usa l'SDK .NET completo. La build fase viene usata per compilare il progetto .NET. Tuttavia, il contenitore di runtime finale viene compilato da base, che contiene solo il runtime ed è notevolmente inferiore all'immagine completa dell'SDK.

Compilazione di un test runner

Un'altra procedura consigliata consiste nell'eseguire unit test nel contenitore. Di seguito, ad esempio, fa parte di un file Docker che compila un runner di test:

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"]

Uno sviluppatore può usare questo file Docker per eseguire i test in locale:

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

La pipeline CI deve anche eseguire i test come parte del passaggio di verifica della compilazione.

Si noti che questo file usa il comando Docker ENTRYPOINT per eseguire i test, non il comando Docker RUN .

  • Se si usa il RUN comando , i test vengono eseguiti ogni volta che si compila l'immagine. ENTRYPOINTUsando , i test sono di consenso esplicito. Vengono eseguiti solo quando si fa riferimento in modo esplicito alla testrunner fase.
  • Un test non superato non causa l'esito negativo del comando Docker build . In questo modo, è possibile distinguere gli errori di compilazione dei contenitori dagli errori di test.
  • I risultati dei test possono essere salvati in un volume montato.

Procedure consigliate per i contenitori

Ecco alcune altre procedure consigliate da considerare per i contenitori:

  • Definire convenzioni a livello di organizzazione per i tag dei contenitori, il controllo delle versioni e la denominazione delle risorse distribuite nel cluster (pod, servizi e così via). Questo può facilitare la diagnosi dei problemi di distribuzione.

  • Durante il ciclo di sviluppo e di test, il processo CI/CD compilerà molte immagini di contenitori. Solo alcune di queste immagini sono candidate per il rilascio e solo alcuni di questi candidati verranno promossi alla produzione. Disporre di una strategia di controllo delle versioni chiara in modo da sapere quali immagini sono attualmente distribuite nell'ambiente di produzione e per eseguire il rollback a una versione precedente, se necessario.

  • Distribuire sempre tag di versione del contenitore specifici, non latest.

  • Usare gli spazi dei nomi in Registro Azure Container per isolare le immagini approvate per la produzione da immagini ancora in fase di test. Non spostare un'immagine nello spazio dei nomi di produzione finché non si è pronti per distribuirla nell'ambiente di produzione. Combinando questa procedura con il versionamento semantico delle immagini di contenitori è possibile ridurre le probabilità di distribuire accidentalmente una versione il cui rilascio non è stato approvato.

  • Seguire il principio dei privilegi minimi eseguendo i contenitori come utente non privilegiato. In Kubernetes è possibile creare criteri di sicurezza dei pod che impediscono l'esecuzione dei contenitori come radice.

Chart Helm

È consigliabile usare Helm per gestire la compilazione e distribuzione di servizi. Ecco alcune delle funzionalità di Helm che consentono di usare CI/CD:

  • Spesso un singolo microservizio viene definito da più oggetti Kubernetes. Helm consente di creare un pacchetto di questi oggetti in un singolo grafico Helm.
  • Un grafico può essere distribuito con un singolo comando Helm anziché una serie di comandi kubectl.
  • I grafici sono con versione esplicita. Usare Helm per rilasciare una versione, visualizzare le versioni ed eseguire il rollback a una versione precedente. Rilevamento aggiornamenti e revisioni, con controllo delle versioni semantico, nonché la possibilità di eseguire il rollback a una versione precedente.
  • I grafici Helm usano modelli per evitare la duplicazione di informazioni, ad esempio etichette e selettori, in molti file.
  • Helm può gestire le dipendenze tra i grafici.
  • I grafici possono essere archiviati in un repository Helm, ad esempio Registro Azure Container, e integrati nella pipeline di compilazione.

Per altre informazioni sull'uso del Registro Container come repository Helm, vedere Usare il Registro Azure Container come repository Helm per i grafici di applicazione.

Un singolo microservizio può includere più file di configurazione di Kubernetes. L'aggiornamento di un servizio può significare toccare tutti questi file per aggiornare selettori, etichette e tag di immagine. Helm li considera come un singolo pacchetto denominato grafico e consente di aggiornare facilmente i file YAML usando le variabili. Helm usa un linguaggio di modello (basato sui modelli Go) per consentire di scrivere file di configurazione YAML con parametri.

Ad esempio, ecco una parte di un file YAML che definisce una distribuzione:

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 }}

È possibile notare che il nome della distribuzione, le etichette e la specifica del contenitore usano tutti i parametri del modello, forniti in fase di distribuzione. Ad esempio, dalla riga di 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

Anche se la pipeline CI/CD potrebbe installare un grafico direttamente in Kubernetes, è consigliabile creare un archivio grafico (file con estensione tgz) ed eseguire il push del grafico in un repository Helm, ad esempio Registro Azure Container. Per altre informazioni, vedere Creare pacchetti di app basate su Docker nei grafici Helm in Azure Pipelines.

Revisioni

I grafici Helm hanno sempre un numero di versione, che deve usare il controllo delle versioni semantiche. Un grafico può anche avere un oggetto appVersion. Questo campo è facoltativo e non deve essere correlato alla versione del grafico. Alcuni team potrebbero voler applicare versioni separatamente dagli aggiornamenti ai grafici. Tuttavia, un approccio più semplice consiste nell'usare un numero di versione, quindi esiste una relazione 1:1 tra la versione del grafico e la versione dell'applicazione. In questo modo, è possibile archiviare un grafico per versione e distribuire facilmente la versione desiderata:

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

Un'altra procedura consigliata consiste nel fornire un'annotazione della causa della modifica nel modello di distribuzione:

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

In questo modo è possibile visualizzare il campo della causa delle modifiche per ogni revisione, usando il kubectl rollout history comando . Nell'esempio precedente, la causa della modifica viene fornita come parametro del grafico Helm.

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

È anche possibile usare il helm list comando per visualizzare la cronologia delle revisioni:

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

Azure DevOps Pipeline

In Azure Pipelines le pipeline sono suddivise in pipeline di compilazione e pipeline di versione. La pipeline di compilazione esegue il processo di integrazione continua e crea artefatti di compilazione. Per un'architettura di microservizi in Kubernetes, questi artefatti sono le immagini del contenitore e i grafici Helm che definiscono ogni microservizio. La pipeline di versione esegue il processo cd che distribuisce un microservizio in un cluster.

In base al flusso di integrazione continua descritto in precedenza in questo articolo, una pipeline di compilazione può essere costituita dalle attività seguenti:

  1. Compilare il contenitore dello strumento di esecuzione di test.

    - task: Docker@1
      inputs:
        azureSubscriptionEndpoint: $(AzureSubscription)
        azureContainerRegistry: $(AzureContainerRegistry)
        arguments: '--pull --target testrunner'
        dockerFile: $(System.DefaultWorkingDirectory)/$(dockerFileName)
        imageName: '$(imageName)-test'
    
  2. Eseguire i test richiamando docker run sul contenitore di test runner.

    - 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. Pubblicare i risultati del test. Vedere Creare un'immagine.

    - task: PublishTestResults@2
      inputs:
        testResultsFormat: 'VSTest'
        testResultsFiles: 'TestResults/*.trx'
        searchFolder: '$(System.DefaultWorkingDirectory)'
        publishRunAttachments: true
    
  4. Compilare il contenitore di runtime.

    - task: Docker@1
      inputs:
        azureSubscriptionEndpoint: $(AzureSubscription)
        azureContainerRegistry: $(AzureContainerRegistry)
        dockerFile: $(System.DefaultWorkingDirectory)/$(dockerFileName)
        includeLatestTag: false
        imageName: '$(imageName)'
    
  5. Eseguire il push dell'immagine del contenitore in Registro Azure Container (o in un altro registro contenitori).

    - task: Docker@1
      inputs:
        azureSubscriptionEndpoint: $(AzureSubscription)
        azureContainerRegistry: $(AzureContainerRegistry)
        command: 'Push an image'
        imageName: '$(imageName)'
        includeSourceTags: false
    
  6. Creare il pacchetto del grafico Helm.

    - task: HelmDeploy@0
      inputs:
        command: package
        chartPath: $(chartPath)
        chartVersion: $(Build.SourceBranchName)
        arguments: '--app-version $(Build.SourceBranchName)'
    
  7. Eseguire il push del pacchetto Helm in Registro Azure Container (o in un altro repository Helm).

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

L'output della pipeline CI è un'immagine del contenitore pronta per la produzione e un grafico Helm aggiornato per il microservizio. A questo punto, la pipeline di versione può assumere il controllo. Per ogni microservizio sarà disponibile una pipeline di versione univoca. La pipeline di versione verrà configurata per avere un'origine trigger impostata sulla pipeline CI che ha pubblicato l'artefatto. Questa pipeline consente di avere distribuzioni indipendenti di ogni microservizio. La pipeline di versione esegue i passaggi seguenti:

  • Distribuire il grafico Helm negli ambienti di sviluppo/qa/staging. Il Helm upgrade comando può essere usato con il --install flag per supportare la prima installazione e gli aggiornamenti successivi.
  • Attendere che un responsabile approvazione approvi o rifiuti la distribuzione.
  • Ripetere il tag dell'immagine del contenitore per il rilascio
  • Eseguire il push del tag di versione nel registro contenitori.
  • Distribuire il grafico Helm nel cluster di produzione.

Per altre informazioni sulla creazione di una pipeline di versione, vedere Pipeline di versione, versioni bozza e opzioni di rilascio.

Il diagramma seguente illustra il processo CI/CD end-to-end descritto in questo articolo:

CD/CD pipeline

Collaboratori

Questo articolo viene gestito da Microsoft. Originariamente è stato scritto dai seguenti contributori.

Autore principale:

Per visualizzare i profili LinkedIn non pubblici, accedere a LinkedIn.

Passaggi successivi