Building a Rails Development OpenSolaris Container

A bit over a year ago I started working with Ruby on Rails and ActiveScaffold for an in-house scheduling application I was building for a new consulting client. At the time the version of Rails was 1.2.3 and ActiveScaffold was somewhere before beta. The project went great and everyone was happy. They’ve been using the app now for a year, managing scheduling of over 600 people for close to 1,000 events.

Recently the client came back with some requests for changes and I figured it was a good time to upgrade versions. I didn’t make a rookie mistake though and upgrade all of the versions on my MacBook Pro development box. No, in that way lies madness. You’ll goof up the Rails upgrade and all of a sudden you have no way to develop. Instead I decided to use my OpenSolaris test server (faithfully running in the basement, right next to the weight bench and the Christmas decorations) and a specially created Container just for this purpose.

What follows is a record of my experiment. I’ll spoil the surprise–I got it to work! But I uncovered a few things along the way that surprised me and I wanted to share. Doubtless one of the gurus at Sun (or even Joyent I reckon) could have short circuited some of the problems I encountered, but to be honest I had a pretty good time here. Well, except when it came time to building Mongrel with its dependent C code using friends, but that’s for later on.

A Blurb about OpenSolaris, Zones

So I’m using OpenSolaris here for the test. I develop on a MacBook Pro running Leopard, and the production server is actually Ubuntu Linux. So, why OpenSolaris?

A while ago for Bravadosoft I started researching the right way to manage our on-demand infrastructure. Joyent quickly came to my attention and I loved the way they created a scalable hosting environment for companies like ours. They use OpenSolaris primarily because of ZFS and Zones, and after a bit of playing around I decided that it worked for us. The Bravadosoft Socialytics infrastructure runs on Joyent, with a dedicated cluster per customer implementation. We can scale it up or down with a few emails to Joyent.

Anyway, I built a test server in my basement to play around with for testing, and now it’s got a new job.

Step 1: Creating a Zone

There are a few good tutorials on the web for doing this. Essentially what we want to do is create a dedicated ZFS file system for our Rails development environment, and then create the zone within it.

Michael Ernest has a post that concisely describes some of these steps.

First, create a space for the zone files to live. 2GB is plenty for our purposes. And we’ll use the “-n” space to make it a simple reservation–it doesn’t actually take all of this space until it’s needed.

-bash-3.2$ su
Password:
# cd /
# ls
bin                home               opt                system
boot               kernel             platform           tmp
catalog            lib                proc               usr
dev                lost+found         root               var
devices            media              sbin
etc                mnt                second_root
export             net                socialytics_zones
# mkfile -n 2G /rails_zone
# ls -lh /rails_zone
-rw------T   1 root     root        2.0G Jun 18 13:01 /rails_zone

Now, create the actual pool for this machine:

# zpool create rails_dev /rails_zone
# zpool list
NAME                SIZE   USED  AVAIL    CAP  HEALTH  ALTROOT
rails_dev          1.98G  92.5K  1.98G     0%  ONLINE  -
socialytics_zones  9.94G   825M  9.13G     8%  ONLINE  -

You can see that I have a 10GB pool in there for the Socialytics zones (I have an entire test infrastructure built) and the new 2GB pool for this effort.

Solaris also knows about it as a file system:

