Docker Container Loading - Teil 4

10 kreative Wege, Docker Images zu bauen: Packer

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.

Im letzten Artikel haben wir gesehen, wie wir mit Ansible ohne Dockerfile Images erstellen können. Nach einem ganz ähnlichem Ansatz können Images auch mit Packer erstellt werden.

Packer

Packer ist ein Open-Source-Werkzeug von Hashicorp, mit dem machine images für die verschiedensten Plattformen wie Amazon EC2, DigitalOcean, Google Compute Engine, Virtual Box, VMWare oder eben auch Docker erstellt werden können. Viele kennen Packer als den Mechanismus zur Erstellung sogenannter Boxes als Grundlage für Vagrant-Konfigurationen.

Mit einer einzigen Konfigurationsdatei können verschiedene, gleichartige Images erzeugt werden. Dabei ersetzt Packer kein Konfigurationsmanagementsystem (CMS) wie Puppet oder Ansible. Im Gegenteil, CM-Systeme werden genutzt, um die machine images reproduzierbar zu bestücken, bevor das Image finalisiert wird.

Packer ist ein in Go geschriebenes Kommandozeilen-Tool, das als einzelnes Binary installiert und für verschiedene Plattformen bereitgestellt wird. Die zu erstelleneden Images werden dann über Templates beschrieben.

Innerhalb der Templates nutzt Packer drei wichtige Abstraktionen:

  • Builders dienen zum eigentlichen Erstellen der Images im jeweiligen Zielformat. In einem Template können mehrere Builder angegeben werden, die jeweils individuell konfiguriert werden können. Für jedes der konfigurierten Zielsysteme wird parallel ein Image gebaut.
  • Provisioners werden genutzt, um die Images zu bestücken. In diesem Schritt können Dateien kopiert und Shell-Skripte ausgeführt werden. Oder aber es werden vollwertige CM-Systeme wie Puppet, Chef oder Ansible aufgerufen, um Anwendungen in dem Image zu installieren und zu konfigurieren.
  • Post Processors werden aufgerufen, nachdem die finalen Images erzeugt worden sind. Damit kann beispielsweise ein gerade gebautes Docker Image getagt oder zu einer Registry gepusht werden.

Insbesondere zur Provisionierung von klassischen Virtuellen Maschinen ist Packer sehr gut geeignet, da es hierfür ja oft keinen intrinsischen Build-Mechanismus gibt. Im Gegensatz dazu bietet Docker ja mit dem Dockerfile bereits einen nativen Weg, Docker Images zu erstellen. Dennoch kann Packer eine interessante Alternative auch zum Bauen von Docker Images sein.

Anhand eines Beispiel wollen wir uns genauer anschauen, wie das geht.

DevOpsCon Whitepaper 2018

Free: BRAND NEW DevOps Whitepaper 2018

Learn about Containers,Continuous Delivery, DevOps Culture, Cloud Platforms & Security with articles by experts like Michiel Rook, Christoph Engelbert, Scott Sanders and many more.

Builder

Analog zu unserem Ansible-Container-Beispiel wollen wir mit Packer ein Docker Image erzeugen, das mit Nginx eine statische Webseite ausliefert. Dazu müssen wir ein Template erstellen, was letztendlich nichts anderes als ein JSON-Dokument ist. Als Builder nehmen wir natürlich einen Docker Builder, den wir so konfigurieren können:

{
  "builders": [
    {
      "type": "docker",
      "image": "centos:7",
      "commit": true,
      "changes": [
        "EXPOSE 80",
        "CMD [ \"nginx\", \"-g\", \"daemon off;\" ]"
      ]
    }
  ]
}

Jeder Builder hat einen spezifischen type, hier ist es docker. Die weiteren Parameter sind Builder-spezifisch und werden per Builder in der Packer-Dokumentation erklärt. Für den Docker Builder brauchen wir ein Basis-Image (image) und wir spezifieren hier, dass der Container am Ende committet wird, sodass daraus wieder ein Image entsteht. Alternativ kann auch "export_path": "image.tar" spezifiert werden. Damit wird der Container am Ende der Provisionierung als Tar-Archiv exportiert.

Mit changes können Metadaten für einen Commit angepasst werden. Falls das image nur exportiert werden soll (via export_path), dann wird changes jedoch ignoriert. Das ist eine gute Möglichkeit, mit CMD das Startkommando innerhalb des Images zu definieren. Dieses wird dann bei einem docker run genutzt.

