Docker Container Loading - Teil 2

10 kreative Wege, Docker Images zu bauen: Dockerfile Template

Roland Huß

© Shutterstock.com / Good_24

Ist der klassische Weg, Docker Images über docker build zu bauen, wirklich in jedem Fall die beste Art und Weise? In seiner Artikelserie geht Roland Huß, Software Engineer bei Red Hat, dieser Frage auf den Grund und stellt zehn alternative und kreative Wege vor, Docker Images zu erstellen.

In der ersten Folge haben wir gesehen, wie standardmäßig Docker Images gebaut werden. Der wichtigste Weg führt über ein Dockerfile, das Schritt für Schritt (oder Layer für Layer) beschreibt, wie das Image erstellt werden soll.

Ein Problem bei der Verwendung von Dockerfiles ist jedoch, dass Vererbungen zwischen Images die einzige wirkliche Möglichkeit sind, Docker Images für andere Images wiederzuverwenden. Es existiert zwar mit der Parameterisierung über build-args eine einfache Möglichkeit zur Variation, jedoch ist diese limitiert. Beispielsweise erlaubte diese Parameterisierung lange keine dynamische Ersetzung innerhalb der FROM:-Direktive. Diese Funktion ist zwar jetzt im Zuge der Multi-Stage-Builds dazugekommen, aber es können immer noch noch nicht externe Dateien eingebunden werden, um Dockerfile Snippets zu teilen. Insbesondere für Basis-Images macht es aber aus Wartbarkeitsgründen Sinn, möglichst einfach verschiedene Variationen eines Docker Images auf Basis eines einzelnen Dockerfile Templates zu erstellen.

Daher sind im Laufe der Zeit verschiedene Templating Tools entstanden, u.a. :

  • fish-pepper ist ein multidimensionales Template-System speziell für Dockerfiles. Mehr dazu gleich.
  • App::Dockerfile::Template ist eine Perl Library, die die Parameterisierung von Dockerfiles ermöglicht.
  • cekit bietet eine eigene YAML-Definition zum Bauen von Images und Unterstützung für Integrations bzw. Unit-Test von Docker Images an. Auch hier steht am Ende ein generiertes Dockerfile. cekit wird beispielsweise bei Red Hat verwendet, um die Basis- und Middlewareimages für OpenShift zu bauen.

All diese Systeme haben gemeinsam, dass aus einer Template- und Konfigurationsdatei ein Dockerfile erstellt wird. Diese Dockerfiles können in einem SCM with Git versioniert werden und somit beispielsweise auch als automated builds mit Docker Hub genutzt werden. Dockerfile Templates empfehlen sich ganz besonders dann, wenn viele ähnliche Docker Images gebaut werden sollen, die sich nur in Details voneinander unterscheiden.

DevOpsCon Whitepaper 2018

Free: 40+ pages of DevOps expert knowledge

Learn about Containers,Continuous Delivery, DevOps Culture, Cloud Platforms & Security with articles by experts like Kai Tödter (Siemens), Nicki Watt (OpenCredo), Tobias Gesellchen (Europace AG) and many more.

fish-pepper

Als Beispiel wollen wir uns nun fish-pepper etwas genauer anschauen. fish-pepper ist ein Tool, das die Verarbeitung multidimensionaler Parameter unterstützt. Das ist beispielsweise für die Erzeugung von Java-Basis-Images nützlich. Angenommen wir wollen Java-Basis-Images bauen, die auf CentOS, Alpine und RHEL basieren, sowohl jeweils Java 7, 8 und 9 unterstützen und die es in den Ausprägungen JDK und JRE geben soll. Kurz ausgerechnet ergeben sich [(centos, alpine, rhel) * (7,8,9) * (JDK, JRE)] 18 verschiedene Image-Variationen. Naiv kann man jetzt natürlich 18 verschiedene, sich leicht unterscheidende Dockerfiles anlegen. Zwar kann man die Komplexität durch Docker build-args sicherlich etwas reduzieren, dennoch ist die Pflege dieser Dockerfiles sehr aufwändig, da bei der Änderung eines gemeinsamen Aspekts (wie z.B. der Modifikation eines LABELS) mehrere Dockerfiles aktualisiert werden müssen. Solche Copy-Paste-Änderungen sind natürlich fehleranfällig und jeder weiß, wie schnell diese Daten auseinanderlaufen können.

Hier hilft fish-pepper: In einer Konfigurationsdatei images.yml werden zunächst die möglichen Parameternamen definiert. Die möglichen Parameterwerte werden in dieser Datei ebenso festgehalten wie die letztendlichen Template Parameter.

Ein Beispiel:

fish-pepper:
  params:
  - "base"
  - "version"
  - "type"
  name: "jolokia/fish-pepper-java"
  maintainer: "Roland Huss <roland@jolokia.org>"
  config:
    base:
      alpine:
        from: "alpine:3.4"
      centos:
        from: "centos"
      jboss:
        from: "jboss"
    version:
      openjdk7:
        java: "java:7u79"
      openjdk8:
        java: "java:8u45"
    type:
      jre:
        extension: "jre"
      jdk:
        extension: "jdk"

Über die Liste params: sind die möglichen Parameternamen definiert, die sich später im Abschnitt config: wiederfinden.

Wenn fish-pepper in Aktion tritt, dann wird es in einer dreifach verschachtelten Schleife über alle Parameter und deren möglichen Werte iterieren und die Templates mit den jeweiligen Parameterwerten befüllen.