# df -h
Filesystem             size   used  avail capacity  Mounted on
/dev/dsk/c0d0s0         15G   6.3G   8.4G    43%    /
/devices                 0K     0K     0K     0%    /devices
/dev                     0K     0K     0K     0%    /dev
ctfs                     0K     0K     0K     0%    /system/contract
proc                     0K     0K     0K     0%    /proc
mnttab                   0K     0K     0K     0%    /etc/mnttab
swap                   4.9G   1.0M   4.9G     1%    /etc/svc/volatile
objfs                    0K     0K     0K     0%    /system/object
sharefs                  0K     0K     0K     0%    /etc/dfs/sharetab
/usr/lib/libc/libc_hwcap2.so.1
15G   6.3G   8.4G    43%    /lib/libc.so.1
fd                       0K     0K     0K     0%    /dev/fd
swap                   4.9G    40K   4.9G     1%    /tmp
swap                   4.9G    40K   4.9G     1%    /var/run
/dev/dsk/c0d0s4         15G    15M    15G     1%    /second_root
/dev/dsk/c0d0s7        427G    10G   413G     3%    /export/home
socialytics_zones      9.8G    19K   9.0G     1%    /socialytics_zones
socialytics_zones/soc_complete
9.8G   799M   9.0G     9%    /socialytics_zones/soc_complete
rails_dev              2.0G     1K   2.0G     1%    /rails_dev

Now, create a specific file system for this container, right there in the pool:

# zfs create rails_dev/zone1
# zfs list
NAME                                      USED  AVAIL  REFER  MOUNTPOINT
rails_dev                                 129K  1.95G    18K  /rails_dev
rails_dev/zone1                            18K  1.95G    18K  /rails_dev/zone1
socialytics_zones                         825M  8.98G    19K  /socialytics_zones
socialytics_zones/soc_complete            824M  8.98G   799M  /socialytics_zones/soc_complete
socialytics_zones/soc_complete@pre-boot  25.0M      -   540M  -
# chmod -R 700 /rails_dev

Now let’s build the Zone. Do this with the zonecfg command and just follow the prompts:

# zonecfg -z railsdev1
railsdev1: No such zone configured
Use 'create' to begin configuring a new zone.
zonecfg:railsdev1> create
zonecfg:railsdev1> set autoboot=false
zonecfg:railsdev1> set zonepath=/rails_dev/zone1
zonecfg:railsdev1> info
zonename: railsdev1
zonepath: /rails_dev/zone1
brand: native
autoboot: false
bootargs:
pool:
limitpriv:
scheduling-class:
ip-type: shared
inherit-pkg-dir:
dir: /lib
inherit-pkg-dir:
dir: /platform
inherit-pkg-dir:
dir: /sbin
inherit-pkg-dir:
dir: /usr
zonecfg:railsdev1> add net
zonecfg:railsdev1:net> set physical=rge0
zonecfg:railsdev1:net> set address=192.168.1.40
zonecfg:railsdev1:net> end
zonecfg:railsdev1> verify
zonecfg:railsdev1> commit
zonecfg:railsdev1> exit

With the Zone now ready to go, let’s install its OS and any shared files:

# zoneadm -z railsdev1 install
cannot create ZFS dataset rails_dev/zone1: dataset already exists
Preparing to install zone .
Creating list of files to copy from the global zone.
Copying <16490> files to the zone.
Initializing zone product registry.
Determining zone package initialization order.
Preparing to initialize <1323> packages on the zone.
Initializing package <16> of <1323>: percent complete: 1%

That will take some time, YMMV, etc. After it’s finished let take a snapshot. ZFS is cool in that you can take a snapshot of a filesystem at any time, and later roll back to it or mount it as an independently running file system somewhere. You might do this, for example, if you later wanted to create another Rails development machine and test load balancing.

# zfs snapshot rails_dev/zone1@pre-boot
# zfs list
NAME                                      USED  AVAIL  REFER  MOUNTPOINT
rails_dev                                 617M  1.35G    19K  /rails_dev
rails_dev/zone1                           617M  1.35G   617M  /rails_dev/zone1
rails_dev/zone1@pre-boot                     0      -   617M  -
socialytics_zones                         825M  8.98G    19K  /socialytics_zones
socialytics_zones/soc_complete            824M  8.98G   799M  /socialytics_zones/soc_complete
socialytics_zones/soc_complete@pre-boot  25.0M      -   540M  -

Ok, let’s get this machine configured:

# zoneadm -z railsdev1 boot
# zlogin -e @ -C railsdev1
[Connected to zone 'railsdev1' console]

You go through the standard Solaris initialization prompts here. I chose the pretty typical options. Have your DNS servers handy. I can never get it to just find the DNS from the network. After the entire process you should get:

System identification is completed.
rebooting system due to change(s) in /etc/default/init

So, it will reboot and then you see the login:

SunOS Release 5.11 Version snv_87 64-bit
Copyright 1983-2008 Sun Microsystems, Inc.  All rights reserved.
Use is subject to license terms.
Hostname: railsdev1
Reading ZFS config: done.
railsdev1 console login: root
Password:

How cool is that? A new server running on the host. After you login, make sure the network actually got configured correctly:

# ping -s reddit.com
PING reddit.com: 56 data bytes
64 bytes from 64-215-156-99.eosinc.net (64.215.156.99): icmp_seq=0. time=20.136 ms
64 bytes from 64-215-156-99.eosinc.net (64.215.156.99): icmp_seq=1. time=22.219 ms
64 bytes from 64-215-156-99.eosinc.net (64.215.156.99): icmp_seq=2. time=21.403 ms

I figure if we can see Reddit we’re doing just fine.

Logout of the console, go back to the host, and see that the zone is running:

# @.
[Connection to zone 'railsdev1' console closed]
# zoneadm list -v
ID NAME             STATUS     PATH                           BRAND    IP
0 global           running    /                              native   shared
2 railsdev1        running    /rails_dev/zone1               native   shared

This server is now running on your network. Now go back to it and create a non-su user:

useradd mattc (with all the typical flags you like for home directories, passwords, etc.)

Rails

Now let’s make sure Ruby is there.

$ ruby --version
ruby 1.8.6 (2007-09-23 patchlevel 110) [i386-solaris2.11]

Let’s install rubygems:

$ wget http://rubyforge.org/frs/download.php/35283/rubygems-1.1.1.tgz
wget: not found

Uh oh, wget isn’t there. It is, but we just don’t have it in our path. Let’s fix that by making this our .profile:

PATH=/opt/csw/bin:$PATH
export PATH
$ . ./.profile

Now let’s install ruby gems:

$ wget http://rubyforge.org/frs/download.php/35283/rubygems-1.1.1.tgz
--2008-06-19 09:39:56--  http://rubyforge.org/frs/download.php/35283/rubygems-1.1.1.tgz
Resolving rubyforge.org... 205.234.109.19
Connecting to rubyforge.org|205.234.109.19|:80... connected.
HTTP request sent, awaiting response... 302 Found
Location: http://rubyforge.rubyuser.de/rubygems/rubygems-1.1.1.tgz [following]
--2008-06-19 09:39:56--  http://rubyforge.rubyuser.de/rubygems/rubygems-1.1.1.tgz
Resolving rubyforge.rubyuser.de... 80.237.222.133
Connecting to rubyforge.rubyuser.de|80.237.222.133|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 423308 (413K) [application/x-tgz]
Saving to: `rubygems-1.1.1.tgz'
100%[=======================================>] 423,308      189K/s   in 2.2s
 
2008-06-19 09:39:59 (189 KB/s) - `rubygems-1.1.1.tgz' saved [423308/423308]
 
$ gtar -xzf *.tgz
 
