わざわざsbt-native-packager経由でdocker imageを作る唯一の大きな利点

みなさんはsbtで作ったものを、(単にライブラリとしてではなく全部まとめて)本番環境にデプロイしたりする場合、どういう形式にしてますか?

最近・・・でもないですが、このあたりの選択肢は色々あるというか、sbtやScalaに限らない話なので、 広く捉えればJVM独自のものでも色々あったり(例えばgraalやjlinkやjibなど?)、Docker関連の技術でも細かいこと考えると色々ありますね。

それらも完全に独立して相反するものもなくもないですが、色々組み合わせる可能性もあるので、さらにややこしいですね。

そこについては個人的に詳しくないので、これ以上語りませんというか語れませんが。

さてDockerに関してですが、sbt-native-packagerというのは、かなり大昔からあるsbt pluginです。

github.com

sbt-native-packager.readthedocs.io

playframeworkなども、デフォルトで勝手にこのpluginに依存してます。

Docker関連以外でも、良くも悪くも大量のsbt pluginが1つのgit repoに全部入りなのですが、今回はDockerPluginに関しての話のみをします。

DockerとsbtとScalaJVMなどの習熟度によって、sbtでScalaのアプリをDocker imageにする場合に、どういうのが最適解なのか?の判断が難しいと思います。 詳細は紹介しませんが、sbt-native-packager以外にもDocker関連のsbt pluginは多少存在します。

ある意味での結論からいうと、最低限動かしたいだけならば、何か動けばどの方法でもよい説すらあります。

では、Docker imageにするときや、Docker imageではなくてもいいですが広い意味で何かまとめてデプロイする成果物を生成するにあたって、こだわる場合というのは、どういう点でしょうか?

雑にすぐに思いつく点では

  • build速度(はやい方がいい)
  • buildした成果物のsize(同じことができるならsizeは小さいほうがいい)
  • わかりやすさ、手軽さ

などでしょうか。 Docker的な技術が流行る前はどうしてたんだろう、くらいに多分最近では関連技術が当たり前になってる気はしますが、Docker使う前提の話をするので、Docker使えば当たり前に解決する話などはしません。

ここまでは、ほぼ前置きで、このもう少し後に本題に入るのですが、主に「buildした成果物のsize」(や build速度)に関連する話をします。

sbtで作った自作のものをDocker imageにする場合に、最低限必要なことは何でしょうか?大雑把にまとめると

  • 依存ライブラリのjarをまとめる
  • 自作のプログラム部分をjarにまとめる
  • configや各種resourceなどをまとめる(上記のjarの中に入れる場合もあるが)
  • それらを起動できるような配置に置く
  • 場合によって起動のためのscriptも作って同封する

などのはずです。 もちろん、sbt-native-packagerはそれらを全部やります。 (Docker image作らない場合は、それらの途中までやるものもあります)

jarを生成したり、依存ライブラリや各種色々の一覧を出すか、それを1箇所にまとめるくらいまでは明らかにsbtでやった方がよいですが、それ以降最後Docker imageにする処理はsbtでなくても何でやっても変わらないように思えるというか、sbt不得意な人がわざわざsbtで実行する意味がないように思えるかもしれません。

詳細を知らないと疑問に感じると思うのですが、何故かsbt-native-packagerのDockerPluginは、base imageやその他色々をsbtのkeyとして指定しないといけなくて、全部を管理しようとします。

実際に自分もそう思っていたというか、場合によっては今でもそう思っています。

ここでやっと

「わざわざsbt-native-packager経由でdocker imageを作る唯一の大きな利点」

というタイトルの件です。

ここまで書いておいて、実際は以下のあたりを読んでもらえば、今回言いたいことが実質全て書いてあるので

これらをかるく読んで理解した人は、この先というかここの前の部分も読んでもらう必要ありませんでした、すいません。

