すきま風

勉強したことのメモとか

JibでLayer architect な Spring Boot Applicationをdocker imageにする

Spring Boot ApplicationをDocker image化する手段として

等があります。(あります、とか大上段からものを言っていますが、ここ1週間くらいで仕入れた知識です。私はクソ雑魚です。)

Spring Boot 2.3.0からmvn spring-boot:build-imageでCloud Native Buildpackを使ったdocker imageが作成できるようになるらしく 今後のデファクトはCloud Native Build Packになりそうな予感があります。

とりあえず、一番簡単そうなJibを試しました。Build Packはその次に勉強する予定です。

今回から3回くらいにわたって、Jibでハマったところを記事に残していきます。


Layer architectでresourcesが別のsubProjectsに存在するケース

2020/1/30 追記

[Notice!!] Version 2.0.0でSpring Boot Fat Jarが正式にサポートされました。 Multi-Project構成の場合、Fat Jarを利用するのがベストプラクティスです!そちらも記事にしておりますのでよろしければどうぞ
以下の記事はVersion 2.0.0以前に、Fat Jarを使わないでどうにかならないか考えていたころのものになります。記事として読む価値がなくなりました、残念!


以下のような、main classが置いてあるdocker-app-webと resourcesを置いているdocker-app-envという2つのsubProjectsで構成しているApplicationで試します (いきなりレアケースですが🙃)

docker-app
├── build.gradle.kts
├── docker-app-env
│   └── src
│       └── main
│           └── resources
│               └── config
│                   ├── application-development.yml
│                   ├── application-local.yml
│                   └── application.yml
├── docker-app-web
│   ├── build.gradle.kts
│   └── src
│       └─── main
│            └── kotlin
│                └── com
│                    └── example
│                        └── dockerapp
│                            ├── DockerAppApplication.kt
│                            └── web
│                                └── HelloController.kt
├── gradlew
└── settings.gradle.kts

settings.gradle.kts

include(":docker-app-web", ":docker-app-env")

build.gradle.kts

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    id("org.springframework.boot") version "2.2.4.RELEASE" apply false
    id("io.spring.dependency-management") version "1.0.9.RELEASE"

    // multi-projectの場合、rootにはapply falseをつけて、main classがあるsubProjectで再度 plugins設定をします。
    id("com.google.cloud.tools.jib") version "1.8.0" apply false
    id("java")
    kotlin("jvm") version "1.3.61"
    kotlin("plugin.spring") version "1.3.61"
}

allprojects {
    group = "com.example"
    version = "0.0.1-SNAPSHOT"
}

repositories {
    mavenCentral()
    jcenter()
}

subprojects {
    apply(plugin = "kotlin")
    apply(plugin = "org.jetbrains.kotlin.plugin.spring")
    apply(plugin = "io.spring.dependency-management")
    dependencyManagement {
        imports {
            mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES) {
                bomProperty("kotlin.version", "1.3.61")
            }
        }
    }

    java.sourceCompatibility = JavaVersion.VERSION_1_8
    java.targetCompatibility = JavaVersion.VERSION_1_8

    repositories {
        mavenCentral()
    }

    dependencies {
        implementation("org.jetbrains.kotlin:kotlin-reflect")
        implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
        implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
        implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
        testImplementation("org.springframework.boot:spring-boot-starter-test") {
            exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
        }
        testImplementation("io.projectreactor:reactor-test")
    }

    tasks {
        withType<Test> {
            useJUnitPlatform()
        }

        withType<KotlinCompile>().configureEach {
            kotlinOptions {
                freeCompilerArgs = listOf("-Xjsr305=strict")
                jvmTarget = "1.8"
            }
        }
    }
}

docker-app-web/build.gradle.kts

import org.springframework.boot.gradle.tasks.bundling.BootJar

plugins {
    id("org.springframework.boot")
    id("io.spring.dependency-management")
    id("com.google.cloud.tools.jib")
}

dependencies {
    implementation(project(":docker-app-env"))
    implementation("org.springframework.boot:spring-boot-starter-webflux")
}

