Wercker はプライベートリポジトリが無料で使える CI サービスとして有名ですが、Wercker CLI を使ったローカル開発もかなり便利です。
今回は Scala 開発環境を Docker で構築するために Wercker CLI を使ってみます1。
準備
Docker engine と Wercker CLI をインストールしてください。Wercker CLI は Github のダウンロードページ から適切なバイナリをダウンロードしてきて chmod +x
して PATH
に置けば OK です。
※Wercker CLI 最新版の 1.0.882 ではこの記事のサンプルは動作しません。Docker container exits (exit status 130) · Issue #312 · wercker/wercker と同様のエラーが発生してしまいます。1つ前のバージョン 1.0.643 での動作は問題ありません。
開発サンプル
今回は Akka HTTP による Web アプリケーションをサンプルとします。
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)
}
}
scalaVersion := "2.12.2"
libraryDependencies += "com.typesafe.akka" %% "akka-http" % "10.0.7"
sbt.version=0.13.15
以下のように配置します :
$ tree
.
├── build.sbt
├── project/
│ └── build.properties
└── Server.scala
第 1 版
プロジェクトルートに 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
実行します :
$ wercker dev --expose-ports
...
...
[info] Running Server
別ターミナルから試します :
$ 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 :
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 の最後の部分 :
- script:
code: |
sbt run
を以下のようにします :
- internal/watch:
reload: true
code: |
sbt run
これでうまくいく... はずなのですが、sbt
がプロジェクトルート内にビルド結果を書き込むせいなのか、2 回目以降の実行ではうまくいきません。そこで次のようなハックで逃げます。
- script:
code: |
rm -rf target project/target
- internal/watch:
reload: true
code: |
sbt run
この状態で wercer dev --expose-ports
を実行し、Server.scala を編集して変更すると :
[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 は以下のようになります。
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 ファイル1個だけのシンプル版としてはこのままで十分ですが、実践的には初期化が終了したコンテナーを Docker イメージとして Docker Hub 等にアップロードしておいてそれを使ったほうが更に快適になります。
Docker イメージは通常 Dockerfile で作成しますが Wercker CLI で作るのも簡単です。以下では開発用の Docker イメージの作成をこれまでの wercker.yml に同居させたものです3 (以下のファイル中 yourname は各自の Docker Hub ID に変更してください) :
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 へのアップロードは以下のようにします :
$ wercker build --artifacts
$ X_USERNAME='yourname' X_PASSWORD='your_long_password' wercker deploy
そして dev
を実行します :
$ wercker dev --expose-ports
脚注
-
筆者の環境(ホスト)は Ubuntu 16.04, Docker 17.03.1-ce です。Mac では挙動が変わる可能性があります(ホスト側に root のファイルが生成されないなど)。 ↩
-
ここでは簡単のため
$WERCKER_CACHE_DIR/ivy2-cache
という名前のディレクトリを使っていますが、本来は衝突回避のため$WERCKER_CACHE_DIR/yourname/ivy2-cache
(yourname は wercker CI サービスのユーザー名) などとすべきでしょう。 ↩ -
実際にはイメージの生成は別プロジェクトにすべきでしょう。そしてせっかく Wercker を使っているのですから Wercker CI に生成させましょう。 ↩