Compilación de una canalización de CI/CD para microservicios en Kubernetes con Azure DevOps y Helm

Azure Kubernetes Service (AKS)
Azure Container Registry
Azure DevOps

Puede ser difícil crear un proceso de integración continua y entrega continua (CI/CD) confiable para una arquitectura de microservicios. Los equipos individuales deben ser capaces de lanzar servicios de forma rápida y confiable, sin interrumpir a otros equipos ni desestabilizar la aplicación en general.

En este artículo se describe una canalización de CI/CD de ejemplo para la implementación de microservicios en Azure Kubernetes Service (AKS). Cada equipo y cada proyecto son diferentes, por lo que no debe tomar este artículo como un conjunto de reglas absolutas. En su lugar, pretende ser un punto de partida para ayudarle a diseñar su propio proceso de CI/CD.

Los objetivos de una canalización de CI/CD para microservicios hospedados por Kubernetes se pueden resumir de la siguiente manera:

  • Los equipos pueden compilar e implementar sus servicios de manera independiente.
  • Los cambios de código que pasan el proceso de integración continua se implementan automáticamente en un entorno similar al de producción.
  • Se aplican pruebas de calidad en cada fase de la canalización.
  • Se puede implementar una nueva versión de un servicio en paralelo con la versión anterior.

Para obtener más información, consulte CI/CD para arquitecturas de microservicios.

Supuestos

Para los fines de este ejemplo, estas son algunas suposiciones sobre el equipo de desarrollo y el código base:

  • El repositorio de código es un repositorio único y sus carpetas están organizadas por microservicio.
  • La estrategia de ramificación del equipo se basa en el desarrollo basado en el tronco.
  • El equipo utiliza ramas de versión para administrar las versiones. Se crean versiones independientes para cada microservicio.
  • El proceso de CI/CD usa Azure Pipelines para compilar, probar e implementar los microservicios en AKS.
  • Las imágenes de contenedor de cada microservicio se almacenan en Azure Container Registry.
  • El equipo usa gráficos de Helm para empaquetar cada microservicio.
  • Se usa un modelo de implementación de inserción, donde Azure Pipelines y los agentes asociados realizan implementaciones mediante la conexión directa al clúster de AKS.

Estas suposiciones guían muchos de los detalles específicos de la canalización de CI/CD. Sin embargo, el enfoque básico que se describe aquí se puede adaptar a otros procesos, herramientas y servicios, como Jenkins o Docker Hub.

Alternativas

A continuación se muestran alternativas comunes que los clientes pueden usar al elegir una estrategia de CI/CD con Azure Kubernetes Service:

  • Como alternativa al uso de Helm como herramienta de administración e implementación de paquetes, Kustomize es una herramienta de administración de configuración nativa de Kubernetes que presenta una forma libre de plantillas para personalizar y parametrizar la configuración de la aplicación.
  • Como alternativa al uso de Azure DevOps para repositorios y canalizaciones de Git, los repositorios de GitHub se pueden usar para repositorios Git privados y públicos, y las Acciones de GitHub se pueden usar para canalizaciones de CI/CD.
  • Como alternativa al uso de un modelo de implementación de inserción, la administración de la configuración de Kubernetes a gran escala se puede realizar mediante GitOps (modelo de implementación de extracción), donde un operador de Kubernetes en clúster sincroniza el estado del clúster, en función de la configuración almacenada en un repositorio de Git.

Compilaciones de validación

