Packaging java applications as RPMs in maven.

I recently migrated away from using debian based servers to using rhel, and with it came the requirement to repackage all my services as rpms. All my software is packaged as an RPM, this makes it easy to install on bare-metal or within a docker container, with ansible or with puppet. It’s easy to manage and vastly superior to zipping things up. You can ensure that the correct permissions are applied to the files to ensure a secure installation, the appropriate users are created, and have a consistent installation path that makes support and documentation easier.

Up until this migration, I had been using a trusty maven plugin called jdeb which works wonders on both windows and linux. I can’t sing the praises of this plugin enough, it works out of the box cross platform and it’s incredibly simple to use. The cross platform aspect was key for me as I was primarily a windows developer who deployed to a linux environment.

However having recently committed to development on a mac for home projects, and a linux machine at work, the requirement to develop on windows has fallen by the wayside. I’ve provided the above context because the RPM plugin I’m going to talk about only works in an environment that has rpmbuildtools which is exclusively a linux environment. However with the recently release of the Windows Subsystem for Linux (WSL), I’m sure it’s possible to build these projects that use this rpm plugin on windows.

For this article, I will be assuming we want to package a java maven project that has a start script which is generated using the app-assembler plugin.

The requirements for this service are as follows.

  • Have maven generate an rpm
  • The rpm should install the application.
  • The rpm should force a particular Java Runtime to be installed if necessary.
  • The service should be configurable to run automatically on startup
  • Any log directories should be created.
  • The service should be installed under a particular user (which should be created if necessary)
  • The service’s configuration directory should be private and secure
  • The service should clean itself up upon uninstallation
  • The service should preserve data files during upgrades

Lets start with the RPM plugin declaration.

This code goes in the pom of whichever projects have artifacts that need to be packaged as an RPM. In a multi-module maven project it’s fine to have 2 rpm declarations. You just have to do the work for each module. For the purposes of this guide, we’ll only do it to one module.

To assist with installation (like ensuring certain users are created, or certain directories exist) we can create some shell scripts - 4 in total. When a package is installed, these 4 separate scripts get run at different stages of the process. The stages are:

  • preinstall
  • postinstall
  • preuninstall
  • postuninstall

We can give the scripts whatever name we like so long as the name matches in the pom file.

Here’s what happens when you run the install, uninstall and upgrade scenarios respectively

yum install mypackage

# preinstall
# files copied
# postinstall
yum remove mypackage

# preuninstall
# files removed
# postuninstall
yum upgrade mypackage

# preinstall (new package)
# new files copied
# postinstall (new package)

# preuninstall (old package)
# old files removed
# postuninstall (old package)

Notice that there’s no ‘upgrade’ script. On the face of it, it looks like this could be a problem, as we probably only want to delete the data directory if the package is being uninstalled for goot. It turns out that an upgrade can be detected by an argument $1 passed into the script. RPM counts how many installations are currently present and passes this total count in as an argument into the script. The argument represents the number of versions installed. (More precisely, the number that would be installed as a result of the ongoing transaction - recall that an upgrade is 2 transactions; an install and an uninstall)

  • When you install for the first time, the parameter is 1 (in the context of the install scripts).
  • When you upgrade, (which is essentially an additional install followed by an uninstall of the previous version)
    • The parameter value is 2 or more (in the context of the install scripts)
    • The parameter value is 1 or more (in the context of uninstall scripts).
  • When you uninstall entirely, the parameter value is 0 (in the context of uninstall scripts).

Using this information, we can ensure that the install scripts only creates users when $1 == 1 and uninstall only removes users once $1 == 0

Now lets look at how we would define the rpm’s creation in maven.

Start by creating 4 scripts and placing them in a scripts directory in the root of the project.