In diesem Fall sind die Parameternamen base, version und type. config: enthält dann genau diese drei Einträge, die wiederum eine Map mit den möglichen Werten beinhalten. fish-pepper iteriert in verschachtelten Schleifen über alle Kombinationen dieser Parameterwerte. In diesem Beispiel wird der Parameter base die Werte alpine, centos und jboss durchlaufen. Unterhalb dieser Keys befinden sich dann die eigentlichen Daten wie sie dann in den Templates benutzt werden. Diese haben alle den gleichen Key (z.B. from) aber unterschiedliche Werte.

So ein Template kann beispielsweise wie folgt aussehen:

# Base image from parameter value
FROM {{= fp.config.base.from }}

MAINTAINER {{= fp.maintainer }}
ENV JAVA_VERSION={{= fp.config.version.java }} \
    JAVA_TYPE={{= fp.param.type }} \

# Include block dynamically
{{= fp.block("java-pkg-" + fp.param.base) || ''}}

....

In der ersten Zeile sieht man, wie das Basis-Image bestimmt wird. Je nach Wert des Parameters base wird der from-Wert des entsprechenden Konfigurationsabschnitts gewählt. Wenn also in der aktuellen Iteration base den Wert alpine hat, dann wird als from der Wert von config:base:alpine:from in der Konfiguration, also alpine:3.4, eingesetzt, sodass im Dockerfile dann an dieser Stelle FROM alpine:3.4 steht. Analoges gilt hier auch für die ENV-Direktive, die jeweils die aktuelle Version und Type per Iteration einfügt. Es können aber auch globale Parameter wie fp.maintainer definiert und verwendet werden. Diese sind für alle Durchläufe gleich.

Interessant ist auch die fp.block()-Anweisung mit der dynamisch sog. Blocks (oder Fragmente) eingebunden werden können. In diesem Beispiel wird je nach Basis-Image ein anderer Block eingelesen, z.B. java-pkg-alpine. Diese Blöcke können etnweder lokal in einem eigenem Verzeichnis definiert sein, oder aber auch extern von einem Git Repository geladen werden. Hier liegt die Datei java-pkg-alpine.dck in einem Verzeichnis blocks und hat folgende Struktur:

{{ var major = fp.config.version.major; }}
# /dev/urandom is used as random source, which is perfectly safe
# according to http://www.2uo.de/myths-about-urandom/
RUN apk add --update \
    curl \
    {{= fp.param.type == 'jre' ? 'openjdk' + major + '-jre-base' : 'openjdk' + major }}={{= fp.config.base.javaPackage[major]}} \
 && rm /var/cache/apk/* \
 && echo "securerandom.source=file:/dev/urandom" >> /usr/lib/jvm/default-jvm/jre/lib/security/java.security

Wie man sieht, verwendet dieses Fragment die gleiche Template-Syntax und kann auch Parameter analog auswerten. Dieses Fragment ist sehr spezifisch für Alpine Linux und dessen Paketverwaltung. Der entsprechende Block für CentOS sieht natürlich anders aus. So lassen sich die Dockerfiles sehr flexibel komponieren. Diese Blöcke können nun auch an anderer Stelle wiederverwendet und wie gesagt extern bereitgestellt werden.

Wenn nun fish-pepper mit dieser Konfiguration und diesem Template gefüttert wird, dann werden die entsprechenden Dockerfiles in einer dreifach verschachtelten Verzeichnisstruktur angelegt (fan out):

Hier sieht man auch schön, dass fish-pepper nicht nur Dockerfile Templates befüllen kann, sondern auch beliebige andere Dateien wie Support-Dateien oder READMEs:

Damit sind wir auch schon am Ende dieser kurzen Vorstellung. fish-pepper kann darüber noch einiges mehr, z.B. auch das eigentlich Bauen der Docker Images oder auch das Hochladen zu einer Docker Registry. Wer sich für die Details interessiert oder auch wissen möchte, woher der Name kommt, auf GitHub fabric8io-images/fish-pepper finden sich alle Antworten. In Aktion findet man fish-pepper bei den Fabric8-Basis-Images für Java und den OpenShift Java S2I-Basis-Images.

Zusammenfassung und Ausblick

Wir haben gesehen, dass für spezielle Anwendungsfälle wie das Erstellen einer Familie von Basis-Images, reine Dockerfiles schnell an ihre Grenzen stoßen. Mit Dockerfile Template Engines lassen sich die Limitierungen jedoch leicht umgehen, ohne dass man die Vorteile des eigentlichen Image-Bauens mit Dockerfiles aufgeben muss.

In der nächsten Folge werden wir dann sehen, wie Docker Images ohne Dockerfiles (und dennoch 100% reproduzierbar) mit Ansible erzeugt werden können.

Verwandte Themen:

Geschrieben von
Roland Huß
Roland Huß
Roland Huß ist ein Software Engineer, der für Red Hat an fabric8, einer Microservices-Plattform für Kubernetes und OpenShift arbeitet. Er entwickelt seit fast 20 Jahren, hat aber niemals seine Wurzeln als Systemadministrator vergessen. Roland arbeitete aktiv in Open-Source-Projekten mit und betreut sowohl Jolokia, die JMX-HTTP Bridge als auch das populäre Docker-Maven-Plug-in fabric8io. Und er liebt Chilis.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: