Akka.io, sbt-assembly and Docker

Written by anicolaspp | Published 2016/11/30
Tech Story Tags: docker | devops | scala | containers | akka

TLDRvia the TL;DR App

We have been using akka capabilities for some time now, but also we are starting to explore Docker deployments of the actor systems we have today. In my previous two posts, Yet another sbt-docker introduction, and Akka Cluster in Docker, a straightforward configuration we have explored how to use some tools, directly embedded into our build pipeline, to do the deployments into Docker. However, a question arises from our team, should we create a fat JAR and put it on docker or should we do a deployment with all dependencies all together?

sbt-assembly

Directly from the docs

sbt-assembly is a sbt plugin originally ported from codahale’s assembly-sbt, which I’m guessing was inspired by Maven’s assembly plugin. The goal is simple: Create a fat JAR of your project with all of its dependencies.

This is a nice tool, very useful for managing dependencies. Using sbt-assembly, we could create a solely JAR and then add it to Docker. Let’s combine sbt-docker and sbt-assembly plugins.

lazy val dockerSettings = Seq(    dockerfile in docker := {        val artifact: File = assembly.value        val artifactTargetPath = s"/app/${artifact.name}"                new Dockerfile {          from("java")          add(artifact, artifactTargetPath)          entryPoint("java", "-jar", artifactTargetPath)        }  },  ....}

This sbt setting will create the fat JAR, add it to the Docker image and create an entry point so when the container starts up our application runs.

There is nothing special here, this should work for any app, except when it doesn’t.

The problem is not the plugins, but Akka itself. Akka documentation says that each module relies on its very own configuration which in other words is the same as saying we will need multiple configuration files for Akka. This brings us to the next problem, if Akka is generating config files with the defaults, how will they be merged on the sbt-assembly process? If you follow this link, you will realize that this has been solved already for us. Still, we want to supply a config file to be used called application.prod.conf that is located in resources/confs/ directory, let’s see how we can do it.

lazy val dockerSettings = Seq(    dockerfile in docker := {             val dockerFiles = {            val resources = (unmanagedResources in Runtime).value            val dockerFilesDir = resources                .find(_.getPath.endsWith("/confs"))                .get            resources                .filter(_.getPath.contains("/confs/"))                .map(r =>                     dockerFilesDir                        .toURI                        .relativize(r.toURI)                        .getPath -> r)                .toMap          }            val artifact: File = assembly.value        val artifactTargetPath = s"/app/${artifact.name}"                new Dockerfile {          from("java")          add(artifact, artifactTargetPath)          add(dockerFiles(            "application.prod.conf"),            ("/app/application.prod.conf")          )          entryPoint(            "java",             "-Dconfig.resource=/app/application.prod.conf",             "-jar",             artifactTargetPath          )        }  },  ....}

In here, we have added our own config and also specify at the entry point that we want to use it as a resource. Now, our Docker image will have the fat JAR and the config we will use as a resource.

This approach works nicely, but there was something we didn’t like about the process. The building time associated with each of these containers was long since sbt has to clean everything up, then compile, run the tests, assembly everything (really long time doing this) and the create the Docker images. You might think that this is a small problem, but if you are building a hundred images every time, trust me, it takes quite a long time.

To assembly or not to assembly

Creating a single JAR from your application makes sense in a lot of scenarios, but is it really worth to go through all this time consuming process if we are going to deploy on Docker?

Well, the idea of having a unique JAR with our app is to encapsulate the dependencies which allow us to manage more efficiently our release process. Having only a JAR with everything inside is, ultimately, convenient. However, isn’t that the same idea behind Docker? A Docker image is an encapsulation of our application that can be deployed everywhere and in this context, it replaces (or works as the same) the idea of having a single JAR.

These were the ideas we as a team discussed favoring the containerization without fat JARs.

Let’s review the required changes.

First, we remove sbt-assembly from plugins.sbt in favor of sbt-native-packager.

Second, we change how we create the Docker image in our build.sbt

lazy val dockerSettings = Seq(    dockerfile in docker := {         val dockerFiles = {            val resources = (unmanagedResources in Runtime).value            val dockerFilesDir = resources                .find(_.getPath.endsWith("/confs"))                .get            resources                .filter(_.getPath.contains("/confs/"))                .map(r =>                     dockerFilesDir                        .toURI                        .relativize(r.toURI)                        .getPath -> r)                .toMap          }

        val appDir: File = stage.value        val targetDir = "/app"            new Dockerfile {          from("java")          add(appDir, targetDir)          add(dockerFiles(            "application.prod.conf"),            ("/app/application.conf"))                    entryPoint(              s"$targetDir/bin/${executableScriptName.value}"          )        }    },  ... }

Note, that now we don’t depend on assembly any longer, we use stage.value instead. In other words, we add the content generated by stage into /app in our container. Also, the entry point has changed. sbt-native-packager uses a starting scripts instead of java -jar command. Remember, we need to add the application.prod.conf file to it, but we don’t want to use additional JVM args, we rename it to its default application.conf.

Ultimately, the changes towards not creating fat JARs reduces our build time 80% which were, in the first place, the biggest problem we had.

Finalizing

Sometimes we have not idea what to do, especially in software engineering, but it’s not until we test different approaches to see which one fits best our necessities.

There is never a unique solution, most of the time is a mix between ideas where we find our solution. In our case (akka on Docker) , sbt-assembly was nice, but adds too much time to the building process. Docker, on the other hand, was a must have platform and at the same time it provides the same encapsulation of our applications as sbt-assembly does in other contexts.

We hope this post gives you some insights into our problems, you might be building systems with similar deployment strategies.

Disclaimer: We use sbt-assembly in our applications outside Docker without any problems, we don’t have anything against it, all the contrary. You just need to find the right balance for your use case.

Hacker Noon is how hackers start their afternoons. We’re a part of the @AMIfamily. We are now accepting submissions and happy to discuss advertising &sponsorship opportunities.

To learn more, read our about page, like/message us on Facebook, or simply, tweet/DM @HackerNoon.

If you enjoyed this story, we recommend reading our latest tech stories and trending tech stories. Until next time, don’t take the realities of the world for granted!


Published by HackerNoon on 2016/11/30