0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Dockerfile内のGradleコマンドを分解して理解する――Spring Bootの実行可能JARをapp.jarにコピーするまで

0
Posted at

Spring BootアプリケーションのDockerイメージを作成する際、Dockerfileに次のような処理を記述することがあります。

RUN ./gradlew clean bootJar --no-daemon \
    && JAR_FILE=$(find build/libs -maxdepth 1 -type f \
        -name "*.jar" ! -name "*-plain.jar" | head -n 1) \
    && cp "$JAR_FILE" app.jar

一見すると複雑ですが、この処理が行っていることは大きく分けて次の3つです。

  1. Spring BootアプリケーションをJARファイルとしてビルドする
  2. 生成された実行可能JARを探す
  3. 見つけたJARをapp.jarという固定名でコピーする

この記事では、コマンドとオプションを一つずつ分解して解説します。

厳密には、このRUN命令だけでアプリケーションをDockerへ「デプロイ」しているわけではありません。Dockerイメージのビルド中に、Spring Bootアプリケーションを実行可能JARとして生成・配置する処理です。

コマンド全体の流れ

まず、改行を取り除いて1行にすると次のようになります。

RUN ./gradlew clean bootJar --no-daemon && JAR_FILE=$(find build/libs -maxdepth 1 -type f -name "*.jar" ! -name "*-plain.jar" | head -n 1) && cp "$JAR_FILE" app.jar

処理の流れを日本語にすると、次のようになります。

Gradleで実行可能JARを作成する
    ↓ 成功した場合
生成されたJARファイルを検索してJAR_FILE変数に設定する
    ↓ 成功した場合
見つけたJARファイルをapp.jarとしてコピーする

RUN

RUN ...

RUNは、Dockerイメージのビルド中にコマンドを実行するためのDockerfile命令です。

この例はシェル形式のRUNであり、Linux系のイメージでは通常、シェルを通してコマンドが実行されます。そのため、次のようなシェルの機能を利用できます。

  • &&によるコマンドの連結
  • $(...)によるコマンド置換
  • $JAR_FILEによる変数展開
  • |によるパイプ処理

RUNで実行された結果は、Dockerイメージの新しいレイヤーとして保存されます。

行末の\

RUN ./gradlew clean bootJar --no-daemon \
    && JAR_FILE=$(...) \
    && cp "$JAR_FILE" app.jar

行末の\は、命令が次の行へ続いていることを表します。

実際には1つのRUN命令ですが、長いコマンドを複数行に分けることで読みやすくしています。

次の2つは、基本的に同じ処理です。

RUN command1 && command2 && command3
RUN command1 \
    && command2 \
    && command3

\の後ろに空白を入れると、正しく行継続されないことがあります。そのため、\は行末の最後の文字として記述します。

./gradlew

./gradlew

gradlewは、Gradle Wrapperを実行するためのスクリプトです。

システムにインストールされているgradleコマンドを直接使用するのではなく、プロジェクトで指定されたバージョンのGradleを使用します。

Gradle公式ドキュメントでも、Gradle Wrapperを使用することが推奨されています。

先頭の./は、現在のディレクトリにあるgradlewを実行するという意味です。

Dockerfileで実行するためには、事前にgradlewなどをイメージ内へコピーしておく必要があります。

COPY gradlew .
COPY gradle gradle
COPY build.gradle .
COPY settings.gradle .
COPY src src

また、Linux上で実行権限が必要な場合は、次の処理も必要です。

RUN chmod +x gradlew

clean

./gradlew clean

cleanは、以前のビルドで生成されたファイルを削除するGradleタスクです。

通常は、プロジェクトのbuildディレクトリが削除されます。

例えば、以前のビルドで次のファイルが残っていたとします。

build/libs/sample-0.0.1-SNAPSHOT.jar
build/libs/sample-0.0.2-SNAPSHOT.jar

この状態でJARファイルを検索すると、古いJARを誤って取得する可能性があります。

最初にcleanを実行することで、古いビルド結果を削除してから新しいJARを作成できます。

ただし、cleanを実行するとGradleの差分ビルドによる恩恵を受けにくくなります。Dockerビルドでは毎回確実に成果物を作り直したい場合に使われますが、常に必須というわけではありません。

bootJar

./gradlew bootJar

bootJarは、Spring Bootアプリケーションを実行可能JARとしてパッケージングするタスクです。

Spring BootのGradle Pluginによって追加されます。

通常のJARとは異なり、bootJarで生成されるJARには、アプリケーションのクラスだけでなく実行に必要な依存ライブラリなども含まれます。

