Keep your tomcat instance up-to-date, and keep apps and configuration tidy.

UPDATED: 26th January 2015, with Tomcat 8 support.

Today I'm gonna share something that I figured out some times ago: how do I keep
my tomcat installation up to date on my servers?

Of course I'm not willing to automatically switch majors, but I'd like to pull bugfix
releases in as soon as possible; most of my servers should work unattended, and I prefer to handle it if something crashes rather than being hacked because of some public exploit.

Apache Tomcat makes it incredibly hard to do so for different reasons: first, there's no latest link for the latest release, you must first check the homepage to see what's the latest version; second, the archive you can download mingles the application itself with the configuration, log and webapp dir.

What should we do, then?

Tomcat allows us to configure almost everything. Let's use its power!

For this setup, I use Tomcat7 and I chose to employ /etc/tomcat7/conf as a configuration directory, /opt/tomcat7/latest as tomcat7 own directory, /opt/tomcat7/webapps for our webapps, /opt/tomcat7/logs,
/opt/tomcat7/temp, /opt/tomcat7/work as logging, temp and work directory, respectively.

The same applies for tomcat8, it's just the prefix that will change.

To begin with, create a user (maybe a service user, I'll leave the details to you) tomcat with a main group tomcat as well, which we'll use to run the servlet container - you can pick a different user if you like, the class allows you to choose.

Now make sure your Puppet manifest includes the tomcat_versioned class with the proper parameters. In order to do so, you'll need this custom fact that helps the system determine the very latest tomcat7 version,
place it in your custom facts directory:

#!/usr/bin/env python
# requires lxml
from lxml.etree import HTML
import re
import sys
import urllib

for version in (7,8):
	pattern = re.compile("^{0}\.0\.\d\d\d?$".format(version))
	root = HTML(urllib.urlopen("{0}0.cgi".format(version)).read())
	for e in root.iterdescendants():
		if isinstance(e.text, basestring) and pattern.match(e.text.strip()):
		print "tomcat{1}_latest_version={0}".format(e.text.strip(), version)

and the tomcat_versioned class, of course; just pass the major of the tomcat you'd like to use, and it will find the latest tomcat update for such major, and install it in /opt/tomcatX:

# this will install latest tomcat from apache website, and yet retain your
# config from /etc/tomcat7/conf
# tested with puppet 3.7 and tomcat 7 and 8.

class tomcat_versioned($tomcat_major=8, $tomcat_user="tomcat") {
	# assumption: the user should exist, it won't be created
	# you should define a tomcatX service outside this class
	$tf = getvar("tomcat${tomcat_major}_latest_version")
	$tm = "tomcat${tomcat_major}"

	exec { "/bin/tar xvf apache-${tm}-archive.tar.gz":
	creates =>"/opt/${tm}/apache-${tm}-${tf}",
	cwd => "/opt/${tm}",
	refreshonly => true,
	alias => "${tm}_unpack",
	require => File["/opt/${tm}"]

file { "/opt/${tm}":
ensure => "directory",
mode => 0755,
owner => "root",
group => "root"

file { "/opt/${tm}/apache-${tm}-archive.tar.gz":
ensure => "present",
source => "/tmp/apache-tomcat-${tf}.tar.gz",
require => Exec["${tm}_download_latest"],
notify => Exec["${tm}_unpack"]

exec { "/usr/bin/wget --timestamping${tomcat_major}/v${tf}/bin/apache-tomcat-${tf}.tar.gz":
alias => "${tm}_download_latest",
cwd => "/tmp"

exec { "/bin/ln -sf --no-target-directory apache-tomcat-${tf} latest":
refreshonly => true,
subscribe => Exec["${tm}_unpack"],
cwd => "/opt/${tm}",
alias => "${tm}_symlink"

exec { "/bin/rm -rf conf.orig && /bin/mv -f conf conf.orig && /bin/ln -sf --no-target-directory /etc/${tm}/conf conf":
refreshonly => true,
cwd => "/opt/${tm}/latest",
subscribe => Exec["${tm}_symlink"],
notify => Service["${tm}"],
alias => "${tm}_config_move"

# first-time only executions. I might like to abstract some logic if I were a bit less lazy than I am.
# in order to stay on the safe side, we never let the normal user to access our files; this may be relaxed,
# depending on your context.

# this contains our config. our servlet container should be able to read it, but never write it.
exec { "/bin/mkdir -p /etc/${tm} && /bin/cp -r /opt/${tm}/latest/conf.orig /etc/${tm}/conf && /bin/chmod 0750 /etc/${tm}/conf && /bin/chown -R root:${tomcat_user} /etc/${tm}/conf && /bin/chmod 0640 /etc/${tm}/conf/*":
creates => "/etc/${tm}/conf",
subscribe => Exec["${tm}_config_move"]

# this will contain the actual code of our webapps. Again, the container must be able to read them,
# never write to them.
exec { "/bin/mkdir -p -m 0750 /opt/${tm}/webapps && /bin/chown root:${tomcat_user} /opt/${tm}/webapps":
creates => "/opt/${tm}/webapps",

# those are working directories where the container must be able to write.
exec { "/bin/mkdir -p -m 0770 /opt/${tm}/work && /bin/chown root:${tomcat_user} /opt/${tm}/work":
creates => "/opt/${tm}/work",

exec { "/bin/mkdir -p -m 0770 /opt/${tm}/temp && /bin/chown root:${tomcat_user} /opt/${tm}/temp":
creates => "/opt/${tm}/temp",

exec { "/bin/mkdir -p -m 0770 /opt/${tm}/logs && /bin/chown root:${tomcat_user} /opt/${tm}/logs":
creates => "/opt/${tm}/logs",

The tomcat7 service is trivial and omitted, fill it in by yourself (or delete it).

Once this is done, you'll find a base configuration in /etc/tomcat7/conf. We must now edit server.xml there and tell Tomcat where we'd like to have our webapps, logs and work directories:

<!-- this should go inside <Server><Service><Engine> -->

<Host name="localhost"  appBase="/opt/tomcat7/webapps"
            unpackWARs="false" autoDeploy="true" workDir="/opt/tomcat7/work">

        <!-- SingleSignOn valve, share authentication between web applications
             Documentation at: /docs/config/valve.html -->
        <Valve className="org.apache.catalina.authenticator.SingleSignOn" />

        <!-- Access log processes all example.
             Documentation at: /docs/config/valve.html
             Note: The pattern used is equivalent to using pattern="common" -->
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="/opt/tomcat7/logs"
               prefix="localhost_access_log." suffix=".txt"
               pattern="%h %l %u %t &quot;%r&quot; %s %b" />


Last but not least, we should tell tomcat what is CATALINA_HOME and CATALINA_TMPDIR. This is an example script that can be used with upstart on Ubuntu 12.04, change it according to your OS:

description "tomcat7"

  start on runlevel [2345]
  stop on runlevel [!2345]
  respawn limit 10 5

  # run as non privileged user
  # add user with this command:
  ## adduser --system --ingroup www-data --home /opt/apache-tomcat apache-tomcat
  # Ubuntu 12.04: (use 'exec sudo -u apache-tomcat' when using 10.04)
  setuid tomcat
  setgid tomcat

  # adapt paths:
  env JAVA_HOME=/usr/lib/jvm/java-7-oracle
  env CATALINA_HOME=/opt/tomcat7/latest
  env CATALINA_TMPDIR=/opt/tomcat7/temp
  env HOME=/home/tomcat

  # adapt java options to suit your needs:
  env JAVA_OPTS="-Djava.awt.headless=true"
  env CATALINA_OPTS="-Xmx1536M -server"

  exec $CATALINA_HOME/bin/ run

  # cleanup temp directory after stop
  post-stop script
    rm -rf /opt/tomcat7/temp/*
  end script

In this situation I had a "normal home" for tomcat because I needed it, but your requirements may work differently.

I hope that's useful to somebody.