//

Automatisierte virtuelle Testumgebungen mit Vagrant und Puppet

24.2.2012 | 11 Minuten Lesezeit

Das Problem kennt wohl jeder Entwickler:

Man arbeitet in einem Team – möglicherweise verteilt – an einer komplexen Anwendung die mehrere Infrstruktur-Komponenten umfasst. In den meisten, einfachen Fällen hat man zumindest schon Bedarf nach einer Datenbank und einem Application Server. Wird es komplexer kommen eventuell noch Komponenten wie eine Messaging Middleware oder ein NoSQL-Store hinzu. Meist installiert man sich als Entwickler alle Komponenten lokal, wenn man Glück hat ist die Installation und Konfiguration zumindest zum Teil von Skripten unterstützt. Oft wird diese „Kunst“ aber auch im Team von Mund zu Mund weitergegeben oder man muss sich durch schlecht oder lieblos gepflegte Wiki-Seiten hangeln. Selbst im besten Fall bleibt noch der Nachteil, dass sich das System insofern vom Test-, Staging- und Produktionssystem darin unterscheidet dass man alle Systeme auf einem Host betreibt. Mit Vagrant , einem Tool zum Erzeugen und Verteilen von virtuellen Umgebungen und Puppet, einer Software für Konfigurationsmanagement kann man hier Abhilfe schaffen. Dieser Artikel zeigt dies am Beispiel eines einfachen Java-Web-Stacks.


Update (Juni 2012): Der Code in Github ist aktualisiert um mit Vagrant-Versionen grösser 1.x zu arbeiten. Die Puppet-Manifeste wurden aktualisiert (kein Sun JDK mehr, Package-Updates).

Installation und erste Schritte

Die folgenden Beispiele enstanden auf einem Ubuntu 11.10, dürften mit Anpassungen aber leicht auch auf anderen Umgebungen lauffähig sein. Da Vagrant auf Ruby und VirtualBox aufbaut sollten beide vorher installiert sein. Anschliessend ist die Installation mit RubyGems der einfachste Weg:

> sudo gem install vagrant

Sollten bei der Installation Fehler auftreten, sind die Timestamps im gemspec-File ein heisser Kandidat. Nach der geglückten Installation kann man sofort loslegen:

> mkdir vagrant-test && cd vagrant-test
> vagrant init lucid64 http://files.vagrantup.com/lucid64.box

Mit vagrant init erzeugen wir im aktuellen Verzeichnis ein Default Vagrantfile, die Basis einer jeden Vagrant-Umgebung. Die letzten beiden Parameter geben den Namen der sog. Basebox an und die Adresse unter der sie geladen wird falls sie nicht vorher schon importiert wurde. Jede Vagrant-Konfiguration baut auf eine Basebox auf, von der ausgehend weitere Schritte erfolgen. Die Macher von Vagrant stellen auf ihrer Seite die 32 und 64bit Version von Ubuntu 10.04 (Lucid Lynx) zur Verfügung. Weitere Boxen findet man auf der Community-Seite vagrantbox.es , oder man macht sich die Mühe und baut sich eine eigene Basebox nach seinen Vorstellungen.

Der obige Befehl legt im Verzeichnis vagrant-test die Datei ‚Vagrantfile‘ an, die die Konfiguration der virtuellen Umgebung beinhaltet:

1Vagrant::Config.run do |config|
2 
3  # All Vagrant configuration is done here. The most common configuration
4  # options are documented and commented below. For a complete reference,
5  # please see the online documentation at vagrantup.com.
6 
7  # Every Vagrant virtual environment requires a box to build off of.
8  config.vm.box = "lucid64"
9 
10  # The url from where the 'config.vm.box' box will be fetched if it
11  # doesn't already exist on the user's system.
12  config.vm.box_url = "http://files.vagrantup.com/lucid64.box"
13 
14  # Boot with a GUI so you can see the screen. (Default is headless)
15  # config.vm.boot_mode = :gui
16 
17  # ...
18 
19end
20

