GitLab CI で複数環境でのビルド・テスト・デプロイを行うためのパイプライン設計

# TL;DR
クロスプラットフォーム開発や後方互換性のために，複数の環境でビルド・テスト・デプロイを実行したいことがよくあります．GitLab の CI/CD でこれを実現するためのパイプライン戦略は3つあります．それぞれ一長一短あるため，要求に応じて使い分ける必要があります．

1. __単一パイプライン・単一ステージ__：すべての環境を1つのパイプラインで処理する．ビルド，テスト，デプロイを1つのステージにまとめて処理する．
2. __単一パイプライン・複数ステージ__：すべての環境を1つのパイプラインで処理する．ビルド，テスト，デプロイを3つのステージに分けて処理する．
3. __複数パイプライン・複数ステージ__：それぞれの環境を異なるパイプラインで処理する．ビルド，テスト，デプロイを3つのステージに分けて処理する．

なお，理論上は「複数パイプライン・単一ステージ」も考えられますが，あまり使いどころがないので本稿では扱いません．

それぞれの長所短所を以下の表にまとめます．

| |単一パイプライン<br>単一ステージ|単一パイプライン<br>複数ステージ|複数パイプライン<br>複数ステージ|
|:--|:-:|:-:|:-:|
|所要時間|〇|×|△|
|ワークフローの視認性|×|〇|△|
|ワークフローの安全性|×|〇|△|
|デバッグのしやすさ|△|×|〇|
|メンテナンス性|△|〇|×|

それぞれの観点で3つの戦略を比べて，最も優れているものを〇，2番目を△，3番目を×としました．あくまでも相対的な順位ですので，△や×だからといって必ずしも使い物にならないわけではありません．また，〇だからといって必ずしも実用的というわけでもありません．


