Home >> Blog >> Mise en place d’une chaîne d’intégration continue GitOps Cloud-Native sur OpenShift/Kubernetes

Mise en place d’une chaîne d’intégration continue GitOps Cloud-Native sur OpenShift/Kubernetes

18 septembre 2023

By Sami AMOURA.

Le modèle GitOps est devenu incontournable ces dernières années, de nombreux produits autour de ce paradigme ont émergés pour répondre à ces nouveaux besoins. Red Hat, à travers sa plateforme OpenShift Container Plateform, a étoffé sa distribution grâce aux intégrations de produits open source tels que ArgoCD (OpenShift GitOps) et Tekton (OpenShift Pipelines). Dans ce blogpost, nous verrons les bénéfices liés à ces nouveaux outils et les usages associés à travers un cas pratique. Nous détaillerons les fonctionnalités essentielles de chacune des solutions et leurs complémentarités dans le cadre d’une chaîne d’intégration Cloud Native CI/CD GitOps.

Ce blogpost se décomposera en deux parties :

  • Présentation des concepts fondamentaux des outils OpenShift Pipelines et OpenShift GitOps
  • Mise en place desd’une chaîne d’intégration Cloud Native CI/CD GitOps sur la plateforme OpenShift

OpenShift Pipelines

Présentation

OpenShift Pipelines est une solution d’intégration et de livraison continue Cloud Native permettant de construire des pipelines CI/CD. Il repose notamment sur le projet open-source Tekton. Ce framework CI/CD Kubernetes native permet d’automatiser les déploiements sur plusieurs plateformes (OpenShift, Kubernetes, Machine virtuelle, serverless…).

Cloud-Native CI/CD

Un pipeline CI/CD Cloud Native repose sur les 3 piliers suivants :

Les 3 piliers d’un pipeline CI/CD Cloud-Native

  • Les conteneurs : Conçu pour les applications conteneurisées et fonctionnant sur Kubernetes,
  • Serverless : Fonctionne sans moteur de CI/CD à gérer et à maintenir (exemple : GitLab CI, Serveur Jenkins, Bitbucket…),
  • DevOps : Conçu en tenant compte des microservices et des équipes distribuées.

Les caractéristiques de Tekton

Voici les principales caractéristiques :

Les 3 piliers d’un pipeline CI/CD Cloud-Native

  • Personnalisable : Les entités Tekton sont entièrement personnalisables ce qui permet aux Ingénieurs DevOps la création d’un catalogue varié et la mise à disposition des développeurs,
  • Réutilisable : les ressources sont portables et peuvent être distribuées à différentes équipes au sein de l’organisation,
  • Extensible : Un catalogue et un Hub (Tekton Hub) sont à disposition,
  • Normalisé : Tekton s’installe et s’exécute comme une extension de votre cluster Kubernetes à l’aide d’un opérateur,permettant la gestion des objets Tekton comme n’importe quel objet Kubernetes,
  • Mise à l’échelle : Pour augmenter les resources liées à vos workloads de CI/CD Cloud Native, il suffit simplement d’ajouter des nœuds à votre cluster sans avoir à modifier les allocations de ressources ou de modifier les pipelines.

Le projet Tekton

Le projet Tekton se compose de deux entités Tekton Pipelines et Tekton Triggers.

  • Tekton Pipelines est un composant définissant un ensemble de CustomResource (CR) Kubernetes permettant de construire un pipeline CI/CD cloud-native.
  • Tekton Triggers est un autre composant permettant de détecter et d’extraire des informations d’évènements de diverses sources mais aussi d’instancier/exécuter des pipelines.

Ces deux composants sont complémentaires pour la mise en place d’une chaîne d’intégration complète.

Tekton Pipelines

Le projet Tekton Pipelines est composé de plusieurs types de CustomResource Kubernetes:

  • Task
    • Step
  • Pipeline
  • TaskRun
  • PipelineRun

Voici l’arborescence hiérarchisée des CustomResource OpenShift Pipelines :

Arborescence des objets Tekton

Task

Les Tasks sont les plus petits éléments qui composent le projet Tekton Pipelines. Elles représentent la définition/déclaration d’une unité de travail à exécuter. On peut notamment citer comme exemple de Task le packaging d’un projet Java, le build d’un conteneur ou encore le scan de vulnérabilité d’une image de conteneur.

Les Tasks possèdent des Input et Output de paramètres afin de pouvoir être dynamiques. Il est aussi possible de configurer des Workspaces pour partager les données entre différentes Tasks. Celles-ci peuvent s’exécuter indépendamment d’un pipeline. Il est aussi possible de de faire appel à des Tasks prédéfinies, développées par la communauté.

Pour réaliser les Tasks, nous devons définir une liste de Steps à exécuter séquentiellement.

Description des Tasks

Step

Les Steps permettent d’exécuter des actions à l’aide de commandes ou scripts dans les conteneurs (pods). Ils ont l’avantage de reprendre les caractéristiques des paramètres Kubernetes que l’on connait déjà :

  • Image
  • Variable d’environnement
  • Volume
  • ConfigMap
  • Secret

Un exemple de Step dans une Task permettant de packager un projet Java est la définition de la version du projet, ou encore la création du jar.

- name: build
  image: maven:3.6.0-jdk-8-slim
  command: ["mvn"]
  args: ["install"]

Pipeline

Le Pipeline définit l’ordre d’execution des Tasks. A l’instar d’une Task, il représente la définition/déclaration d’une suite de Tasks. Il a notamment pour rôle d’orchestrer l’execution des Tasks à l’aide de conditions, de relancer les Tasks ainsi que de définir des Inputs, Outputs et les Workspaces permettant le partage de données entres les Tasks.

Voici un schéma illustrant l’orchestration de Tasks lors de la mise en place d’un pipeline :

Description d'un Pipeline

TaskRun

La TaskRun représente l’instanciation/l’exécution d’une Task. Elle est exécutée en tant que pods, sur le cluster Kubernetes. Elle référence une Task spécifique ou permet la déclaration d’une Task directement dans la TaskRun.