そのため、生成されたJARは次のように実行できます。

java -jar app.jar

Spring Boot公式ドキュメントでは、bootJarは実行可能JARを作成するタスクとして説明されています。アプリケーションのクラスはBOOT-INF/classes、依存ライブラリはBOOT-INF/libへ格納されます。

JARファイルは、通常、次のディレクトリに生成されます。

build/libs/

なお、bootJarタスクはJARの作成を目的としたタスクです。一般的なbuildタスクとは異なり、通常はテストの実行までは行いません。

テストも含めてビルドしたい場合は、プロジェクトの設定に応じて、事前に次のような処理を実行します。

./gradlew clean test bootJar

または、通常のビルド処理として次を使用します。

./gradlew clean build

--no-daemon

./gradlew clean bootJar --no-daemon

--no-daemonは、Gradle Daemonを継続利用せずにビルドするためのオプションです。

Gradle Daemonは、Gradleの実行に必要なJVMプロセスをバックグラウンドで動作させ、次回以降のビルドを高速化する仕組みです。現在のGradleではDaemonの使用がデフォルトになっています。

ローカル開発ではビルド時間を短縮できるため便利ですが、Dockerイメージのビルドではビルド処理が終わると実行環境も破棄されます。

そのため、バックグラウンドプロセスを継続させる利点が小さく、次のように--no-daemonを付けることがあります。

./gradlew bootJar --no-daemon

ただし、--no-daemonを付けた場合でも、Gradleの実行条件によってはビルド専用の一時的なプロセスが使用される場合があります。

&&

command1 && command2

&&は、左側のコマンドが成功した場合だけ、右側のコマンドを実行する演算子です。

今回のコマンドでは次のように連結されています。

./gradlew clean bootJar --no-daemon \
    && JAR_FILE=$(...) \
    && cp "$JAR_FILE" app.jar

例えば、Gradleのビルドが失敗した場合は、JARファイルの検索やコピーは実行されません。

Gradleビルド成功
    → JAR検索を実行

Gradleビルド失敗
    → 以降の処理を実行しない

Dockerイメージのビルドも、その時点で失敗します。

複数のRUN命令に分けず、&&で連結することで、関連する処理をひとまとまりとして扱えます。

JAR_FILE=$(...)

JAR_FILE=$(find ...)

これは、コマンドの実行結果をJAR_FILEというシェル変数へ代入しています。

$(...)はコマンド置換と呼ばれる構文です。

括弧内のコマンドを実行し、その標準出力を文字列として取得します。

簡単な例を挙げると、次のコマンドではpwdの実行結果がCURRENT_DIRへ代入されます。

CURRENT_DIR=$(pwd)

今回の場合は、findコマンドで検索したJARファイルのパスが代入されます。

JAR_FILE=$(find build/libs ...)

例えば検索結果が次のパスだった場合、

build/libs/sample-0.0.1-SNAPSHOT.jar

変数は次のような状態になります。

JAR_FILE="build/libs/sample-0.0.1-SNAPSHOT.jar"

find build/libs

find build/libs

findは、指定したディレクトリ以下から条件に一致するファイルやディレクトリを検索するコマンドです。

ここでは、build/libsを検索開始地点にしています。

build/libs
├── sample-0.0.1-SNAPSHOT.jar
└── sample-0.0.1-SNAPSHOT-plain.jar

この中から、Dockerコンテナで実行するJARを探します。

-maxdepth 1

find build/libs -maxdepth 1

-maxdepthは、検索するディレクトリの深さを制限するオプションです。

-maxdepth 1

とした場合、検索対象はbuild/libs自身と、その直下までになります。

例えば次の構成の場合、

build/libs/
├── sample.jar
└── backup/
    └── old-sample.jar

-maxdepth 1を指定すると、backupの中にあるold-sample.jarまでは検索しません。

GNU Findutilsのドキュメントでは、-maxdepthは検索開始地点から降りる階層数の上限を指定するものと説明されています。

今回生成されるJARは通常build/libsの直下にあるため、それより深いディレクトリを検索する必要はありません。

なお、-maxdepthはGNU系のfindなどで利用されるオプションです。使用するベースイメージやOSによって、搭載されているfindの実装が異なる可能性には注意が必要です。

-type f

-type f

-typeは、検索対象の種類を指定する条件です。

fは通常のファイルを表します。

-type f

とすることで、ディレクトリではなくファイルだけを検索対象にします。

似た指定として、ディレクトリを検索する場合はdを使います。

