A customer of ITQ is running SDL Tridion content management software and has asked us to deliver a proof-of-concept of running a Tridion website and the Tridion 8.5 micro services on Pivotal CloudFoundry. This post is a journal of my attempts of deploying the SDL Web 8.5 Discovery Service on CloudFoundry.

This is just part 1 of a series of unknown length (at the moment of writing). Here are all parts:

  1. Deploy Tridion SDL Web 8.5 Discovery Service on Pivotal CloudFoundry (part 1) (this post)
  2. Deploy Tridion SDL Web 8.5 Discovery Service on Pivotal CloudFoundry (part 2)

The discovery service is distributed as a binary Spring Boot application with the following directory structure:

│README.md
├bin
│    start.sh
│    stop.sh
├config
│    application.properties
│    cd_ambient_conf.xml
│    cd_ambient_conf.xml.org
│    cd_storage_conf.xml
│    logback.xml
│    serviceName.txt
├lib
│   ....
│   service-container-core-8.5.0-1014.jar
│   ....
└services
    ├discovery-service
    └odata-v4-framework

So there’s a bin folder with a start and stop script, some configuration and a lib folder that has a lot of jar files, including the one with our main class.

Binary buildpack

Since this is a binary distribution of a micro service, I first tried the CloudFoundry binary buildpack. A build pack is a small piece of software that takes your source code, compiles it and runs it on CloudFoundry (this is a very simplistic explanation). Let’s see how far the binary buildpack gets us.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
$ cf push discovery_service -b binary_buildpack -c './bin/start.sh' -i 1 -m 128m
Creating app discovery_service in org PCF / space Test as admin...
OK

Creating route discovery-service.cf-prod.intranet...
OK

Binding discovery-service.cf-prod.intranet to discovery_service...
OK

Uploading discovery_service...
Uploading app files from: /home/wildenbergr/microservices/discovery
Uploading 7.2M, 72 files
Done uploading
OK

Starting app discovery_service in org PCF / space Test as admin...
Downloading binary_buildpack...
Downloaded binary_buildpack
Creating container
Successfully created container
Downloading app package...
Downloaded app package (59.3M)
Staging...
-------> Buildpack version 1.0.13
Exit status 0
Staging complete
Uploading droplet, build artifacts cache...
Uploading build artifacts cache...
Uploading droplet...
Uploaded build artifacts cache (200B)
Uploaded droplet (59.3M)
Uploading complete
Destroying container
Successfully destroyed container

0 of 1 instances running, 1 crashed
FAILED
Error restarting application: Start unsuccessful

TIP: use 'cf logs discovery_service --recent' for more information
$ 

Obviously, the deploy did not go as planned so let’s check the logs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
$ cf logs discovery_service --recent
Retrieving logs for app discovery_service in org PCF / space Test as admin...

[API/0] OUT Created app with guid fd8dd243-bc3f-4a26-83f7-44b8a06d95dd
[API/1] OUT Updated app with guid fd8dd243-bc3f-4a26-83f7-44b8a06d95dd ({"route"=>"5c279e23-17a0-48d6-b6dd-0c7fe8cbf17b", :verb=>"add", :relation=>"routes", :related_guid=>"5c279e23-17a0-48d6-b6dd-0c7fe8cbf17b"})
[API/0] OUT Updated app with guid fd8dd243-bc3f-4a26-83f7-44b8a06d95dd ({"state"=>"STARTED"})
[STG/0] OUT Downloading binary_buildpack...
[STG/0] OUT Downloaded binary_buildpack
[STG/0] OUT Creating container
[STG/0] OUT Successfully created container
[STG/0] OUT Downloading app package...
[STG/0] OUT Downloaded app package (59.3M)
[STG/0] OUT Staging...
[STG/0] OUT -------> Buildpack version 1.0.13
[STG/0] OUT Exit status 0
[STG/0] OUT Staging complete
[STG/0] OUT Uploading droplet, build artifacts cache...
[STG/0] OUT Uploading build artifacts cache...
[STG/0] OUT Uploading droplet...
[STG/0] OUT Uploaded build artifacts cache (200B)
[STG/0] OUT Uploaded droplet (59.3M)
[STG/0] OUT Uploading complete
[STG/0] OUT Destroying container
[CELL/0] OUT Creating container
[CELL/0] OUT Successfully created container
[STG/0] OUT Successfully destroyed container
[CELL/0] OUT Starting health monitoring of container
[APP/PROC/WEB/0] OUT Starting service.
[APP/PROC/WEB/0] ERR ./bin/start.sh: line 49: java: command not found
[APP/PROC/WEB/0] OUT Exit status 0
[CELL/0] OUT Exit status 143
[CELL/0] OUT Destroying container
[API/2] OUT Process has crashed with type: "web"
[API/2] OUT App instance exited with guid fd8dd243-bc3f-4a26-83f7-44b8a06d95dd payload: {"instance"=>"", "index"=>0, "reason"=>"CRASHED", "exit_description"=>"2 error(s) occurred:\n\n* 2 error(s) occurred:\n\n* Codependent step exited\n* cancelled\n* cancelled", "crash_count"=>1, "crash_timestamp"=>1512986370928003691, "version"=>"26a55501-fbae-4e1e-87d0-4704f9ad0c78"}

And there we have it at line 29: the java command was not found. Makes sense of course because we used the binary buildpack that doesn’t know anything about Java.

Java buildpack

Ok, so the binary buildpack is a no-go. This would suggest we go with the Java buildpack. On the other hand, this buildpack by default assumes you push source code that needs to be compiled. Let’s see what happens.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
$ cf push discovery_service -b java_buildpack_offline -c './bin/start.sh' -i 1 -m 128m
Updating app discovery_service in org PCF / space Test as admin...
OK

Uploading discovery_service...
Uploading app files from: /home/wildenbergr/microservices/discovery
Uploading 7.2M, 72 files
Done uploading
OK

Stopping app discovery_service in org PCF / space Test as admin...
OK

Starting app discovery_service in org PCF / space Test as admin...
Downloading java_buildpack_offline...
Downloaded java_buildpack_offline
Creating container
Successfully created container
Downloading app package...
Downloaded app package (59.3M)
Downloading build artifacts cache...
Downloaded build artifacts cache (200B)
Staging...
-----> Java Buildpack Version: v3.17 (offline) | https://github.com/cloudfoundry/java-buildpack.git#87fb619
[Buildpack]                      ERROR Compile failed with exception #<RuntimeError: No container can run this application. Please ensure that you've pushed a valid JVM artifact or artifacts using the -p command line argument or path manifest entry. Information about valid JVM artifacts can be found at https://github.com/cloudfoundry/java-buildpack#additional-documentation. >
No container can run this application. Please ensure that you've pushed a valid JVM artifact or artifacts using the -p command line argument or path manifest entry. Information about valid JVM artifacts can be found at https://github.com/cloudfoundry/java-buildpack#additional-documentation.
Failed to compile droplet
Exit status 223
Staging failed: Exited with status 223
Destroying container
Successfully destroyed container

FAILED
Error restarting application: BuildpackCompileFailed

TIP: use 'cf logs discovery_service --recent' for more information

And this fails as well. The Java buildpack doesn’t understand what we are pushing. So with the binary buildpack we can run a shell script but we do not have java. With the Java buildpack we have java but it doesn’t understand the artifact we’re pushing. What to do?

Java buildpack with main() method

Digging around in the Java buildpack documentation, it looks like there is an option to run a self-executable jar file. The jar file we’d like to execute is lib/service-container-core-8.5.0-1014.jar. Let’s take a look at the start.sh script that is normally used to run the discovery micro service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/usr/bin/env bash

# Java options and system properties to pass to the JVM when starting the service. For example:
# JVM_OPTIONS="-Xrs -Xms128m -Xmx128m -Dmy.system.property=/var/share"
JVM_OPTIONS="-Xrs -Xms128m -Xmx128m"
SERVER_PORT=--server.port=8082

# set max size of request header to 64Kb
MAX_HTTP_HEADER_SIZE=--server.tomcat.max-http-header-size=65536

BASEDIR=$(dirname $0)
CLASS_PATH=.:config:bin:lib/*
CLASS_NAME="com.sdl.delivery.service.ServiceContainer"

cd $BASEDIR/..
ARGUMENTS=()
for ARG in $@
do
    if [[ $ARG == --server\.port=* ]]
    then
        SERVER_PORT=$ARG
    elif [[ $ARG =~ -D.+ ]]; then
    	JVM_OPTIONS=$JVM_OPTIONS" "$ARG
    else
        ARGUMENTS+=($ARG)
    fi
done
ARGUMENTS+=($SERVER_PORT)
ARGUMENTS+=($MAX_HTTP_HEADER_SIZE)

for SERVICE_DIR in `find services -type d`
do
    CLASS_PATH=$SERVICE_DIR:$SERVICE_DIR/*:$CLASS_PATH
done

echo "Starting service."

java -cp $CLASS_PATH $JVM_OPTIONS $CLASS_NAME ${ARGUMENTS[@]}

A lot is going on in here but in the end the script runs the java command with a classpath, a main class and some options. Maybe we can accomplish the same with the Java buildpack. So, first let’s create a manifest.yml file in the root of the micro service folder structure:

---
applications:
- name: discovery_service
  path: lib/service-container-core-8.5.0-1014.jar
  buildpack: java_buildpack_offline

The path points to the jar file that has the class com.sdl.delivery.service.ServiceContainer with a main() method. However, if we deploy with this manifest, we get the same error: No container can run this application. So what is going on?

When running a Java application directly from a jar file, java has to know which class has the main() method. You can specify this on the command line or inside a manifest file inside the jar file. The service-container-core-8.5.0-1014.jar manifest file does not have a Main-Class entry so we have to specify it on the command line. How to do that?

Digging some more through the Java buildpack documentation I found that you can override buildpack settings by setting application environment variables. In our case, we want to override settings from the config/java_main.yml file so we update our manifest.yml file again:

---
applications:
- name: discovery-service
  path: lib/service-container-core-8.5.0-1014.jar
  buildpack: java_buildpack_offline
  env:
    JBP_CONFIG_JAVA_MAIN: '{ java_main_class: "com.sdl.delivery.service.ServiceContainer", arguments: "-Xrs -Xms128m -Xmx128m" }'
    JBP_LOG_LEVEL: DEBUG

Let’s see what happens this time:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[CELL/0] OUT Creating container
[CELL/0] OUT Successfully created container
[STG/0] OUT Successfully destroyed container
[CELL/0] OUT Starting health monitoring of container
[APP/PROC/WEB/0] ERR Exception in thread "main" java.lang.NoClassDefFoundError: org/slf4j/LoggerFactory
[APP/PROC/WEB/0] ERR     at com.sdl.delivery.service.ServiceContainer.<clinit>(ServiceContainer.java:57)
[APP/PROC/WEB/0] ERR Caused by: java.lang.ClassNotFoundException: org.slf4j.LoggerFactory
[APP/PROC/WEB/0] ERR     at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
[APP/PROC/WEB/0] ERR     at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
[APP/PROC/WEB/0] ERR     at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
[APP/PROC/WEB/0] ERR     at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
[APP/PROC/WEB/0] ERR     ... 1 more
[APP/PROC/WEB/0] OUT Exit status 1
[CELL/0] OUT Exit status 0
[CELL/0] OUT Destroying container
[API/0] OUT Process has crashed with type: "web"
[API/0] OUT App instance exited with guid da7e3f48-151b-4d9a-9df6-cc8479efa839 payload: {"instance"=>"", "index"=>0, "reason"=>"CRASHED", "exit_description"=>"2 error(s) occurred:\n\n* 2 error(s) occurred:\n\n* Exited with status 1\n* cancelled\n* cancelled", "crash_count"=>1, "crash_timestamp"=>1513012904337368910, "version"=>"f75e2238-95dd-45ed-9d7f-66c6c3ef4d7f"}

Now it seems we’re getting somewhere: a NoClassDefFoundError for org/slf4j/LoggerFactory. This means that at least we managed to start a Java process, whoopdeedoo! So now we have to find the missing classes by adding them to the classpath somehow. This is where it all started to get complicated. There is no way I could find to add additional jar files to the classpath in the chosen setup. In fact, this setup has a serious flaw. The documentation for cf push on ‘how it finds the application’ states: if the path is to a file, cf push pushes only that file. So this is never going to work because we need a whole bunch of other files.

Java buildpack with main() method and explicit command

So, what’s next? Luckily, a colleague of mine who knows his way around CloudFoundry, found this blog post. The idea is to specify a number of settings to trick the buildpack into doing what we want (repeating some stuff from the aforementioned post in my own words):

  1. In the buildpack detect phase, we want to make sure the correct container is chosen: java-main. We force this by setting the JBP_CONFIG_JAVA_MAIN environment variable as before.
  2. For the buildpack compile phase, we need all the artifacts from the Tridion Discovery Microservice folder. So we specify a path of ./. Since we use the java-main container we do not really have a compile phase but we still need all microservice files.
  3. In the buildpack release phase we want to run our own Java command that has everything we want on the classpath. We can do this by explicitly specifying a command in our manifest.yml file.

Given these requirements, we come up with the following manifest file:

---
applications:
- name: discovery_service
  path: ./
  buildpack: java_buildpack_offline
  command: $PWD/.java-buildpack/open_jdk_jre/bin/java -cp $PWD/*:.:$PWD/lib/*:$PWD/config/* com.sdl.delivery.service.ServiceContainer -Xrs -Xms128m -Xmx128m
  env:
    JBP_CONFIG_JAVA_MAIN: '{ java_main_class: "com.sdl.delivery.service.ServiceContainer", arguments: "-Xrs -Xms128m -Xmx128m" }'
    JBP_LOG_LEVEL: DEBUG

And if we push the app this time, it works!!

App discovery_service was started using this command `$PWD/.java-buildpack/open_jdk_jre/bin/java -cp $PWD/*:.:$PWD/lib/*:$PWD/config/* com.sdl.delivery.service.ServiceContainer -Xrs -Xms128m -Xmx128m`

Showing health and status for app discovery_service in org PCF / space Test as admin...
OK

requested state: started
instances: 1/1
usage: 1G x 1 instances
urls: discovery-service.test-cf-prod.intranet
last uploaded: Tue Dec 12 15:05:32 UTC 2017
stack: cflinuxfs2
buildpack: java_buildpack_offline

     state     since                    cpu    memory    disk      details
#0   running   2017-12-12 04:06:04 PM   0.0%   0 of 1G   0 of 1G

You see that our new command is used, making everything we want available on the classpath. You may wonder, how did we know that the location of the java executable was $PWD/.java-buildpack/open_jdk_jre/bin/java (besides from the blog post I referred to earlier). This is where the JBP_LOG_LEVEL environment variable comes in. It is a variable specific to the Java buildpack that tells it to generate debug output. Part of the output is the exact command the buildpack will execute (if you do not specify your own command).