Episode précédent:

# 🇫🇷 J'utilise K3S comme un PaaS - Partie 2 - Builder les images dans K3S avec Kaniko

Oui, je sais, comparer K3S à un PaaS, c'est provocateur 😉. C'est plus ma façon de "vivre" avec K3S qui s'inspire de la façon d'utiliser un PaaS.

# Introduction

Dans le précédent blog post je vous partageais une expérimentation où je poussais mon code par l'intermédiare d'un repository git dans un "pod runtime" construit à partir d'une image Docker contenant le runtime approprié (nodejs, java...) qui s'occupait de "compiler" (si nécessaire) le projet et de l'exécuter.

Alors, ça fonctionne (1), le but c'était d'éviter d'avoir à construire mon image Docker sur mon poste, mais ce n'est (peut-être 🤔) pas très orthodoxe (2) et pour être honnête 😉 je ne savais pas encore commment faire construire mon image Docker par un pod dans mon cluster.

Aujourd'hui, je vais utiliser le projet Kaniko (opens new window) pour construire mes images à l'intérieur de mon cluster K3S.

Donc cette fois-ci mon workflow sera le suivant:

alt workflow4

  • je code, je commit, je push vers un repository git (moi c'est sur GitLab.com (opens new window) mais cela fonctionne avec n'importe quel serveur de repositories git)
  • je "fais" un kubectl apply -f de mon manifeste de déploiement
  • 1️⃣ j'ai mon 1er initContainer qui fait un git clone de mon projet
  • une fois le projet cloné, j'ai un 2ème container qui va utiliser Kanilo pour:
    • 2️⃣ "builder" l'image de mon applcation
    • 3️⃣ la pousser sur le Docker hub
  • et enfin créer le pod applicatif 4️⃣ à partir de l'image générée
  • (1): je me garde ça dans un coin, j'ai quelques idées pour l'utiliser dans d'autres expérimentation
  • (2): j'aimais bien aussi l'idée de "cacher" l'adhérence à Docker (pas besoin de Dockerfile pour déployer mon application)

# Tout est dans le manifeste

# Dockerfile

🖐️ Bon, il faut quand même un Dockerfile. Je repars de mon application ExpressJS (je vous donnerais tout le code source un peu plus loin):

FROM node:13.12-slim
COPY . .
RUN npm install
CMD [ "node", "index.js" ]

# Credentials

Il faut aussi "transmettre" vos credentials Docker à Kaniko pour lui permettre de se connecter au Docker hub. Kaniko a besoin d'un fichier de configuration config.json placé dans /kaniko/.docker et qui ressemble à ceci:

{
  "auths": {
    "https://index.docker.io/v1/": {
      "auth": "xxxxxxxxxxxxxxx"
    }
  }
}

le contenu de "xxxxxxxxxxxxxxx" doit être votre_user_docker:votre_mot_de_passe_docker encodé en base 64.

Donc si vous êtes bob et que votre mot de passe est morane, faites:

echo -n bob:morane | base64

Vous obtenez:

Ym9iOm1vcmFuZQ==

Utilisez cette chaîne de caractères dans votre fichier config.json:

{
  "auths": {
    "https://index.docker.io/v1/": {
      "auth": "Ym9iOm1vcmFuZQ=="
    }
  }
}

Pour fournir nos credentials au pod Kaniko, nous allons créer une ConfigMap:

kubectl create configmap docker-config -n funky-demo-prod --from-file=./config.json
  • 🖐️ funky-demo-prod c'est le nom du namespace que j'utilise, changez avec le vôtre
  • ✋ si vous laissez votre fichier config.json dans votre repository, N'OUBLIEZ PAS DE LE REFERENCER DANS .gitignore

Vérifiez que "tout est là":

kubectl describe configmap docker-config -n funky-demo-prod

Vous devriez obtenir ceci:

Name:         docker-config
Namespace:    funky-demo-prod
Labels:       <none>
Annotations:  <none>

Data
====
config.json:
----
{
  "auths": {
    "https://index.docker.io/v1/": {
      "auth": "Ym9iOm1vcmFuZQ=="
    }
  }
}
Events:  <none>

Maintenant on passe aux choses intéressantes.

# Le manifeste

Et enfin mon template de manifeste yaml:

# My Kube is a PaaS
---
# Service
apiVersion: v1
kind: Service
metadata:
  name: ${APPLICATION_NAME}
spec:
  selector:
    app: ${APPLICATION_NAME}
  ports:
    - port: ${EXPOSED_PORT}
      targetPort: ${CONTAINER_PORT}
