Wercker CLI で Docker 上に Scala 開発環境を簡単構築

Wercker はプライベートリポジトリが無料で使える CI サービスとして有名ですが、Wercker CLI を使ったローカル開発もかなり便利です。

今回は Scala 開発環境を Docker で構築するために Wercker CLI を使ってみます[^1]。

## 準備

Docker engine と Wercker CLI をインストールしてください。Wercker CLI は [Github のダウンロードページ]( https://github.com/wercker/wercker/releases ) から適切なバイナリをダウンロードしてきて `chmod +x` して `PATH` に置けば OK です。

<font color="orangered">※Wercker CLI 最新版の 1.0.882 ではこの記事のサンプルは動作しません。[Docker container exits (exit status 130) · Issue #312 · wercker/wercker]( https://github.com/wercker/wercker/issues/312 ) と同様のエラーが発生してしまいます。１つ前のバージョン 1.0.643 での動作は問題ありません。</span>

## 開発サンプル

今回は Akka HTTP による Web アプリケーションをサンプルとします。

```scala:Server.scala
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.ContentTypes
import akka.http.scaladsl.model.HttpEntity
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
import akka.stream.ActorMaterializer

object Server {

  implicit val system = ActorSystem("simple-rest-system")
  implicit val materializer = ActorMaterializer()
  implicit val executionContext = system.dispatcher

  val route: Route = {
    path("") {
      get {
        complete(HttpEntity(ContentTypes.`text/plain(UTF-8)`, "Hello!"))
      }
    }
  }

  def main(args: Array[String]): Unit = {
    Http().bindAndHandle(route, "0.0.0.0", 9000)
  }

}
```

```scala:build.sbt
scalaVersion := "2.12.2"
libraryDependencies += "com.typesafe.akka" %% "akka-http" % "10.0.7"
```

```properties:project/build.properties
sbt.version=0.13.15
```

以下のように配置します :

```shell-session
$ tree
.
├── build.sbt
├── project/
│   └── build.properties
└── Server.scala
```

## 第 1 版

プロジェクトルートに wercker.yml を作って配置します : 

```yaml:wercker.yml
dev:
   box:
      id: java:8
      ports:
       - '9000'
   steps:
    - script:
         code: |
            echo 'deb http://dl.bintray.com/sbt/debian /' > /etc/apt/sources.list.d/sbt.list
            apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 2EE0EA64E40A89B84B2DF73499E82A75642AC823
            apt-get update
            apt-get install -y sbt
    - script:
         code: |
            sbt run
```

実行します :

```shell-session
$ wercker dev --expose-ports
...
...
[info] Running Server
```

別ターミナルから試します :

```shell-session
$ curl http://localhost:9000/
Hello!
```

終了するには `wercker dev` を Ctrl-C で中止します。

一応実行できますが、いろいろ問題点があります : 

- ソースコード Server.scala を書き換えるたびにいちいち Ctrl-C で止めてから `wercker dev` して再起動する必要がある。
- 再起動するたびに `apt-get update` して遅い。
- 再起動するたびに `sbt` が依存ライブラリをダウンロードしてめちゃ遅い。

## 第 2 版

まず `apt-get update` や `sbt` の依存ライブラリのロードが遅い問題に対処しましょう。どちらも Wercker CLI がもつキャッシュ機能を利用します。

`apt-get update` の内容をキャッシュするためには、公式 step の `install-packages` を使います。今回のサンプルで注意する点としては、`sbt` をインストールするために `install-packages` の前に必要な設定を済ませておく必要がある点です。

`sbt` でダウンロードした内容をキャッシュするのは自前でやる必要があります。`wercker` が使うキャッシュ用のディレクトリが、コンテナー内から環境変数 `$WERCKER_CACHE_DIR` で参照できるのでそれを使います。キャッシュされた内容はホストのプロジェクトルート下の ./.wercker/cache ディレクトリに保存され明示的に消去されるまで無くなりません。

新しい wercker.yml は以下のようになります[^2] :

```yaml:wercker.yml
dev:
   box:
      id: java:8
      ports:
       - '9000'
   steps:
    - script:
         code: |
            [[ ! -e "$WERCKER_CACHE_DIR/ivy2-cache" ]] && mkdir "$WERCKER_CACHE_DIR/ivy2-cache"
            mkdir ~/.ivy2
            ln -s "$WERCKER_CACHE_DIR/ivy2-cache" ~/.ivy2/cache
    - script:
         code: |
            echo 'deb http://dl.bintray.com/sbt/debian /' > /etc/apt/sources.list.d/sbt.list
            apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 2EE0EA64E40A89B84B2DF73499E82A75642AC823
    - install-packages:
         packages: sbt
    - script:
         code: |
            sbt run
```

これで `wercker dev` を実行してから `[info] Running Server` と表示されるまでの時間はかなり短縮されました。とはいえ、ソースコード Server.scala を修正するたびに `wercker dev` を再起動しなくてはならないので、まだまだ快適とはいえません。次はこの点を改善してみます。

## 第 3 版

`wercker dev` はプロジェクトディレクトリのファイル変更を監視する機能を持っています。`internal/watch` がそれです。wercker.yml の最後の部分 :

```yaml:wercker.yml
    - script:
         code: |
            sbt run
```

を以下のようにします :

```yaml:wercker.yml
    - internal/watch:
         reload: true
         code: |
            sbt run
```

これでうまくいく... はずなのですが、`sbt` がプロジェクトルート内にビルド結果を書き込むせいなのか、2 回目以降の実行ではうまくいきません。そこで次のようなハックで逃げます。

```yaml:wercker.yml
    - script:
         code: |
            rm -rf target project/target
    - internal/watch:
         reload: true
         code: |
            sbt run
```

この状態で `wercer dev --expose-ports` を実行し、Server.scala を編集して変更すると : 

```shell-session
[info] Running Server
--> Reloading
[info] Loading project definition from /pipeline/source/project
[info] Set current project to source (in build file:/pipeline/source/)
[info] Compiling 1 Scala source to /pipeline/source/target/scala-2.12/classes...
[info] Running Server
```

のようになって、リロード（再コンパイル）されます。

前記のハックにより `wercker` を再起動した時に必ず全コンパイルになるのが悲しいですが、一旦起動した後のファイル変更に伴う再コンパイルでは必要ファイルのみが再コンパイルされるのでまあ良しとしましょう。

もう一つ気になるのは、コンテナー内では `sbt` が root 権限で動作するために、ビルドの成果物が root の所有のままホスト側のプロジェクトディレクトリに生成されてしまう点です。今回は、前記ハックのために起動時に毎回消去するので、終了時にも消去してしまいましょう。

最終的な wercker.yml は以下のようになります。

```yaml:wercker.yml
dev:
   box:
      id: java:8
      ports:
       - '9000'
   steps:
    - script:
         code: |
            [[ ! -e "$WERCKER_CACHE_DIR/ivy2-cache" ]] && mkdir "$WERCKER_CACHE_DIR/ivy2-cache"
            mkdir ~/.ivy2
            ln -s "$WERCKER_CACHE_DIR/ivy2-cache" ~/.ivy2/cache
    - script:
         code: |
            echo 'deb http://dl.bintray.com/sbt/debian /' > /etc/apt/sources.list.d/sbt.list
            apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 2EE0EA64E40A89B84B2DF73499E82A75642AC823
    - install-packages:
         packages: sbt
    - script:
         code: |
            rm -rf target project/target
    - internal/watch:
         reload: true
         code: |
            sbt run
    - script:
         code: |
            rm -rf target project/target
```

## 最終版

wercker.yml ファイル１個だけのシンプル版としてはこのままで十分ですが、実践的には初期化が終了したコンテナーを Docker イメージとして Docker Hub 等にアップロードしておいてそれを使ったほうが更に快適になります。

Docker イメージは通常 Dockerfile で作成しますが Wercker CLI で作るのも簡単です。以下では開発用の Docker イメージの作成をこれまでの wercker.yml に同居させたものです[^3] (以下のファイル中 yourname は各自の Docker Hub ID に変更してください) :

```yaml:wercker.yml
dev:
   box:
      id: yourname/sbt
      ports:
       - '9000'
   steps:
    - script:
         code: |
            [[ ! -e "$WERCKER_CACHE_DIR/ivy2-cache" ]] && mkdir "$WERCKER_CACHE_DIR/ivy2-cache"
            mkdir ~/.ivy2
            ln -s "$WERCKER_CACHE_DIR/ivy2-cache" ~/.ivy2/cache
    - script:
         code: |
            rm -rf target project/target
    - internal/watch:
         reload: true
         code: |
            sbt run
    - script:
         code: |
            rm -rf target project/target
build:
   box: java:8
   steps:
    - script:
         code: touch "$WERCKER_OUTPUT_DIR/dummy"
deploy:
   box: java:8
   steps:
    - script:
         code: |
            echo 'deb http://dl.bintray.com/sbt/debian /' > /etc/apt/sources.list.d/sbt.list
            apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 2EE0EA64E40A89B84B2DF73499E82A75642AC823
    - install-packages:
         packages: sbt
    - internal/docker-push:
         username: $USERNAME
         password: $PASSWORD
         repository: yourname/sbt
```

`dev:` セクションの内容も微妙に変わっているので注意してください。

イメージの Docker Hub へのアップロードは以下のようにします :

```shell-session
$ wercker build --artifacts
$ X_USERNAME='yourname' X_PASSWORD='your_long_password' wercker deploy
```

そして `dev` を実行します :

```shell-session
$ wercker dev --expose-ports
```

## 脚注

[^1]: 筆者の環境（ホスト）は Ubuntu 16.04, Docker 17.03.1-ce です。Mac では挙動が変わる可能性があります（ホスト側に root のファイルが生成されないなど）。
[^2]: ここでは簡単のため `$WERCKER_CACHE_DIR/ivy2-cache` という名前のディレクトリを使っていますが、本来は衝突回避のため `$WERCKER_CACHE_DIR/yourname/ivy2-cache` (yourname は wercker CI サービスのユーザー名) などとすべきでしょう。
[^3]: 実際にはイメージの生成は別プロジェクトにすべきでしょう。そしてせっかく Wercker を使っているのですから Wercker CI に生成させましょう。