Brock's Blog

Deploying RhoConnect to Multiple Environments - Part 1

| Comments

It’s relatively straightforward to configure RhoConnect for a single environment (i.e. production), but things get more complicated if you want to have a robust way to deploy to several environments (i.e. development, testing, production).

In this post I’ll show you how to do the following:

  1. Set up global and environment-specific configurations
  2. Configure RhoConnect to use a given environment’s settings

Global and Environment-Specific Configurations

First, create a global section (at the root level) within settings/settings.yml:

settings/settings.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
:sources:
  User:
    :poll_interval: 1
  Group:
    :poll_interval: 1

:global:
  :rhoadmin_password: 'admin'
  :log:
    :path: ./log/rhoconnect.log
    :mode: stdout
  :blob_storage_path: './blobs/'

:development:
  :licensefile: settings/license.key
  :redis: redis-dev:6379
  :syncserver: http://rhoconnect.dev.example.com/api/application/
  :log:
    :mode: file
:uat:
    :rhoadmin_password: 'uatadmin'
  :licensefile: settings/license.key
  :redis: redis-uat:6379
  :syncserver: http://rhoconnect.uat.example.com/api/application/
  :log:
    :mode: file
:production:
    :rhoadmin_password: 'prodadmin'
  :licensefile: settings/license.key
  :redis: redis-prod:6379
  :syncserver: http://rhoconnect.example.com/api/application/
  :log:
    :mode: file

:test:
    :licensefile: settings/license.key
    :redis: localhost:6379
    :syncserver: http://localhost:9292/api/application/

Put whatever custom configuration options you want in the global section; this will be used as the base upon which the environment-specific settings get applied.

Note: Any options used natively by the RhoConnect framework (i.e. licensefile, redis, syncserver, iphonecertfile, iphoneserver, raise_on_expired_lock, etc.) must explicitly reside in each environment’s section. This is because RhoConnect independently parses settings.yml and will not use the custom config util we’re going to create below.

Configure RhoConnect

There are two main options for configuring which environment a particular RhoConnect instance will target:

  1. Set the RACK_ENV environment variable (cleaner, but doesn’t work on RhoHub)
  2. Hard-code it in settings.yml (harder to manage, but works on RhoHub)

Option 1: Setting the RACK_ENV environment variable

Note: This will not work if you’re hosting your RhoConnect application on RhoHub. You must use option #2 in this case.

First, create initializers/hash_extension.rb (borrowed from here):

initializers/hash_extension.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Hash
  # Merges self with another hash, recursively.
  # 
  # This code was lovingly stolen from some random gem:
  # http://gemjack.com/gems/tartan-0.1.1/classes/Hash.html
  # 
  # Thanks to whoever made it.
  def deep_merge(hash)
    target = dup

    hash.keys.each do |key|
      if hash[key].is_a? Hash and self[key].is_a? Hash
        target[key] = target[key].deep_merge(hash[key])
        next
      end

      target[key] = hash[key]
    end

    target
  end
end

Second, create util/config_util.rb:

util/config_util.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
require File.expand_path("#{File.dirname(__FILE__)}/../initializers/hash_extension")
require 'yaml'

class ConfigUtil
  def self.load
    settings = YAML::load_file("#{File.dirname(__FILE__)}/../settings/settings.yml")
    env = ENV['RACK_ENV'].to_sym
    get_settings_for_environment(settings, env)
  end

  def self.get_settings_for_environment(settings_yaml, env)
    # load and merge all global and env-specific settings from the given settings yml
    environment_settings = settings_yaml[env]
    raise "Environment #{env} not found" unless environment_settings
    config = settings_yaml[:global].deep_merge(environment_settings)
    config[:sources] = settings_yaml[:sources]
    config
  end
end

CONFIG = ConfigUtil.load unless defined?(CONFIG)

Lastly, add a line to the top of application.rb:

Snippet - application.rb
1
require "#{ROOT_PATH}/util/config_util"

Now that we’ve got all of the required code, the last step is to set the RACK_ENV environment variable accordingly. While developing and using rhoconnect’s built-in rake tasks, you would do something like this to use the uat environment:

1
$ RACK_ENV=uat rake rhoconnect:start

