JSF in the cloud : Finding the PaaS - Part 5 : Heroku
Basic usages
Heroku is often mentionned as an example of PaaS provider. Although I found a few opinions stating that «Java is a second or third class passenger on Heroku», I wanted to test it myself.
Creating a free account is straightforward. Language specific tutorials are provided. I followed the one dedicated to Java.
Requirements, apart from an heroku account, are just «Java» (no version mentionned, but the example in the tutorial i Java 7) and Maven 3.
CLI tools should be downloaded and are available for Debian, Windows, OS X and «standalone» (?). As a debian user, I followed the instructions :
wget -qO- https://toolbelt.heroku.com/install-ubuntu.sh | sh
This :
- adds a package repository :
deb http://toolbelt.heroku.com/ubuntu ./
- installs two packages :
foreman
andheroku
.
After that, you have to login on heroku using the
heroku login
commands.
It checks your account/password then let you choose the SSH public key you want to upload.
The getting started tutorial then let you clone an example project. Its pom.xml is a bit surprising :
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <version>1.0-SNAPSHOT</version> <artifactId>helloworld</artifactId> <dependencies> <dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-servlet</artifactId> <version>7.6.0.v20120127</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> </dependency> <dependency> <groupId>postgresql</groupId> <artifactId>postgresql</artifactId> <version>9.0-801.jdbc4</version> </dependency> </dependencies> <build> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>2.3.2</version> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <version>2.4</version> <executions> <execution> <id>copy-dependencies</id> <phase>package</phase> <goals><goal>copy-dependencies</goal></goals> </execution> </executions> </plugin> </plugins> </build> </project>
The target artefact is not a war but a jar. The only class seems to be some kind of autosufficient servlet. It uses jetty 7.6.0. It uses the dependency-plugin to copy jar dependencies in the target/dependency subdir.
The recommended way to proceed if to use a beta feature : creating a git repo where the project will be pushed.
$ heroku create --http-git Creating glacial-sands-3101... done, stack is cedar-14 https://glacial-sands-3101.herokuapp.com/ | https://git.heroku.com/glacial-sands-3101.git Git remote heroku added
Then push the code to remote master branch :
$ git push heroku master Décompte des objets: 33, fait. Delta compression using up to 2 threads. Compression des objets: 100% (27/27), fait. Écriture des objets: 100% (33/33), 4.89 KiB | 0 bytes/s, fait. Total 33 (delta 12), reused 0 (delta 0) remote: Compressing source files... done. remote: Building source: remote: remote: -----> Java app detected remote: -----> Installing OpenJDK 1.7... done remote: -----> Installing Maven 3.2.3... done remote: -----> executing /app/tmp/cache/.maven/bin/mvn -B -Duser.home=/tmp/build_5d419e548f5f5358e7f369fc9f0f73e0 -Dmaven.repo.local=/app/tmp/cache/.m2/repository -DskipTests=true clean install remote: [INFO] Scanning for projects... remote: [INFO] remote: [INFO] ------------------------------------------------------------------------ remote: [INFO] Building helloworld 1.0-SNAPSHOT remote: [INFO] ------------------------------------------------------------------------ remote: [INFO] Downloading: https://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-clean-plugin/2.5/maven-clean-plugin-2.5.pom ... remote: [INFO] Downloaded: https://repo.maven.apache.org/maven2/org/codehaus/plexus/plexus-utils/3.0.5/plexus-utils-3.0.5.jar (226 KB at 6619.0 KB/sec) remote: [INFO] Installing /tmp/build_5d419e548f5f5358e7f369fc9f0f73e0/target/helloworld-1.0-SNAPSHOT.jar to /app/tmp/cache/.m2/repository/com/example/helloworld/1.0-SNAPSHOT/helloworld-1.0-SNAPSHOT.jar remote: [INFO] Installing /tmp/build_5d419e548f5f5358e7f369fc9f0f73e0/pom.xml to /app/tmp/cache/.m2/repository/com/example/helloworld/1.0-SNAPSHOT/helloworld-1.0-SNAPSHOT.pom remote: [INFO] ------------------------------------------------------------------------ remote: [INFO] BUILD SUCCESS remote: [INFO] ------------------------------------------------------------------------ remote: [INFO] Total time: 9.911 s remote: [INFO] Finished at: 2014-12-03T09:56:38+00:00 remote: [INFO] Final Memory: 19M/644M remote: [INFO] ------------------------------------------------------------------------ remote: -----> Discovering process types remote: Procfile declares types -> web remote: remote: -----> Compressing... done, 62.8MB remote: -----> Launching... done, v6 remote: https://glacial-sands-3101.herokuapp.com/ deployed to Heroku remote: remote: Verifying deploy... done. To https://git.heroku.com/glacial-sands-3101.git * [new branch] master -> master
this automatically builds the artefact, then launch it using the Procfile :
web: java -cp target/classes:target/dependency/* Main
The app is then up and running.
The tutorial indicates then how to view logs :
heroku logs --tail
The tutorial then gives a hint on how to scale an app, manually allocating nodes using heroku ps
.
Then, like Google App Engine, the tutorial indicates how to run the app locally using foreman :
$ foreman start web 11:09:05 web.1 | started with pid 8160 11:09:05 web.1 | 2014-12-03 11:09:05.695:INFO:oejs.Server:jetty-7.6.0.v20120127 11:09:05 web.1 | 2014-12-03 11:09:05.760:INFO:oejsh.ContextHandler:started o.e.j.s.ServletContextHandler{/,null} 11:09:05 web.1 | 2014-12-03 11:09:05.822:INFO:oejs.AbstractConnector:Started SelectChannelConnector@0.0.0.0:5000
This is something that I really appreciate. Local (and thus fast) debugging is mandatory in most complex projects.
I just skipped the part on pushing changes with maven (it is just @@git push heroku master@...).
Next, the tutorial introduces add-ons.
As I guessed, an add-on was already configured : the free "postgresql instance" I provisionned when creating my account.
$ heroku addons === glacial-sands-3101 Configured Add-ons heroku-postgresql:hobby-dev HEROKU_POSTGRESQL_MAROON
Heroku allows the execution of remote commands, using what it calls a "one-off dyno". Dyno are, in a nutshell, nodes instantiated for your apps. Command execution is performed by nodes instantiated on demand, in opposition to the always running web nodes. That why they are called "off nodes".
Lauching bash is straightforward :
$ heroku run bash Running `bash` attached to terminal... up, run.4406 ~ $
The filesystem seems to be the same as the webapp.
The tutorial finally deals with environment variables and database (PostgreSQL).
Running a very simple JSF app
I then wanted to run this very simple test, which uses a PrimeFaces 5.1 / MyFaces 2.2.6 / OpenWebBeans 1.2.6 / DeltaSpike 1.1 stack.
Heroku features detailed information on how to get a Tomcat webapp up and running.
I followed the recommendation and added the maven-dependency-plugin declaration, as a heroku profile :
<profile> <id>heroku</id> <properties> <cloud.extension>-heroku</cloud.extension> </properties> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <version>2.3</version> <executions> <execution> <phase>package</phase> <goals><goal>copy</goal></goals> <configuration> <artifactItems> <artifactItem> <groupId>com.github.jsimone</groupId> <artifactId>webapp-runner</artifactId> <version>7.0.40.0</version> <destFileName>webapp-runner.jar</destFileName> </artifactItem> </artifactItems> </configuration> </execution> </executions> </plugin> </plugins> </build> </profile>
Then, I build the project using :
$ mvn -Pheroku clean install
and tried to run it using :
$ java -jar target/dependency/webapp-runner.jar target/test-heroku##1.0-SNAPSHOT.war
It did not work at first. I got exactly the same problem I had with OpenShift : it seems that my beans were not instantiated.
Suspecting some file-scanning problem, I tried again with the --expand-war
parameter... And it worked !
Following the tutorial, I added the following Procfile :
web: java $JAVA_OPTS -jar target/dependency/webapp-runner.jar --port $PORT --expand-war target/*.war
Then I created the app, requesting that it runs in the European Union and sepcifying a name (because test is, well, widely used. ).
$ heroku create --region eu testlp-heroku
and uploaded the app
$ git push heroku master
...and it did not work. The reason was simple : the heroku profile was not activated, the webapp-runner was not downloaded and not found, as you can se in the logs :
lpenet@dsi-lpenet-personnel:~/heroku/test-jelastic$ heroku logs --tail 2014-12-03T12:54:14.883097+00:00 heroku[api]: Enable Logplex by ludovic@penet.org 2014-12-03T12:54:14.883131+00:00 heroku[api]: Release v2 created by ludovic@penet.org 2014-12-03T12:55:37+00:00 heroku[slug-compiler]: Slug compilation started 2014-12-03T12:57:11.381707+00:00 heroku[api]: Scale to web=1 by ludovic@penet.org 2014-12-03T12:57:11.913533+00:00 heroku[api]: Set DATABASE_URL config vars by ludovic@penet.org 2014-12-03T12:57:11.913616+00:00 heroku[api]: Release v3 created by ludovic@penet.org 2014-12-03T12:57:11.996784+00:00 heroku[api]: Attach HEROKU_POSTGRESQL_CYAN resource by ludovic@penet.org 2014-12-03T12:57:11.996784+00:00 heroku[api]: Release v4 created by ludovic@penet.org 2014-12-03T12:57:12+00:00 heroku[slug-compiler]: Slug compilation finished 2014-12-03T12:57:12.109260+00:00 heroku[api]: Set PATH, JAVA_OPTS config vars by ludovic@penet.org 2014-12-03T12:57:12.109289+00:00 heroku[api]: Release v5 created by ludovic@penet.org 2014-12-03T12:57:12.178189+00:00 heroku[api]: Deploy 8b59698 by ludovic@penet.org 2014-12-03T12:57:12.178189+00:00 heroku[api]: Release v6 created by ludovic@penet.org 2014-12-03T12:57:21.961334+00:00 app[web.1]: Error: Unable to access jarfile target/dependency/webapp-runner.jar 2014-12-03T12:57:22.898521+00:00 heroku[web.1]: State changed from starting to crashed 2014-12-03T12:57:22.899254+00:00 heroku[web.1]: State changed from crashed to starting 2014-12-03T12:57:21.452363+00:00 heroku[web.1]: Starting process with command `java -Xss512k -XX:+UseCompressedOops -jar target/dependency/webapp-runner.jar --port 35347 --expand-war target/*.war` 2014-12-03T12:57:22.895807+00:00 heroku[web.1]: Process exited with status 1 2014-12-03T12:57:36.194058+00:00 app[web.1]: Error: Unable to access jarfile target/dependency/webapp-runner.jar 2014-12-03T12:57:35.466658+00:00 heroku[web.1]: Starting process with command `java -Xss512k -XX:+UseCompressedOops -jar target/dependency/webapp-runner.jar --port 11978 --expand-war target/*.war` 2014-12-03T12:57:37.051245+00:00 heroku[web.1]: Process exited with status 1 2014-12-03T12:57:39.153329+00:00 heroku[router]: at=error code=H10 desc="App crashed" method=GET path="/" host=testlp-heroku.herokuapp.com request_id=dd20e9a1-0038-48af-bbea-34998ffacb0a fwd="83.202.122.226" dyno= connect= service= status=503 bytes= 2014-12-03T12:57:39.804428+00:00 heroku[router]: at=error code=H10 desc="App crashed" method=GET path="/favicon.ico" host=testlp-heroku.herokuapp.com request_id=dcd0ea94-4895-46f2-95dd-75f372778ad2 fwd="83.202.122.226" dyno= connect= service= status=503 bytes= 2014-12-03T12:57:37.060623+00:00 heroku[web.1]: State changed from starting to crashed
So I added an automatic activation when an heroku specific variable is defined, DYNO :
<activation> <property> <name>env.DYNO</name> </property> </activation>
... and it worked like a charm.
More complex example, with PostgreSQL database
I then decided to run my (a little) more complex test, which features JPA 2 / Hibernate 4.2 / PostgreSQL.
So, I added a Procfile and copied in pom.xml the heroku profile from the simple test.
I wanted to avoid copying the DB parameters in my application context.xml.
So, I first tried to use an observer of the PostConstructApplicationEvent. This bean was in an additional build tree, src/heroku/java, that is added using the build-helper-maven-plugin. However, it failed because the InitialContext was not accessible.
So, I switched back to using environment variables that will then be used as username, password and URL in the resource declared in context.xml. It leverages heroku's environment variable management features.
Despite a lot of efforts, I could not get the JNDI part up and running. It it rather disappointing. Heroku java documentation explains how to instantiate a connection programmatically but, well, we are in 2014 and this should be possible using xml configuration files and annotations.
Conclusion
Heroku is a pleasant alternative for a developper, mostly because of the ability to execute locally the webapp in a controlled environment. However, the lack of netbeans support is disappointing, along with the lack of JNDI support. AFAIK, there is no private PaaS version.