Introduction

Dans l'article précédent, je vous ai présenté K3S et comment le mettre en place facilement. Vous aurez noté que cet outil n'utilise pas Docker par défaut, mais alors ! Comment faire pour construire des images dans le cas d'un contexte d'intégration continue ?

Un peu de rappel...

K3S est un cluster Kubernetes allégé qui utilise RunC et Containerd. Le premier ayant pour but d'exécuter des conteneurs et le second est un peu plus haut niveau : il va être capable de s'occuper de puller des images, de les stocker, etc. Une troisième couche est généralement utilisée pour s'occuper de la partie build et c'est Docker qui s'en occupe dans la plupart des cas. Néanmoins, dans notre situation, nous ne possédons pas cette dernière couche.

Le but de cet article est de vous montrer qu'il est tout à fait possible de construire une image sans Docker en utilisant, comme nous le verrons par la suite, un outil qui s'appelle Kaniko.

D'autres outils existent comme BuildKit ou img, mais d'un point de vue personnel, Kaniko est celui qui fournit le plus d'options avec une configuration et mise en place très rapide.

Les dangers du Docker in Docker

L'une des façons que l'on retrouve le plus souvent pour construire une image est l'utilisation du Docker in Docker qui permet d'exécuter une image conteneurisée contenant Docker pour en créer une autre.

Cette approche n'est pas forcément recommandée dans des contextes où la sécurité est cruciale car elle nécessite de modifier la configuration initiale d'un cluster Kubernetes en permettant à des conteneurs de s'exécuter avec des privilèges élevés.

De plus, certains mécanismes de sécurité peuvent s'appliquer partiellement ou pas du tout, c'est le cas de la couche SELinux ou des Linux Security Modules qui ne peuvent pas voir un conteneur qui se lance à l'intérieur d'un autre et donc, de ce fait, ces outils ne sont pas en mesure de faire respecter les règles de sécurité définies au sein du système d'exploitation.

Et si on utilisait Kaniko ?

Kaniko est outil qui va nous permettre de construire des images sans aucune dépendance au daemon Docker. Comme nous l'avons vu au-dessus, cela permet de garder un environnement sécurisé pour notre build sans avoir à modifier des privilèges du cluster lors de l'exécution de conteneurs.

Il utilise le fichier Dockerfile pour construire les images, ce qui permet de ne pas avoir à modifier notre dépôt de code que l'on souhaite builder avec Docker ou Kaniko.

De plus, Kaniko est très facile à utiliser : il suffit de se baser sur cette image gcr.io/kaniko-project/executor:v1.6.0 tout en spécifiant certains paramètres (le tag de l'image était à la version 1.6.0 lors de la rédaction de l'article).

Kaniko gère plusieurs contextes de build, il peut très bien récupérer des fichiers nécessaires à la construction de l'image sur Git, S3, le système de fichiers, etc.

Enfin, il est capable de pousser les images sur un certain nombre de registres, par exemple : Docker Hub, GCR ou encore ECR.

Démonstration

La démonstration qui va suivre se base sur un cluster K3S qui a été installé avec la même méthode que l'article "À la découverte de K3S", le but sera de créer une image à partir d'un Dockerfile qui englobe une application web assez minimaliste en NodeJS.

Afin d'aller à l'essentiel, nous utiliserons l'objet configmap au sein de Kubernetes pour monter en volume les fichiers nécessaires au build de l'image :

  • Le Dockerfile permettant de lister les instructions afin de créer notre image ;
  • Le package.json contenant les dépendances et informations de notre application ;
  • Le server.js qui représente notre application web.

Voici son contenu :

cat <<EOF | kubectl create -f -
apiVersion: v1
kind: ConfigMap
metadata:
  name: build-tools
data:
  Dockerfile: |
    FROM node:16-alpine3.12

    # Create app directory
    WORKDIR /usr/src/app

    # Install app dependencies
    COPY package*.json ./

    RUN npm install

    # Copy source code
    COPY . .

    EXPOSE 8080
    CMD [ "node", "server.js" ]

  package.json: |
    {
      "name": "my_nodejs_app",
      "version": "1.0.0",
      "description": "Node.js App",
      "author": "Romain Boulanger",
      "main": "server.js",
      "scripts": {
        "start": "node server.js"
      },
      "dependencies": {
        "express": "^4.17.1"
      }
    }

  server.js: |
    'use strict';

    const express = require('express');

    // Define host and port
    const PORT = 8080;
    const HOST = '0.0.0.0';

    // App
    const app = express();
    app.get('/', (req, res) => {
      res.send('Hello Filador Blog !');
    });

    app.listen(PORT, HOST);
