null

Gradle Docker plugin

Добрый день!

Давайте сегодня рассмотрим такую полезную штуку, как bmuschko/gradle-docker-plugin
Важно! Данный плагин требует версию Gradle >=5.1

Для начала созданим маленькое приложение, которое будем разворачивать в докере.

build.gradle

plugins {
    id 'java'
    id 'application'
    id 'org.springframework.boot' version '2.1.0.RELEASE'
}

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.springframework.boot:spring-boot-starter-web:2.1.0.RELEASE'
}

application {
    mainClassName = 'com.tune_it.App'
}

jar {
    manifest {
        attributes 'Main-Class': 'com.tune_it.App'
    }

    from {
        configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
    }
}

App.java

package com.tune_it;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {
    public static void main(String... args){
        SpringApplication.run(App.class);
    }
}

HelloController.java

package com.tune_it;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(value = "/hello")
public class HelloController {
    @GetMapping
    public String hello() {
        return "Hello!\n";
    }
}

 

Далее научим gradle создавать Dockerfile, дополнив наш build.gradle

plugins {
    id 'java'
    id 'application'
    id 'org.springframework.boot' version '2.1.0.RELEASE'
    id 'com.bmuschko.docker-remote-api' version '4.8.0'
}

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


def dockerBuildDir = 'build/docker/'

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.springframework.boot:spring-boot-starter-web:2.1.0.RELEASE'
}

application {
    mainClassName = 'com.tune_it.App'
}

jar {
    manifest {
        attributes 'Main-Class': 'com.tune_it.App'
    }

    from {
        configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
    }
}

task createDockerfile(type: Dockerfile) {
    destFile = project.file("$dockerBuildDir/Dockerfile")
    from 'openjdk:8-jre-alpine'
    copyFile jar.archiveName, '/app/test_service.jar'
    entryPoint 'java'
    defaultCommand '-jar', '/app/test_service.jar'
    exposePort 8080
    runCommand 'apk --update --no-cache add curl'
    instruction 'HEALTHCHECK CMD curl -f http://localhost:8080/hello || exit 1'
}

Здесь мы добавили тот самый плагин, о котором сегодня идёт речь, добавили константу, определяющую путь к директории, в которой будет производится сборка докер образа, добавили свою таску, которая, собственно, и создаёт Dockerfile. Для создания таски нам потребовалось сделать импорт типа таски.

После запуска таски в указанной ранее директории должен появится Dockerfile со следующим содержимым:

FROM openjdk:8-jre-alpine
COPY gradle_docker_plugin.jar /app/test_service.jar
ENTRYPOINT ["java"]
CMD ["-jar", "/app/test_service.jar"]
EXPOSE 8080
RUN apk --update --no-cache add curl
HEALTHCHECK CMD curl -f http://localhost:8080/hello || exit 1

 

После этого начнём собирать сам образ, для этого добавим следующие импорты

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

И следующие таски

def imageVersion = '1.0'

task syncJar(type: Copy) {
    dependsOn assemble
    from jar.archivePath
    into dockerBuildDir
}

task removeImage(type: DockerRemoveImage) {
    targetImageId("adpashnin/gradle_docker_plugin:$imageVersion")
    onError { exc ->
        if (exc.message!=null && !exc.message.contains('NotModifiedException')) {
            throw new RuntimeException(exc)
        }
    }
}

task buildImage(type: DockerBuildImage) {
    dependsOn createDockerfile, syncJar
    inputDir = project.file(dockerBuildDir)
    tags = ["adpashnin/gradle_docker_plugin:$imageVersion"]
}

 

removeImage нужна нам для того, чтобы удалить существуюй образ в случае, если он есть, в противном случае он будет переименован на null и дополнительно будет создан новый с указанным именем.

Однако могут возникнуть проблемы с удалением образа, если существует контейнер, который его использует. Чтобы этого избежать, добавим задачу остановки и удаления контейнера с определённым именем.

 

import com.bmuschko.gradle.docker.tasks.container.DockerRemoveContainer
import com.bmuschko.gradle.docker.tasks.container.DockerStopContainer
def uniqueContainerName = 'test_docker_plugin'


task stopContainer(type: DockerStopContainer) {
    targetContainerId("$uniqueContainerName")
    onError { exc ->
        if (exc.message!=null && !exc.message.contains('NotModifiedException')) {
            throw new RuntimeException(exc)
        }
    }
}

task removeContainer(type: DockerRemoveContainer) {
    dependsOn stopContainer
    targetContainerId("$uniqueContainerName")
    onError { exc ->
        if (exc.message!=null && !exc.message.contains('NotModifiedException')) {
            throw new RuntimeException(exc)
        }
    }
}

Добавим removeContainer в зависимости задачи removeImage

