ScalaプロジェクトをCircleCI 2.0に対応させてみた話

編集履歴

- npm5系での `npm install` に対応しました

## 概要

某Scalaプロジェクトで、 CircleCI を1.0から2.0へ対応させたときにハマったことや気をつけたことをメモしました。（雑に書いているので、ご質問などあればコメントにてお願いします）

## プロジェクトの移行

- 移行前の状態
- 移行後の状態

を見た後に、試行錯誤の過程を書いていきます。

### CircleCI 1.0の頃の設定

circle.ymlを置いておくだけです。シンプルで良いですが、カスタマイズ性に欠けますね。

```yaml:circle.yml
machine:
  timezone: Asia/Tokyo
  node:
    version: 8.1.0
  java:
    version: oraclejdk8
  environment:
    PATH: "${PATH}:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin"
    GOOGLE_APPLICATION_CREDENTIALS: "${HOME}/gcloud-service-key.json"

database:
  post:
    - mysql -u root < ./setup.sql
    - cd ./server; sbt server/test:flywayMigrate

dependencies:
  cache_directories:
  - "~/.npm/_cacache"
  - "~/.ivy2"
  - "~/.sbt"

  override:
  - echo $GCLOUD_SERVICE_KEY | base64 --decode > $GOOGLE_APPLICATION_CREDENTIALS
  - cd ./front; npm install; npm run build:prod
  - cd ./server; sbt compile

test:
  override:
  - cd ./front; npm run test:ci
  - cd ./server; sbt coverage test

  post:
  - cd ./front; npm run codecov
  - cd ./server; sbt coverageReport && bash <(curl -s https://codecov.io/bash)
```

一部省略していますが、大方こんな感じです。

- frontフォルダはJSで、npm scriptを用いてcompileやtestを実施
  - testは、karmaを使ってchrome上で走らせています。
- serverフォルダはscalaで、sbtを用いてcompileやtestを実施
- mysqlを用いたテストもある。
- GCPのcredentialsやcoverageも扱う

### CircleCI 2.0のでの設定

用意するものとしては、基本的には

- カスタムイメージを作るためのDockerfile
- ビルドの手順を記した設定ファイル

のみです。
当たり前ですが、ソースコードには（ほとんど）手を入れません。

#### Dockerfile

```text:Dockerfile
FROM ubuntu:16.04

# update
RUN set -x && \
  apt-get update

# base tool install
RUN set -x && \
  apt-get install -y chromium-browser curl
ENV CHROME_BIN=/usr/bin/chromium-browser

# nodejs & npm
ENV NODEJS_VERSION=8.1.0
ENV NPM_VERSION=5.3.0
ENV PATH=/root/.nodebrew/current/bin:$PATH
RUN set -x && \
  curl -L git.io/nodebrew | perl - setup && \
  nodebrew install-binary ${NODEJS_VERSION} && \
  nodebrew use ${NODEJS_VERSION} && \
  npm install -g npm@${NPM_VERSION}

# java8
RUN set -x && \
  apt-get install -y software-properties-common && \
  add-apt-repository -y ppa:webupd8team/java && \
  apt-get update && \
  echo "oracle-java8-installer shared/accepted-oracle-license-v1-1 select true" | debconf-set-selections && \
  echo "oracle-java8-installer shared/accepted-oracle-license-v1-1 seen true" | debconf-set-selections && \
  apt-get install -y oracle-java8-installer

# sbt
ENV SBT_VERSION=0.13.15
RUN set -x && \
  curl -L -o sbt-$SBT_VERSION.deb https://dl.bintray.com/sbt/debian/sbt-$SBT_VERSION.deb && \
  dpkg -i sbt-$SBT_VERSION.deb && \
  rm sbt-$SBT_VERSION.deb && \
  apt-get update && \
  apt-get install -y sbt && \
  sbt sbtVersion

# install mysql-client & Japanese setting
RUN set -x && \
  apt-get install -y mysql-client && \
  apt-get install -y language-pack-ja && \
  locale-gen ja_JP.UTF-8

ENV LANG ja_JP.UTF-8
ENV LANGUAGE ja_JP:ja
ENV LC_ALL ja_JP.UTF-8
```

上記1.0の頃の設定を見ても、

- nodejs 8
- java 8
- mysql-client
- sbt
- chrome

が必要なことは分かっていたのでそれを入れます。
また、Scalaのコードの一部にUTF-8の日本語文字が入っていてテストで使用していたため、Languageをjaに変えました。

ベースイメージがUbuntuな理由は後ほど書きます。

（Dockerfileの作りが雑なのはご愛嬌。。。）

#### config.yml

```yaml:.circleci/config.yml
version: 2
jobs:
  build:
    working_directory: /home/circleci/my-project
    docker:
      - image: uryyyyyyy/hogehoge:0.1.0
        environment:
          GOOGLE_APPLICATION_CREDENTIALS: "/home/circleci/gcloud-service-key.json"
      - image: mysql:5.7
        environment:
          MYSQL_ALLOW_EMPTY_PASSWORD: yes
    steps:
      - checkout
      - restore_cache:
          key: my-project-3
      - run:
          command: cd ./front && npm install && npm run build:prod
      - run:
          command: mysql -h 127.0.0.1 -u root < ./setup.sql
      - run:
          command: cd ./server && sbt server/test:flywayMigrate exit
      - run:
          command: cd ./server && sbt compile exit
      - save_cache:
          paths:
            - "/root/.npm/_cacache/"
            - "/root/.ivy2"
            - "/root/.sbt"
          key: my-project-3
      - run:
          command: cd ./front && npm run test:ci
      - run:
          command: echo $GCLOUD_SERVICE_KEY | base64 -d > $GOOGLE_APPLICATION_CREDENTIALS
      - run:
          command: cat $GOOGLE_APPLICATION_CREDENTIALS
      - run:
          command: cd ./server && sbt coverage test exit
      - run:
          command: cd ./front && npm run codecov
      - run:
          command: cd ./server && sbt coverageReport exit && bash <(curl -s https://codecov.io/bash)
```

CircleCI 1.0の手順とほとんど同じです。工夫している所は後述します。

### ハマったところ

#### Alpine Linuxだとscalatestでプロセスが落ちる（未解決）

当初、UbuntuでなくAlpine Linuxで同様の構成で試していました。
nodejsを自分でビルドしたり、openJDKを入れたりと面倒はあったものの、ほとんど動くようにはできたんです。

ただ、なぜかscalatest実行中に毎回EOFExceptionで失敗してしまい、ログを見ると SIGSEGVでプロセスが落とされている様子。。。そして手元で `circleci build` しても再現する。

一応ちょっと関連するかもしれない[issue](https://github.com/scalatest/scalatest/issues/770)は見つけたものの、CircleCIのイメージの方で落とされているのか、Alpine Linuxのスレッド制限なのか、などの可能性があって解決に難航したため、最終的にはおとなしくUbuntuのイメージを使いました。

（なお、core dumpを吐いていたのでなんとなく見ていたのですが、CPU33コア（2.9GHz?）でメモリもたくさんあるマシンの上で実行されていたようでした。）

#### npm installで落ちる（解決済み）

`npm@5.3` で解決していました。

#### KarmaでChromeを動かす時に落ちる

こちらは、むしろCircleCI1.0が優秀過ぎたのですが、最初からChromeが入っていて且つheadlessでなくても動く謎仕様（裏でGUI立ち上がってる？）だったので、
CircleCI 2.0でも、Chromiumを入れてパスを通してそのままテストをしてみたのですが、「GUIがない」と言われて落ちてしまいました。
こちらは、[このあたりのissue](https://github.com/karma-runner/karma/issues/2603)を参考にHeadlessChromeを導入することで無事通るようになりました。

#### sbtが入力を受け付けた状態のままで制御が返ってこない

AlpineにしろUbuntuにしろ、なぜか `sbt test` した後に入力を受け付け続けてしまい次のステップに進んでくれなかったため、ワークアラウンドとして全sbtコマンドの最後にexitを付けています。

#### mysqlコンテナの起動に時間がかかる

こちらは、既にCircleCI 2.0を扱ったことのある方ならご存知かと思いますが、CircleCI 2.0ではミドルウェアなどを設定のために複数コンテナを立ち上げることが出来るようになっています。
そして、exposeされているportへ、ｐｒｉｍａｒｙコンテナ（一番上に記載されたimage）の同じ番号のportからport forwardingされています。

が、普通にmysqlクライアントからコマンドを叩くとなぜか繋がらない。。。色々ログを見てみると、どうもmysqlコンテナが起動して初期設定が終わり切る前にmysqlクライアントがつなぎに行っているようでした。
そのため、とりあえずsleepして待っている例もあれば、今回のように先にmysql以外の箇所のCIを実施してしまおう、というワークアラウンドを取る場合もあります。

#### Dockerhubへのイメージ配置

CircleCI2.0では、[普通に組むとpublicなイメージしか取ってこれない](https://discuss.circleci.com/t/docker-image-dockerhub-private-repo/9979/2)ようなのでdockerhubに置いておくことにしました。

会社のプロジェクトとして開発していたため、個人名義のリポジトリではなく、会社名義のorganizatinを作ってそちらにイメージをpushしておく形にしてあります。
（言うまでもないですが、publicに見えてしまうためimageに含める情報は気をつけましょう。）

## まとめ

コレだけ読むと面倒に思うかもしれませんが、おかげさまでビルド時間が半分以下に縮まりました。（今だけかもしれませんがCIマシンの性能もネットワークも速くなってる感があります。）