Ansible-Playbooks und Docker-Machine

Der Titel könnte auch lauten “Ansible Playbooks auf mit Docker-Machine eingerichteten Hosts ausführen”. War mir zwar ein wenig zu sperrig, aber genau darum geht es. Ansible ist eine Open-Source-Software zur Orchestrierung, Konfiguration und Administration und wird eingesetzt, wenn derartige Aufgaben automatisiert werden sollen. Im Gegensatz zu vergleichbaren Tools benötigt Ansible einzig eine SSH-Verbindung zu den zu verwaltenden Systemen. Ansible ist in Python geschrieben und nutzt eine einfache Syntax im YAML-Format zur Beschreibung der Aufgaben und Konfiguration der Systeme. Dabei liegen die Beschreibungen in so genannten “Playbooks” vor, die mit Hilfe des Tools “ansible-playbook” ausgeführt werden können. Für eine weiter gehende Einführung siehe z.B. im Artikel “Konfigurationsmanagement mit Ansible” oder natürlich auf den Ansible-Webseiten.

Ansible bietet Module für das Management von Docker-Containern sowie das Erstellen von Docker-Images an, doch dieses Teilproblem konnte ich bereits als gelöst ansehen. Zur Erstellung der Docker-Images dient eine Kombination aus Jenkins mit entsprechenden Plugins sowie eine private Docker-Registry, während das Management der Container auf einzelnen Docker-Hosts oder im Docker Swarm mit den nativen Docker-Tools erledigt wird.

Der Ausgangspunkt hingegen war folgender: Mit Docker-Machine lassen sich Docker-Hosts sehr einfach aufbauen, z.B. auf Amazon AWS EC2-Maschinen, der Google Compute Engine oder auch auf einfachen VMs, die ohne Cloud-Unterbau auskommen. Insofern laufen die Docker-Hosts und sind als Docker-Swarm konfiguriert, es existiert ein Overlay-Network, und mit geeigneten Umgebungsvariablen bzw. Parametern hat das Tool “docker” den Zugriff auf jene Systeme. Die Hosts sind mit Tags versehen, so dass z.B. die Installation der Datenbank-Container auf den “db”-Instanzen erfolgt, daneben gibt es “web”- und andere Tags, so dass die VMs je nach Leistung unterschiedliche Aufgaben erfüllen sollen.

Die konkrete Problemstellung bestand darin, einen Weg zu finden, wie sich die Daten-Verzeichnisse für die Datenbank-Container auf den per Docker-Machine eingerichteten Hosts erstellen lassen. Der einfachste und manuelle Weg wäre gewesen, sich per “docker-machine ssh db...” jeweils einzuloggen und das benötigte Verzeichnis zu erstellen. Besonders elegant oder gar per Skript bzw. Programm wiederholbar ist dies jedoch nicht.

D.h. während es einfach möglich ist, den zu startenden Docker-Containern Konfigurationsparameter mitzugeben, ist der mit Docker-Machine erstellte Host bis auf die grundlegende Docker-Engine-Installation bzw. ggf. Docker-Swarm-Images noch “jungfräulich”. Weitere Tools liegen nicht vor, es handelt sich per Default um eine Standard-Ubuntu-Installation.

Ein anderes Beispiel sind System-Updates, etwa aus Sicherheits-Aspekten. Natürlich könnte man den Host auch komplett neu aufbauen und erneut mit den Docker-Containern bestücken, aber die schnelle Ausführung der Update- und Upgrade-Kommandos könnte dies verhindern.

Das Kernproblem lag nun darin, dass die Hosts, die von Ansible verwaltet werden, zunächst recht statisch definiert sind. Zwar lassen sich Host-Listen und -Gruppen definieren, jedoch beziehen sich die Ansible-Playbooks dann genau auf diese Hosts. Im hier beschriebenen Fall sind die Hosts jedoch als dynamisch anzusehen, da im ersten Schritt von Docker-Machine erzeugt. So ist z.B. die IP-Adresse genau wie der Zugang, d.h. SSH-Keys, nur den Docker-Tools (docker-machine bzw. letztlich docker) bekannt.

Letztlich besteht die Lösung darin, dass Ansible zunächst Tasks auf dem lokalen Rechner bzw. dem System ausführt, das als Steuerungszentrale für Docker dient, so dass Docker-Machine vorliegt. Innerhalb dieser Tasks werden die Parameter abgefragt, die zur Verbindung der Docker-Hosts notwendig sind, d.h. IP-Adressen und SSH-Key. Docker-Machine stellt jene Daten durch das Kommando “inspect” zur Verfügung.

Diese Daten werden für das laufenden Playbook gesammelt, so dass darauf zurück gegriffen werden kann. Im Beispiel sieht dies wie folgt aus – Voraussetzung ist ein installiertes Ansible, ich habe mich hier an die von Ansible heraus gegebenen Versionen gehalten:

sudo apt-get install software-properties-common
sudo apt-add-repository ppa:ansible/ansible
sudo apt-get update
sudo apt-get install ansible

Kurzer Test:

deploy@vm1:~$ ansible --version
ansible 2.0.1.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = Default w/o overrides

