Play Framework 2.6 アプリをDocker上で開発する

# 動機
Play Framework でアプリを開発するにあたって

- ローカルの環境にあれこれ（色々なバージョンのJDKなど）インストールしたくない
- 開発メンバーと開発環境を手軽に共有したい（エディタ除く）
- エティタは個人のものを使いたい（ソースコードはローカルの環境から編集したい）
- 出来上がったアプリはDocker環境でデプロイしたい

という希望があって、Dockerで開発環境を構築したのでその記録です。

Play Frameworkは開発が活発で、バージョンごとに変更点も多いです。この記事では現時点（2018/2/6）で最新の Play Framework 2.6について、記述します。

# コツ
たとえばRuby on Railsでしたら、一つのDockerコンテナで開発からデプロイまで全部まかなうことができます（実際にはデプロイではまったりしますが）。
参考：[Quickstart: Compose and Rails (docker docs)](https://docs.docker.com/compose/rails/) 

しかし、Play Framework 2.6の場合、

- プロジェクトの初期化
- 開発
- 本番の実行環境

と、3つのDocker環境を分けて考えたほうがうまくいきます。
これを順を追って説明していきます。

# 全体の流れ
Dockerの有無に関わらず、Play Framework 2.6では、プロジェクトのビルドに[sbt](https://www.scala-sbt.org/)を使用します。
[公式ドキュメント](https://www.playframework.com/documentation/2.6.x/Home)に従うと、Play Frameworkによるアプリの開発は

1. [sbt をインストールする](https://www.playframework.com/documentation/2.6.x/Installing)
2. [sbt new でプロジェクトを初期化する](https://www.playframework.com/documentation/2.6.x/NewApplication)
3. [SBT consoleを起動してアプリケーションを開発する](https://www.playframework.com/documentation/2.6.x/PlayConsole)
4. [出来上がったアプリケーションを本番環境にデプロイする](https://www.playframework.com/documentation/2.6.x/Production)

という流れになります。
これら一連の作業をDockerの環境内に封じ込めてしまいたいというのが本記事の主旨です。
Dockerはインストール済みであるという前提で話を進めていきます。

# sbt のインストール
まずはsbtをインストールしたDockerコンテナを用意します。
hseeberger さんが Docker Hub に、sbtインストール済みのコンテナを公開してくれています。これを利用するのが早くて確実です。
[hseeberger/scala-sbt](https://hub.docker.com/r/hseeberger/scala-sbt/)

これを使えば

```
$ docker run -it --rm hseeberger/scala-sbt:8u151-2.12.4-1.1.0
```
でsbtのイメージを含んだDocker環境を利用できます。

- -it はDockerコンテナをインタラクティブな状態で起動するためのオプションです
- --rm は使い終わったコンテナを自動で破棄するためのオプションです
- Javaのバージョンは大事なので、タグまでがっつり指定しています

上記のコマンドを実行すると、Dockerコンテナ内の```/bin/bash```が起動しますので、bash上でsbtを起動することができます。

```
root@72ece8e13c72:~# sbt
```

※ここでもしも個人公開のリポジトリを使うのが不安だというのでしたら、hseeberger さんが公開してくれているDockerfileを見て、オリジナルのDockerfileを作成しておくのもありです。シンプルなDockerfileですので、一見の価値ありです。
※Java8を使っているのがポイントです。Java9以降でPlay Framework2.6を使おうとするとモジュール不足で色々と大変なことになります。これはDockerの有無とは関係ありません。また、現時点でこの問題を解決できたという情報を見つけられていません。

# プロジェクトの初期化

作業するディレクトリに移動します。このディレクトリの直下にアプリ開発用のディレクトリが新たに作られることになります。
たとえば、```~/workspaces```ディレクトリに```hello```ディレクトリを作ってそこにhelloプロジェクトを収めたい場合は、

```
cd ~/workspaces
```

して、ここからsbtのコマンドを使ってhelloディレクトリを作っていくことになります（あせって```$ mkdir hello```しないこと）。

作業ディレクトリに```cd```したらプロジェクトの初期化のためのDockerコンテナ（コンテナその1）を起動します。

```
$ docker run -it --rm -v=$(pwd):/root hseeberger/scala-sbt:8u151-2.12.4-1.1.0
```

- -vはコンテナ内のディレクトリをローカルのディレクトリにマウントするためのオプションです。ここでは、Dockerコンテナ内の```/root```ディレクトリが、ローカルの作業用のディレクトリ(```~/workspaces```)にマウントされます。

※Dockerコンテナを起動したときに```/root```がコンテナ内のカレントディレクトリになるので、このような指定をしています。

Dockerコンテナが起動したら、コンテナ内で```sbt new```を実行します。```hello```はこれから作るアプリケーション名です。

```
# sbt new playframework/play-scala-seed.g8 --name=hello
```

ここまでで一段落です。
Dockerコンテナを抜けて、カレントディレクトリにプロジェクトが生成されているのを確認します。

```
$ ls hello
app		build.sbt	gradle		gradlew.bat	public
build.gradle	conf		gradlew		project		test
```

共同開発の場合は、この```hello```ディレクトリ以下を```git```などで共有することになります。

# 開発

開発の際には、まずプロジェクトのディレクトリに移動します。　

```
$ cd hello
```

開発環境（コンテナその2）を起動します。このコンテナは先ほどのプロジェクト初期化用のコンテナとは別物と考えたほうがすっきりいけます。

```
$ docker run -it --rm -p=9000:9000 -v=$(pwd):/root hseeberger/scala-sbt:8u151-2.12.4-1.1.0
```

- -p はDockerコンテナのポートをローカル環境のポートに接続するためのオプションです。

Dockerコンテナ内で```/bin/bash```が起動されるので、そのコマンドラインからsbtコンソールを起動します。

```
# sbt
```

ここでsbtコンソールの起動に時間がかかる（手元の環境で10分以上）ので、このコンテナはなるべく起動しっぱなしにしておいたほうが時間の節約になります。
sbtが起動すると、sbtのコマンドライン上で

```
$ ~run
```
などが使えるようになります。

開発中のアプリのソースにはローカルの ```~/workspaces/hello```以下の通常のファイルとしてアクセスできるので、好きなエディタやIDEを使って開発できます。

# デプロイ

上記の開発環境内のsbtのコマンドライン上で

```
$ dist
```

とすれば、ローカルの```~/workspaces/hello/target/universal```フォルダに```hello-1.0-SNAPSHOT.zip```といったファイルができます。これはDockerを使わない開発手順と何ら変わりがありません。
このzipファイルを本番サーバーに持って行って展開する、というのがPlay Frameworkの通常のデプロイ手順になります。もちろん、サーバー上にJavaの実行環境が正しくインストールされている必要があります。
しかし、サーバー上にたくさんのバージョンのJava実行環境がインストールされているのは美しくないのでDockerでいきましょう。

ここでは、本番サーバー上でDockerが稼働している前提で、```docker-compose up```できるようにしていきます。

## Docker用ファイルの準備
Play Frameworkでは、プロジェクトのディレクトリ内に```dist```というディレクトリを作ってそこにファイルを置いておくと、そのファイルがデプロイの際の配布物にそのまま取り込まれます。
ですので、まず```dist```ディレクトリを作成します。

```
$ cd ~/workspaces/hello
$ mkdir dist
```

作成した```dist```ディレクトリ内に```Dockerfile```と```docker-compose.yml```を用意します。

```Dockerfile``` は以下のようになります。

```:Dockerfile
FROM openjdk:8u151-jre
COPY . /root/
RUN chmod +x /root/bin/hello
```

- 開発環境と、Javaのバージョンを揃えるように注意してください。
- 最後の ```RUN chmod...```は、zip形式でアーカイブする際に必要になるものです。tgzなどファイル属性を保持できる形式でアーカイブするのであれば必要ありません。

docker-compose.ymlは以下のようになります。

```:docker-compose.yml
version: '3'
services:
  app:
    build: .
    command: /root/bin/hello -Dplay.http.secret.key=xxxxxxxxxxxx
    ports:
      - "9000:9000"
```

- play.http.secret.keyは適宜設定してください。

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

Docker用のファイルの準備ができたら、__開発用のコンテナ（コンテナその2）__のsbtコマンドライン上で、```$ dist```（zip形式でアーカイブする場合。tgzの場合は```$ universal:packageBin```）を実行します。
```~/workspaces/hello/target/universal```ディレクトリに```hello-1.0-SNAPSHOT.zip```（tgz形式の場合は```hello-1.0-SNAPSHOT.tgz```）ができます。

このファイルを本番用サーバーに持っていって展開すると

```
Dockerfile		conf			lib
bin			docker-compose.yml	share
```
といったファイル群が取り出せます。

このディレクトリで

```$ docker-compose up```

すればDockerコンテナ（コンテナその３）内でアプリを起動することができます。

# 結論

Play FrameworkアプリをDocker上で開発する場合はDockerコンテナを3つ用意して

- Dockerコンテナその1は、```sbt new```専用
- Dockerコンテナその2は開発専用
- Dockerコンテナその3は本番環境専用

と分けて考えるとうまくいきます。