Vagrant und Docker: Eine zeitgemäße Entwicklungsumgebung

the next big thing  –  3 Kommentare

Vermutlich kennt nahezu jeder Entwickler den Spruch "it works on my machine". Wer sich auf die Suche nach der Ursache für eine nicht funktionierende Anwendung macht, verzweifelt rasch an verschiedenen Installationen und Versionen. Warum also nicht eine einheitliche Entwicklungsumgebung als virtuelle Maschine für das ganze Team bereitstellen?

Das größte Problem an virtuellen Maschinen ist deren Größe, die sich üblicherweise im mindestens einstelligen GByte-Bereich bewegt. Das erschwert nicht nur die Portabilität der virtuellen Maschine innerhalb des Teams, sondern auch deren Weiterentwicklung mit Hilfe einer Versionsverwaltung.

Abhilfe schafft das als Open Source verfügbare Werkzeug Vagrant, das virtuelle Maschinen auf Basis einer Skriptdatei automatisiert erzeugen kann. Da sich die VM auf diesem Weg auf Tastendruck jederzeit reproduzieren lässt, genügt es, lediglich die Skriptdatei zu verteilen und zu versionieren.

Als Grundgerüst für die Datei Vagrantfile dient das folgende Codeschnippsel, das Ubuntu 14.04 als Basis für die zu erzeugende virtuelle Maschine festlegt:

VAGRANTFILE_API_VERSION = "2"

Vagrant.require_version ">= 1.6.5"

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.box = "chef/ubuntu-14.04"
config.vm.box_check_update = true
end

Standardmäßig richtet Vagrant in jeder erzeugten VM das Verzeichnis /vagrant ein, das automatisch mit dem Verzeichnis des Hosts synchronisiert wird, das die Datei Vagrantfile enthält. Zusätzlich lassen sich aber beliebige weitere Verzeichnisse teilen. Dazu ist ein Verzeichnis des Hosts in ein Verzeichnis der virtuellen Maschine zu mounten.

Legt man das Vagrantfile in einem Verzeichnis ab, das sich auf gleicher Ebene wie die übrigen Projekte befindet, genügt die folgende Zeile, um alle Projekte in dem Verzeichnis /home/vagrant/projects bereitzustellen. So lassen sich Projekte innerhalb der virtuellen Maschine übersetzen und ausführen, aber auf dem Host mit dem Editor der Wahl bearbeiten:

config.vm.synced_folder "../", "/home/vagrant/projects"

Anschließend lässt sich die virtuelle Maschine erstellen und starten, indem man die Anweisung vagrant up auf der Kommandozeile ausführt. Diese lädt, falls noch nicht geschehen, das Image von Ubuntu 14.04 herunter und erzeugt danach auf dessen Basis wie gewünscht eine neue virtuelle Maschine:

$ vagrant up

Damit das funktioniert, muss außer Vagrant auch eine Virtualisierungssoftware wie VirtualBox oder VMware installiert sein. Details hierzu finden sich in der Dokumentation von Vagrant.

Standardmäßig startet Vagrant die virtuelle Maschine ohne grafische Benutzeroberfläche. Der Zugriff erfolgt stattdessen über SSH, wobei entsprechende Schlüssel automatisch eingerichtet werden. Daher genügt die Eingabe der Anweisung

$ vagrant ssh

auf der Kommandozeile, um in die VM zu wechseln. Diese lässt sich dann nach Bedarf verwenden. Wird sie nicht mehr benötigt, kann man sie entweder mit der Anweisung

$ vagrant halt

anhalten und für den späteren Gebrauch aufbewahren oder mit

$ vagrant destroy

vollständig löschen. In beiden Fällen lässt sich die virtuelle Maschine durch Eingabe der Anweisung

$ vagrant up

zu einem späteren Zeitpunkt wieder starten. Der SSH-Zugriff funktioniert zwar ohne Weiteres, verwendet aber andere Schlüssel als die des Hostsystems. Dies wäre aber wünschenswert, um aus der virtuellen Maschine heraus beispielsweise auf GitHub zugreifen zu können. Eine einfache Lösung besteht darin, SSH Agent Forwarding zu aktivieren. Dazu muss man dem Vagrantfile die Zeile

