Deploying a Scala app is not simple. One solution is to build a big fat jar with all the dependencies using sbt-assembly, or just run the app with sbt in production.

If you are using Akka, there is a very good alternative to these options.

Ingredients

SBT

I am using sbt with the akka-sbt-plugin to generate all the jars I need to upload to the server.

Akka microkernel

You can use the included akka microkernel mechanism to deploy your app, which makes everything so much easier. You will just need to create a Bootable class that acts as a Main class and the microkernel will take care of the rest for you.

supervisord

Running akka in a microkernel is just like running a normal app, it runs in the foreground, so you need to use daemonize your app. If you don't want to mess with init.d/upstart/systemd scripts, I recommend you use supervisord and save yourself lots of trouble and painful debugging.

fabric and rsync

You will need to upload the files to a production server and run some commands there. I chose to use fabric because it is very lightweight, but you could use capistrano or some equivalent

Preparing your app

First you need to make your app run in an akka microkernel. This is fairly easy and you just need to create a Bootable class like this:

class RiepeteKernel extends Bootable {
  val system = ActorSystem("riepeteActorSystem")

  def startup() = {
    system.actorOf(RiepeteServer.props(), "RiepeteServer")
  }

  def shutdown() = {
    system.shutdown()
  }
}

You need to define both a startup and a shutdown method. In the startup method you just need to spin up whatever actors your app needs.

Preparing your build

Create a directory named /project and in there create a file named plugins.sbt with the following content:

addSbtPlugin("com.typesafe.akka" % "akka-sbt-plugin" % "2.3-M2")

This loads the akka-sbt-plugin into sbt.

You then need to create a build definition for your project. Create a <sbt root>/project/Build.scala with the following contents:

import akka.sbt.AkkaKernelPlugin
import akka.sbt.AkkaKernelPlugin.{Dist, distJvmOptions, outputDirectory}
import sbt.Keys._
import sbt._
import akka.sbt.AkkaKernelPlugin.distMainClass

object RiepeteKernelBuild extends Build {
  val Organization = "io.simao"
  val Version      = "0.0.1"
  val ScalaVersion = "2.11.1"

  lazy val RiepeteKernel = Project(
    id = "riepete-kernel",
    base = file("."),
    settings = defaultSettings ++ AkkaKernelPlugin.distSettings ++ Seq(
      libraryDependencies ++= Dependencies.all,
      distJvmOptions in Dist := "-Xms256M -Xmx1024M",
      outputDirectory in Dist := file("target/riepete-dist"),
      // Use the name of the Bootable class you created earlier
      distMainClass in Dist := "akka.kernel.Main io.simao.riepete.server.RiepeteKernel"
    )
  )

  lazy val buildSettings = Defaults.defaultSettings ++ Seq(
    organization := Organization,
    version      := Version,
    scalaVersion := ScalaVersion,
    crossPaths   := false,
    organizationName := "simao.io",
    organizationHomepage := Some(url("https://simao.io"))
  )

  lazy val defaultSettings = buildSettings ++ Seq(
    // compile options
    scalacOptions ++= Seq("-encoding", "UTF-8", "-deprecation", "-unchecked"),
    javacOptions  ++= Seq("-Xlint:unchecked", "-Xlint:deprecation"),
  )
}

object Dependencies {
  object Versions {
    val Akka      = "2.3.3"
  }

  val all = List(
    "com.typesafe.akka" %% "akka-kernel" % Versions.Akka,
    "com.typesafe.akka" %% "akka-slf4j"  % Versions.Akka,
    "com.typesafe.akka" %% "akka-actor" % Versions.Akka,
    "io.argonaut" %% "argonaut" % "6.0.4"
  )
}

Of course you'll need to adapt the contents to match your app.

Now running sbt dist will create a riepete-dist directory in <sbt root>/target containing all the dependencies and a bash script that can be used to run the app. You can test your application runs executing bin/start.

Create a supervisord config file

You will now need to configure supervisord so it knows how to run your app.

Supervisord needs to run the java process itself to work properly, so we won't be using the bin/start script generated by sbt dist.

Create a file similar to this in your supervisord config directory, usually /etc/supervisord.d:

[program:riepete]
command=java -Xms256M -Xmx1024M -cp /opt/riepete/config:/opt/riepete/lib/* -Dakka.home=/opt/riepete akka.kernel.Main io.simao.riepete.server.RiepeteKernel
user=riepete
redirect_stderr=true
directory=/opt/riepete

You will again need to change these values to match your app name and the directory where it is deployed.

Writing a deploy script

To deploy a new version of the app we just need to rsync our target/riepete-dist directory into /opt/riepete in our production server.

from fabric.api import local, run, cd, put, env, path
from fabric.contrib.project import rsync_project

env.use_ssh_config = True

def sync_bins():
    rsync_project("/opt/riepete", "./target/riepete-dist/", delete=True,
                  exclude=[".git", "log", "config/application.conf", "config/riepete.conf"],
                  extra_opts="--exclude-from .gitignore")

def compile_project():
    local("sbt dist")

def restart():
    run('sudo supervisorctl restart riepete')

def deploy():
    target_dir = '/opt/riepete'

    with cd(target_dir):
        compile_project()
        sync_bins()

    restart()

Deploying

After installing supervisord on the production server and creating the app directory, you can deploy the app using:

fab -H <your-hostname> deploy

You can login into the production server and run supervisorctl tail -f <app name> to check the standard output.



comments powered by Disqus