🇫🇷 J'utilise K3S comme un PaaS - Partie 1 - un Workflow plus simple

Aujourd'hui, j'attaque une nouvelle série autour de K3S. La série précédente est née de mon besoin d'apprendre Kubernetes et avoir un environnement sur mon poste pour pouvoir tester mes développements (webapp et microservices) - et toujours avec une "vision développeur", l'objectif n'étant pas de devenir un admin Kube, mais plutôt un utilisateur "éclairé", ce qui ne peut avoir qu'un impact bénéfique sur ma façon de coder.

Après cette 1ère série, donc, mon workflow local de développeur ressemble à ceci:

alt workflow1

Mais j'aimerais quelque chose de plus "GitOps", où je pourrais faire abstraction de mon Dockerfile (et de ma phase de build locale), quelque chose un peu plus "à la Clever Cloud" ou "à la Heroku": je "pousse" mon code, et je lance le déploiement. Mon workflow ressemblerait à ceci:

alt workflow2

la commande git push implique qu'il nous faudra un serveur git

C'est donc à partir de ce cas d'utilisation que va commencer cette nouvelle série "K3S as a PaaS"

Remerciements une fois de plus à Louis Tournayre pour la très instructive conversation de Samedi matin qui m'a permis de démarrer cette nouvelle série.

Quelle est ma solution ? Les spécifications

👋 Disclaimer: c'est un POC qui date d'hier, tout est bien sûr perfectible - cet article à uniquement pour vocation d'expérimenter et apprendre.

