Deploying WAR files to Tomcat with Jenkins

Table of Contents

A co-worker asked me this week about how to deploy a WAR file to Tomcat with Jenkins. In my team we are currently maintaining and deploying about 10 Java web systems, but we have no consistent way of deploying the applications to Tomcat yet. In the past I used Ant, Maven, Cargo, Grunt, and Jenkins, so I decided to write this short post to show a few different ways it can be achieved, à la Perl’s TMTOWTDI motto.

#1 Deploying with custom scripts

At first you may be tempted to write your own script to deploy to Tomcat with some Shell, Perl, Python or Java. But I think I would choose this option only because either I needed some feature that is not available in the other options, or in order to call other tasks or debug some problem.

Example:

$ docker run -d -p 8888:8080 jeanblanchard/tomcat:8
$ git clone https://github.com/spring-projects/spring-petclinic.git && cd spring-petclinic && mvn package
$ curl --upload-file target/petclinic.war "http://admin:admin@localhost:8888/manager/text/deploy?path=/spring-petclinic&update=true"
OK - Deployed application at context path /spring-petclinic

Pros

Cons

#2 Deploying with a build tool

For this example I will use Apache Maven, but you can achieve the same with Grunt, Ant, Gradle, or Make. Each of these tools provide different mechanisms to call external tools, some providing plug-ins that can be used to deploy a WAR file to Tomcat, like Maven and the Cargo plug-in.

But even with Maven you have a few options. For example.

Example:

$ git clone https://github.com/spring-projects/spring-petclinic.git && cd spring-petclinic

Add the following to the pom.xml file, under the right XML tags, of course.

<!-- from https://gist.github.com/mdread/5900034 -->
<plugins>
    <plugin>
        <groupId>org.codehaus.cargo</groupId>
        <artifactId>cargo-maven2-plugin</artifactId>
        <configuration>
            <container>
                <containerId>tomcat7x</containerId>
                <type>remote</type>
            </container>
            <configuration>
                <type>runtime</type>
                <properties>
                    <cargo.hostname>${cargo.hostname}</cargo.hostname>
                    <cargo.servlet.port>${cargo.servlet.port}</cargo.servlet.port>
                    <cargo.tomcat.manager.url>${cargo.tomcat.manager.url}</cargo.tomcat.manager.url>
                    <cargo.remote.username>${cargo.remote.username}</cargo.remote.username>
                    <cargo.remote.password>${cargo.remote.password}</cargo.remote.password>
                </properties>
            </configuration>
            <deployer>
                <type>remote</type>
            </deployer>
            <deployables>
                <deployable>
                    <groupId>${project.groupId}</groupId>
                    <artifactId>${project.artifactId}</artifactId>
                    <type>${project.packaging}</type>
                </deployable>
            </deployables>

        </configuration>
    </plugin>
</plugins>

<profiles>
    <profile>
        <id>prod</id>
        <properties>
            <deploy.env>prod</deploy.env>
            <cargo.hostname>srvprd001</cargo.hostname>
            <cargo.servlet.port>8080</cargo.servlet.port>
            <cargo.tomcat.manager.url>http://srvprd001:8080/manager</cargo.tomcat.manager.url>
            <cargo.remote.username>user</cargo.remote.username>
            <cargo.remote.password>pass</cargo.remote.password>
        </properties>
    </profile>
    <profile>
        <id>test</id>
        <properties>
            <deploy.env>dev</deploy.env>
            <cargo.hostname>srvtst001</cargo.hostname>
            <cargo.servlet.port>9090</cargo.servlet.port>
            <cargo.tomcat.manager.url>http://srvtst001:9090/manager</cargo.tomcat.manager.url>
            <cargo.remote.username>user</cargo.remote.username>
            <cargo.remote.password>pass</cargo.remote.password>
        </properties>
    </profile>
</profiles>

And finally start Tomcat and call the Cargo Maven plug-in.

$ docker run -d -p 8888:8080 jeanblanchard/tomcat:8
$ mvn package org.codehaus.cargo:cargo-maven2-plugin:deploy -Ptest -Dcargo.hostname=localhost -Dcargo.servlet.port=8888 -Dcargo.tomcat.manager.url=http://localhost:8888/manager/text -Dcargo.remote.username=admin -Dcargo.remote.password=admin

What I like about this approach is that using profiles and environments with Maven, you can have pre-defined variables per profile, but also overwrite them when necessary. For example in the previous command line, the host, port, user and password are overwritten to match the default values from the Docker image used.

Pros

Cons

#3 Deploying with a build server

Deploying with a build server is not very different from approach #2. For this example I will use Jenkins, as this is the build server I am most familiar with, and also the one that I am using at work.