Nun lassen sich bereits ad-hoc-Kommandos per ansible-Kommandozeilentool ausführen, aber das soll an dieser Stelle nicht interessieren.

Als weitere Vorbereitung ist eine Ansible-Konfigurationsdatei zu erstellen. Denn Ansible verbindet sich mit anfangs unbekannten Hosts, so dass die bei SSH-Verbindungen übliche Abfrage nach dem Host-Key den Ablauf stören würde. Der Inhalt der ansible.cfg:

[defaults]
host_key_checking = False

Diese Datei liegt für den Test im aktuellen Verzeichnis. Ebenfalls als Test bzw. Proof-of-Concept anzusehen ist das eigentliche Playbook:

---
- name: Get IP app01
  hosts: localhost
  tasks:
    - shell: "docker-machine ip app01" 
      register: dockerip_app01
    - shell: 'docker-machine inspect --format=''{''{.Driver.SSHKeyPath''}''} app01'
      register: sshkey_app01
    - add_host: name="{{ dockerip_app01.stdout }}" groups=appgroup ansible_ssh_user=ubuntu ansible_ssh_private_key_file="{{ sshkey_app01.stdout }}" 

- name: Get IP app02
  hosts: localhost
  tasks:
    - shell: "docker-machine ip app02" 
      register: dockerip_app02
    - shell: 'docker-machine inspect --format=''{''{.Driver.SSHKeyPath''}''} app02'
      register: sshkey_app02
    - add_host: name="{{ dockerip_app02.stdout }}" groups=appgroup ansible_ssh_user=ubuntu ansible_ssh_private_key_file="{{ sshkey_app02.stdout }}" 

- name: Test
  hosts: appgroup
  tasks:
    - command: /bin/echo foo
    - file: path=/srv/test1 state=directory
      become: yes

Erläuterung:

Für die Hosts “app01” und “app02” werden dieselben Tasks ausgeführt. Die Kommandos laufen auf “localhost“, somit wird wie oben beschrieben das Tool docker-machine aufgerufen. Mit der Zeile “- shell: "docker-machine ip app01” wird die IP-Adresse des Hosts, der bei docker-machine unter dem Namen “app01” bekannt ist, ermittelt. Die komplette Ausgabe wird in der Variablen “dockerip_app01” gespeichert (register).

Dasselbe erfolgt nun für den SSH-Private-Key, der für den Zugriff auf die jeweilige Maschine notwendig ist:

- shell: 'docker-machine inspect --format=''{''{.Driver.SSHKeyPath''}''} app01'

Mit docker-machine inspect <hostname> erhält man umfassende Angaben über den jeweiligen Host, benötigt wird jedoch nur der komplette Pfad des SSH-Keys. Dazu dient das kurze Go-Text-Template, was mit –format angegeben wird. Falls es nicht innerhalb des Playbooks geschrieben wäre, würde es einfach --format={{.Driver.SSHKeyPath}} lauten, die zusätzlichen Hochkommata dienen als Maskierung der geschweiften Klammern, da diese innerhalb von Ansible-Playbooks eigentlich Ansible-Variablen darstellen.

Die Ausgabe dieses Kommandos wird wiederum in der Variablen sshkey_app01 gespeichert.

Zuletzt wird eine dynamische Host-Liste erzeugt, d.h. mit add_host die soeben ermittelte IP-Adresse inklusive der jeweilige SSH-Key sowie der von Docker-Machine standardmässig benutzte User “ubuntu” der Host-Gruppe namens “appgroup” hinzu gefügt.

Für den Host app02 erfolgen die Schritte analog, so dass danach in der Gruppe beide Hosts enthalten sind.

Mit Hilfe der Gruppe lassen sich nun weitere Ansible-Tasks ausführen, und zwar wie beabsichtigt auf den jeweiligen von Docker-Machine erzeugten Hosts. Im Beispiel werden eine Testausgabe erstellt sowie ein Verzeichnis /srv/test1 erzeugt. Somit befindet man sich an dieser Stelle wieder in der Ansible-üblichen Umgebung, alle Module und Kommandos können verwendet werden, da Ansible den SSH-Zugriff auf die Hosts besitzt.

Ausgeführt wird das Beispiel durch den Aufruf von ansible-playbook:

deploy@vm1:~$ ansible-playbook test.yml

Dessen Ausgabe:

 [WARNING]: provided hosts list is empty, only localhost is available


PLAY [Get IP app01] ************************************************************

TASK [setup] *******************************************************************
ok: [localhost]

TASK [command] *****************************************************************
changed: [localhost]

TASK [command] *****************************************************************
changed: [localhost]

TASK [command] *****************************************************************
changed: [localhost]

TASK [command] *****************************************************************
changed: [localhost]

TASK [add_host] ****************************************************************
changed: [localhost]

PLAY [Get IP app02] ************************************************************

TASK [setup] *******************************************************************
ok: [localhost]

TASK [command] *****************************************************************
changed: [localhost]

TASK [command] *****************************************************************
changed: [localhost]

TASK [command] *****************************************************************
changed: [localhost]