In your actual deployed environments, typically the best place to specify environment variables is in the Apache/Nginx config. I like to use Phusion Passenger on top of nginx to host RhoConnect, for which I’d add the rack_env configuration directive provided by passenger. Let’s say we wanted RhoConnect to use settings from the uat section in settings.yml:

Snippet - nginx.conf
1
2
3
4
5
6
7
8
9
server {
  listen 80 default_server;
  server_name _;
  server_name_in_redirect on;
  root /Users/brock/Projects/RhoConnect/playground/public;
  passenger_enabled on;
  passenger_spawn_method conservative;
  rack_env uat;
}

There’s a similar directive if you’re using Phusion Passenger on top of Apache called RackEnv (which I again set to target ‘uat’ below):

Snippet - httpd.conf
1
2
3
4
5
6
7
8
9
10
11
<VirtualHost *:80>
  ServerName rhoconnect
  DocumentRoot /Users/brock/Projects/RhoConnect/playground/public
  RackEnv uat
  <Directory /Users/brock/Projects/RhoConnect/playground/public>
      Options FollowSymLinks
      AllowOverride None
      Order allow,deny
      Allow from all
  </Directory>
</VirtualHost>

If you’re using something other than Passenger to host RhoConnect, it should support setting environment variables for your application; a quick google search should help you there. All you need to do is set the RACK_ENV environment variable to your desired deployment environment (development, uat, production, etc.) so that it’s included in the environment at runtime of the process hosting the RhoConnect application.

Option 2: Hard-code in settings.yml

This will come soon in Part 2 of this series.

Using the CONFIG variable

If you look closely at config_util.rb that we create earlier, you’ll see that it sets a constant named CONFIG. This is a hash that has the configuration keys/values of the section in settings.yml corresponding to the RACK_ENV environment variable merged on top of the values of the global configuration section.

Here’s an example to explain this a bit further:

Assumptions

  • You have a settings.yml similar to that described above
  • The RACK_ENV environment variable is set to uat

With these assumptions, the CONFIG global, accessible from anywhere in your application, will be a hash with the following contents:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
  :licensefile => "settings/license.key",
  :blob_storage_path => "./blobs/",
  :syncserver => "http://rhoconnect.uat.example.com/api/application/",
  :redis => "redis-uat:6379",
  :rhoadmin_password => "uatadmin",
  :log => {
    :path => "./log/rhoconnect.log",
    :mode => "file"
  },
  :sources => {
      "User" => {
          :poll_interval => 1
      },
      "Group" => {
          :poll_interval => 1
      }
  }
}

This represents the contents of the global configuration section with the uat configuration section merged on top of it. Notice that the log:mode configuration value is set to “file”; the global section specifies log:mode to be “stdout”, but that was overridden by the uat configuration section with a log:mode value of “file”. Same thing for rhoadmin_password.

A quick example of how you could use this would be to initialize the rhoadmin password to the current environment’s rhoadmin_password setting (this defaults to blank):

Snippet - application.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
require "#{ROOT_PATH}/util/config_file"

class Application < Rhoconnect::Base
  class << self
    def authenticate(username,password,session)
      # your authentication code here
    end

    def initializer(path)
      super
      # load the existing rhoadmin user, create it if it doesn't exist yet
      admin = User.load('rhoadmin') || User.create({:login => 'rhoadmin', :admin => 1})
      # set rhoadmin's password to the configured value
      admin.password = CONFIG[:rhoadmin_password] || ''
    end

    def store_blob(object,field_name,blob)
      # your blob sync code here
      super
    end
  end
end

Application.initializer(ROOT_PATH)

In the above snippet, the initializer method would set the rhoadmin password to ‘uatadmin’ for our example scenario.

Notice that we didn’t use something like CONFIG[:uat][:rhoadmin_password]; this would be terrible practice as we would be hard-coding a specific environment in our application and throwing away the flexibility of specifying the target environment at a higher level (i.e. the RACK_ENV environment variable).

Wrap-up

Now you can hopefully see how useful this can be. You can use the CONFIG constant anywhere in your application to get access to environment-scoped configuration values, which are easily maintained within the existing settings.yml file used by RhoConnect.

Coming Soon: Part 2, which will go into how you can get a similar setup when you don’t have control over environment variables (i.e. on RhoHub).

Comments