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 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 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, 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

Last Updated: 15/04/2020 à 06:38:53