Scala, Akka and Docker, oh my!

Scala and Docker, living together

Mario Camou / @thedoc

Who am I?

  • Working with Scala for a few years
  • Currently working at eBay EPD
  • Recently moved a project to Docker

Agenda

  • Introduction to Docker
  • Docker and Scala
  • Docker and Akka

Introduction to Docker

  • What is Docker?
  • Why Docker?
  • Docker basics

What is Docker?

"A way to pack, ship and run any application as a lightweight container"
- From the Docker website
A way for developers to package any application to run on (almost) any infrastructure

What is a Docker image?

A way of packaging applications

  • Self-contained
  • Isolated
  • Centrally stored
  • Snapshot of a container

What is a Docker container?

  • A running instance of an image
  • Isolated set of resources
    • Processes
    • Networking
    • Filesystem
  • OS-agnostic*
  • Lighter than a VM
  • Usually runs a single process

Why Docker?

Dependency and environment management

An image contains all dependencies
  • JVM
  • Libraries (including native)
  • Scripts
  • Configuration files

...and is created from a text description
  • Infrastructure as code
  • Controlled in VCS

Standardized infrastructure

Independent of:
  • Underlying OS
  • Linux distro and packages

No more:
  • "it works on my machine"
  • "it works on QA but not in production"

Ephemeral, immutable infrastructure

  • Do not update: destroy and recreate!
  • Machines should be able to come and go
  • All changes are in VCS
  • No hot-fixes
  • No ad-hoc package updates

Docker basics

Installing Docker

Docker must be installed on all machines that will generate or use Docker images:
  • Developers
  • CI
  • Production hosts

Installing Docker

On Linux:

                # apt-get install docker
            
(or equivalent command)

On OS/X and Windows:

Install Docker Toolbox which includes docker-machine.

Will use a VirtualBox VM to host the Docker containers

Building a Docker image

Creating a Dockerfile
              
FROM java:openjdk-8-jre
RUN /usr/bin/apt-get update && \
    /usr/bin/apt-get install -y locales git postgresql-client && \
    /usr/bin/apt-get clean && \
    /usr/sbin/locale-gen en_US en_US.UTF-8 && \
    /usr/sbin/useradd -r -s /bin/false -d /srv appuser
COPY target/scala-2.11/myApp.jar /srv/myApp.jar
COPY bin/run-docker.sh /srv/run.sh
COPY lib/etc /srv/etc
RUN chown -R myUser /srv && chmod 0544 /srv/run.sh
USER myUser
ENTRYPOINT /srv/run.sh
              
            

Building an image

              
$ docker build .
Sending build context to Docker daemon  78.2 MB
Sending build context to Docker daemon
Step 0 : FROM java:openjdk-8-jre
---> 45a4bb374fcb
Step 1 : RUN /usr/bin/apt-get update && /usr/bin/apt-get install -y locales git postgresql-client && /usr/bin/apt-get clean && /usr/sbin/locale-gen en_US en_US.UTF-8 && /usr/sbin/useradd -r -s /bin/false -d /srv appuser
---> Using cache
---> 01b419a0e4fe
Step 2 : WORKDIR /srv
---> Using cache
---> 0b4060deee6d
Step 3 : COPY target/scala_2.11/myApp.jar /srv/myApp.jar
---> Using cache
---> 5a2fdbe7988f
Step 4 : COPY bin/run-docker.sh bin/run.sh
---> Using cache
---> d298418084c5
Step 5 : COPY lib/etc etc
---> Using cache
---> 667c3afe09c5
Step 6 : RUN chown -R myUser /srv && chmod 0544 /srv/run.sh
---> Using cache
---> 239fa5c77ad0
Step 7 : USER myUser
---> Using cache
---> 848a559a8550
Step 8 : ENTRYPOINT /srv/run.sh
---> Using cache
---> 2a31f1128eee
Successfully built 2a31f1128eee
      
              
            

Running an image

              
$ docker run 2a31f1128eee
$ docker push myOrg/myApp
$ docker pull myOrg/myApp
$ docker run myOrg/myApp:latest
              
            

Caveats

Images should be ephemeral and immutable

  • Centralized logging
  • Network data sources
  • Config files in build
  • Env vars for dynamic config

Networking

Docker containers use a virtual net interface
  • Can't listen to the outside world
  • Can't talk to other containers
  • Docker itself adds network overhead

Solution
Command-line to expose ports and connect containers

Docker and Scala

  • Dockerizing a Scala application
  • Integrating Docker with sbt
  • Testing with Docker
  • Docker and Akka

Dockerizing a Scala application

Selecting a base image

  • java:openjdk-8
  • Official java image
  • Based on Ubuntu LTS and OpenJDK
  • JDK and JRE-based for Java 6, 7, 8 and 9

https://hub.docker.com/_/java/

Selecting a base image

There are other Java images using other Linux distributions (Alpine, CentOS). Some of them also include the Oracle JDK/JRE

https://hub.docker.com/search/?q=java

... or roll your own using Alpine or Ubuntu

Selecting a base image

              
FROM java:8
FROM anapsix/alpine-java
              
            

Create a user, etc.

              
RUN /usr/bin/apt-get update && \
    /usr/bin/apt-get install -y locales git postgresql-client && \
    /usr/bin/apt-get clean && \
    /usr/sbin/locale-gen en_US en_US.UTF-8 && \
    /usr/sbin/useradd -r -s /bin/false -d /home/appuser appuser
              
            

Add your code

  • Use COPY to add any .class or .jar files
  • It's easiest to create a Fat JAR with sbt-assembly or sbt-native-packager
  •               
    COPY target/scala-2.11/myApp.jar /home/appuser/lib
                  
                

Managing configuration files

  • Separate Dockerfiles/images per config
  • Single image, different config files
  • Single config file
              
COPY bin/run-docker.sh /srv/run.sh
COPY etc /srv/etc
RUN chown -R myUser /srv && chmod 0544 /srv/run.sh
USER myUser
ENTRYPOINT /srv/run.sh
              
            

Creating and publishing the image

              
$ docker build
$ docker push myOrg/myApp
              
            

Integrating Docker with sbt

Why?

  • Because we can!

Why?

  • Easier integration into build pipeline
  • Running integration tests

sbt-docker

An sbt plugin that:
  • Creates a Dockerfile
  • Creates the Docker image
  • Pushes the image to a registry

https://github.com/marcuslonnberg/sbt-docker

Setting the image name

              
imageNames in docker := Seq(
  ImageName(
    namespace = Some("mcamou"),
    repository = name.value,
    tag = Some(s"v${version.value}")
  ),
  ImageName(
    namespace = Some("mcamou"),
    repository = name.value
    tag = Some("latest")
  ),
)
              
            

Some useful vals

              
val artifact = (outputPath in assembly).value
val baseDir = "/srv"
val preInstall = Seq(
  "/usr/bin/apt-get update",
  "/usr/bin/apt-get install -y locales git postgresql-client",
  "/usr/bin/apt-get clean",
  "/usr/sbin/locale-gen en_US en_US.UTF-8",
  s"/usr/sbin/useradd -r -s /bin/false -d $baseDir myUser"
).mkString(" && ")
              
            

Configuring the image

              
dockerfile in docker := {
  new Dockerfile {
    from("java:openjdk-8-jre")
    runRaw(preInstall)
    copy(artifact, s"$baseDir/app.jar")
    copy(new File("bin/run-docker.sh"), s"$baseDir/run.sh")
    copy(new File("local/etc"), "$baseDir/etc")
    runRaw("chown -R myUser $baseDir && chmod 0544 $baseDir/run.sh")
    user("myUser")
    entryPoint("bin/run.sh")
  }
}
              
            

Integrating with sbt-assembly

Ensure assembly always runs before docker:
              
                docker <<= (docker dependsOn assembly)
              
            

Creating and publishing the image

              
sbt> docker
sbt> dockerPush
              
            

Caveats and recommendations

  • Create a fat JAR: sbt-assembly or sbt-native-packager
  • In non-Linux platforms, start up docker-machine and set up the environment variables before starting up sbt
                      
    $ docker-machine start theMachine
    $ eval $(docker-machine env theMachine)
                      
                    
Dockerizing your Scala apps with sbt-docker

Testing with Docker

Integration testing

  • Testing interactions with external systems
    • Database
    • Message queue
    • External web services
    • ...
  • Setup is often complicated, especially on dev boxes

Integration testing

Create Docker image(s) containing external resources
  • Start up the containers
  • Wait for them to start
  • Run the tests
  • Stop the containers
Can we automate all of this?

Enter docker-it-scala

Integration testing with docker-it-scala

Defining the container
              
trait DockerMongodbService extends DockerKit {
  val DefaultMongodbPort = 27017
  val mongodbContainer = DockerContainer("mongo:2.6.5")
    .withPorts(DefaultMongodbPort -> None)
    .withReadyChecker(DockerReadyChecker.LogLine(
       _.contains("waiting for connections on port")
    ))
    .withCommand("mongod", "--nojournal", "--smallfiles", "--syncdelay", "0")

  abstract override def dockerContainers: List[DockerContainer] =
    mongodbContainer :: super.dockerContainers
}
                           

Integration testing with docker-it-scala

MongoDB example: Writing your tests
              
class MyMongoSpec extends FlatSpec
  with Matchers
  with DockerMongodbService {
  // Test assumes the MongoDB container is running
}
              
            

Docker and Akka

  • Why?
  • Caveats
  • Akka testing

Docker and Akka

Why?

  • Docker is great for elasticity
    • Fast startup
    • Immutable, ephemeral images
    • Orchestration (Amazon ECS, Kubernetes, Docker Swarm)
  • Akka is too
    • Location transparency
    • Clustering

A marriage made in heaven?

Caveats: Remoting

  • Akka < 2.4 can't use NAT (forces use of --net=host)
  • In 2.4, use:
                      
    akka.remote.netty.tcp.bind-hostname // Internal
    akka.remote.netty.tcp.bind-port
    akka.remote.netty.tcp.hostname  // External
    akka.remote.netty.tcp.port
                      
                  
FAQ: Why are replies not received from a remote actor?

Caveats: Clustering

Problem: Finding the seed nodes
  • docker -link or docker network
  • Docker Compose
  • Roll your own (cloud provider's SDK, Consul, etcd, ...)
  • ZooKeeper (akka-zk-cluster-seed)
  • Typesafe ConductR
Problem: Finding the seed nodes

References:

Akka testing

  • Persistence setup
  • Cluster setup
  • Response when server is down

Use reactive-docker to control the Docker daemon

reactive-docker example

              
implicit val docker = Docker("localhost")
implicit def timeout: Duration = Duration(20, SECONDS)
val containerName = "reactive-docker"
val cmd = Seq("/bin/sh", "-c", "while true; do echo hello world; sleep 1; done")
val cfg = ContainerConfig("busybox", cmd)
val (containerId, _) = Await.result(docker.containerCreate("myOrg/myApp", cfg, Some(containerName)), timeout)
val started = await(docker.containerStart(containerId))
val stopped = await(docker.containerStop(containerId))
              
            

More info: Using Docker, Scala and Akka for Integration Tests presentation by Andreas Gies

Summary

Docker

  • Simplifies application deployment
  • Can be integrated with sbt
  • Combined with Akka can increase elasticity
  • Can be useful for integration testing
Thank you!

http://tecnoguru.com/scala-akka-docker

Any questions?