# 動作環境・予備知識
GitLab のバージョンは2018年9月11日現在の [GitLab.com](https://gitlab.com) の最新版 (11.3.0-rc4-ee) を想定します．基本的な GitLab CI の仕組みと `gitlab-ci.yml` の書き方は既知とします．より具体的には，GitLab CI の [Getting started](https://docs.gitlab.com/ee/ci/README.html#getting-started) の節は既読とします．

以下，2つのバージョンのコンパイラでそれぞれバイナリをビルドし，テストし，配布する，というワークフローを想定して説明します．

# 戦略1: 単一パイプライン・単一ステージ
```yml:gitlab-ci.yml
gcc73:
  image: gcc:7.3.0
  script:
    - make build
    - make test
    - make deploy

gcc82:
  image: gcc:8.2.0
  script:
    - make build
    - make test
    - make deploy
```

|項目|評価|理由|
|:--|:--|:--|
|所要時間|〇|環境構築が1度きりで，各ジョブが独立に処理されるので，オーバーヘッドはほぼゼロ．|
|ワークフローの視認性|×|Pipeline 画面でワークフローが可視化されない．|
|ワークフローの安全性|×|ビルド後の環境を使いまわしてテスト，デプロイするため，「CI では動いたのにクリーンな環境では動かない」タイプのバグが紛れ込みやすい．|
|デバッグのしやすさ|△|1つのジョブが担う役割が大きいので問題の切り分けが難しい．|
|メンテナンス性|△|本来 CI が担うべきワークフロー制御を自前構築することになるため，他者が理解しにくい．|

設定ファイルの記述が簡単なので，ファーストステップにはいい感じです．でも使い続けてみると，うっかりバグのあるコードをデプロイしてしまったり，CI では暗黙的になっているワークフローを他人に説明するのが大変だったりと辛い面が見えてきます．

# 戦略2: 単一パイプライン・複数ステージ
```yml:gitlab-ci.yml
build-gcc73:
  stage: build
  image: gcc:7.3.0
  script: make build
  artifacts:
    paths:
      - binaries/

build-gcc82:
  stage: build
  image: gcc:8.2.0
  script: make build
  artifacts:
    paths:
      - binaries/

test-gcc73:
  stage: test
  image: gcc:7.3.0
  script: make test
  dependencies:
    - build-gcc73

test-gcc82:
  stage: test
  image: gcc:8.2.0
  script: make test
  dependencies:
    - build-gcc82

deploy-gcc73:
  stage: deploy
  image: gcc:7.3.0
  script: make deploy
  dependencies:
    - build-gcc73

deploy-gcc82:
  stage: deploy
  image: gcc:8.2.0
  script: make deploy
  dependencies:
    - build-gcc82
```
|項目|評価|理由|
|:--|:--|:--|
|所要時間|×|依存関係にないジョブの完了を待機するため，オーバーヘッドが大きい．|
|ワークフローの視認性|〇|Pipeline 画面ですべてのワークフローが一覧できる．|
|ワークフローの安全性|〇|ビルド，テスト，デプロイをそれぞれクリーンな環境で行うため，意図しない依存関係が紛れ込みにくい．問題があるかぎり何もデプロイしない．|
|デバッグのしやすさ|×|どれか1つでもジョブが失敗するとパイプライン全体が停止するため，問題点が一度に洗い出せない．|
|メンテナンス性|〇|GitLab CI のドキュメントでも紹介されているデファクトスタンダードな方法であるため，他人にも理解しやすい．|

おそらくこれが標準的な方法だと思います．ワークフローが GitLab の UI から見えるし，設定ファイルの書き方も自然です．しかしとにかくデバッグが大変．1つのジョブが失敗すると本来は依存関係にない後段のジョブまですべて止まってしまうので，問題が1つずつしか見つかりません．潜在的に複数の問題があり，それらを適切な順番で修正しないといけないケースもあります．そのようなとき，このパイプラインで見つかった問題を順次修正していく方法では堂々巡りに陥ることがあります．さらに，各ステージですべてのジョブの完了を待つせいで，ターンアラウンドタイムが長くなり修正が捗りません．

# 戦略3: 複数パイプライン・複数ステージ
そもそも複数パイプラインとは何かというと，あるパイプラインの実行中に別のパイプラインを起動する機能です．GitLab EE の PREMIUM 版，もしくは GitLab.com の SILVER プラン以上でのみ利用できる機能なので，あまり知られていないのも無理はありません．GitLab 上では，複数パイプラインを使ったパイプライングラフは下記のような感じに表示されます．Upstream / Downstreamという見慣れないステージが見えますが，これらが，このパイプラインの上流 / 下流にあたるパイプラインです．
![複数パイプラインのグラフ](https://docs.gitlab.com/ee/ci/img/multi_project_pipeline_graph.png)

この機能を使うと，複数ステージからなるパイプラインを並列に実行することができます．

```yml:gitlab-ci.yml
entry-point:
  except:
    - pipelines
  image: erikvdbergh/alpine-curl:latest
  script:
    - curl --request POST --form "variables[CXX]=gcc73" --form "token=$CI_JOB_TOKEN" --form "ref=$CI_COMMIT_REF_NAME" https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/trigger/pipeline
    - curl --request POST --form "variables[CXX]=gcc82" --form "token=$CI_JOB_TOKEN" --form "ref=$CI_COMMIT_REF_NAME" https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/trigger/pipeline

build-gcc73:
  only:
    variables:
      - $CXX == "gcc73"
  stage: build
  image: gcc:7.3.0
  script: make build
  artifacts:
    paths:
      - binaries/

build-gcc82:
  only:
    variables:
      - $CXX == "gcc82"
  stage: build
  image: gcc:8.2.0
  script: make build
  artifacts:
    paths:
      - binaries/

test-gcc73:
  only:
    variables:
      - $CXX == "gcc73"
  stage: test
  image: gcc:7.3.0
  script: make test
  dependencies:
    - build-gcc73

test-gcc82:
  only:
    variables:
      - $CXX == "gcc82"
  stage: test
  image: gcc:8.2.0
  script: make test
  dependencies:
    - build-gcc82

deploy-gcc73:
  only:
    variables:
      - $CXX == "gcc73"
  stage: deploy
  image: gcc:7.3.0
  script: make deploy
  dependencies:
    - build-gcc73

deploy-gcc82:
  only:
    variables:
      - $CXX == "gcc82"
  stage: deploy
  image: gcc:8.2.0
  script: make deploy
  dependencies:
    - build-gcc82
```

|項目|評価|理由|
|:--|:--|:--|
|所要時間|△|余分な同期をしないため，ほぼ単一パイプライン・単一ステージと同じ．環境を作り直すぶんだけ若干遅い．|
|ワークフローの視認性|△|Pipeline 画面でワークフローが確認できるが，パイプラインごとに別ページに別れてしまうため一覧性は若干劣る．しかし，ジョブの依存関係がパイプラインとして明示されるという点では優れる．|
|ワークフローの安全性|△|ビルド，テスト，デプロイをそれぞれクリーンな環境で行うため，意図しない依存関係が紛れ込みにくい．一部のジョブが失敗しても，それらに依存しないデプロイは実行される．|
|デバッグのしやすさ|〇|一部のジョブが失敗しても，それらに依存しないジョブは継続するため，問題点を一度に洗い出せる．|
|メンテナンス性|×|高度な機能を駆使するため，設定ファイルの可読性が低い．α版の機能を使っているため，将来の動作は保証されない．|

表では△が多くなっていますが，いずれも〇と比べてほぼ遜色ないです．つまり，この方法は高速で安全，視認性が高く，デバッグも容易と機能面ではいいことづくめです．その反面，設定ファイルがトリッキーになり，メンテナンス性が低くなります．果たしてどれくらいの人が以下の説明なしにこの CI の挙動を読み取れるでしょうか？このメンテが障害にならないほど GitLab に習熟したチームにならオススメですが，そうでなければやめておいた方が無難です．実際， [GitLab.com のプロジェクト一覧](https://gitlab.com/explore)を眺めても，この戦略を採用しているプロジェクトはほとんどありません．

__この CI の挙動__: はじめにこの CI が起動されると，ジョブ `entry-point` のみからなるパイプラインが作成されます．他のジョブは `only: variables` 条件を満たさないため（この時点では変数 `$CXX` は空です），パイプラインから除外されます．ジョブ `entry-point` は [GitLab Trigger API](https://docs.gitlab.com/ee/ci/triggers/README.html) を使って2つのパイプラインを起動します．それぞれ，変数 `$CXX` の値を `gcc73` と `gcc82` に設定しています．それぞれのパイプラインでは，今度はジョブ `entry-point` は `except: pipelines` 条件により除外されます．残る `only: variables` 条件を満たすジョブのみがそれぞれのパイプラインにおいて逐次実行されます．
なお， `only: variables` は[α版の機能なので，予告なく仕様変更されることがあります](https://docs.gitlab.com/ee/ci/yaml/README.html#only-and-except-complex)．

# 複数パイプライン機能の動向
機能的には便利な複数パイプライン・複数ステージをもっと簡単に書けるようにしたいという要望はたびたび Issue に挙がっており，2年前から議論されていますが，正式機能として取り込まれるにはまだ時間がかかりそうです．まだしばらくはプロジェクトの状況を考えてその都度判断することになりそうです．

- [Parallel pipelines for different environments of a project.](https://gitlab.com/gitlab-org/gitlab-ce/issues/19471)
- [Make it possible to define multiple pipelines](https://gitlab.com/gitlab-org/gitlab-ce/issues/22972)
- [parallel independent pipelines](https://gitlab.com/gitlab-org/gitlab-ee/issues/1689)
- [Allow Arbitrary CI Stage Dependencies (DAG Graphs)](https://gitlab.com/gitlab-org/gitlab-ce/issues/29347)