Supongamos que un desarrollador está trabajando en un microservicio denominado Delivery Service. Al desarrollar una característica nueva, el desarrollador comprueba el código de una rama de características. Por convención, las ramas de características se denominan feature/*.

CI/CD workflow

El archivo de definición de compilación incluye un desencadenador que filtra por el nombre de rama y la ruta de acceso de origen:

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 este enfoque, cada equipo puede tener su propia canalización de compilación. Solo el código que se protege en la carpeta /src/shipping/delivery desencadena una compilación de Delivery Service. La inserción de confirmaciones en una rama que coincide con el filtro desencadena una compilación de CI. En este punto del flujo de trabajo, la compilación de CI ejecuta una comprobación mínima del código:

  1. Compilación del código.
  2. Ejecución de pruebas unitarias.

El objetivo es que las compilaciones tarden poco en completarse, para que el desarrollador pueda obtener comentarios rápidamente. Cuando la característica está lista para combinarse con la maestra, el desarrollador abre una solicitud de inserción. Esta operación desencadena otra compilación de integración continua que realiza más comprobaciones:

  1. Compilación del código.
  2. Ejecución de pruebas unitarias.
  3. Compilación de la imagen de contenedor en tiempo de ejecución.
  4. Ejecución de exámenes de vulnerabilidades en la imagen.

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

Nota:

En Repos de Azure DevOps se pueden definir directivas para proteger las ramas. Por ejemplo, la directiva puede requerir una compilación de CI correcta, además de que un aprobador cierre sesión para poder realizan la combinación con el maestro.

Compilación de CI/CD completa

En algún momento, el equipo estará preparado para implementar una nueva versión del microservicio Delivery Service. El administrador de versiones crea una rama a partir de la rama principal con este patrón de nomenclatura: release/<microservice name>/<semver>. Por ejemplo, release/delivery/v1.0.2.

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

La creación de esta rama desencadena una compilación de CI completa que ejecuta todos los pasos anteriores y, además, los siguientes:

  1. Inserción de una imagen de contenedor en Azure Container Registry. La imagen se etiqueta con el número de versión tomado del nombre de la rama.
  2. Ejecución de helm package para empaquetar el gráfico de Helm para el servicio. El gráfico también se etiqueta con un número de versión.
  3. Inserción del paquete de Helm en Container Registry.

Si esta compilación funciona correctamente, desencadena un proceso de implementación (CD) mediante una canalización de versiones de Azure Pipelines. La canalización presenta los pasos siguientes:

  1. Implementación del gráfico de Helm en un entorno de control de calidad.
  2. Un aprobador cierra sesión antes de que el paquete pase a producción. Consulte Release deployment control using approvals (Liberación del control de implementación mediante aprobaciones).
  3. Nuevo etiquetado de la imagen de Docker para el espacio de nombres de producción en Azure Container Registry. Por ejemplo, si la etiqueta actual es myrepo.azurecr.io/delivery:v1.0.2, la etiqueta de producción es myrepo.azurecr.io/prod/delivery:v1.0.2.
  4. Implementación del gráfico de Helm en el entorno de producción.

Incluso en los repositorios únicos, es posible que estas tareas estén destinadas a microservicios individuales para que los equipos puedan realizar las implementaciones a gran velocidad. El proceso tiene algunos pasos manuales: la aprobación de solicitudes de inserción, la creación de ramas de la versión y la aprobación de implementaciones en el clúster de producción. Estos pasos son manuales, pero se pueden automatizar si la organización lo prefiere.

Aislamiento de los entornos

Tendrá varios entornos en los que implementa los servicios, incluidos los entornos de desarrollo, prueba de aceptación de la compilación, pruebas de integración, pruebas de carga y, por último, producción. Estos entornos necesitan cierto nivel de aislamiento. En Kubernetes, puede optar entre el aislamiento físico y el aislamiento lógico. El aislamiento físico significa implementar en clústeres independientes. El aislamiento lógico hace uso de espacios de nombres y directivas, como se describió anteriormente.

Nuestra recomendación es crear un clúster de producción dedicado junto con un clúster independiente para los entornos de desarrollo y pruebas. Use el aislamiento lógico para separar los entornos dentro del clúster de desarrollo y pruebas. Los servicios implementados en el clúster de desarrollo y pruebas nunca deben tener acceso a almacenes de datos que contengan datos empresariales.

Proceso de compilación

Cuando sea posible, empaquete el proceso de compilación en un contenedor de Docker. Esta configuración le permite compilar los artefactos de código mediante Docker y sin necesidad de configurar un entorno de compilación en cada máquina de compilación. Un proceso de compilación en contenedor facilita el escalado horizontal de la canalización de CI mediante la adición de nuevos agentes de compilación. Además, cualquier desarrollador del equipo puede compilar el código con solo ejecutar el contenedor de compilación.

Si usa compilaciones de varias fases en Docker, puede definir el entorno de compilación y la imagen en tiempo de ejecución en un único Dockerfile. Por ejemplo, este es un archivo Dockerfile que compila una aplicación .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 varias fases de compilación. Tenga en cuenta que la fase denominada base utiliza el entorno de ejecución de .NET, mientras que la fase denominada build usa el SDK de .NET completo. La fase build se utiliza para compilar el proyecto de .NET. No obstante, el contenedor final en tiempo de ejecución se crea a partir de base, que solo contiene el tiempo de ejecución y es significativamente menor que la imagen completa del SDK.

Compilación de una instancia de Test Runner

Otro procedimiento recomendado es ejecutar pruebas unitarias en el contenedor. Por ejemplo, aquí forma parte de un archivo de Docker que compila una instancia de Test Runner:

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

Un desarrollador puede usar este archivo de Docker para ejecutar las pruebas localmente:

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

La canalización de CI también debe ejecutar las pruebas como parte del paso de comprobación de la compilación.

Tenga en cuenta que este archivo usa el comando ENTRYPOINT de Docker para ejecutar las pruebas, no el comando RUN de Docker.

  • Si usa el comando RUN, las pruebas se ejecutan cada vez que se compila la imagen. Si usa ENTRYPOINT, las pruebas son opcionales. Solo se ejecutan cuando se dirige explícitamente a la fase testrunner.
  • Una prueba con errores no hace que se produzca un error en el comando Docker build. De este modo, puede distinguir los errores de compilación del contenedor de los errores de prueba.
  • Los resultados de las pruebas se pueden guardar en un volumen montado.

Procedimientos recomendados de contenedores

Estos son otros procedimientos recomendados que se deben tener en cuenta para los contenedores:

  • Defina convenciones para toda la organización de etiquetas contenedoras, control de versiones y nomenclatura para los recursos implementados en el clúster (pods, servicios, etc.). Esto puede ayudar a diagnosticar problemas de implementación más fácilmente.

  • Durante el ciclo de desarrollo y pruebas, el proceso de CI/CD compilará muchas imágenes de contenedor. Solo algunas de esas imágenes serán candidatas a publicarse y, de ellas, solo algunas pasarán a producción. Defina una estrategia clara de control de versiones para saber qué imágenes se están implementando actualmente en producción y poder revertir a una versión anterior si es necesario.

  • Implemente siempre etiquetas de versión de contenedor específicas, no latest.

  • Utilice los espacios de nombres en Azure Container Registry para aislar las imágenes que están aprobadas para producción de las imágenes que aún se están probando. No mueva una imagen al espacio de nombres de producción hasta que esté listo para implementarla en producción. Si combina esta práctica con el versionamiento semántico de imágenes de contenedor, puede reducir la posibilidad de implementar accidentalmente una versión que no se ha aprobado para su publicación.

  • Siga el principio de privilegios mínimos mediante la ejecución de contenedores como un usuario sin privilegios. En Kubernetes, puede crear una directiva de seguridad de pod que impida que los contenedores se ejecuten como raíz.

Gráficos de Helm

Considere el uso de Helm para administrar la creación e implementación de servicios. A continuación se indican algunas de las características de Helm que ayudan en CI/CD:

  • A menudo, un solo microservicio se define mediante varios objetos de Kubernetes. Helm permite empaquetar estos objetos en un único gráfico de Helm.
  • Un gráfico se puede implementar con un comando de Helm único, en lugar de usar una serie de comandos de kubectl.
  • Los gráficos tienen versiones explícitas. Use Helm para publicar una versión, ver versiones y revertir a una versión anterior. Seguimiento de actualizaciones y revisiones, con control de versiones semántico, junto con la capacidad de revertir a una versión anterior.
  • Los gráficos de Helm usan plantillas para evitar la duplicación de información, como etiquetas y selectores, en muchos archivos.
  • Helm puede administrar dependencias entre gráficos.
  • Los gráficos se pueden almacenar en un repositorio de Helm, como Azure Container Registry, e integrarse en la canalización de compilación.

Para más información sobre el uso de Container Registry como un repositorio de Helm, consulte Uso de Azure Container Registry como un repositorio de Helm para los gráficos de aplicación.

Un único microservicio puede incluir varios archivos de configuración de Kubernetes. La actualización de un servicio puede significar tocar todos estos archivos para actualizar los selectores, las etiquetas y las etiquetas de imagen. Helm los trata como un único paquete denominado gráfico y permite actualizar fácilmente los archivos YAML mediante variables. Helm usa un lenguaje de plantilla (basado en plantillas de Go) para permitirle escribir archivos de configuración de YAML con parámetros.

Por ejemplo, a continuación se muestra una parte de un archivo YAML que define una implementación:

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

Puede ver que el nombre de la implementación, las etiquetas y la especificación del contenedor usan parámetros de plantilla, que se proporcionan en el momento de la implementación. Por ejemplo, desde la línea de comandos:

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

Aunque la canalización de CI/CD podría instalar un gráfico directamente en Kubernetes, se recomienda crear un archivo de gráfico (archivo .tgz) e insertar el gráfico en un repositorio de Helm (por ejemplo, Azure Container Registry). Para obtener más información, consulte Empaquetado de aplicaciones basadas en Docker en gráficos de Helm en Azure Pipelines.

Revisiones

Los gráficos de Helm siempre tienen un número de versión, que debe usar Versionamiento Semántico. Un gráfico también puede tener un elemento appVersion. Este campo es opcional y no tiene que estar relacionado con la versión del gráfico. Es posible que algunos equipos quieran usar versiones de aplicaciones por separado de las actualizaciones de los gráficos. No obstante, un enfoque más sencillo es usar un número de versión, de modo que exista una relación 1:1 entre la versión del gráfico y la versión de la aplicación. De este modo, puede almacenar un gráfico por versión e implementar fácilmente la versión deseada:

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

Otro procedimiento recomendado es proporcionar una anotación de causa del cambio en la plantilla de implementación:

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

Esto le permite ver el campo de la causa del cambio para cada revisión mediante el comando kubectl rollout history. En el ejemplo anterior, la causa del cambio se proporciona como un parámetro del gráfico de Helm.

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

También puede usar el comando helm list para ver el historial de revisiones:

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

Canalización de Azure DevOps

En Azure Pipelines, las canalizaciones se dividen en canalizaciones de compilación y canalizaciones de versión. La canalización de compilación ejecuta el proceso de CI y crea artefactos de compilación. En el caso de una arquitectura de microservicios en Kubernetes, estos artefactos son las imágenes de contenedor y los gráficos de Helm que definen cada microservicio. La canalización de versión ejecuta ese proceso de CD que implementa un microservicio en un clúster.

Según el flujo de CI descrito anteriormente en este artículo, una canalización de compilación puede constar de las siguientes tareas:

  1. Compilación del contenedor de Test Runner.

    - task: Docker@1
      inputs:
        azureSubscriptionEndpoint: $(AzureSubscription)
        azureContainerRegistry: $(AzureContainerRegistry)
        arguments: '--pull --target testrunner'
        dockerFile: $(System.DefaultWorkingDirectory)/$(dockerFileName)
        imageName: '$(imageName)-test'
    
  2. Ejecute las pruebas mediante la invocación de docker run en el contenedor de 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. Publicación de los resultados de las pruebas. Consulte Compilación de una imagen.

    - task: PublishTestResults@2
      inputs:
        testResultsFormat: 'VSTest'
        testResultsFiles: 'TestResults/*.trx'
        searchFolder: '$(System.DefaultWorkingDirectory)'
        publishRunAttachments: true
    
  4. Compilación del contenedor en tiempo de ejecución.

    - task: Docker@1
      inputs:
        azureSubscriptionEndpoint: $(AzureSubscription)
        azureContainerRegistry: $(AzureContainerRegistry)
        dockerFile: $(System.DefaultWorkingDirectory)/$(dockerFileName)
        includeLatestTag: false
        imageName: '$(imageName)'
    
  5. Inserte la imagen de contenedor en Azure Container Registry (u otro registro de contenedor).

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

    - task: HelmDeploy@0
      inputs:
        command: package
        chartPath: $(chartPath)
        chartVersion: $(Build.SourceBranchName)
        arguments: '--app-version $(Build.SourceBranchName)'
    
  7. Inserte el paquete Helm en Azure Container Registry (u otro repositorio Helm).

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

La salida de la canalización de CI es una imagen de contenedor lista para producción y un gráfico de Helm actualizado para el microservicio. En este punto, la canalización de versión puede asumir el control. Habrá una canalización de versión única para cada microservicio. La canalización de versión se configurará para que tenga un origen de desencadenador establecido en la canalización de CI que publicó el artefacto. Esta canalización permite tener implementaciones independientes de cada microservicio. La canalización de versión realiza los pasos siguientes:

  • Implementar el gráfico de Helm en entornos de desarrollo, control de calidad y ensayo. El comando Helm upgrade se puede usar con la marca --install para admitir la primera instalación y las actualizaciones posteriores.
  • Esperar que un aprobador apruebe o rechace la implementación.
  • Volver a etiquetar la imagen de contenedor para la versión.
  • Insertar la etiqueta de versión en el registro de contenedor.
  • Implementar el gráfico de Helm en el clúster de producción.

Para obtener más información sobre la creación de una canalización de versión, consulte Canalizaciones de versión, versiones de borrador y opciones de versiones.

En el diagrama siguiente se muestra el proceso de CI/CD de un extremo a otro que se describe en este artículo:

CD/CD pipeline

Colaboradores

Microsoft mantiene este artículo. Originalmente lo escribieron los siguientes colaboradores.

Autor principal:

  • John Poole | Arquitecto sénior de soluciones en la nube

Para ver los perfiles no públicos de LinkedIn, inicie sesión en LinkedIn.

Pasos siguientes