last edited byusericonadmin on 30-Apr-2010

Contents

Managing Multiple Environment Configurations

When developing an application you will most likely have a number of environments that your code will need to run in. For example, you may have a "development" environment, a "staging" (or "testing") environment and a live "production" environment. If each member of your team is developing an application on their own machine then they may each have their own specific environment requirements.

Let's take a look and how different environments may be handled using an object oriented approach.

Environment properties

Your application will have a standard set of environment "properties" that differ within each environment. This may include datasource details, file paths and domain names. You may also have a set of configuration properties that should be the same across all of your environments, such as a company name or application constants.

Let's assume that for each of your environments you need the following variable properties defined:

And assume that the common properties for your environments are:

There are many ways of storing this information, but a common technique is to use an XML file. Let's create a single XML file that contains details for all of the environments.

<environments>

    <common>
        <assetsUrl>/assets</assetsUrl>
        <documentsUrl>/documents</documentsUrl>
        <companyName>Your Company</companyName>
    </common>

    <development>
        <assetsPath>c:/data/workspace/yourcompany/assets</assetsPath>
        <documentsPath>c:/data/workspace/yourcompany/files/documents</documentsPath>
        <datasourceName>yourcompany_dev</datasourceName>
        <datasourceUser>yourcompany</datasourceUser>
        <datasourcePass>abc123</datasourcePass>
    </development>

    <staging>
        <assetsPath>/staging/sites/yourcompany/assets</assetsPath>
        <documentsPath>/staging/sites/yourcompany/files/documents</documentsPath>
        <datasourceName>yourcompany_staging</datasourceName>
        <datasourceUser>yourcompany</datasourceUser>
        <datasourcePass>def456</datasourcePass>
    </staging>

    <production>
        <assetsPath>/production/sites/yourcompany/assets</assetsPath>
        <documentsPath>/production/sites/yourcompany/files/documents</documentsPath>
        <datasourceName>yourcompany_live</datasourceName>
        <datasourceUser>yourcompany</datasourceUser>
        <datasourcePass>ghi789</datasourcePass>
    </production>

</environments> 

You'll notice a section called "common" which will be used to hold properties that are common to all environments. Each other environment gets its own section in the file. You can add as many different environments as you need.

There are many different ways of designing your XML (see references below), but this approach nicely lends itself to direct conversion to CFML struct, and can also supports nested values if required.

What objects do we need?

When planning objects you are looking to identify any things that may need to change over time and separate those things.

Firstly, the storage format of our environment data may change. The format of the XML file may change, or we may choose to move the environment information from an XML file into a component.

We will also need a way of identifying the environment we are currently running in. For example, initially we may start with using domain names to identify our environment. But if our application moves to a more complex environment with multiple ColdFusion instances in a cluster then we may need to use another technique such as storing the current environment name in a file on each server instance.

So at the very least we will create two objects:

EnvronmentIdentifier, which tells us the environment we are running in. It will hide (encapsulate) the environment identification technique, whether it be by domain name, file or other means.

Environment, which provides us with the properties relevant for the current environment. It will hide (encapsulate) how the environment properties are stored.

Designing an Environment object

Let's start by creating an Environment object:

Our environment object will know about all of the possible environments, so the use() function will tell it which environment values it should use. The property() function will just return the value of that property for the currently selected environment.

In this example, we are using an XML file for all of the environment details, so we will need to initialise our object with the file path of the environment XML file. Let's take a look at how we may use our environment object:

<!--- Create the environment object ---> 
<cfset envPath = expandPath("/config/environments.xml.cfm")>
<cfset environment = createObject("component","com.util.Environment").init( envPath )>

<!--- Tell it to use the "development" environment --->
<cfset environment.use("development")>

<!--- Display one of the environment properties --->
<cfoutput>
    #environment.property("datasourceName")#
</cfoutput>

Implementing an Environment object

Let's take a look at how we might implement our environment object.

First, we need to read the XML file and convert the contained data to a format that's easy to use. The format described above converts nicely into a struct using an XML to Struct utility by Anuj Gakhar.

So to read and convert our XML file to a struct we have:

<cffunction name="environmentXMLFileToStruct" output="false" access="private">
    <cfargument name="environmentFilePath" required="true">
    <cfset var xmlString = 0>
    <cfset var xmlUtil = 0>
    <cffile action="read" file="#arguments.environmentFilePath#" variable="xmlString">
    <cfset xmlUtil = createObject("component","com.util.xml2struct")>
    <cfreturn xmlUtil.convertXmlToStruct(xmlString,structNew())>
</cffunction>

This returns a struct that has the identical structure to the XML file above:

Environment Struct Dump

We will convert the XML file to a struct when we initialise our Environment.cfc object:

<cfset variables.instance = {}>
<cfset variables.instance.environments = 0>

<cffunction name="init" output="false">
    <cfargument name="environmentFilePath" required="true">
    <cfset var environments = environmentXMLFileToStruct(arguments.environmentFilePath)>
    <cfset variables.instance.environments = environments>
    <cfreturn this>
</cffunction>

Implementing the use() function is quite simple because the data for each environment is stored in it's own "sub" struct.

<cfset variables.instance.environment = 0>

<cffunction name="use" output="false">
    <cfargument name="environmentName" required="true">
    <cfset variables.instance.environment = variables.instance.environments[arguments.environmentName]>
</cffunction>

Lastly, getting a property is also quite easy to retrieve from the currently selected environment:

<cffunction name="property" output="false">
    <cfargument name="name" required="true">
    <cfreturn variables.instance.environment[arguments.name]>
</cffunction>

Lastly, we just need a little special handling of the "common" environment properties. These properties need to be added to all of the other environments, so we add a little extra code to the init() function:

<cffunction  name="init" output="false">
    <cfargument name="environmentFilePath" required="true">
    <cfset var envName = 0>
    <cfset var environments = environmentXMLFileToStruct(arguments.environmentFilePath)>

    <!--- Add the common properties to each of the other environments --->
    <cfif structKeyExists(environments,"common")>
        <cfloop item="envName" collection="#environments#">
            <cfif envName neq "common">
                <cfset structAppend(environments[envName],environments["common"],true)>
            </cfif>
        </cfloop>
    </cfif>
	
    <cfset variables.instance.environments = environments>
    <cfreturn this>
</cffunction>

This code has omitted a few details such as error handling, but the complete code is available further below.

Identifying our current environment

In each location where your code will execute there will need to be something present than can be used to decide what environment you are in. Some approaches that may be used include inspecting the domain name the code is running from, or perhaps inspecting the contents of a file that is different in each location.

Identifying the environment using a domain name

Let's first consider that you are using the domain name to determine your environment. Suppose you have three environments; development, staging and production that run on the following domains:

Let's create an object that identifies the environment you are running in. This will be called an "environment identifier" and provides only one function which returns a string representing the current environment.

When our DomainEnvironmentIdentifier is created we need to pass in the current "domain name". This is obtained from the cgi.SERVER_NAME variable.

<cfcomponent name="DomainEnvironmentIdentifier" output="false">

    <cfset variables.domainName = "">

    <cfset variables.map = {}>
    <cfset variables.map["yourapp.localhost"] = "development">
    <cfset variables.map["staging.yourapp.com"] = "staging">
    <cfset variables.map["www.yourapp.com"] = "production">

    <!--- INITIALISATION --->

     <cffunction name="init" output="false">
        <cfargument name="domainName" required="true">
        <cfset variables.domainName = arguments.domainName>
        <cfreturn this>
    </cffunction>

    <!--- PUBLIC FUNCTIONS --->

    <cffunction name="currentEnvironment" output="false">
        <cfif not structKeyExists(variables.map,variables.domainName)>
            <cfthrow message="Cannot determine the environment for domain name '#variables.domainName#'">
        </cfif>
        <cfreturn variables.map[variables.domainName]>
    </cffunction>

</cfcomponent>

This is quite a simple component with the single task of determining the environment based on the domain name. The object may be used as follows:

<cfset environmentIdentifier = createObject("component","DomainEnvironmentIdentifier").init(cgi.SERVER_NAME)>
<cfset environment = environmentIdentifier.currentEnvironent()>

You'll notice that the mappings for the domain names are stored within the object. For a larger application this mapping information could perhaps be moved to a separate file which is read in by the environment identifier.

Identifying the environment from a file

In more complicated environments, a domain name alone may not be enough to identify an environment. For example, suppose you have two clustered servers running your live production code, and some details are a little different on each server. The domain to access them will be the same, but the environment details may be slightly different.

In cases such as these we can place a file on each server that contains the name of the current environment.

CFML supports a file format called an "ini" file which is essentially made up of "section names" in square brackets, followed by name=value pairs. We can use this as a file format for our environment file:

[environment]
environment=production

So this shows a section called "environment" that contains an item named "environment" with the value "production". These types of files are called INI files and conventionally have a .ini file extension.

We can use the CFML function getProfileString(filePath,section,item) to read this value.

Let's create a FileEnvironmentIdentifier object that uses this file to identify the current environment.

When our FileEnvironmentIdentifier is created we need to pass in the path to the environment file.

<cfcomponent name="FileEnvironmentIdentifier" output="false">

    <!--- PRIVATE VARIABLES --->    

    <cfset variables.environment = "">

    <!--- INITIALISATION --->

     <cffunction name="init" output="false">
        <cfargument name="environmentFilePath" required="true">
        <cfset variables.environment = getProfileString(arguments.environmentFilePath, "environment", "environment")>
        <cfreturn this>
    </cffunction>
    
    <!--- PUBLIC VARIABLES --->

    <cffunction name="currentEnvironment" output="false">
        <cfreturn variables.environment>
    </cffunction>

</cfcomponent>

Using our component is very similar to the DomainEnvironmentIdentifier:

<cfset envPath = expandPath("/config/environment.ini.cfm")>
<cfset environmentIdentifier = createObject("component","FileEnvironmentIdentifier").init( envPath )>
<cfset environment = environmentIdentifier.currentEnvironent()>

Implementing an Environment object in your application

Your environment settings are something that typically only need to be created once in an application, so it's an ideal candidate for creating on application startup. Let's write an example of how the environment may be created on application startup within an Application.cfc file.

Using domain name environment identifier:

<cffunction name="onApplicationStartup" output="false">
    <cfset var identifier = createObject("component","DomainEnvironmentIdentifier").init( cgi.SERVER_NAME )>
    <cfset var environment = createObject("component","").init( expandPath("/config/environment.xml.cfm") )>
    <cfset environment.use( identifier.currentEnvironment() )>
    <cfset application.environment = environment>
</cffunction> 

Using file environment identifier:

<cffunction name="onApplicationStartup" output="false">
    <cfset var envPath = expandPath("/config/environment.ini.cfm")>
    <cfset var identifier = createObject("component","FileEnvironmentIdentifier").init( envPath )>
    <cfset var environment = createObject("component","Environment").init( expandPath("/config/environment.xml.cfm") )>
    <cfset environment.use( identifier.currentEnvironment() )>
    <cfset application.environment = environment>
</cffunction> 

Separation of environment properties from environment identification

In this design of environment objects we have separated out the environment properties from the environment identification. This means that either one is able to freely change with little to no impact on the other.

Further to this, the environment properties and environment identification code only exists in one place making this part of the application easy to change.

Securing your configuration files

In the examples above we made use of two configuration files:

You'll notice that these files both have a .cfm file extension. This is used to help ensure that the files may not be downloaded. This type of file security implemented as follows:

1. Place the configuration files into a subdirectory, such as /config.

2. Create an Application.cfc in the same directory that simply contains a <cfabort> tag.

<cfcomponent>
    <cfabort>
</cfcomponent>  

Any request to a .cfm file within this folder will immediately abort and prevent the file from being downloaded.

Environment object code

Complete code for Environment.cfc component.

This is a full version of the component that contains a few additional features:

<cfcomponent name="Environment" output="false">

    <!--- PRIVATE VARIABLES --->

    <cfset variables.instance = {}>
    <cfset variables.instance.environments = 0>
    <cfset variables.instance.environment = {}>
    <cfset variables.instance.environmentName = "">
    
    <!--- INITIALISATION --->

     <cffunction name="init" output="false">
        <cfargument name="environmentFilePath" required="true">
        <cfargument name="environment" required="false">
        <cfset var envName = 0>
        <cfset var environments = 0>
        
        <cfset environments = environmentXMLFileToStruct(arguments.environmentFilePath)>
        
        <!--- Add the common properties to all of the environments --->
        <cfif structKeyExists(environments,"common")>
            <cfloop item="envName" collection="#environments#">
                <cfif envName neq "common">
                    <cfset structAppend(environments[envName],environments["common"],true)>
                </cfif>
            </cfloop>
        </cfif>
        
        <cfset variables.instance.environments = environments>

        <!--- Set the initial environment, if provided --->
        <cfif structKeyExists(arguments,environment)>
            <cfset use(arguments.environment)>
        </cfif>

        <cfreturn this>
    </cffunction>
    
    <!--- PUBLIC FUNCTIONS --->

     <cffunction name="use" output="false">
        <cfargument name="environmentName" required="true">
        <cfif not structKeyExists(variables.instance.environments,arguments.environmentName)>
            <cfthrow message="Invalid environment name '#arguments.environmentName#'">
        </cfif>
        <cfset variables.instance.environment = variables.instance.environments[arguments.environmentName]>
        <cfset variables.instance.environmentName = arguments.environmentName>
    </cffunction>

    <cffunction name="name" output="false">
        <cfreturn variables.instance.environmentName>
    </cffunction>

    <cffunction name="properties" output="false">
        <cfreturn variables.instance.environment>
    </cffunction>

    <cffunction name="property" output="false">
        <cfargument name="name" required="true">
        <cfif not structKeyExists(variables.instance.environment,arguments.name)>
            <cfthrow message="Environment property '#arguments.name#' not found">
        </cfif>
        <cfreturn variables.instance.environment[arguments.name]>
    </cffunction>

    <!--- PRIVATE FUNCTIONS --->

    <cffunction name="environmentXMLFileToStruct" output="false" access="private">
        <cfargument name="environmentFilePath" required="true">
        <cfset var xmlString = 0>
        <cfset var xmlUtil = 0>
        <cffile action="read" file="#arguments.environmentFilePath#" variable="xmlString">
        <cfset xmlUtil = createObject("component","com.util.xml2struct")>
        <cfreturn xmlUtil.convertXmlToStruct(xmlString,structNew())>
    </cffunction>

</cfcomponent>

Integrating an Environment object with ColdSpring (Advanced)

If you are making use of ColdSpring to manage your objects then it is useful to have your environment object available as a ColdSpring bean.

ColdSpring allows "parameters" to be provided when its "bean" XML file is loaded. This allows us to provide it with:

  1. the environment configuration file path, and
  2. the current environment, which is found via our Environment Identifier object.
<!--- Create our  environment identifier, in this case a Domain based identifier --->
<cfset environmentIdentifier = createObject("component","DomainEnvironmentIdentifier").init(cgi.SERVER_NAME)>

<!--- Set the two properties that will be provided as parameters to ColdSpring ---> 
<cfset properties = {}>
<cfset properties.environmentFilePath = expandPath("/config/environments.xml.cfm")>
<cfset properties.environment = environmentIdentifier.currentEnvironment()>

<!--- Create the ColdSpring factory, providing our custom properties ---> 
<cfset beanFactory = createObject("component", "coldspring.beans.DefaultXmlBeanFactory").init(structNew(),properties)>
<cfset beanFactory.loadBeans(expandPath("/config/coldspring.xml.cfm"))>

Our Environment object would be defined within ColdSpring as follows. Notice the use of the two parameter values ${environmentFilePath} and ${environment} which are provided in the "properties" struct when the ColdSpring factory is initialised.

<bean id="environment" class="com.util.environment.Environment" singleton="true">
    <constructor-arg name="environmentFilePath">
        <value>${environmentFilePath}</value>
    </constructor-arg>
    <constructor-arg name="environment">
        <value>${environment}</value>
    </constructor-arg>
</bean>

References and further reading

Creating Configuration Files

EnvironmentConfig Utility

ColdFusion Application Multi-Environment Configuration