Neuigkeiten von trion.
Immer gut informiert.

Docker Images ohne Dämon bauen mit Kaniko

Kubernetes nutzt Container Images im OCI- oder Docker-Format, um Workload im Cluster zu verteilen und auszuführen. Möchte man - zum Beispiel im Rahmen eines CI-Builds - in einem Kubernetes-Cluster auch Docker-Images bauen, gibt es im wesentlichen drei Ansätze:

  1. Verwendung des außenliegenden Docker-Daemons

  2. Verwendung eines Docker-Daemons in einem Container (Docker-in-Docker)

  3. Verwendung eines alternativen Build Werkzeugs

Den Docker-Daemon, der im Cluster selbst Container bereitstellt, in einem Build-Container zu verwenden bringt vor allem den Nachteil mit sich, dass der Build die Stabilität der Cluster-Node beeinträchtigen kann. Zudem ist nicht gesagt, dass überhaupt ein Docker-Daemon bereitsteht, schließlich könnte der Kubernetes-Cluster auch CRI-O als Runtime verwenden. Diese Option scheidet somit in der Regel aus.

Docker-in-Docker (DinD) ist eine häufig gewählte Option, verlangt jedoch, dass der entsprechende Container priviligiert gestartet wird. Aus Sicherheitsaspekten ist diese Option zwar nicht optimal, lässt sich jedoch in der Praxis gut einsetzen. Das gilt selbst dann, wenn die Container-Engine des Clusters gar kein Docker verwendet.

Als letzte Optionen bleibt noch die Verwendung spezialisierter Werkzeuge zur Erstellung von Container-Images. In diesem Beitrag wurde die Verwendung von Buildah beschrieben, in Spring Boot mit Jib die Verwendung von Jib.

Jib ist speziell auf Java-Anwendungen ausgerichtet, buildah benötigt zum Bauen von Images root-Berechtigungen - beide Werkzeuge bringen damit gewisse Einschränkungen mit.

Von Google kommt das Werkzeug Kaniko, das in diesem Beitrag vorgestellt wird, und speziell für den Einsatz in Kubernetes konzipiert wurde.

Kaniko wurde von Google im Frühjahr 2018 als OpenSource veröffentlicht: https://github.com/GoogleContainerTools/kaniko

Der Ansatz von Kaniko fasst Dockerfiles als API auf und implementiert die in einem Dockerfile gültigen Befehle selbst. So wie Docker für jeden Schritt einen Intermediate-Container starten würde und das Ergebnis als Image-Layer festschreibt verfährt Kaniko im Prinzip auch. Die Befehle werden dabei jedoch ohne einen Docker-Container direkt von Kaniko interpretiert und anschließend das Filesystem als Snapshot verwendet, um die sich daraus ergebenden Unterschiede in einem Image-Layer zu speichern.

Kaniko ist zwar für Kubernetes entwickelt worden, jedoch spricht nichts gegen eine Verwendung in einem normalen Docker-Kontext, z.B. im Rahmen eines Buildservers.

Kaniko mit Docker

Google stellt ein fertiges Container-Image von Kaniko in der Google-Container-Registry bereit. Kaniko kann somit sehr leicht ausprobiert werden. Auch vom Ausprobieren abgesehen ist Kaniko so konzipiert, dass es selbst als Container ausgeführt wird. Es spielt dabei keine Rolle, ob Docker, CRI-O oder eine andere Container-Implementierung verwendet wird.

Ein erster Start von Kaniko in Docker
$ docker run --rm gcr.io/kaniko-project/executor:latest --help
Unable to find image 'gcr.io/kaniko-project/executor:latest' locally
latest: Pulling from kaniko-project/executor
45b582e341f6: Pull complete
12b927e6d59d: Pull complete
fd4f6ba7764d: Pull complete
78d71abc9fd7: Pull complete
df54e1046228: Pull complete
20c7f65d10a9: Pull complete
d9105813dc86: Pull complete
Digest: sha256:bb4e75b74db3c240764857a9ed52a417920e4ebca2ae19a09e9785b57d248c2c
Status: Downloaded newer image for gcr.io/kaniko-project/executor:latest
Usage:
  executor [flags]
...

Um mit Kaniko nun Container-Images zu bauen, gilt es ein paar Punkte zu beachten: Kaniko erwartet den Build-Kontext unter /workspace, falls nicht anders spezifiziert. Durch den Parameter -d wird in Kaniko das Zielrepository für das Docker-Image spezifiziert, wohin auch nach erfolgreichem Build automatisch gepusht wird.

Arbeitet man mit einer lokalen Registry, kann man nun relativ einfach einen Build testen. Zur Demonstration wird sowohl die Registry als auch Kaniko mit --net=host gestartet, damit der Zugriff auf die Registry einfach möglich ist und keine TLS-Zertifikate benötigt werden. Dies ist in der Praxis mit einer produktiv konfigurierten Registry natürlich nicht notwendig.

Start einer lokalen Registry, die unter localhost verfügbar ist
$ docker run --rm --net=host registry:2

Für einen ersten Test wird ein FROM-scratch Image erzeugt, in den ein statisch gelinktes C-Binary kopiert wird. Entsprechend sieht das Dockerfile relativ simpel aus, das zu verwendende Binary wird außerhalb von Docker erzeugt.

Dockerfile für ein simples Image
FROM scratch
COPY hello /hello
CMD ["/hello"]

Als Beispiel kann folgendes simples C Programm dienen, dass lediglich einen Text in die Standardausgabe schreibt.

Quellcode für hello-world in C
#include <stdio.h>

void main() {
    printf("Hello, world - from Kaniko!\n");
}

Durch gcc -static -o hello hello.c kann der Quelltext übersetzt werden. Anschließend wird Kaniko aus dem Verzeichnis heraus aufgerufen, in dem das Binary liegt. Der Ordner stellt damit den Build-Kontext bereit.

Aufruf von Kaniko mit lokaler Registry
$ docker run --rm -v $PWD:/workspace \
  gcr.io/kaniko-project/executor:latest \
  -d localhost:5000/demo/scratch
INFO[0000] No base image, nothing to extract
INFO[0000] Taking snapshot of full filesystem...
INFO[0001] Using files from context: [/workspace/hello]
INFO[0001] COPY hello /hello
INFO[0001] Taking snapshot of files...
INFO[0001] CMD ["/hello"]
2018/11/01 13:51:35 pushed blob sha256:3eaa76c16b058747f21fc7ac14bb18fb682594787cb61c1d173fdf161d3ef466
2018/11/01 13:51:35 pushed blob sha256:cd66848f6eab62af97869cbd1a4cbdcef2a01daf741c023f66e8c3695e6c5593
2018/11/01 13:51:35 localhost:5000/demo/scratch:latest: digest: sha256:3cd19de73f929830c947613bd655a6d35c6401ed6cc86fd928c52cead644beb4 size: 427

Nachdem das Image in die Registry gepusht wurde, kann über die Docker-Registry-API das Ergebnis geprüft werden.

Abfrage der Images in der Registry
$ curl -X GET http://localhost:5000/v2/_catalog
{"repositories":["demo/scratch"]}
curl -X GET http://localhost:5000/v2/demo/scratch/tags/list
{"name":"demo/scratch","tags":["latest"]}

Für den produktiven Einsatz kommt in der Regel eine Registry zum Einsatz, die lediglich nach erfolgreicher Authentifizierung genutzt werden kann. Die Credentials verwaltet Docker in der Datei config.json, von der Kaniko dann auch Gebrauch machen kann. Der Pfad zur Datei wird für Kaniko durch den Environment-Parameter DOCKER_CONFIG spezifiziert, ein passender Volume-Mount macht die Datei im Kaniko-Container verfügbar.

