Gitlab CI を使ってみるメモ

Jenkins からの移行のために今更だけど使ってみたメモ。

なお、うちの Gitlab はソースから入れていてデータベースも MySQL です。たまにしかバージョンアップしていないのでちょっと古いです(8.17.2)。

参考

ざっくり

  • Gitlab 8.0 からは Gitlab に統合されている
  • Runner をどこかのサーバでセットアップする必要がある
    • shell とか docker とかでビルドが実行される環境
    • Gitlab と同じサーバじゃ無い方が良い
  • リポジトリルートに .gitlab-ci.yml を追加する
    • .gitlab-ci.yml は Runner に何をさせるかを記述する
  • どの Runner を使うかはプロジェクトごとに選択できる
    • タグ付けでさらに細かく制御できる

Runner とは

ビルドを実行するための環境。公式の実装として gitlab-ci-multi-runner があるけれども自前で実装することもできます、たぶん。

起動すると Gitlab に HTTP で常時接続して Gitlab からのビルドの通知を受け取ります。なので「Runner → Gitlab」の方向に HTTP で繋がるだけで良いです。

Runner には、特定のプロジェクトに固有のものと(Specific Runner)、すべてのプロジェクトで使用可能なものがあります(Shared Runner)。

Shared Runner は fair usage queue という方法でジョブをキューイングします。一方で Specific Runner は FIFO でキューイングされます。fair usage queue は、やたらたくさんビルドを要求するプロジェクトが原因で他のプロジェクトのビルドが滞るのを避けるためのアルゴリズムのようです。

Specific Runner は複数のプロジェクトで使いまわすこともできます。Shared Runner との違いはそれぞれのプロジェクトで個別に有効にする必要があるかどうかです。Shared Runner はデフォですべてのプロジェクトで有効になります。Specific Runner はプロジェクトの設定で選択しなければ有効になりません。Specific Runner は使い回しを禁止して特定のプロジェクトにロックすることもできます。

Shared Runner を登録するとき、普通は Runner に処理可能なジョブのタグを指定します。そうしないとすべてのジョブを実行しようとしてしまうので。もちろん Specific Runner でも特定の環境でだけ実行するジョブのためにタグを指定しても良い。

Shared Runner でジョブを実行する場合、同じ Runner で実行される他のプロジェクトのコードにアクセスできるので注意が必要。また、Runner のトークンが実行するコードで取得できるので、Runner のクローンを作成して誤ったジョブをサブミットできる??(Runner に間違ったビルドを支持できるということ? もしくは Runner を上書きできる? あるいは 任意の Runner が追加できてしまうということ?)

Runner のインストール

最新版だと今使ってる Gitlab とバージョンが合わなかったのでダウングレードします(最初からバージョン指定でインストールしても良い)。

curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-ci-multi-runner/script.rpm.sh |
  sudo bash

sudo yum install gitlab-ci-multi-runner
sudo yum downgrade gitlab-ci-multi-runner-1.11.4

rpm -ql gitlab-ci-multi-runner
#=> /usr/bin/gitlab-ci-multi-runner
#=> /usr/bin/gitlab-runner
#=> /usr/share/gitlab-runner/clear-docker-cache
#=> /usr/share/gitlab-runner/post-install
#=> /usr/share/gitlab-runner/pre-remove

RPM に systemd のユニットファイルとかは含まれていませんが、インストール時に /usr/share/gitlab-runner/post-install で生成されているようなのですぐ開始できます(というかインストールした時点で開始されているっぽいですが)。

sudo systemctl start gitlab-runner.service
sudo systemctl status gitlab-runner.service
sudo systemctl enable gitlab-runner.service

Runner を Gitlab に登録

特定のプロジェクト用に Runner を登録してみる。

プロジェクトの CI/CD Pipelines を開いて Specific Runners のとこの URL とトークンをメモって、下記のコマンドで登録します。

sudo gitlab-runner register \
    --url http://gitlab.example.net/ci \
    --registration-token "$gitlab_ci_token" \
    --name ore-no-shell \
    --executor shell

Enter 連打で登録できる。Executor で shell を指定しているのでビルドはこのホスト上の gitlab-runner というユーザーを使ってそのままスクリプトが実行されます。

Docker の Runner も登録してみる。

sudo gitlab-runner register \
    --url http://gitlab.example.net/ci \
    --registration-token "$gitlab_ci_token" \
    --name ore-no-docker \
    --tag-list docker \
    --executor docker \
    --docker-image alpine:latest

タグを指定したのでこの Runner では docker というタグが付けられたジョブだけが実行されます。また、指定している Docker image は .gitlab-ci.yml で指定されなかったときのデフォルトになります。.gitlab-ci.yml で指定できるイメージのホワイトリストとかも指定できるようですね。

.gitlab-ci.yml

.gitlab-ci.yml をリポジトリルートに追加する。このファイルに Runner が何をするか記述します。

image: php:alpine

before_script:
  - uname -n
  - id
  - pwd
  - which php
  - php -v

job_shell:
  script:
    - echo "this is shell"