-type d

-name "*.jar"

-name "*.jar"

-nameは、ファイル名のパターンを指定する条件です。

-name "*.jar"

は、名前が.jarで終わるファイルを検索します。

例えば次のファイルが対象になります。

sample.jar
sample-0.0.1.jar
sample-0.0.1-SNAPSHOT.jar

*.jarをダブルクォートで囲んでいる点も重要です。

-name "*.jar"

クォートしなかった場合、findが実行される前に、シェルが*を展開してしまう可能性があります。

パターンをそのままfindへ渡すため、クォートで囲んでいます。

! -name "*-plain.jar"

! -name "*-plain.jar"

!は、直後の検索条件を否定します。

-name "*-plain.jar"

が「名前が-plain.jarで終わるファイル」を表すため、

! -name "*-plain.jar"

は「名前が-plain.jarで終わらないファイル」という意味になります。

Spring BootのGradle Pluginでは、実行可能JARと通常のJARを区別するため、通常のjarタスクによる成果物にplainというclassifierが設定されます。

例えば、次の2つのJARが生成される場合があります。

sample-0.0.1-SNAPSHOT.jar
sample-0.0.1-SNAPSHOT-plain.jar

それぞれの役割は、おおむね次のようになります。

sample-0.0.1-SNAPSHOT.jar
    Spring Bootの実行可能JAR

sample-0.0.1-SNAPSHOT-plain.jar
    通常のJAR

Dockerコンテナでは一般的に、java -jarで起動できるSpring Bootの実行可能JARを使用します。

そのため、次の条件によって-plain.jarを除外しています。

! -name "*-plain.jar"

なお、今回のようにclean bootJarだけを実行する一般的な構成では、実行可能JARだけが生成される場合もあります。この除外条件は、複数のJARが存在する場合に備えた防御的な指定と考えられます。

|

find ... | head -n 1

|はパイプと呼ばれる機能です。

左側のコマンドの標準出力を、右側のコマンドの標準入力へ渡します。

今回の場合、findが出力した検索結果をheadへ渡しています。

findによる検索結果
    ↓
headコマンド

例えばfindが次のような結果を出力したとします。

build/libs/sample.jar
build/libs/another.jar

この2行がheadコマンドへ渡されます。

head -n 1

head -n 1

headは、入力されたデータの先頭部分を取得するコマンドです。

-nは、取得する行数を指定するオプションです。

head -n 1

は、先頭の1行だけを取得します。

GNU Coreutilsのheadは、ファイルが指定されていない場合、標準入力からデータを読み取ります。

したがって、次の処理では、

find build/libs ... | head -n 1

findで見つかったJARファイルのうち、先頭の1件だけを取得します。

その結果がJAR_FILEへ代入されます。

JAR_FILE=$(find build/libs ... | head -n 1)

cp "$JAR_FILE" app.jar

cp "$JAR_FILE" app.jar

cpは、ファイルをコピーするコマンドです。

この処理では、JAR_FILE変数に格納されたJARファイルを、app.jarという名前でコピーしています。

例えば変数の内容が次の場合、

JAR_FILE="build/libs/sample-0.0.1-SNAPSHOT.jar"

実際には次のような処理になります。

cp build/libs/sample-0.0.1-SNAPSHOT.jar app.jar

コピー後は、次のようになります。

app.jar
build/
└── libs/
    └── sample-0.0.1-SNAPSHOT.jar

なぜapp.jarに名前を統一するのか

Gradleで生成されるJARファイル名には、プロジェクト名やバージョンが含まれることがあります。

sample-0.0.1-SNAPSHOT.jar
sample-0.0.2-SNAPSHOT.jar
sample-1.0.0.jar

そのままDockerfileに記述すると、バージョンを変更するたびに起動コマンドも修正しなければなりません。

ENTRYPOINT ["java", "-jar", "sample-0.0.1-SNAPSHOT.jar"]

JARファイルをapp.jarという固定名にしておけば、元のファイル名やバージョンが変わっても、起動コマンドを変更する必要がありません。

ENTRYPOINT ["java", "-jar", "app.jar"]

ダブルクォートを付ける理由

cp "$JAR_FILE" app.jar

変数をダブルクォートで囲んでいるのは、ファイルパスに空白などが含まれていても、1つの引数として扱えるようにするためです。

例えば、変数に次の値が入っていたとします。

build/libs/sample application.jar

クォートしない場合、

cp $JAR_FILE app.jar

シェルからは次のように複数の引数として解釈される可能性があります。

build/libs/sample
application.jar
app.jar

