CircleCI 2.0 + Docker Sbt Build

Scala の Dockar Image を作る方法を考える。例として CircleCI 2.0 で Playframework 2.6 のアプリケーションのビルドを行う。

実験コード [nokamoto/proposal-circileci2.0-sbt-docker](https://github.com/nokamoto/proposal-circileci2.0-sbt-docker)

## Dockerfile
### Build
```bash
sbt universal:packageZipTarball

VERSION=$(cat VERSION | tr -d '\n')

docker build -t nokamoto/proposal-circileci2.0-sbt-docker:${VERSION} --build-arg VERSION=${VERSION} .
```

初めはコンテナ内での sbt ビルドを考えたが .ivy2 キャッシュの再利用など実行時間が問題となったので、外で作った tar をコピーする方法とした。

```scala
version := IO.readLines(file("VERSION")).head
```

```bash
docker build ... --build-arg VERSION=${VERSION} ...
```

[sbt](https://github.com/nokamoto/proposal-circileci2.0-sbt-docker/blob/cae3552a960ceca5eec39c416be6a3ae5345344f/build.sbt#L5) と [docker](https://github.com/nokamoto/proposal-circileci2.0-sbt-docker/blob/cae3552a960ceca5eec39c416be6a3ae5345344f/Dockerfile#L3) でバージョン方法を共有するため [VERSION](https://github.com/nokamoto/proposal-circileci2.0-sbt-docker/blob/master/VERSION) ファイルを作成した。
Docker へのビルド時の変数共有は [ARG](https://docs.docker.com/v1.8/engine/reference/builder/#arg) によって可能となる。

```dockerfile
COPY target/universal/${APP}-${VERSION}.tgz .
```

無駄なデータが入るとキャッシュが効かなくなる可能性が高まるので、sbt が作成した tar のみコピーする。

```
*
!./target/universal/*.tgz
```

上記のような .dockerignore により tgz ファイルのみをビルド時のコンテキストに取り込ませることができる。
今回のビルドでは tgz ファイルのみ利用するため単純な記述となるが、 git repo の構成次第では管理が煩雑となるかもしれない。少なくとも Scala での開発では target ディレクトリなど巨大なファイルが出来るので .dockerignore はあったほうが良さそうに思う。

### 実行結果

`target/universal` に変更がなければ Using cache となる。

```bash
$ VERSION=$(cat VERSION | tr -d '\n')
$ docker build -t nokamoto/proposal-circileci2.0-sbt-docker:${VERSION} --build-arg VERSION=${VERSION} .
Sending build context to Docker daemon  32.66MB
Step 1/8 : FROM openjdk:8u181-jre-slim
 ---> c32c7f04676c
Step 2/8 : ARG VERSION
 ---> Using cache
 ---> e4376ecb0a5c
Step 3/8 : ARG APP=proposal-circileci2-0-sbt-docker
 ---> Using cache
 ---> b909a0260c5d
Step 4/8 : COPY target/universal/${APP}-${VERSION}.tgz .
 ---> Using cache
 ---> 3cd6d6285625
Step 5/8 : RUN tar -zxvf ${APP}-${VERSION}.tgz && mv ${APP}-${VERSION} ${APP}
 ---> Using cache
 ---> ed10a36c35e6
Step 6/8 : RUN rm ${APP}-${VERSION}.tgz
 ---> Using cache
 ---> d203430f0b0b
Step 7/8 : ENTRYPOINT [ "proposal-circileci2-0-sbt-docker/bin/proposal-circileci2-0-sbt-docker" ]
 ---> Using cache
 ---> dca9f0c6f3cc
Step 8/8 : CMD [ "-h" ]
 ---> Using cache
 ---> fd1139d10b8b
Successfully built fd1139d10b8b
Successfully tagged nokamoto/proposal-circileci2.0-sbt-docker:0.0.0
```

## CircleCI 2.0
### Workflow
```yaml
workflows:
  version: 2
  ci:
    jobs:
      - test
      - build:
          requires:
            - test
```

`test` を実行して tar を作成してから `build` で docker build & push を行う。
今回はデプロイまでは考えていないので上記の wrorkflow で完了する。

### test
基本は [Language Guide: Scala](https://circleci.com/docs/2.0/language-scala/) の流れと変わらない。

```yaml
      - run:
          name: Install sbt-extras
          command: |
            curl -Ls https://git.io/sbt > /usr/local/bin/sbt
            chmod u+x /usr/local/bin/sbt
```

sbt のインストールを [sbt-extras](https://github.com/paulp/sbt-extras) に変更した。

```yaml
      - run:
          name: Sbt test & package
          command: sbt test universal:packageZipTarball
      - save_cache:
          key: sbt-universal-{{ .Revision }}
          paths:
            - "target/universal"
```

`test` ジョブで sbt の実行環境を整備しているのでここで tar を作成し [Caching](https://circleci.com/docs/2.0/caching/) で `sbt-universal-{{ .Revision }}` としてキャッシュする。

### build

```yaml
      - restore_cache:
          key: sbt-universal-{{ .Revision }}
      - run:
          name: Docker build & push
          command: |
            VERSION=$(cat VERSION | tr -d '\n')
            TAG=nokamoto13/proposal-circileci2.0-sbt-docker:${VERSION}
            docker login -u $DOCKER_USER -p $DOCKER_PASS
            docker build -t ${TAG} --build-arg VERSION=${VERSION} .
            docker push ${TAG}
```

`sbt-universal-{{ .Revision }}` をリストアしてから docker build & push を実行する。