EOF

Pour lancer notre build, il est recommandé d'utiliser l'objet Job de Kubernetes pour avoir un statut après l'exécution de celui-ci.

Dans ce job, on vient monter nos trois fichiers précédents dans le chemin /tmp/build qui sera notre contexte pour créer notre image en utilisant le paramètre --context=dir:///tmp/build, on indique aussi la place du Dockerfile --dockerfile=/tmp/build/Dockerfile. Enfin, dans le cas ci-dessous, on ne souhaite pas pousser notre image vers un registre en utilisant l'option --no-push.

cat <<EOF | kubectl create -f -
apiVersion: batch/v1
kind: Job
metadata:
  creationTimestamp: null
  name: kaniko-build
spec:
  template:
    metadata:
      creationTimestamp: null
    spec:
      containers:
      - image: gcr.io/kaniko-project/executor:v1.6.0
        args:
        - "--dockerfile=/tmp/build/Dockerfile"
        - "--context=dir:///tmp/build"
        - "--no-push"
        name: kaniko-build
        resources: {}
        volumeMounts:
        - name: build-tools-volume
          mountPath: /tmp/build/Dockerfile
          subPath: Dockerfile
        - name: build-tools-volume
          mountPath: /tmp/build/package.json
          subPath: package.json
        - name: build-tools-volume
          mountPath: /tmp/build/server.js
          subPath: server.js
      volumes:
        - name: build-tools-volume
          configMap:
            name: build-tools
      restartPolicy: Never
status: {}
EOF

Dès le job est créé au sein du cluster, on peut voir qu'il s'exécute avec les commandes ci-dessous :

❯ kubectl get job
NAME           COMPLETIONS   DURATION   AGE
kaniko-build   0/1           4s         4s

❯ kubectl get po
NAME                 READY   STATUS    RESTARTS   AGE
kaniko-build-rr4cm   1/1     Running   0          14s

Et après quelques secondes d'attente :

❯ kubectl get job
NAME           COMPLETIONS   DURATION   AGE
kaniko-build   1/1           16s        105s

Comme vous pouvez voir, la log détaillée de Kaniko permet de montrer l'exécution de toutes les étapes du Dockerfile.

❯ kubectl logs kaniko-build-rr4cm
INFO[0000] Retrieving image manifest node:16-alpine3.12
INFO[0000] Retrieving image node:16-alpine3.12 from registry index.docker.io
INFO[0004] Built cross stage deps: map[]
INFO[0004] Retrieving image manifest node:16-alpine3.12
INFO[0004] Returning cached image manifest
INFO[0004] Executing 0 build triggers
INFO[0004] Unpacking rootfs as cmd COPY package*.json ./ requires it.
INFO[0007] WORKDIR /usr/src/app
INFO[0007] cmd: workdir
INFO[0007] Changed working directory to /usr/src/app
INFO[0007] Creating directory /usr/src/app
INFO[0007] Taking snapshot of files...
INFO[0007] Resolving srcs [package*.json]...
INFO[0007] COPY package*.json ./
INFO[0007] Resolving srcs [package*.json]...
INFO[0007] Taking snapshot of files...
INFO[0007] RUN npm install
INFO[0007] Taking snapshot of full filesystem...
INFO[0007] cmd: /bin/sh
INFO[0007] args: [-c npm install]
INFO[0007] Running: [/bin/sh -c npm install]

added 50 packages, and audited 51 packages in 3s

found 0 vulnerabilities
npm notice
npm notice New minor version of npm available! 7.15.1 -> 7.18.1
npm notice Changelog: <https://github.com/npm/cli/releases/tag/v7.18.1>
npm notice Run `npm install -g npm@7.18.1` to update!
npm notice
INFO[0010] Taking snapshot of full filesystem...
INFO[0010] COPY . .
INFO[0010] Taking snapshot of files...
INFO[0010] EXPOSE 8080
INFO[0010] cmd: EXPOSE
INFO[0010] Adding exposed port: 8080/tcp
INFO[0010] CMD [ "node", "server.js" ]
INFO[0010] Skipping push to container registry due to --no-push flag

Tout s'est bien déroulé, l'image est à présent buildée !

Conclusion

Kaniko est un outil très facile à prendre en main et il permet de construire des images conteneurisées sans altérer la sécurité de votre cluster Kubernetes.

Je le recommande fortement de par sa multitude d'options que ce soit pour construire ou pousser votre image.