Use Chef to manage multiple instances of Tomcat on a single server

Overview

In this article I will demonstrate how to use Chef to manage multiple instances of Apache Tomcat on the same server.

Background

Containerization has taken the DevOps world by storm and has a solid following in the marketplace. However, there are still circumstances where it makes sense to run a Java Web application in a standalone instance of Tomcat on a virtual server.

There are many excellent tools and frameworks to automate the installation and configuration of this deployment. This article focuses on using Chef, the popular automation tool that is supported on many different operating systems.

One scenario where this deployment configuration may make sense is if there’s an application that scales vertically very well, but does not scale horizontally. In other words, this application may benefit from existing as a static deployment with a large amount of resources allocated to it, rather than several small instances of the same application.

Wait — isn’t everything a container nowadays?

Even though there is a bit push in the market to switch to ephemeral, containerized, cloud-native infrastructure, it’s simply not possible for every application type. However, it’s still possible to implement a significant amount of deployment and configuration automation for these app types, which provides many benefits.

OK, what will we need?

For this exercise, we’re going to target installing four separate instances of Tomcat on a single server. Each instance must:

  • Listen on a separate port for incoming connections.
  • Listen on a separate shutdown port.
  • Be independently configurable for critical memory settings, such as min/max JVM memory and Permgen settings.
  • Be run as separate services, so one can be stopped/started without affecting the other instances.

We’ll need the following to implement our automation:

  • A Chef new cookbook.
  • A single recipe.
  • An attributes file to hold config/replacement values.
  • A couple file templates that will get populated with variable values at deployment time.

OK let’s get started

Let’s take a look at a recipe. First, let’s install Java, because Tomcat runs on Java

include_recipe 'java'

Then we’re going to install the net-tools package, which provides some useful utilities, including ifconfig and netstat.

['net-tools'].each do |p|
    package p do
      action :install
    end
  end

Next we’ll create a user and group to install and manage Tomcat with.

user node['tomcat']['tomcat_user']

group node['tomcat']['tomcat_group'] do
  members node['tomcat']['tomcat_user']
  action :create
end

Next, we’ll install Tomcat by downloading a package directly from the archive. This makes use of the Chef Tomcat cookbook.

tomcat_install node['tomcat1']['name'] do
  tarball_uri 'http://archive.apache.org/dist/tomcat/tomcat-8/v8.5.38/bin/apache-tomcat-8.5.38.tar.gz'
  verify_checksum false # Don't do this in Prod
  install_path node['tomcat1']['base_dir']
end

Before we create the Systemd service, we’ll need to customize a couple of the configuration files that Tomcat depends on. The two files we’ll be working with both live in the <TOMCAT_HOME>/conf directory. They are: server.xml and startup.sh.

To modify these files, I added them to the templates directory in my cookbook by following these instructions. To templatize them, it’s necessary to add ruby-based references that Chef can find when it merges the template with variables. For example, to templatize the port number, I modified the server.xml.rb file as such:

    <Connector port="<%= @port_number %>" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />

where port_number is the variable that Chef looks for when it does the substitution.

The code to create the files from the templates and variables looks like this:


template node['tomcat1']['base_dir'] + 'conf/server.xml' do
    source 'server.xml.erb'
    owner node['tomcat']['tomcat_user']
    group node['tomcat']['tomcat_group']
    mode '0644'
    variables(
      port_number: node['tomcat1']['port_number'],
      shutdown_port_number: node['tomcat1']['shutdown_port_number']
      )
  end

  template node['tomcat1']['base_dir'] + 'bin/setenv.sh' do
    source 'setenv.sh.erb'
    owner node['tomcat']['tomcat_user']
    group node['tomcat']['tomcat_group']
    mode '0644'
    variables(
      min_heap: node['tomcat1']['min_heap'],
      max_heap: node['tomcat1']['max_heap'],
      max_permgen: node['tomcat1']['max_permgen']
    )
  end

Next, to prove that Tomcat works, I added a small sample .war application using Chef’s cookbook_file resource.

cookbook_file node['tomcat1']['base_dir'] + 'webapps/sample.war' do
    owner node['tomcat']['tomcat_user']
    mode '0644'
    source 'sample.war'
  end

Finally, I use Chef’s Service resource to install the appropriate service for Tomcat.

tomcat_service node['tomcat1']['name'] do
  action [:start, :enable]
  env_vars [
      { 'CATALINA_BASE' => node['tomcat1']['base_dir'] },
      { 'CATALINA_PID' => node['tomcat1']['base_dir'] + 'bin/non_standard_location.pid' }
    ]
  sensitive true
end

In addition to the recipe described above, it’s important to briefly touch on the attributes file that make this all happen. Attributes act as variables in Chef, and represent a highly expressive but simple mechanism to declare default values as well as override them when necessary. The following is a sample of the this cookbook’s attributes file.

default['java']['jdk_version'] = '8'
default['java']['set_etc_environment'] = 'true'


default['tomcat']['tomcat_user'] = 'usr_tomcat'
default['tomcat']['tomcat_group'] = 'grp_tomcat_owner'

# Instance 1
default['tomcat1']['name'] = 'tomcat_1'
default['tomcat1']['base_dir'] = '/opt/tomcat_1/'
default['tomcat1']['port_number'] = '8001'
default['tomcat1']['shutdown_port_number'] = '8011'
default['tomcat1']['min_heap'] = '64'
default['tomcat1']['max_heap'] = '128'
default['tomcat1']['max_permgen'] = '256'

# Instance 2
default['tomcat2']['name'] = 'tomcat_2'
default['tomcat2']['base_dir'] = '/opt/tomcat_2/'
default['tomcat2']['port_number'] = '8002'
default['tomcat2']['shutdown_port_number'] = '8012'
default['tomcat2']['min_heap'] = '64'
default['tomcat2']['max_heap'] = '128'
default['tomcat2']['max_permgen'] = '256'

The first two blocks of code represent values specific to the JDK installation. The next pair of values represent the names of the Tomcat user and group, respectively.

The third and fourth block represent the values that will be used for the first two instances of Tomcat installed and managed by the cookbook. As you can see, adding an additional instance, or modifying the values of the existing instances, can be as simple as modifying the attribute values defined in the file above.

When the cookbook is run using Chef’s Test Kitchen testing environment, the resulting virtual machine has four separate instances of Tomcat installed and running. Here are screenshots showing the four instances responding to their respective ports.

Tomcat listening on port 8001
Tomcat listening on port 8002
Tomcat listening on port 8003
Tomcat listening on port 8004

Conclusion

There are several ways to manage middleware, and this is just one approach. Additionally, the sample code code could use to be refactored to increase re-use. I will write another article discussing that.

In the meantime, please check out the cookbook on GitHub here, and let me know if you have any questions.


Comments

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.