Cómo deployar en Heroku

(Y no morir en el intento)

Buenas! Este documento tiene como objetivo funcionar como una pequeña guía de cómo deployar una aplicación de Java usando Maven en Heroku. Así que, si están tratando de usar esto para deployar otra cosa (por ejemplo, Ruby con sus Gems), puede ser que les sirva hasta cierto punto el cómo usar Heroku, pero está enfocado en facilitar el deploy de Java-Maven.

Configuración inicial

Antes que nada, vamos a necesitar tener el CLI de Heroku instalado para poder deployar. Está disponible para Windows, Mac y Linux, así que no debería haber mucho problema. Una vez lo tengamos instalado, vamos a necesitar loguearnos, lo hacemos sencillamente con heroku login. Esto nos va a abrir una ventana nueva en el navegador y nos logueamos por ahí. Una vez hecho esto, el CLI de Heroku nos dará por autenticados y ya podremos trabajar libremente con Heroku.

También vamos a necesitar Git. En caso de no tenerlo instalado, acá están todas las opciones.

Lo primero que vamos a hacer, va a ser agregar estos dos plugins al pom:

<plugin>

    <artifactId>maven-assembly-plugin</artifactId>

    <executions>

        <execution>

            <phase>package</phase>

             <goals>

                 <goal>single</goal>

             </goals>

         </execution>

     </executions>

     <configuration>

         <descriptorRefs>

             <!-- This tells Maven to include all dependencies -->

             <descriptorRef>jar-with-dependencies</descriptorRef>

         </descriptorRefs>

         <archive>

             <manifest>

                 <mainClass>archivo.con.el.Main</mainClass>

             </manifest>

         </archive>

    </configuration>

</plugin>

<plugin>

    <groupId>com.heroku.sdk</groupId>

    <artifactId>heroku-maven-plugin</artifactId>

    <configuration>

        <jdkVersion>1.8</jdkVersion>

        <!-- Use your own application name -->

        <appName>heroku-test</appName>

        <processTypes>

            <!-- Tell Heroku how to launch your application -->

            <!-- You might have to remove the ./ in front   -->

            <web>java -jar target/nombre-de-la-app-with-dependencies.jar</web>

        </processTypes>

    </configuration>

</plugin>

El primer plugin lo que hace es armar un jar resolviendo todas las dependencias del proyecto. Esto se debe a que en Heroku no vamos a poder correr un mvn clean install, así que necesitamos el jar con todas las dependencias empaquetadas para que pueda ejecutar. En el tag <mainClass>, hay que poner el path completo (o sea, el paquete hasta el nombre de la clase) de la clase que contiene nuestro main.

El jar generado por este plugin entonces pasa a ser nombre-de-la-app-with-dependencies.jar. Dado que es probable que hagamos varios releases de la app, y vamos a querer cambiar la etiqueta <version> del pom, recomiendo ampliamente agregar el tag <finalName>${artifactId}</finalName> dentro del build, para no andar cambiando el plugin de Heroku todo el tiempo. Esto se podría automatizar con un pipeline, que si bien Heroku ofrece, no se aborda en esta guía.

El segundo plugin es el plugin de Heroku. Esto nos resuelve la gran parte de los problemas de deployar en Heroku. Tenemos que especificarle en el tag de <web> el jar que queremos que corra. Como usamos el plugin anterior, entonces le indicamos ese mismo jar. En el tag <appName>, hay que indicar el nombre de la app que creamos en Heroku, que veremos en breve.

Heroku

Antes que nada, quiero dejar el link a la documentación oficial de Heroku para Maven. Está bastante completa, y podrían tranquilamente levantar la app con solo leerla, pero la idea es condensar todo en un único lugar.

Lo primero que tenemos que hacer, es crear una aplicación en Heroku. Podemos hacerlo desde la página, o por línea de comando mediante el comando heroku create nombreApp. Podemos no especificarle el nombre que queremos, en cuyo caso Heroku va a intentar buscar el nombre de la app especificado en el etiqueta de <git> en el pom, o bien, va a ponerle algún nombre falopa.

Nótese que esto nos agrega un remote más a nuestro repositorio, el remote de heroku.

Esto nos agrega unos cuantos ciclos de vida para maven, entre ellos, nos importa este: mvn heroku:deploy. Este comando, como bien lo indica su nombre, se encarga de hacer el deploy a Heroku. Esto implica un empacado (mvn package), y una subida. Este suele ser un proceso que toma bastante tiempo, así que paciencia. El package hace que se corran los tests. El jar suele pesar bastante, también, por lo que suele tardar bastante la subida. Recomiendo correr los tests aparte cuando sea necesario, y correr el comando con el argumento -Dmaven.test.skip=true.

Es importante también correr el comando con clean, para limpiar la carpeta target. Así, nos aseguramos que no haya archivos empacados que no sirvan y puedan traer problemas en runtime. El comando quedaría de la siguiente manera:

mvn clean heroku:deploy -Dmaven.test.skip=true
Ya con esto, tenemos algo (con suerte) andando. Mientras deploya andá a hacerte un café o algo, porque esto va a tardar un rato. Podemos leer los logs de la aplicación mediante
heroku logs --tail, que funciona muy similar al comando t o tail --follow de linux.

Spark

Vamos a agregar las siguientes dependencias al pom:

<dependency>

    <groupId>com.sparkjava</groupId>

    <artifactId>spark-core</artifactId>

    <version>2.7.2</version>

</dependency>

    <dependency>

    <groupId>com.sparkjava</groupId>

    <artifactId>spark-debug-tools</artifactId>

    <version>0.5</version>

</dependency>

<dependency>

    <groupId>com.sparkjava</groupId>

    <artifactId>spark-template-velocity</artifactId>

    <version>2.7.1</version>

</dependency>

Simplemente porque era el primero que aparecía en la página de Spark, se está usando el render de Velocity, pero se puede usar cualquiera.

En el Main (o donde estemos declarando los Spark.get()) agregamos este método

static int getHerokuAssignedPort() {

    ProcessBuilder processBuilder = new ProcessBuilder();

    if (processBuilder.environment().get("PORT") != null) {

        return Integer.parseInt(processBuilder.environment().get("PORT"));

    }

   

    return 4567; //return default port if heroku-port isn't set (i.e. on localhost)

}

Este método luego lo llamamos en el main como Spark.port(getHerokuAssignedPort()). Esto hace que en el ambiente de Heroku, configure el puerto de Spark al que usa Heroku internamente. Caso contrario, nos devuelve un puerto default (podemos usarlo para debuggear en localhost)  Podemos asignarle el puerto que queramos al default.

Podemos especificarle a Spark dónde buscar nuestros recursos estáticos (templates, css, html, js) en la carpeta de resources con el método Spark.staticFileLocation(“/path/to/sources”).

Acá un repo de ejemplo (súper pequeño) funcionando de Spark con Heroku

Integración con bases de datos

Heroku cuenta con un amplio abanico de bases de datos como add-ons. Todas tienen un costo de financiación, pero algunas nos ofrecen un pequeño espacio gratis para probar. Heroku tiene una política para los add-ons, y es que te pide que pongas una tarjeta de crédito para instalarlos. Por suerte, podemos simplemente agregar la tarjeta y no gastar un peso, y tampoco nos va a cobrar nada (o no lo hizo hasta ahora, al menos).

Para la conexión a la base, nos van a proveer un usuario y contraseña. Idealmente, no queremos commitear esto a nuestro repositorio, así que podemos, o bien ignorar el archivo de configuración, o subirlo con referencias a variables de entorno.

SQL - Hibernate

Heroku nos da un par de opciones para bases SQL. Recomiendo usar JawsDB MySQL, que nos ofrece 5 MB de información gratuitos. Una vez la tengamos instalada, para acceder a los datos de conexión a la base de datos, vamos a nuestra aplicación en el dev-center de Heroku y hacemos click en la flechita al lado del add-on de JawsDB. Esto nos lleva a una nueva página donde están todos los datos (puerto, usuario, contraseña, dirección).

Ahora, tenemos que modificar el persistence.xml para agregar la conexión. Agregamos lo siguiente y reemplazamos donde corresponde:

<property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver"/>

<property name="hibernate.connection.url" ="jdbc:mysql://<CONNECTION_STRING>"/>

<property name="hibernate.connection.username" value="<USER>"/>

<property name="hibernate.connection.password" value="<PASSWORD>"/>

<property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5Dialect"/>

Los campos de CONNECTION_STRING, USER y PASSWORD son los que nos provee JawsDB. Estas keys son confidenciales, y no queremos commitearlas. En este caso, no podemos guardarlo como variable de entorno, pero podemos hackearla. Lo que hacemos es duplicar el persistence.xml, e ignoramos el que tenga las keys productivas. A la hora de hacer un deploy a Heroku, simplemente reemplazamos el persistence.xml con el que tiene las keys (pero recordemos guardar el otro). Esto lo podemos automatizar fácilmente con bash:

mv src/main/resources/META-INF/persistence.xml src/main/resources/META-INF/persistence-test.xml

mv src/main/resources/META-INF/persistence-prod.xml src/main/resources/META-INF/persistence.xml

mvn clean heroku:deploy -Dmaven.test.skip=true

mv src/main/resources/META-INF/persistence.xml src/main/resources/META-INF/persistence-prod.xml

mv src/main/resources/META-INF/persistence-test.xml src/main/resources/META-INF/persistence.xml


De esta manera no tenemos que hacer la conversión a mano y no tenemos problemas con configuración sensible.