Packaging Applications via JDeb

Suppose you have an application written in java which you want to deploy to an ubuntu based server via puppet. Puppet uses native packaging in order to install packages in an idempotent manner, for ubuntu this means your application must be available as a debian package.

This article explains how to package a service as part of your maven build in a cross platform manner using JDeb with the minimal set of files. The result is a deb file containing your service which can be deployed to your server, installed, configured, and run as a service.

Lets start off by adding the jdeb plugin to the pom.


<plugin>
  <artifactId>jdeb</artifactId>
  <groupId>org.vafer</groupId>
  <version>1.3</version>
  <executions>
    <execution>
      <phase>package</phase>
      <goals>
        <goal>jdeb</goal>
      </goals>
      <configuration>
        <deb>${project.build.directory}/${project.artifactId}.deb</deb>
        <verbose>true</verbose>
        <snapshotExpand>true</snapshotExpand>
        <!-- expand "SNAPSHOT" to what is in the "USER" env variable -->
        <snapshotEnv>USER</snapshotEnv>
        <verbose>true</verbose>
        <controlDir>${basedir}/src/deb/control</controlDir>
        <dataSet>
          <data>
            <src>${project.build.directory}/appassembler/bin</src>
            <type>directory</type>
            <mapper>
              <type>perm</type>
              <prefix>/opt/${company.name}/${project.artifactId}/${project.version}/bin</prefix>
              <user>root</user>
              <group>root</group>
              <filemode>755</filemode>
            </mapper>
          </data>
          <data>
            <src>${project.build.directory}/appassembler</src>
            <excludes>bin/**</excludes>
            <type>directory</type>
            <mapper>
              <type>perm</type>
              <prefix>/opt/${company.name}/${project.artifactId}/${project.version}</prefix>
              <user>root</user>
              <group>root</group>
              <filemode>644</filemode>
            </mapper>
          </data>
          <data>
            <type>template</type>
            <paths>
            <path>/opt/${company.name}/${project.artifactId}/${project.version}/conf</path>
            <path>etc/${project.artifactId}</path>
            <path>var/lib/${project.artifactId}</path>
            <path>var/log/${project.artifactId}</path>
            <path>var/run/${project.artifactId}</path>
            </paths>
            <mapper>
              <type>perm</type>
              <user>loader</user>
              <group>loader</group>
            </mapper>
          </data>
          <data>
            <type>link</type>
            <linkName>/opt/${company.name}/${project.artifactId}/current</linkName>
            <linkTarget>/opt/${company.name}/${project.artifactId}/${project.version}</linkTarget>
            <symlink>true</symlink>
          </data>
          <data>
            <src>src/deb/upstart/${project.artifactId}.conf</src>
            <type>file</type>
            <mapper>
              <type>perm</type>
              <prefix>/etc/init</prefix>
              <filemode>644</filemode>
              <user>root</user>
              <group>root</group>
            </mapper>
          </data>
        </dataSet>
      </configuration>
    </execution>
  </executions>
</plugin>
    

The above config can broken down into the following components:

  • Upstart Script config
  • ‘current version’ symlink creation
  • Config directory creation
  • Start-up scripts directory
  • Control directory declaration
  • Jar capture
  • Install directory creation

Control directory

We must create a control directory in our project to hold the set of files required to build a debian package.

/src/deb/control

Then create a file called control within the above control directory with this exact contents, substituting the maintainer email address only (leave the name, version, description as placeholders):

Package: [[name]]
Version: [[version]]
Section: misc
Priority: low
Architecture: all
Depends: 
Description: [[description]]
Maintainer: name@yourcompany.com

Create the following files with no content in the control directory:

  • src/deb/control/postinst
  • src/deb/control/postrm
  • src/deb/control/preinst
  • src/deb/control/prerm

The above files can be used as part of the debian package install lifecycle (post install, pre install etc). We don’t need to do any configuration so we can leave them without content for now.

Other files

Create another blank file:

/src/deb/init.d/myservice

Upstart directory and script

Create a script named after your project (e.g. myproject.conf) in the /src/deb/upstart directory, replace myproject with the name of your project and company with the name of your company.

/src/deb/upstart/myproject.conf

# vim: set ft=upstart ts=4 et:
description "myproject"

start on runlevel [2345]
stop on runlevel [!2345]

limit nofile 64000 64000

kill timeout 300 # wait 300s between SIGTERM and SIGKILL.

pre-start script
    mkdir -p /var/lib/myproject/
    mkdir -p /var/log/myproject/
end script

script
    ENABLE_MYPROJECT="yes"

    if [ "x$ENABLE_MYPROJECT" = "xyes" ]; then
        exec start-stop-daemon --start --quiet --chuid root  \
            --exec /opt/company/myproject/current/bin/start -- 
    fi
end script

The above script will allow your service to run as a daemon on startup. It also makes sure the log directories are created.

When you build out the project using mvn install, it will compile the jar and create a debian artifact which you can deploy to your server.

The package will be installed at /opt/company/project/version

In addition to the core directory there will be a symlink to the ‘current version’ /opt/company/project/current

Within this installation directory there will be a configuration directory and a binaries directory. You can then use puppet or some other provisioner to create the configuration property files required in the config directory.

Lastly you’ll want to create a set of run scripts which are compatible with the upstart script fork model.

Lets examine the following build plugin

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>appassembler-maven-plugin</artifactId>
    <version>1.8.1</version>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>assemble</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <includeConfigurationDirectoryInClasspath>true</includeConfigurationDirectoryInClasspath>
        <configurationDirectory>conf</configurationDirectory>
        <programs>
            <program>
                <mainClass>uk.co.solong.application.main.spring.java.AutoAnnotationWebApplication</mainClass>
                <id>start</id>
                <jvmSettings>
                    <initialMemorySize>20m</initialMemorySize>
                    <maxMemorySize>256m</maxMemorySize>
                    <maxStackSize>128m</maxStackSize>
                    <systemProperties>
                        <systemProperty>logback.configurationFile=logback.xml</systemProperty>
                        <systemProperty>APP_ENV=prod</systemProperty>
                    </systemProperties>
                </jvmSettings>
            </program>
        </programs>
    </configuration>
</plugin>

In the above script we provide the AutoAnnotationWebApplication class as the Main, and any necessary system properties.

Be sure to include the configuration directory in the class path otherwise properties files will not be visible to the service.

For a concrete example, take a look at the pom used for list.tf