Beliebte Suchanfragen

Cloud Native

DevOps

IT-Security

Agile Methoden

Java

|
//

Automated virtual test-environments with Vagrant and Puppet

24.2.2012 | 11 minutes of reading time

Almost every developer knows the problem:
You work in a team, maybe even a distributed one, on a complex application which encompasses several infrastructure components. In most cases you already have the need for a databse and a application server. When it’s getting more complex, stuff like a messaging middleware or a NoSQL store may get added. Most times developers will install all components on their local machine and if you’re lucky, installation and configuration is at least aided by some scripts. But often enough, this “art” is passed on from mouth to mouth or you have to work through some poorly maintained wiki pages. Even in the best cases, the downside is that your test-environment differs from test, staging and production environments in that on your local machine all services are on one host and effects that will happen in a distributed environment are more likely not to occur. But things can be put right with Vagrant , a tool to create and distribute virtualized environments and Puppet, a tool which helps you with your configuration management. This article will illustrate their use with an example of how to construct a simple Java web application stack.


Update (June 2012): The code on Github is updated to work with Vagrant versions 1.x now. Puppet manifests are fixed (no more Sun JDK, update of package lists).

Installation and first steps

The following examples have been tested on Ubuntu 11.10 but should be easily adoptable for other environments. Due to the fact that Vagrant is built upon Ruby and VirtualBox , these should be installed onto your system. Then the installation with RubyGems is the most simple way:

> gem install vagrant

Note: If you encounter errors during the installation, invalid timestamps in the gemspec file are a good bet. After the successful installation, we can start right through:

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

With ‘vagrant init’, we create a default Vagrantfile in the working directory, which is the basis for each Vagrant environment. The last two parameters are the name and URL of the base box on which this environment should build. Each Vagrant project builds upon such a base box and from there on other steps like custom configuration or software provisioning are done. The Vagrant guys offer Ubuntu 10.04 (Lucid Lynx) directly from their site. Other boxes can be found on the community site vagrantbox.es , or you may want to build your own base box.

The above command now creates the file Vagrantfile, which looks like this:

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

Now our first automated virtual environment is just one command away:

> vagrant up

Console output of 'vagrant up'

The warnings concerning the VirtualBox GuestAdditions can be ignored in most cases. However, if you get in troubles with it, you should update and repackage your base box.

Now Vagrant offers you several commands to ssh into your box, halt it or destroy it completely if it’s no longer needed or you want to restart with the initial configured state.

1> vagrant ssh
2> vagrant halt
3> vagrant destroy

This initial configured state is what interests us now. Within a project, their should be one defined state for the test environment, even the one locally used by the developers. This helps getting rid of the classical “I got it to run this way …” situations and everybody has the option to generate this state from the versioned configuration. This is where Vagrant helps you again, as it offers several provisioning mechanisms to be configured to manage installation and configuration of software and the system itself. Currently you can use either plain shell scripts, Chef in its solo or server flavour, or Puppet. Pavlos article Provisioning of Java web applications using Chef, VirtualBox and Vagrant describes how to work with Vagrant and Chef, this article will look into Puppet.

Mutli-VM Environments

On big advantage of Vagrant is its capability to not only define one box in one Vagrantfile, but a whole environment of boxes, if you like. In the course of this article we will build a simple, tiny although typical stack for a Java web application:

  • one database server, running MySQL
  • one application server, running tomcat

Most configuration details are found in this article, but if you like to have the whole setup right away, you can find it on GitHub .

After we destroyed our previously built default box, we start with editing our Vagrantfile like this:

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

We define two separate boxes for our severs and assign them aliases and static IPs. We also enable that the backing virtual boxes are started in GUI mode, because due to a VirtualBox problem , sometimes Vagrant hangs on startup. If that happens, and you see that the virtual box has already booted, login in with vagrant/vagrant, run ‘sudo dhclient’ and Vagrant will carry on.

Configuration Management with Puppet

We create Puppet manifests for both machines in the manifests directory. The basic unit in a Puppet manifest is a resource, which consists of a type, a title and several parameters:

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

Resources can stand for quite a lot, e.g.

  • simple files, directories, symlinks
  • users and groups
  • packages
  • services
  • cronjobs

A complete list of all builtin types with all their possible parameters can be found in the Puppet documentation.
Now, let’s first take a look at our database server manifest:

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

First, we define a class ‘mysql_5’. Classes are a way to group together several resources into bigger units and then include them later on. The first resource in our class tells the underlying package manager (apt-get in this case) to check if the package ‘mysql-server-5.1’ is already installed and to install it if not. The ensure parameter describes the desired state of the package, so in this case we expect the package to be present on the machine.

The second resource is about the MySQL service. Again, our expected state is that it’s running, and if not, Puppet will start it for us. As Puppet does not guarantee a execution order, we need to tell it that this resource depends on the package to be handled first. We can do this with the require parameter, whose value is a reference on our package resource. Note that references on resources are started with a uppercase letter instead of the resources definitions, which start with a lower case letter.

The next resource is the execution of a command. We use the MySQL CLI to create the initial schema and user for our application. Again, we use require to ensure that the resource we depend on, the starting of the server process, will be executed before this one. In a optimal case we would not put the SQL statements with all the credentials directly into our manifest, but would maintain it in some sort of VCS or artifact repository and pull it from there. But for the sake of simplicity we keep it there for these examples.

The last source replaces the default MySQL configuration with ours, which configures our server to listen on remote connections. The notify parameter does – you might have guessed it – notify the MySQL service to restart if it was already running at the time the configuration changed.

Now, we can start our database server with

> vagrant up dbserver

and take a look at the output.

Puppet output for our DB server

The output shows that puppet changed the package status of the MySQL package from ‘purged’ to ‘present’, simply meaning that it instructed the package manager to install it. Furthermore we see that our configuration file has been copied and that the service has been refreshed after that.

Now we can check out if everything went well and try to log into our database server (password: dbuser):

> mysql -h 33.33.33.11 -u dbuser -p

In a similar way, we write a manifest for our application server:

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

The class ‘sun_java_6’ includes a few steps which lead to Sun/Oracle Java being installed on our box. First the Ubuntu partner repository is added, then a ‘apt-get update’ is run to ensure that the package is available. Then the package itself is installed (source: http://offbytwo.com/2011/07/20/scripted-installation-java-ubuntu.html ).

The second class is responsible for installing and starting Tomcat and the Tomcat Manager application. Furthermore we use a template to replace the tomcat-users.xml file. In the template, the line of interest is

The variables get replaced later with the values we set further down in the manifest. To be able to connect to our database server later on, we also add a MySQL connector to Tomcats classpath. Again, for simplicity reasons, we just put it into the files directory but the above argument still holds that you would normally have it somewhere central, e.g. in your artifact repository and pull it from there. With the subscribe parameter, we tell the tomcat service to restart if any of the referenced resources changes.

Now we can also start our application server:

> vagrant up appserver

If everything has gone right, you’ll be able to got to http://33.33.33.10:8080/manager/html and login in with the above configured credentials (tomcat-admin/12345).

With that, our test infrastructure is complete, and now we can connect to booth the Tomcat and the MySQL instance from within our IDE, we may use Maven profiles to integrate them, just deploy stuff by hand or whatever we like.

If the environments are not of use for the moment, we can halt them to restart them later, or we can destroy them completely if we need them no more or want to restart from scratch with our configured state. If you run destroy without a box name, all boxes in the current Vagrantfile will be destroyed.

> vagrant halt
> vagrant destroy

Conclusion

The advantages of such a setup are obvious. Given you use freely available base boxes or have a central repository in your organization which contains your base boxes, it easy for a new developer to set up a test environment on demand, as needed. Also, changes to the environment can be distributed in an easy way. Simply put the Vagrant project under version control, so your developers just need to update, rebuilt the boxes and everybody is guaranteed to have the same setup, without the risk of having several different states on several developer machines. Your environment is always just on command away.

If you consequently think ahead there’s another advantage. You can also manage test, QA, staging and even production environments with Puppet. Put your configurations into a central place, versioned and parameterized for several environments, maintained collaboratively by development and operations (well, the DevOps buzzword had to pop up, right? 😉 ). This eliminates all the small and not so small differences between your environments that lead to all these “… but it used to work well in environment X” situations you really want to avoid. The next article on the topic will show the client/server setup of Puppet and how to work with it to target several environments.

|

share post

Likes

0

//

More articles in this subject area

Discover exciting further topics and let the codecentric world inspire you.

//

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.