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,229