La TaskRun fournit des données aux Tasks :

  • Paramètres
  • Ressources
  • Service Account
  • Workspace

Interaction entre les différentes CustomResource Kubernetes lors de l’execution d’une Task :

Description d'une TaskRun

PipelineRun

Le PipelineRun permet l’instanciation/l’exécution d’un Pipeline. Elle référence un Pipeline spécifique ou permet la déclaration d’un Pipeline directement dans le PipelineRun.

Au même titre qu’une TaskRun, le PipelineRun fournit des données aux Pipelines :

  • Parametres
  • Ressources
  • Service Account
  • Workspace

Interaction entre les différentes entités Kubernetes lors de l’execution d’un Pipeline :

Description d'un PipelineRun

Exemple d’instanciation de PipelineRun à partir de pipelines à différentes heures :

Exemple d'instanciation de PipelineRun à partir de pipelines à différentes heures :

Tekton Triggers

Le projet Tekton Triggers est composé de plusieurs types de CustomResource Kubernetes :

  • Event Listener
  • Trigger
    • Interceptor
  • TriggerBinding
  • TriggerTemplate

Déclenchement d'un pipeline

Event Listener

L’EventListener est une ressource déployée dans le cluster Kubernetes permettant d’écouter les événements d’une application tierce (webhook). Cette ressource permet de détecter et de transmettre l’évènement à un ou plusieurs Triggers référencés.

Trigger

Le Trigger permet de spécifier sur quel type d’évènement le pipeline sera déclenché. Par exemple, le push d’un nouveau commit sur le gestionnaire de dépôt, l’approbation d’une merge request… Un Trigger spécifie des CustomResource de type TriggerTemplate, un TriggerBinding et généralement un parmamètre interceptor.

Interceptor

Le Trigger spécifie un interceptor permettant de filtrer les données utiles, de sécuriser les échanges à l’aide d’un secret (token), de transformer les meta données ainsi que de définir et de tester les conditions de déclenchements. Voici la liste des interceptors :

  • Webhook
  • GitHub
  • GitLab
  • BitBucket
  • CEL

TriggerBinding

La ressource TriggerBinding permet de récupérer les données interceptées et transformées par le Trigger et de les transmettre au TriggerTemplate.

TriggerTemplate

Le TriggerTemplate spécifie un modèle pour les CustomResource TaskRun ou PipelineRun qui permet d’instancier/exécuter lorsque l’EventListener détecte un évènement. Il permet d’exposer les paramètres récupérés dynamiquement lors de l’évènement et de les utiliser pour exécuter le PipelineRun/TaskRun.

Voici le schéma de d’execution d’un workflow classique d’Intégration Continue (CI) :

Workflow classique d'une Intégration Continue

OpenShift GitOps

Présentation

OpenShift GitOps est une solution de livraison continue (CD) basée sur le modèle déclaratif pour Kubernetes/OpenShift reposant sur le projet open source ArgoCD. Il permet la gestion de la configuration d’infrastructure ainsi que les mises à jour des applications dans un gestionnaire de dépôt Git.

Les 4 piliers fondamentaux

Le modèle GitOps repose sur 4 piliers fondamentaux :

  • L’approche déclarative : l’ensemble du système est décrit de manière déclarative,
  • Le dépôt Git comme unique source de vérité : les changements déclaratifs vous permettent de considérer les modifications comme des transactions permettant le versionnement, la traçabilité ou le contrôle d’accès,
  • Un opérateur Kubernetes : les changements approuvés de l’état souhaité sont automatiquement appliqués au système grâce à l’opérateur qui scrute en temps réel le dépôt Git,
  • L’observabilité continue : les agents logiciels garantissent l’exactitude et alertent en cas de divergence.

4 piliers fondamentaux

Méthode Push vs Pull

Lorsqu’on évoque le GitOps il existe deux types d’approches. La méthode classique de type Push et celle de type Pull davantage associée au GitOps.

Pour avoir une description détaillées des deux types d’approches et leurs spécificités, vous pouvez vous référer à un article précédemment écrit sur le blog SoKube GitOps and the Millefeuille dilemma.

Le projet ArgoCD

Le projet ArgoCD est composé de plusieurs types de ressources :

  • AppProject
  • Application
  • ApplicationSet

Dans le cadre de ce blogpost nous utiliserions essentiellement les ressources AppProject et Application .

AppProject

L’AppProject représente un groupement logique d’applications. Cette CustomResource Kubernetes permet notamment de définir depuis quel dépôt Git les manifestes peuvent être récupérés, dans quel cluster et namespace les applications doivent être déployées, une section RBAC permettant d’établir un contrôle d’accès aux ressources Kubernetes et enfin quel type d’objet Kubernetes peut être créé.

Application

Une Application dans ArgoCD représente une application déployée dans un environnement au sein du cluster Kubernetes. Elle permet notamment de spécifier la configuration Git avec l’URL du dépôt, la branche, le dossier ou l’environnement afin de récupérer et de déployer les manifestes Kubernetes. Il est aussi possible de configurer d’autres paramètres comme la destination de déploiement (cluster et namespace), la politique de synchronisation ou encore le type de déploiement (manifests natifs, Helm ou Kustomize).

Pipeline GitOps Cloud-Native

Après expliqué les concepts liés à OpenShift Pipelines et OpenShift GitOps dans la première partie de ce blogpost, nous allons dans cette seconde section, mettre en évidence l’interaction entre ces différents outils. L’objectif consistera à construire un pipeline CI/CD GitOps Cloud Native pour le déploiement d’une application appelée Fruitz. Celle-ci est composée de deux microservices :

  • Un backend développé avec en Java Quarkus : Fruit Quarkus,
  • Un frontend reposant sur la technologie Angular.