tasks.getByName<BootJar>("bootJar") {
   mainClassName = "com.example.dockerapp.DockerAppApplicationKt"
}

// https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin
jib {
    from {
        // test用にdebugを利用
        image = "gcr.io/distroless/java:11-debug"
    }

    container {
        mainClass = "com.example.dockerapp.DockerAppApplicationKt"
        creationTime = "USE_CURRENT_TIMESTAMP"
    }
}

exec Jib

以上のApplicationに対してJibDockerBuildでimageを作ると、 docker-app-env上のresourcesはcopyされません。

$ ./gradlew jibDockerBuild
# ...jib end
$ docker run -it --entrypoint /busybox/sh docker-app-web:0.0.1-SNAPSHOT
# # ls -l /app/
drwxr-xr-x    4 root     root          4096 Jan  1  1970 classes
drwxr-xr-x    1 root     root          4096 Jan  1  1970 libs

subProjectsをincludeしていても、Jibを実行するsubProjects上のclass pathに存在するresources (つまり、docker-app-webのresources) しかcopyしてくれないようです (悲)

gradle を修正する

仕方がないので、docker-app-envをincludeしないで、docker-app-web上でsrcDirsに追加するようにします。

settings.gradle.kts

// include(":docker-app-web", ":docker-app-env")
include(":docker-app-web")

docker-app-web/build.gradle.kts

import org.springframework.boot.gradle.tasks.bundling.BootJar

plugins {
    id("org.springframework.boot")
    id("io.spring.dependency-management")
    id("com.google.cloud.tools.jib")
}

dependencies {
    // コメントアウト
    // implementation(project(":docker-app-env"))
    implementation("org.springframework.boot:spring-boot-starter-webflux")
}

// srcDirs設定
sourceSets {
    main {
        resources {
            setSrcDirs(
                listOf(
                    "src/main/resources",
                    "${project.rootDir}/docker-app-env/src/main/resources"
                )
            )
        }
    }
}

tasks.getByName<BootJar>("bootJar") {
    mainClassName = "com.example.practice.PracticeApplicationKt"
}

// https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin
jib {
    from {
        // image = 'gcr.io/distroless/java@sha256:0ce06c40e99e0dce26bdbcec30afe7a890a57bbd250777bd31ff2d1b798c7809'
        image = "gcr.io/distroless/java:11-debug"
    }

    container {
        mainClass = "com.example.dockerapp.DockerAppApplicationKt"
        creationTime = "USE_CURRENT_TIMESTAMP"
    }
}
$ ./gradlew jibDockerBuild
# ...jib end
$ docker run -it --entrypoint /busybox/sh docker-app-web:0.0.1-SNAPSHOT
# ls -l /app/resources/config/
total 12
-rw-r--r--    1 root     root            70 Jan  1  1970 application-development.yml
-rw-r--r--    1 root     root            62 Jan  1  1970 application-local.yml
-rw-r--r--    1 root     root           165 Jan  1  1970 application.yml

resourcesをcopyできました。Dockerを起動する場合も、LocalでIDEで起動する場合でもresourcesを参照できます。
でも、なんかダサい。とてつもなくダサい。


extraDirectoriesを使ってenv層のresources以下をrootにcopyしてclass pathに追加、ということも考えましたが、entrypointを上書きするとjvmFlagsみたいなoptionが使えなくなってしまうのでこれも微妙。
もっといい方法がありそうだけどFat Jarを使わないケースでのbuildに関して、自分ではこれ以上はむりくぼでした。

参考

GitHub - GoogleContainerTools/jib: 🏗 Build container images for your Java applications.

Cloud Native Buildpack Documentation · Cloud Native Buildpack Documentation

Spring Boot 2.3.0 M1 Release Notes · spring-projects/spring-boot Wiki · GitHub

Dockerで動かす軽量なJava環境の作成 - Qiita

SpringBootのdockerイメージを必要最小限に絞りたい(2019年9月版) - エムスリーテックブログ