クォートしておけば、変数全体が1つのファイルパスとして扱われます。

cp "$JAR_FILE" app.jar

シェル変数をファイルパスとして使用する場合は、基本的にダブルクォートで囲むのが安全です。

コマンド全体を改めて確認する

RUN ./gradlew clean bootJar --no-daemon \
    && JAR_FILE=$(find build/libs -maxdepth 1 -type f \
        -name "*.jar" ! -name "*-plain.jar" | head -n 1) \
    && cp "$JAR_FILE" app.jar

それぞれの処理を日本語に置き換えると、次のようになります。

./gradlew
    プロジェクト付属のGradle Wrapperを実行する

clean
    以前のビルド結果を削除する

bootJar
    Spring Bootの実行可能JARを生成する

--no-daemon
    Gradle Daemonを継続利用しない

&&
    直前のコマンドが成功した場合だけ次へ進む

find build/libs
    build/libs以下を検索する

-maxdepth 1
    build/libsの直下までを検索する

-type f
    通常のファイルだけを対象にする

-name "*.jar"
    拡張子が.jarのファイルを対象にする

! -name "*-plain.jar"
    -plain.jarで終わるファイルを除外する

head -n 1
    検索結果の先頭1件を取得する

JAR_FILE=$(...)
    検索結果をJAR_FILE変数へ代入する

cp "$JAR_FILE" app.jar
    対象のJARをapp.jarという名前でコピーする

この書き方の注意点

JARが複数見つかった場合

head -n 1

によって先頭の1件だけを取得していますが、findの出力順は、常に期待した順序になるとは限りません。

実行可能なJARが複数存在する場合、意図しないJARを選ぶ可能性があります。

今回の処理では最初にcleanを実行しているため、古いJARが残る可能性は低くなっています。それでも、複数のモジュールや複数の成果物を生成するプロジェクトでは注意が必要です。

JARが見つからなかった場合

条件に一致するJARが存在しなければ、JAR_FILEは空文字になります。

その後、次のようなコピーが実行されます。

cp "" app.jar

この処理は失敗するため、Dockerイメージのビルドも失敗します。

ただし、エラーメッセージだけでは「JARが見つからなかった」ことが分かりにくい場合があります。

明示的にチェックするなら、次のように記述できます。

RUN ./gradlew clean bootJar --no-daemon \
    && JAR_FILE=$(find build/libs -maxdepth 1 -type f \
        -name "*.jar" ! -name "*-plain.jar" | head -n 1) \
    && test -n "$JAR_FILE" \
    && cp "$JAR_FILE" app.jar

test -n "$JAR_FILE"は、変数が空でないことを確認しています。

よりシンプルにする方法

JARファイルの出力名をGradle側で最初からapp.jarに設定すれば、findcpを使用しなくても済みます。

Groovy DSLのbuild.gradleでは、例えば次のように設定できます。

tasks.named('bootJar') {
    archiveFileName = 'app.jar'
}

Kotlin DSLのbuild.gradle.ktsでは次のようになります。

tasks.named<org.springframework.boot.gradle.tasks.bundling.BootJar>("bootJar") {
    archiveFileName.set("app.jar")
}

この場合、Dockerfileの処理は次のように簡略化できます。

RUN ./gradlew clean bootJar --no-daemon

生成先は次のようになります。

build/libs/app.jar

マルチステージビルドで実行用イメージへコピーする場合は、次のように記述できます。

COPY --from=builder /app/build/libs/app.jar app.jar

ファイル名をGradle側で固定できるのであれば、こちらの方が対象ファイルが明確であり、複数のJARから誤ったファイルを選ぶ心配も少なくなります。

まとめ

今回のDockerfileの処理は、Spring Bootの実行可能JARを生成し、そのJARをapp.jarという固定名に統一するためのものです。

RUN ./gradlew clean bootJar --no-daemon \
    && JAR_FILE=$(find build/libs -maxdepth 1 -type f \
        -name "*.jar" ! -name "*-plain.jar" | head -n 1) \
    && cp "$JAR_FILE" app.jar

長いコマンドでも、次の3段階に分けると理解しやすくなります。

1. clean bootJarで実行可能JARを生成する
2. findで実行可能JARを検索する
3. cpでapp.jarという固定名にする

Dockerfileでは、Gradleだけでなくシェルコマンドも組み合わせて使用します。

それぞれのコマンドを個別に理解することで、Dockerイメージのビルドに失敗した際にも、どの処理で問題が発生しているのかを切り分けやすくなります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?