Bootiful Containers

with Spring-Boot and Docker

Agenda

  • Run Spring-Boot Apps inside a Docker container
    • Make Spring-Boot Apps configurable using Environment Variables
    • Build your Spring-Boot Docker image using Gradle
    • Deploy the image via docker-compose
  • Unit- and Integration-Test your Spring-Boot App with Spock and Docker
  • Deploy and configure Spring-Boot microservices using docker-compose

Run Spring-Boot Apps inside a Docker container

1

Spring-Boot

  • Opinionated view on Spring
  • Lots of autoconfigure magic
  • Useful features and defaults for microservices and 12-factor apps
    • Supports externalized configuration using environment variables
    • Easy to use health and metrics endpoints
    • Use of embedded Tomcat allows for-self contained application bundle
  • Works well inside a container
@Component
public class MyBean {

    @Value("${foo.bar}")
    private String name;

    // ...

}
$ export FOO_BAR=mobydock

Externalized Configuration

@ConfigurationProperties("foo")
@Component
public class MyBeanProperties {

    private String name;

    public String getName() { ... }

    public void setName(String name) { ... }

}

Externalized Configuration

Type-safe Configuration Properties

Hands-On!

Refactoring "Hello World"

Use Type-safe Configuration Properties

  • Create a GreeterConfiguration component using the @ConfigurationProperties annotation

  • Inject your bean into the greeter
  • Run the hello-world application to verify
$ GREETER_NAME=gradle-runner ./gradlew bootrun
$ git clone https://github.com/devops-gathering/bootiful-hello-world.git
  • Groovy DSL for build management
  • Rich plugin ecosystem
  • Allows to use Maven dependencies
  • A bit like Maven if Maven was cool ;)

buildscript {
  ext {
    springBootVersion = '1.5.1.RELEASE'
  }
  repositories {
    jcenter()
  }
  dependencies {
    classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    classpath 'com.bmuschko:gradle-docker-plugin:3.0.5'
  }
}

apply plugin: 'java'
apply plugin: 'org.springframework.boot'

sourceCompatibility = 1.8

repositories {
    jcenter()
}


dependencies {
    compile('org.springframework.boot:spring-boot-starter')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}
apply plugin: 'com.bmuschko.docker-remote-api'

import com.bmuschko.gradle.docker.tasks.image.Dockerfile
import com.bmuschko.gradle.docker.tasks.image.DockerBuildImage

def dockerBuildDir = 'build/docker/'
def applicationJar = "${archivesBaseName}.jar"

task copyJar(type: Copy) {
    dependsOn bootRepackage
    file(dockerBuildDir).mkdirs()
    from "$libsDir/$applicationJar"
    into dockerBuildDir
}

task createDockerfile(type: Dockerfile) {
    dependsOn copyJar
    destFile = project.file(dockerBuildDir + "Dockerfile")

    from 'openjdk:8-jre'
    copyFile(applicationJar, "/")
    entryPoint "sh", "-c", 
      "java \$JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /$applicationJar"
}

task buildImage(type: DockerBuildImage) {
    dependsOn createDockerfile
    inputDir = createDockerfile.destFile.parentFile
    tag = 'devops/hello-world'
}

Hands-on!

"Hello World" from a container with externalized configuration

$ docker run \
--rm \
--name "hello-world-test" \
-e "GREETER_NAME=docker-command-line" \
devops/hello-world
$ ./gradlew buildImage

Build your image

(or via IDE...)

Run a container from your image

  • Try using different names!
  • What happens if you omit the environment variable?
version: "3"

services:
  greeter:
    image: dog.bootcamp/greeter
    environment: 
      GREETER_NAME: docker-compose

Deployment with Docker-Compose

Like before:

  • image is the tag from the build.gradle

  • Configure application properties via environment variable

Hands-On!

Deploy "Hello World" via the docker-compose file

$ docker-compose up
  • Add a second "greeter"-service greeting another name
  • Run a few times
    • Make an observation about the greeting order!

Summary

  • We can externalize the configuration of spring-boot applications
  • We can configure those properties via environment variables
  • We can build a container from our app via gradle using a docker-plugin
  • We can pass the property values both from docker-cli and docker-compose
  • We can not predict any timing behaviour of docker-compose services on startup

Unit- and Integration-Test your Spring-Boot App

with Spock and Docker

2

class HelloSpockSpec extends spock.lang.Specification {
  def "length of Spock's and his friends' names"() {
    expect:
    name.size() == length

    where:
    name     | length
    "Spock"  | 5
    "Kirk"   | 4
    "Scotty" | 6
  }
}  
@ContextConfiguration
@SpringBootTest
class ApplicationTests extends Specification {

    def "should boot up"() {
        expect:
        true
    }

}

Hands-On!

Writing Spock tests

  • Extend the BookRepositoryTests by a test that ensures that a book without IBAN can't be stored
  • Extend the BookRepositoryTests by a test that ensures that a book with IBAN can be found after it was stored
$ git clone https://github.com/devops-gathering/bootiful-book-store.git
$ gradlew check

Run the test suite with:

For better visualized results, run the gradle task from your IDE:

Testcontainers

  • Use Docker containers in your JUnit tests 
  • Spock extension also available ;)
@Testcontainers
class JpaTests extends Specification {

    @Shared
    PostgreSQLContainer postgresContainer = new PostgreSQLContainer()
            .withDatabaseName("bootiful")
            .withUsername("bootiful")
            .withPassword("secret")

    ...
}
  • PostgreSQLContainer will wait until the database is fully operational

Hands-On!

Using Testcontainers

  • Change the BookRepositoryTests to use a MySQL 5.5 container instead of a postgres
    • Keep track: How many project files do you have to change?
    • Also change to the latest Jitpack version of the library

Your client tells you they're - by some obscure policy - not allowed to run a postgres database. They are instead confined to using MySQL 5.5. No way you're going to install that on your local machine for testing.

Summary

  • We can use the Testcontainers library to use containers for integration tests
    • This brings our test and production environment closer together
  • We can easily simulate other deployment scenarios
  • We can isolate the test environment from the host system
    • This allows for more stable integration tests between developer machines and your continuous integration server
    • Any developer can run the same integration tests - no weird dependency management, no "works on my machine", no excuses

Deploy and configure Spring-Boot microservices

using docker-compose

3

Dockerize

  • Our bookstore application depends on creating the schema at startup...

  • In our integration test, using the PostgreSQLContainer ensures the database is ready.

  • But how do we ensure the database is ready in our deployment scenario?

'Correct' answer:

"We don't"

  • Rather build applications that can easily recover from network failure
  • Rather fail to boot dependent services in a swarm environment
    • service will be restarted until it works
  • BUT there are legacy applications...
  • ... and more often than not, framework mechanisms don't play nice with that idea

Introducing Dockerize

  • Delay your application start until a port is reachable
version: "2"
services:
  db:
    image: postgres
  myservice:
    image: devops/myservice
    depends_on:
      - "db"
    entrypoint:
      - dockerize
      - -wait
      - tcp://db:5432
      - -timeout 
      - 120s
      - java
      - -jar
      - myservice.jar

Installing Dockerize

  • In your createDockerfile-task:

task createDockerfile(type: Dockerfile) {
    [...]
	// Install dockerize
	environmentVariable('DOCKERIZE_VERSION', 'v0.3.0')
	runCommand 'apt-get update && apt-get install -y wget'
	runCommand 'wget \
 https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
    		&& tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
    		&& rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz'
    [...]
}

https://github.com/jwilder/dockerize#installation

Bootiful Containers

By Kevin Wittek

Bootiful Containers

  • 2,241