GitLab-CI + Dockerの実践的利用方法を模索中

Gitlab-CIの環境構築と実用的なDockerを使った開発環境、CIのやり方を試してみました。

## 環境構築

### Gitlab-CIを構築

まず、Gitlab、Gitlab-ci-runnerを全てDockerで構築します。

以下のコマンドは、全てDockerホスト上で実行します。
OSXやWindows上では以下のコマンドでホストに入ったところから始めます。

```bash
docker-machine ssh default
```

まず最初にGitlabを起動します。これはGitlab公式のDockerを使った起動方法でが、一点、Dockerホストが既に22番を使っているのでSSHのpublishポートを2022番に変えています。

```bash
# 参考: http://doc.gitlab.com/omnibus/docker/
docker run --detach \
    --hostname gitlab.example.com \
    --publish 443:443 --publish 80:80 --publish 2022:22 \
    --name gitlab \
    --restart always \
    --volume /srv/gitlab/config:/etc/gitlab \
    --volume /srv/gitlab/logs:/var/log/gitlab \
    --volume /srv/gitlab/data:/var/opt/gitlab \
    gitlab/gitlab-ce:latest
```

これでDockerホストのIPアドレスを使ってGitlabにアクセスできるようになります。ホスト外からアクセスしやすいように、ホスト外の環境ではhostsファイルに定義を追加しておくと便利です。

```bash
# 手元の環境にて以下要領でhostsに追加
sudo sh -c "echo \"$(docker-machine ip) gitlab.example.com\" >> /etc/hosts"
```

これでGitlabが起動できたので、初期のrootアカウントをセットアップし、Admin AreaのRunnersの画面にて表示される"Registration token"を覚えておきます。

  * Admin Area Runners  
    http://gitlab.example.com/admin/runners


次に、Gitlab-CI Runnerの環境をセットアップします。
Runnerはジョブの実行方式(executor)がいくつかありますが、以下はDockerを使う方式のためのコンテナの起動方法です。コンテナ内でdockerコマンドを実行するため、ホストの `/var/run/docker.sock` をvolume マウントしています。

```bash
# 参考: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/install/docker.md
docker run -d --name gitlab-runner --restart always \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /srv/gitlab-runner/config:/etc/gitlab-runner \
  gitlab/gitlab-runner:latest
```

GitlabとRunnerを起動できたところで、Gitlabに `gitlab.example.com` というホスト名でアクセスできるように準備が必要です。ここでは、ホスト名の解決のためDockerのnetwork機能を使っています。

```bash
docker network create -d bridge gitlab_network
docker network connect --alias=gitlab.example.com gitlab_network gitlab
docker network connect gitlab_network gitlab-runner
```

ここまでではRunner用のデーモンが起動できただけなので、続けて実際にジョブを処理する"Runner"を登録します。

```bash
TOKEN=${GitlabのRunnersページのRegistration Token}

docker exec -it gitlab-runner gitlab-runner register \
  --non-interactive \
  --name docker-runner \
  --url http://gitlab.example.com/ci \
  --registration-token ${TOKEN} \
  --executor docker \
  --limit 3 \
  --docker-image maven \  # デフォルトのジョブ実行用Dockerイメージ
  --docker-volumes /root/.m2:/root/.m2 \ # ジョブ用のコンテナ実行時に指定するvolume
  --docker-links gitlab \ # ジョブ用のコンテナ実行時に指定するlink
  --tag-list docker

docker exec -it gitlab-runner gitlab-runner register \
  --non-interactive \
  --name docker-shell-runner \
  --url http://gitlab.example.com/ci \
  --registration-token ${TOKEN} \
  --executor shell \
  --limit 3 \
  --tag-list shell
```

ここでは、２つのRunnerを登録しています。１つ目はジョブをdockerコンテナ内で処理するRunnerで、２つ目はRunnerと同じサーバー上でジョブを処理するものです。このように、Runnerのサービス上には複数のRunnerを登録することができます。(あくまで例で実際にはshellの方は使っていません)