job_docker:
  script:
    - echo "this is docker"
  tags:
    - docker

before_script はすべてのジョブに先立って実行されます。job_shelljob_docker がジョブで任意の名前を付けることができる。ジョブの子要素には script が必須。

このファイルをリポジトリに追加してプッシュすると次のようにビルドが実行されます(見やすくするために少し編集)。

job_shell

Running with gitlab-ci-multi-runner 1.11.4 (7e2b646)
  on ore-no-shell (9b476e4f)
WARNING: image is not supported by selected executor and shell
Using Shell executor...
Running on ore.example.com...
Fetching changes...
HEAD is now at e20b44a .gitlab-ci.yml
From http://gitlab.example.net/ore/testing
   e20b44a..fc613e6  master     -> origin/master
Checking out fc613e6d as master...
Skipping Git submodules setup

$ uname -n
ore.example.com

$ id
uid=983(gitlab-runner) gid=981(gitlab-runner) groups=981(gitlab-runner)

$ pwd
/home/gitlab-runner/builds/9b476e4f/0/ore/testing

$ which php
/usr/bin/php

$ php -v
PHP 7.1.6 (cli) (built: Jun  7 2017 12:15:54) ( NTS )
Copyright (c) 1997-2017 The PHP Group
Zend Engine v3.1.0, Copyright (c) 1998-2017 Zend Technologies
    with Zend OPcache v7.1.6, Copyright (c) 1999-2017, by Zend Technologies
    with Xdebug v2.5.5, Copyright (c) 2002-2017, by Derick Rethans

$ echo "this is shell"
this is shell

Job succeeded

job_docker

Running with gitlab-ci-multi-runner 1.11.4 (7e2b646)
  on ore-no-docker (adea055c)
Using Docker executor with image php:alpine ...
Pulling docker image php:alpine ...
Running on runner-adea055c-project-42-concurrent-0 via ore.example.com...
Cloning repository...
Cloning into '/builds/ore/testing'...
Checking out fc613e6d as master...
Skipping Git submodules setup

$ uname -n
runner-adea055c-project-42-concurrent-0

$ id
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)

$ pwd
/builds/ore/testing

$ which php
/usr/local/bin/php

$ php -v
PHP 7.1.6 (cli) (built: Jun 28 2017 20:57:42) ( NTS )
Copyright (c) 1997-2017 The PHP Group
Zend Engine v3.1.0, Copyright (c) 1998-2017 Zend Technologies

$ echo "this is docker"
this is docker

Job succeeded

CI Lint

Gitlab の Pipelines のページから CI Lint に移動できます。その画面で .gitlab-ci.yml の検証ができる。

Docker cache

Docker でジョブを実行するとき、composer install で毎回パッケージがダウンロードされたのではビルドが遅くなりすぎて辛いので、ビルドの終了時に一部のディレクトリをキャッシュして次回のビルドで使うようにできます。

.gitlab-ci.yml でキャッシュするディレクトリを指定します。

image: php:alpine

phpunit:
  script:
    - php composer.phar install --prefer-dist --no-progress --ansi --dev
  tags:
    - docker
  cache:
    paths:
      - vendor/

ビルドの終了時に指定していたディレクトリがアーカイブされて保存されます。次回の実行時はそのアーカイブが展開してからビルドが開始します。

ちなみにアーカイブは自動的に作成された Docker Volume に保存されています。docker volume ls で見れるアレです。

下記のように登録時に --docker-cache-dir を指定するとホストのディレクトリが使われるので Docker Volume がぼこぼこ作成されることはなくなります。

sudo gitlab-runner unregister \
    --url http://gitlab.example.net/ci \
    --name ore-no-docker

sudo gitlab-runner register \
    --url http://gitlab.example.net/ci \
    --registration-token "$gitlab_ci_token" \
    --name ore-no-docker \
    --tag-list docker \
    --executor docker \
    --docker-image alpine:latest \
    --docker-cache-dir "/srv/gitlab-runner/cache/"

特定の処理だけ実行する Runner を作る

gitlab-runner--pre-build-script でスクリプトを指定すると、この Runner に固有のビルド前の処理が実行できます。

さらに、指定したスクリプトはビルドのジョブの script と繋げられて1つのスクリプトとして実行されるので、次のように exec とかしてやるとジョブの script が実行されずにジョブが終了します。

sudo gitlab-runner register \
    --url http://gitlab.example.net/ci \
    --registration-token "$gitlab_ci_token" \
    --name ore-no-task \
    --tag-list ore-no-task \
    --executor shell \
    --builds-dir /srv/gitlab-runner/builds \
    --pre-build-script 'exec /opt/gitlab-runner/pre-build-script.sh'

sudo mkdir -p /opt/gitlab-runner/

cat <<'EOS'| sudo tee /opt/gitlab-runner/pre-build-script.sh
#!/bin/bash
set -x
id
pwd
echo "CI_PROJECT_PATH=$CI_PROJECT_PATH"
git show -s
EOS

sudo chmod +x /opt/gitlab-runner/pre-build-script.sh

