Playframework 3.0.0にアップデートしてみた

こんにちは、エンジニアの岩松です。

この記事はScala Advent Calendar 2023 20日目の記事となります。つまり今日は12月20日です。

AssuredではバックエンドアプリケーションにScalaおよびPlayframeworkを採用しており、直近メジャーアップデートとなる3.0.0が公開されたため、2.8.x→3.0.0のアップデート対応をしていました。作業メモとして公開しますのでご参考になれば幸いです。

何が変わったのか?

Playframeworkのメジャーアップデートは久しぶりで、今回2.9.03.0.0が同時期にリリースされたのですが大きな変更点はそれぞれ

  • 2.9.0 … 最新のJVM(2023/12/27時点で21)やScala3への対応
  • 3.0.0 … AkkaからPekkoへの切り替え

となっています。AkkaはPlayframeworkの内部で利用される並列・分散処理のライブラリなのですが、Lightbend社がライセンスを変更したためAkkaをフォークしたPekkoが利用されることになりました(「ペッコ」と呼べばいいんでしょうか?)。

ハマったこと

基本的にマイグレーションガイド通りに作業を進めれば良いのですが、いくつかハマりポイントがあったのでご紹介していきますので、ご参考になれば幸いです。

Pekko関連ライブラリがバージョン解決できずDI(Dependency Injection)が失敗する

各種修正が完了しコンパイルができるようになったので、いざ実行してみるとDIが失敗してしまいました。DIの失敗とともに以下のようなエラーメッセージが表示されます。

You are using version 1.0.2 of Apache Pekko, but it appears you (perhaps indirectly) also depend on older versions of related artifacts. You can solve this by adding an explicit dependency on version 1.0.2 of the [pekko-protobuf-v3, pekko-slf4j, pekko-serialization-jackson, pekko-stream, pekko-actor-typed] artifacts to your project. Here's a complete collection of detected artifacts: (1.0.1, [pekko-actor-typed, pekko-protobuf-v3, pekko-serialization-jackson, pekko-slf4j, pekko-stream]), (1.0.2, [pekko-actor, pekko-testkit]). See also: https://pekko.apache.org/docs/pekko/current/common/binary-compatibility-rules.html#mixed-versioning-is-not-allowed

これはエラーメッセージ自体やリンク先で説明されている通りですが、関連ライブラリのバージョンが混在しないよう、以下のようにbuild.sbtで直接インポート(バージョン指定)していく必要があります。

val PekkoVersion = "1.0.2"

libraryDependencies ++= Seq(
  ...
  "org.apache.pekko" %% "pekko-protobuf-v3" % PekkoVersion,
  "org.apache.pekko" %% "pekko-slf4j" % PekkoVersion,
  ...
)

設定ファイル経由でのExecutionContext生成ができない

PlayframeworkではThread Poolをアプリケーション起動時に確保しておき、任意のタスクに対して割り振るということができます。これにはAkkaが利用されており、設定ファイル(application.conf)に

akka {
  my-executor {
    parallelism-factor = 20.0
    parallelism-max = 200
  }
}

といった具合で記述しておくことで

val myExecutionContext: ExecutionContext = actorSystem.dispatchers.lookup("my-executor")

このように利用することができました。3.0.0以降ではマイグレーションガイドに従い

pekko {
  my-executor {
    parallelism-factor = 20.0
    parallelism-max = 200
  }
}

と書き換えるだけで良いはずでしたが、実際にアプリケーションを起動するとExecutionContextがうまく作成できていません。色々試してみたところ

val myExecutionContext: ExecutionContext = actorSystem.dispatchers.lookup("pekko.my-executor")

のように記述する必要があるとわかりました。

また、地味にハマってしまったのですが、Playframework内部で利用されているPekko(Akka)はPekko Typed(Akka Typed)ではないため、Classic Dispatchersのドキュメントを参照する必要があります。ググるとPekko Typed(Akka Typed)向けの情報が出て来やすいのですが、型が合わなくて頭を悩ませることになります。

Dockerコンテナが動かない

AssuredではKuberenetes(GKE)を利用しているため、実際にアプリケーションを稼働する際にはDockerイメージを作成しているのですがこちらも失敗してしまいました。正確にはイメージが作られているものの、あるべきバイナリが存在しておらずコンテナを起動しても何も実行できていないという状態です。Dockerイメージのビルド結果を確認してみるとディレクトリ構成が

target/docker/stage
└─opt/docker
│ ├─bin
│ │ ├─...
│ ├─conf
│ │ ├─...
│ ├─lib
│ │ ├─...
│ ├─...
└───Dockerfile

から

# バイナリ実行なファイル群が何故か小分けになった
target/docker/stage
├─1
│ └─opt/docker
│   └─conf
│     ├─...
├─2
│ └─opt/docker
│   └─lib
│     ├─...
├─4
│ └─opt/docker
│   └─bin
│     ├─...
├─opt/docker
│ ├─...
└─Dockerfile

に変わっていることがわかりました。Dockerイメージ作成に利用されるsbt-native-packagerはデフォルトだと良い感じにDockerfileを作成してくれるのですが、Assuredでは利用するJVMを指定するため、以下のように直接Dockerfileを記述していました。

// build.sbt
dockerCommands := Seq(
  Cmd("FROM", "CUSTOM_JVM:21"),
  ...
  Cmd("COPY", "opt", "/opt"), // opt を持ってきても必要なファイルが揃っていない
  ...
)

したがって、Dockerイメージ内でバイナリや設定ファイルが見つからず問題が起きていたということになります。

おわりに

これらのハマりポイントを解決したことで、無事にPlayframework 3.0.0へアップデートすることができました。PekkoがAkkaのフォークであるためか、動作上大きな変化も確認していません。また、併せて最新のJVMを利用できるようになったことで、コンパイル速度や実行速度が改善している気がします(実際のパフォーマンスは測定し損ねてしまいました…)。

ライブラリの都合でScala3はまだ導入できていないのですが、障壁はかなり下がったと思います。そのライブラリもScala3対応が進んでいることは確認しているので、AssuredでScala3を本格運用できる日も近いのではないかと思います!😤