最後にRunnerの同時実行数を変更します。設定ファイルの `concurrent` のパラメータは初期が1になっているようなので、直接修正しておきます。

```bash
vi /srv/gitlab-runner/config/config.toml

-concurrent = 1
+concurrent = 5
```

この状態でGitlabのAdmin AreaのRunnersページでで２つのRunnerが登録されていることが確認できます。


### アプリ開発環境

Gitlab環境の準備ができたので、次はこのGitlabを利用するサーバーアプリケーションの開発方法を紹介します。
実際の中身は置いておいて、このアプリは以下のような内容とします。

  * アプリはJavaのServletでMavenでビルドする
  * アプリ内からPostgreSQLのDBにアクセスする
  * 開発時のアプリ実行は、Mavenのjetty-maven-pluginを使って `mvn jetty:run`コマンドで行う

開発時のアプリの実行はwebアプリ用とDB用の２つのコンテナを起動するのでDocker Composeを使用します。

まず、Webアプリを実行するDockerfileは以下のとおりです。

```bash:Dockerfile
# mavenを使う
FROM maven:3.3.3

# /myapp を作業場所として準備
# 実際には、volumeでマウントして使う
RUN mkdir /myapp
WORKDIR /myapp
```

次に`docker-compose.yml`ファイルの内容です。

```yaml:docker-compose.yml
version: '2'
services:
  # DB コンテナ
  db: # コンテナのホスト名
    # 参考: https://hub.docker.com/_/postgres/
    image: postgres
    # PostgreSQLはdocker run時に `/docker-entrypoint-initdb.d`フォルダ化の`*.sh`、`*.sql`ファイルを実行する
    # ここにDBの初期セットアップ内容用のファイルを配置する
    volumes:
      - ./db:/docker-entrypoint-initdb.d

  # Webアプリ コンテナ
  web:
    # カレントディレクリで docker build する
    build: .
    # エントリーポイントとなるコマンドで、アプリを起動
    command: mvn -B jetty:run
    # DB接続用の設定を環境変数で指定する
    # アプリ内で環境変数からこれらの値を取得して使う
    environment:
      # DBのホスト名は、このファイルの`services`で定義した名前が使える
      DB_URL: jdbc:postgresql://db:5432/myappdb
      DB_USER: myapp
      DB_PASS: myapppass
    # カレントディレクトリをコンテナ内のアプリディレクトリにマウントする
    volumes:
      - .:/myapp
    ports:
      - "8081:8081"
    depends_on:
      # `services`で定義したコンテナに依存していることを明記
      - db
```

ここでは、PostgreSQLのDBの初期化を `/docker-entrypoint-initdb.d`ディレクトリの機能にて行っています。今回の初期化処理は以下の`./db/schema.sql`というファイルで定義しています。

```sql:db/schema.sql
-- PostgreSQLコンテナの初期superユーザ `postgres`で実行される

-- user、databaseを作成
CREATE USER myapp PASSWORD 'myapppass';
CREATE DATABASE myappdb;
GRANT ALL PRIVILEGES ON DATABASE myappdb TO myapp;

-- 作成したDBに指定のユーザで接続
-- これ以降は、`myappdb`データベース内で`myapp` ユーザで実行される
\connect myappdb myapp

-- スキーマ作成。ownerは `myapp`ユーザになる
CREATE SCHEMA myapp;
GRANT ALL ON SCHEMA myapp TO myapp;
GRANT ALL ON ALL TABLES IN SCHEMA myapp TO myapp;

-- テーブル作成。ownerは `myapp`ユーザになる
create table myapp.info (
    id serial primary key,
    name varchar(100) not null,
    value varchar(200)
);
```

これで準備ができたので以下のコマンドでアプリを起動します。

```bash
docker-compose up -d
```

これで問題がなければ、２つのコンテナが起動してアプリにもアクセスできるようになるはずです。上記のコマンドでは、コンテナがデーモンモードで起動するので、アプリのログがみたい場合は、`docker-compose logs`を使用します。

起動後にコードの修正内容を反映する場合は、以下のコマンドでWebアプリ用コンテナを再起動できます。

```bash
# build は行わず、stopとstartが実行される
docker-compose restart web
```

なお、jettyはアプリの`src/main/webapp`フォルダを直接使っているので、この下にあるファイルは保存した時点で即座に反映されます。また、mavenのjetty-maven-pluginの設定で、`scanIntervalSeconds`の設定を行って自動リロード機能を使えば、コンパイルしなおすだけで反映させることもできます。

```bash
# webコンテナ内でコンパイルしなおす
# 手元の環境で`mvn compile`してもリロードはされません
docker-compose exec web mvn compile
```


次に、PostgreSQLについてですがDocker Composeでリビルドや再起動などを行ってもデータは継続して利用できます。PostgreSQLのDockerイメージではデータフォルダ(`/var/lib/postgresql/data`)がvolumeとなっているのでイメージとは切り離して管理されています。その上で、Docker Composeでは`docker-compose up`などでコンテナが再生成された場合でも、volumeについては同じ領域を継続して利用するようになっています。
逆にいうと、この機能があるため `/docker-entrypoint-initdb.d`の初期化スクリプトを修正した場合には、リビルドでは反映させることができません。既にデータが初期化済みである場合は`docker run`を行っても初期化処理がスキップされるようです。初期化を再度行うためには、一度 `docker-compose down`でコンテナを削除してから再度起動させる必要があります。


### Gitlab-CIでのテスト実行

Gitlab-CIでテストを実行させるには、`.gitlab-ci.yml`というファイルを作成します。Gitlabのプロジェクトに`git push`されたときに、このファイルがあると自動でジョブが実行されます。

今回のテストを行う`.gitlab-ci.yml`は以下の通りです。

```yml:.gitlab-ci.yml
test:
  tags:
    - docker
  image: maven:3.3.3
  services:
    - postgres
  variables:
    # `services` で指定したイメージ名がそのままホスト名になる
    DB_URL: jdbc:postgresql://postgres:5432/myappdb
    DB_USER: myapp
    DB_PASS: myapppass
  script:
    # psql コマンドをインストール
    - which psql || apt-get update && apt-get install -y postgresql-client
    # DBを初期化
    - psql -h postgres -U postgres < ./db/schema.sql
    # テスト実行
    - mvn --batch-mode clean verify
```

今回はDockerコンテナ上でテストを実行する方式にしたいので、`tags`でジョブを実行するRunnerを指定しています。このタグ名はRunnerを`gitlab-runner register`で登録した時の`--tag-list`に記載したものと対応しています。
ジョブの実行では`services`に列挙したDockerイメージのコンテナが事前に起動され、 `image`で指定したDockerイメージのコンテナの中でジョブが実行されます。ここで`services`で起動されるコンテナに対してvolumeの指定などのオプションを設定することができません。そのため、`psql`コマンドをインストールしてDBの初期化を行っています。


## 補足

### Runnerのセットアップ方法について

Runnerは必ずDockerコンテナとして起動する必要はありません。
通常のアプリ同様に`apt-get`や`yum`などでインストールしてデーモンサービスとして起動することでも利用できます。

### Docker networkの利用について

Dockerのnetwork機能を使っていますが、Gitlabなどが単独の別サーバーにあるような場合は不要です。

### Mavenキャッシュの再利用

Mavenでは`$HOME/.m2`がキャッシュディレクトリになり、簡単には場所を変更することができません。コンテナ上では全てのファイルが消えてしまうのでビルドのたびに全ての依存ファイルをダウンロードすることになってしまいます。そのため、`gitlab-runner register` の `--docker-volumes` オプションにてジョブ用のDockerコンテナで必ず`/root/.m2`をvolumeマウントするようにしています。