Migrate a Java application to Nomad (Linux)
Unlike many other application schedulers, Nomad can run non-containerized applications. If you have existing Java applications, you can run them directly on Nomad without converting them into containers.
The Nomad Java task driver enables you to run Java applications in your Nomad cluster without the need to containerize them. The Nomad Java task driver also allows you to use Nomad's standardized, declarative job configuration with the familiar configuration elements of typical JVM applications.
Objective
For this tutorial, you will use a sample Java application—Membrane Service Proxy—and:
Identify required values from your startup script and configuration files
Translate them to an equivalent Nomad job specification
Use fundamental job specification stanzas:
group
,task
,driver
,config
, andenv
Download files for a job with
artifact
stanzasInclude configuration elements as
template
stanzas
About the sample application
The Membrane Service Proxy application is an Open Source API Gateway & HTTP reverse proxy for REST & SOAP. The application configuration for this tutorial is based on the Membrane examples and:
- Consumes a REST-style query
- Converts it to a SOAP query
- Proxies it to a remote SOAP server
- Receives the response
- Formats the response based on the
Accept
header - Returns the response to the caller
Note
This tutorial's sample application queries a remote SOAP service that is not maintained by HashiCorp.
Prerequisites
Local dependencies
- The
nomad
binary - Java
curl
Target dependencies
You can deploy the tutorial's job file to a local Nomad dev agent or a remote Nomad cluster.
Start a shell session and run the following command.
$ sudo nomad agent -dev -bind 0.0.0.0
Create a learning environment
This tutorial uses the command line. Open a new shell session to get started.
There is an accompanying GitHub repository that contains the completed Nomad job file for Linux and Windows. It is not required to complete the tutorial but provided for a reference.
Make a working directory
$ mkdir ~/java-guide
$ cd ~/java-guide
Run the workload outside of Nomad
To validate the sample Java workload runs as expected in your environment, try it locally or on a host outside of the Nomad cluster. This tutorial includes the test steps for you.
Download and decompress the application
Download Membrane-SOA to your local machine.
$ curl --location \ --output membrane-service-proxy-4.7.3.zip \ https://github.com/membrane/service-proxy/releases/download/v4.7.3/membrane-service-proxy-4.7.3.zip
Once downloaded, unzip the membrane-service-proxy-4.7.3.zip
file.
$ unzip membrane-service-proxy-4.7.3.zip
Change into the workload directory
$ cd membrane-service-proxy-4.7.3/examples/rest2soap-json
You can consult the README in this folder for more information about the sample; however, you will find the vital information here.
Start the proxy
$ ./service-proxy.sh
Logging will be returned in the terminal. A successful startup is indicated by
a line containing Membrane Service Proxy 4.7.3 up and running!
Observe the baseline behavior
Open a new shell. Verify that the running proxy is behaving as expected by
fetching data from http://localhost:2000/bank/37050198
.
Use the curl
command to fetch the API endpoint.
$ curl http://localhost:2000/bank/37050198
The proxy returns an XML response to your request.
<?xml version="1.0" encoding="UTF-8"?><getBankResponse><details><bezeichnung>Sparkasse KölnBonn</bezeichnung><bic>COLSDE33XXX</bic><ort>Köln</ort><plz>50667</plz></details></getBankResponse>
Now, add the Accept: application/json
header to the request.
$ curl --header "Accept: application/json" http://localhost:2000/bank/37050198
The proxy returns the response in JSON format.
{"getBankResponse": {"details": {"bezeichnung": "Sparkasse KölnBonn","bic": "COLSDE33XXX","ort": "Köln","plz": 50667}}}
Now that you have verified that the proxy is running as expected, close this shell session.
Stop the proxy
Return to the terminal session running the proxy. Press Ctrl-C
to stop it.
The proxy process will stop and return you to a shell prompt.
Now that you have validated the Java application runs as expected, you can migrate it to Nomad.
Read the example startup code
Open the service-proxy.sh
file in a text editor to review its contents.
Key observations
For now, observe that the java
command does the following. This information
is the foundation of your Nomad job specification.
Has a
-classpath
that depends on the value of$CLASSPATH
$CLASSPATH
is based on$MEMBRANE_HOME
, which is the top-level directory created when decompressing the archive.$CLASSPATH
expands to:$MEMBRANE_HOME/conf:$MEMBRANE_HOME/starter.jar
Runs a class named
com.predic8.membrane.core.Starter
Passes the arguments
-c
andproxies.xml
Change into your work directory
$ cd ~/java-guide
Build your Nomad job
Now that you have reviewed the startup script, you can use Nomad's declarative job specification to define the workload.
Required job specification
Create a file named rest2json.nomad.hcl
with the following template Nomad job
specification. This job file is not runnable; however, every element here is
required to create a parsable job.
job "«job_name»" { datacenters = ["«datacenter»"] group "«group_name»" { task "«task_name»" { driver = "«driver_plugin_id»" } }}
Start customizing the template job
- Replace
«job_name»
withrest2json
- Replace
«group_name»
withproxy
- Replace
«task_name»
withmembrane
- Replace
«driver_plugin_id»
withjava
Fetch valid datacenter
values
The datacenters
value is a list of datacenter names as strings.
Run the nomad node status
command, which lists all of the clients in the
target Nomad cluster or dev agent.
$ nomad node statusID DC Name Class Drain Eligibility Statusd715f8b4 dc1 nomad-client-1.node.consul <none> false eligible ready14ab9290 dc1 nomad-client-2.node.consul <none> false eligible ready0f357b26 dc1 nomad-client-3.node.consul <none> false eligible ready
In this output, all of the clients are in a datacenter named dc1
.
Replace «datacenter»
with a valid value for your deployment target.
The remaining tutorial content uses dc1
; which is the default value for
datacenter. Always use the correct value for your deployment target as the
value for this field.
Your job file should look like this now.
job "rest2json" { datacenters = ["dc1"] group "proxy" { task "membrane" { driver = "java" } }}
Migrate the configuration into the job
The Nomad job specification provides job specific configuration to
the task driver using the config
stanza. This stanza tells Nomad how to start the Java
application and contains attributes like class
, class_path
, and args
Create a config
stanza inside of the task "membrane"
stanza with the
information you discovered from reading the startup script.
Set the
class
argument tocom.predic8.membrane.core.Starter
.Set the
class_path
argument to${MEMBRANE_HOME}/conf:${MEMBRANE_HOME}/starter.jar
Finally, set the
args
attribute to a list of quoted arguments:["-c", "proxies.xml"]
This yields the following config stanza.
config { class = "com.predic8.membrane.core.Starter" class_path = "${MEMBRANE_HOME}/conf:${MEMBRANE_HOME}/starter.jar" args = ["-c", "proxies.xml"] }
Download the app with an artifact stanza
Nomad jobs generally fetch the workload as a part of starting up. For Java
workloads, operators typically use the artifact
stanza to download files
into the allocation's filesystem as the task starts up. The artifact stanza
will also decompress archive files automatically by default.
Inside of the task "membrane"
stanza, add an artifact
stanza to fetch the
application from GitHub.
artifact { source = "https://github.com/membrane/service-proxy/releases/download/v4.7.3/membrane-service-proxy-4.7.3.zip" destination = "local" }
This artifact stanza tells Nomad to download the application to the tasks's
working directory named local
. You can learn more about the allocation
Filesystem internals in the Nomad Documentation.
Nomad will automatically unzip the application archive once it is downloaded.
Define the MEMBRANE_HOME environment variable
Add an env
stanza inside of the task "membrane"
stanza. This creates the
MEMBRANE_HOME
environment variable with the appropriate path.
Nomad variable interpolation provides the correct value for
the Nomad task directory ($NOMAD_TASK_DIR
) in the MEMBRANE_HOME
path.
env { MEMBRANE_HOME = "${NOMAD_TASK_DIR}/membrane-service-proxy-4.7.3" }
Generate the configuration files with template stanzas
The template
stanza provides the job specification creator the ability to
generate dynamic content and save it into the Nomad job's working directories.
This is useful for jobs that have runtime-environment specific configuration,
especially when paired with variable interpolation like you used in the env stanza
.
This tutorial uses the template stanza to provide the configuration
files necessary to run the example Java application: proxies.xml
,
get2soap.xsl
and strip-env.xsl
Inside of the task "membrane"
stanza, add the following three template stanzas.
These templates use the heredoc syntax to provide a long, formatted value to
the data
attribute.
proxies.xml
template { destination = "local/proxy-conf/proxies.xml" data =<<EOD<spring:beans xmlns="http://membrane-soa.org/proxies/1/" xmlns:spring="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd http://membrane-soa.org/proxies/1/ http://membrane-soa.org/schemas/proxies-1.xsd"> <router> <serviceProxy port="2000"> <rest2Soap> <mapping regex="/bank/.*" soapAction="" soapURI="/axis2/services/BLZService" requestXSLT="./get2soap.xsl" responseXSLT="./strip-env.xsl" /> </rest2Soap> <target host="thomas-bayer.com" /> </serviceProxy> <serviceProxy name="Console" port="9000"> <adminConsole /> </serviceProxy> </router> </spring:beans>EOD }
get2soap.xsl
template { destination = "local/proxy-conf/get2soap.xsl" data =<<EOD<?xml version="1.0" encoding="UTF-8"?><xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:s11="http://schemas.xmlsoap.org/soap/envelope/"> <xsl:template match="/"> <s11:Envelope > <s11:Body> <blz:getBank xmlns:blz="http://thomas-bayer.com/blz/"> <blz:blz><xsl:value-of select="//path/component[2]"/></blz:blz> </blz:getBank> </s11:Body> </s11:Envelope> </xsl:template></xsl:stylesheet>EOD }
strip-env.xsl
template { destination = "local/proxy-conf/strip-env.xsl" data =<<EOD<?xml version="1.0" encoding="UTF-8"?><xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:s11="http://schemas.xmlsoap.org/soap/envelope/"> <xsl:template match="/"> <xsl:apply-templates select="//s11:Body/*"/> </xsl:template> <xsl:template match="@*|node()"> <xsl:copy> <xsl:apply-templates /> </xsl:copy> </xsl:template> <!-- Get rid of the namespace prefixes in json. So ns1:getBank will be just getBank --> <xsl:template match="*"> <xsl:element name="{local-name()}"> <xsl:apply-templates/> </xsl:element> </xsl:template> </xsl:stylesheet>EOD }
Fix path to proxies.xml
Update the args
value in the config
stanza from "proxies.xml"
to
"local/proxy-conf/proxies.xml"
to reflect where Nomad will write the
rendered templates.
Configure the required network resources
Nomad networking is generally configured at the group
level. This job
requires two network ports to be available.
- Port 2000 - proxies API requests.
- Port 9000 - administration interface for the proxy
Create a network
stanza inside the group "proxy"
stanza.
network { port "admin" { static = 9000 } port "proxy" { static = 2000 } }
Run the job file
At this point, you have a complete Nomad job specification for this Java application. If you think you might have missed a step, click the Show the completed job specification link below to reveal a complete version for comparison. command.
job "rest2json" { datacenters = ["dc1"] group "proxy" { network { port "admin" { static = 9000 } port "proxy" { static = 2000 } } task "membrane" { template { destination = "local/proxy-conf/get2soap.xsl" data =<<EOD<?xml version="1.0" encoding="UTF-8"?><xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:s11="http://schemas.xmlsoap.org/soap/envelope/"> <xsl:template match="/"> <s11:Envelope > <s11:Body> <blz:getBank xmlns:blz="http://thomas-bayer.com/blz/"> <blz:blz><xsl:value-of select="//path/component[2]"/></blz:blz> </blz:getBank> </s11:Body> </s11:Envelope> </xsl:template></xsl:stylesheet>EOD } template { destination = "local/proxy-conf/strip-env.xsl" data =<<EOD<?xml version="1.0" encoding="UTF-8"?><xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:s11="http://schemas.xmlsoap.org/soap/envelope/"> <xsl:template match="/"> <xsl:apply-templates select="//s11:Body/*"/> </xsl:template> <xsl:template match="@*|node()"> <xsl:copy> <xsl:apply-templates /> </xsl:copy> </xsl:template> <!-- Get rid of the namespace prefixes in json. So ns1:getBank will be just getBank --> <xsl:template match="*"> <xsl:element name="{local-name()}"> <xsl:apply-templates/> </xsl:element> </xsl:template> </xsl:stylesheet>EOD } template { destination = "local/proxy-conf/proxies.xml" data =<<EOD<spring:beans xmlns="http://membrane-soa.org/proxies/1/" xmlns:spring="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd http://membrane-soa.org/proxies/1/ http://membrane-soa.org/schemas/proxies-1.xsd"> <router> <serviceProxy port="2000"> <rest2Soap> <mapping regex="/bank/.*" soapAction="" soapURI="/axis2/services/BLZService" requestXSLT="./get2soap.xsl" responseXSLT="./strip-env.xsl" /> </rest2Soap> <target host="thomas-bayer.com" /> </serviceProxy> <serviceProxy name="Console" port="9000"> <adminConsole /> </serviceProxy> </router> </spring:beans>EOD } env { MEMBRANE_HOME="${NOMAD_TASK_DIR}/membrane-service-proxy-4.7.3" } artifact { source = "https://github.com/membrane/service-proxy/releases/download/v4.7.3/membrane-service-proxy-4.7.3.zip" destination = "local" } driver = "java" config { class = "com.predic8.membrane.core.Starter" class_path = "${MEMBRANE_HOME}/conf:${MEMBRANE_HOME}/starter.jar" args = ["-c", "local/proxy-conf/proxies.xml"] } } }}
$ nomad job run rest2json.nomad.hcl
Look for the allocation ID in the output from the nomad job run
command.
==> Monitoring evaluation "13b407ab" Evaluation triggered by job "rest2json" Allocation "38e715aa" created: node "d715f8b4", group "proxy"==> Monitoring evaluation "13b407ab" Evaluation within deployment: "c1be19df" Evaluation status changed: "pending" -> "complete"==> Evaluation "13b407ab" finished with status "complete"
In this sample output, the allocation ID is 38e715aa
.
Check the allocation health
Use the nomad alloc status
command to view the state of the allocation.
Replace the sample allocation ID with your actual allocation ID.
$ nomad alloc status 38e715aa ID = 38e715aa-7635-adee-634a-fe0026c2baadEval ID = 13b407abName = soap-proxy.membrane[0]Node ID = d715f8b4Node Name = nomad-client-1.node.consulJob ID = soap-proxyJob Version = 0Client Status = runningClient Description = Tasks are runningDesired Status = runDesired Description = <none>Created = 5m5s agoModified = 4m22s agoDeployment ID = de2f0a4fDeployment Health = unset Allocation AddressesLabel Dynamic Address*admin yes 10.0.2.51:9000*proxy yes 10.0.2.51:2000 Task "membrane" is "running"Task ResourcesCPU Memory Disk Addresses2/500 MHz 67 MiB/256 MiB 300 MiB Task Events:Started At = 2021-04-08T21:15:31ZFinished At = N/ATotal Restarts = 0Last Restart = N/A Recent Events:Time Type Description2021-04-08T17:15:31-04:00 Started Task started by client2021-04-08T17:15:28-04:00 Downloading Artifacts Client is downloading artifacts2021-04-08T17:14:49-04:00 Task Setup Building Task Directory2021-04-08T17:14:49-04:00 Received Task received by client
Troubleshooting
If the application doesn't start, run the nomad alloc logs -stderr
command
passing in the allocation ID.
nomad alloc logs -stderr 38e715aa
Common errors
Following are a few common error cases you might encounter while migrating the sample job to Nomad and troubleshooting tips.
Could not find or load main class
Error: Could not find or load main class com.predic8.membrane.core.Starter
Common causes for this error are making an error in the class_path
value or using
the incorrect separator for your operating system: Linux uses colons (:
)
where Windows uses semicolons (;
).
Class not found
ClassNotFoundException: com.predic8.membrane.core.RouterCLI
This error indicates that MEMBRANE_HOME is not set properly. Verify that your job contains the template stanza that produces it.
Make requests to the running application
Verify that the running proxy application is behaving as expected by fetching data from it.
Running the job on a remote Nomad cluster
When running the job on a remote Nomad cluster:
The Nomad client running the job must allow connections on port 2000 and port 9000. This guide does not cover using dynamic ports.
Use the
nomad alloc status
command to obtain the address of the proxy. It's displayed in the Allocation Addresses section of the output.Example output displaying the proxy application's address
$ nomad alloc status 41e605ba[...]Allocation AddressesLabel Dynamic Address*admin yes 67.122.55.51:9000*proxy yes 67.122.55.51:2000[...]
Use the curl
command to fetch the API endpoint.
Note
If you are running the job on a remote Nomad cluster, remember to replace localhost
in the following commands with the deployed proxy's allocation address found in the output of the nomad alloc status
command.
$ curl http://localhost:2000/bank/37050198
The proxy returns an XML response to the request.
<?xml version="1.0" encoding="UTF-8"?><getBankResponse><details><bezeichnung>Sparkasse KölnBonn</bezeichnung><bic>COLSDE33XXX</bic><ort>Köln</ort><plz>50667</plz></details></getBankResponse>
Now, add the Accept: application/json
header to the request.
$ curl --header "Accept: application/json" \ http://localhost:2000/bank/37050198
The proxy returns the response in JSON format.
{"getBankResponse": {"details": {"bezeichnung": "Sparkasse KölnBonn","bic": "COLSDE33XXX","ort": "Köln","plz": 50667}}}
Clean up
Now that you have run and tested a Java workload in Nomad, clean up your learning environment.
Stop the Nomad job
Stop the tutorial job with the nomad job stop
command.
$ nomad job stop rest2json
Stop dev agent (if applicable)
If you are running a local Nomad dev agent, you can switch to the shell
session containing it, stop Nomad with Ctrl-C
, and then exit the session.
Remove the tutorial's working directory
Ensure that you have closed any file editors that might have files in the working directory open.
Change to your home directory and recursively remove the java-guide
folder
you created in the first step.
$ cd ~;$ rm -rf java-guide
Review what you learned
Nomad's Java task driver reduces time to deploy non-containerized Java applications into a cluster.
The artifact
stanza is used to download items into a Nomad allocation as it
is started. Read more in the artifact
stanza documentation.
Linux. The Nomad Java task driver uses a chroot
to provides additional
isolation. Read more in the Resource Isolation section of the Java task
driver documentation.
Next steps
You can read more about the specifics of the Java task driver in the Nomad Documentation.