Play Frameworkで作成したアプリケーションをIBM Cloud Code Engineにデプロイしてみる

# はじめに
前回の記事でIBM Cloud Code Engineにサンプルアプリケーションをデプロイしましたが、今回は自作のアプリケーションをデプロイしてみようと思います。アプリケーションはコンテナになっていればどんな言語やフレームワークを使ってもいいはずなので、今回は（あまりネット上にも情報がなさそうだった）Play Frameworkを使ったアプリケーションをデプロイしてみようと思います。

# Play Frameworkとは
Play Frameworkとは、[公式サイト](https://www.playframework.com)によると

> The High Velocity Web Framework For Java and Scala

とのことです。日本語だと *JavaとScalaを使った高速ウェブアプリケーションフレームワーク* といったところでしょうか。私はJavaの経験が長いので今回はこちらのフレームワークを選んでみます。


# Dockerを使った開発

Play Frameworkを動かすには[Javaはもちろんsbtが動く環境](https://www.playframework.com/documentation/2.8.x/Requirements)が必要です。ですが、Javaもsbtもローカル環境にインストールするとバージョンやら何やらの依存関係で案外管理が難しくなりそうです。なので今回はDockerを使ってなるべく実行環境をコンテナ内に閉じ込めて開発するスタイルを探っていきます。

### 開発環境
まず開発環境の方は、Javaとsbtが既に入っている[scala-sbt](https://hub.docker.com/r/hseeberger/scala-sbt/)を使わせてもらうことにします。以下のような `docker-compose.yml` を用意して簡単に起動できるようにしておきます。

```yaml:docker-compose.yml
version: "3.9"
services:
  sbt:
    image: hseeberger/scala-sbt:11.0.13_1.5.8_2.13.7
    ports:
      - 9000:9000
    volumes:
      - ./root:/root
```
これを使ってコンテナを起動します。

```shell
$ mkdir root
$ docker-compose up -d
[+] Running 1/1
 ⠿ Container play_test_sbt_1  Started
$ docker exec -it play_test_sbt_1 bash
root@8a50fee5f6b7:~# pwd
/root
```

こうなっているので、このディレクトリに作られたファイルはローカルの `./root` に保持されます。

### アプリケーションの作成
では早速この環境を使ってアプリケーションを作成してみます。[ドキュメント](https://www.playframework.com/documentation/2.8.x/NewApplication)にある通りにコマンドを実行します。

```shell
root@8a50fee5f6b7:~# sbt new playframework/play-scala-seed.g8 --name=fterui-play-test
[info] welcome to sbt 1.5.8 (Oracle Corporation Java 11.0.13)
[info] set current project to new (in build file:/tmp/sbt_56c7b9f2/new/)
```
結構時間がかかるのでしばらく待ちます。アプリケーションが作成されると以下のようになります。

```shell
root@8a50fee5f6b7:~# cd fterui-play-test/
root@8a50fee5f6b7:~/fterui-play-test# ls
app  build.sbt	conf  project  public  test
```

早速実行してみます。

```shell
root@8a50fee5f6b7:~/fterui-play-test# sbt run
[info] [launcher] getting org.scala-sbt sbt 1.5.2  (this may take some time)...
```

多くのライブラリをダウンロードした後アプリケーションが起動します。

```shell
--- (Running the application, auto-reloading is enabled) ---

[info] p.c.s.AkkaHttpServer - Listening for HTTP on /0.0.0.0:9000

(Server started, use Enter to stop and go back to the console...)

```
このように表示されれば成功です。このコンテナの9000番ポートはローカルにマップされていますので、ブラウザに`localhost:9000`と入力すればアプリケーションにアクセスできます。

![スクリーンショット 2021-12-23 17.29.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632713/b27809ab-076f-155f-8c42-3466fa820485.png)


### アプリケーションの開発
上記のコンテナの`/root`は`./root`がマウントされているので、ローカルのファイルシステムにあるファイルを編集すれば自動的に実行環境に反映されます。つまりローカルのPCにインストールされている好みのエディタやIDEを使って開発を行うことができます。ここでは例としてこのトップページに`Hello World!`の文字列を表示する変更を加えたいと思います。 `app/views/index.scala.html`を開くと以下のようになっていると思います。

```scala:app/views/index.scala.html
@()

@main("Welcome to Play") {
  <h1>Welcome to Play!</h1>
}
```

これを以下のように変更します。

```scala:app/views/index.scala.html
@()

@main("Welcome to Play") {
  <h1>Welcome to Play!</h1>
  Hello World!
}
```

これを保存してブラウザをリロードすると、ちゃんと変更が反映されていることがわかります。
![スクリーンショット 2021-12-23 17.36.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632713/a0b99d6e-f49a-96f6-a2da-9927a5bc797d.png)

アプリケーションのコンソールの方でもリロードされたことが確認できます。

```shell
2021-12-23 08:26:54 INFO  play.api.Play  Application started (Dev) (no global state)
[info] compiling 1 Scala source to /root/fterui-play-test/target/scala-2.13/classes ...

--- (RELOAD) ---

2021-12-23 08:35:17 INFO  play.api.http.EnabledFilters  Enabled Filters (see <https://www.playframework.com/documentation/latest/Filters>):

    play.filters.csrf.CSRFFilter
    play.filters.headers.SecurityHeadersFilter
    play.filters.hosts.AllowedHostsFilter
2021-12-23 08:35:17 INFO  play.api.Play  Application started (Dev) (no global state)
```

### アプリケーションのビルド
この状態のアプリケーションはあくまでも開発用なので、実際にデプロイする前にはプロダクション用にビルドする必要があります。ビルドはこれも`sbt`コマンドから簡単に行うことができます。

```shell
root@8a50fee5f6b7:~/fterui-play-test# sbt dist
[info] welcome to sbt 1.5.2 (Oracle Corporation Java 11.0.13)
[info] loading settings for project fterui-play-test-build from plugins.sbt ...
[info] loading project definition from /root/fterui-play-test/project
[info] loading settings for project root from build.sbt ...
...(snip)...
[success] All package validations passed
[info] Your package is ready in /root/fterui-play-test/target/universal/fterui-play-test-1.0-SNAPSHOT.zip
[success] Total time: 21 s, completed Dec 23, 2021, 8:40:10 AM
```

このように表示されたら完了です。 `target/universal`以下にできたzipファイルが成果物となります。試しにこちらを起動してみます。

Play Frameworkは起動するときにapplication secretを要求します。詳しくは[ドキュメント](https://www.playframework.com/documentation/2.8.x/ApplicationSecret)にありますので参照してください。例えば以下のようなコマンドで作成することができます。

```shell
# head -c 32 /dev/urandom | base64
HORVsaoVwdmKFsw8Pxl2vvWhCwmOCEuMCUjf/IFawzs=
```

`sbt dist`で作成されたzipファイルを展開したディレクトリで、以下のコマンドでアプリケーションを実行できます。実行ファイル名はプロジェクト名です。

```shell
# ./bin/fterui-play-test -Dplay.http.secret.key=HORVsaoVwdmKFsw8Pxl2vvWhCwmOCEuMCUjf/IFawzs=
...(snip)...
2021-12-23 08:51:08 INFO  play.api.Play  Application started (Prod) (no global state)
2021-12-23 08:51:09 INFO  play.core.server.AkkaHttpServer  Listening for HTTP on /0.0.0.0:9000
```

開発中同様9000番ポートにアクセスすればアプリケーションの動作が確認できます。

# デプロイするイメージの作成

上で作成したzipファイルを使ってデプロイするイメージを作成します。実行にはjavaが必要ですがsbtは不要なので、もっとサイズの小さいイメージを利用できます。ここでは[openjdkの11-jre-slim](https://hub.docker.com/_/openjdk?tab=tags&page=1&name=11-jre-slim)を使います。以下のような`Dockerfile`を作成し、zipファイルを展開したディレクトリに置きます。

```dockerfile:Dockerfile
FROM openjdk:11-jre-slim
COPY . /root/

CMD ["/root/bin/fterui-play-test", "-Dplay.http.secret.key=HORVsaoVwdmKFsw8Pxl2vvWhCwmOCEuMCUjf/IFawzs="]
```

実際の運用ではsecret keyは外から与えることになると思いますが、ここではそのままイメージ内で指定します。

イメージを作成して動作確認を行います。

```shell
$ docker build . -t fterui_play_test

...(snip)...

$ docker run --rm -p 9001:9000 fterui_play_test

...(snip)...

2021-12-23 09:07:21 INFO  play.api.Play  Application started (Prod) (no global state)
2021-12-23 09:07:21 INFO  play.core.server.AkkaHttpServer  Listening for HTTP on /0.0.0.0:9000
```
開発用のコンテナが9000番ポートを使っているので、こちらは9001番にマップしました。ブラウザから`localhost:9001`にアクセスするとプロダクション用のアプリケーションにアクセスできます。

![スクリーンショット 2021-12-23 18.16.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632713/7421cd89-cdb4-a72f-ca94-e00fbd4d9e82.png)

問題なく動作していることが確認できました。

# IBM Cloud Container Registryへpush

作成したイメージをIBM Cloud Code Engineで実行するためには、Code Engine側からpullできる場所へイメージをpushしておく必要があります。

まず`ibmcloud cr`コマンドが使えるようになっている必要があります。[ドキュメント](https://cloud.ibm.com/docs/Registry?topic=Registry-getting-started#gs_registry_cli_install)によるとIBM Cloud CLIをインストールすると使えるようになっているはずですが、私のところではプラグインがインストールされていなかったので[手動でインストール](https://cloud.ibm.com/docs/cli?topic=cli-install-devtools-manually#idt-install-container-registry-cli-plugin)しました。

```shell
$ ibmcloud cr    
失敗
'cr' は登録済みコマンドではありません。 インストール済みプラグインのリストを確認してください。 'ibmcloud help' を参照してください。

$ ibmcloud plugin install container-registry
リポジトリー 'IBM Cloud' から 'container-registry' を検索しています...
プラグイン 'container-registry 0.1.553' がリポジトリー 'IBM Cloud' 内で見つかりました
バイナリー・ファイルをダウンロードしようとしています...
 11.50 MiB / 11.50 MiB [=============================================================================================================] 100.00% 0s
12058224 バイトがダウンロードされました
バイナリーをインストールしています...
OK
プラグイン 'container-registry 0.1.553' は /Users/fterui/.bluemix/plugins/container-registry に正常にインストールされました。 'ibmcloud plugin show container-registry' を使用して詳細を表示してください。
$ ibmcloud cr
名前:
  ibmcloud cr - 

使用法:
  ibmcloud cr command [arguments...] [command options]

...(snip)...
```
コマンドが使えるようになったら、まずnamespaceを作成します。

```shell
$ ibmcloud cr namespace-add fterui_test_ns
レジストリー jp.icr.io のアカウント Fumihiko Terui's Account 用のリソース・グループ「Default」に、名前空間「fterui_test_ns」を追加中...

名前空間「fterui_test_ns」は正常に追加されました

OK
```
ローカルのdockerをcontainer registryに接続させます。

```shell
$ ibmcloud cr login
「jp.icr.io」にログインしています...
「jp.icr.io」にログインしました。

OK
```
接続したら、pushしたいローカルのイメージにタグ付けをします。

```shell
$ docker tag fterui_play_test:latest jp.icr.io/fterui_test_ns/fterui_play_test:latest
$ docker images
REPOSITORY                                  TAG                    IMAGE ID       CREATED          SIZE
fterui_play_test                            latest                 fbf7830ee235   28 minutes ago   264MB
jp.icr.io/fterui_test_ns/fterui_play_test   latest                 fbf7830ee235   28 minutes ago   264MB
```

いざpushします。

```shell
$ docker push jp.icr.io/fterui_test_ns/fterui_play_test:latest                       
The push refers to repository [jp.icr.io/fterui_test_ns/fterui_play_test]
cde2813440c5: Pushed 
7fe0bd342c72: Pushed 
053ceb7fe9fc: Pushed 
b549e2f2b0ec: Pushed 
f397c6a338e4: Pushed 
1c79be3b9ceb: Pushed 
latest: digest: sha256:ef8a1cb7baacac3b55b87325b602aab7c1f01d342f90a200dface3605bb56701 size: 1577
```

これでローカルのイメージがレジストリにpushされました。

# アプリケーションのデプロイ

Pushしたイメージからアプリケーションをデプロイします。IBM Cloudのコンソールからプロジェクトを選んでアプリケーションの作成を選択します。名前を決定してコンテナー・イメージを選択するところまではサンプルの時と同様で、イメージ参照のところに先ほどpushしたイメージを指定します。このイメージはパブリックではないレジストリに置いてあるので場所を指定する必要があります。「イメージの構成」をクリックすると自分のアカウントに結び付けられているレジストリー・サーバーや名前空間などを選択するフライアウトが表示されます。それぞれ1つしか作成してなければ自動的に選択された状態になりますね。

![スクリーンショット 2021-12-23 18.43.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632713/80a49a52-b016-42ce-2668-48926d3592af.png)

Listenポートに9000を指定するのをお忘れなく。全て設定したら「作成」をクリックして完了です。デプロイされるまでお茶でも飲んで待ちます。

が、実際は待てど暮らせど起動しません。流石にそんなに遅いはずはないと思い、ログを確認してみました。

```shell
$ ibmcloud ce application logs -n fterui-play-test
アプリケーション 'fterui-play-test' のすべてのインスタンスのログを取得中...
OK

fterui-play-test-00001-deployment-699c85577-flctb/user-container:    
standard_init_linux.go:228: exec user process caused: exec format error
```
これは実行ファイルのアーキテクチャが違ってる時とかに出るやつですね。私の開発環境はMacbook Air(M1)なのでもしかしたらこれが原因かもと。あともう一つ、[Code Engineではrootユーザーで実行してはいけない](https://cloud.ibm.com/docs/codeengine?topic=codeengine-dockerfile#container-non-root)に引っかかってる可能性もありました。なのでまず`Dockerfile`を変更します。

```dockerfile:Dockerfile
FROM openjdk:11-jre-slim
COPY . /root/

RUN addgroup nonroot --gid 1100 && \
  adduser nonroot --ingroup nonroot --uid 1100 --disabled-password && \
  chown -R nonroot:nonroot /root

USER nonroot
WORKDIR /root

CMD ["/root/bin/fterui-play-test", "-Dplay.http.secret.key=HORVsaoVwdmKFsw8Pxl2vvWhCwmOCEuMCUjf/IFawzs="]
```

さらにM1 Mac上でビルドする時にplatformを指定します。

```shell
$ docker build . -t fterui_play_test --platform linux/amd64   
```

そしてもう一度 `docker tag`と`docker push`をしてイメージを更新してアプリケーションを作成したところ、無事起動しました！

![スクリーンショット 2021-12-23 19.32.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632713/3574678f-bab4-d69e-7d99-ab984a55f252.png)

喜び勇んでアプリケーションにアクセスしたところ、見事以下のような… orz

![スクリーンショット 2021-12-23 19.34.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632713/2a8ba054-5c76-62d2-d0b8-162e018039e9.png)

これは `play.filters.hosts.AllowedHostsFilter` によって許可されてないホストからのアクセスとみなされたためですね。アプリケーションを起動したときにコンソールに出ているやつです。このフィルターにアプリケーションのホスト名を追加してやらないといけなさそうです。詳細は[こちら](https://www.playframework.com/documentation/2.8.x/AllowedHostsFilter)の公式ドキュメントを参照してください。

アプリケーションの`application.conf`に以下のようなエントリを作成します。

```
play.filters.hosts {
    allowed = ["your_host_name", "localhost:9000"]
}
```
(もちろん`your_host_name`は実際のホスト名を入れることになります。）
開発環境で起動して普通にアクセスできることを確認し、それからもう一度イメージをビルドしてローカルのdockerで起動してアクセスしてみます。すると以下のように表示されます。
![スクリーンショット 2021-12-24 9.44.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632713/a219f6a5-8dc8-aeb2-f698-6ac874df8fb2.png)

これはポートを9001番にマップしているせいで許されてないホストからのアクセスだと認識されて拒否されているわけです。開発環境で起動した9000番は大丈夫だったのでちゃんと設定が効いていることが確認できました。このイメージを再度レジストリにpushして、コンソールのアプリケーションから「編集して新規リビジョンを作成」を選び、そのまま何も変更せず作成してしばらく待ちます。デプロイが終わったらいざ確認です。

![スクリーンショット 2021-12-24 9.55.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632713/a2b8ee88-7bd3-a6f8-5257-a82da571246e.png)
無事アプリケーションの動作が確認できました。

# まとめ
IBM Cloud Code Engineを使ってPlay Frameworkを利用したアプリケーションのデプロイを行うのに必要な手順と陥りがちな注意点についてまとめました。完了してみるとある意味straight forwardでしたが、気にしてないとハマる点もいくつか見えたので、同じことを試される方の一助になれば良いかなと思っております。