Pour cette démonstration le pipeline GitOps Cloud-Native sera uniquement réalisé autour du microservice Java fruitz-quarkus. Il sera composé d’une CI (Intégration Continue) standard orchestrée par OpenShift Pipelines (Tekton) et d’une CD (Déploiement Continu) gérée par OpenShift GitOps (ArgoCD). Dans le cadre de cette démonstration, nous avons volontairement introduit une limitation (bug) applicative sur le backend Java fruitz-quarkus. L’objectif est de corriger cette limitation à l’aide du pipeline CI/CD GitOps Cloud-Native ainsi que d’en démontrer l’ensemble des bénéfices.

Pour respecter le modèle GitOps le code applicatif et celui de déploiement seront hébergés dans deux repositories distincts sur la plateforme GitLab.com. Le repository GitOps de déploiement Fruit-Deploy utilisera le package manager Helm pour déployer l’application au sein du cluster OpenShift.

Workflow d’un pipeline CI/CD GitOps :

Workflow d'un pipeline CI/CD GitOps

Installation des outils

Installation d’OpenShift Pipelines et OpenShift GitOps

Cette démonstration sera orchestrée autour de la plateforme OpenShift. Vous aurez besoin d’avoir un cluster OpenShift fonctionnel avec les permissions de cluster-admin. Si vous souhaitez déployer un cluster OpenShift sur AWS vous pouvez vous référer à l’article Comment déployer un cluster Openshift dans AWS sur le blog SoKube.

Se connecter à la console OpenShift et installer les opérateurs RedHat OpenShift GitOps et RedHat OpenShift Pipelines :

Installation des opérateurs

Installation StorageClass

Afin de pouvoir provisionner des volumes (PersitentVolume) dynamiquement lors de l’exécution des pipelines Tekton, mais aussi pour déployer les applications, vous pouvez, à des fin de tests, déployer et utiliser le projet Kubernetes NFS Subdir External Provisioner qui permettra de créer une StorageClass reposant sur du NFS. Dans notre cas nous utiliserons la StorageClass appelée nfs-client.

StorageClass nfs-client:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-client
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner
parameters:
  archiveOnDelete: "false"

OpenShift GitOps

Après l’installation d’OpenShift Pipelines à travers la console, nous allons maintenant déployer l’AppProject et l’Application ArgoCD. Créez les manifests suivants :

AppProjet fruitz-deployment:

apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: fruitz-deployment
  namespace: openshift-gitops
  labels:
    owner: sokube
    scope: fruitz
spec:
  description: ArgoCD Projet for the Fruitz Deploylent applications
  # Allow manifests to deploy only from this repositories 
  sourceRepos:
    -  git@gitlab.com:sokube-io/sample-apps/fruitz/fruitz-deploy.git
  # Only permit to deploy applications in the following clusters & namespaces
  destinations:
    - namespace: fruitz
      server: https://kubernetes.default.svc
  clusterResourceWhitelist:
  - group: '*'
    kind: Namespace
  # Enables namespace orphaned resource monitoring.
  orphanedResources:
    warn: false

Application fruitz-helm :

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: fruitz-helm
  labels:
    owner: sokube
    scope: fruitz
  namespace: openshift-gitops
spec:
  # Link the ArgoCD application to the fruitz-deployment AppProject 
  project: fruitz-deployment
  # Configure the synchronization of the ArgoCD application
  syncPolicy:
    automated:
      prune: true
      selfHeal: true    
    syncOptions:
      - CreateNamespace=true
  # The specifications of the GitOps source code to be deployed in the cluster 
  source:
    path: helm
    targetRevision: redhat-opentour-geneva
    repoURL: 'git@gitlab.com:sokube-io/sample-apps/fruitz/fruitz-deploy.git'
    helm:
      valueFiles:
        - values.yaml
  # The target kubernetes cluster in which to deploy manifests  
  destination:
    server: https://kubernetes.default.svc
    namespace: fruitz

Pour permettre à OpenShift GitOps d’accéder au repository dans lequel est hébergé le code, si celui-ci ne dispose pas d’une visibilité publique, il est alors nécessaire de créer un Secret Kubernetes avec la clé privée permettant à ArgoCD d’y accéder. Entrez la commande suivante :

Secret private-repo :

apiVersion: v1
kind: Secret
metadata:
  name: private-repo
  namespace: openshift-gitops
  labels:
    argocd.argoproj.io/secret-type: repository
stringData:
  type: git
  url: git@gitlab.com:sokube-io/sample-apps/fruitz/fruitz-deploy.git
  sshPrivateKey: |
    -----BEGIN OPENSSH PRIVATE KEY-----
    ...
    -----END OPENSSH PRIVATE KEY-----

ℹ️   Pensez bien à mettre votre clé privée SSH.

Depuis la console OpenShift, cliquez sur le petit menu, puis sélectionnez Cluster ArgoCD afin d’arriver sur l’application ArgoCD :

OpenShift GitOps logo

OpenShift GitOps dispose d’une intégration SSO utilisant celle déjà intégrée dans OpenShift. Sur la page d’authentification d’OpenShift GitOps sélectionnez LOG IN VIA OPENSHIFT afin d’utiliser le SSO et authentifiez-vous :

OpenShift GitOps login

Vous arrivez sur la page des applications ArgoCD, nous pouvons constater que l’application fruitz-helm est présente ainsi qu’en état Synced (synchronisée) :

OpenShift GitOps synced app

Vous pouvez cliquer sur l’application afin d’obtenir une vue d’ensemble de tous les objets déployés :

OpenShift GitOps global app view

Sur l’objet fruitz-front-ingress tout en bas de la page, vous pouvez cliquer afin d’ouvrir la page web du frontend de l’application :

OpenShift GitOps ingress button

Vous êtes à présent sur la page du frontend de l’application. Vous pouvez tester que celle-ci fonctionne correctement avec l’ajout du fruit Pear. Cependant, l’ajout de tous les fruits ne fonctionne pas. En effet, nous pouvons aussi essayer d’ajouter le fruit Pineapple, et constater que ce fruit aussi n’est pas ajouté. Ce comportement/bug est bien entendu prévu. C’est notamment celui-ci qui met en evidence le bénéfice de l’association des outils ArgocCD et Tekton avec la mise en place du pipeline GitOps Cloud Native corrigeant le bug/comportement volontairement introduit.

