From Maven to Gradle with Docker in a multi-product pipeline

by Tobias Gesellchen @ Hypoport AG

From Maven to Gradle with Docker in a multi-product pipeline

by Tobias Gesellchen

About

Tobias Gesellchen

Agenda

  1. Migrating a Maven multi-module project
  2. Using Gradle and Docker for continuous deployment

From Maven to Gradle

Origin: Maven multi-module project

From Maven to Gradle

Dependency Management

Using transitive dependencies is possible in Maven due to compile scope propagation.
Make your dependencies explicit!

(it's generally better to know one's dependencies)

Dependency Enforcement

def dependencyVersions = [
    'javax.servlet:javax.servlet-api:3.0.1',
    'log4j:log4j:1.2.17'
]

configurations {
    all*.exclude module: 'jaxb-xjc'
    all.resolutionStrategy {
        failOnVersionConflict()

        force dependencyVersions.collect { it }

        eachDependency { DependencyResolveDetails details ->
            if (details.requested.group == 'org.springframework') {
                details.useVersion '3.2.4.RELEASE'
            }
        }
    }
}

            

Shared dependencies

// root build.gradle
subprojects {
    ext.libraries = [
        javax: [
            validation: [
                'javax.validation:validation-api:1.1.0.Final',
                'javax.validation:validation-api:1.1.0.Final:sources'
            ]
        ],
        gwt  : [
            user: dependencies.create('com.google.gwt:gwt-user:2.6.0') {
                transitive = false
            }
        ]
    ]
}

// child build.gradle
dependencies {
    compile  libraries.javax.validation
    provided libraries.gwt.user
}

            

'provided' scope in jar modules

Provided scope in Gradle only for projects with the 'war' plugin applied.
Use the propdeps plugin:
Adds a custom 'provided' configuration

GWT (Google Web Toolkit)

GWT needs to compile Java to browser-specific JavaScript code.
Use the GWT Gradle plugin:
It just works, has good defaults, minimal config needed

Groovy compiler only

apply plugin: 'groovy'

sourceSets.main.java.srcDirs = []
sourceSets.main.groovy.srcDirs += ["src/main/java"]

sourceSets.test.java.srcDirs = []
sourceSets.test.groovy.srcDirs += ["src/test/java"]

            

JUnit and TestNG with reports

task testng(type: Test) {
    useTestNG()
}
check.dependsOn testng

task aggregateTestReports(type: TestReport) {
    destinationDir = test.reports.html.destination
    reportOn test, testng
}
aggregateTestReports.mustRunAfter testng
check.dependsOn aggregateTestReports

tasks.withType(Test) {
    systemProperty 'db.url', System.getProperty('db.url', 'http://db.example.com/test')
}

            

Artifact version only for publishing

publishing {
    publications {
        mavenJava(MavenPublication) {
            version new SimpleDateFormat('yyyyMMddHHmmss').format(new Date())
            from components.java
        }
    }
}
            

Current state

Continuous deployment with Gradle

Deployment Pipeline

Grunt

Building a JavaScript frontend during the Gradle build.
Use the Gradle plugin for Grunt:
Supports Gradle's incremental build via 'inputs' and 'outputs'

Protractor e2e tests

no need for a dedicated plugin, Gradle allows execution of arbitrary commands

Consumer-driven contract tests

Dynamically resolved dependency

configurations {
    contracttests
}

afterEvaluate {
  // TeamCity build artifact from publish build goal
  def appVersion = new File('version.txt').text.trim()

  def contracttestDep = dependencies.create("de.hypoport:app-contracttests:${appVersion}") {
    transitive = false
  }
  dependencies.add 'contracttests', contracttestDep

  def testRuntimeDep = dependencies.create("de.hypoport:app-contracttests:${appVersion}")
  dependencies.add 'testRuntime', testRuntimeDep
}

def contracttestsDirectory = "${buildDir}/contracttests"

task unzipContracttests(type: Copy) {
  from {
    zipTree(configurations.contracttests.singleFile)
  }
  into {
    contracttestsDirectory
  }
}

task contracttests(type: Test) {
  dependsOn unzipContracttests
  testClassesDir file(contracttestsDirectory)
}
        

Docker

Build Docker image

"docker pull http://docker-registry/kreditsmart-base".execute()
"docker build -t kreditsmart --rm .".execute()
"docker tag kreditsmart http://docker-registry/kreditsmart:${version}".execute()
"docker push http://docker-registry/kreditsmart".execute()
"docker rmi http://docker-registry/kreditsmart:${version}".execute()

        
"docker pull baseimage".execute()
"docker build .".execute()
"docker tag image ${version}".execute()
"docker push image.execute()
"docker rmi image".execute()

        

Multi layer images

Deploy Docker image

"docker -H tcp://app-server:2375 pull ...".execute()
"docker -H ... stop ...".execute()
"docker -H ... rm ...".execute()
"docker -H ... run -d ...".execute()
"docker -H ... ps".execute()

        
"docker -H tcp://app-server:2375 pull http://docker-registry/kreditsmart:${version}".execute()
def prevImageId = "docker -H tcp://app-server:2375 inspect --format={{.Image}} kreditsmart-${selectedStage}".execute()
"docker -H tcp://app-server:2375 stop kreditsmart-${selectedStage}".execute()
"docker -H tcp://app-server:2375 rm kreditsmart-${selectedStage}".execute()
"docker -H tcp://app-server:2375 rmi ${prevImageId}".execute()
"""\
  docker -H tcp://app-server:2375 run -d --name=kreditsmart-${selectedStage} \
  -v /logs/${selectedStage}:/opt/kreditsmart-backend/logs:rw -p ${publicPortBackend}:8090 \
  -v /logs/${selectedStage}:/opt/kreditsmart-frontend/logs:rw -p ${publicPortFrontend}:8080 \
  ${serverEnvironment.run_env} http://docker-registry/kreditsmart:${version} \
""".execute()
["sh", "-c", "docker -H tcp://app-server:2375 images | grep none | awk '{print \$3}' | xargs docker -H tcp://${hostname} rmi"].execute()
"docker -H tcp://app-server:2375 ps".execute()

        

Other helpful plugins


Why Gradle?

Questions!