Hierarchical Configurations

It Came From Version Control

A long time ago, some article convinced me to check my entire dev environment into CVS. Not just the code, but my servers, libs, configurations, everything. It turned out to be very good advice.

Soon my entire dev team shared a single, version controlled environment. Third party library mis-matches were a thing of the past. Upgrades were pushed out via version control. Automated builds were also checked in for easy roll-backs. It wasn’t long before QA and our Sysadmins jumped on the bandwagon.

Want the newest, stable environment? Just check it out.When done right the only difference between the same environement for the various developer, QA, and Production environments were the configuration files.

Problem of Scale

We got into the practice of having a directory of configuration files for each environment:

/configurations
    /dev
        /developer_one
        /developer_two
        /developer_three
    /qa
        /frontend_servers
        /backend_servers
    /production
        /frontend_servers
        /backend_servers

It all was very simple and easy…until our configurations and teams grew. We ran into two styles of problems: changes made in a local developer directory that wouldn’t propagate to production, or; development configurations that made it into production without someone looking at them and adjusting them for a production environment. (256MB Max memory and 5 threads?!?)

Developer One would add configuration to her local files and they wouldn’t be merged up. Developers Two and Three would waste time looking for “bugs.”

Being a Unix shop, we tried to use diff and patch as a way for people to manage all the difference. It turned out to be a lot of moving parts, white space errors, and frustrated Sysadmins at 2am for a launch.

Split It Up.

What is we could make diff/patch part of how we did configuration instead of a seperate process? What if we had a root global directory and a set of hierarchical directories that only held override values?

/conf
    combined.properties
    /global/
        /developer_one/runtime.properties
        /developer_two/runtime.properties
        /developer_three/runtime.properties
        /qa/
            /frontend_servers/runtime.properties
            /backend_servers/runtime.properties
        /prod
            /frontend_servers/runtime.properties
            /backend_servers/runtime.properties
    /templates
        server.xml.vm

So global_runtime.properties holds all of the values that we wanted as default. Then each environment could override just the properties that it cared about.

When a developer created a new property, instead of putting it in his local file and making a note for a sysadmin; he would put the new property and the correct default value in combined.properties. If he needed another value in dev, he would override it in his runtime.properties override file.

Make It Work!

ANT

To make this work, we wanted to stick with Ant and not depend on any outside tools or scripts, you will however need almost all of the Ant extra jars. If you are on RedHat, it is simple to grab them all ala yum:

yum install ant.x86_64 
yum install ant-commons-logging.x86_64 ant-apache-log4j.x86_64 
yum install ant-javamail.x86_64 ant-junit.x86_64
yum install ant-nodepsant-nodeps.x86_64 ant-jdepend.x86_64
yum install ant-antlr.x86_64 ant-apache-bsf.x86_64 ant-jsch.x86_64 ant-apache-bcel.x86_64 ant-jmf.x86_64

TEMPLATES

Our system needed more than a runtime.properties file. We used a whole bunch of third party apps that each had their own set of configuration files. Instead of having all of our developers and Sysadmins be familiar with each config, we made a FMPP template for each config file and put them in the /conf/templates directory.

FMPP uses the normal ${name} substitution format that most Java developers and any Ant scripter will know. Also, it works within at and allows for recursion and other useful features.

The advtanges of creating temp files were:

  1. Not everyone needed to know the format of every conf file
  2. People did not edit (and destroy) conf files by hand. All files were generated from templates.
  3. You didn’t have to hunt around for values, as they all live in runtime.properties.
  4. Values that you have to apply in multiple locations are only updated once.

THE PROPERTY FILE

This system works based on how FMPP loads values from a property file. If the same property appears in the same file multiple times, the first time it is set wins. So if your combined.properties looked like

value=override2
value=override
value=original

and your template looked like

the value is "${value}"

Your output would be

the value is "override2"

THE ANT FILE

First we are going to add some targets to help us build directory lists:

<target name="__g_d_l" >
 <antcallback target="_generate_dir_list" return="dir_list" />

 </target>

 <!--
 GIVEN TO VARS: top_dir, current_dir
 RECURSE UP THE CURRENT_DIR UNTIL IT EQUALS TOP_DIR
 AND GENERATE A ":" SEPERATED LIST OF DIRS
 STORED IN VARIABLE dir_list
 -->
 <target name="_generate_dir_list" description="load the correct properties files">

 <var name="dir_list" value="${current_dir}:${dir_list}" />

 <if>
 <equals arg1="${top_dir}" arg2="${current_dir}" />
 <then>

 </then>
 <else>

 <echo message="Load This Dir: ${current_dir}" />

 <propertyregex input="${current_dir}"
 property="current_dir"
 override="true"
 regexp="([a-zA-Z0-9_/]*)/([a-zA-Z0-9_]*$)"
 select="\1"
 />

 <antcallback target="__g_d_l" return="dir_list"/>

 </else>

 </if>

 </target>

These calls work with the following target:

<property name="global_conf_dir" value="${conf_dir}/global" />

<!-- 
 NEEDS TO HAVE THE -Dconftype SYSTEM PROPERTY SET, I.E.:
 ant -Dconftype="dev/developer_name"
 THEN IT READS ALL THE PROPERTY FILES FROM THE TOP DOWN
 MERGES INTO A SINGLE COMBINED.PROPERTIES FILE
 THEN THIS FILE IS USED TO CREATE A SINGLE PROPERTY SET
 THAT IS USED BY ALL OPERTAIONS
 -->
<target name="setup_conf_file" description="build the combined prop file and load all props into memory">

 <var name="top_dir" value="${global_conf_dir}" />
 <var name="current_dir" value="${global_conf_dir}/${conftype}"/>
 <var name="dir_list" value=""/>

 <antcallback target="_generate_dir_list"  return="dir_list" />

 <!-- delete the combined conf file if it already exists -->
 <delete file="${combined_conf_file}" failonerror="no" />

 <concat destfile="${combined_conf_file}"  append="true" fixlastline="true">
## THIS IS THE CONFTYPE THAT WAS DECLARED
## TO SETUP THIS FILE
conf_type=${conftype}

 </concat>

 <foreach list="${dir_list}" delimiter=":" target="_concat_property_file" param="property_dir" inheritall="true" inheritrefs="true" />

 </target>

<!-- READ ALL THE PROPERTY FILES IN A DIR
 AND APPEND THEM TO A MASTER PROPERTY FILE
 -->
 <target name="_concat_property_file">

 <echo message="concat: ${property_dir} to ${combined_conf_file}"/>

 <concat destfile="${combined_conf_file}" append="true" fixlastline="true">
###########################
#  FOLLOWING PROPERTIES LOADED FROM: ${property_dir}
###########################

 </concat>

 <concat destfile="${combined_conf_file}"  append="true" fixlastline="true">
 <fileset dir="${property_dir}" includes="*.properties"/>
 <filterchain>
 <expandproperties/>
 </filterchain>
 </concat>

 </target>

This last call does the work. It reads the -Dconftype environmental property and treats it as a directory listing from ${global_conf_dir}. It uses the previous two targets to build dir_list variable, which is an ordered list of directories. Then the system loops through each directory and concatenates the runtime.properties into the top level combined.properties.

Each time it concatenates a file, it puts a header in to show which file it is taking from. So if you have to ever read through concatinated files, you can see what overrides generated it.

USING FMPP TEMPLATES

Now that you have a combined.properties, you can use it to drive all of your templates.

<!-- LOAD THE FMPP CONTRIBUTION TASK  -->
 <taskdef name="fmpp" classname="fmpp.tools.AntTask" classpathref="ant.path"/>
<--LOAD THE COMBINED PROPERTIES -->
<load-conf-props file="combined.properties" rootdir="${conf_dir}" searchlist="${props_override_list}" />
<!-- RUN A TEMPLATE -->
<fmpp 
    sourceFile="${conf_dir}/templates/server.xml.vm"
    outputFile="${app_server_deploy_dir}/server.xml"
    data="antProperties(), proj:antProject(), task:antTask()" />

You can add whatever templates, or expand the property file combiner to do it for specific files. This was enough to get my team out of patch/diff several hundred lines of runtime.properties. It was an error prone task that was not fit to run by hand. Almost a year later, people hardly even remember how we used to do it.


					

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s