Intro
We will write a simple REST application and deploy it to a server using
Chef by writing a cookbook and deploying via knife solo
.
This will seem like a lot of work to deploy an application, but makes
more sense if you are setting up a more distributed application. For
example, we could have 3 web servers running this application on Tomcat,
with another server setup to load balance using nginx
, using redis
as
a distributed cache, and postgres
as the database, across two
servers setup with replication.
Creating the project
Let’s first create a dummy application to upload. This assumes you have Maven installed. I was using Maven 3.1.1 with Apple Java 1.6.0_65. The source for the jax-rs project is on Github.
mvn archetype:generate
You’ll be presented with a list of options, which you may filter. Input
jaxrs
and press enter to filter by that term. Enter the number
corresponding to the archetype
org.apache.cxf.archetype:cxf-jaxrs-service
. Enter options for the
groupId
, artifactId
, and version
, and accept the defaults. I
entered the following values:
groupId=ly.jamie
artifactId=jaxrs_tutorial
version=1.0.0
The Route and Controller
This will generate a controller
or service bean
called HelloWorld
.
HelloWorld.java
is presented below, for reference.
@Path("/hello")
public class HelloWorld {
@GET
@Path("/echo/{input}")
@Produces("text/plain")
public String ping(
@PathParam("input") String input
) {
return input;
}
// ...
}
For our purposes, all we really need to know from this file is that
there will be a route at the application context that looks like
/hello/echo/***
, where, for GET requests, whatever is entered for the
asterisks will be echoed back. The annotations are specific to
JAX-RS
and explaining them is not in the scope of this article.
Running the app
We can confirm our belief by running the web application. Although you could build the war and deploy it to an application server, we’ll take a simpler approach using the maven-tomcat7 plugin.
mvn tomcat7:run
Running the command above will run our application in its own instance of Tomcat. After running the command, you’ll see several logging statements including something that looks like:
[INFO] Running war on http://localhost:13000/jaxrs-service
That will tell you the port and context of the application.
Sanity Check
Once the application is started, we can test the endpoint via curl
.
curl -v localhost:13000/jaxrs-service/hello/echo/test
# * About to connect() to localhost port 13000 (#0)
# * Trying ::1...
# * connected
# * Connected to localhost (::1) port 13000 (#0)
# > GET /jaxrs-service/hello/echo/test HTTP/1.1
# > User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8y zlib/1.2.5
# > Host: localhost:13000
# > Accept: */*
# >
# < HTTP/1.1 200 OK
# < Server: Apache-Coyote/1.1
# < Date: Sat, 01 Mar 2014 19:52:57 GMT
# < Content-Type: text/plain
# < Content-Length: 4
# <
# { [data not shown]
# * Connection #0 to host localhost left intact
# test* Closing connection #0
We can see in the last line that “test” was echoed, since it was entered
after the echo/
in the URL.
Starting the Cookbook
These next steps assume that you have setup your system for Chef. The final cookbook we develop below is available on Github
First, create a new cookbook template using
Berkshelf. Assuming we have have Ruby installed
(I am using ruby 2.0.0p247), we can grab this dependency by running the
command gem install berkshelf
. This will install the berks
command,
which provides an easy way to create a cookbook.
berks cookbook jaxrs_tutorial
cd
into the cookbook directory and run bundle install
to install the
gems specified in the Gemfile
.
Go into the metadata.rb
file and specify that your cookbook depends on
java
and
application_java
.
depends 'java'
depends 'application_java', '~> 3.0.0'
We will use a custom version of the application_java
cookbook that I
forked from the main version. Let’s modify the Berksfile
to point at
this version. The Berksfile
should look something like this:
site :opscode
cookbook 'apt'
cookbook 'application_java',
git: 'https://github.com/jamiely/application_java'
metadata
Now, we’ll edit default recipe in recipes/default.rb
. We’ll make use
of the java_webapp
and tomcat
LWRPs
that the application_java
cookbook provides.
case node['platform']
when 'debian', 'ubuntu'
include_recipe 'apt'
package 'curl' # here for tests. Don't do this!
end
include_recipe 'java' # Need described below
application 'jaxrs_tutorial' do
path '/var/www/jaxrs_tutorial'
repository 'http://nexus/jaxrs_tutorial-1.0.0.war'
revision '1.0.0'
scm_provider Chef::Provider::RemoteFile::Deploy
# Handles war specifics and creates the `context.xml`
java_webapp
tomcat # Symlinks the context.xml into $CATALINA_HOME
end
How it works
The recipe above will create the application at the path
/var/www/jaxrs_tutorial
with directories:
- current
- shared
- releases
Then, it will download the war from the URI given to the repository
method (this can be the path to the war in your Nexus) to the releases
directory to releases/1.0.0
, and symlink the war to current
.
Finally, it will install Tomcat (and Java) if they’re not installed,
then create a context.xml
file at
$CATALINA_HOME/conf/Catalina/jaxrs_tutorial.xml
.
There are a few special notes about this recipe.
Firstly, we must have allowed Chef to install Java for this recipe
to work (at least on CentOS). If we didn’t we will probably get a
message about keytool
not being found. This is because the default
Java package does not add the keytool
tool inside $JAVA_HOME/bin
to
the $PATH
. However, if one uses Chef to manage it, keytool
will be
added the $PATH
using the alternatives
command. There is another way
to get around this error, by specifying the keytool
path in attribute
node['tomcat']['keytool']
. It’s not actually necessary to specify in
the recipe itself. Instead, you could specify java before this cookbook
in the run list of the role or node you are operating on.
Secondly, note that the java_webapp
LWRP may
accept a block to configure database parameters and to provide a custom
context.xml
template.
Thirdly, note the package 'curl'
call in the first block. This is around
purely for tests, which will be described later, and installs the curl
command. When creating real cookbooks, you should create cookbooks just
for testing purposes, and add these to the run list in your
.kitchen.yml
file, described later.
Lastly, we include the apt
recipe if we’re working on ubuntu so that
the apt repository gets updated. Otherwise, java installation might
fail.
Making Things Configurable
Since we want some of the settings to be configurable, we’ll swap them
out with cookbook attributes
. Let’s create a file attributes/default.rb
that looks like the following:
default[:jaxrs_tutorial][:application_name] =
'jaxrs_tutorial'
default[:jaxrs_tutorial][:application_path] =
'/var/www/jaxrs_tutorial'
default[:jaxrs_tutorial][:application_version] =
'1.0.0'
default[:jaxrs_tutorial][:war_uri] =
'http://path_to_your_nexus/jaxrs_tutorial-1.0.0.war'
And the new recipes/default.rb
using these attributes looks like:
case node['platform']
when 'debian', 'ubuntu'
include_recipe 'apt'
package 'curl' # here for tests. Don't do this!
end
include_recipe 'java'
application node[:jaxrs_tutorial][:application_name] do
path node[:jaxrs_tutorial][:application_path]
repository node[:jaxrs_tutorial][:war_uri]
revision node[:jaxrs_tutorial][:application_version]
scm_provider Chef::Provider::RemoteFile::Deploy
java_webapp
tomcat
end
Testing the Cookbook
Now that we’re done with the cookbook, it’d be helpful to test it. We
can use test-kitchen
to do so. It was installed when you ran
bundle install
earlier. test-kitchen
allows us to integration test
our cookbook by spinning up a virtual machine. Using the default setup,
it’ll use Vagrant to spin up the machine.
The Chef run is configured via the .kitchen.yml
file that was created by
Berkshelf. Let’s modify the file to change some of the attribute
settings for the default suite. These are attributes we will test for
later.
suites:
- name: default
run_list:
- recipe[jaxrs_tutorial::default]
attributes:
jaxrs_tutorial:
application_name: 'jaxrs_tutorial'
application_path: '/var/www/jaxrs_tutorial_test'
war_uri: 'http://33.33.33.1:8999/jaxrs_tutorial-1.0.0.war'
For the war uri, you can specify a nexus URI. Alternatively, you could run a
web server on your host machine (which is reachable on the guest via
33.33.33.1). I ran the following command in my java project target
directory
to do that:
python -m SimpleHTTPServer 8999
If you have a web server installed locally, you may just want to drop the war there.
Let’s kick off the Chef run now by running the command below:
bundle exec kitchen converge centos
This will spin up a CentOS box for testing, and run our Chef recipe on it. test-kitchen will spin up the machine, but we need to write a test. Although there are many tests we could perform, let’s perform two tests directly relevant to what we’re doing above.
Writing the Tests
Let’s check that the application path exists, and we’ll hit the /echo
endpoint and make sure that whatever we give it is echoed back. We’ll
write the test using the Bats
Bash testing framework. Here’s the test:
# test/integration/default/bats/app_running.bats
@test "app folder exists" {
ls /var/www/jaxrs_tutorial_test
}
@test "app is running" {
HOST=localhost:8080
URI=$HOST/jaxrs_tutorial/hello/echo/hello_world
curl $URI | grep -i 'hello_world'
}
We also need to add the busser-bats
gem to our Gemfile
. The
Gemfile
should look something like this now:
source 'https://rubygems.org'
gem 'berkshelf'
gem 'test-kitchen'
gem 'kitchen-vagrant'
gem 'busser-bats'
And make sure the gems are installed by running bundle install
again.
Now, we can have the test run on the node we provisioned via the
command:
bundle exec kitchen verify centos
If we’re confident that everything works, we can perform a full test on
both CentOS
and Ubuntu
at the same time using the command:
bundle exec kitchen test --parallel
Deploying
Now that we have an application WAR and a corresponding Chef cookbook, we can deploy the application via Chef Server, Hosted Chef, or Chef Solo. I will briefly go over how to deploy using Chef Solo.
First, we want to install the knife-solo gem. Then, in an empty directory, run the command below to generate a kitchen directory structure.
knife solo init .
Modify the created Berksfile
to reference the cookbook we just
created.
Next, let’s say we have ssh access to a node whose hostname is
example.local
. Let’s also assume we’ve setup key-based authentication
with that node. We can issue the following command to install Chef on
the node:
knife solo prepare user@example.local
We’ll be asked to enter our sudo
password. Once the preparation is
done, we can issue the following command to start the Chef run. We’ll
have to enter our sudo password each time we run this command.
knife solo cook user@example.local
This will create a file nodes/example.local.json
. Now, in the
run_list
array in that file, we can add a reference to our cookbook.
We must also specify the WAR URI, as it is the only required attribute
of the cookbook.
{
"run_list": [
"recipe[jaxrs_tutorial]"
],
"jaxrs_tutorial": {
"war_uri": "http://33.33.33.1:8999/jaxrs_tutorial-1.0.0.war"
}
}
Finally, running cook
again will run Chef on the node and run the
cookbook.
knife solo cook user@example.local
Conclusion
We did a lot of things above! We created an application, a Chef cookbook with a test, and deployed the application via Chef Solo. There are many things I glossed over and did not explain, since this is such a big topic area. If you have any questions on the process or notice a problem, feel free to reach out to me on twitter!