Je disais donc que la commande git push implique qu'il nous faudra un serveur git. Je vais au hasard utiliser GitLab.com, mais tout autre serveur git ferait l'affaire (même installé en local, mais soyez juste sûr que votre cluster puisse "voir" votre serveur git local). Pour arriver à cela, voici un schéma de la "mécanique" associée à mon workflow (j'utilise une application ExpressJS pour mon exemple):

alt workflow3

Dorénavant, je vais "pousser" (git push) mon code dans un projet sur GitLab.com. Et c'est lorsque j'aurais lancé la commande kubectl apply -f etc... que je vais déléguer la partie "chiante" à mon cluster (ou plus précisément à un pod)

  1. Lorsque le pod va se "charger" et se "construire", il va le faire à partir d'une image de container qui est construite à partir d'une image nodejs (node:13.12-slim).
  2. Ensuite le pod va faire un git clone du projet sur GitLab.com.
  3. Une fois le projet "récupéré", le pod va lancer un npm install pour récupérer les dépendances.
  4. Et enfin, le pod exécute un npm start

Et la webapp est déployée 🎉

Il y a certainement d'autres façons de faire (et je suis curieux de les connaître). Là je suis parti au plus simple.

Quelle est ma solution ? La réalisation

Dockerfile

Pour faire cela, tout va se passer dans le manifest de déploiement. Mais avant cela nous devons créer notre image de "container runtime". Mon Dockerfile est tout simple:

FROM node:13.12-slim
WORKDIR /home/app

CMD /bin/bash -c "npm install; npm start"

Le rôle du container sera de télécharger les dépendances et d'exécuter l'application.

Donc buildez votre image et "poussez" là sur une registry. Vous avez différentes solutions:

Ou vous buildez et poussez sur Docker Hub:

DOCKER_USER="votre_docker_user"
IMAGE_NAME="node-js-runtime"
TAG="0.0.0"
IMAGE="${DOCKER_USER}/${IMAGE_NAME}:${TAG}"
docker build -t ${IMAGE_NAME} .
docker tag ${IMAGE_NAME} ${IMAGE}
docker push ${IMAGE}

Et vous utiliserez quelque chose comme ceci: votre_docker_user/node-js-runtime:0.0.0

Où vous buildez et poussez sur votre registry privée:

DOCKER_REGISTRY="acme-registry.docker.nimbo:5000"
IMAGE_NAME="node-js-runtime"
TAG="0.0.0"
IMAGE="${DOCKER_REGISTRY}/${IMAGE_NAME}:${TAG}"
docker build -t ${IMAGE_NAME} .
docker tag ${IMAGE_NAME} ${IMAGE}
docker push ${IMAGE}

Et vous utiliserez quelque chose comme ceci: acme-registry.docker.nimbo:5000/node-js-runtime:0.0.0

Où (la solution la plus simple) vous utilisez mon image Docker toute prête: registry.gitlab.com/bots-garden/funky/funky-node-js-runtime:latest

Pour la suite de l'article c'est ce que j'utiliserai, libre à vous d'adapter bien sûr.

L'application ExpressJS

vous pouvez utilisez tout autre framework JavaScript bien sûr

L'application en elle-même n'a que peu d'intérêt, vous trouverez son code ici: https://gitlab.com/bots-garden/funky/yo.

Le plus important est le fichier (le manifeste) de déploiement. Comme pour la série précédente, je crée un template de manifeste, et je substitue les variables d'environnement avec envsubst.

Le manifeste de déploiement

Pour faire exécuter un git clone de mon projet à mon pod, je vais utiliser ce que l'on appelle un init Container (qui est monté temporairement) et un volume de type EmptyDir

Il sera donc décrit/créé de cette manière:

initContainers: # 1️⃣
- name: git-repository-container 
    image: alpine/git # 2️⃣ 
    imagePullPolicy: Always
    args:
    - clone # 3️⃣
    - -b
    - ${BRANCH}
    - ${GIT_REPOSITORY}
    - /home/app # 4️⃣
    volumeMounts: # 5️⃣
    - name: git-repository-${TAG} # 6️⃣
        mountPath: /home/app
  1. L'init container est monté avant le container du pod
  2. J'utilise l'image alpine/git cf. https://hub.docker.com/r/alpine/git/ qui va me permettre d'exécuter des commandes git dans le "init container"
  3. J'exécute donc un git clone,
  4. Dans le répertoirs /home/app (cf le WORKDIR de mon Dockerfile)
  5. Et cela sera monté/copié dans le volume de type EmptyDir
  6. Je lui donne un nom: git-repository-${TAG} (TAG est calculé comme ceci: $(git rev-parse --short HEAD) comme cela j'ai la garantie que le contenu du volume est renouvelé)

Ensuite je définis mon container "principal":

containers:
- name: ${APPLICATION_NAME}
    image: ${RUNTIME_IMAGE}
    ports:
    - containerPort: ${CONTAINER_PORT}
    imagePullPolicy: Always
    volumeMounts:
    - name: git-repository-${TAG} # 1️⃣
        mountPath: /home/app 
volumes: # 2️⃣
- name: git-repository-${TAG}
    emptyDir: {}
  1. Mon container applicatif partage donc le volume monté sur /home/app avec l'init container
  2. Et enfin, je monte mon volume de type emptyDir

Et finalement on déploie

Les commandes pour déployer seront les suivantes:

à adapter en fonction de votre contexte

export KUBECONFIG=../../cluster/config/k3s.yaml
export SUB_DOMAIN="192.168.64.33.nip.io"
export RUNTIME_IMAGE="registry.gitlab.com/bots-garden/funky/funky-node-js-runtime:latest"
export GIT_REPOSITORY="https://gitlab.com/bots-garden/funky/yo.git" 

git add .; git commit -m "👋 update and 🚀 deploy"; git push

export CONTAINER_PORT=${CONTAINER_PORT:-8080}
export EXPOSED_PORT=${EXPOSED_PORT:-80}

export APPLICATION_NAME=$(basename $(git rev-parse --show-toplevel))
export TAG=$(git rev-parse --short HEAD)
export BRANCH=$(git symbolic-ref --short HEAD)

export HOST="${APPLICATION_NAME}.${BRANCH}.${SUB_DOMAIN}"
export NAMESPACE="funky-apps-${BRANCH}"

kubectl describe namespace ${NAMESPACE} 
exit_code=$?
if [[ exit_code -eq 1 ]]; then
  echo "🖐️ ${NAMESPACE} does not exist"
  echo "⏳ Creating the namespace..."
  kubectl create namespace ${NAMESPACE}
else 
  echo "👋 ${NAMESPACE} already exists"
fi

rm ./kube/*.yaml

envsubst < ./deploy.template.yaml > ./kube/deploy.${TAG}.yaml

kubectl apply -f ./kube/deploy.${TAG}.yaml -n ${NAMESPACE}

Un peu de simplification

Je vous engage à scripter ces commandes bien sûr.

✋ Vous pouvez vous inspirer de mon script de déploiement: https://gitlab.com/bots-garden/funky/yo/-/blob/master/deploy

Maintenant, ma commande de déploiement ressemble à ça:

export KUBECONFIG=../../cluster/config/k3s.yaml
export SUB_DOMAIN="192.168.64.33.nip.io"
./deploy "✨ feature"

Hop hop hop! Ce n'est pas fini

Vous l'aurez sans doute compris, vous pouvez vous créer votre propre runtime pour votre techno préférée.

Quand je ne fais pas de JavaScript, je fais du Vert.x avec du Kotlin 😍. J'ai donc créé un runtime pour pouvoir faire un build maven et exécuter mon application Vert.x.

Le Dockerfile est tout simple:

# build
FROM maven:3.6.0-jdk-8-alpine
WORKDIR /home/app

CMD /bin/bash -c "mvn clean package; cp target/*-fat.jar app.jar; java -jar ./app.jar"

Voici le build maven "dans le pod": alt maven

Et voici une application pour faire vos tests: https://gitlab.com/bots-garden/funky/ping-vert-x

🎉 Amusez vous (bien j'espère) avec ça et 👋 à bientôt pour la suite. Nous traiterons de monitoring (un PaaS sans monitoring, n'est pas un PaaS 😉)

Pour rappel: Précédents blog posts de la série "Kit de survie K8S pour les dévs avec K3S":

Last Articles

Last Updated: 16/04/2020 à 18:46:46