---
# Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ${APPLICATION_NAME}
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ${APPLICATION_NAME}
  template:
    metadata:
      labels:
        app: ${APPLICATION_NAME}
    spec:
      initContainers:
        # 1️⃣ This container clones the git repository to the EmptyDir volume (git-repository-${TAG})
        - name: git-repository-container
          image: alpine/git
          imagePullPolicy: Always
          args:
            - clone
            - -b
            - ${BRANCH}
            - ${GIT_REPOSITORY}
            - /home/app
          volumeMounts:
            - name: git-repository-${TAG}
              mountPath: /home/app
        # 2️⃣ kaniko build
        - name: build
          image: gcr.io/kaniko-project/executor:latest
          args:
            - "--dockerfile=/home/app/Dockerfile"
            - "--context=/home/app/"
            - "--destination=${IMAGE}"
          volumeMounts:
            - name: git-repository-${TAG}
              mountPath: /home/app
            - name: docker-config
              mountPath: /kaniko/.docker  
      containers:
       # 3️⃣ web application container
        - name: ${APPLICATION_NAME}
          image: ${IMAGE}
          ports:
            - containerPort: ${CONTAINER_PORT}
          imagePullPolicy: Always
      volumes:
        - name: git-repository-${TAG}
          emptyDir: {}
        - name: docker-config
          configMap:
            name: docker-config
---
# Ingress
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: ${APPLICATION_NAME}
  annotations:
    kubernetes.io/ingress.class: traefik
spec:
  rules:
    - host: ${HOST}
      http:
        paths:
          - backend:
              serviceName: ${APPLICATION_NAME}
              servicePort: ${EXPOSED_PORT}

Il est un peu long, mais les 3 choses importantes sont:

  • 1️⃣ le 1er initContainer, qui va cloner notre repository dans un volume de type emptyDir
  • 2️⃣ le 2ème initContainer, qui va builder à l'aide de Kaniko l'image Docker et la pousser sur le Docker hub
    • vous remarquez qu'il utilise 2 volumes, le volume de type emptyDir et un volume "mappé" sur la ConfigMap
  • 3️⃣ et enfin le container applicatif construit à partir de l'image générée

# Et maintenant, on déploie

🤔 je dois encore travailler sur cette partie, mais pour le moment j'utilise les commandes suivantes:

export KUBECONFIG=/path_to_your_config_file/k3s.yaml
export SUB_DOMAIN="172.16.245.122.nip.io" # 1️⃣
export DOCKER_USER="k33g" # 2️⃣
COMMIT_MESSAGE="👋 update and 🚀 deploy"

git add .; git commit -m "${COMMIT_MESSAGE}"; git push

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

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

export IMAGE_NAME="${APPLICATION_NAME}-${BRANCH}-img"
export IMAGE="${DOCKER_USER}/${IMAGE_NAME}:${TAG}"

# 4️⃣ calculate the url of my git repository
url=$(git remote get-url origin)
url="${url/://}"
url="${url/git@/https://}"
export GIT_REPOSITORY="${url}"

export HOST="${APPLICATION_NAME}.${BRANCH}.${SUB_DOMAIN}"

# 5️⃣ 🖐️ the namespace must exist
export NAMESPACE="funky-demo-prod"

# === Deploy ===
rm ./kube/*.yaml

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

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

echo "🌍 http://${HOST}" # 6️⃣
  • 1️⃣ 172.16.245.122 est l'ip de la VM qui contient mon cluster et j'utilise le service nip.io pour bénéficier d'un DNS wildcard
  • 2️⃣ k33g est mon handle sur le Docker hub
  • 3️⃣ on est bien sûr dans un repository git
  • 4️⃣ je calcule l'url de mon repository (rien de ne vous empêche de le mettre en dur)
  • 5️⃣ n'oubliais pas de créer le namespace
  • 6️⃣ l'url de votre application va ressembler à: http://${APPLICATION_NAME}.${BRANCH}.172.16.245.122.nip.io

# Remarque à propos du/des namespaces

Pour pouvoir déployer plusieurs versions de mon application je construis mon namespace comme ceci:

export NAMESPACE="funky-${APPLICATION_NAME}-${BRANCH}"

Donc pour créer ma ConfigMap, je fais ceci:

kubectl create configmap docker-config -n ${NAMESPACE} --from-file=./config.json

Et pour créer mon namespace de manière dynamique, uniquement s'il n'existe pas:

if [ -z "${NAMESPACE}" ]
then
  export NAMESPACE="funky-${APPLICATION_NAME}-${BRANCH}"
fi

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

🎉 Et voilà, c'est tout pour cette fois ci, maintenant vos build Docker se font dans votre cluster. J'ai un autre scénario où je le fais faire, toujours avec Kaniko, à un GitLab runner (vous pouvez trouver un exemple ici: https://tanuki-core-tutorials.gitlab.io/docker.registry (opens new window), j'écrirais probablement un article sur le sujet plus tard).

Bonne journée à tous. Bon courage pour le confinement, mettez le à profit pour apprendre si vous le pouvez (ça aide à ne pas tourner en rond). 😗

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

Last Articles