EC2 Continuous Deployment: Cooking with Chef and a stint of Gradle

Note: This is the fourth article in the series on continuous deployment. If you want to start with the overview, go here.

What we’ve done so far is a pretty basic Continuous Integration setup. To take this to the next level, we’ll add a new job, to be run at night, that provisions and configures a new EC2 instance, deploys the application and runs the integration tests.

The steps that needs to be executed are

  • Start a new EC2 instance
  • Configure the instance with all the software needed to run the application (in our case Jetty & nginx)
  • Deploy the application
  • Run the integration tests
  • Shutdown the instance

Chef

Chef is a new tool for automated infrastructure management. Like Gradle it’s a task specific DSL (for managing infrastructure) based on a scripting language (Ruby).

For a full Chef implementation, you would run a Chef server that knows everything about your infrastructure. It also holds your Cookbook which is a collection of recipes, where each recipe describes how to install/configure some software on a node. New nodes will run chef-client to configure the node according to the “recipes” defined on the sever.

But the nice thing about Chef is that it also seems to scale down to the quite simple scenario we are going to use here, which won’t be using the client/server setup but the chef-solo command. This will still execute recipes to configure the node (and those same recipes can be used in the full scale setup), but will not need a Chef server.

The helloworld recipe

Whenever a node is brought up it should have a specific role (i.e. db server, web server, app server etc). I think it makes sense to create a recipe for each role and then have that recipe contain all the stuff that needs to be installed and configured for this role. In our example, we only have a single role, helloworld, which will contain

  • nginx web server
  • Jetty6 application server
  • The helloworld application

This is specified in the recipe shown below. It basically includes recipes for the dependencies nginx and jetty (which in turn depends on java). After the prerequisites are installed, we download the helloworld application from S3 and put it into the Jetty webapps dir. Finally, we restart jetty6 to deploy the application.

Note that the source of the war file is specified as source node[:war]. This means the actual value is a property of the node configuration. In a full Chef setup the value will come from the Chef server, but when running chef-solo we will specify the node properties in a file called dna.json.

Unfortunately, until this bug is fixed in Chef, the URL cannot contain special characters.  When using S3, this means you cannot create a signed URL for the war file but need to make the file public readable. Hopefully it will be fixed soon (the fix is simple, see ticket)

include_recipe "jetty"
include_recipe "nginx"

remote_file "helloworld_war" do
        path "/usr/share/jetty6/webapps/ROOT.war"
        owner "jetty"
        mode  0640
        source node[:war]
end

service "jetty6" do
  action :restart
end

The jetty recipe

The previous recipe was very simple (which is a good thing :-). The jetty recipe is a little more involved and shows some of the power in Chef. It installs a few packages (using Apt ), downloads a number of deb files, installs them, configures the package defaults and enables the startup script.

include_recipe "java"
# Needed by jetty extra & jsp
%w{ ant libgnujaf-java libgnumail-java }.each do |pkg|
 package pkg do
 action :install
 end
end

jetty_version = "6.1.16_all"
jetty_debs = %w{libjetty6-java libjetty6-jsp-java libjetty6-extra-java jetty6}

jetty_debs.each do |deb|
 remote_file deb do
 path "/tmp/#{deb}_#{jetty_version}.deb"
 source "http://dist.codehaus.org/jetty/jetty-6.1.16/debs/#{deb}_#{jetty_version}.deb"
 end
end

bash "install-jetty" do
 code "cd /tmp && dpkg --install *jetty6*.deb"
end

bash "enable-jetty" do
 code "sed s/NO_START=1/NO_START=0/ -i /etc/default/jetty6"
end

service "jetty6" do
 supports :restart => true, :reload => true
 action [ :enable, :start ]
end

Chef comes with recipes for many packages, including java and nginx, and they are a good inspiration.

Bootstrapping an EC2 instance

Before we can begin cooking, we need to launch an EC2 instance. Since Gradle scripts are Groovy scripts, we can perform all this directly in our build scripts by creating a few new tasks & methods. By using the Java libraries Typica and Jets3t, it is relatively easy to perform these tasks. I’ll not go through all the code here, just highlight a few points.

createTask('testDeploy', dependsOn:['libs','createInstance']) {
  urls = uploadCookbooksToS3()
  bootstrap(urls[0], urls[1])
}

createTask('createInstance', dependsOn: 'libs') {
  def config = new LaunchConfiguration(imageId)
  config.keyName = ec2keyname
  def instances = ec2.runInstances(config)
  def instance = instances.instances.get(0)
  createInstance.instanceId = instance.instanceId
  println "Started instance with id $createInstance.instanceId"
}

The testDeploy task depends on the createInstance task, which is the task that actually launches the EC2 instance. I’m using a RightScale Ubuntu AMI which includes Ruby and rubygem. The instance id is stored as a property of the createInstance task. The uploadCookboksToS3 method, uploads the cookbook with all the recipes to S3 and creates and uploads the following dna.json file

{"war": "http://mys3bucket.s3.amazonaws.com/helloworld-1.war", "recipes": "helloworld"}

This file contains the URL to the war file to be used by the helloworld recipe as well as the list of recipes that should be installed on the node. uploadCookbooksToS3 returns URLs for the cookbook.tgz and dna.json files which are needed to download from S3. The bootstrap method first waits for the EC2 instance to fully boot and then performs the following tasks:

  sshExec("gem install --no-ri --no-rdoc ohai chef --source http://gems.opscode.com --source http://gems.rubyforge.org");
  ant.scp(passphrase:"", todir: "root@$createInstance.dnsName:", trust: true, keyfile: ec2KeyFile) {
		fileset(dir: "deployment") {
			include(name: "solo.rb")
		}
	}
  sshExec("chef-solo -c solo.rb -j '$dnaURL' -l info -r '$cookbookURL'");

Basically,

  • Install Chef using gem
  • Copy the solo.rb config file to the instance
  • Configure the node by calling chef-solo with URLs for the dna & cookbook files

Chef will now download the recipes and run them to configure the node.

What we have accomplished so far is that when you type

$ gradle testDeploy

You can sit back and wait for a few minutes. When all is done, you can point your browser to the public DNS of the EC2 instance and …tadaaa…..you should see something like this:

helloworld

This might not be a very impressive application but you can find resources on the net that enable you to write a Twitter clone in Lift in 30 minutes. You might need a little more advanced deployment architecture though :-)

About these ads

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: