«    »

Designing for Deployability

In my previous article Architecting for Deployability, I wrote about the importance of deployability - how reliably and easily software can be deployed from development into the production environment. To accomplish this, one approach I recommended was to encapsulate differences between environments to isolate them from the majority of the application, and thus simplify deployment. This is assuming, of course, that these differences cannot be eliminated. The technology (language and platform) you are using and the type of environmental difference you need to deal with will influence the specific techniques available to manage the difference.

Differences between environments that you are likely to encounter are:

  • Database connections
  • Third-party service connections (i.e. for a web service, or naming service)
  • Logging settings
  • Performance tuning settings (i.e. database storage options)
  • Security settings (i.e. user ids, passwords)
  • Directory paths (i.e. for installed programs or libraries)

Based on my experience, there are three design approaches to dealing with these environmental differences. They are discussed below in the order in which I feel they should be used: i.e. only use the second approach if the first does not work out, and only use the third when the first two approaches are not appropriate.

Use support provided by the platform - but only when it makes sense

The platform you are basing your development on - whether an application server, operating system, or set of language libraries - may have built-in support for dealing with certain environmental differences. Rather than building your own solution (which the other two approaches cover), it is often easiest to use the provided functionality. I have a number of examples involving a variety of platforms, including an example that shows why you should not necessarily use the built-in support if it has been badly designed.

The Java Enterprise Edition (Java EE) platform provides several options for dealing with environmental differences. One of the essential pieces is JNDI - the Java Naming and Directory Interface, which provides an environmentally-neutral way to lookup basically anything, ranging from simple strings to fully configured services. JNDI works great for looking up a DataSource as per the following sample code:

Context rootContext = new InitialContext();

String jndiDataSourcePrefix = "java:/"; // Varies by application server 
String dataSourceName = "myDataSource";
DataSource dataSource = (DataSource) rootContext.lookup(
  jndiDataSourcePrefix + dataSourceName);

This not only hides the environment-specific details concerning the actual database connection, but also hides details concerning connection pooling which often vary between environments as well. The specifics concerning the database connection and connection pooling are specified within the application server for each environment. The code can therefore be promoted between environments without change.

Java EE also allows for simple strings to be stored in JNDI essentially like environment variables. The retrieval of these values is much like the retrieval of a DataSource as the following code shows:

Context rootContext = new InitialContext();
Context envContext = (Context) rootContext.lookup("java:comp/env");
String supportEmail = (String) envContext.lookup("support.email");

Unfortunately, this approach is not well suited to handling environmental differences. The values for such variables are specified in the deployment descriptor - ejb-jar.xml for EJBs or web.xml for web applications - as shown below:

<env-entry>
  <description>Support Email Address</description>
  <env-entry-name>support.email</env-entry-name>
  <env-entry-type>java.lang.String</env-entry-type>
  <env-entry-value>support@company.com</env-entry-value>
</env-entry>

The problem is that the deployment descriptor also contains important configuration information that does not vary per environment. Worse, the deployment descriptor file is bundled into the EJB jar file or WAR file for deployment. So how exactly can you specify different values for environment variables, without complicating your deployment process? You cannot.

The deployment descriptor represents a good idea but a flawed design. The creators of the Java EE specification envisioned multiple roles, including not just a developer role, but also an application assembler role and a deployer role. The idea was that the developer could specify the default value for the environment variable, and it could be overridden by either the assembler or deployer. However, the specification does not specify an easy way to do this override, and implies that the assembler or deployer would have to directly modify the deployment descriptor or use the proprietary administration interface of the application server as is necessary for configuring datasources and connection pools. The other flaw with this design is the idea of these separate roles. In practice, the developers are also the assemblers and often the deployers as well. When there are separate individuals doing the deployment, they typically know nothing about the application and could only override settings based on instructions provided by the developers.

The Java EE platform provides good support for data sources, but other types of environmental differences are really not supported well. I recommend not using the environment variable mechanism provided in Java EE, as it does not address the requirement of easily deployable software.

My next example involves Ruby on Rails, a web application framework for the Ruby language that has been receiving a lot of attention and hype in the last few years. One of the attractions of Rails is that it provides built-in support for many of the common features of web applications, including handling of environmental differences. Rails explicitly defines the notion of an environment via the RAILS_ENV environment variable, and even comes pre-configured with three: development, test, and production. Specifying environment-specific settings is extremely simple. There is a common file shared between environments (config/environment.rb), and one configuration file per environment (config/environments/<env>.rb). Since the configuration files are Ruby scripts, any type of setup can be done - you are not limited as in the case of Java EE deployment descriptor XML files. Rails is a clear winner over Java EE when it comes to deployability for multiple environments.

Parameterize and lookup differences by environment

If the platform you are using does not provide the support you need for multiple environments, then the next design approach is to implement your own support for multiple environments within the platform. While the specific mechanisms can vary, conceptually such support requires a parameter representing the environment (like the RAILS_ENV environment variable), and a lookup mechanism to retrieve environment-specific values based on this parameter (like the config/environments/*.rb files in Rails). The lookup mechanism can be as simple as settings in a property file named after the environment, or can involve properties in a database table keyed by the environment. A more sophisticated mechanism is to use a configuration interface with a factory to create the appropriate environment-specific subclass based on the environment. This allows for any type of setup to be implemented, rather than being limited to strings or other primitive types.

The catch with using this approach is that the parameter representing the environment must be handled using whatever support the platform provides for dealing with environmental differences. For a Java EE application this is not a big deal - there are several different options I have used. One approach is to store the environment in a special database table. Since Java EE handles the data source fine, the code can retrieve the data source and then query the environment table to determine the environment. Another approach is to use a Java system property or even an environment variable like Rails does. A third approach I have used is to have an environment specific directory (i.e. config/prod/) holding one or more property files or other resources. The classpath is defined within each application server to include the appropriate environment-specific directory. The code simply loads the property files or other resources from the classpath, which resolves correctly to the desired version of the files. This works especially well for log4j configuration files.

For platforms that provide absolutely no support for handling environmental differences, this design approach will not work. That is when the third approach becomes useful.

Generate per environment

This design approach is most appropriate when the platform provides no support for handling environmental differences. The most common example of this I have encountered is database DDL statements, which can have environment-specific storage settings but do not support variables that would allow one to parameterize these settings. If you want to fully script database changes, then it is necessary to have these DDL scripts be environmentally neutral. The solution is to use file generation to produce a different version of the script for each environment from a template using the appropriate values to substitute into the template for each environment.

I provide a software utility called EnvGen on my Software page that performs environment-specific file generation. EnvGen is an Ant task for generating different versions of the same file parameterized for different environments (i.e. development, test, and production). File generation is done using FreeMarker, a template engine with a full-featured templating language. You specify environment-specific properties in a CSV file (comma separated value spreadsheet). You can read more about EnvGen in the EnvGen Release Documentation.

For all of these approaches, no matter what language or platform you are using, the underlying concept remains the same: separate the settings and code that changes across environments from that which remains the same to achieve the goal of reliably and easily promoting the code into the production environment.

If you find this article helpful, please make a donation.

«    »