10 kreative Wege, Docker Images zu bauen: Ansible Container

© 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 vorherigen Artikel haben wir gelernt, wie man mithilfe von Templates einige der Einschränkungen von Dockerfiles umschiffen kann. Heute werden wir auf Dockerfiles komplett verzichten und uns auf die Erstellung von Docker Images aus Containern mit docker commit
konzentrieren. „Aber das ist doch Blödsinn, solche Images sind ja nicht reproduzierbar“, mag man sich da denken. Nun ja, das stimmt schon. Aber nur, wenn der Container von Hand „bestückt“ wurde. Was wäre denn, wenn wir einen laufenden Container mit einem Configuration-Management-System (CMS) wie Chef, Puppet or Ansible genauso konfigurieren würden wie „bare metal“? Genau: es wäre mindestens genauso reproduzierbar wie ein Dockerfile. Am Ende der Provisionierung machen wir dann einfach ein docker commit
und haben unser Docker Image, das wir jederzeit wieder exakt gleich via CMS erstellen können. Genau diesen Ansatz verfolgt Ansible Container.
Das Prinzip ist wie gerade beschrieben:
- Zu Beginn wird ein sogenannter Conductor als Container gestartet, in dem Python und Ansible installiert sind.
- Danach werden ein (oder auch mehrere) Basis-Container gestartet, die in einer zentralen,
docker compose
ähnlichen Datei definiert sind. Diese enthält auch die Ansible Roles, die auf die Container angewendet werden sollen. - Dann wird vom Conductor aus ein Ansible Playbook ausgeführt, das dynamisch aus den konfigurierten Rollen erstellt wird. Dieses verbindet sich mit den laufenden Containern und führt dort die Ansible Tasks aus, die innerhalb der Rollen definiert sind.
- Am Ende werden die Container heruntergefahren und mit
docker commit
entsprechende Docker Images erstellt.
Eine spannende Frage ist natürlich, wie Ansible im Container Prozesse ausführen kann und Dateien in den Container bekommt. Normalerweise verwenden CM-Systeme SSH als das Protokoll der Wahl, was wiederum voraussetzen würde, dass ein sshd
auf dem jeweiligen Container laufen müsste. Aus verschiedenen Gründen ist ein sshd
im Container unerwünscht, da dieser ja auch in dem finalen Image enthalten sein würde. Daher geht Ansible Container einen anderen Weg: Es verwendet einen eigenes Docker Connection Plug-in, das intern docker cp
und docker exec
verwendet, sodass auf den Ziel-Containern nichts weiter installiert sein muss. Nun, ganz stimmt das nicht: Die Minimalvoraussetzung ist, das Python auf dem Zielsystem vorhanden ist, damit Ansible seine Tasks ausführen kann. Da jedoch viele Basis-Images von vornherein Python mitbringen und, falls es wirklich notwendig ist, man ja vor dem docker commit
Python wieder deinstallieren könnte, ist das wohl für die meisten Anwendungsfälle keine grosse Einschränkung.
Beispiel
Schauen wir uns ein einfaches Beispiel an, bei dem wir ein Image mit einem Nginx-Server und einer eigenen statischen Seite erstellen. Zunächst einmal müssen wir Ansible Container installieren. Das geht am besten mit dem Python Installer pip
:
$ sudo pip install ansible-container[docker,k8s] ....
Aktuell gibt es ein Problem mit der neuesten Python-Docker-Bibliothek (betrifft nicht nur Ansible Container), sodass ein Downgrade erforderlich ist:
$ sudo pip install 'docker>=2.4.0,<3.0' ....
Die aktuelle Version von Ansible Container ist 0.9.2 und es sollte auch keine ältere installiert werden, da sich das Konfigurationsformat über die Zeit hinweg geändert hat und wir uns hier auf die letzte Version 2
des Formats stützen.
Nun können wir ein leeres Projekt initialisieren:
$ ansible-container init Ansible Container initialized from Galaxy container app 'ansible.django-gulp-nginx'
Es werden mehrere Beispieldateien angelegt. Die zentrale Datei hier ist container.yml
. Sie ist sehr ähnlich zu einer docker-compose.yml
-Spezifikation aufgebaut und bestimmt, wie Docker Images gebaut werden, aber auch, wie sie zur Laufzeit gestartet und verlinkt oder welche Images zu einer Registry gepusht werden. Auch kann aus der container.yml
ein Ansible Playbook generiert werden, das gleich beschreibt, wie die Images in der Cloud (z.B. auf Kubernetes oder OpenShift) deployt werden sollen.
Wir beschränken uns aber in diesem Artikel ausschließlich auf das Bauen von Images und fokussieren uns daher nur auf den Teil in container.yml
, der für die Erstellung von Images relevant ist.
In der container.yml
gibt es eine Sektion services:
, die wir für einen einfachen Webserver zum Ausliefern einer statischen Webseite konfigurieren wollen.
Um den Webserver zu installieren, holen wir uns eine Nginx-Rolle von Ansible Galaxy:
$ ansible-container install ansible.nginx-container Parsing conductor CLI args. - downloading role 'nginx-container', owned by ansible - downloading role from https://github.com/ansible/nginx-container/archive/master.tar.gz - extracting ansible.nginx-container to /tmp/tmpBFIZj5/ansible.nginx-container - ansible.nginx-container (master) was installed successfully Conductor terminated. Cleaning up. command_rc=0 conductor_id=365197... save_container=False
Hierbei wird automatisch die container.yml
angepasst:
services: ansible.nginx-container: roles: - ansible.nginx-container
Die hinzugefügte Rolle ansible.nginx-container
wird ebenfalls in requirements.xml
definiert und als externe Rolle referenziert.
Nun wollen wir eine eigene Rolle hinzufügen, die einfach eine statische HTML-Seite installiert. Dazu erzeugen wir eine YAML-Datei roles/hello-world/tasks/main.yml
:
- name: Create "hello world" HTML page copy: content: "<html><body><h1>Hello Ansible Container !</h1></body></html>" dest: /static/index.html
Das Zielverzeichnis /static
ist dabei von ansible.nginx-container
vorgegeben, sodass statische Dateien aus diesem Verzeichnis ausgeliefert werden. Diese Ansible-Rolle tragen wir in container.yml
ein und passen dabei noch ein paar Sachen an:
services: web: from: centos:7 roles: - ansible.nginx-container - hello-world ports: - "8000:8000"
Neben dem Hinzufügen von hello-world
zu der Liste der auszuführenden Rollen haben wir den Service noch in web
umbenannt und für das spätere Testen ein einfaches Portmapping für den Nginx Port eingetragen.
Image bauen & starten
Nun ist es an der Zeit, das Image zu bauen:
$ ansible-container build Building Docker Engine context... Starting Docker build of Ansible Container Conductor image (please be patient)... Parsing conductor CLI args. Docker™ daemon integration engine loaded. Build starting. project=demo Building service... project=demo service=web PLAY [web] ********************************************************************* TASK [Gathering Facts] ********************************************************* ok: [web] TASK [ansible.nginx-container : Install epel-release] ************************** changed: [web] TASK [ansible.nginx-container : Install nginx] ********************************* changed: [web] => (item=[u'nginx', u'rsync']) ..... ..... PLAY RECAP ********************************************************************* web : ok=18 changed=14 unreachable=0 failed=0 Applied role to service role=ansible.nginx-container service=web Committed layer as image image=sha256:0f3581... service=web PLAY [web] ********************************************************************* TASK [Gathering Facts] ********************************************************* ok: [web] TASK [hello-world : Create "hello world" HTML page] **************************** changed: [web] PLAY RECAP ********************************************************************* web : ok=2 changed=1 unreachable=0 failed=0 Applied role to service role=hello-world service=web Committed layer as image image=sha256:be46b1.... service=web Build complete. service=web All images successfully built. Conductor terminated. Cleaning up. command_rc=0 conductor_id=589cb00... save_container=False
In der Ausgabe sieht man schön, wie der Conductor gestartet wird und die beiden spezifizierten Rollen von zwei dynamisch erzeugten Playbooks ausgeführt werden. Interessant ist auch, dass nach jeder Rolle eine Image-Schicht comitted wird. Man kann aber auch mit der Option --flatten
einstellen, dass am Ende das erzeugte Image aus nur einer Schicht besteht.
Wenn wir jetzt die Images unseres Docker daemons auflisten, sehen wir, dass ein Image mit zwei Tags erzeugt wurde:
docker images | head -3 REPOSITORY TAG IMAGE ID CREATED SIZE demo-web 20180407081908 322c2515597b 29 seconds ago 266MB demo-web latest 322c2515597b 29 seconds ago 266MB
Zum Schluss können wir nun auch direkt mit Ansible Container das Image starten:
$ ansible-container run Parsing conductor CLI args. Engine integration loaded. Preparing run. engine=Docker™ daemon Verifying service image service=web PLAY [Deploy demo] ************************************************************* TASK [docker_service] ********************************************************** changed: [localhost] PLAY RECAP ********************************************************************* localhost : ok=1 changed=1 unreachable=0 failed=0 All services running. playbook_rc=0 Conductor terminated. Cleaning up. command_rc=0 conductor_id=969f40... save_container=False
Dank unserer Port-Zuordnung liegt nun unter http://localhost:8000
unsere fantastische Webseite, was wir leicht mit einem Browser überprüfen können (bzw. mit der IP des Docker daemons, falls dieser nicht auf localhost
läuft)
Damit sind wir am Ende unseres Beispiels, das auch direkt auf GitHub unter rhuss/ansible-container-demo heruntergeladen werden kann.
Zusammenfassung und Ausblick
Der Ansatz mit einem Configuration-Management-System Docker-Container zu bestücken, hat seinen Charme. Der grösste Vorteil ist sicherlich, dass viele bereits existierende Roles/Recipes/Manifests wiederverwendet werden können. Auch sind die Möglichkeiten, die ein CMS bietet, wesentlich größer als das doch recht eingeschränkte Dockerfile-Vokabular.
Das Schöne ganz speziell bei Ansible Container (im Gegensatz zu anderen CM-Lösungen) ist, dass es dank der flexiblen Architektur des Connector-Plug-ins von Ansible sehr einfach möglich ist, ohne SSH die Container zu provisionieren.
Ein Nachteil des Ansatzes mit Ansible Container ist sicherlich, dass er wesentlich komplexer ist als die einfache Verwendung eines Dockerfiles. Auch sollte man den Laufzeit-Overhead nicht unterschätzen. Beim ersten Lauf fühlt sich Ansible Container recht zäh an, jedoch wird das bei den anschließenden Aufrufen durch aggressives Caching deutlich besser.
Lesen Sie auch: 10 kreative Wege, Docker Images zu bauen: Dockerfile Template
Nicht zuletzt sollte noch erwähnt werden, dass Ansible Container inzwischen ein offizielles Ansible-Projekt und auch ganz hervoragend dokumentiert ist.
Damit sind wir auch schon am Ende für heute. In der nächsten Etappe werden wir dann einen ganz ähnlichen Ansatz mit Packer von HashiCorp kennenlernen.
10 kreative Wege, Docker Images zu bauen: Dockerfile & Docker Commit
Hinterlasse einen Kommentar
Hinterlasse den ersten Kommentar!