Wednesday, May 23, 2007

Using Spring Framework to enable multi-environment deployment without rebuilding or repackaging.

Each software development project must inevitably deploy its artifacts at multiple points in its lifecycle. Often, these artifacts need to be deployed to multiple environments, such as development, integration, staging / qual, production (the number, naming & uses of these environments is beyond the scope of this article). Different environments will have different configurations; for example, in development there may be additional instrumentation for testing purposes that wouldn't be appropriate for a production environment. Or perhaps there are differing resource paths or other configuration values between environments.

A common pattern for accomplishing this has been a 'search and replace' during the build process (hopefully automated using Ant or an equivalent!). There are some drawbacks to this approach:
  1. The artifacts you are testing in one environment are not *identical* to the (supposedly same) artifacts in another environment. Since the artifacts have been rebuilt / repackaged, this opens up a 'risk-window' during which something could go wrong. I've personally had this occur when a developer unknowingly built the production release using Java 5 (the application was 1.4 at the time), resulting in a botched production deployment even though testing was successful across all other environments.
  2. A 'search and replace', usually being file & text-based, can be fragile and prone to errors. Perhaps a new configuration file is added, without being added to the search pattern; perhaps there is some valid text that coincidentally matches a replacement pattern.
  3. On a large project with multiple environments, multiple builds can be time and space consuming, essentially multiplying your time/space needs by the number of environments.
  4. Replacement values are now 'hard-coded' into the deployed artifacts, and cannot be adjusted without rebuilding and redeploying (and the inherent risk therein).
The alternate approach documented here is to extend Spring's PropertyPlaceholderConfigurer, introducing the concept of a 'runtime environment' - the RuntimeEnvironmentPropertyPlaceholderConfigurer. At application startup time, the runtime environment is determined using a RuntimeEnvironmentKeyResolver implementation (the default is to look for a system property); from this, the corresponding runtime-specific properties files are located and loaded.

Here is a sample bean definition (borrowed from PropertyPlaceholderConfigurer's JavaDoc):
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${jdbc.driver}" />
<property name="url" value="${jdbc.url}" />
</bean>
...and the addition of a RuntimeEnvironmentPropertyPlaceholderConfigurer bean:
<util:set id="runtimeEnvironments">
<value>production</value>
<value>staging</value>
<value>integration</value>
<value>development</value>
</util:set>

<bean class="digitalascent.framework.support.environment.RuntimeEnvironmentPropertyPlaceholderConfigurer">
<property name="propertyLocation" value="/WEB-INF/runtime-properties/" />
<property name="environments" ref="runtimeEnvironments"/>
<property name="defaultEnvironment" value="development" />
</bean>

Now the properties 'jdbc.driver' and 'jdbc.url' can be defined in the environment specific properties files: WEB-INF/runtime-environment/development.properties, integration.properties, staging.properties and production.properties.

By default, the implementation looks to the system property 'runtime.environment' for the environment key (e.g. -Druntime.environment=production); this property name can be changed (its a property of the bean), or for more advanced uses a custom RuntimeEnvironmentKeyResolver can be wired to to resolve the runtime environment.

The RuntimeEnvironmentPropertyPlaceholderConfigurer also, by default, enables overriding of any properties by system properties; this allows for 'tweaking' the configuration post-deployment, without having to rebuild and redeploy. In the above example, if the hostname of your database server needs to change, you can override the jdbc.url property by adding '-Djdbc.url=newUrlHere' to the relevant startup script.

The source code can be found attached to the Spring JIRA enhancement request.

Some points to note:
  • Source is based on Java 5+; it should be trivial to back-port to Java 1.4 if necessary;
  • The examples are based on Spring 2.0; there is nothing in the code that should prevent use on Spring 1.2.x;
  • If your application has multiple Spring contexts (say a web context and a servlet context), you will need to define the RuntimeEnvironmentPropertyPlaceholderConfigurer bean in each context
Thanks goes to:
  • Peter Monks for providing the use-case / inspiration for the RuntimeEnvironmentKeyResolver;
  • Members of the Spring community for feedback (forum post)
  • Digital Ascent for sponsoring the development of this solution

5 comments:

Anonymous said...

I've implemented the exact same concept some 20 months ago (just checked svn), and it has proven to be really helpful. Too bad I missed the discussion on the forum!

The only drawback is that all settings are accessible wherever the war file is deployed. Support for encrypted property values would probably be nice (haven't come around to do this myself).

Anyway, glad to see this!

Cheers,
-Ralph.

Chris Lee said...

Glad to see that others are finding this concept useful as well!

Recent modifications to Spring's PropertyPlaceholderConfigurer provide support for processing property values (e.g. decrypting them) via the convertPropertyValue method - see http://www.springframework.org/docs/api/org/springframework/beans/factory/config/PropertyPlaceholderConfigurer.html

Clint said...

Great idea Chris. I've posted the specification for an alternative approach here.

acme said...

Chris,

This looks like a excellent solution, and I'm trying to get it set up.

However, how can I deal with multiple hibernate config files? Specify different file names (with everything copied except the few db connect properties)? Do you know of a way to override or include hibernate properties, specified in the standard config file (loaded with LocalSessionFactoryBean)?

Thanks

Chris Lee said...

acme,

I've never tried this myself, but presumably you could define multiple LocalSessionFactoryBeans (with lazy="false" on each) and then reference the appropriate one as a property placeholder.