GitLab CI で Docker を使って Rails アプリを CI する

**2017-10-25 更新**: GitLab Runner 10.0 からインストール手順が変わったので、コマンドを修正しました。

----

最近 GitLab が GitHub を追い越す勢いで進化しています。プライベートリポジトリが無制限に使え、UI も使い勝手が良く、汎用的な CI ツールの [GitLab CI](https://about.gitlab.com/features/gitlab-ci-cd/) まで無料で使えます。おかげで BitBucket は使うことがなくなりました。

今回、Docker コンテナー上で動かす Rails プロジェクトを gitlab.com 上で CI することになったので、そのやり方をメモしておきます。


やりたいこと
----

- gitlab.com のリポジトリにソースを push したら自動で CI が走るようにしたい
- Docker 化した Rails アプリを Docker コンテナーのままテストしたい


検証環境
----

- Ubuntu 16.04.2 Xenial 64bit (on Vagrant)
- Docker Engine 17.03.1-CE
- Docker Compose 1.12.0
- Ruby on Rails 5.1.2
- GitLab Runner 10.1.0


GitLab CI とは？
----

- GitLab 8.0 から同梱されたCIツール
- オープンソース
  - GitLab と GitLab CI 環境を自分でホスティングすれば、プライベート CI サービスも構築可能
- プライベートリポジトリでも利用可能
- 各種開発言語のほか、 Docker のビルドも可能
- パイプラインの構築が可能
  - 例: 環境設定→テスト→コード静的解析→成果物の保存→デプロイ
- ビルドの並列実行も可能


GitLab CI の仕組み
----

- CI が起動すると、 Runner が動く
  -  Specific Runners (自分で用意したビルド環境) か、 Shared Runner (GitLab.com で CI する場合に利用可能 / ほかのユーザーと共有) を選べる
- Runner は `.gitlab-ci.yml` に記述したコマンド通りに各ステップを実行する
- Runner 内のテスト結果は、インターネット経由で GitLab.com に通知される


GitLab Runner のインストール
----

GitLab CI を利用するためには、GitLab CI API と会話するための GitLab Runner が必要です。 [Docker コンテナー内で動かす方法もあります](https://docs.gitlab.com/runner/install/docker.html)が、今回は直接ホストPCにインストールしました。

参考: [Install GitLab Runner using the official GitLab repositories - GitLab Documentation](https://docs.gitlab.com/runner/install/linux-repository.html#upgrading-to-gitlab-runner-10)

```sh
curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | sudo bash
sudo apt-get install gitlab-runner
```


GitLab Runner の登録 (Register)
----

あらかじめ GitLab 上で作っておいたリポジトリのトップページのヘッダーから "Settings -> Pipelines" 画面を開くと、 Runner Token が書いてあるので、この値をメモしてください。

![Runner Token](https://qiita-image-store.s3.amazonaws.com/0/8267/cb426d5d-cfb0-a848-2e3f-c454f87faa0d.png)

その後、以下のコマンドを叩いて、 GitLab Runner を登録します。

```sh
sudo gitlab-runner register -n \
  --url https://gitlab.com/ \
  --registration-token <上でメモしたトークン> \
  --executor docker \
  --description "Docker-in-Docker" \
  --docker-image "docker:latest" \
  --docker-privileged
```

成功すると、 `/etc/gitlab-runner/config.toml` が以下のような内容で作成されます。

```toml:/etc/gitlab-runner/config.toml
concurrent = 1
check_interval = 0

[[runners]]
  name = "Docker-in-Docker"
  url = "https://gitlab.com/"
  token = "<ユニークなトークン>"
  executor = "docker"
  [runners.docker]
    tls_verify = false
    image = "docker:latest"
    privileged = true
    disable_cache = false
    volumes = ["/cache"]
    shm_size = 0
  [runners.cache] 
```

また、 Pipelines 画面にも、いま登録した Runner が表示されます。


設定ファイルの作成
----

リポジトリ内のファイルは以下のように配置しています。

```
.
├── .gitlab-ci.yml
├── Dockerfile
├── Gemfile
├── Gemfile.lock
├── README.md
├── Rakefile
├── app
│   :
├── config
│   ├── database.yml
│   :
├── docker-compose.yml
│   :
```

重要なファイルの中身は以下のようになっています。

### Gemfile

```:Gemfile
source 'https://rubygems.org'
gem 'mysql2'
gem 'rails', '5.1.2'
    
group :development, :test do
  gem 'listen'
  gem 'database_cleaner'
  gem 'rspec-rails', '~> 3.5'
end
```

### Dockerfile

Rails を動かすためのイメージを構築します。

```:Dockerfile
FROM ruby:2.4.1

ENV APP_ROOT /myapp

WORKDIR $APP_ROOT
EXPOSE 3000

RUN apt-get update -qq && apt-get install -y \
      build-essential \
      libpq-dev \
      nodejs \
      mysql-client \
      sqlite3 \
      postgresql-client

ADD Gemfile $APP_ROOT
ADD Gemfile.lock $APP_ROOT
RUN bundle install
ADD . $APP_ROOT
```

### docker-compose.yml

```yaml:docker-compose.yml
version: '2' 
services:
  app:
    build: .
    environment:
      RAILS_ENV: development
      MYAPP_DATABASE_PASSWORD: FOOBAR
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/myapp
    ports:
      - "3000:3000"
    depends_on:
      - db
  db:
    image: mysql:5.7
    volumes:
      - db-data:/var/lib/mysql
    environment:
      MYSQL_DATABASE: sampleapp_dev
      MYSQL_ROOT_PASSWORD: FOOBAR
    ports:
      - "3306:3306"
volumes:
  db-data:
    driver: local
```

何をやっているか簡単に説明すると、以下のようになります。

- Rails アプリと MySQL を別々のコンテナーで立て、アプリから DB を参照できるようにしている
- DB のデータをホストPC上に永続化できるよう、データボリューム (db-data) を db コンテナーの `/var/lib/mysql` にマウントしている

### config/database.yml

```yml:config/database.yml
default: &default
  adapter: mysql2
  pool: 5
  encoding: utf8mb4
  collation: utf8mb4_unicode_ci
  username: root
  password: <%= ENV['MYAPP_DATABASE_PASSWORD'] %>
  host: db

development:
  <<: *default
  database: sampleapp_dev
  username: root
  password: <%= ENV['MYAPP_DATABASE_PASSWORD'] %>

test:
  <<: *default
  database: sampleapp_test

production:
  <<: *default
  database: sampleapp_production
```

### .gitlab-ci.yml

GitLab CI のビルドスクリプトは、 `.gitlab-ci.yml` に定義します。

なお、GitLab CI は `image` プロパティで指定した Docker イメージの中でビルドスクリプトを実行するため、今回は `docker:latest` コンテナーの中で app と db コンテナーを立ち上げる Docker-in-Docker (dind) 構成を作ります。

```yaml:.gitlab-ci.yml
image: docker:latest

variables:
  DOCKER_DRIVER: overlay

services:
  - docker:dind

before_script:
  - docker info
  - apk update
  - apk upgrade
  - apk add python python-dev py-pip build-base
  - pip install docker-compose

build:
  stage: build
  script:
    # To connect db from app, launch 'db' at first
    - docker-compose up -d --build db
    - docker-compose up -d --build app 
    - docker-compose ps
    - docker-compose run app rake db:create
    - docker-compose run app rails db:migrate RAILS_ENV=development
    - docker-compose run --rm app bundle exec rspec
```

`docker:latest` コンテナー内に Docker Compose をインストールするため、 `before_script` ブロック内でインストールコマンドを記述しています。これについては後述の「なんで docker/compose イメージを使わないの？」で補足しています。

また、`build` ブロック内で、わざわざ `docker-compose up -d --build` を冗長に書いている理由は、後述の「ビルドを冗長に書いてある理由は？」を参照してください。


CI 実行
----

ここまで用意したら、リポジトリにソースコードを push してみましょう。 GitLab がソースコードの更新を検知すると、 `.gitlab-ci.yml` の内容通りに CI を開始します。進捗は "Pipelines -> Jobs" 画面に表示されます。

![jobs](https://qiita-image-store.s3.amazonaws.com/0/8267/038ca03e-bd12-cc32-63de-f51f0a1be519.png)

無事 app コンテナー内の RSpec が実行され、ビルドが成功しました。素晴らしい :metal_tone5:


補足
----

### なんで docker/compose イメージを使わないの？

公式イメージの [docker/compose](https://hub.docker.com/r/docker/compose/) は、 [Dockerfile](https://github.com/docker/compose/blob/1.14.0/Dockerfile) 内で `ENTRYPOINT ["/code/.tox/py27/bin/docker-compose"]` と記述されているため、ビルドを実行すると `No such command: sh` というエラーを吐いてビルドが失敗します。

```:ビルドエラー

Running with gitlab-ci-multi-runner 9.3.0-rc.2 (110d530)
  on docker-auto-scale (4e4528ca)
Using Docker executor with image docker/compose:1.14.0 ...
Using docker image sha256:3ae34cc70032f8241371a7c6f33c2ea28cbf251db3ab03630f5bb77c648070fa for predefined container...
Pulling docker image docker/compose:1.14.0 ...
Using docker image docker/compose:1.14.0 ID=sha256:c75d0b12cd81547301e8d485063127df882edf1e46924e9455fda31d2920519d for build container...
Running on runner-4e4528ca-project-3115969-concurrent-0 via runner-4e4528ca-machine-1499654908-5011913b-digital-ocean-2gb...
Cloning repository...
Cloning into '/builds/1000k/sampleapp'...
Checking out 70775b6b as build-ci-cycle...
Skipping Git submodules setup

No such command: sh

Commands:
  build              Build or rebuild services
  bundle             Generate a Docker bundle from the Compose file
  :

ERROR: Job failed: exit code 1
```

Docker なら `--entrypoint` というパラメーターを付けて ENTRYPOINT を上書きできるのですが、 `.gitlab-ci.yml` ではその機能が現時点で最新の 9.3 で実装されていないため、回避できません。

なお、 2017年7月22日リリースの GitLab 9.4 では `.gitlab-ci.yml` 内でイメージの ENTRYPOINT を上書きできるようになるそうです。 (参考: [Allow overriding ENTRYPOINT from .gitlab-ci.yml (#1421) · Issues · GitLab.org / gitlab-runner · GitLab](https://gitlab.com/gitlab-org/gitlab-runner/issues/1421))

加えて、 [docker/compose の tags](https://hub.docker.com/r/docker/compose/tags/) を見ると、どのバージョンも *'This image has vulnerabilities'* と書いてあるのが不安。

仕方ないので、今回の例では docker イメージを pull した後に docker-compose を apk で追加インストールしています。

### ビルドを冗長に書いてある理由は？

```yaml:.gitlab-ci.yml
build:
  stage: build
  script:
    # 冗長じゃん？
    - docker-compose up -d --build db
    - docker-compose up -d --build app 

    # これでよくない？
    - docker-compose up -d --build
```

db コンテナーを app より先に立ち上げるために、あえて冗長に書いています。

Rails のテストを成功させるには、テスト実行前に DB の作成とマイグレーションが必要になります。しかし、 GitLab CI は script で定義されたコマンドの実行結果を待たずに先に進みます。したがって、もし先に app が db より先に立ち上がってしまうと、テスト実行時に DB に接続できず、以下のようなエラーを吐いてビルドが失敗してしまいます。

```:ビルドログ
 :
# テスト開始
$ docker-compose run --rm app bundle exec rspec

# テスト開始後に、並列で走っていた db コンテナーの作成が完了している
Starting sampleapp_db_1 ... 
Starting sampleapp_db_1 ... done

An error occurred while loading ./spec/models/user_spec.rb.
Failure/Error: ActiveRecord::Migration.maintain_test_schema!

Mysql2::Error:
  Can't connect to MySQL server on 'db' (111)
```

今回は app コンテナー作成時に `bundle install` で長らく待たされるのが明らかなので、 `docker-compose build` する順番を変えるだけで対応しています。

もっと厳格に「db に接続できるまでリトライする」という処理を実装したい場合は、[wait-for-it](https://github.com/vishnubob/wait-for-it) などのラッパースクリプトを用意するのがいいでしょう。詳しくは以下の記事が参考になります。

- [Compose の起動順番を制御 — Docker-docs-ja 1.13.RC ドキュメント](http://docs.docker.jp/compose/startup-order.html)
- [docker-composeでDBの起動完了を待ってからWebアプリを実行する - Qiita](http://qiita.com/shiena/items/47437f4f7874bf70d664)


参考
----

- [GitLab Continuous Integration (GitLab CI) - GitLab Documentation](https://docs.gitlab.com/ee/ci/)
    - GitLab 公式の CI マニュアル
    - [Using Docker Build - GitLab Documentation](https://docs.gitlab.com/ee/ci/docker/using_docker_build.html)
        - 今回紹介した方法を含め、 Docker イメージを CI する方法が複数書かれています
- [Getting started with GitLab and GitLab CI | GitLab](https://about.gitlab.com/2015/12/14/getting-started-with-gitlab-and-gitlab-ci/)
- [RailsアプリをDockerで開発するための手順 - Qiita](http://qiita.com/togana/items/30b22fc39fe6f7a188ec)
- [[docker] CMD とENTRYPOINT の違いを試してみた - Qiita](http://qiita.com/hihihiroro/items/d7ceaadc9340a4dbeb8f)
- [Docker in Docker のベタープラクティス - Qiita](http://qiita.com/sugiyasu-qr/items/85a1bedb6458d4573407)
    - dind 構成の解説がわかりやすい
    - 「dind はイケてない」というのが最近の風潮なので、本当は `/var/run/docker.sock` を使う方法の方がベター
      - すんなり GitLab CI で実現できなかったので、今回は見送りました