Chez Ikuzo, nous gérons l’intégralité de nos projets en interne avec Docker. Se pose la question de : comment pousser notre travail en staging afin de faire tester nos clients, et ce, en utilisant notre container afin de rester au plus près de notre environnement de développement ?
Nous aurions pu pousser directement le container sur un serveur embarquant Docker, mais cela nous aurait forcé, pour chaque projet, à gérer un reverse proxy pour mapper l’URL de staging avec le container déployé. Mais nous n’aimons pas répéter nos tâches :).
C’est là qu’intervient Kubernetes ! Couplé à l’intégration continue de Gitlab, nous pouvons automatiser le processus pour pousser, au fil des commits, nos modifications sur notre environnement de staging. Prêt ? C’est parti !
Les prérequis
Avant toute chose, assurez-vous qu’une image Docker de votre projet peut être buildée et exécutée sans bug (votre projet doit comporter un Dockerfile embarquant les sources de votre application).
Vous devez vous munir d’un cluster Kubernetes (DigitalOcean, Scaleway, Google Cloud, Amazon EKS).
Vous devez également être familier avec Docker (qu’est-ce qu’un container, une image, etc.) et savoir empaqueter votre application sous Docker.
Et pour finir, votre projet doit-être versionné via Gitlab (on-premise ou cloud, pas d’importance).
Maintenant, le plan d’attaque ! Voici les étapes à mettre en place pour notre déploiement :
- À chaque commit (sur une branche spécifique), rebuild l’image Docker et pousser la dernière version sur un private registry
- Une fois le build terminé, pousser la nouvelle version de l’image vers Kubernetes.
Alors je vous vois venir : Oui, il faut également configurer Kubernetes, mais nous avons mis en place des fichiers génériques afin de faciliter nos déploiements.
Intégration du cluster à Gitlab
La première étape est de connecter votre répository Gitlab à votre cluster. Pour cela, une fois sur votre répository, allez dans le menu « Operations -> Kubernetes ». Je vous laisser consulter la documentation liée à cette étape, qui changera en fonction de votre fournisseur Kubernetes.
De notre côté, étant donné que l’intégralité de nos projets clients sont dans un groupe Gitlab propre, nous avons configuré le cluster directement sur ce groupe, afin d’éviter de le reconfigurer pour chaque nouveau projet (c’est déjà une étape de configuration récurrente en moins !).
À noter que nous vous conseillons d’installer les applications Helm Tiller, Ingress (qui sera le front de notre cluster) et Cert Manager (requis pour la génération de certificats SSL).
Profitez-en pour ajouter un token d’accès au registry Gitlab ! Kubernetes en aura besoin pour accéder aux registres de conteneur privés. Pour cela, il faut générer une clé d’API dans votre compte utilisateur.
Une fois obtenue, gardez cette clé de côté et nous allons l’injecter comme variable d’environnement pour tout nos projets.
Rendez-vous dans « Paramètres -> Intégration et livraisons continues » puis Variables. Je vous conseille d’utiliser les mêmes clés que la capture d’écran ci-dessous, afin de ne pas à avoir éditer en conséquence les prochaines configurations.
GITLAB_REGISTRY_EMAIL : l’email attaché à votre compte Gitlab
GITLAB_REGISTRY_TOKEN : votre clé API générée
GITLAB_REGISTRY_USER : votre nom d’utilisateur Gitlab
Configuration du projet
Nous partons d’un Symfony Stock, que je ne détaillerai pas ici mais nous allons nous arrêter sur 5 fichiers importants :
- Dockerfile : fichier qui donne les directives de build pour l’image du conteneur
- .gitlab-ci.yaml : défini les étapes de build et déploiement que Gitlab exécutera.
- k8s/deployment.yml : fichier de configuration Kubernetes qui défini toute la configuration du container (pod)
- k8s/ingress.yml : fichier de configuration Kubernetes qui défini la configuration du front-end de l’application (via Nginx)
- k8s/service.yml : fichier de configuration Kubernetes qui fait le lien entre le pod et notre front.
Inspectons déjà notre fichier Dockerfile :
# De l'image thecodingmachine/php avec les arguments ci dessous
ARG PHP_EXTENSIONS="pdo_mysql intl zip gd"
FROM thecodingmachine/php:7.3-v2-apache
# Je souhaite passer ces variables à notre container
ENV TEMPLATE_PHP_INI=production
ENV APP_ENV=prod
ENV APACHE_DOCUMENT_ROOT=public/
# Je copie toutes les sources du projets à la racine du répertoire de travail de l'image
# (ce chemin dépendra de l'image que vous utilisez comme base de build)
COPY --chown=docker:docker ./ /var/www/html
# Je défini ce repertoire comme répertoire de travail
WORKDIR /var/www/html
# J'installe mes dépendences
RUN composer install
Optimisable (surtout en terme de poids d’image), mais ce fichier contient tous les services afin de build et de « containeriser » notre projet.
Testez en local votre image. Si aucun souci de build n’est déclaré et que vous arrivez à lancer un container basée sur votre image en local, nous pouvons passer à la prochaine étape : les pipelines Gitlab.
Pour déclarer des tâches qui seront exécutées par Gitlab (nommées Pipelines), il vous faudra créer un fichier .gitlab-ci.yml à la racine de votre repository.
image: docker:latest
services:
- docker:dind
stages:
- build
#- deploy
build-dev:
stage: build
script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com
- docker build --pull -t $CI_REGISTRY_IMAGE .
- docker push $CI_REGISTRY_IMAGE
only:
- develop
- Nous partons d’une image docker, qui a plusieurs tâches à accomplir : build et deploy (commenté car détaillé plus bas dans cet article)
- Pour la tâche build-dev, je donne 3 commandes à lancer
- Connecte toi au registry du repository
- Build l’image en te basant sur le fichier Dockerfile à la racine du repository (-t $CI_REGISTRY_IMAGE permet de définir le nom du projet comme nom de l’image)
- Push l’image build sur le registry
- Only : cette tâche ne s’exécute que sur la branche develop
Grâce à ces directives, chaque commit sur la branche develop lance une tâche qui buildera votre image automatiquement, avec vos dernières modifications.
À noter que les variables CI_* sont automatiquement injectées par Gitlab (voir la liste complète).
Nous avons désormais notre image, il ne manque plus qu’à la déployer ! Mais avant tout, il est important d’indiquer à Kubernetes de quoi est composé votre application et comment cette dernière doit-être lancée.
apiVersion: apps/v1
kind: Deployment
metadata:
name: _APP_NAME_
labels:
app: _APP_NAME_
spec:
replicas: 1
selector:
matchLabels:
app: _APP_NAME_
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 33%
template:
metadata:
labels:
app: _APP_NAME_
spec:
imagePullSecrets:
- name: gitlab-registry
containers:
- name: _APP_NAME_
image: registry.gitlab.com/ikuzo/_APP_NAME_:latest
ports:
- containerPort: 80
env:
- name: FOO
value: bar
Voici notre fichier de déploiement. Il permet de définir comment le container doit-être lancé, quelle est son image, ses variables d’environnement, la redondance des pods, etc.
Ne remplacez pas les _APP_NAME_, il seront redéfinis automatiquement un peu plus tard (#automatisation :))
apiVersion: v1
kind: Service
metadata:
labels:
app: front
name: _APP_NAME_
spec:
ports:
- name: http
port: 80
protocol: TCP
selector:
app: _APP_NAME_
type: ClusterIP
Voici notre fichier de service. Il permet de publier en dehors du cluster un port sur un container particulier. Dans notre cas, nous utilisons toujours notre variable _APP_NAME_.
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: _APP_NAME_
annotations:
ingress.kubernetes.io/ssl-redirect: "true"
kubernetes.io/tls-acme: "true"
certmanager.k8s.io/clusterissuer: letsencrypt-prod
kubernetes.io/ingress.class: "nginx"
spec:
tls:
- hosts:
- _APP_NAME_.ikuzo-studio.fr
secretName: _APP_NAME_-letsencrypt
rules:
- host: _APP_NAME_.ikuzo-studio.fr
http:
paths:
- path: /
backend:
serviceName: _APP_NAME_
servicePort: 80
Un peu plus complexe (mais rien d’insurmontable), voici notre fichier ingress, permettant de déclarer le frontend de notre application. Grâce à cette configuration, vous allez pouvoir :
- Générer automatiquement un certificat SSL via Let’s Encrypt
- Générer et activer le vhost automatiquement (veillez à ce qu’un sous-domaine wildcard soit configuré dans les entrées DNS de votre nom de domaine)
- Stocker le certificat dans unsecret kubernetes
À noter qu’en cas de problème de déploiement via Gitlab, vous pouvez toujours pousser ces fichiers de configuration via le client kubectl.
Maintenant, poussons notre container sur Kubernetes ! Voici la suite de notre fichier .gitlab-ci.yml :
deploy-dev:
stage: deploy
image: alpine
environment:
name: staging
script:
- mkdir $HOME/.kube
- cp $KUBECONFIG $HOME/.kube/config
- cat $HOME/.kube/config
- apk update && apk add --no-cache curl
- curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl
- chmod +x ./kubectl && mv ./kubectl /usr/local/bin/kubectl
- kubectl config set-context $(kubectl config current-context)
- 'printf "apiVersion: v1\nkind: Secret\n$(kubectl create secret docker-registry gitlab-registry --docker-server=$CI_REGISTRY --docker-username=$GITLAB_REGISTRY_USER --docker-password=$GITLAB_REGISTRY_TOKEN --docker-email=$GITLAB_USER_EMAIL --docker-email=GITLAB_REGISTRY_EMAIL -o yaml --dry-run)" | kubectl apply -f -'
- sed -i 's/_APP_NAME_/'"$CI_PROJECT_NAME"'/g; s/_VERSION_/'"$CI_COMMIT_SHA"'/g' k8s/deployment.yaml;
- sed -i 's/_APP_NAME_/'"$CI_PROJECT_NAME"'/g; s/_VERSION_/'"$CI_COMMIT_SHA"'/g' k8s/ingress.yaml;
- sed -i 's/_APP_NAME_/'"$CI_PROJECT_NAME"'/g; s/_VERSION_/'"$CI_COMMIT_SHA"'/g' k8s/service.yaml;
- kubectl apply -f k8s/deployment.yaml
- kubectl apply -f k8s/service.yaml
- kubectl apply -f k8s/ingress.yaml
- kubectl patch deployment $CI_PROJECT_NAME -p "{\"spec\":{\"template\":{\"metadata\":{\"labels\":{\"date\":\"`date +'%s'`\"}}}}}"
only:
- develop
- Les 7 premières lignes de script permettent d’installer kubectl pour la tâche (requis pour l’interaction entre le processus Gitlab et notre cluster Kubernetes
- Nous créons un secret nommé gitlab-registry permettant au cluster d’accéder à notre registry privé (souvenez-vous des variables que nous avons ajoutées précédemment !) puis nous l’appliquons au cluster
- 3 sed consécutifs permettant de remplacer _APP_NAME_ par le nom de notre projet
- Puis l’on envoi nos configurations à kubernetes.
- Attention, la dernière ligne sert à forcer le repull de l’image pour chaque commit (par défaut, si le tag de l’image ne change pas, cette dernière reste en cache et donc vous ne verrez pas les modifications apportées depuis l’installation de votre container).
Commitez et pushez vos modifications. Si tout se passe bien (voir capture ci-dessous), vous devriez pouvoir accéder à votre application via l’URL renseignée dans le fichier ingress ! Sinon, cela donnera lieu à un prochain article : Débug ses déploiements Kubernetes.
Sources : https://github.com/Ikuzostudio/demo-kubernetes-gitlabci
Si besoin, n’hésitez pas à nous interpeller sur nos réseaux sociaux, si on peut donner un coup de main ;).
Crédit Image : Praqma.com