config.ssh.forward_agent = true

hinzufügen. Nach einem Zerstören und erneuten Erstellen der virtuellen Maschine ist nun der SSH-Zugriff beispielsweise auf GitHub auch aus der VM heraus möglich.

Führt man Anwendungen in der virtuellen Maschine aus, sind die von ihnen belegten Ports zunächst nur innerhalb der virtuellen Maschine erreichbar. Um diese auch vom Host erreichen zu können, muss man das Weiterleitungen für Ports aktivieren. Dazu dient eine weitere Zeile in der Datei Vagrantfile, die allerdings für jeden Port einzeln angegeben werden muss:

config.vm.network "forwarded_port", guest: 80, host: 80

Falls das verwendete Protokoll nicht TCP, sondern UDP ist, ist dieses zusätzlich anzugeben:

config.vm.network "forwarded_port", guest: 80, host: 80, protocol: "udp"

Die Dokumentation von Vagrant beschreibt zahlreiche weitere Konfigurationsmöglichkeiten für das Netzwerk, im Normalfall genügt das Einrichten von Weiterleitungen allerdings völlig.

Eine virtuelle Maschine ist für die Entwicklung nur dann hilfreich, wenn die für die Entwicklung erforderlichen Anwendungen darin installiert sind. Das erfolgt in Vagrant über sogenannte Provisioner, im einfachsten Fall über ein Shellskript. Alternativ unterstützt Vagrant auch Provisionierungswerkzeuge wie Chef und Puppet, dieser Aufwand ist aber ebenfalls häufig nicht erforderlich.

Um die Installation von Anwendungen mit Hilfe eines Shellskripts durchzuführen, muss ein solches angelegt und im Vagrantfile registriert werden. Standardmäßig läuft das Skript mit administrativen Berechtigungen, bei Bedarf lässt sich das aber durch Angabe des privileged-Parameters abschalten:

config.vm.provision "shell", path: "provision.sh", privileged: false

Die Datei provision.sh ist ein reguläres Shellskript. Als Minimum sollte es die Zeitzone korrekt einstellen, die Paketdatenbank aktualisieren und einige Basispakete wie build-essential und libssl-dev installieren:

#!/bin/bash

sudo echo "Europe/Berlin" | sudo tee /etc/timezone
sudo dpkg-reconfigure -f noninteractive tzdata

sudo apt-get update -y
sudo apt-get install -y build-essential curl git libssl-dev man

Zusätzlich lassen sich natürlich beliebige Anweisungen ausführen. Wird GitHub verwendet, ist das automatisierte Eintragen der IP-Adressen der GitHub-Server in der Datei ~/.ssh/known_hosts sinnvoll. Das erledigt die folgende Anweisung:

ssh-keyscan github.com >> ~/.ssh/known_hosts

Auch der automatisierte Wechsel in das ~/projects-Verzeichnis nach dem Anmelden ist eine sinnvolle Voreinstellung. Dazu genügt es, die Anweisung cd in die Datei ~/.profile einzutragen:

echo "cd ~/projects" >> ~/.profile

Allerdings ist es nicht sinnvoll, alle Anwendungen auf diesem Weg zu installieren. Insbesondere Server sollten sich leicht aktivieren und deaktivieren beziehungsweise neu starten lassen. Auch die Isolation der einzelnen Server wäre praktisch, da sich deren Konfiguration dann nicht in die Quere kommen kann.

Genau das lässt sich mit dem ebenfalls als Open Source erhältlichen Werkzeug Docker umsetzen. Neben zahlreichen vorgefertigten Containern kann man auch eigene Anwendungen in leichtgewichtige und portable Container verpacken.

Dazu muss allerdings Docker in der virtuellen Maschine installiert werden. Glücklicherweise unterstützt Vagrant das bereits von Haus aus, zudem lassen sich bereits während des Provisionierens häufig benötigte Basis-Container wie ubuntu herunterladen:

config.vm.provision "docker", version: "1.2" do |docker|
docker.pull_images "ubuntu"
end

Führt man einen Docker-Container aus und richtet eine Port-Weiterleitung ein, ist allerdings zu beachten, dass diese zwischen Docker und der virtuellen Maschine gilt. Soll der Port auch vom Host aus erreichbar sein, ist eine weitere Weiterleitung im Vagrantfile zu registrieren.

Zu guter Letzt bietet sich an, der virtuellen Maschine gewisse Systemressourcen zuzuweisen, beispielsweise die Anzahl der zu verwendenden Prozessoren oder die Menge an bereitgestelltem Arbeitsspeicher. Das lässt sich in Vagrant für jede Virtualisierungslösung individuell einstellen, weshalb ein einzelnes Vagrantfile durchaus mehrere solcher Konfigurationsblöcke enthalten kann:

config.vm.provider "virtualbox" do |vm|
vm.memory = 2048
vm.cpus = 2
end

config.vm.provider "vmware_fusion" do |vm|
vm.memory = 2048
vm.cpus = 2
end

Um mit der virtuellen Maschine produktiv arbeiten zu können, fehlt nun nur noch ein konkreter Technologiestack. Dieser wird, abgesehen von eventuellen Port-Weiterleitungen, ausschließlich in der Datei provision.sh installiert und konfiguriert. Da das Unternehmen des Autors, the native web, mit JavaScript und Node.js entwickelt, bietet sich Node.js als konkretes Beispiel an.

Es ist ratsam, Node.js nicht direkt zu installieren, sondern ein Werkzeug wie nvm zu verwenden. So lassen sich mehrere Versionen von Node.js parallel installieren und einfach wechseln. Die Installation von nvm erfolgt durch das Klonen eines GitHub-Repositorys und das Ausführen eines Shellskripts:

git clone https://github.com/creationix/nvm.git ~/.nvm ↵
&& cd ~/.nvm ↵
&& git checkout `git describe --abbrev=0 --tags`
echo "source ~/.nvm/nvm.sh" >> ~/.profile
source ~/.profile

Anschließend kann die aktuelle Version 0.10 von Node.js mit Hilfe von nvm installiert werden:

nvm install 0.10
nvm alias default 0.10

Nun stehen die ausführbaren Anwendungen node und npm zur Verfügung, so dass für die produktive Entwicklung lediglich noch einige global installierte Werkzeuge fehlen, die sich jedoch ebenfalls leicht installieren lassen:

npm install -g browserify
npm install -g eslint
npm install -g grunt-cli
npm install -g harp
npm install -g http-server
npm install -g less
npm install -g mocha
npm install -g uglify-js

Trägt man diese Aufrufe in der Datei provision.sh ein, genügt im Anschluss der Aufruf der Anweisung

$ vagrant up

zum Erstellen einer virtuellen Maschine, die alles Notwendige für die Arbeit mit Node.js enthält. Wer die Mühe scheut, die Dateien Vagrantfile und provision.sh selbst zu schreiben, findet vorgefertigte Dateien mitsamt der zugehörigen Dokumentation im GitHub-Repository thenativeweb/vagrant-node.

Vagrant und Docker sind hervorragende Werkzeuge, um die Entwicklung von Software drastisch zu vereinfachen. Insbesondere die erreichbare Automatisierung und Reproduzierbarkeit sparen wertvolle Zeit und helfen, sich auf das Wesentliche konzentrieren zu können.

Allerdings ist der Einsatz von OS X oder Linux als Betriebssystem auf dem Host ratsam: Vagrant funktioniert zwar auch unter Windows, vieles fühlt sich allerdings nach einer Unterstützung zweiter Klasse an.

tl;dr: Vagrant ermöglicht das automatisierte und reproduzierbare Erstellen von virtuellen Maschinen, Docker hingegen das isolierte Verpacken von Anwendungen in Container. Die Kombination beider bildet die Basis für eine zeitgemäße Entwicklungsumgebung.