# ruby setup.rb
install -c -m 0644 ubygems.rb /usr/ruby/1.8/lib/ruby/site_ruby/1.8/ubygems.rb
install -c -m 0644 rubygems/server.rb /usr/ruby/1.8/lib/ruby/site_ruby/1.8/rubygems/server.rb
/usr/ruby/1.8/lib/ruby/1.8/fileutils.rb:1262:in `initialize': Read-only file system - /usr/ruby/1.8/lib/ruby/site_ruby/1.8/rubygems/server.rb (Errno::EROFS)
from /usr/ruby/1.8/lib/ruby/1.8/fileutils.rb:1262:in `open'
from /usr/ruby/1.8/lib/ruby/1.8/fileutils.rb:1262:in `copy_file'
from /usr/ruby/1.8/lib/ruby/1.8/fileutils.rb:1261:in `open'
from /usr/ruby/1.8/lib/ruby/1.8/fileutils.rb:1261:in `copy_file'
from /usr/ruby/1.8/lib/ruby/1.8/fileutils.rb:463:in `copy_file'
from /usr/ruby/1.8/lib/ruby/1.8/fileutils.rb:844:in `install'
from /usr/ruby/1.8/lib/ruby/1.8/fileutils.rb:1395:in `fu_each_src_dest'
from /usr/ruby/1.8/lib/ruby/1.8/fileutils.rb:1411:in `fu_each_src_dest0'
from /usr/ruby/1.8/lib/ruby/1.8/fileutils.rb:1393:in `fu_each_src_dest'
from /usr/ruby/1.8/lib/ruby/1.8/fileutils.rb:840:in `install'
from /usr/ruby/1.8/lib/ruby/1.8/fileutils.rb:1527:in `install'
from setup.rb:113
from setup.rb:108:in `each'
from setup.rb:108
from setup.rb:105:in `chdir'
from setup.rb:105

Uh oh, it’s a read only file system. Remember that Zones are by default using a shared, read-only /usr file system. That turns out to be a hassle, no two ways about it. It significantly lessened the fun of this venture.

Let’s first create a place for it in our home directory, by simply creating an environment variable:

$ cat .profile
PATH=.:/opt/csw/bin:$PATH
export PATH
PREFIX=$HOME/ruby
GEM_HOME=$PREFIX/lib/ruby/gems/1.8
export GEM_HOME
RUBYLIB=$PREFIX/lib/ruby:$PREFIX/lib/site_ruby/1.8
export RUBYLIB

Now we install, and you’ll see that the setup routine creates the paths as necessary:

$ ruby setup.rb --prefix=$PREFIX
mkdir -p /export/home/mattc/ruby/lib
mkdir -p /export/home/mattc/ruby/bin
install -c -m 0644 ubygems.rb /export/home/mattc/ruby/lib/ubygems.rb
mkdir -p /export/home/mattc/ruby/lib/rubygems
...
------------------------------------------------------------------------------

RubyGems installed the following executables:
/export/home/mattc/ruby/bin/gem

At this point I went through an hour of struggles, with errors like:

no such file to load -- rubygems/exceptions

I fixed this by simply doing an update on the host’s gems:

# gem update --system

which fixed it. Honestly it’s the little stuff.

Ok now let’s install Rails.

$ gem install rails --include-dependencies
INFO:  `gem install -y` is now default and will be removed
INFO:  use --ignore-dependencies to install only the gems you list
Bulk updating Gem source index for: http://gems.rubyforge.org/
Bulk updating Gem source index for: http://gems.rubyforge.org/
Successfully installed rake-0.8.1
Successfully installed activesupport-2.1.0
Successfully installed activerecord-2.1.0
Successfully installed actionpack-2.1.0
Successfully installed actionmailer-2.1.0
Successfully installed activeresource-2.1.0
Successfully installed rails-2.1.0
7 gems installed
Installing ri documentation for rake-0.8.1...
Installing ri documentation for activesupport-2.1.0...
Installing ri documentation for activerecord-2.1.0...
Installing ri documentation for actionpack-2.1.0...
Installing ri documentation for actionmailer-2.1.0...
Installing ri documentation for activeresource-2.1.0...
Installing RDoc documentation for rake-0.8.1...
Installing RDoc documentation for activesupport-2.1.0...
Installing RDoc documentation for activerecord-2.1.0...
Installing RDoc documentation for actionpack-2.1.0...
Installing RDoc documentation for actionmailer-2.1.0...
Installing RDoc documentation for activeresource-2.1.0...

Ok, can we run rails now?

