Slide 1

Slide 1 text

Powering a build pipeline with Docker and Jenkins Benjamin Muschko

Slide 2

Slide 2 text

AUTOMATED ASCENT bmuschko bmuschko bmuschko.com About the speaker automatedascent.com

Slide 3

Slide 3 text

As a Java developer, I want to use Docker but…

Slide 4

Slide 4 text

Find your Docker Zen

Slide 5

Slide 5 text

Agenda Typical Continuous Delivery uses cases How can Docker help? Show me some code! The glue as code in Jenkins

Slide 6

Slide 6 text

Typical workflows

Slide 7

Slide 7 text

Dockerizing a Java application

Slide 8

Slide 8 text

Pushing an image to a registry

Slide 9

Slide 9 text

Image as fixture for testing

Slide 10

Slide 10 text

Running application stacks

Slide 11

Slide 11 text

Docker as part of the pipeline

Slide 12

Slide 12 text

Docker’s CLI is great but do I need to operate it by hand?

Slide 13

Slide 13 text

Pick the right tool for the job

Slide 14

Slide 14 text

Docker CLI Other options

Slide 15

Slide 15 text

Show me something beyond Hello World!

Slide 16

Slide 16 text

Sample architecture whiteboard Client Web application Spring Boot Web service Spring Boot Database PostgreSQL

Slide 17

Slide 17 text

Demo Time

Slide 18

Slide 18 text

Containerization

Slide 19

Slide 19 text

Pick a small base image

Slide 20

Slide 20 text

Copy app archive or files?

Slide 21

Slide 21 text

Controlling runtime behavior

Slide 22

Slide 22 text

Is application healthy?

Slide 23

Slide 23 text

Version appropriately 4.2.6

Slide 24

Slide 24 text

Dockerfile for Spring Boot app FROM openjdk:jre-alpine COPY todo-webservice-1.0.jar↵ /app/todo-webservice-1.0.jar
 ENTRYPOINT ["java"]
 CMD ["-jar", "/app/todo-webservice-1.0.jar"]
 HEALTHCHECK CMD wget --quiet --tries=1 --spider↵ http://localhost:8080/actuator/health || exit 1
 EXPOSE 8080

Slide 25

Slide 25 text

Dockerfile with layers for app files FROM openjdk:jre-alpine WORKDIR /app COPY libs libs/ COPY resources resources/ COPY classes classes/ ENTRYPOINT ["java", "-cp",↵ "/app/resources:/app/classes:/app/libs/*",↵ "com.bmuschko.MyApplication"]
 HEALTHCHECK CMD wget --quiet --tries=1 --spider↵ http://localhost:8080/actuator/health || exit 1
 EXPOSE 8080

Slide 26

Slide 26 text

Building and running image # Build image from Dockerfile
 docker build -t my-todo-web-service:1.0.0 .
 
 # Run built image in container
 docker run -d -p 8080:8080 my-todo-web-service:1.0.0

Slide 27

Slide 27 text

Containerization with Google Jib Build images without the need for Docker Engine https://github.com/GoogleContainerTools/jib

Slide 28

Slide 28 text

No need for Docker Engine

Slide 29

Slide 29 text

FROM … COPY … EXPOSE … ENTRYPOINT … Dockerfile is optional

Slide 30

Slide 30 text

Layers for cacheability

Slide 31

Slide 31 text

Integration with build tools

Slide 32

Slide 32 text

Google Jib in Maven build 
 ...
 
 
 
 com.google.cloud.tools
 jib-maven-plugin
 1.6.0
 
 
 bmuschko/my-web-service:1.0.0
 
 
 
 
 
 $ ./mvnw compile jib:dockerBuild

Slide 33

Slide 33 text

Google Jib in Gradle build plugins { id 'java' id 'com.google.cloud.tools.jib' version ‘1.6.0' } jib.to.image = 'bmuschko/my-web-service:1.0.0' $ ./gradlew jibDockerBuild

Slide 34

Slide 34 text

Demo Time

Slide 35

Slide 35 text

Pushing image # Log into Docker registry
 docker login --username=bmuschko↵ —email=benjamin.muschko@gmail.com
 
 # Tag the image
 docker tag bb38976d03cf bmuschko/todo-web-service:1.0.0
 
 # Push image to Docker registry
 docker push bmuschko/todo-web-service

Slide 36

Slide 36 text

Using Google Jib $ ./gradlew jib $ ./mvnw compile jib:build

Slide 37

Slide 37 text

Demo Time

Slide 38

Slide 38 text

Integrated Testing

Slide 39

Slide 39 text

2 unit tests, 0 integration tests

Slide 40

Slide 40 text

Reproducible environment

Slide 41

Slide 41 text

Slow startup times

Slide 42

Slide 42 text

Isolated & cross-platform

Slide 43

Slide 43 text

The obvious choice

Slide 44

Slide 44 text

Pull image and start/stop container # Pull image from registry (or build locally)
 docker pull bmuschko/todo-web-service:1.0.0
 
 # Start container
 docker run -d -p 8080:8080 -name todo-web-service↵ my-todo-web-service:1.0.0
 
 # Stop container
 docker container stop todo-web-service
 
 # Remove container
 docker container rm todo-web-service

Slide 45

Slide 45 text

Testing with TestContainers Managing lightweight, throwaway Docker containers for testing https://www.testcontainers.org/

Slide 46

Slide 46 text

Docker Engine Communication

Slide 47

Slide 47 text

Docker Environment Discovery

Slide 48

Slide 48 text

Automatic container cleanup

Slide 49

Slide 49 text

Multi-container coordination

Slide 50

Slide 50 text

Testcontainers in Maven build 
 org.testcontainers
 testcontainers
 1.12.0
 test


Slide 51

Slide 51 text

Testcontainers in Gradle build repositories {
 mavenCentral()
 }
 
 dependencies {
 testImplementation 'org.testcontainers:junit-jupiter:1.12.0'
 testImplementation 'org.testcontainers:postgresql:1.12.0'
 testRuntime 'org.postgresql:postgresql'
 }

Slide 52

Slide 52 text

Integration test with JUnit 5 @DataJpaTest
 public class ToDoRepositoryIntegrationTest {
 @Autowired
 private ToDoRepository repository;
 
 @Test
 public void testCanSaveNewToDoItem() {
 ToDoItem toDoItem = createToDoItem("Buy milk");
 assertNull(toDoItem.getId());
 repository.save(toDoItem);
 assertNotNull(toDoItem.getId());
 }
 
 private ToDoItem createToDoItem(String name) {
 ToDoItem toDoItem = new ToDoItem();
 toDoItem.setName(name);
 return toDoItem;
 }
 } Class under test

Slide 53

Slide 53 text

Containerized database as fixture @Testcontainers
 @AutoConfigureTestDatabase(replace = NONE)
 @ContextConfiguration(initializers = { ToDoRepositoryIntegrationTest.Initializer.class })
 public class ToDoRepositoryIntegrationTest {
 @Container
 public static PostgreSQLContainer postgreSQLContainer = createDbContainer();
 
 private static PostgreSQLContainer createDbContainer() {
 return new PostgreSQLContainer("postgres:9.6.10-alpine")
 .withUsername("postgres")
 .withPassword("postgres")
 .withDatabaseName("todo");
 }
 
 static class Initializer implements ApplicationContextInitializer {
 @Override
 public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
 TestPropertyValues.of(
 "spring.datasource.url=" + postgreSQLContainer.getJdbcUrl(),
 "spring.datasource.username=" + postgreSQLContainer.getUsername(),
 "spring.datasource.password=" + postgreSQLContainer.getPassword(),
 "spring.datasource.driver-class-name=org.postgresql.Driver",
 "spring.jpa.generate-ddl=true"
 ).applyTo(configurableApplicationContext.getEnvironment());
 }
 }
 } Create database container Inject database Configuration

Slide 54

Slide 54 text

Functional test with JUnit 5 public class ToDoWebServiceFunctionalTest {
 @Test
 @DisplayName("can retrieve all items before and after inserting new ones")
 void retrieveAllItems() {
 String allItems = getAllItems();
 assertEquals("[]", allItems);
 
 ToDoItem toDoItem = new ToDoItem();
 toDoItem.setName("Buy milk");
 toDoItem.setCompleted(false);
 insertItem(toDoItem);
 
 allItems = getAllItems();
 assertEquals("[{\"id\":1,\"name\":\"Buy milk\",\"completed\":false}]", allItems);
 } } Calls to HTTP endpoints

Slide 55

Slide 55 text

Containerized application as fixture @Testcontainers
 public class ToDoWebServiceFunctionalTest {
 @Container
 private static GenericContainer appContainer = createContainer();
 
 private static GenericContainer createContainer() {
 return new GenericContainer(buildImageDockerfile())
 .withExposedPorts(8080)
 .waitingFor(Wait.forHttp("/actuator/health")
 .forStatusCode(200));
 }
 
 private static ImageFromDockerfile buildImageDockerfile() {
 return new ImageFromDockerfile()
 .withFileFromFile(ARCHIVE_NAME, new File(DISTRIBUTION_DIR, ARCHIVE_NAME))
 .withDockerfileFromBuilder(builder -> builder
 .from("openjdk:jre-alpine")
 .copy(ARCHIVE_NAME, "/app/" + ARCHIVE_NAME)
 .entryPoint("java", "-jar", "/app/" + ARCHIVE_NAME)
 .build());
 }
 } Build container on-the-fly

Slide 56

Slide 56 text

Demo Time

Slide 57

Slide 57 text

Running a multi-container app # Start composed apps
 docker-compose up
 
 # Stop composed apps
 docker-compose down

Slide 58

Slide 58 text

Compose test with JUnit 5 @ExtendWith(SpringExtension.class) @SpringBootTest
 public class ToDoAppIntegrationTest {
 @Autowired
 private ToDoService service;
 
 @Test
 public void canCreateNewItemAndRetrieveIt() {
 ToDoItem newItem = newItem("Buy milk");
 assertNull(newItem.getId());
 service.save(newItem);
 assertNotNull(newItem.getId());
 ToDoItem retrievedItem = service.findOne(newItem.getId());
 assertEquals(newItem, retrievedItem);
 } } Class under test

Slide 59

Slide 59 text

Compose fixture @Testcontainers
 public class ToDoAppImplIntegrationTest {
 private static final Logger logger = LoggerFactory.getLogger(ToDoAppImplIntegrationTest.class);
 private final static String WEB_SERVICE_NAME = "webservice_1";
 private final static int WEB_SERVICE_PORT = 8080;
 private final static String DATABASE_NAME = "database_1";
 private final static int DATABASE_PORT = 5432;
 
 @Container
 public static DockerComposeContainer environment = createComposeContainer();
 
 private static DockerComposeContainer createComposeContainer() {
 return new DockerComposeContainer(new File("src/integrationTest/resources/compose-test.yml"))
 .withLogConsumer(WEB_SERVICE_NAME, new Slf4jLogConsumer(logger))
 .withExposedService(WEB_SERVICE_NAME, WEB_SERVICE_PORT,
 Wait.forHttp("/actuator/health")
 .forStatusCode(200))
 .withExposedService(DATABASE_NAME, DATABASE_PORT,
 Wait.forLogMessage(".*database system is ready to accept connections.*\\s", 2));
 } } Create Compose container

Slide 60

Slide 60 text

Compose test file version: "2.0"
 services:
 database:
 image: "postgres:9.6.10-alpine"
 environment:
 - POSTGRES_USER=postgres
 - POSTGRES_PASSWORD=postgres
 - POSTGRES_DB=todo
 webservice:
 image: "bmuschko/todo-web-service:1.0.0"
 environment:
 - SPRING_PROFILES_ACTIVE=dev
 depends_on:
 - database

Slide 61

Slide 61 text

Demo Time

Slide 62

Slide 62 text

Glueing together the pipeline

Slide 63

Slide 63 text

Jenkins plugins Optional: Blue Ocean Pipeline Suite

Slide 64

Slide 64 text

Standard Pipeline Blue Ocean Pipeline

Slide 65

Slide 65 text

Pipeline as code Jenkinsfile Jenkins Pipeline

Slide 66

Slide 66 text

Invoke build with Wrapper pipeline { stages { stage('Compile & Unit Tests') { steps { gradlew('clean', 'test') } } } ... } def gradlew(String... args) { sh "./gradlew ${args.join(' ')} -s" }

Slide 67

Slide 67 text

Credentials Environment variables

Slide 68

Slide 68 text

Injecting credentials & env vars stage('Push Image') { environment { DOCKER_USERNAME = "${env.DOCKER_USERNAME}" DOCKER_PASSWORD = credentials('DOCKER_PASSWORD') DOCKER_EMAIL = "${env.DOCKER_EMAIL}" } steps { gradlew('dockerPushImage') } }

Slide 69

Slide 69 text

Scaling job execution

Slide 70

Slide 70 text

Configuration Execution

Slide 71

Slide 71 text

Containerized pipeline stages pipeline {
 agent {
 kubernetes {
 defaultContainer 'jnlp'
 yaml """
 apiVersion: v1
 kind: Pod
 metadata:
 labels:
 some-label: some-label-value
 spec:
 containers:
 - name: maven
 image: maven:alpine
 command:
 - cat
 tty: true
 - name: busybox
 image: busybox
 command:
 - cat
 tty: true
 """
 }
 }
 } stages {
 stage('Run in Maven container') {
 steps {
 container('maven') {
 sh 'mvn -version'
 }
 }
 }
 stage('Run in Busybox container') {
 steps {
 container('busybox') {
 sh '/bin/busybox'
 }
 }
 }
 stage('Run in default container') {
 steps {
 sh "echo 'Running in default container'"
 }
 }
 }

Slide 72

Slide 72 text

Demo Time

Slide 73

Slide 73 text

Resources github.com/bmuschko/todo-web-service github.com/bmuschko/todo-web-app github.com/bmuschko/jenkins-with-kubernetes

Slide 74

Slide 74 text

Thank you! Please ask questions…