Integrar la pipeline de CI con el repositorio para GitOps en Azure DevOps

Una de las pruebas que estuve haciendo estos días era cómo integrar la pipeline de CI con el repositorio para implementar GitOps, con el objetivo de actualizar de manera automática la imagen del manifiesto correspondiente a ese integración continua, para que luego el operador que estemos utilizando, como Argo CD o Flux, detecte el cambio y lo aplique en el entorno que corresponda. En este artículo te cuento cómo lo he hecho.

Manifiesto de ejemplo

Para esta prueba he utilizado este manifiesto, el cuál solamente contiene la definición de un Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: tour-of-heroes-api
  name: tour-of-heroes-api
spec:
  selector:
    matchLabels:
      app: tour-of-heroes-api
  template:
    metadata:
      labels:
        app: tour-of-heroes-api
    spec:
      containers:
      - image: <YOUR_ACR_NAME>.azurecr.io/tourofheroesapi:927
        name: tour-of-heroes-api
        ports:
        - containerPort: 80
        resources:
          limits:
            cpu: 500m
            memory: 128Mi

El objetivo es poder modificar el valor de image para no tener que hacerlo de forma manual una vez que haya finalizado la pipeline de CI, para actualizar de manera automática un entorno de desarrollo (para el de producción mejor pull requests 😉).

Pipeline que genera la imagen y actualiza el manifiesto

Para poder lograr mi objetivo, me he montado una pipeline tan sencilla como esta:

trigger:
- main
variables:
  tag: '$(Build.BuildId)'
stages:
- stage: Build
  displayName: Build image
  jobs:
  - job: Build
    displayName: Build
    pool:
      vmImage: ubuntu-latest
    steps:
    - task: Docker@2
      inputs:
        containerRegistry: '$(ACR_NAME).azurecr.io'
        repository: 'tourofheroesapi'
        command: 'buildAndPush'
        Dockerfile: '**/Dockerfile'
- stage: GitOps
  displayName: Modify manifest with the new Docker image
  jobs:
    - job: Ops
      steps:
        - checkout: git://Tour Of Heroes GitOps/Tour Of Heroes GitOps
          persistCredentials: true
        - bash: |
            git config --global user.email "devops@azure.com"
            git config --global user.name "Azure DevOps pipeline"
            git checkout dev
            kubectl patch --local -f tour-of-heroes-api/deployment.yaml -p '{"spec":{"template":{"spec":{"containers":[{"name":"tour-of-heroes-api","image":"$(ACR_NAME).azurecr.io/tourofheroesapi:$(Build.BuildId)"}]}}}}' -o yaml > temp.yaml && mv temp.yaml tour-of-heroes-api/deployment.yaml
            cat tour-of-heroes-api/deployment.yaml
            git add . 
            git commit -m "Update tourofheroesapi to $(Build.BuildId)"
            git push
          displayName: Update manifest

Cada vez que genere un cambio en mi código, esta pipeline se ejecutará, generará la nueva imagen de Docker, la publicará en un Azure Container Registry y, por último, actualizará en el manifiesto correspondiente, en el repositorio destinado a GitOps, la nueva versión para que el operador correspondiente, como Argo CD o Flux, detecte el cambio y lo despliegue en el clúster de Kubernetes correspondiente.
En este ejemplo estoy usando dos repositorios dentro del mismo proyecto de Azure DevOps: uno donde está el código fuente de mi API y otro para los manifiestos que componen mi aplicación. Esta pipeline forma parte del primero, pero al estar en el mismo proyecto puedo utilizar checkout: git://Tour Of Heroes GitOps/Tour Of Heroes GitOps para descargar el contenido del otro repositorio. Además, para que esto sea posible debo modificar los permisos en Project Settings > Repositories > Security y seleccionando para el usuario Build Service el valor de Contribute a Allow.

Project Settings – Repositories – Security Build Service – Contribute Allow

La primera vez que se ejecute la pipeline nos pedirá permiso para ejecutarla.

¡Saludos!