noizZze

Monit, Jenkins and Ruby on Rails

My latest challenge was setting up a continuous integration environment on one of the staging servers. If you don’t know what CI is, allocate a couple of hours to study the concept. If you are in software development, my bet is you need the thing, and it will probably change the way you look at your project release cycle forever.

I chose Jenkins for its ease of installation and extensive plugin net. You’ll need Java 1.5+ for it, but that’s rarely is a problem. I just grabbed a copy from java.com and got it all up and running in less than 5 minutes on our CentOS 5.4 box. Previously we tried CruiseControl.rb and it worked well for some time, but as I started linking it to our Rails 3 projects, I figured that it’s easier to try something else than make the tool work. I’m sure it’s a great tool and all, just didn’t work for me.

Running Jenkins in the standalone mode is as easy as:

java -jar jenkins.war --httpPort=3333

And that will put it on your system’s port 3333. But we don’t want to run it manually every time the server restarts. So I figured it’s time to configure Monit to look after the app. So I grabbed excellent daemonize and put together a simple command:

/usr/local/sbin/daemonize \
  -c /home/deploy \
  -E JENKINS_HOME=/home/deploy/.jenkins \
  -p /home/deploy/.jenkins/jenkins.pid \
  /home/deploy/jre1.6.0_27/bin/java -jar jenkins.war --httpPort=3333

Here we basically instruct it to change the working directory to the “/home/deploy”, provide a JENKINS_HOME environment variable for it to know where to put data and look for plugins, and then give the name of the PID file that will be used by Monit later.

Now to tie it all to the Monit, I create a config file “jenkins.conf” (hold my configs separately in /etc/monit.d/) like this:

check process jenkins with pidfile /home/deploy/.jenkins/jenkins.pid
  start program = "/usr/local/sbin/daemonize -c /home/deploy -E JENKINS_HOME=/home/deploy/.jenkins -p /home/deploy/.jenkins/jenkins.pid /home/deploy/jre1.6.0_27/bin/java -jar jenkins.war --httpPort=3333" as uid deploy and gid deploy with timeout 60 seconds
  stop program  = "/bin/sh -c 'kill -9 `cat /var/run/jenkins.pid`'"

One thing to note is that Monit runs under “root” user / group, but we don’t want it to start Jenkins under these almighty credentials. That’s why we need “as uid deploy and gid deploy” at the end of the start program line.

Configuring a Rails app is easy:

  • Create JENKINS_HOME/configs directory and use subdirectories per project to hold database.yml and anything else you may want to keep environment specific
  • Create JENKINS_HOME/bundles directory and use subdirectories per project to hold gem bundles. You can get away with installing all gems each time, but why not save and share from build to build.
  • Log into Jenkins and create a project. You may need to install a Git plugin to handle Git repositories as Jenkins comes with CVS and Subversion support built-in.

The final bit is to configure the build script that will:

  • Go to the project workspace
  • Link a shared gem bundle directory into vendor/bundle
  • Link any config files from the shared location
  • Install gems from the bundle
  • Configure the database from schema.rb
  • Run specs

I use this little script. Just create a build step in your Jenkins project configuration and paste it. Don’t forget to substitute the PROJECT name.

export RAILS_ENV=test
cd /home/deploy/.jenkins/jobs/PROJECT/workspace/
ln -sf /home/deploy/.jenkins/bundles/PROJECT vendor/bundle
ln -sf /home/deploy/.jenkins/configs/PROJECT/database.yml config/database.yml
/opt/ruby/bin/bundle install --deployment
/opt/ruby/bin/bundle exec rake db:schema:load
/opt/ruby/bin/bundle exec rake spec

As you noticed, I call bundler with “–deployment” flag that tells it to put gems into vendor/bundle (which is already linked to a shared location). The reason is that you’ll need root permissions to install gems into the main system-wide location. While bundling them into the project’s vendor directory is the weight any user can lift.

Depending on how the testing goes, your build will either succeed of fail.