Mit dieser Konfiguration startet der Docker Builder zunächst einen Container mit dem Basis-Image centos:7 als Vorlage. Danach läuft die Provisionierung (siehe nächster Abschnitt) bevor dann der Container wieder zu einem Image committet wird. Dieses Image hat zunächst keinen Namen, dieser kann aber mit einem Post Prozessor, wie später erklärt, ebenfalls gesetzt werden.

Provisionierer

Zur Bestückung stehen verschiedene Provisionierer zur Verfügung. Die einfachsten Provisionierer sind file und shell, die für jedes Docker Basis-Image direkt genutzt werden können. Mit file werden Dateien in den Container kopiert. Packer nutzt dazu einen Volume Mount beim Start des Containers und kopiert die spezifierten Dateien in den Container. shell spezifiert entweder ein lokales Shell-Skript oder auch direkt Shell-Kommandos (mit inline). Diese werden ebenfalls über das Volume zu Beginn in den Container kopiert, bevor sie dort ausgeführt werden.

In unserem Beispiel könnte die Provisionierung wie folgt aussehen:

{
  ...
  "provisioners": [
    {
      "type": "shell",
      "inline": [
        "yum -y install epel-release",
        "yum -y install nginx",
        "echo '<html><body><h1>Hello Packer !</h1></body></html>' > /usr/share/nginx/html/index.html"
      ]
    }
  ],
}

In diesem Beispiel wird eine Reihe von Shell-Kommandos innerhalb des Containers ausgeführt, um nginx zu installieren und ein index.html zu erzeugen.

Das index.html können wir aber auch mit einem file-Provisionierer von der lokalen Festplatte kopieren. Dieser Schritt wird einfach an den shell-Provisionierer angehängt und im Anschluss ausgeführt.

{
  ...
  "provisioners": [
    {
      "type": "shell",
      "inline": [
        "yum -y install epel-release",
        "yum -y install nginx",
      ]
    },
    {
      "type": "file",
      "source": "index.html",
      "destination": "/usr/share/nginx/html/index.html"
    }
  ],
}

Zugebenermaßen wäre alles, was wir bisher gesehen haben, wesentlich einfacher mit einem Dockerfile zu realisieren. Die Stärke von Packer jedoch sind die weiteren von Packer angebotenen Provisionierer:

  • Ansible Local und Ansible Remote dienen zur Provisionierung via Ansible, ganz ähnlich, wie wir das im letzten Artikel gesehen haben. Bei ansible-local wird ein Ansible Playbook direkt im Container ausgeführt. Dazu muss Ansible entweder schon im Basis-Image installiert sein, oder aber es wird mit dem shell-Provisionierer vor ansible-local installiert (und eventuell nach der Provisionierung wieder deinstalliert). Beim ansible-Provisionierer dagegen wird ansible-playbook auf dem lokalen Rechner ausgeführt, der via ssh mit dem Container kommuniziert. Das bedeutet hier muss im Container während der Provisionierung ein sshd-Daemon laufen.
  • Chef Client und Chef Solo wird für Chef-basierte Installationen genutzt. chef-client wird verwendet, um aus dem Container heraus von einem entfernten Chef-Server zu installieren. chef-solo dagegen benutzt Chefs local mode, der ohne Server auskommt und Cookbooks lokal ausführt. Falls die Chef-Client-Software noch nicht installiert ist, wird diese automatische von Packer nachinstalliert.
  • Puppet Masterless und Puppet Server erlauben die Installation mithilfe von Puppet. puppet-masterless kommt dabei ohne einen zentralen Master-Server aus, während puppet-server sich mit einem Puppet-Server verbindet. Für beide Modi muss Puppet auf dem Container installiert sein, was wie bei den Ansible-Provisionierern mit einem vorgelagerten shell-Schritt durchgeführt werden kann.
  • Salt Masterless ist gedacht für die Installation von Salt States und funktioniert ebenfalls ohne zentralen Salt-Server.

Diese Vielfalt an Provisionierern ist insbesondere für alle interessant, die bereits Zeit und Energie in die Erstellung ausgefeilter CM-Konfigurationen gesteckt haben und diese ganz oder auch teilweise zum Erstellen von Docker Images wiederverwenden möchten.

Jedoch werden immer gewisse Voraussetzungen an den Zielcontainer gestellt:

  • Für eine remote-Provisionierung muss auf dem Zielcontainer zwingend ein sshd-Daemon laufen. Da normalerweise sshd für Docker Container ein allgemein akzeptiertes Antipattern ist, müsste man den sshd nur temporär installieren und starten, sodass vor dem finalen docker commit sshd gestoppt und entfernt wird. Abgesehen von der erhöhten Komplexität geht das natürlich auch zu Lasten der Build-Laufzeit.
  • Ähnlich gelagert ist der Fall für einen lokalen Aufruf des CM-Systems innerhalb des Containers. Auch hier muß extra Software nachinstalliert und idealerweise vor dem Commit auch wieder entfernt werden. Es ist dann nicht SSH, sondern Ansible, Chef, Puppet oder Salt. Der Einfluss auf die Buildzeiten ist ebenfalls negativ.
  • Die einzigen Provisionierer, die ohne Mehraufwand mit Docker genutzt werden können, sind der shell– und file-Provisionierer. Wenn man jedoch ausschließlich diese benutzen möchte, dann ist Packer sicherlich zu viel des Guten, da die gleiche Funktionalität einfacher und schneller mit einem Dockerfile bereitgestellt werden kann.

Post Prozessoren

Nach der Provisierung (und beim Docker Builder nach dem Image commit) bietet Packer noch eine Nachbearbeitung mit sogenannten post-processors an. Für Docker im Speziellen sind folgende Post Prozessoren interessant:

  • Docker Import zum Importieren eines Images, das mit export_path deklariert wurde. Hier kann man das Docker Repository und einen Tag angeben, mit dem das Docker Image importiert werden soll.
  • Docker Tag wird genutzt, um einem mit commit: true gebauten Image einen Namen zu geben.
  • Docker Push kann ein Image zu einer Registry pushen. Dieser Schritt kommt entweder nach einem docker-import (export build) oder einem docker-tag (commit build).

In unserem Beispiel können wir unserem gebauten Image via Post Prozessor einen Namen geben und zu einer Registry pushen:

{
  ...
  "post-processors": [
    {
      "type": "docker-tag",
      "repository": "demo/web-server",
      "tag": "1.0"
    },
    {
      "type": "docker-push"
    }
  ]
}

Fazit

Welche Vorteile bietet nun die Image-Erzeugung via Packer ?

Der grösste Vorteil von Packer ist sicherlich die Unterstützung aller gängigen CM-Systeme, die es erlauben, bereits erstellte CM-Konfigurationen wiederzuverwenden. Zwar ist das Setup etwas aufwendiger, da beispielsweise ein ssh-Daemon zu Beginn der Provisionierung gestartet und am Ende vor dem Commit auch wieder gestoppt werden muss. Jedoch können bereits erstellte CM-Konfigurationen sehr leicht wiederverwendet werden.

Wer Ansible als CM-System nutzt, dem sei nochmal Ansible Container vom letzten Artikel nahegelegt. Hier werden Docker-native Mechanismen mit einem speziellen Ansible-Konnektor genutzt, um Playbooks auf dem Zielcontainer einzuspielen, sodass hier keine zusätzlichen Schritte notwendig sind.

Lesen Sie auch: 10 kreative Wege, Docker Images zu bauen: Dockerfile & Docker Commit

Ein Kernfeature von Packer ist es, mit einer einzigen Konfigurationsdatei viele verschiedene machine images zu erzeugen. Diese machine images sind (bis auf Docker) alles „richtige“ Virtuelle Maschinen, die andere Anforderungen haben. So ist es durchaus üblich, in einer VM mehrere Prozesse gleichzeitig zu starten, während bei Containern das Pid 1 Pattern dominiert (d.h. dass bei Start eines Containers auch nur ein Prozess gestartet wird). Das wiederum erfordert, insbesondere bei der Provisionierung via SSH-Kommunikation, bei Containern eine andere Vorgehensweise als bei VMs, bei denen typischerweise von Haus aus bereits ein sshd im Basis-Image gestartet wird. Daher muss man, wenn man Docker Images zusammen mit anderen VMs bauen will, viele Fallunterscheidungen in das Template einbauen, sodass es wahrscheinlich Sinn macht, getrennte Templates zu nutzen.

Wer dagegen nur den shell– oder file-Provisionierer nutzt, für den hat Packer gegenüber normalen Dockerfiles keinen wirklichen Vorteil.

Das soweit zu Packer. Im nächsten Artikel dieser Serie werden wir uns dann ausführlich damit beschäftigen, wie man Java-Anwendungen am einfachsten in Container packen und in einen Maven Build-Prozess integrieren kann.

Mehr zum Thema:

10 kreative Wege, Docker Images zu bauen: Dockerfile Template

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: