Déploiement continu avec Gitlab-CI et Kubernetes
Créez une chaîne de déploiement continue simple, sécurisée et évolutive avec Gitlab-CI pour des applications exécutées dans Kubernetes. Pas besoin de machines dédiées ni de jeton d'authentification dans les fichiers de configuration de Gitlab-CI.
Table des matières
Prérequis
- Le code source de l'application réside dans une instance Gitlab, registre de conteneurs activé
- Un espace de noms Kubernetes dédié existe déjà pour l'application
- La dernière version de Helm a été installée. Voici la Documentation d'installation de Helm
Enregistrer le runner de déploiement sur Gitlab
Tout d'abord, suivez cette documentation Jeton d'authentification du coureur de projet Gitlab pour créer un jeton d'authentification pour le runner nous allons nous inscrire sur le projet Gitlab.
Nous utiliserons ce jeton dans la configuration du runner Gitlab afin de l'ajouter au projet.
L'étape suivante consiste à utiliser Helm pour enregistrer le runner. Voici comment procéder :
- Ajouter le dépôt des charts Helm Gitlab
$ helm repo add gitlab https://charts.gitlab.io
- Créez le fichier de configuration « values.yml » pour le chart :
gitlabUrl: http://gitlab.your-domain.com/
runnerRegistrationToken: "$runner_registration_token"
rbac:
create: true
clusterWideAccess: false
serviceAccountName: myapp-deployer
rules:
- apiGroups: ["*"]
resources: ["*"]
verbs: ["*"]
runners:
name: myapp
serviceAccountName: myapp-deployer
tags: myapp
- Pour créer le fichier de valeurs par défaut contenant tous les paramètres de configuration disponibles et les valeurs par défaut que nous pouvons utiliser comme référence pour adapter le fichier « values.yml » précédent :
$ helm show values gitlab/gitlab-runner > values-default.yml
Voici une explication des paramètres de configuration que nous mettons dans « values.yml » :
gitlabUrl: l'URL de l'instance Gitlab où réside le projet contenant le code source de l'applicationrunnerRegistrationToken: le jeton d'enregistrement du runner, précédemment récupéré dans les paramètres de Runners du projet Gitlab contenant le code source de l'applicationrbac.create: faut-il créer un compte de service Kubernetes dédié (ainsi que les ressources RBAC associées) qui sera utilisé par les pods qui gèrent les runners Gitlab (les pods enregistrés dans le projet Gitlab qui créent dynamiquement d'autres pods pour exécuter les jobs Gitlab-CI)rbac.clusterWideAccess: il est possible de limiter les autorisations du compte de service utilisé par les runners à l'espace de noms dans lequel il se trouve et donc de créer des ressources RBAC de type role et rolebinding au lieu de clusterrole et clusterrolebindingrbac.serviceAccountName: le nom du compte de service qui sera créé pour être utilisé par les pods qui gèrent les runners Gitlabrbac.rules: définir les règles RBAC à appliquer au compte de service spécifié avecrbac.serviceAccountNamerunners.tags: les tags associés au runner. Peut être utilisé dans les jobs de déploiement pour sélectionner explicitement le runner sur lequel nous voulons que le job s'exécuterunners.serviceAccountName: le nom du compte de service Kubernetes qui sera utilisé par les runners (pods qui exécuteront les tâches de déploiement). Nous définissons cette valeur sur le même compte de service utilisé par les pods qui gèrent les runners pour donner aux pods des runners (pods qui exécutent réellement les tâches CI) les autorisations définies avecrbac.rules
Une fois que « values.yml » est correctement configuré, nous pouvons utiliser la commande suivante pour enregistrer le runner. Les ressources du runner seront créées dans l'espace de nom de l'application que nous appelons myapp:
$ helm upgrade --install --namespace myapp gitlab-runner -f values.yml gitlab/gitlab-runner
Lorsque nous apportons des modifications au fichier de configuration « values.yml », nous pouvons également utiliser cette même commande pour mettre à jour le runner. Nous devrions maintenant voir que le runner de déploiement est enregistré sur le projet en accédant à nouveau aux paramètres des runners.
Le runner est maintenant enregistré et prêt à être utilisé pour exécuter les tâches de déploiement.
Affiner les autorisations du runner de déploiement
Dans la section précédente, nous avons spécifié le compte de service Kubernetes « myapp-deployer » dans la configuration du chart Helm Gitlab « values.yml », comme celui qui sera utilisé par les pods runners des jobs Gitlab-CI pour déployer l'application.
Nous avons accordé à ce compte de service des autorisations pour créer des ressources uniquement dans l'espace de noms de l'application, en utilisant la section de configuration « rbac » dans la configuration « values.yml » du chart.
Dans la section « rbac.rules », tout ce qui n'est pas explicitement autorisé sera refusé. Nous pouvons donc facilement réduire les autorisations du compte de service si nécessaire.
Lorsque nous essayons de faire quelque chose que nous ne sommes pas autorisés à faire avec les exécuteurs (runners) de déploiement, un message d'erreur nous indique précisément quelle autorisation est manquante. Par exemple, nous obtenons ce résultat de Gitlab-CI lorsque nous essayons de supprimer une ressource « secrets » alors que les règles RBAC nous interdisent de le faire :
Et celui-ci lorsque nous essayons de lire des ressources « secrets » alors que les règles RBAC nous interdisent de le faire :
Comme vous pouvez le voir, les 3 éléments nécessaires pour ajouter les règles de permissions manquantes dans 'rbac.rules' à l'intérieur du fichier 'values.yml' sont clairement mentionnés dans le message d'erreur : le groupe API de la ressource, le nom de la ressource et le verbe (delete, get...), et peuvent être très utiles pour déboguer les problèmes de règles RBAC lorsque l'on essaie de donner aux runners uniquement les permissions dont ils ont besoin.
Configurer Gitlab-CI pour un déploiement continu
Nous devons créer un fichier appelé « .gitlab-ci.yml » dans le répertoire racine du projet Gitlab contenant le code source de l'application. Dans ce fichier, nous définirons les tâches de déploiement continu. La première tâche que nous appelons « build » sera utilisée pour créer une image de conteneur à partir du code source de l'application et envoyer cette image au registre de conteneurs Gitlab du projet. La seconde tâche que nous appelons « deploy » sera utilisée pour créer les ressources Kubernetes nécessaires à l'exécution de l'application.
Pour publier une nouvelle version de l'application via le pipeline de déploiement continu, nous devons créer une balise Git dans le projet, nommée avec le nouveau numéro de version de l'application. L'image du conteneur d'application créée pendant la tâche « build » sera étiquetée avec le nouveau numéro de version de l'application défini par la balise Git. De cette façon, la tâche « déployer » pourra récupérer la bonne version de l'image du conteneur de l'application à déployer dans le cluster Kubernetes. Pour plus d'informations sur la syntaxe de configuration et les mots-clés de « .gitlab-ci.yml », voir Référence du mot-clé .gitlab-ci.yml.
Voici le contenu du fichier '.gitlab-ci.yml' :
build:
stage: build
image:
name: gcr.io/kaniko-project/executor:v1.9.0-debug
entrypoint: [""]
script:
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"auth\":\"$(printf "%s:%s" "${CI_REGISTRY_USER}" "${CI_REGISTRY_PASSWORD}" | base64 | tr -d '\n')\"}}}"
- /kaniko/executor
--context "${CI_PROJECT_DIR}"
--dockerfile "${CI_PROJECT_DIR}/Dockerfile"
--destination "${CI_REGISTRY_IMAGE}:${CI_COMMIT_TAG}"
rules:
- if: $CI_COMMIT_TAG
deploy:
stage: deploy
image:
name: registry.gitlab.com/hackerstack/k8s-deployer:v1.25.3
script:
- cat ${CI_PROJECT_DIR}/deploy.yml | envsubst | kubectl apply -f -
tags:
- myapp
rules:
- if: $CI_COMMIT_TAG
Le job de build
Nous utilisons les Kaniko pour créer et envoyer l'image du conteneur d'application au registre de conteneurs Gitlab du projet. De cette façon, nous pouvons créer l'image du conteneur à partir d'un autre conteneur sans utiliser Docker dans Docker et des conteneurs privilégiés, ce qui réduit la complexité et améliore la sécurité. L'image du conteneur d'application est créée à partir d'un Dockerfile présent dans le répertoire racine du code source de l'application dans un projet Gitlab. Pour que Kaniko s'authentifie auprès du registre de conteneurs Gitlab du projet afin d'y enregistrer les images du conteneur d'application, nous créons le fichier '/kaniko/.docker/config.json', contenant les éléments suivants :
{
"auths":{
"registry.gitlab.com":{
"auth":"<base64_encoded_credentials>"
}
}
}
Nous générons ce fichier pendant le job de « build » grâce aux variables d'environnement suivantes définies automatiquement par Gitlab-CI :
CI_REGISTRY: l'adresse du registre de conteneurs Gitlab. 'registry.gitlab.com' dans ce casCI_REGISTRY_USERNAME: le nom d'utilisateur requis pour envoyer les images de conteneur dans le registre de conteneurs Gitlab du projetCI_REGISTRY_PASSWORD: le mot de passe requis pour envoyer les images de conteneur dans le registre de conteneurs Gitlab du projet.
La routine shell <base64_encoded_credentials> représente l'encodage en « base64 » de ${CI_REGISTRY_USERNAME}:${CI_REGISTRY_PASSWORD}.
Les variables d'environnement suivantes utilisées lors du « build » sont également définies automatiquement par Gitlab-CI :
CI_PROJECT_DIR: chemin vers le répertoire actuel du projet Gitlab dans le système de fichiers du runnerCI_REGISTRY_IMAGE: l'adresse de l'image du conteneur dans le registre de conteneurs du projet. Dans ce cas, « registry.gitlab.com/hackerstack/myapp »CI_COMMIT_TAG: le nom du tag Git qui a déclenché la pipeline Gitlab-CI. Dans ce cas, contiendra le numéro de version de l'application. Nous utilisons cette variable pour taguer l'image du conteneur avec le numéro de version de l'application défini dans le tag Git. L'URL de l'image du conteneur résultante ressemblera donc à quelque chose commeregistry.gitlab.com/hackerstack/myapp:<app_version>. Cette variable est finalement utilisée dans la section « rules » de la définition du job de « build » afin de faire en sorte que ce job s'exécute uniquement lorsqu'un tag Git est créé sur le projet.
Pour une liste complète des variables d'environnement qui sont automatiquement définies pour les jobs Gitlab-CI, consultez Variables prédéfinies Gitlab-CI.
Le job de déploiement
Dans le job « deploy », nous créons les ressources Kubernetes nécessaires à l'exécution de l'application dans le cluster Kubernetes. Pour y parvenir, nous utilisons les éléments suivants :
- une image de conteneur contenant les utilitaires « kubectl » et « envsubst »
- le fichier 'deploy.yml' contenant les déclarations de ressources Kubernetes pour l'application
Voici le contenu du fichier 'deploy.yml' :
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
labels:
app: myapp
spec:
replicas: 1
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
imagePullSecrets:
- name: myapp
containers:
- name: myapp
image: registry.gitlab.com/hackerstack/myapp:$CI_COMMIT_TAG
ports:
- name: http
containerPort: 80
resources:
limits:
memory: "500Mi"
requests:
memory: "500Mi"
Il y a deux choses dans ce fichier que nous souhaitons souligner :
-
nous utilisons un « imagePullSecrets » appelé « myapp ». Ce secret sera utilisé pour s'authentifier auprès du registre de conteneurs Gitlab du projet afin d'extraire l'image du conteneur d'application, car le projet n'est pas publiquement accessible. Si le projet était publiquement accessible, nous n'aurions pas besoin de cet « imagePullSecrets ».
-
Pour créer ce secret, vous devez d’abord Créer un jeton de déploiement Gitlab sur le projet et récupérer les « gitlab_deploy_token_username » et « gitlab_deploy_token_password » résultants. Il faut ensuite créer le secret d'extraction de l'image du conteneur d'applications dans l'espace de noms contenant les ressources de l'application dans le cluster Kubernetes à l'aide de la commande suivante :
$ kubectl -n <namespace> create secret docker-registry <secret-name> --docker-server <registry_address> --docker-username=<gitlab_deploy_token_username> --docker-password=<gitlab_deploy_token_password>
Voici un exemple:
$ kubectl -n myapp create secret docker-registry myapp --docker-server registry.gitlab.com --docker-username=registry-ro --docker-password=mysuperpassword
secret/myapp created
$ kubectl describe secret myapp -n myapp
Name: myapp
Namespace: myapp
Labels: <none>
Annotations: <none>
Type: kubernetes.io/dockerconfigjson
Data
====
.dockerconfigjson: 148 bytes
- L'URL de l'image du conteneur que nous utilisons contient la variable
$CI_COMMIT_TAG. Cette variable doit être remplacée pendant le déploiement de la ressource, sinon cela ne fonctionnera pas. C'est pourquoi nous avons transmis le contenu du fichier 'deploy.yml' à l'utilitaire 'envsubst' afin de remplacer la variable$CI_COMMIT_TAGà l'intérieur du fichier par la valeur de cette même variable extraite des variables d'environnement du job. N'oubliez pas que cette variable d'environnement est automatiquement définie par Gitlab-CI et correspond à la nouvelle version de l'application dans ce cas. Une fois la variable$CI_COMMIT_TAGremplacée à l'intérieur du fichier 'deploy.yml', nous transmettons le contenu de ce fichier à l'entrée de la commande 'kubectl apply -f' afin de créer la ressource à l'intérieur du cluster Kubernetes.
Pour exécuter le job « deploy », nous nous assurons d'utiliser le runner de déploiement que nous avons précédemment créé et configuré en spécifiant le tag « myapp » du runner dans la configuration du job.
Voici les journaux Gitlab-CIdu job « deploy ». Nous avons ajouté deux autres commandes à la section « script » du job pour afficher la ressource de déploiement créée et l'image de conteneur utilisée (la variable $CI_COMMIT_TAG a été remplacée comme prévu dans l'URL de l'image du conteneur) :
Configuration de Kubectl
Jetons un œil à la configuration « kubectl » à l’intérieur du conteneur de déploiement.
Comme vous pouvez le voir, il n'y a pas de configuration spécifique à « kubectl ». Vous vous demandez peut-être comment nous pouvons exécuter des commandes « kubectl » pour créer des ressources à l'intérieur du cluster Kubernetes ?
Bonne question... C'est parce que le pod runner que nous utilisons est déployé à l'intérieur du cluster (il sait donc déjà comment communiquer avec lui via son point de terminaison API interne) et utilise le jeton du compte de service « myapp-deployer » (que nous avons configuré précédemment) pour l'authentification, car ce compte de service est associé au pod du runner exécutant le job.
Pour communiquer avec l'API du cluster Kubernetes, le pod du runner utilise les variables d'environnement KUBERNETES_* . Voir la commande 'env' dans l'image suivante.
Sur cette même image, le fichier « ca.crt » affiché dans le résultat de la commande « ls » est le certificat de l'autorité de certification racine du cluster Kubernetes. Ce certificat sera utilisé par le pod du runner lors des communications HTTPS avec l'API du cluster, pour vérifier le certificat du serveur API.
Le fichier « namespace » contient le nom de l'espace de noms Kubernetes dans lequel le pod s'exécute et le fichier « token » contient le jeton du compte de service « myapp-deployer » associé au pod du runner exécutant le job.