Anschliessend ist die erste virtuelle Umgebung nur noch einen Befehl entfernt:

> vagrant up

Konsolen-Output von 'vagrant up'

Die Warnung bezüglich der VirtualBox GuestAdditions kann man in den meisten Fällen ignorieren. Sollten doch Probleme auftreten kann man seine Basebox aktualisieren und neu paketieren.

Schliesslich kann man sich auf die erzeugte Box einloggen, kann sie wieder anhalten, oder sie löschen, wenn man sie nicht mehr benötigt oder wieder einen sauberen Stand braucht.

> vagrant ssh
> vagrant halt
> vagrant destroy

Der saubere Stand ist hier das Stichwort. In einem Projekt sollte für eine Testumgebung, auch für die lokale Entwicklerumgebung, ein definierter Stand existieren. Damit verhindert man die klassischen „bei mir hat es so funktioniert …“-Diskussionen und hat jederzeit die Möglichkeit diesen Stand wieder zu erzeugen. Auch hier kommt einem Vagrant entgegen, da es die Möglichkeit bietet im Vagrantfile einen Provisioning-Mechanismus zu konfigurieren der dann auf der erzeugten Box die Installation von Software und deren Konfiguration vornimmt. Derzeit unterstützt Vagrant entweder einfache Shell-Skripte, Chef Solo/Server oder eben Puppet. Pavlos Artikel Provisioning of Java web applications using Chef, VirtualBox and Vagrant beleuchtet die Verwendung von Vagrant mit Chef, dieser Artikel demonstriert die Verwendung von Puppet.

Multi-VM-Umgebungen

Ein grosser Vorteil von Vagrant ist, dass man nicht nur eine einzelne Box in einem Vagrantfile definieren kann, sondern ganze Umgebungen aus mehreren Boxen. Im Laufe des Artikels werden wir mit Hilfe von Vagrant und Puppet einen einfachen, typischen Stack für eine Java-Webanwendung aufbauen:

  • einen Datenbankserver mit einer MySQL-Instanz
  • einen Appserver mit einer Tomcat-Instanz

Die meisten Details finden sich im Artikel wieder, die vollständigen Sourcen des Beispiels gibt es auch bei Github .

Nachdem wir unsere Default-Box von vorher wieder zerstört haben editieren wir unser Vagranttfile wie folgt:

1Vagrant::Config.run do |config|
2 
3  # base box and URL where to get it if not present
4  config.vm.box = "lucid64"
5  config.vm.box_url = "http://files.vagrantup.com/lucid64.box"
6 
7  # config for the appserver box
8  config.vm.define "appserver" do |app|
9    app.vm.boot_mode = :gui
10    app.vm.network "33.33.33.10"
11    app.vm.host_name = "appserver01.local"
12    app.vm.provision :puppet do |puppet|
13      puppet.manifests_path = "manifests"
14      puppet.manifest_file = "appserver.pp"
15    end
16  end
17 
18  # config for the dbserver box
19  config.vm.define "dbserver" do |db|
20    db.vm.boot_mode = :gui
21    db.vm.network "33.33.33.11"
22    db.vm.host_name = "dbserver01.local"
23    db.vm.provision :puppet do |puppet|
24      puppet.manifests_path = "manifests"
25      puppet.manifest_file = "dbserver.pp"
26    end
27  end
28 
29end
30

Wir konfigurieren zwei seperate Boxen für unsere beiden Server. Beide gehen von der zuvor importierten lucid64-Box aus und haben statische IP-Adressen. Ausserdem schalten wir für die zugrundeliegenden virtuellen Maschinen den GUI-Modus ein, da ein Problem mit VirtualBox manchmal dazu führt, dass Vagrant beim Starten hängt. Sollte die dazugehörige virtuelle Box schon hochgefahren sein kann man sich dort einfach mit vagrant/vagrant einloggen und ’sudo dhclient‘ ausführen, dann läuft Vagrant weiter.

Configuration Management mit Puppet

Für beide Maschinen legen wir im Verzeichnis manifests entsprechende Puppet-Manifeste an. Die grundsätzliche Einheit in einem Puppet-Manifest ist eine Resource. Sie besteht aus einem Typ, einem Titel und weiteren Parametern:

resource_type { "resource_title":
  param1 => 'value1',
  param2 => 'value2'
}

Resource können bei Puppet eine ganze Menge darstellen, z.B.

  • einfache Dateien, Verzeichnisse, Symlinks …
  • Benutzer und Gruppen
  • Pakete
  • Dienste
  • Cronjobs

Eine vollständige Liste aller Standard-Typen inklusive der möglichen Parameter findet man in der Puppet-Dokumentation. Sehen wir uns zuerst das Manifest für unseren Datenbank-Server an:

1group { 'puppet': ensure => 'present' }
2 
3class mysql_5 {
4 
5  package { "mysql-server-5.1":
6    ensure => present
7  }
8 
9  service { "mysql":
10    ensure => running,
11    require => Package["mysql-server-5.1"]
12  }
13 
14  exec { "create-db-schema-and-user":
15    command => "/usr/bin/mysql -u root -p -e \"drop database if exists testapp; create database testapp; create user dbuser@'%' identified by 'dbuser'; grant all on testapp.* to dbuser@'%'; flush privileges;\"",
16    require => Service["mysql"]
17  }
18 
19  file { "/etc/mysql/my.cnf":
20    owner => 'root',
21    group => 'root',
22    mode => 644,
23    notify => Service['mysql'],
24    source => '/vagrant/files/my.cnf'
25  }
26 
27}
28 
29include mysql_5
30

Zuerst wird die Klasse mysql_5 definiert. Klassen können in Puppet genutzt werden um mehrere zusammengehörige Resourcen zu einer Einheit zusammenzufassen und diese später an einer geeignete Stelle einzubinden. Die erste Resource in unserer Klasse sorgt dafür, dass der Paket-Manager in unserem System (in diesem Fall apt-get) überprüft ob das Paket ‚mysql-server-5.1‘ installiert ist. Sollte das nicht der Fall sein wird Puppet die Installation für uns vornehmen. Der Parameter ensure gibt an was wir für dieses Paket für einen Zustand erwarten, also in diesem Fall dass es auf unserer Maschine vorhanden ist.

Die zweite Anweisung sorgt dafür dass der MySQL-Server-Dienst gestartet ist. Da es in Puppet keine garantierte Ausführungsreihenfolge gibt, sorgen wir mit dem require-Parameter dafür, dass diese Anweisung erst ausgeführt wird wenn zuvor schon die Installation des Pakets stattgefunden hat. Der Wert des Parameters referenziert unsere Paket-Resource. Im Gegensatz zu den Resourcen-Definitionen beginnen die Resourcen-Referenzen mit einem Grossbuchstaben.

Die nächste Resource beschreibt das Ausführen eines Befehls. Mit require versichern wir uns wieder, dass eine andere benötigte Resource – das Starten des MySQL-Servers – zuvor schon ausgeführt wurde. Dann legen wir mit einem einfachen SQL-Befehl das Schema und den Nutzer für unsere Applikation an. Optimalerweise würde man ein Skript mit den Befehlen zum Anlegen in einer Versionsverwaltung oder einem Artefakt-Repository vorhalten und es sich im Manifest von dort ziehen. Der Einfachheit halber sind die Befehle in diesem Beispiel aber direkt enthalten.

Mit der letzten Resource spielen wir unsere veränderte Konfiguration für MySQL ein, die dafür sorgt, dass unsere Instanz auch auf Verbindungen von ausserhalb lauscht. Das notify-Argument benachrichtig den Dienst das die Konfiguration geändert wurde und er sich neu starten soll.

Starten wir nun unsere DB-Box mit

> vagrant up dbserver

sehen wir folgenden Output:

Puppet-Output für das DB-Server-Setup

Der Output von Puppet zeigt, dass das MySQL-Paket vom Status ‚purged‘ auf den Status ‚present‘ geändert wurde, dass unser Konfigurationsfile kopiert wurde und daraufhin der Service refresht wurde.

Um zu testen ob alles geklappt hat kann man sich auf die Datenbank des virtuellen Hosts verbinden (Passwort: dbuser):

> mysql -h 33.33.33.11 -u dbuser -p

In ähnlicher Weise setzen wir unseren Application-Server auf. Hier der Inhalt des Manifests:

1group { 'puppet': ensure => 'present' }
2 
3class sun_java_6 {
4 
5  $release = regsubst(generate("/usr/bin/lsb_release", "-s", "-c"), '(\w+)\s', '\1')
6 
7  # adds the partner repositry to apt
8  file { "partner.list":
9    path => "/etc/apt/sources.list.d/partner.list",
10    ensure => file,
11    owner => "root",
12    group => "root",
13    content => "deb http://archive.canonical.com/ $release partner\ndeb-src http://archive.canonical.com/ $release partner\n",
14    notify => Exec["apt-get-update"],
15  }
16 
17  exec { "apt-get-update":
18    command => "/usr/bin/apt-get update",
19    refreshonly => true,
20  }
21 
22  package { "debconf-utils":
23    ensure => installed
24  }
25 
26  exec { "agree-to-jdk-license":
27    command => "/bin/echo -e sun-java6-jdk shared/accepted-sun-dlj-v1-1 select true | debconf-set-selections",
28    unless => "debconf-get-selections | grep 'sun-java6-jdk.*shared/accepted-sun-dlj-v1-1.*true'",
29    path => ["/bin", "/usr/bin"], require => Package["debconf-utils"],
30  }
31 
32  package { "sun-java6-jdk":
33    ensure => latest,
34    require => [ File["partner.list"], Exec["agree-to-jdk-license"], Exec["apt-get-update"] ],
35  }
36 
37}
38 
39class tomcat_6 {
40  package { "tomcat6":
41    ensure => installed,
42    require => Package['sun-java6-jdk'],
43  }
44 
45  package { "tomcat6-admin":
46    ensure => installed,
47    require => Package['tomcat6'],
48  }
49 
50  service { "tomcat6":
51    ensure => running,
52    require => Package['tomcat6'],
53    subscribe => File["mysql-connector.jar", "tomcat-users.xml"]
54  }
55 
56  file { "tomcat-users.xml":
57    owner => 'root',
58    path => '/etc/tomcat6/tomcat-users.xml',
59    require => Package['tomcat6'],
60    notify => Service['tomcat6'],
61    content => template('/vagrant/templates/tomcat-users.xml.erb')
62  }
63 
64  file { "mysql-connector.jar":
65    require => Package['tomcat6'],
66    owner => 'root',
67    path => '/usr/share/tomcat6/lib/mysql-connector-java-5.1.15.jar',
68    source => '/vagrant/files/mysql-connector-java-5.1.15.jar'
69  }
70}
71 
72# set variables
73$tomcat_password = '12345'
74$tomcat_user = 'tomcat-admin'
75 
76include sun_java_6
77include tomcat_6
78

In der Klasse ’sun_java_6′ wird in mehreren Schritten dafür gesorgt, dass Sun/Orcale Java in der Version 6 auf der Box installiert wird. Es wird das Ubuntu Partner Repository hinzugefügt, anschliessend ein ‚apt-get update‘ ausgeführt, damit das entsprechende Paket auch sichtbar ist und dann das Paket installiert (Quelle: http://offbytwo.com/2011/07/20/scripted-installation-java-ubuntu.html )

In der zweiten Klasse installieren wir Tomcat und die Tomcat-Manager-App und sorgen dafür dass der Tomcat-Service gestartet ist. Ausserdem benutzen wir ein Template, um die Datei tomcat-users.xml zu überschreiben. Im Template selbst steht an der für uns interessanten Stelle

1<user name="<%= tomcat_user %>" password="<%= tomcat_password %>" roles="manager,admin"/>
2

Die Variablen werden im Manifest vor der Nutzung der Klasse gesetzt und dann beim Anlegen der Datei entsprechend gesetzt. Damit wir später auch mit unserem Datenbank-Server sprechen können fügen wir dem Tomcat-Classpath noch einen MySQL-Connector hinzu, der im Beispiel der Einfachheit halber auch wieder im files-Verzeichnis liegt. Wie oben gilt hier, dass man optimalerweise auf ein Repository zugreift, um das Projekt kompakt zu halten. Mit dem subscribe-Parameter am Service sorgen wir dafür, dass Tomcat sich bei bei Änderungen an den referenzierten Resourcen neu startet. Jetzt können wir auch unseren virtuellen Appserver starten:

> vagrant up appserver

Wenn alles geklappt hat kann man sich anschliessend unter http://33.33.33.10:8080/manager/html mit den vorher gesetzen Credentials (tomcat-admin/12345) in der Tomcat-Manager-App einloggen.

Damit steht unsere initiale Infrastruktur. Der Entwickler hat die Möglichkeit sich den Tomcat und die Datenbank z.B. in seine IDE einzubinden, mit Maven-Profilen zu arbeiten um Deployment- oder Datenbank-Update-Schritte direkt gegen die virtuelle Umgebung vorzunehmen oder einfach in den Umgebungen per Hand zu arbeiten

Werden die Umgebungen nicht mehr gebraucht, lassen sie sich wahlweise anhalten damit man sie später wieder mit dem selben Stand weiterverwenden kann oder man zerstört sie um wieder vom konfigurierten Ausgangszustand zu starten. Gibt man beim destroy keinen Box-Namen an werden alle im vorliegenden Vagrantfile definiereten Boxen gelöscht.

> vagrant halt 
> vagrant destroy 

Fazit

Der Vorteil eines solchen Setups liegt auf der Hand. Gesetzt den Fall dass man eine frei zugängige Basebox nutzt, oder man innerhalb der Organisation ein Artefakt-Repository pflegt in dem man auch seine eigenen Baseboxen ablegt, hat jeder neue Entwickler sofort die Möglichkeit sich nach Bedarf eine Testumgebung zu erzeugen. Auch Änderungen an Konfigurationen, hinzunehmen neuer Komponenten o.ä. kann leicht an das ganze Team verteilt werden, ohne dass man Gefahr läuft dass unterschiedliche Mitglieder unterschiedliche Stände pflegen. Einfach das Vagrant-Projekt unter Versionsverwaltung stellen, und jeder Entwickler bekommt Änderungen sofort mit und kann sich die neue Umgebung auf Knopfdruck erzeugen.

Denkt man die Idee konsequent weiter hat man einen weiteren Vorteil: auch gemeinsame genutzte Test-, QA-, Staging- und Produktions-Umgebungen lassen sich mit Puppet managen. Man kann also die Konfigurationen zentral in Zusammenarbeit von Entwicklung und Betrieb (das Stichwort DevOps musste ja noch kommen 😉 ) pflegen und dann auf allen Umgebungen nutzen. Das vermeidet kleine und grosse Differenzen auf den unterschiedlicchen Umgebungen und die daraus resultierenden Fehler. Im nächsten Artikel zu dem Thema werden wir einen genaueren Blick auf das Client/Server-Setup von Puppet werfen, mit man damit mehrere Umgebungen versioniert verwalten kann.

Beitrag teilen

Gefällt mir

0

//

Weitere Artikel in diesem Themenbereich

Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.

//

Gemeinsam bessere Projekte umsetzen

Wir helfen Deinem Unternehmen

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.