Lambdaカクテル

京都在住Webエンジニアの日記です

Invite link for Scalaわいわいランド

sbt-packでScalaのDockerイメージを作る

以前、sbt-native-packagerでScalaのDockerイメージを作る方法を紹介した。今回はsbt-packを利用した、より軽量な手法を紹介する。

前回の記事はこちら:

blog.3qe.us

sbt-pack: 1つのディレクトリにまとめる君

sbt-packを聞いたことがなくても、sbt-assemblyについて聞いたことがあるScalaエンジニアは多いと思う。今回紹介するsbt-packは、sbt-assemblyからUberjar*1を作成する機能を取り除いたような、シンプルなsbtプラグインだ。Scala 3のパッケージングにも使われているとのことで、信頼性は折り紙付きだ。

github.com

sbt-packの使い方は簡単だ。sbt packを実行すると、target/pack/以下に、アプリケーションの実行に必要な全てのライブラリとアプリケーションコードが展開される。これだけだ。

$ sbt pack
...

$ tree target/pack
target/pack # 簡易的なMakefileとVERSIONファイルが入る
├── bin # ラッパースクリプトが入る
└── lib # JARファイルがぞろぞろ入っている

3 directories

デフォルトでは、target/pack/bin/mainにmainメソッド実行用のラッパーが出力される。エントリポイントは複数指定でき、複数指定した場合はその数だけラッパースクリプトが配置されるという仕組みだ。

設定自体は簡単なので公式ページを参照されたい。最低限必要な設定は以下の通りだ:

// plugins.sbt
// 執筆時点での最新版
addSbtPlugin("org.xerial.sbt" % "sbt-pack" % "0.17")

// build.sbt
enablePlugins(PackPlugin)

これだけあればsbt packを実行可能だ。

sbt-packを使ってDockerイメージを作成する

sbt-packで作成されたファイル群をJavaのDockerイメージの上に載せれば、それだけでDockerファイルが完成する。この方式の利点は、圧縮工程が含まれない(後述)ためsbt-assemblyを使う手法よりも非常に速いという点だ。

例えば以下のようなDockerfileを構成できる:

# Dockerfile
# RUN THIS SCRIPT AFTER sbt pack.
FROM amazoncorretto:17.0.8
WORKDIR /opt/docker
COPY --chown=daemon:root /target/pack/bin /opt/docker/bin
COPY --chown=daemon:root /target/pack/lib /opt/docker/lib
RUN ["chmod", "u+x,g+x", "/opt/docker/bin/main"]
USER daemon
ENTRYPOINT ["sh", "/opt/docker/bin/main"]
CMD ["some", "argument", "for", "entrypoint"]

このDockerfileは、sbt packした結果をそっくりイメージに吸い上げる。簡単だ。このイメージを実行すると、あらかじめ定義しておいたmainメソッドに引数(some argument for entrypoint)が渡される。

ちなみにADDコマンドは.tgz形式の展開を行ってくれるため、以下のようなテクニカルな構成も可能だ*2:

// build.sbt
// tar.gz内に中間ディレクトリを挟まず直接binやlibを配置する
packArchiveStem := ""

$ sbt packArchiveTgz # target/project名-0.1.0-SNAPSHOT.tar.gzが生成される

# Dockerfile
FROM amazoncorretto:17.0.8
WORKDIR /opt/docker
# ADDがtar.gzを展開する
ADD --chown=daemon:root /target/project名-0.1.0-SNAPSHOT.tar.gz /opt/docker/
RUN ["chmod", "u+x,g+x", "/opt/docker/bin/main"]
USER daemon
ENTRYPOINT ["sh", "/opt/docker/bin/main"]
CMD ["some", "argument", "for", "entrypoint"]

何らかの都合でアーカイブをURL上に配置したい場合などに使えるかもしれない(だったらJARにしたほうがいいと思うけど)。

個人的には無圧縮TARを生成するタスクが定義されていると速度とハンドリングの簡易さを両立できて便利だと思う(コードを見たところ、そのようなタスクは定義されていないが、PRを作るのは容易そうに見える)。

sbt-assemblyとの対比

sbt-packの特異な点は、実行に必要なファイルを集める一方、圧縮などを一切行なわないという点だ。これが速度に利している。JARファイルは実はZIPファイルなので、作成時に圧縮過程が入り時間を取られてしまう。sbt-packを使う場合はこの過程がまるっと不要になるので、CI/CD速度を上げられる。

加えて、JARファイルにパッケージする際に必ずと言ってもいいほど必要になるファイルコンフリクト時の処置についても考えなくて良いため、build.sbtはかなりシンプルになる。

sbt-packが複数のエントリポイントにも対応しているのも嬉しいポイントだ。sbt-assemblyを使う場合はjava -jarのかわりにjava -classpathを使ってJARファイルを起動することになるだろう。

まとめ

ScalaのDockerイメージを構成したい場合はsbt-packをファーストチョイスとしても良いかもしれない。直交的でミニマルな優れたプラグインだ。よくメンテナンスされており実績もある。

参考文献

@taroleoさんにsbt-packを教えていただきました。ありがとうございました。

*1:java -jarで直接実行できるような1つにまとまったJARのこと

*2:圧縮に時間がかかるため、あまり旨みはない

★記事をRTしてもらえると喜びます
Webアプリケーション開発関連の記事を投稿しています.読者になってみませんか?