Nous ajoutons le fruit Pear qui apparait maintenant sur l’interface web. Nous essayons aussi d’ajouter le fruit Pineapple mais l’ajout de celui-ci ne fonctionne pas et n’apparait donc pas sur l’interface web :

Fruitz application

Après l’échec de l’ajout du fruit Pineapple nous pouvons analyser la stack trace en interrogeant les logs du pod backend. Nous constatons l’erreur suivante ERROR: value too long for type character varying(6). La taille de la chaîne de caractères est trop longue :

Fruitz application logs

En effet, la taille de la chaîne de caractères est limitée à 6. Cela correspond au code en surbrillance jaune :

Fruitz application source code

Nous avons explicitement spécifié la contrainte que la chaîne de caractères pour le nom des fruits ne pouvait pas dépasser 6 caractères.

OpenShift Pipelines

Après avoir démontré que notre application était fonctionnelle mais volontairement limitée, nous allons dans cette partie, montrer comment avoir une chaîne d’intégration complète permettant au développeur d’accélérer le développement de son application. La création d’un pipeline d’Intégration Continue (CI) directement depuis le commit d’une nouvelle fonctionnalité grâce à Tekton et déployer la nouvelle image dans le cluster OpenShift grâce à ArgoCD.

Tekton Trigger

Créez le projet (namespace) OpenShift cicd :

oc new-project cicd
  • Un EventListener: cette ressource permet de créer un point d’entrée sur notre cluster OpenShift afin de déclencher les pipelines,
  • Un TriggerBinding: cette ressource permet de faire la liaison entre les metadatas récupérées et les transmettre aux pipelines,
  • Un Trigger: ressource permettant de définir le type d’intercepteur (ex: GitLab, GitHub …) mais aussi sur quel type d’évènement déclencher les pipelines mais aussi d’extraire et transformer sur les metadatas récupérées
    • un secret utilisé par le Trigger contenant le token qui sera associé au webhook permettant l’authentification,
  • Un TriggerTemplate permettant de variabiliser le pipeline qui sera déclenché.

EventListener gitlab-listener-interceptor:

apiVersion: triggers.tekton.dev/v1beta1
kind: EventListener
metadata:
  name: gitlab-listener-interceptor
  namespace: cicd
spec:
  serviceAccountName: pipeline
  triggers:
    - triggerRef: gitlab-listener

La création de cette ressource entraîne la création d’une Route OpenShift. Vous aurez besoin du host lié à cette route lors de la création d’un webhook. Pour récupérer le host lié à à la route, entrez la commande suivante :

oc -n cicd get route -ojsonpath='{.items[*].status.ingress[*].host}'

Le résultat est le suivant :

el-gitlab-listener-interceptor-cicd.apps.ocp-dev.infrasokube.io

TriggerBinding gitlab-triggerbinding:

apiVersion: triggers.tekton.dev/v1alpha1
kind: TriggerBinding
metadata:
  name: gitlab-triggerbinding
  namespace: cicd
spec:
  params:
  - name: buildRevision
    value: $(body.checkout_sha)
  - name: gitrepositoryurl
    value: $(body.repository.git_ssh_url)
  - name: buildRevisionShort
    value: $(extensions.short_sha)    
  - name: buildRevisionBranch
    value: $(extensions.branch_name)

Trigger gitlab-listener:

apiVersion: triggers.tekton.dev/v1beta1
kind: Trigger
metadata:
  name: gitlab-listener
  namespace: cicd
spec:
  serviceAccountName: pipeline
  interceptors:
    - name: gitlab 
      ref:
        name: "gitlab"
      params:
        - name: "secretRef"
          value:
            secretName: gitlab-trigger-secret
            secretKey: secretToken
        - name: "eventTypes"
          value: ["Push Hook"]
    - name: custom-parameters
      ref:
        name: "cel"
      params:
        - name: "overlays"
          value:
          - key: short_sha
            expression: "body.checkout_sha.truncate(8)"
          - key: branch_name
            expression: "body.ref.split('/')[2]"
  bindings:
    - ref: gitlab-triggerbinding
  template:
    ref: gitlab-triggertemplate

Le secret associé gitlab-trigger-secret:

apiVersion: v1
kind: Secret
metadata:
  name: gitlab-trigger-secret
  namespace: cicd
type: Opaque
stringData:
  secretToken: "abcdefghijklmnopqrstuvwxyz0123456789ABCDEF"

ℹ️   Ce secret devra aussi être utilisé lors de la création du webhook sur GitLab.

TriggerTemplate gitlab-triggertemplate :

apiVersion: triggers.tekton.dev/v1alpha1
kind: TriggerTemplate
metadata:
  name: gitlab-triggertemplate
  namespace: cicd
spec:
  params:
  - name: buildRevision
    description: The Git commit revision
  - name: buildRevisionShort
    description: The Git commit revision
  - name: gitrepositoryurl
    description: The git repository url
  - name: buildRevisionBranch
    description: The git branch

  resourcetemplates:
    - apiVersion: tekton.dev/v1beta1
      kind: PipelineRun
      metadata:
        generateName: fruitz-quarkus-pipelinerun- 
      spec:
        serviceAccountName: pipeline
        pipelineRef:
          name: fruitz-quarkus
        params:
          - name: buildRevision
            value: $(tt.params.buildRevision)
          - name: buildRevisionShort
            value: $(tt.params.buildRevisionShort)
          - name: buildRevisionBranch
            value: $(tt.params.buildRevisionBranch)
        workspaces:
          - name: shared-workspace
            volumeClaimTemplate:
              spec:
                storageClassName: "nfs-client"
                accessModes:
                  - ReadWriteOnce
                resources:
                  requests:
                    storage: 1Gi
        resources:
          - name: git-source-fruitz-application
            resourceSpec:
              type: git
              params:
                - name: revision
                  value: $(tt.params.buildRevisionBranch)
                - name: url
                  value: $(tt.params.gitrepositoryurl)
          - name: git-source-fruitz-deployment
            resourceSpec:
              type: git
              params:
                - name: revision
                  value: redhat-opentour-geneva
                - name: url
                  value: git@gitlab.com:sokube-io/sample-apps/fruitz/fruitz-deploy.git

        taskRunSpecs:
          - pipelineTaskName: maven-package
            taskServiceAccountName: pipeline
            taskPodTemplate:
              volumes:
                - name: config-volume
                  configMap:
                    name: mvn-settings