TASK [command] *****************************************************************
changed: [localhost]

TASK [add_host] ****************************************************************
changed: [localhost]

PLAY [Test] ********************************************************************

TASK [setup] *******************************************************************
ok: [52.48.x.y]
ok: [52.49.x.z]

TASK [command] *****************************************************************
changed: [52.48.x.y]
changed: [52.49.x.z]

TASK [file] ********************************************************************
changed: [52.49.x.z]
changed: [52.48.x.y]

PLAY RECAP *********************************************************************
52.48.x.y                  : ok=3    changed=2    unreachable=0    failed=0
52.49.x.z                  : ok=3    changed=2    unreachable=0    failed=0
localhost                  : ok=12   changed=10   unreachable=0    failed=0

Die anfängliche Warnung ist verständlich – üblicherweise wird Ansible ein Inventory mit Host-Listen, Gruppen-Definitionen etc. übergeben bzw. Ansible verwendet die Einstellungen aus /etc/ansible/hosts, jedoch werden hier die Hosts erst zur Laufzeit hinzu gefügt.

Der Rest funktioniert wie gewünscht, zunächst werden auf localhost die notwendigen Daten gesammelt, anschließend die Tasks auf den entfernten Hosts ausgeführt.

Im Beispiel wurden die Hosts einzeln im Playbook definiert. Dies ist dem Charakter des Proof-of-Concept geschuldet und sollte sich auch skripten lassen. Ebenso geht das Beispiel davon aus, dass die Docker-Hosts bereits von Docker-Machine erzeugt worden sind. Geht man einen Schritt weiter, könnte genau dies vorab wiederum per Ansible geschehen sein, denn letztlich handelt es sich auch nur um wiederholte Ausführungen des Kommandos docker-machine. Insofern ließe sich die komplette Infrastruktur mit Hilfe von Ansible und den Docker-eigenen Tools aufbauen.

Interessanterweise habe ich bis dato wenig über die Kombination von Ansible und Docker-Machine gefunden. Im Artikel “Using Ansible with Docker Machine to Bootstrap Host Nodes” wird zwar eine grundsätzlich ähnliche Lösung beschrieben, jedoch gefiel mir der Schritt der Generierung der SSH-Keys nicht wirklich – schließlich wird dies von docker-machine beim “Bootstrap” bereits erledigt. Dem gegenüber beschreibt “Docker Machine and Ansible” einen Weg, an den benötigten SSH-Key per Environment-Variable zu gelangen, nur was ist, wenn man mehrere Hosts ansprechen möchte? Und bei “docker machine and aws in combination with ansible” finden zunächst abseits des Playbooks shell-basierte Manipulationen des Inventory-Files (ergo hosts von Ansible) statt.

Die beschriebene Lösung zeigt somit einen Weg, Ansible-Playbooks auf per Docker-Machine erzeugten Docker-Hosts mit Ansible- und Docker-Bordmitteln auszuführen.

 

 

2 Gedanken zu „Ansible-Playbooks und Docker-Machine“

  1. very good, i was looking for that, but it fails for me… 🙁
    TASK [command] *****************************************************************
    fatal: [localhost]: FAILED! => {“changed”: true, “cmd”: “docker-machine ip app01”, “delta”: “0:00:00.016721”, “end”: “2016-07-26 16:27:29.886918”, “failed”: true, “rc”: 1, “start”: “2016-07-26 16:27:29.870197”, “stderr”: “Host does not exist: \”app01\””, “stdout”: “”, “stdout_lines”: [], “warnings”: []}

    1. In the above blog entry, the names “app01” and “app02” are just examples of host names. Please have a look at your configured host names in the output of “docker-machine ls”. You have to modify the playbook, please enter your hostname(s) instead of “app01” or “app02”. Maybe it’s worth to try out the parameters at command line first, so you are sure that it works in the ansible playbook.

      geschke@connewitz:~$ docker-machine ls
      NAME        ACTIVE   DRIVER    STATE     URL                        SWARM              DOCKER        ERRORS
      connewitz   -        generic   Running   tcp://192.168.10.60:2376                      v1.12.0-rc4
      kaditz      -        generic   Running   tcp://192.168.10.39:2376   miltitz            v1.12.0-rc4
      lausen      -        generic   Running   tcp://192.168.10.64:2376                      v1.12.0-rc4
      lindenau    -        generic   Running   tcp://192.168.10.66:2376   miltitz            v1.12.0-rc4
      miltitz     -        generic   Running   tcp://192.168.10.65:2376   miltitz (master)   v1.12.0-rc4
      pirita      -        generic   Running   tcp://192.168.10.72:2376   miltitz            v1.12.0-rc4
      tolkewitz   -        generic   Running   tcp://192.168.10.43:2376   miltitz            v1.12.0-rc4
      tondi       -        generic   Running   tcp://192.168.10.73:2376   miltitz            v1.12.0-rc4
      
      geschke@connewitz:~$ docker-machine ip pirita
      192.168.10.72
      

      In this example one of the machines is “pirita”, so you can get the IP address by “docker-machine ip pirita”.

      Thanks for your feedback and good luck!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Tags: