views:

1081

answers:

6

I am wondering how people manage maintaining JNDI resources across multiple instances of their Tomcat Application server. Lets take, for example, my database JNDI resource. It is declared within my /conf/context.xml file and references from my apps web.xml file.

That JNDI resource has to be independently defined on my development box, staging box, and production box. What if I want to set up a new instance of a developer/staging/production box? That means I have to recreate the resource name inside the context.xml for each new instance I bring up? From a setup perspective this is a place where there could be some human error that could cause a app server to start pointing to the wrong DB.

I am finding this to be cumbersome and confusing as I scale up both the number of developers on my project as well as eventually the number of production servers I might use.

Do you just make it part of your setup or create a setup script that deals with this for you every time you re-install tomcat and setup a box? Or is there some other level of indirection that could make this easier?

+1  A: 

Are you deploying multiple web applications that must use shared resources?

If not, there's absolutely NO reason to declare your resources in /conf/context.xml. They should instead be declared in a separate, private to your web application context.xml file that will be deployed as /META-INF/context.xml inside your WAR. That file, along with your web.xml should be checked into your source control system and be deployed as part of your build - no manual intervention whatsoever.

If you are deploying multiple web apps with shared resources you can either write a custom resource factory that would expose the same resource to multiple webapps (see Tomcat documentation, at the bottom of the page) and use the above approach or - for development environment at least - you can automatically change (or even replace as default does nothing) /conf/context.xml as part of your build. For production deployment that's not advisable, of course.

ChssPly76
Those drive-by down votes are really annoying. If you're voting someone down, have some courage and leave a comment why. That goes double for when you think I said something wrong in my answer - how else would anyone learn?
ChssPly76
I did not vote down but I strongly disagree with you. Resources such as JDBC datasources and mail session should never be defined in the WAR itself. The database connection parameters are properties of the environment, not of the application. One should be able to deploy the WAR in a test environment before it goes to production.
Maurice Perry
I agree with Maurice but I think that ChssPly76 is worth an up vote because he is giving an answer to the question. IIRC Bea Weblogic have a "JNDI proxy" that allows to create a JNDI reference pointing to other JNDI resource
ATorras
@Maurice - you are absolutely correct about resources being properties of the environment. However, I don't agree that the _same_ WAR has to be deployed on dev box, qa and production - decision to include / exclude context.xml in WAR file canbe triggered by build property, for example. The original question was "how to automate this for multiple developers" - doing it in context.xml is extremely straightforward. Should you "automate" multiple production / staging deployments the same way? Of course not.
ChssPly76
Deploying the _same_ WAR file to both QA and PROD gives you the assurance that nothing could have changed between testing and going live. What you don't want is to hope you didn't forget to checkin the right version of the config file for the production build. This also means not having to have 200 versions of the config file for each transition from prod back to dev (patch) to test to prod back to dev (enhancement) to test to prod, etc.
Kelly French
@Kelly - what I don't want is to check in local configuration files at all. Builds that get deployed to staging / production come from CI box that has its own build.properties; local dev. builds use uniform setup and thus get _everything_ from source control. I've been doing this for a long time. I can't tell you how many times I had to deal with "my build is failing" coming from new developer because they didn't configure something somewhere. Yes, we have a nice detailed "new dev. environment setup" wiki and no, it didn't help. Replacing it with "checkout; ant all" did.
ChssPly76
A: 

The point of JNDI is to have environment specific resources defined independently. Your development, staging and production environments should not be sharing the same database (or at any rate, JNDI was designed to allow separate databases for each environment).

If on the other hand you're trying to load balance multiple Tomcat servers, and you want all instances to share the same configuration, I'd think you could always split up your context.xml and have the common bits in a shared file. Here's the Tomcat documentation talking about context.xml.

How you manage these is up to you. It can be simple, like having a "template" context.xml files that you start with each time you create a new Tomcat instance (having these in a source control system, especially a distributed one, can be helpful). Or you could write a script to do this.

If you want more, I believe there are products out there that puts a nice UI around the whole process. One that I believe does this is SpringSource tc Server.

Jack Leow
I will not be sharing the same DB among all the various servers. How are you supposed to share a common context.xml among various server boxes? Also if indeed it seems I must maintain various context.xml for each box what good processes/practices are there for automating this tasks and reducing human error?
Ish
I don't think you'll really be able to share files amongst different boxes without resorting to shared file systems (e.g., NFS with symbolic links). I have also updated the answer to reference the Tomcat document that talks about the context.xml configuration.
Jack Leow
+4  A: 

I'm assuming that for a given resource, you using the same JNDI name in each environment. Otherwise you'd have to edit your code to point to the new resource(JNDI) name.

Setting up the environment the first time can be almost impossible to test ahead of time. There is no way to verify that some string, like the production database connection string, didn't get fat-fingered until you actually have to use it. It's the nature of environment configuration. With that said, if you want to reduce the likely-hood of mistakes, first you need to make sure that each resource is given a name that is used regardless on which environment it is hosted. Create a dbConnectionString resource name in a properties file that points to jndi:/jdbc/myproject/resources/dbConnectionString and make sure all environments declare that same resource. Below is how we kept the code isolated from these types of environmental dependencies. That being said, you will always have to manually verify that the configuration of a specific server is using the appropriate values for the defined resources.

NOTE: never create resource names like "dbProdConnectionString", "dbQAConnectionString", "dbDevConnectionString". You will be defeating the purpose of trying to eliminate manual intervention because then you've added an indirection step that will need a code change (to point the code to the correct resource name) and build (to package the changes into your .war file) when moving between environments.


What we did was create a folder structure for the properties that were environment specific. Under that folder we created folders for each specific environment targeted for deployment, including local development. It looked like this:

Project
\
 -Properties
 \
  -Local (your PC)
  -Dev (shared dev server)
  -Test (pre-production)
  -Prod (Production)

In each folder we put parallel copies of the properties/config files and put the different configurations only in the file in the appropriate folder. The secret was to control the classpath of the deployment environment. We defined a PROPERTIES classpath entry on every server. On Prod, it would be set to "$ProjectDir/Properties/Prod" while on Test the same variable would be set to "$ProjectDir/Properties/Test".

This way we could have database connection strings for the dev/test/prod database preconfigured and not have to checkout/in the property file each time we wanted to build for a different environment.

This also meant that we could deploy the exact same .war/.ear file to Test and Prod without rebuilding. Any properties that weren't declared in the properties file we handled in a similar way by using the same JNDI name in each environment but using values that were specific to that environment.

Kelly French
A: 

My solution was to put all definitions into a server-template.xml file and then use a clever XSLT transform to generate the final server.xml for each instance. I'm using an ant build file to copy all files for an Tomcat install and let it create a server.xml from the template. Everything is saved in CVS, so I can quickly restore the installation without having to worry that something might not work. Here is what the template looks like:

<Server port="${tomcat.server.port}" shutdown="SHUTDOWN" 
  xmlns:x="http://my.company.com/tomcat-template"&gt;

  <x:define name="Derby-DataSource" username="???" password="???" url="???"
        auth="Container" type="javax.sql.DataSource"
        maxActive="50" maxIdle="5" maxWait="300"
        driverClassName="org.apache.derby.jdbc.ClientDriver"
        testWhileIdle="true" timeBetweenEvictionRunsMillis="3600000"
        removeAbandoned="true" removeAbandonedTimeout="60" logAbandoned="true" />
  <x:define x:element="Resource" name="Derby/Embedded/TDB" auth="Container" type="javax.sql.DataSource"
        maxActive="50" maxIdle="5" maxWait="300"
        username="" password="" driverClassName="org.apache.derby.jdbc.EmbeddedDriver"
        url="jdbc:derby:D:/tmp/TestDB" />
  <x:define x:element="Resource" name="Derby/TDB" auth="Container" type="javax.sql.DataSource"
        maxActive="50" maxIdle="5" maxWait="300"
        username="junit" password="test" driverClassName="org.apache.derby.jdbc.ClientDriver"
        url="jdbc:derby://localhost:1527/TDB" />
  <x:define x:element="Resource" name="Derby/P6/TDB" auth="Container" type="javax.sql.DataSource"
        maxActive="50" maxIdle="5" maxWait="300"
        username="junit" password="test" driverClassName="com.p6spy.engine.spy.P6SpyDriver"
        url="jdbc:derby://localhost:1527/TDB" />

   ... lots of Tomcat stuff ...

  <!-- Global JNDI resources -->
  <GlobalNamingResources>
    <x:if server="local">
      <!-- Local with Derby Network Server -->
      <x:use name="Derby/TDB"><x:override name="jdbc/DB" maxIdle="1" /></x:use>
      <x:use name="Derby/TDB"><x:override name="jdbc/DB_APP" maxIdle="1" /></x:use>
      <x:use name="Derby/TDB"><x:override name="jdbc/DB2" maxIdle="1" /></x:use>
    </x:if>

    <x:if env="test"> ... same for test </x:if>
    <x:if env="prod"> ... same for test </x:if>
  </GlobalNamingResources>
</Server>

As you can see, I define the defaults and then specialize the settings. Within the environment, I then overwrite some things (local system gets a smaller pool than production and integration test).

The filter script looks like this:

<!-- 

This XSLT Stylesheet transforms the file server-template.xml into server-new.xml.

It will perform the following operations:

- All x:define elements are removed
- All x:use elements will be replaces with the content and attributes
  of the respective x:define element. The name of the new element is
  specified with the attribute "x:element".
- All attributes in the x:override elements will overwrite the respective
  attributes from the x:define element.
- x:if allows to suppress certain parts of the file altogether.

Example:

  <x:define element="Resource" name="Derby/Embedded/TDB" auth="Container" ... />
  <x:use name="Derby/Embedded/TDB"><x:override name="NewTDB" /></x:use>

becomes:

  <Resource name="NewTDB" auth="Container" ... />

i.e. the attribute x:element="Resource" in x:define becomes the name of the
new element, name="Derby/Embedded/ABSTDB" in x:use specifies which x:define
to use and name="NewTDB" in x:override replaces the value of the "name"
attribute in x:define.
-->


<xsl:stylesheet version="1.0" 
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:x="http://my.company.com/tomcat-template"&gt;
<xsl:output method="xml"/>
<!-- Key for fast lookup of x:defines -->
<xsl:key name="def" match="//x:define" use="@name" />
<!-- Parameters which can be used in x:if -->
<xsl:param name="server" /> 
<xsl:param name="env" />    
<xsl:param name="instance" />   

<!-- Copy everything by default -->
<xsl:template match="node()|@*">
  <xsl:copy><xsl:apply-templates select="node()|@*"/></xsl:copy>
</xsl:template>

<!-- filter x:defines -->
<xsl:template match="x:define"></xsl:template>

<!-- x:use is replaced by the matching x:define -->
<xsl:template match="x:use">
  <!-- Find the x:define -->
  <xsl:variable name="def" select="key('def', @name)" />
  <!-- Save the x:use node in a variable -->
  <xsl:variable name="node" select="." />

  <!-- Start a new element. the name is in the attribute x:element of the x:define -->
  <xsl:element name="{$def/@x:element}">
    <!-- Process all attributes in the x: namespace -->
    <xsl:for-each select="$def/@x:*">
      <xsl:choose>
        <xsl:when test="name() = 'x:extends'">
          <xsl:variable name="extName" select="." />
          <xsl:variable name="ext" select="key('def', $extName)" />
          <xsl:for-each select="$ext/@*">
            <xsl:if test="namespace-uri() != 'http://my.company.com/tomcat-template'"&gt;
              <xsl:copy />
            </xsl:if>
          </xsl:for-each>
        </xsl:when>
      </xsl:choose>
    </xsl:for-each>

    <!-- Copy all attributes from the x:define (except those in the x: namespace) -->
    <xsl:for-each select="$def/@*">
      <xsl:if test="namespace-uri() != 'http://my.company.com/tomcat-template'"&gt;
        <xsl:copy />
      </xsl:if>
    </xsl:for-each>

    <!-- If there is an x:override-Element, copy those attributes. This
         will overwrite existing attributes with the same name. -->
    <xsl:for-each select="$node/x:override/@*">
      <xsl:variable name="name" select="name()" />
      <xsl:variable name="value" select="." />
      <xsl:variable name="orig" select="$def/attribute::*[name() = $name]" />

      <xsl:choose>
        <!-- ${orig} allows to acces the attributes from the x:define -->
        <xsl:when test="contains($value, '${orig}')">
          <xsl:attribute name="{$name}"><xsl:value-of select="substring-before($value, '${orig}')" 
            /><xsl:value-of select="$orig" 
            /><xsl:value-of select="substring-after($value, '${orig}')" 
            /></xsl:attribute>
        </xsl:when>
        <xsl:otherwise>
          <xsl:copy />
        </xsl:otherwise>
      </xsl:choose>
    </xsl:for-each>
    <!-- Copy all child nodes, too -->
    <xsl:apply-templates select="$def/node()"/>
  </xsl:element>
</xsl:template>

<!-- x:if, to filter parts of the document -->
<xsl:template match="x:if">
  <!-- t will be non-empty if any of the conditions matches -->
  <xsl:variable name="t">
    <!-- Check for each paramater whether it is used in the x:if. If so,
         check the value. If the value is the same as the stylesheet
         paramater, the condition is met. Missing conditions count
         as met, too.
    <xsl:if test="not(@server) or @server = $server">1</xsl:if>
    <xsl:if test="not(@env) or @env = $env">1</xsl:if> 
    <xsl:if test="not(@instance) or @instance = $instance">1</xsl:if> 
  </xsl:variable>
  <xsl:if test="normalize-space($t) = '111'">
    <xsl:comment> <xsl:value-of select="$server" />, Env <xsl:value-of select="$env" />, Instance <xsl:value-of select="$instance" /> </xsl:comment>
    <xsl:apply-templates select="node()|@*"/>
  </xsl:if>
</xsl:template>

</xsl:stylesheet>
Aaron Digulla
A: 

If you are using the spring framework you can solve this issue very easily using the PropertyPlaceholderConfigurer. This class let's you move the definitions into external properties files. The data source configuration is as follows:

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName"><value>${jdbc.driver}</value></property>
<property name="url"><value>${jdbc.url}</value></property>
<property name="username"><value>${jdbc.user}</value></property>
<property name="password"><value>${jdbc.password}</value></property>
</bean>

The properties themselves are defined in a standard properties file:

jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://host/database
jdbc.username=user
jdbc.password=secret

For the actual properties you have two options:

  1. Put the properties file externally on the file system, so it will be different on every machine :

    <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
      <property name="location">file:/etc/yourapp/jdbc.properties</property>
      <!-- on windows, put the file in c:\etc\yourapp, the definition will work -->
    </bean>
    
  2. Add the following system property to your server -Denv=[development|test|production]. Then, put three configuration files in your WEB-INF/classes directory: jdbc-development.properties, test-development.properties and production-development.properties. The context configuration will be:

    <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
      <property name="location">classpath:jdbc-${env}.properties</property>
    </bean>
    
David Rabinowitz
A: 

If you're able to bind a remote JNDI directory to Tomcat's "global" JNDI directory, you could use this mechanism: http://stackoverflow.com/questions/1940546/using-a-jndi-datasource-created-by-another-application-with-tomcat/3827026#3827026

Regards.

mrrtnn