こんな感じに実行されます。.gitlab-ci.ymlscript に何を書いていてもここで止まります。

Running with gitlab-ci-multi-runner 1.10.8 (2c34bd0)
Using Shell executor...
Running on ore.example.com...
Fetching changes...
HEAD is now at e20b44a .gitlab-ci.yml
Checking out e20b44a6 as master...
Skipping Git submodules setup

$ exec /opt/gitlab-runner/pre-build-script.sh

+ id
uid=983(gitlab-runner) gid=981(gitlab-runner) groups=981(gitlab-runner)

+ pwd
/srv/gitlab-runner/builds/ce74c254/0/ore/testing

+ echo CI_PROJECT_PATH=ore/testing
CI_PROJECT_PATH=ore/testing

+ git show -s
commit e20b44a678298d9280ce0f6e90128512c078a955
Author: ore <ore@example.com>
Date:   Fri Jun 30 12:22:44 2017 +0900

    .gitlab-ci.yml

Build succeeded

複数のプロジェクトで横断的に特定のホストである決まった処理を実行するために使えそうです。

Jenkins を置き換える

今は Jenkins で下記のような感じで CI/CD してます。

  • 開発環境を Jenkins Slave としてセットアップ
    • 世間ではステージングと呼ばれるかも
  • リポジトリにプッシュされたらテストを実行
    • composer install とか
    • migration とか
    • phpunit とか
    • php-cs-fixer とか
    • phan とかもやりたい
  • master ブランチでテストが通れば開発環境の公開ディレクトリへデプロイ
    • Jenkins のワーキングディレクトリを公開ディレクトリに指定してる
    • ので Jenkins がファイルを撒くまでやってくれる(チェックアウトするだけ)
    • マイグレーションとかサービスのリスタートとかだけ後処理でやってる
  • さらに master ブランチを Redmine にチェックアウトして fetch changesets で反映
    • Redmine も Jenkins Slave として登録している
    • トピックブランチは Redmine に反映したくない

これらを Jenkins の WebUI で設定するのは流石にしんどいので Jenkins DSL で設定しているのですが・・それはそれでツラミあります。あと、現状のビルドの一部を並列化して両方終わったらデプロイを実行、とかが Jenkins DSL だとかなりつらいです。Jenkins Pipeline で多少マシになっているっぽいですが。。。

Gitlab CI ならこんな感じでできそう(phan はそれっぽい Docker Image を作った)。

before_script:
  - curl -fsSL https://getcomposer.org/download/1.4.2/composer.phar > composer.phar

phan:
  image: ngyuki/php-phan
  script:
    - php composer.phar install --prefer-dist --no-progress --ansi --no-dev
    - phan --version
    - phan -l src/ -l vendor/ -3 vendor/
  tags:
    - docker
  cache:
    paths:
      - vendor/

cs-fixer:
  image: php:alpine
  script:
    - php composer.phar install --prefer-dist --no-progress --ansi --dev
    - vendor/bin/php-cs-fixer fix --dry-run --diff --ansi -vvv -- src/
  tags:
    - docker
  cache:
    key: vendor
    paths:
      - vendor/

phpunit:
  image: php:alpine
  script:
    - php composer.phar install --prefer-dist --no-progress --ansi --dev
    - vendor/bin/phpunit
  tags:
    - docker
  cache:
    key: vendor
    paths:
      - vendor/

deploy:
  stage: deploy
  script:
    - php composer.phar install --prefer-dist --no-progress --ansi --no-dev --optimize-autoloader
    - ln -sfn -- "$PWD" /opt/myapp/current
  only:
    - master
  tags:
    - dev

redmine:
  stage: deploy
  script: |
    set -eu
    mkdir -p -- "/srv/gitlab-runner/repos/${CI_PROJECT_PATH%/*}"
    ln -sfn -- "$PWD/.git" "/srv/gitlab-runner/repos/${CI_PROJECT_PATH}"
    git branch --force -- "$CI_BUILD_REF_NAME" "$CI_BUILD_REF"
    git symbolic-ref HEAD "refs/heads/$CI_BUILD_REF_NAME"
    curl -kfs "https://redmine.example.net/sys/fetch_changesets?id=ore"
  only:
    - master
  tags:
    - redmine

最後の redmine のジョブは gitlab-runner--pre-build-script を使って Shared Runner でやると良さそう。

特定のユーザーで Runner を実行する

Runner がどのユーザーで実行されるかは gitlab-runner run の引数で決まるのだけど、これは systemd のユニットファイルで指定されている。

cat /etc/systemd/system/gitlab-runner.service  | grep ExecStart
#=> ExecStart=/usr/bin/gitlab-ci-multi-runner "run" ... "--user" "gitlab-runner"

このファイルは gitlab-runner install で生成されるので、下記のようにすれば別のユーザーで実行させることができる。

gitlab-runner uninstall
gitlab-runner install -d /tmp -u ore
systemctl restart gitlab-runner

なお gitlab-runner register で登録する Runner ごとには変更できないっぽい。