大雑把にまとめると

  • Dockerというのはレイヤーがある
  • 同じレイヤーは再利用される
    • AWSのECRなどのdocker registory側でも。つまり転送量観点でも保存量観点でも、お金の節約になる可能性がある
  • sbtやScalaJVMに限らずDocker一般的な話として、その仕組みを利用して、ある程度いい感じに再利用されるように工夫するべきである
  • 具体的なScalaJVM的な話だと「依存ライブラリのjar」と「自分たちのプログラムのjar」は、普通は依存ライブラリのjarの更新頻度の方が低いはず?だから、そこでレイヤーを分ければ、再利用される可能性が高まる
  • sbt-native-packagerの1.7.0、2020年から、デフォルトでそれをする機能が入ってる
  • すごくsbtにもdockerにも習熟してる人は、逆にそこまで自分で何もかも頑張れるかもしれないが、普通の人はsbt-native-packagerのその仕組みに乗っかっていた方が、(独自に頑張るより、他のそれに対応してないsbt plugin使うより)効率が良くなる可能性がある
  • そこの分け方はカスタマイズ可能になっているので、必要に応じてさらにレイヤーの分け方を工夫するべきである
  • 上記の英語のblog記事からの引用ですが、この機能が入る前のsbt-native-packagerと入った後で、依存ライブラリは変更せずに自身のプログラムの一部だけ変えて何度もimageを作成した場合の、全体の容量の変化のグラフが以下。大半の部分が再利用されるので一目瞭然で節約になる

という感じです。

ちなみに、sbt-native-packagerに当初入ったレイヤー分け方法と、最新versionでは、大まかな考え方は同じですが、若干デフォルトの分け方の実装が異なってるので注意してください。

ただし、最初の方に書いた前置きの話がここでやっと役立つのですが、この恩恵を受けるべきか?は、ある程度の条件があって、例えば

  • Docker imageは作るが、(良し悪しはともかく、用途として何故か)1ヶ月に1回程度しか作らないし、レイヤーの共有され具合は全く気にするレベルじゃないよ
  • 同じく、更新頻度がとても低いか何かで、プログラムが更新される時は、依存ライブラリその他も大体が全部更新されるので、別にレイヤー分けても役にたつことがなさそうだよ
  • そもそもDocker registoryに登録もせずに、生でdocker imageを配布する?k8sも何も使わずに直接デプロイする?的な運用なので、あまり恩恵を受ける場面がないよ
  • Docker registoryの容量程度のお金なんて全く気にする必要もないほどお金を大量に持ってるし、build速度その他で現状に不満もないし、とにかくsbtは嫌いなんだsbtでやることを減らしたいんだ!
  • これらの概念や仕組みは知っているけど、何かこだわりがあって完全に自前でそれ相当のことをやりたい、やれるほど色々習熟してるから大丈夫だよ!使わないよ!

という人は必要ないと思います。 とはいえ、おそらくそういうパターンはそこまで多くなく、普通はsbt-native-packager経由で生成してレイヤー勝手に分けてもらったほうが、簡単に恩恵を受けれていいのではないでしょうか?

もし、本番環境のデプロイは週に1回とか月に1回とか、頻度が低くても、開発用の環境では、毎日?あるいはデフォルトbranchにpushされたら全自動ですぐimage作ってデプロイ、というのはよくあると思いますし、そういう場合は明らかに役立つことは多いと思います。

少し上で

何故かsbt-native-packagerのDockerPluginは、base imageやその他色々をsbtのkeyとして指定しないといけなくて、全部を管理

と書きましたが、レイヤー分けしてそれに応じたDockerfile相当を裏で作るなら、ある意味では納得のいく作りだと思います。

また、タイトルに「唯一の」と入れましたが、他に同等かそれ以上にもっとすごい利点があるのか?は個人的にはよくわかっていません。

繰り返しになりますが、もし他の利点がなく、今回説明したレイヤー分けの利点が必要ないならば、現時点では個人的には別にこれ経由でdocker image作成しなくても、途中のjarまとめるまで実行し、その後は手動でDockerfile書いてもいいと思ってます。 実は他にも?大量に?すごい利点あるよ!という情報があれば教えてください。

あと、sbt-native-packager以外に同等の処理をデフォルトで勝手に実施してくれるsbt pluginを自分は知らないのですが、実は他にもあったら教えてください。

自分もsbt-native-packagerがこれをやってることを最近知ったのですが、とはいえ適切なレイヤー分けって難しいだろうし、でもそれらを深く考えると最近のDockerは、中間表現?内部表現?がLLBでDAGになっていて単純な直線的なレイヤーではなくて〜、といった話もあって奥が深いですね。

若干まとまりがないですが、今回はこれで終了です。