logo

Deploying Ruby-on-Rails applications using RPM packaging

It’s been a long time between posts but the time has come!

In this post I hope to take a good look at one way to deploy a working ruby on rails (RoR) application by packaging it in an RPM.

In this example all of the gems the application requires are downloaded and built/compiled at the same time the RPM is, and thus the RPM contains all the required gems (100+ in this example). The best way to deploy an application, in my opinion, would be to standardize on a set of gems that is available at the OS level–so the RPM would not contain any gems, rather would require the general OS level gems.

Unfortunately, for many reasons, which I won’t get into, that is just not possible for me at this time. Maybe in the future when all gems can easily be built into RPMs, and also when internal developers can agree on a set of gems. Someday…

Environment

We’re deploying to a specific RHEL6 server environment.

Ruby version

We’ll be deploying the RoR application to Redhat Enterprise 6 (RHEL6) virtual machine which has, and likely always will have, @ruby 1.8.7@ (with backported security patches of course!).

[root@RoR-TEST ~]# ruby -v
ruby 1.8.7 (2010-06-23 patchlevel 299) [x86_64-linux]
[root@RoR-TEST ~]# cat /etc/redhat-release 
Red Hat Enterprise Linux Server release 6.1 (Santiago)

This will likely be a problem in the future, as it seems that Rails 3.2 will be the last version that supports ruby 1.8.X (where X seems to be 7+ as 1.8.6 is specifially not supported). At some point the dev team may want to go to a Rails version that will not run on Ruby 1.8.7.

Apache and passenger

We’ll also be deploying the RoR app using apache and passenger.

Requirements

A few things are required to build and deploy an RPM.

The application code in some kind of version control system and hopefully that VCS supports tagging…svn, mercurial, and git all support tags.

A build server that is the same as OS and arch as the production server being deployed to. In this case, RHEL6 and X86_64.

** A spec file for the application. ** This build server needs bundle and gem available in the binary PATH because currently the example spec file needs it to be there. ** A working rpmbuild environment, configured as appropriate.

A test server to test the RPM deployment, ie. a place to actually install the RPM into.

The spec file

Building a RPM requires, among other things, a spec file. This file is the heart of a RoR RPM deployment.

I have put an example spec file up on github to peruse and abuse. Again, it’s not going to work out of the box, but it’s a good example, or will be at some point. :)

The build portion of the spec file is what is interesting in terms of deploying a RoR app with RPM.

Prior to the build section the code has been pulled out of a git repository into a local build directory by the rpmbuild process.

In the build section, which I’m cutting and pasting examples out of, we are going to cd into that checked out repository and use bundle to compile and install all the gems into ./vendor/bundle.

%build
pushd %{name}

# Install all required gems into ./vendor/bundle using the handy bundle commmand
bundle install --deployment

Once that has completed, which could be quite a long process depending on the number and complexity of the gems required, we remove the assets and recompile them.

