Entwicklungsumgebung mit Vagrant

Veröffentlicht am 06 April 2017 von Lukas Sadzik

Vorab

Über die Vorteile von einer virtuellen und provisionierten Entwicklungsumgebung muss man sicherlich nicht mehr viele Worte verlieren.

Daher nur ein paar Stichpunkte:

  • Je ähnlicher sich Produktiv-, Staging- und Entwicklungsumgebung sind, desto weniger Fehler werden während des Umzugs von einem auf das andere System passieren.
  • Das "Bei mir läufts aber"-Phänomen wird reduziert, sowie die Zeit zum erneuten Aufsetzen der Umgebung reduziert.
  • Dadurch, dass die Infrastruktur als "Code" vorliegt, bekommt man die Möglichkeit besser darüber zu reden. Wikiseiten, die hauptsächlich nur aus "dann installiere packet-xy, schreibe in `/konfigurationsdatei/abc` dies uns jenes" gehören damit der Vergangenheit an.
  • Hat man für jedes Projekt eine eigene Box, bekommt man keine Probleme, wenn Projekt A noch PHP5.6, Apache und MySQL verwendet, Projekt B aber PHP7, Nginx, MongoDB benötigt. Das Hostsystem bekommt davon nichts mit, und das Projektrepo hat "die Batterien schon mit dabei".

Das Schlüsselwort "DevOps" ist nicht neu, und viele nutzen schon die Vorteile, die einem diese enge Zusammenarbeit von Betrieb und Entwicklung bringen. Doch von Zeit zu Zeit sollte man einen kleinen Rundumblick wagen, ob die eigenen Werkzeuge noch aktuell sind, und was sich in der letzten Zeit alles getan hat.

Im Falle der eigenen Entwicklungsumgebung kann man sich da zum Beispiel folgende Fragen stellen:

  • Nutze ich eine aktuelle Version des Betriebssystems als Grundlage? Im April 2016 kam eine neue Ubuntu LTS version (Xenial Xerus, 16.04) heraus.
  • Nutze ich eine aktuelle PHP Version? PHP7 erschien im Dezember 2015.
  • Nutze ich eine aktuelle Provisionierungssoftware? Ansible 2.1 wurde im April 2016 veröffentlicht.
  • Verwende ich das richtige Werkzeug für die Entwicklungsumgebung? Vagrant genießt zur Zeit den Status eines Quasi-Standards. Docker ist eine weitere mögliche Variante - aber nicht Thema dieses Posts.

Wenn man nicht alle Fragen mit "Ja" beantworten kann, könnte man über ein Update nachdenken.

Entwicklungsumgebung einrichten.

Was haben wir vor?

Gönnen wir unserer Entwicklungsumgebung also ein Update. Das Ziel ist:

  • Ubuntu 16.04 LTS (Xenial Xerus) als Betriebssystem.
  • PHP 7 mit Konfiguration für Symfony und Xdebug (via PHPStorm)
  • Apache als Server
  • MySQL als Datenbanksoftware
  • Ansible 2.1 zur Provisionierung
  • Vagrant als VM-Verwaltungstool

Als einzige Abhängigkeit auf unserem Host-System haben wir damit Vagrant und Virtualbox.

Vagrantfile erstellen

Wir erstellen unser Vagrantfile mit

Nachdem man alle Kommentare gelöscht hat, sieht das Vagrantfile so aus:

Bevor wir mit der Provisionierung beginnen, konfigurieren wir noch ein paar Grundeigenschaften unserer Box:

Private Network

Damit wir Shared Folders via NFS verwenden können, müssen wir der Box eine statische IP Adresse geben.

Zusätzlich bekommen wir hierdurch die Möglichkeit, für unsere Box einen Eintrag in der /etc/hosts des Host-Systems anzulegen. Dann brauchen wir uns die IP-Adresse nicht merken und können bequem über den Hostnamen zugreifen.

Windowsnutzer können leider kein NFS mit Vagrant verwenden. Dennoch lohnt sich diese Konfiguration, um die Box über eine eigene IP-Adresse aufzurufen

Shared Folder

Wir möchten das Verzeichnis, in dem sich das Vagrantfile befindet in der Box unter /srv/share verfügbar machen (Natürlich lässt sich der Zielpfad den eigenen Gewohnheiten anpassen).

Windowsnutzer können NFS leider kein NFS mit Vagrant verwenden. Daher muss  die Zeile unter Windows wie folgt enden: :nfs => false

Hostname

