Deploying Ruby in Windows Azure

You probably know already that the Windows Azure Platform supports many types of technologies; we often mention Java or PHP in that regard, but today I would like to show you how to deploy a basic Ruby application in Windows Azure. This will also give us the opportunity to see a few utilities and tips & tricks around application deployment automation in Windows Azure Roles using Startup Tasks.

First, a quick word about Windows Azure Platform support in Ruby in general: I am going to use the Open Source WAZ-Storage library that allows you to access Windows Azure Blobs, Tables & Queues from your Ruby code. This will allow us both to run our Ruby application in the cloud, and leverage our non-relational storage services.

In order to start with the most compact application possible, I am going to use the Sinatra framework that allows very terse Web application deveopment; here the small test application I am going to deploy, it consists in a single Ruby file:

 require 'rubygems'
require 'sinatra'

require 'waz-blobs'

require 'kconv'
get '/' do
 WAZ::Storage::Base.establish_connection!(:account_name => 'foo', :access_key => 'bar', :use_ssl => true)
    c = WAZ::Blobs::Container.find('images')
 blobnames = c.blobs.map { |blob| blob.name }
 blobnames.join("<br/>")
end

Here are a few key points of this superb application:

  • It is a test application that will run within Sinatra’s “development” configuration, and not a real production configuration… But this will do for our demonstration!
  • The WAZ-Storage library is very easy to use: just change the “foo/bar” in establish_connection() with your Storage account and key, then choose an existing container name for the call to Container.find()
  • The code will assemble the list of Blob names from the container into a character string that will be displayed in the resulting page
  • The WAZ-Storage library of course exposes a way more complete API, that allows creation, modification, destruction of Blobs and Containers… You can go to their Github to download the library, and you are of course welcome to contribute ;-)

Let’s see how we are going to prepare our environement to execute this application in Windows Azure.

We are going to use a Worker Role to execute the Ruby interpreter. The benefit of using the Ruby interpreter and the Sinatra framework is that their impact on the system is very light: all you need to do is install the interpreter, download a few Gems, and you will have everything you need. It’s a perfect scenario to use Startup Tasks in a Worker Role, a feature that will allow us to customize our environment and still benefit from the Platform As A Service (PAAS) features, like automatic system updates & upgrades.

I have created a new project of type Cloud Service in Visual Studio 2010, where I added a Worker Role. I then added a “startup” directory in the Worker Role project, where I will store all the files required to install Ruby. It looks like this:

image

Let’s see what all these files do:

ruby-1.9.2-p180-i386-mingw32.7z is of course the Ruby interpreter itself. I chose the RubyInstaller for Windows version, which is ready to use. I chose the 7-Zip version instead of the EXE installer, because it will just make operations simpler (all you need to do is extract the archive contents). I could also dynamically download the archive (see below), but since it’s only 6 MB, I finally decided to include it directly in the project.

test.rb is of course my Ruby “application”.

curl is a very popular command-line utility, that allows you to download URL-accessible resources (like files via HTTP). I am not using it for the moment, but it can be very useful for Startup Tasks: you can use it to download the interpreter, or additional components, directly from their hosting site (risky!) or from a staging area in Blob Storage. It is typically what I would do for bigger components, like a JDK. I downloaded the Windows 64-bit binary with no SSL.

7za is of course the command-line version of 7-Zip, the best archiver on the planet. You will find this version on the download page.

WAUtils will be used in the startup script to find the path of the local storage area that Windows Azure allocated for me. This is where we will install the Ruby interpreter. There is no way to “guess” the path of this area, you need to ask the Azure runtime environment for the information. The source code for WAUtils is laughably simple, I just compiled it in a separate project and copied the executable in my “startup” folder.

 namespace WAUtils
{
    class Program
    {
        static void Main(string[] args)
        {
            if (args.Length < 2)
            {
                System.Console.Error.WriteLine("usage: WAUtils [GetLocalResource <resource name>|GetIPAddresses <rolename>]");
                System.Environment.Exit(1);
            }
            switch (args[0])
            {
                case "GetLocalResource":
                    GetLocalResource(args[1]);
                    break;
                case "GetIPAddresses":
                    break;
                default:
                    System.Console.Error.WriteLine("unknown command: " + args[0]);
                    System.Environment.Exit(1);
                    break;
            }
        }
        static void GetLocalResource(string arg)
        {
            LocalResource res = RoleEnvironment.GetLocalResource(arg);
            System.Console.WriteLine(res.RootPath);
        }
    }
}

As you can see, I use the RoleEnvironment.GetLocalResource() method to find the path to my local storage.

Let’s now have a look in my solution’s ServiceDefinition.csdef; this is where I will add this local storage definition, called “Ruby” for which I asked 1 GB.

 <LocalResources>
  <LocalStorage name="Ruby" cleanOnRoleRecycle="false" sizeInMB="1024" />
</LocalResources>

I will also add an Input Endpoint, to ask the Windows Azure network infrastructure to expose my loca port 4567 (Sinatra’s default) to the outside world as port 80.

 <Endpoints>
  <InputEndpoint name="Ruby" protocol="tcp" port="80" localPort="4567" />
</Endpoints>

I will then add two Startup Tasks:

 <Startup>
  <Task commandLine="startup\installruby.cmd" executionContext="elevated" />
  <Task commandLine="startup\startruby.cmd" taskType="background" />
</Startup>
  • installruby.cmd will execute with Administrator rights (eve if it is not scritcly required in this case… but this is typically what you would use for system configuration tasks)
  • startupruby.cmd will start the Ruby interpreter, will not use Administrator rights (for security reasons), and will run in “background” mode, which means that the Worker Role will not wait for the task completion to finish its startup procedure.

Let’s now see these two scripts!

First, installruby.cmd:

 cd %~dp0
for /f %%p in ('WAUtils.exe GetLocalResource Ruby') do set RUBYPATH=%%p

echo y| cacls %RUBYPATH% /grant everyone:f /t

7za x ruby-1.9.2-p180-i386-mingw32.7z -y -o"%RUBYPATH%"

set PATH=%PATH%;%RUBYPATH%\ruby-1.9.2-p180-i386-mingw32\bin

call gem install sinatra haml waz-storage --no-ri --no-rdoc

exit /b 0

Here are the steps:

  • We run WAUtils.exe to find the local storage path, and we save it in a variable called RUBYPATH
  • We use the Windows cacls.exe command to give all right to this directory
  • We extract the 7-Zip archive using 7za, in the RUBYPATH directory
  • We add Ruby’s “bin” sub-directory to the system PATH
  • Finally, we use the “gem” command to dynamically download the Ruby libraries we need

Now, startupruby.cmd that will run my application:

 for /f %%p in ('startup\WAUtils.exe GetLocalResource Ruby') do set RUBYPATH=%%p
set PATH=%PATH%;%RUBYPATH%\ruby-1.9.2-p180-i386-mingw32\bin

ruby test.rb

That’s it, I hope these guidelines will help you re-create your own Ruby service in Windows Azure!

And thanks to my colleague Steve Marx for inspiration & guidance :-)