Then use this for the pom in the <build><plugins> section

  <plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>rpm-maven-plugin</artifactId>
    <version>2.2.0</version>
    <extensions>true</extensions>
    <executions>
      <execution>
        <id>build-rpm</id>
        <goals>
          <goal>attached-rpm</goal>
        </goals>
        <phase>package</phase>
      </execution>
    </executions>
    <configuration>
      <license>MIT (c) $project.inceptionYear $project.organization</license>
      <distribution>Project Distribution</distribution>
      <icon>src/main/resources/icon.xpm</icon>
      <group>Applications/Editor</group>
      <packager>Dan Burrell daniel@solong.co.uk</packager>
      <prefix>/usr/local</prefix>
      <changelogFile>src/changelog</changelogFile>
      <defineStatements>
        <defineStatement>_unpackaged_files_terminate_build 0</defineStatement>
      </defineStatements>
      <mappings>
        <mapping>
          <directory>/opt/companyname/product/repo</directory>
          <filemode>755</filemode>
          <sources>
            <source>
              <location>target/appassembler/repo</location>
            </source>
          </sources>
        </mapping>
        <mapping>
          <directory>/usr/lib/systemd/system/</directory>
          <filemode>644</filemode>
          <sources>
            <source>
              <location>src/main/scripts/service/myproduct.service</location>
              <destination>myproduct.service</destination>
            </source>
          </sources>
          <directoryIncluded>false</directoryIncluded>
          <configuration>false</configuration>
        </mapping>
        <mapping>
          <directory>/opt/companyname/product/bin</directory>
          <filemode>755</filemode>
          <sources>
            <source>
              <location>target/appassembler/bin</location>
            </source>
          </sources>
        </mapping>
      </mappings>
      <preinstallScriptlet>
        <scriptFile>src/main/rpm-scripts/preinstall.sh</scriptFile>
        <fileEncoding>utf-8</fileEncoding>
        <filter>true</filter>
      </preinstallScriptlet>
      <postinstallScriptlet>
        <scriptFile>src/main/rpm-scripts/postinstall.sh</scriptFile>
        <fileEncoding>utf-8</fileEncoding>
        <filter>true</filter>
      </postinstallScriptlet>
      <preremoveScriptlet>
        <scriptFile>src/main/rpm-scripts/preremove.sh</scriptFile>
        <fileEncoding>utf-8</fileEncoding>
        <filter>true</filter>
      </preremoveScriptlet>
      <postremoveScriptlet>
        <scriptFile>src/main/rpm-scripts/postremove.sh</scriptFile>
        <fileEncoding>utf-8</fileEncoding>
        <filter>true</filter>
      </postremoveScriptlet>
      <requires>
        <require>java-1.8.0-openjdk &gt; 1.8</require>
      </requires>
    </configuration>
  </plugin>

  • The use of the filter tags ensures that we can inject relevant property information in all our scripts.
  • The service section ensures we can configure the service to startup automatically (if we enable it).
  • The permissions 644 root:root are correct.
  • You’ll need an xpm file as the icon.
  • The license normally has a license type, but I often put the copyright details there (because I can).
  • Note how we map from the target/appassembler/bin directory where executable scripts which start the application are located.
    • These are mapped to the /bin directory on the destination system under an opt path. Ensure that the filemode is correct for your application. (755 is reasonable).
  • Note how we also map our jar binaries from /repo in the build folder to /rep on the destination system.
    • Again 755 is fine.
  • That last requires section allows us to automatically install java 8 to ensure our service works.

The preinstall script should do things like create users with nologin

   useradd -s /sbin/nologin myuser

Here’s an example .service file

[Unit]
Description=My Service
[Service]
ExecStart=/opt/companyname/product/bin/myproduct
User=myuser
Type=simple
SuccessExitStatus=143
[Install]
WantedBy=multi-user.target

What’s with SuccessExitStatus=143? Well the JVM exits with code 143 when it receives SIGTERM; it conflates its exit status with the SIGTERM received (128+15). WIthout being told that this is fine, systemd will think something went wrong. This solves the problem.

That’s it! Once you know how to invoke the scripts, and what their responsibility is, the rest is fairly easy.