Après avoir créé les différentes ressources Kubernetes, nous devons créer un webhook sur le repository applicatif Java fruitz-quarkus qui permettra de prendre en compte les modifications en fonction du type d’évènement et de les transmettre à Tekton. Dans notre cas, lors du push d’un nouveau commit sur le repository fuitz-quarkus un pipeline d’Intégration Continue (CI) sera automatiquement déclenché.

Pour rappel, nous utilisons le SCM GitLab. Dans le projet, veuillez vous rendre dans partie latérale et cliquer sur Settings ► Webhook :

GitLab create webhook

Puis entrez les informations suivantes:

  • URL: https://el-gitlab-listener-interceptor-cicd.apps.ocp-dev.infrasokube.io
  • Secret Token: abcdefghijklmnopqrstuvwxyz0123456789ABCDEF
  • Trigger:
    • [x] Push events
    • [x] All branches
  • SSL verification
    • [x] Enable SSL verification

Le champ URL représente l’URL de la route OpenShift qui est créée par l’objet Tekton EventListener. C’est l’URL sur laquelle Tekton Trigger écoute et réceptionne les webhooks envoyés par GitLab. Elle permet notamment de déclencher les pipelines mais aussi la transmission des informations et des meta données à Tekton.

  • Le champ Secret Token permet l’authentification lors du webhook envoyé,
  • Le champ Trigger permet de définir sur quel type d’évènement les webhooks seront déclenchés,
  • Le champs SSL, quant à lui, permet de vérifier la terminaison TLS de l’URL.

GitLab create webhooks

Tekton Pipelines

Configuration

Tasks

Nous devons maintenant créer les tâches qui composeront le pipeline de d’Intégration Continue (CI) applicatif. Notre pipeline sera simple et composé de tâches classiques :

  1. Le packaging du projet Java Quarkus et la création de l’artefact Java (.jar)
  2. Le build de l’image Docker avec le package Java Quarkus et le push de l’image buildée dans un registre de conteneur
  3. Un scan de vulnérabilité de l’image Docker
  4. La mise à jour du repository Helm de déploiement qui permettra de redéployer l’application avec la correction du bug applicatif.

Vous devrez créer les manifests suivants :

Task maven-package :

apiVersion: tekton.dev/v1alpha1
kind: Task
metadata:
  name: maven-package
  namespace: cicd
spec:
  description: >-
    Maven packaging Task with multiple steps

  resources:
    inputs:
      - name: git-source-fruitz-application
        type: git

  workspaces:
    - name: shared-workspace

  params:
    - name: mavenSettingsPath
      type: string
      description: The location path of the maven settings file
      default: "/tmp"
    - name: mavenSettingsFileName
      type: string
      description: The name of the maven settings file
      default: "mvn-settings.xml"
    - name: mavenContainerImage
      type: string
      description: The name of maven container image
      default: maven:3.6.3-jdk-11

  results:
    - name: mavenProjectVersion
      description: The Maven project version number

  steps:
    - name: get-maven-project-version
      image: $(params.mavenContainerImage)
      workingDir: "$(resources.inputs.git-source-fruitz-application.path)"
      env:
        - name: "MAVEN_OPTS"
          value: "-Dmaven.repo.local=$(workspaces.shared-workspace.path)"
      script: |
        #!/usr/bin/env sh
        mvn -s $(params.mavenSettingsPath)/$(params.mavenSettingsFileName) help:evaluate 
          -Dexpression=project.version 
          -q -DforceStdout > $(results.mavenProjectVersion.path)
      volumeMounts:
        - name: config-volume
          mountPath: "$(params.mavenSettingsPath)"

    - name: maven-package
      image: $(params.mavenContainerImage)
      workingDir: "$(resources.inputs.git-source-fruitz-application.path)"
      env:
        - name: "MAVEN_OPTS"
          value: "-Dmaven.repo.local=$(workspaces.shared-workspace.path)"
      script: |
        #!/usr/bin/env sh
        mvn -s $(params.mavenSettingsPath)/$(params.mavenSettingsFileName) clean package -DskipTests
      volumeMounts:
        - name: config-volume
          mountPath: "$(params.mavenSettingsPath)"
      securityContext:
        privileged: true

    - name: copy-target-artifacts-folder
      image: $(params.mavenContainerImage)
      workingDir: "$(resources.inputs.git-source-fruitz-application.path)"
      script: |
        #!/usr/bin/env sh
        cp -R $(resources.inputs.git-source-fruitz-application.path)/target 
          $(workspaces.shared-workspace.path)

Task container-image-build-push :

apiVersion: tekton.dev/v1alpha1
kind: Task
metadata:
  name: container-image-build-push
  namespace: cicd
