Back when Brian and I first started this blog, I wrote about several methods for deploying Ruby applications to Windows Azure. There’s a new way to deploy that I wanted to cover before continuing on the testing Ruby Applications on Windows Azure, as this new deployment method is going to be the basis of the next few posts in this line.
During the fall, Windows Azure added a new feature that allowed you to specify the entry point for a role in the ServiceDefinition.csdef file. This means that you could directly specify the executable or script that you wanted to run instead of having to build a .NET wrapper. Steve Marx created an example of using this to run Python on Windows Azure (https://github.com/smarx/pythonrole,) which I’ve adapted into an example for running Ruby on Windows Azure. You can find the example project at https://github.com/Blackmist/rubyrole.
One slight drawback to this example; it requires access to a Windows machine. This is because it relies on utilities provided as part of the Windows Azure SDK. Any version of the SDK listed should probably work, but I’ve only tested with the “Other” and “.NET” versions of the SDK.
Here’s a brief overview of the how this sample works.
The ServiceDefinition file defines the ports used by this role, per-instance file storage, custom environment variables, startup tasks, and finally the command to run as the program entry point for this role. Specifically, it does the following:
- Defines a public TCP port of 80, which is named ‘HttpIn’
- Defines per-instance local storage named ‘ruby’
- Defines the following startup tasks:
- installRuby.cmd - installs Ruby from RubyInstaller.org
- installDk.cmd - installs DevKit from RubyInstaller.org
- installDependencies.cmd - installs Bundler and then runs ‘bundle install’ to install any gems listed in Gemfile.
- Defines the application entry point - run.cmd
Note that the .cmd files specified all live in the \WorkerRole subdirectory of this sample. The path isn’t specified in the ServiceDefinition file entries.
The ‘HttpIn’ and ‘ruby’ resources allocated in this file are normally only accessible to .NET applications, however we can expose them as environment variables. ‘HttpIn’ is queried and used to create the ADDRESS and PORT environment variables, while the full physical path to the storage allocated for ‘ruby’ is stored in the RUBY_PATH environment variable. There’s also an EMULATED environment variable, which is used to determine if the project is running in the Windows Azure Emulator or on Windows Azure.
During initialization, the role will allocate storage and name it ‘ruby’ and open up port 80 as specified by ‘HttpIn’. It will then start running the startup commands. After those have completed, it will run ‘run.cmd’ to launch the application.
Here's the interesting portions of the ServiceDefinition file. For a complete listing of the file see https://github.com/Blackmist/rubyrole/blob/master/ServiceDefinition.csdef.
<Endpoints> <InputEndpoint name="HttpIn" protocol="tcp" port="80" /> </Endpoints>
<LocalResources> <LocalStorage name="ruby" cleanOnRoleRecycle="true" sizeInMB="1000" /> </LocalResources>
InstallRuby.cmd Startup Task
Note that this defines not only the command line to run, but also the environment variables to create for this task.
<Task commandLine="installRuby.cmd" executionContext="elevated"> <Environment> <Variable name="EMULATED"> <RoleInstanceValue xpath="/RoleEnvironment/Deployment/@emulated" /> </Variable> <Variable name="RUBY_PATH"> <RoleInstanceValue xpath="/RoleEnvironment/CurrentInstance/LocalResources/LocalResource[@name='ruby']/@path" /> </Variable> </Environment> </Task>
RUBY_PATH Variable Definition
<Variable name="RUBY_PATH"> <RoleInstanceValue xpath="/RoleEnvironment/CurrentInstance/LocalResources/LocalResource[@name='ruby']/@path" /> </Variable>
Application Entry Point
<EntryPoint> <ProgramEntryPoint commandLine="run.cmd" setReadyOnProcessStart="true" /> </EntryPoint>
ServiceConfiguration.Cloud.csfg and ServiceConfiguration.Local.csfg
The ServiceConfiguration file specifies service configuration options such as the number of instances to create for your role. That’s all that I’ve really specified for now, however this file can also be used to enable remote desktop functionality for a deployment, as well as diagnostic configuration. Currently the only important value in here is Instances count, which is 2 for cloud and 1 for local.
Why 2 for the cloud? Because Microsoft’s SLA guarantee requires at least two instances of a role. If one is taken down due to hardware failure, resource balancing, etc. you still have another one running. Setting the count to 1 for local is so that we only spin up one instance when we test using the Windows Azure emulator on your local machine.
The cloud version of this file is used when you deploy to the cloud, while the local version is used by the emulator. Here's the cloud version as an example:
<?xml version="1.0"?> <ServiceConfiguration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" serviceName="RubyRole" xmlns="http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceConfiguration" osFamily="2" osVersion="*"> <Role name="WorkerRole"> <ConfigurationSettings /> <Instances count="2" /> </Role> </ServiceConfiguration>
The application is contained in the ‘/WorkerRole/app’ directory. For this sample it’s just a basic Sinatra application (app.rb) as follows:
require 'sinatra' set :server, :thin set :port, ENV['PORT'] set :bind, ENV['ADDRESS'] get '/' do "Hello World!" end
One important thing to note is that this uses the PORT and ADDRESS environment variables to determine the port and address to listen on. If you’ll look back at the ServiceDefinition.csfg file, these environment variables are populated there based on the 'HttpIn' endpoint. Using these two environment variables allows the application to run correctly based on whether it is ran in the emulator (which may remap the port if you already have something on port 80,) or in Windows Azure.
When using this, the typical workflow is:
- Create a new web application in the ‘/WorkerRole/app’ directory.
- Make any modifications needed to ‘/WorkerRole/run.cmd’ in order to launch the application. E.g ‘call rails s’ instead of ‘ruby app.rb’.
- Launch ‘run.cmd’ from the ‘/rubyrole’ directory. This will launch the application in the Windows Azure emulator.
- After everything is working as desired, run ‘pack.cmd’ from the ‘/rubyrole’ directory to create the ‘RubyRole.cspkg’ deployment package.
- Browse to Windows.Azure.com and login to your subscription, then create a new hosted service. Use the RubyRole.cspkg as the package file and ServiceConfiguration.Cloud.csfg as the configuration file.
@echo off if "%ServiceHostingSDKInstallPath%" == "" ( echo Can't see the ServiceHostingSDKInstallPath environment variable. Please run from a Windows Azure SDK command-line (run Program Files\Windows Azure SDK\^<version^>\bin\setenv.cmd^). GOTO :eof ) cspack ServiceDefinition.csdef /out:RubyRole.cspkg
@echo off if "%ServiceHostingSDKInstallPath%" == "" ( echo Can't see the ServiceHostingSDKInstallPath environment variable. Please run from a Windows Azure SDK command-line (run Program Files\Windows Azure SDK\^<version^>\bin\setenv.cmd^). GOTO :eof ) cspack ServiceDefinition.csdef /copyOnly /out:RubyRole.csx csrun RubyRole.csx ServiceConfiguration.Local.cscfg if "%ERRORLEVEL%"=="0" ( echo Browse to the port you see above to view the app. To stop the compute emulator, use "csrun /devfabric:shutdown" )
I'm not going to list the source for all the startup tasks here, but I do want to call out a specific thing to be aware of. This is that when you run the application using the run.cmd command to launch the application in the Windows Azure Emulator, it's actually using the local copy of Ruby installed on your machine, along with whatever gems you have installed. So it's important that these startup tasks be crafted so that they don't clobber your installs when ran in the emulator.
This is the purpose of the EMULATED environment variable; it will only exist when running in the emulator. Here's the source for installRuby.cmd, note that the first thing we do is check if EMULATED exists and exit to prevent from reinstalling Ruby on your local box.
REM Skip Ruby install if we're running under the emulator if "%EMULATED%"=="true" exit /b 0 REM Strip the trailing backslash (if present) if %RUBY_PATH:~-1%==\ SET RUBY_PATH=%RUBY_PATH:~0,-1% cd /d "%~dp0" REM Download directly from rubyinstaller.org powershell -c "(new-object System.Net.WebClient).DownloadFile('http://rubyforge.org/frs/download.php/75465/rubyinstaller-1.9.3-p0.exe', 'ruby.exe')" REM Install Ruby and DevKit start /w ruby.exe /verysilent /dir="%RUBY_PATH%" REM Ensure permissive ACLs so other users (like the one that's about to run Ruby) can use everything. icacls "%RUBY_PATH%" /grant everyone:f icacls . /grant everyone:f REM Make sure Ruby was installed properly (will produce a non-zero exit code if not) "%RUBY_PATH%\bin\ruby" --version
We’ve gotten to the point that we can deploy Ruby applications to Windows Azure without requiring Visual Studio .NET and a .NET wrapper, but we still require a Windows system for this approach. Hopefully we’ll have a solution for developing and deploying from non-Windows systems one day.
Next week, I’ll demonstrate how to use this sample to run Ruby tests in Windows Azure.