$ rails
rails: not found

Nope, need to add to path. Here’s what the profile says afterwards:

PATH=.:$HOME/bin:$HOME/ruby/bin:$PREFIX/lib/ruby/gems/1.8/bin:/opt/csw/bin:$PATH

Ok, let’s create a test app:

$ rails testapp
create
create  app/controllers
...

And now it should run. I don’t have to tell you how excited I was as I typed in these keys:

$ cd testapp
$ script/server
> Booting WEBrick...
> Rails 2.1.0 application started on http://0.0.0.0:3000
> Ctrl-C to shutdown server; call with --help for options

And from my Mac:

Rails from the Mac
Yes! An unqualified success. The hard work pays off, a Heineken is opened, my hand rises to the air in triumph, and then I forget that I’m using Mongrel on this project. Well let’s just install Mongrel!

$ gem install mongrel -include-dependencies
Bulk updating Gem source index for: http://gems.rubyforge.org/
Building native extensions.  This could take a while...
ERROR:  Error installing mongrel:
ERROR: Failed to build gem native extension.
 
/usr/ruby/1.8/bin/ruby extconf.rb install mongrel -include-dependencies
creating Makefile
 
make
/opt/SUNWspro.40/SS11/bin/cc -I. -I/usr/ruby/1.8/lib/ruby/1.8/i386-solaris2.11 -I/usr/ruby/1.8/lib/ruby/1.8/i386-solaris2.11 -I. -DTEXT_DOMAIN=""  -I/builds2/sfwnv-gate/proto/root_i386/usr/sfw/include  -I/builds2/sfwnv-gate/proto/root_i386/usr/include    -I/builds2/sfwnv-gate/proto/root_i386//readline-5.2/include -KPIC -xO3 -xbuiltin=%all -xinline=auto -xprefetch=auto -xdepend  -KPIC -c fastthread.c
sh: /opt/SUNWspro.40/SS11/bin/cc: not found
*** Error code 1
make: Fatal error: Command failed for target `fastthread.o'

And that, dear friends, is where the fun kinda fizzled. I like Mongrel. I like Zed Shaw, and his rant remains at the same time the funniest and potentially most career limiting blog post I’ve ever read. But the fun stopped when I tried to get Mongrel running on OpenSolaris from within a zone.

The problem, to boil down a couple hours of frustration to one blog post section, is that Sun sets up the C compiler infrastructure in weird places, at least to the Fastthread and HTTPI gems that Mongrel uses. There are a few posts (here and here) out there on this, but I eventually ran out of steam. Besides I’d accomplished what I set out to do. Eventually my app ported to Rails 2.1 and ActiveScaffold 1.1.1 just fine, the client was happy, and I was (again) drinking Heineken.

Lessons

    1. ZFS and Zones are great. I didn’t really show it in this post but you can take snapshots, go to another machine, run several at once, you name it. Makes it pretty easy really.
    2. The read-only /usr path really gets annoying. I need to research a bit to find out how to most effectively circumvent that, without losing its benefits.
    3. I apparently would have made this easier on myself had I installed Sun Studio, which has the C compiler and the related tools.
    4. Somebody ought to do a package for Rails / Mongrel specifically for Zones. Maybe I will eventually.
    5. Always use the -e @ (or some other character) when doing zlogin via ssh. If you don’t you’ll have the standard ~. escape sequence, and that will exit the connection to the host machine.

I think the next post will get into my Python / Pylons zone I used for Socialytics. I’ve generally found that the packages and installation routines for Python-based tools are a bit more baked so lets give it a whack.

Read more from the Infrastructure, Rails category. If you would like to leave a comment, click here: . or stay up to date with this post via RSS, or you can Trackback from your site.

Leave a Comment

Name (required)

Email (required)

Website

Comments

1 Comment so far

  1. [...] I wrote about a Rails development container I kinda left off with “it all worked and everyone’s [...]