Einsatz in Kubernetes

Kaniko wurde für den Einsatz in Kubernetes konzipiert, daher soll ein entsprechendes Beispiel die Verwendung erläutern. Kubernetes stellt das Konzept von Secrets zur Verfügung, worüber sich die Registry-Credentials im Cluster verfügbar machen lassen. Ein Kubernetes-Secret kann über die Kommandozeile mit kubectl aus einer Datei erzeugt werden, oder alternativ über die Kubernetes-API.

Credentials zum Registryzugriff als Kubernetes-Secret
$  kubectl create secret generic docker-config --from-file $HOME/.docker/config.json

Das Pendant zu docker run in Kubernetes sind einzelne Pods oder ein Job Objekt. Der Unterschied zwischen einem Pod und einem Job besteht darin, dass im Falle eines Fehlschlages, z.B. durch einen Hardware-Ausfall, ein Job automatisch einen neuen Pod erzeugt, und es nocheinmal versucht.

Um nach erfolgreichem Build keinen neuen Pod zu erzeugen, wird die restartPolicy: Never verwendet. Ein Kaniko-Kubernetes-Job könnte daher wie folgt aussehen:

Kubernetes Job um mit Kaniko ein Image zu bauen
apiVersion: batch/v1
kind: Job
metadata:
 name: kaniko
spec:
 template:
  spec:
   restartPolicy: Never
   containers:
   - name: kaniko
     image: gcr.io/kaniko-project/executor:latest
     args: ["--destination=<registry/$PROJECT/$REPO:$TAG"]
     volumeMounts:
       - name: docker-config
         mountPath: /kaniko/secrets
     env:
       - name: DOCKER_CONFIG
         value: /kaniko/secrets
   volumes:
   - name: docker-config
     secret:
       secretName: docker-config
    - name: build-context
      emptyDir: {}

Natürlich benötigt der laufende Kaniko-Container auch Zugriff auf die in dem Image zu verpackenden Inhalte. Dies kann durch ein Shared-Volume zusammen mit einem initContainer erfolgen: InitContainer werden von Kubernetes ausgeführt, bevor die eigentliche Workload gestartet wird. Als Operation in dem Container kann z.B. ein git clone oder ein HTTP-Download ausgeführt werden.

Verwendung von Init-Container zur Artefakterzeugung
initContainers:
- name: clone
  image: alpine
  command: ["/bin/sh","-c"]
  args: ['apk add --no-cache git && git clone https://github.com/trion-development/spring-boot-rest-sample.git /workspace/']
  volumeMounts:
  - name: build-context
    mountPath: /workspace
volumes:
- name: build-context
  emptyDir: {}

Fazit

Mit Kaniko ist ein vielversprechendes Werkzeug entstanden, das auf das etablierte Format eines Dockerfile zurückgreift. Um Images zu bauen ist kein Zugriff auf einen Docker-Daemon erforderlich, was die Sicherheit und auch Robustheit deutlich erhöht. Zusätzlich kann eine Containter-Runtime wie gvisor oder eine Virtualisierung zum Einsatz kommen, wenn erhöhte Sicherheitsanforderungen einzuhalten sind.

Natürlich lässt sich Kaniko auch mit einem Buildserver kombinieren, z.B. GitLab CI oder Jenkins, und ist nicht auf die oben beschriebenen Szenarien beschränkt.




Zu den Themen Kubernetes, Docker und Cloud Architektur bieten wir sowohl Beratung, Entwicklungsunterstützung als auch passende Schulungen an:

Auch für Ihren individuellen Bedarf können wir Workshops und Schulungen anbieten. Sprechen Sie uns gerne an.

Feedback oder Fragen zu einem Artikel - per Twitter @triondevelop oder E-Mail freuen wir uns auf eine Kontaktaufnahme!

Zur Desktop Version des Artikels