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 :
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…).
Un pipeline CI/CD Cloud Native repose sur les 3 piliers suivants :
Voici les principales caractéristiques :
Le projet Tekton se compose de deux entités Tekton Pipelines et Tekton Triggers.
Ces deux composants sont complémentaires pour la mise en place d’une chaîne d’intégration complète.
Le projet Tekton Pipelines est composé de plusieurs types de CustomResource Kubernetes:
Voici l’arborescence hiérarchisée des CustomResource OpenShift Pipelines :
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.
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à :
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"]
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 :
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 :
Interaction entre les différentes CustomResource Kubernetes lors de l’execution d’une Task :
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 :
Interaction entre les différentes entités Kubernetes lors de l’execution d’un Pipeline :
Exemple d’instanciation de PipelineRun à partir de pipelines à différentes heures :
Le projet Tekton Triggers est composé de plusieurs types de CustomResource Kubernetes :
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.
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.
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 :
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.
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) :
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.
Le modèle GitOps repose sur 4 piliers fondamentaux :
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 est composé de plusieurs types de ressources :
Dans le cadre de ce blogpost nous utiliserions essentiellement les ressources AppProject et Application .
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éé.
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).
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 :
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 :
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 :
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"
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 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 :
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) :
Vous pouvez cliquer sur l’application afin d’obtenir une vue d’ensemble de tous les objets déployés :
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 :
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 :
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 :
En effet, la taille de la chaîne de caractères est limitée à 6
. Cela correspond au code en surbrillance jaune :
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.
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.
Créez le projet (namespace) OpenShift cicd
:
oc new-project cicd
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 :
Puis entrez les informations suivantes:
https://el-gitlab-listener-interceptor-cicd.apps.ocp-dev.infrasokube.io
abcdefghijklmnopqrstuvwxyz0123456789ABCDEF
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.
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 :
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 :
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éé :
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…
✅ La configuration Tekton est maintenant terminée.
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 :
ℹ️ 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 :
Il est possible d’avoir plus de détails en cliquant sur le Pipeline :
Nous pouvons aussi avoir une vue détaillée des différentes Tasks avec l’exécution des Steps :
Nous pouvons voir que l’image a bien été poussée dans la conteneur registry GitLab avec le bon tag (commit ID 157dbedb
) :
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
) :
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.
Pour terminer nous pouvons tester la nouvelle fonctionnalité sur l’application. Nous pouvons voir qu’il est maintenant possible d’ajouter le fruit Pineapple
:
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.