We recently had an interesting requirement surface. In anticipation of the release of a number of demo environments, our customer requested that system configuration be able to be done at the server level. The goal was to avoid being forced to use source control to manage configuration files.
A little background is in order. The systems that we are working on are pretty large. There are two distinct, decoupled systems, each consisting of between 10 and 40 subsystems. Of these subsystems, a handful on each side are Rails applications. Most others are small Ruby application that talk a central Rinda server.
We already have a configuration strategy in place for the two systems, which involves a central configuration file per system which populates a globally accessible configuration hash. The directory structure looks something like this:
The systemone/etc/config.rb
file creates the configuration hash and populates it with all of the appropriate configurations. In practice, config.rb
actually loads the appropriate configuration file based on an environment variable, i.e. RAILSENV
, but we will ignore that here for brevity and clarity:
From the context of a Rails application, this central configuration file is loaded from railsappone/config/environment.rb
:
With this configuration strategy in place already, the team was tasked with setting up multiple demo environments for the entire system, each on a different server and accessed via a different external URL.
We could have attempted to solve the configuration issues by just adding many separate Rails environments, say demo1 and demo2, but there were a couple problems with that.
First, the configurations for the different environments would have been nearly identical. Second, we wanted to avoid the complexity of relying on source control to manage the configurations for each deployment.
In most cases, relying on the Rails convention of setting up a new environment makes a lot of sense. However, the only differences between the environments would be setting the asset_host
and routes.
Storing this in source control means that in order to make a change to the external URL means that files under source control need to be modified, checked in and then redeployed to the affected system. It makes much more sense to have some reasonable defaults in source control and then provide a mechanism to override these configurations at the server level.
The solution to this problem ended up being quite simple. We first agreed on an acceptable location for the server configuration file:
In order to allow for overriding the system configuration, this code was added to /deployment/system_one/etc/config.rb
:
The server configuration in deployment/config/serverconfig.rb
will be loaded and executed, if it exists. If it doesn’t exist, the default configurations will be used, which is the desirable behavior.
The first task was to configure the assethost
. To start with, we add the desired assethost
configuration to /deployment/config/serverconfig.rb
:
For each Rails environment that we want the assethost to be configurable from the server configuration file, just add this line to the appropriate config file:
You would want to add a separate assethost
configuration for each Rails application, and each Rails application would set its own assethost
to the appropriate configuration.
For a simple configuration like asset_host
, this works great. For routes, though, it gets a bit more complicated. We need a Mapper instance in order to build a route. For example, your routes configuration looks something like this:
We initially saw two options.
- Create a data structure representing the desired routes, store it in the configuration, then from the routes file iterate through the structure created in the configuration file, creating the appropriate routes;
- Store some Ruby code in the configuration so that the context can be passed to the block at the run-time.
This second option eliminates the need for a secondary data structure to represent the routes. How better to configure the routes than with the actual code used to configure them?
It turned out that this was nearly as simple as the assethost
configuration. First, let’s define the routes in /deployment/config/serverconfig.rb
:
We have used the lambda method to convert a block into a Proc object. The Proc object is stored as a value in a hash which can be executed later. The rest should look familiar to anyone using Rails; it is exactly what we would have had in our routes file. Now we can do this:
In order to invoke the Proc object stored in serverconfig.rb
, we just send the call message to the Proc stored in the configuration hash, if it exists.
In our implementation, we invoke the Proc configured in serverconfig.rb
before all of the default routes, with the assumption that the configured routes should have the highest priority. If we run into a case where this isn’t the case, we can address that problem then.