Example:

Install the Deploy Plugin in Jenkins (it appears as “Deploy to container Plugin” in the plug-in selection screen), as well as the git plug-in to check out the project.

In your job configuration, add a SCM step to clone the petclinic war project, and another step to invoke a Maven top level target execute mvn package. Also add a post build step to deploy with the following settings.

Instead of a screenshot, here is the config.xml file for my example job - easier to diff your job configuration.

<?xml version='1.0' encoding='UTF-8'?>
<project>
  <actions/>
  <description></description>
  <keepDependencies>false</keepDependencies>
  <properties/>
  <scm class="hudson.plugins.git.GitSCM" plugin="git@2.4.3">
    <configVersion>2</configVersion>
    <userRemoteConfigs>
      <hudson.plugins.git.UserRemoteConfig>
        <url>https://github.com/spring-projects/spring-petclinic.git</url>
      </hudson.plugins.git.UserRemoteConfig>
    </userRemoteConfigs>
    <branches>
      <hudson.plugins.git.BranchSpec>
        <name>*/master</name>
      </hudson.plugins.git.BranchSpec>
    </branches>
    <doGenerateSubmoduleConfigurations>false</doGenerateSubmoduleConfigurations>
    <submoduleCfg class="list"/>
  </scm>
  <canRoam>true</canRoam>
  <disabled>false</disabled>
  <blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>
  <blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
  <triggers/>
  <concurrentBuild>false</concurrentBuild>
  <builders>
    <hudson.tasks.Maven>
      <targets>clean package</targets>
      <mavenName>3.3.9</mavenName>
      <usePrivateRepository>false</usePrivateRepository>
      <settings class="jenkins.mvn.DefaultSettingsProvider"/>
      <globalSettings class="jenkins.mvn.DefaultGlobalSettingsProvider"/>
    </hudson.tasks.Maven>
  </builders>
  <publishers>
    <hudson.plugins.deploy.DeployPublisher plugin="deploy@1.10">
      <adapters>
        <hudson.plugins.deploy.tomcat.Tomcat7xAdapter>
          <userName>admin</userName>
          <passwordScrambled>YWRtaW4=</passwordScrambled>
          <url>http://localhost:8888</url>
        </hudson.plugins.deploy.tomcat.Tomcat7xAdapter>
      </adapters>
      <contextPath>spring-petclinic</contextPath>
      <war>target/*.war</war>
      <onFailure>false</onFailure>
    </hudson.plugins.deploy.DeployPublisher>
  </publishers>
  <buildWrappers/>
</project>

When running this job, you should see in the end of the console output, something similar to this.

[INFO] Building war: /tmp/1/jobs/deploy01/workspace/target/petclinic.war
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 20.590 s
[INFO] Finished at: 2016-03-20T19:13:51+13:00
[INFO] Final Memory: 36M/359M
[INFO] ------------------------------------------------------------------------
Deploying /tmp/1/jobs/deploy01/workspace/target/petclinic.war to container Tomcat 7.x Remote
  [/tmp/1/jobs/deploy01/workspace/target/petclinic.war] is not deployed. Doing a fresh deployment.
  Deploying [/tmp/1/jobs/deploy01/workspace/target/petclinic.war]
Finished: SUCCESS

You can chain several jobs, creating a pipeline with one job to build, one for functional tests, and another job to deploy.

Pros

Cons

Final thoughts

Whenever I can, I try to avoid reinventing the wheel. The less code I write, and the more good quality code that I reuse, the merrier for me. So approach #1 is my less favorite way of deploying WAR files to Tomcat.

My preferred approach for deploying WAR files to Tomcat, is a mix of #2 and #3.

You configure the deploy tasks in your build tool, be it Maven, Grunt, Ant, or etc. And configure a job in Jenkins to check out the code and deploy it, calling your build tool.

Using the previous examples, you would configure profiles in your pom.xml, and also combine parameters in Jenkins to define which profile to activate (as well as override parameters if necessary).

This way you give developers the power to choose how/where to deploy. In case they need to deploy to a different environment, they can change the build scripts, commit, and wait for Jenkins to be ready to deploy. Leaving the deployment environment configuration in Jenkins jobs would require developers to request changes in jobs, which would slow down the development pipeline.

Furthermore, you can also overwrite values in the build tools, so you can still control it in the build server too. And you are getting the best of both worlds, having Jenkins parameters, over 1000 plug-ins, and being able to create a build workflow/pipeline.

But remember, that is my opinion. I hope you can assimilate everything you have read here, and choose what will work best in your case.

Happy hacking!