task removeImage(type: DockerRemoveImage) {
    dependsOn removeContainer
    targetImageId("adpashnin/gradle_docker_plugin:$imageVersion")
    onError { exc ->
        if (exc.message!=null && !exc.message.contains('NotModifiedException')) {
            throw new RuntimeException(exc)
        }
    }
}

Теперь, наконец, научим наш грейдл создавать и запускать контейнеры

import com.bmuschko.gradle.docker.tasks.container.DockerCreateContainer
import com.bmuschko.gradle.docker.tasks.container.DockerStartContainer
task createContainer(type: DockerCreateContainer) {
    dependsOn buildImage, removeContainer
    targetImageId buildImage.getImageId()
    containerName = "$uniqueContainerName"
    portBindings = ['8080:8080']
}

task startContainer(type: DockerStartContainer) {
    dependsOn createContainer
    targetContainerId("$uniqueContainerName")
}

 

Итоговый build.gradle должен выглядеть как-то так

plugins {
    id 'java'
    id 'application'
    id 'org.springframework.boot' version '2.1.0.RELEASE'
    id 'com.bmuschko.docker-remote-api' version '4.8.0'
}

import com.bmuschko.gradle.docker.tasks.container.DockerCreateContainer
import com.bmuschko.gradle.docker.tasks.container.DockerStartContainer
import com.bmuschko.gradle.docker.tasks.container.DockerRemoveContainer
import com.bmuschko.gradle.docker.tasks.container.DockerStopContainer
import com.bmuschko.gradle.docker.tasks.image.DockerBuildImage
import com.bmuschko.gradle.docker.tasks.image.DockerRemoveImage
import com.bmuschko.gradle.docker.tasks.image.Dockerfile


def dockerBuildDir = 'build/docker/'
def imageVersion = '1.0'
def uniqueContainerName = 'test_docker_plugin'

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.springframework.boot:spring-boot-starter-web:2.1.0.RELEASE'
}

application {
    mainClassName = 'com.tune_it.App'
}

jar {
    manifest {
        attributes 'Main-Class': 'com.tune_it.App'
    }

    from {
        configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
    }
}

task createDockerfile(type: Dockerfile) {
    destFile = project.file("$dockerBuildDir/Dockerfile")
    from 'openjdk:8-jre-alpine'
    copyFile jar.archiveName, '/app/test_service.jar'
    entryPoint 'java'
    defaultCommand '-jar', '/app/test_service.jar'
    exposePort 8080
    runCommand 'apk --update --no-cache add curl'
    instruction 'HEALTHCHECK CMD curl -f http://localhost:8080/hello || exit 1'
}

task syncJar(type: Copy) {
    dependsOn assemble
    from jar.archivePath
    into dockerBuildDir
}

task stopContainer(type: DockerStopContainer) {
    targetContainerId("$uniqueContainerName")
    onError { exc ->
        if (exc.message!=null && !exc.message.contains('NotModifiedException')) {
            throw new RuntimeException(exc)
        }
    }
}

task removeContainer(type: DockerRemoveContainer) {
    dependsOn stopContainer
    targetContainerId("$uniqueContainerName")
    onError { exc ->
        if (exc.message!=null && !exc.message.contains('NotModifiedException')) {
            throw new RuntimeException(exc)
        }
    }
}

task removeImage(type: DockerRemoveImage) {
    dependsOn removeContainer
    targetImageId("adpashnin/gradle_docker_plugin:$imageVersion")
    onError { exc ->
        if (exc.message!=null && !exc.message.contains('NotModifiedException')) {
            throw new RuntimeException(exc)
        }
    }
}

task buildImage(type: DockerBuildImage) {
    dependsOn createDockerfile, syncJar
    inputDir = project.file(dockerBuildDir)
    tags = ["adpashnin/gradle_docker_plugin:$imageVersion"]
}

task createContainer(type: DockerCreateContainer) {
    dependsOn buildImage, removeContainer
    targetImageId buildImage.getImageId()
    containerName = "$uniqueContainerName"
    portBindings = ['8080:8080']
}

task startContainer(type: DockerStartContainer) {
    dependsOn createContainer
    targetContainerId("$uniqueContainerName")
}

 

Протестируем что же у нас получилось

Выполним gradle startContainer в лог дожны вывестись сообщения от докера о процессе сборки образа. 

Выполним docker ps

CONTAINER ID        IMAGE               COMMAND                  CREATED                            STATUS                             PORTS                         NAMES
e7df554a0573        80fcf9fc17d7        "java -jar /app/test…"   About a minute ago   Up About a minute (healthy)   0.0.0.0:8080->8080/tcp   test_docker_plugin

Протестируем

$ curl localhost:8080/hello
Hello!

Успех! Оно даже работает:D

Если необходимо внести изменения в код и перезапустить контейнер, просто делаем gradle startContainer и всё, gradle сам остановит и удалит старые версии контейнера и образа, затем создаст и запустит уже обновлённые.
 

На это всё, спасибо за внимание