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:
-
Verwendung des außenliegenden Docker-Daemons
-
Verwendung eines Docker-Daemons in einem Container (Docker-in-Docker)
-
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.
$ 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.
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.
FROM scratch
COPY hello /hello
CMD ["/hello"]
Als Beispiel kann folgendes simples C Programm dienen, dass lediglich einen Text in die Standardausgabe schreibt.
#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.
$ 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.
$ 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.
$ 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:
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.
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.