Simplifying Resin XML With Dynamic Configuration Part 1
From Resin 3.0
Configuration variable substitution using <resin:properties> and rvar()
This first part of a multi-part article on Resin configuration examines how to use EL variables in various situations to simplify Resin configuration. When we're done you'll understand how a simple database resource like this:
<database jndi-name="jdbc/mysql">
<driver type="com.mysql.jdbc.Driver">
<url>jdbc:mysql://user:password@$192.168.1.1:3306/dbname</url>
</driver>
</database>
... can be dynamically merged with per-server properties like this:
dbserver : 192.168.1.1
app-0.dbserver : 192.168.1.23
app-1.dbserver : 192.168.1.64
... resulting in configuration that is simple, powerful, and well suited for deployment in a cloud environment.
Background
The main Resin 4 configuration is based on a single XML file, resin.xml, which is usually identical between all Resin nodes in the same environment. resin.xml configures application clusters, tiers, servers, JVM parameters, and environment resources. It is usually maintained by a devops team with knowledge of the various deployment environments.
Web application configuration files such as web.xml and resin-web.xml are packaged inside a web application archive, and thus are usually identical between all environments. These configuration files are the domain of the developer and are usually not modified by a devops team at all.
The contract between the two teams is generally as simple as possible; known lookup keys. This is where a resource registry like CDI or JNDI comes into play. Environment specific configuration is maintained in resin.xml, resources are created by Resin, bound in a registry using an agreed upon key, and can thus be accessed by the web applications regardless of the environment.
The problem is that maintaining resin.xml can become cumbersome and error-prone when you factor in multiple web application, tiers, hosts, jvms, databases, etc. For this reason Resin supports dynamic configuration based on EL evaluation. Imports, variable substitutions, and control structures are available to help simplify XML configuration and make maintenance much easier.
Variable substitution using EL
The basis of Resin dynamic configuration is Unified Expression Language (EL) evaluation. You'll see this used extensively throughout resin.xml inside the standard EL "${ ... }" block. Following are some common Resin EL variables:
${resin.home} - The Resin home directory, where /bin and /lib are located.
${resin.root} - The content root directory, typically where /webapps and /resin-data are located.
${__DIR__} - The directory containing resin.xml.
${resin.professional} - A boolean indicating if the Pro version is running with a valid license.
${server.id} - The id of the current server.
Environment variables and java system properties are also available and can be very convenient in some situations. For example, consider a case where each system user can startup Resin to serve content:
<web-app id="/${user.name}">
<document-directory>${user.home}/wwwroot</document-directory>
</web-app>
Please refer to the Resin documentation on EL Variables and Functions for more information.
Lets explore dynamic configuration in more depth by considering a common example; declaring a database resource. Database connections are usually deployment dependent resources configured in the <cluster> section of resin.xml so they are restricted to a specific cluster while being available to all web application configured in that cluster.
<cluster id="app_tier">
...
<database jndi-name="jdbc/mysql">
<driver type="com.mysql.jdbc.Driver">
<url>jdbc:mysql://user:password@192.168.1.1:3306/dbname</url>
</driver>
</database>
Certainly hard coding the URL in resin.xml is not ideal, so lets declare it as an EL variable and replace the IP address of the database server:
<system-property dbserver="192.168.1.1"/>
...
<database jndi-name="jdbc/mysql">
<driver type="com.mysql.jdbc.Driver">
<url>jdbc:mysql://user:password@${dbserver}:3306/dbname</url>
</driver>
</database>
This doesn't help much; we still hard coded the IP address, just in a different place in resin.xml. So remove <system-property> and start Resin with a -D system property like this:
> ./bin/resin.sh -Ddbserver=192.168.1.1 -server app-0 start
That works, but now we need a special script to startup Resin each time. Instead, lets declare the IP address as an OS environment variable and start Resin normally:
> export dbserver=192.168.1.1
> ./bin/resin.sh -server app-0 start
Environment variables are available in EL, so this works and may be sufficient for some situations. Its easy to declare an environment variable in your .bashrc or /etc/profile or other OS specific configuration. However now Resin configuration is operating system specific and not centralized.
Loading child configuration files using <resin:import>
An alternative to using EL in resin.xml is to drop an environment dependent configuration file on each machine, for example database.xml:
<cluster xmlns="http://caucho.com/ns/resin">
<database jndi-name="jdbc/mysql">
<driver type="com.mysql.jdbc.Driver">
<url>jdbc:mysql://user:password@192.168.1.1:3306/dbname</url>
</driver>
</database>
</cluster>
... and then simply declare an import in resin.xml:
<cluster id="app_tier">
...
<resin:import path="${__DIR__}/database.xml"/>
This simplifies resin.xml by splitting configuration into static and dynamic files, and removes dependencies on the operating system. The downside of this is that every <server> in the cluster gets the same database server IP address. What if you want each server in the environment to have it's own configuration? One way to work around this is to use ${server.id} and create a file named app-0.xml:
<cluster xmlns="http://caucho.com/ns/resin">
<database jndi-name="jdbc/mysql">
<driver type="com.mysql.jdbc.Driver">
<url>jdbc:mysql://user:password@192.168.1.1:3306/dbname</url>
</driver>
</database>
</cluster>
> ./bin/resin.sh -server app-0 start
And in resin.xml:
<resin:import path="${__DIR__}/${server.id}.xml"/>
Now you can have configuration files for each server in the Resin conf directory and each server will load its respective file. Refer to the Resin reference on <resin:import> for more information.
Declaring EL variables with <resin:properties>
Resin 4.0.24 introduced the <resin:properties> construct which externalizes variable declaration into a central properties file, further simplifying dynamic configuration. Along with the use of the "rvar" function (discussed below), dynamic configuration (particularly in a cloud environment) becomes elegant and intuitive.
You should notice that since Resin version 4.0.24 there is a new resin.properties file in the configuration directory, and resin.xml by default declares a <resin:properties> as follows:
<resin:properties path="${__DIR__}/resin.properties" optional="true"/>
If you are upgrading it's a simple matter of adding this line as a child of <resin> to take advantage of the new functionality.
<resin:properties> is similar to <resin:import>, but loads a properties file and saves the values in EL expressions which are then available in resin.xml. Going back to our database example from above, lets modify it to take advantage of resin.properties:
dbserver : 192.168.1.1
And in resin.xml:
<cluster id="app_tier">
...
<database jndi-name="jdbc/mysql">
<driver type="com.mysql.jdbc.Driver">
<url>jdbc:mysql://user:password@${dbserver}:3306/dbname</url>
</driver>
</database>
This gives us the advantages of externalized configuration without operating system dependence. However we've now regressed to a single configuration for every server in the environment. This could be solved using the ${server.id} technique above, but the rvar function provides a more elegant alternative.
Using rvar() for per-server variable overrides
The EL function rvar is a variable lookup mechanism introduced in Resin 4.0.24 and designed to work with <resin:properties>. It permits the overriding of variables on a per-server basis by pre-pending the server.id to the variable name in resin.properties.
Here is our ongoing database example, now modified to take advantage of resin.properties and rvar:
dbserver : 192.168.1.1
app-0.dbserver : 192.168.1.23
app-1.dbserver : 192.168.1.64
And in resin.xml:
<cluster id="app_tier">
...
<database jndi-name="jdbc/mysql">
<driver type="com.mysql.jdbc.Driver">
<url>jdbc:mysql://user:password@${rvar('dbserver')}:3306/dbname</url>
</driver>
</database>
Notice we have replaced ${dbserver} with ${rvar('dbserver')}, thus invoking the rvar function with 'dbserver' as a parameter instead of referencing the EL variable directly. (Note: be sure your parameter is enclosed in quotes, or it will be interpreted as an EL variable itself!) Rvar will determine the current server.id (in our case "app-0"), append '.dbserver' to it, and attempt to lookup the value "app-0.dbserver" in resin.properties. If it's not found, rvar will then look for just "dbserver" as a default.
This style of configuration is well suited to a cloud environment where nodes are commonly spun-up from a base configuration and a small dynamic property blob.
Conclusion
Resin uses EL evaluation for dynamic configuration. Variables can be declared a number of way; inside resin.xml, as a command line parameter, as an environment variable, or using <resin:properties>. Imports allow for the splitting of configuration into multiple files. Rvar overrides variables for server respective configuration.
This first part of a series of articles on Resin configuration examined how to use EL variables in various situations to create simpler and more powerful configurations. While imports were touched on, part two of the series will go into more depth regarding importing filesets and the local.d directory. The objective is to understand how Resin enables configuration that is both simple and powerful and well suited for a deployment in a cloud environment.