spec:
  description: >-
    Container image build and push task with multiple steps

  resources:
    inputs:
      - name: git-source-fruitz-application
        type: git

  workspaces:
    - name: shared-workspace

  params:
    - name: dockerRegistryName
      type: string
      description: Name of Docker registry
      default: "registry.gitlab.com"
    - name: gitlabUsernameAccountName
      type: string
      description: Name of gitlab username
      default: "sokube-io"
    - name: gitlabGroupSampleAppsName
      type: string
      description: Name of gitlab group (1st level)
      default: "sample-apps"
    - name: gitlabSubGroupFruitzName
      type: string
      description: Name of gitlab subgroup (2nd level)
      default: "fruitz"
    - name: gitlabProjectFruitzQuarkusName
      type: string
      description: Name of quarkus project
      default: "fruitz-quarkus"
    - name: mavenProjectVersion
      type: string
      description: The Maven project version number
    - name: buildRevision
      type: string
      description: The Git commit revision
    - name: buildRevisionShort
      type: string
      description: The short Git commit revision
    - name: buildRevisionBranch
      type: string
    - name: mavenContainerImage
      type: string
      description: The name of maven container image
      default: maven:3.6.3-jdk-11
    - name: buildahContainerImage
      description: The location of the buildah builder image
      default: registry.redhat.io/rhel8/buildah:latest
    - name: buildahStorageDriver
      type: string
      description: Set buildah storage driver
      default: vfs
    - name: dockerfilePath
      type: string
      description: Path to the Dockerfile to build
      default: ./Dockerfile
    - name: buildContext
      type: string
      description: Path to the directory to use as context
      default: .
    - name: registryTlsVerify
      type: string
      description: Verify the TLS on the registry endpoint (for push/pull to a non-TLS registry)
      default: "true"
    - name: containerBuiltFormat
      type: string
      description: The format of the built container, oci or docker
      default: docker
    - name: buildahBuildExtraArgs
      type: string
      description: Extra parameters passed for the build command when building images.
      default: ""
    - name: buildahPushExtraArgs
      type: string
      description: Extra parameters passed for the push command when pushing images.
      default: ""

  results:
    - name: dockerImageFullName
      description: Full name of docker image builded
    - name: dockerImageFullTag
      description: Full name of docker image builded
    - name: IMAGE_DIGEST
      description: Digest of the image just built.

  steps:
    - name: show-informations
      image: $(params.mavenContainerImage)
      workingDir: "$(resources.inputs.git-source-fruitz-application.path)"
      script: |
        #!/usr/bin/env sh
        echo "------------------------------"
        echo "Project version: $(params.mavenProjectVersion)"
        echo "------------------------------"
        echo " "
        echo "------------------------------"
        echo "The commit ID: $(params.buildRevision)"
        echo "------------------------------"
        echo " "
        echo "------------------------------"
        echo "The short commit ID: $(params.buildRevisionShort)"
        echo "------------------------------"

    - name: retrieve-target-artifacts-folder
      image: $(params.mavenContainerImage)
      workingDir: "$(resources.inputs.git-source-fruitz-application.path)"
      script: |
        #!/usr/bin/env sh
        cp -R $(workspaces.shared-workspace.path)/target 
          $(resources.inputs.git-source-fruitz-application.path)

    - name: build-image
      image: $(params.buildahContainerImage)
      script: |
        # Build Image
        buildah --storage-driver=$(params.buildahStorageDriver) bud 
          $(params.buildahBuildExtraArgs) 
          --format=$(params.containerBuiltFormat) 
          --build-arg PROJECT_VERSION=$(params.mavenProjectVersion)-$(params.buildRevisionShort) 
          --build-arg BUILD_GIT_COMMIT=$(params.buildRevisionShort)  
          --build-arg BUILD_BRANCH_NAME=$(params.buildRevisionBranch) 
          --tls-verify=$(params.registryTlsVerify) --no-cache 
          -f $(params.dockerfilePath) 
          -t $(params.dockerRegistryName)/$(params.gitlabUsernameAccountName)/$(params.gitlabGroupSampleAppsName)/$(params.gitlabSubGroupFruitzName)/$(params.gitlabProjectFruitzQuarkusName)/$(params.gitlabProjectFruitzQuarkusName):$(params.mavenProjectVersion)-$(params.buildRevisionShort) 
          $(params.buildContext)

        # Save image name to Tekton result 
        echo $(params.dockerRegistryName)/$(params.gitlabUsernameAccountName)/$(params.gitlabGroupSampleAppsName)/$(params.gitlabSubGroupFruitzName)/$(params.gitlabProjectFruitzQuarkusName)/$(params.gitlabProjectFruitzQuarkusName):$(params.mavenProjectVersion)-$(params.buildRevisionShort) 
          > $(results.dockerImageFullName.path)

        # Save project version to Tekton result 
        echo $(params.mavenProjectVersion)-$(params.buildRevisionShort) 
          > $(results.dockerImageFullTag.path)
      volumeMounts:
        - mountPath: /var/lib/containers
          name: varlibcontainers
      workingDir: "$(resources.inputs.git-source-fruitz-application.path)"

    - name: push-image
      image: $(params.buildahContainerImage)
      script: |
        buildah --storage-driver=$(params.buildahStorageDriver) push 
          $(params.buildahPushExtraArgs) 
          --tls-verify=$(params.registryTlsVerify) 
          --digestfile $(resources.inputs.git-source-fruitz-application.path)/image-digest 
          $(params.dockerRegistryName)/$(params.gitlabUsernameAccountName)/$(params.gitlabGroupSampleAppsName)/$(params.gitlabSubGroupFruitzName)/$(params.gitlabProjectFruitzQuarkusName)/$(params.gitlabProjectFruitzQuarkusName):$(params.mavenProjectVersion)-$(params.buildRevisionShort) 
          docker://$(params.dockerRegistryName)/$(params.gitlabUsernameAccountName)/$(params.gitlabGroupSampleAppsName)/$(params.gitlabSubGroupFruitzName)/$(params.gitlabProjectFruitzQuarkusName)/$(params.gitlabProjectFruitzQuarkusName):$(params.mavenProjectVersion)-$(params.buildRevisionShort)
      volumeMounts:
        - mountPath: /var/lib/containers
          name: varlibcontainers
      workingDir: "$(resources.inputs.git-source-fruitz-application.path)"
  volumes:
    - emptyDir: {}
      name: varlibcontainers

Task scan-trivy :

apiVersion: tekton.dev/v1alpha1
kind: Task
metadata:
  name: scan-trivy
  namespace: cicd