Den Hostnamen konfigurieren wir mit der Zeile

Provisionierung

Nun können wir die Provisionierung konfigurieren:

Mit config.vm.provision "ansible_local" sagen wir Vagrant, dass Ansible innerhalb der Box verwendet werden soll. Unser Host-System bleibt dadurch frei von zusätzlicher Software, und wir stellen sicher, dass die Box sich selbst um alles kümmert, was sie braucht.

Beim ansible.provisioning_path = "/srv/share/ansible" erkennen wir den Pfad der Shared Folders wieder, ergänzt um ein ansible Verzeichnis, das bei uns noch nicht existiert. Wir müssen es erstellen: mkdir ansible.

Mit ansible.playbook = "dev.yml" geben wir das Playbook an, das ausgeführt werden soll. Der Pfad ist relativ zum ansible.provisioning_path. Mit touch ansible/dev.yml erstellen wir die Datei.

Das fertige Vagrantfile

Unser Vagrantfile sollte nun so aussehen:

Mit Vagrant arbeiten

Vagrant ist ein reines Kommandozeilenprogramm. Die üblichen Befehle, die man benötigt sind diese:

Ansible Playbook

Testlauf

Fangen wir mit einem sehr einfachem Playbook an, mit dem wir zunächst nur prüfen wollen, ob wir Vagrant richtig eingerichtet haben:

Nun sollten bei einem vagrant provision (oder einem vagrant up ohne erstellte Box) einige Informationen über das Box-System ausgegeben werden. Falls nicht, solltet Ihr auf jeden Fall prüfen, ob Ihr die letzte Version von Vagrant nutzt, den ansible_local (anstatt nur ansible) Provisioner verwendet und (natürlich) nach Tippfehlern Ausschau halten.

Apache installieren

Das sieht nach viel Text für ein apt-get install apache aus. Jedoch ist das zugleich auch der Codeblock, den wir nachher für alle anderen APT Pakete verwenden werden. Zudem sorgt das update_cache: yes dafür, dass wir bei jedem Durchlauf der Provisionierung auch noch apt-get update ausführen.

Als Nächstes aktivieren wir das rewrite Apache-Modul und platzieren die Vhost-Konfigurationsdatei von Apache mit Hilfe eines Templates:

Dieses Template existiert noch nicht. Wir erstellen es in ansible/simple_vhost.conf.j2 mit folgendem Inhalt:

Die Templatesprache, die hier verwendet wird ist Jinja2. Twig ist ein Klon von Jinja2, deshalb sollte sich der gemeine Symfony-Entwickler hier sofort zurechtfinden. Und ihm mag auch schon aufgefallen sein, dass wir hier eine Variable ansprechen, die wir noch nicht definiert haben: .

Wir können mit dem template-Modul von Ansible keine Variablen weitergeben. Stattdessen haben wir alle Variablen, die Ansible kennt, in unseren Templates verfügbar. Die zweite Variable in dem Template, , gehört zu den "Facts" die Ansible aus dem System einließt

Daher setzen wir die Variable für das document_root am Kopf unseres Playbooks:

Zum Schluss stellen wir noch sicher, dass der Apache Service auch gestartet wird und fügen einen Handler hinzu, welcher dafür sorgt, dass Apache neugestartet wird, sollte sich etwas an der Konfiguration ändern:

Damit der Handler aufgerufen wird, muss an die entsprechenden Tasks jeweils noch ein notify angehangen werden:

PHP installieren

Wir installieren PHP nicht aus den standard APT-Repositories, sondern verwenden das PPA von Ondřej Surý, welches einen "Quasi-Standard"-Status genießt.
Der Vorteil dieses PPA sind aktuellere Versionen und schnellere Updates  im Vergleich zu den Standard APT-Repositories.

Den entsprechenden Ansible-Befehl setzen wir *vor* das apt-Modul, welches Apache installiert.

Nun erweitern wir das apt Modul um eine Schleife und installieren die PHP Pakete. (Dabei nicht vergessen apache2 mit in die Liste der Pakete zu übernehmen).

Dann wollen wir noch sicherstellen, dass ein paar Zeilen in den PHP-INI-Dateien existieren:

Dank des regexp des lineinfile-Moduls können wir die Werte der einzelnen Einstellungen auch nachträglich ändern, ohne das diese Zeile dann mehrfach in den php.ini Dateien auftaucht.
Die Möglichkeit, die Werte in der Form 'setting.name': 'settingValue' zu schreiben verdanken wir dem with_dict. Leider lässt sich das nicht mit weiteren Schleifen in Ansible kombinieren, so dass wir für jede einzelne php-ini einen solchen Block definieren müssen.

MySQL installieren

Für MySQL fügen wir drei weitere Pakete zu unserem apt-Modul hinzu:

Die ersten beiden (mysql-server und php-mysql) sollte man schon kennen. Die installieren MySQL und das dazugehörige PHP-Modul. Das letzte (python-mysqldb) installiert die Python Bibliothek, die Ansible benötigt, um mit MySQL zu kommunizieren.

Warum muss Ansible mit MySQL kommunizieren? Mit Ubuntu Xenial hat sich die Sicherheitspolitik von MySQL geändert. Das heißt, der Benutzer root ohne Passwort funktioniert nicht mehr und wir müssen selbst einen MySQL-Benutzer anlegen:

Damit MySQL auch sicher läuft, modifizieren wir noch das service Modul, mit dem wir Apache starten:

Das heißt auch, dass wir später in unserer `app/config/parameters.yml` die Einträge für `database_user` und `database_password` dementsprechend anpassen müssen!

Composer installieren

Der einfachste Weg, Composer via Ansible zu installieren wäre es, den Installer-Befehl einfach mit dem shell-Modul aufzurufen.
Dadurch übergehen wir aber den Signaturcheck von composer. Um diesen mit Ansible zu realisieren müssen wir etwas weiter ausholen:

Zuerst prüfen wir, ob die ausführbare Datei von compser existiert. Wenn ja, wird der block danach nicht ausgeführt (when: composer_state.stat.exists == false am Ende des Blocks).

Wenn nicht, passiert folgendes in dem Block:

Mit uri holen wir uns die aktuelle Signatur und nutzen diese um den Download des `composer-setup.php`-Scripts zu verifizieren.

Zum Schluss wird das composer-setup.php dann mit PHP ausgeführt.

Symfony installer installieren

Der Symfony Installer installiert sich dank des get_url Moduls von Ansible trivial einfach:

Zusätzliche Pakete

Für den reibungslosen Betrieb mit Symfony und Composer wollen wir noch folgende Pakete installieren: acl, curl, git und zip:

Kleiner Bonus: Ein Xdebug Script für die Kommandozeile

Xdebug mit PHPStorm kann Spaß machen. Wenn man Web Requests debuggen möchte, reicht ein kleines JS-Bookmark, welches man sich einfach via WebFrontend erstellen lassen kann, und die Xdebug Konfiguration, welche wir schon in unserem Playbook haben.
Für Kommandozeilen Kommandos sieht das nicht ganz so einfach aus. Noch nicht:

Wir platzieren folgendes Shell Script in ansible/xdebug

Und kopieren es mit Ansible nach /usr/local/bin/xdebug

Nun können wir mit xdebug php-command auch Kommandozeilenbefehle wie gewohnt mit PHPStorm debuggen :)

Das fertige Playbook

Unser Playbook sollte nun so aussehen:

Die Box starten und das Projekt initialisieren.

Nun können wir die Entwicklungsumgebung starten und unser neues Symfony-Projekt beginnen:

Nicht vergessen, die app/config/parameters.yml für den MySQL Nutzer anzupassen.

Da sich das Symfony-Verzeichnis mit in dem shared-folder befindet, werden wir beim Aufruf des Frontcontrollers eine Fehlermeldung bekommen, welche uns darauf hinweist, dass die Session nicht gespeichert werden konnte. Deswegen, und auch um an Performance zu gewinnen, wollen wir nun die Dateipfade für Cache, Logs und die Session ändern:

Session:

Cache und Logs:

Und schon kann es losgehen mit dem nächsten Symfonyprojekt. Ich wünsche viel Spaß beim Umsetzen eurer Ideen.

Anmerkung zur Basebox

Die hier genutzte Basebox für Vagrant (ubuntu/xenial64) ist die offizielle von Canonical herausgegebene Box. In der Vergangenheit gab es ein paar Probleme mit der Box, zudem hält sie sich nicht an einige Vorgaben von Vagrant (so lautet der Nutzername der in der Box verwendet wird ubuntu und nicht vagrant).
Wen das stört, der sollte einen Blick auf die Basebox von Jeff Geerling werfen: geerlingguy/ubuntu1604

Repository

Das Vagrantfile + Ansible Playbook kann man auf Github finden, clonen und natürlich Pull Requests stellen.