# Compile assets, this only has to be done once AFAIK, so in the RPM is fine
rm -rf ./public/assets/*
bundle exec rake assets:precompile


Then we need to also build bundler into the RPM as well, which requires a smidge of trickery:

# For some reason bundler doesn't install itself, this is probably right,
# but I guess it expects bundler to be on the server being deployed to
# already. But the rails-helloworld app crashes on passenger looking for
# bundler, so it would seem to me to be required. So, I used gem to install
# bundler after bundle deployment. :) And the app then works under passenger.

PWD=`pwd`
cat > gemrc <<EOGEMRC
gemhome: $PWD/vendor/bundle/ruby/1.8
gempath:
- $PWD/vendor/bundle/ruby/1.8
EOGEMRC
        #gem --source %{gem_source} --config-file ./gemrc install bundler
        gem --config-file ./gemrc install bundler
# Don't need the gemrc any more...
rm ./gemrc


Finally, it seems that some of the gems have a funny location for ruby set, which we need to change because the rpmbuild process will mark that as a requirement. This issue may be fixed now.

# Some of the files in here have /usr/local/bin/ruby set as the bang
# but that won't work, and makes the rpmbuild process add /usr/local/bin/ruby
# to the dependencies. So I'm changing that here. Either way it prob won't
# work. But at least this rids us of the dependencie that we can never meet.
for f in `grep -ril "\/usr\/local\/bin\/ruby" ./vendor`; do
         sed -i "s|/usr/local/bin/ruby|/usr/bin/ruby|g" $f
         head -1 $f
done

popd

Basically, three major things happen in the build section:

Use the handy bundler application to install all the required gems

Also install bundler itself

Work around other issues as found

Once that is done, we have a nice spec file that can be built and then installed!

rpmbuild

Now we build our RPM. In this example I’m building a RoR application called special_collections. rhel6b is my RHEL6 build server/environment.

[curtis@rhel6b SPECS]$ rpmbuild -ba special_collections.spec 
Executing(%prep): /bin/sh -e /var/tmp/rpm-tmp.J1hbLc
+ umask 022
+ cd /home/curtis/rpmbuild/BUILD
+ rm -rf ./special_collections
+ git clone https://code.example.com/git/special_collections
Initialized empty Git repository in /home/curtis/rpmbuild/BUILD/special_collections/.git/
SNIP!
Checking for unpackaged file(s): /usr/lib/rpm/check-files /home/curtis/rpmbuild/BUILDROOT/special_collections-0.1.4-1.el6.ualib.x86_64
Wrote: /home/curtis/rpmbuild/SRPMS/special_collections-0.1.4-1.el6.ualib.src.rpm
Wrote: /home/curtis/rpmbuild/RPMS/x86_64/special_collections-0.1.4-1.el6.ualib.x86_64.rpm
Executing(%clean): /bin/sh -e /var/tmp/rpm-tmp.VOkPMU
+ umask 022
+ cd /home/curtis/rpmbuild/BUILD
+ rm -rf /home/curtis/rpmbuild/BUILDROOT/special_collections-0.1.4-1.el6.ualib.x86_64
+ exit 0

NOTES:

Installing the RPM on a brand new server

I have a brand new server all ready for this ruby application to be deployed. It’s a minimal install.

[root@RoR-TEST ~]# rpm -qa | grep -i "apache\|ruby\|passenger"
[root@RoR-TEST ~]# 
# Nothing! No ruby, passenger, or apache currently installed.
[root@RoR-TEST ~]# rpm -qa | wc -l
293
# And only 293 RPMs!

Normally I install a RPM from a custom yum repository, but in this example I will use @yum localinstall@ so I copy the RPM from the build server to the new server.

Note that I have several 3rd party repositories configured on this server, including epel, rpmforge, and the passenger repository. Obviously one has to trust a 3rd party repository to use it. Configuring yum priorities might be a good idea as well to try to avoid unwanted collisions.

So, to install:

[root@RoR-TEST tmp]# yum localinstall special_collections-0.1.4-1.el6.ualib.x86_64.rpm 
SNIP!
 rubygem-passenger-native-libs  x86_64  1:3.0.11-1.el6_1.8.7.352   passenger                                       29 k
 rubygem-rack                   noarch  1:1.1.0-2.el6              epel                                           446 k
 rubygem-rake                   noarch  0.8.7-2.1.el6              optional                                       403 k
 rubygems                       noarch  1.3.7-1.el6                optional                                       206 k
 sgml-common                    noarch  0.6.3-32.el6               base                                            43 k

Transaction Summary
========================================================================================================================
Install      73 Package(s)

Total size: 234 M
Total download size: 74 M
Installed size: 413 M
Is this ok [y/N]: y
SNIP!
  rubygem-daemon_controller.noarch 0:0.2.6-1.el6                   rubygem-fastthread.x86_64 0:1.0.7-2.el6             
  rubygem-passenger.x86_64 1:3.0.11-1.el6                          rubygem-passenger-native.x86_64 1:3.0.11-1.el6      
  rubygem-passenger-native-libs.x86_64 1:3.0.11-1.el6_1.8.7.352    rubygem-rack.noarch 1:1.1.0-2.el6                   
  rubygem-rake.noarch 0:0.8.7-2.1.el6                              rubygems.noarch 0:1.3.7-1.el6                       
  sgml-common.noarch 0:0.6.3-32.el6                               

Complete!


Configure the application

Currently the RPM will create a directory in /etc/ that contains the database.yml file for the rails app:

[root@RoR-TEST special_collections]# pwd
/etc/railsapps/special_collections
[root@RoR-TEST special_collections]# ls
database.yml

Edit that to set the proper database information.

Configure apache

Now that apache has been installed because it is required by the custom RPM it needs to be configured.

First let’s make sure it’ll start on a reboot. Don’t want to have to login on the weekend three months from now after a spontaneous reboot now do we? :)

[root@RoR-TEST yum.repos.d]# chkconfig httpd on

Now to setup the apache rails environment for this particular application. Note that in this case, we’re doing one RoR app per virtual host. It’s just easier for me because there are some variables that need to be set in the virtual host config file.

I also always configure a /etc/httpd/conf.d/vhost.d directory for virtual host files, and tell httpd to check there for *.conf files.

[root@RoR-TEST vhost.d]# grep vhost.d /etc/httpd/conf/httpd.conf 
Include conf.d/vhost.d/*.conf

The vhost config file looks like this:

[root@RoR-TEST vhost.d]# cat specialcollections.example.com.conf 
<VirtualHost *:80>
   ServerName specialcollections.example.com
   DocumentRoot /usr/share/railsapps/special_collections/public

   # Because of the way we're deploying rails apps, ie. by using bundler during the rpm
   # build process to install all the required gems into $RAILSAPP/$NAME/vendor/bundle/ruby/1.8
   # this has to be set here. Otherwise the app will not have the required gems to run.
   SetEnv GEM_HOME /usr/share/railsapps/special_collections/vendor/bundle/ruby/1.8/
   <Directory /usr/share/railsapps/special_collections/public>
        Options -MultiViews
    </Directory>
</VirtualHost>

Startup apache:

[root@RoR-TEST vhost.d]# service httpd configtest
[root@RoR-TEST vhost.d]# service httpd start

Done with apache.

Rake

Now to configure the initial database.

First, the paths need to be setup. I create a file called special_collectionsrc that has path information setup. Note that this rc file is someting I created specifically for this application because each rails app will have it’s own paths and gems. Then, when wanting to use rake with the specific application that file is sourced to ensure the correct rake and other gems are used.

[root@RoR-TEST ~]# which rake
/usr/bin/rake
# oops not the right one!
[root@RoR-TEST ~]# which bundle
/usr/bin/which: no bundle in (/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin)
# oops isn't on the path!
[root@RoR-TEST ~]# cat special_collectionsrc 
#!/bin/bash
export GEM_HOME=/usr/share/railsapps/special_collections/vendor/bundle/ruby/1.8
PATH=/usr/share/railsapps/special_collections/vendor/bundle/ruby/1.8/bin:$PATH
export RAILS_ENV=production

Once that file is sourced, we should be able to find rake on the path:

[root@RoR-TEST ~]# source special_collectionsrc 
[root@RoR-TEST ~]# which rake
/usr/share/railsapps/special_collections/vendor/bundle/ruby/1.8/bin/rake
[root@RoR-TEST ~]# which bundle
/usr/share/railsapps/special_collections/vendor/bundle/ruby/1.8/bin/bundle

cd to /usr/share/railsapps/special_collections/ and load the db:

[root@RoR-TEST special_collections]# rake db:load
/usr/share/railsapps/special_collections/vendor/bundle/ruby/1.8/gems/curb-0.7.16/lib/curb_core.so: warning: already initialized constant CURL_SSLVERSION_DEFAULT
-- create_table("collections", {:force=>true})
   -> 0.4194s
-- create_table("gallery_images", {:force=>true})
   -> 0.0040s
-- initialize_schema_migrations_table()
   -> 0.0077s
-- assume_migrated_upto_version(20111104163654, ["db/migrate"])
   -> 0.0048s

Whenever working with this particular RoR app the rc file should be sourced.

Done raking.

That’s…it

At this point the rails app should be available at the virtual host URL that was configured in the vhost. :)

While it’s a long process to get that intial spec file and rpmbuild working, once it’s done the application can be deployed in a few minutes, and now the developers can simply worry about commiting and tagging code, and let the sysadmin deal with deploying the actual application in a replicable manner. Of course there will be some back and forth, new gems might not compile, etc, but the general structure is in place. Further, the deployment is quite automatable–a new tag could mean a new RPM build and deployment to test.