spec:
  description: >-
    Security scan with Trivy tool

  workspaces:
    - name: shared-workspace

  params:
    - name: dockerRegistryName
      type: string
      description: Name of Docker registry
      default: "registry.gitlab.com"
    - name: dockerImageFullName
      type: string
      description: Name of docker image to scan
    - name: trivyContainerImage
      type: string
      description: The name of Snyk container image
      default: aquasec/trivy:0.31.3

  steps:
    - name: trivy-scan
      image: $(params.trivyContainerImage)
      workingDir: "$(workspaces.shared-workspace.path)"
      env:
        - name: "TRIVY_AUTH_URL"
          value: "$(params.dockerRegistryName)"
        - name: "TRIVY_USERNAME"
          valueFrom:
            secretKeyRef:
              name: trivy-registry-gitlab-com-credentials
              key: gitlab-username-account
        - name: "TRIVY_PASSWORD"
          valueFrom:
            secretKeyRef:
              name: trivy-registry-gitlab-com-credentials
              key: gitlab-registry-token

      script: |
        #!/usr/bin/env sh

        ## Create trivy folder
        mkdir -p $(workspaces.shared-workspace.path)/trivy/scan_result 

        ## Show Trivy version
        echo "Trivy version:"
        trivy --version
        echo ""

        ## Show image to scan
        echo "Image to scan:"
        echo $(params.dockerImageFullName)

        ## Scan 
        trivy image --exit-code 0 
          --cache-dir $(workspaces.shared-workspace.path)/trivy/.trivycache/ 
          --format table 
          $(params.dockerImageFullName)

Task update-helm-deployment-repository :

apiVersion: tekton.dev/v1alpha1
kind: Task
metadata:
  name: update-helm-deployment-repository
  namespace: cicd
spec:
  description: >-
    The Helm deployment repository update 

  resources:
    inputs:
      - name: git-source-fruitz-deployment
        type: git

  workspaces:
    - name: shared-workspace

  params:
    - name: buildRevision
      type: string
      description: The Git commit revision    
    - name: buildRevisionShort
      type: string
      description: The short Git commit revision
    - name: dockerImageFullTag
      type: string
    - name: toolboxContainerImage
      type: string
      description: The name of Toolbox container image
      default: samiamoura/ci-toolbox:1.1.0-fd40008e
    - name: helmDeploymentRepositoryBranch
      type: string
      description: The name of the Helm Fruitz deployment repository
      default: redhat-opentour-geneva

  steps:
    - name: update-helm-deployment-repository
      image: $(params.toolboxContainerImage)
      workingDir: "$(resources.inputs.git-source-fruitz-deployment.path)"
      script: |
        #!/usr/bin/env sh

        ## SSH configuration
        ls -la ~/.ssh/
        eval $(ssh-agent)
        ssh-add ~/.ssh/id_*

        ## Git commands
        git branch
        git checkout $(params.helmDeploymentRepositoryBranch)

        echo " "
        echo "------------------------------"
        echo "Container Image tag: $(params.dockerImageFullTag)"
        echo "------------------------------"
        echo " "

        ## Update the values.yaml file
        yq w -i helm/values.yaml backend.image.tag --style=double $(params.dockerImageFullTag)

        ## Git commands
        git --no-pager diff
        git config user.email "sami.amoura@sokube.ch"
        git config user.name "Sami Amoura"

        git add helm/values.yaml
        git commit -m "Automatic update $(params.dockerImageFullTag)"
        git push origin $(params.helmDeploymentRepositoryBranch)

Vous devez aussi créer les secrets associés:

Le Secret gitlab-com-ssh-key qui permettra à Tekton d’opérer votre repository GitLab privé :

apiVersion: v1
kind: Secret
metadata:
  namespace: cicd
  name: gitlab-com-ssh-key
  annotations:
    tekton.dev/git-0: gitlab.com
type: kubernetes.io/ssh-auth
stringData:
  ssh-privatekey: |
    -----BEGIN OPENSSH PRIVATE KEY-----
    ...
    -----END OPENSSH PRIVATE KEY-----

ℹ️   Pensez bien à mettre votre clé privée SSH.

Le Secret gitlab-com-docker-registry-token qui permettra à Tekton d’opérer votre registre de conteneurs GitLab :

apiVersion: v1
kind: Secret
metadata:
  name: gitlab-com-docker-registry-token
  namespace: cicd
  annotations:
    tekton.dev/docker-0: https://registry.gitlab.com
type: kubernetes.io/basic-auth
stringData:
  username: samiamoura
  password: MY_GITLAB_SECRET_TOKEN

ℹ️   Pensez bien à renseigner le champ stringData.password en générant un token sur GitLab.

Le Secret trivy-registry-gitlab-com-credentials qui permettra quant à lui d’autoriser l’outil Trivy à analyser directement les images hébergées dans votre registre d’image GitLab :

apiVersion: v1
kind: Secret
metadata:
  name: trivy-registry-gitlab-com-credentials
  namespace: cicd
stringData:
  gitlab-registry-token: GITLAB_SECRET_TOKEN
  gitlab-username-account: sokube-io

ℹ️   Pensez bien à renseigner le champ stringData.gitlab-registry-token en générant un token sur GitLab.

Lors de la création du Pprojet (namespace) sur OpenShift, un ServiceAccount pipeline est créé par défaut avec l’ensemble des permissions nécessaires pour executer les différentes tasks. Pour associer les secrets créés précédemment au ServiceAccount Tekton et permettre l’utilisation des secrets durant le pipeline, il est nécéssaire de patcher le ServiceAccount* à l’aide des commandes suivantes :

oc -n cicd patch sa pipeline 
  --type='json' 
  -p='[{"op": "add", "path": "/secrets/0/name", "value":"gitlab-com-docker-registry-token"}]'

oc -n cicd patch sa pipeline 
  --type='json' 
  -p='[{"op": "add", "path": "/secrets/1/name", "value":"gitlab-com-ssh-key"}]'

Vous pouvez maintenant vous rendre dans la console OpenShift et cliquer sur Pipeline ► Tasks afin de voir les tasks créées :

Tekton all tasks

Pipeline

Après avoir créé les ressources de type Tasks nous allons maintenant créer la ressource de type Pipeline qui nous permettra d’orchestrer l’exécution des Tasks avec notamment l’ordre d’exécution ou entre autre leur conditionnalité d’exécution.

Pipeline fruitz-quarkus :

apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
  name: fruitz-quarkus
  namespace: cicd
spec:

  workspaces:
    - name: shared-workspace

  resources:
    - name: git-source-fruitz-application
      type: git
    - name: git-source-fruitz-deployment
      type: git

  params:
    - name: buildRevision
    - name: buildRevisionShort
    - name: buildRevisionBranch

  tasks:
    - name: maven-package
      taskRef:
        name: maven-package
      resources:
        inputs:
          - name: git-source-fruitz-application
            resource: git-source-fruitz-application
      workspaces:
        - name: shared-workspace
          workspace: shared-workspace

    - name: container-image-build-push
      taskRef:
        name: container-image-build-push
      params:
        - name: buildRevision
          value: $(params.buildRevision)
        - name: buildRevisionShort
          value: $(params.buildRevisionShort)
        - name: buildRevisionBranch
          value: $(params.buildRevisionBranch)
        - name: mavenProjectVersion
          value: $(tasks.maven-package.results.mavenProjectVersion)
      resources:
        inputs:
          - name: git-source-fruitz-application
            resource: git-source-fruitz-application
      workspaces:
        - name: shared-workspace
          workspace: shared-workspace
      runAfter:
        - maven-package

    - name: scan-trivy
      taskRef:
        name: scan-trivy
      params:
        - name: dockerImageFullName
          value: $(tasks.container-image-build-push.results.dockerImageFullName)
      workspaces:
        - name: shared-workspace
          workspace: shared-workspace
      runAfter:
        - container-image-build-push

    - name: update-helm-deployment-repository
      taskRef:
        name: update-helm-deployment-repository
      params:
        - name: dockerImageFullTag
          value: $(tasks.container-image-build-push.results.dockerImageFullTag)
        - name: buildRevision
          value: $(params.buildRevision)
        - name: buildRevisionShort
          value: $(params.buildRevisionShort)
      resources:
        inputs:
          - name: git-source-fruitz-deployment
            resource: git-source-fruitz-deployment    
      workspaces:
        - name: shared-workspace
          workspace: shared-workspace
      runAfter:
        - scan-trivy

Vous pouvez maintenant vous rendre dans la console OpenShift et cliquer sur Pipelines ► Pipelines ► Pipelines afin de voir le pipeline créé :

Tekton pipeline view

Vous pouvez aussi cliquer directement sur le pipeline fruitz-quarkus afin d’avoir plus d’informations sur celui-ci comme le nombre de Tasks liées, les Workspaces, les lLbels associés…

Tekton pipeline detailled view

✅   La configuration Tekton est maintenant terminée.

Execution du pipeline

Nous allons maintenant corriger le bug applicatif limitant l’ajout de fruits dont le nombre maximal de caractères ne peut dépasser 6. Nous étendrons cette limite à 9 caractères afin de pouvoir ajouter le fruit Pineapple.

Sur le repository applicatif fruitz-quarkus (backend), éditez le fichier src/main/java/org/sokube/hibernate/orm/Fruit.java pour augmenter le nombre maximal de caractères à 9 :

GitLab fruitz-quarkus update

ℹ️   On peut constater que le commit ID est le suivant 157dbedb

Après avoir commité et poussé les modifications sur le repository applicatif fruitz-quarkus, nous pouvons constater que le pipeline Tekton a bien été déclenché et exécuté avec succès :

Tekton pipeline triggered

Tekton pipeline success

Il est possible d’avoir plus de détails en cliquant sur le Pipeline :

Tekton pipeline success

Nous pouvons aussi avoir une vue détaillée des différentes Tasks avec l’exécution des Steps :

Tekton pipeline success

Nous pouvons voir que l’image a bien été poussée dans la conteneur registry GitLab avec le bon tag (commit ID 157dbedb) :

GitLab container registry

Sur ArgoCD, nous constatons que le pod fruitz-backend a bien été redéployé avec la nouvelle image et le bon tag (commit ID 157dbedb) :

ArgoCD new image

ArgoCD new deployed image

Cela a été rendu possible car le pipeline Tekton et notamment la Task update-helm-deployment-repository à mis à jour le repository GitOps de déploiement synchronisé avec ArgoCD.

GitLab GitOps source code updated

Pour terminer nous pouvons tester la nouvelle fonctionnalité sur l’application. Nous pouvons voir qu’il est maintenant possible d’ajouter le fruit Pineapple :

GitLab GitOps source code updated

Dans ce blog post nous avons pu voir comment mettre en place une chaîne d’intégration GitOps Cloud Native sur la plateforme OpenShift. Cet article présente les outils, comment les associer ainsi que comment les prendre rapidement en main. Bien entendu, il s’agit d’une démonstration pour une expérimentation rapide, de nombreuses choses supplémentaires doivent être prises en compte dans un contexte d’entreprise comme notamment, la gestion des secrets, la partie RBAC autour d’OpenShift GitOps ou encore les Steps composant le pipeline Tekton (ajout de tests, définition scanner les vulnérabilités pour les dépendances applicatives, pousser l’image dans un registry dédiée uniquement si le scan de vulnérabilité est négatif…).

Dans un prochain blogpost nous montrerons comment mettre en place le plugin ArgoCD Image Updater. Cet outil qui s’intègre parfaitement à ArgoCD, permet d’avoir un pipeline GitOps encore plus Cloud Native. Effectivement, il permet de se soustraire de la Task Tekton update-helm-deployment-repository qui vient commiter dans le repository de déploiement. Cette partie sera entièrement gérée par l’outil ArgoCD Image Updater.

Laisser un commentaire

  Edit this page