GitLab CI の便利なところ ~ yml include extends, Protected 環境変数, Container registry, Deploy environment, JUnit test report ~

こんにちは。
巷では GitHub Actions がアツい今日このごろですが、GitLab にもあまり知られていない便利な機能があるんやで。ということで、最近知った GitLab CI の便利な機能をご紹介しようと思います。

今更感は否めない :thinking: 

## はじめに

以降で参照するサンプルリポジトリはこちらになります。

https://gitlab.com/yo-C-ta/gitlab-ci-sample
https://gitlab.com/yo-C-ta/gitlab-ci-ymls

Cloud版で Public リポジトリとして利用するのであれば大概の機能が使用可能ですが、Private Repository では料金タイプに応じてできることが制限されます。
セルフホスト版（CE/EE)でも同様に、料金タイプに応じて使用できる機能が変わってきます。

## gitlab-ci.yml `include` and `extends`

御存知の通り、GitLab で CI/CD を行うためには、`gitlab-ci.yml`というファイルに「いつ/どこで/なにを/どうする」のか記述し、リポジトリ内で管理する必要があります。

_:pencil: 余談：デフォルトで`gitlab-ci.yml`ですが、設定で変更できます。_

この`gitlab-ci.yml`には`include`というキーワードがあり、

1. 同じリポジトリの別ファイル
1. 別リポジトリのファイル
1. GitLabに同梱されている各種テンプレート
1. HTTP/HTTPSでアクセスできるロケーション（認証非対応）

からのファイルインクルードに対応しています。
_:link: 参考：https://gitlab.com/help/ci/yaml/README.md#include_

ユースケースとしては、チームで管理する複数のリポジトリに対して、標準で実行するべきジョブを規定したり、パイプラインの対象となる ref を個別に定義する必要をなくす、などが考えられますね。

例えば上記「別リポジトリのファイル」は以下のような記述になります。この例では include される側のリポジトリに`base.yml`と、ステージごとに4つのyamlファイルを置き、include しています。
（冒頭の3行ではyamlの anchor/alias によってinclude されるリポジトリとブランチを1か所で宣言しています。）

```yaml
.inc_prj: &inc_prj
  project: 'yo-C-ta/gitlab-ci-ymls'
  ref: master

include:
  - <<: *inc_prj
    file: '/base.yml'
  - <<: *inc_prj
    file: '/build.yml'
  - <<: *inc_prj
    file: '/test.yml'
  - <<: *inc_prj
    file: '/tagging.yml'
  - <<: *inc_prj
    file: '/deploy.yml'
```
https://gitlab.com/yo-C-ta/gitlab-ci-sample/blob/master/.gitlab-ci.yml#L5

これにより、各ジョブでは、`extends`を使用して下記のように include される側の記述を展開することが可能になります。各リポジトリに固有のパラメータがある場合には、`variable`を使用して値を渡しています。

```yaml
build:
  extends: .go_build
  variables:
    GO_BUILD_OUT: ./dst/fizzbuzz
    GO_BUILD_TARGET: main.go
```

こうすることで include する側の yaml ファイルからプロジェクトで汎用的な記述がなくなり、シンプルに記述することができます。

### 注意点１：include する側で上書き可能であること

include される側の記述は、include する側で上書きすることが可能です。上記のサンプルでは、include されるファイルで`script`を記述し、include する側では`before_script/after_script`記述のみ許容するという__ルール__のもとで運用すると、上書きされず効果的です。

1つのチームですべてのリポジトリを管理している場合にはこの方法でも運用可能だと思いますが、各リポジトリを管理しているチームが異なる中で、ベースとなる yaml ファイルを管理したい場合や、人的ミスを極力排除したい場合に、上書きが許容されては困ります。これを完璧に阻止するためには file lock の機能を用いて、include する側の gitlab-ci.yml ファイルをロックしてしまうといいでしょう。file lock をかけると lock されたファイルのマージが容易にはできなくなります。（ただし、プライベートリポジトリの場合、file lock は SILVER/PREMIUM の提供機能です:moneybag:）

一方で、各ブランチでのパイプラインは開発者の管理下にあります。build/test は開発者の管理下にあるべきだと考えていますが、deploy については別途権限を管理する必要があります。deploy の権限管理については「Environments」のところで触れます。

### 注意点２：多重 include 

include の制約についても触れておきます。階層的な include も含めて、合計で 100 include までサポートされているようです。ところが、重複した include は configuration error となります。

> Nested includes allow you to compose a set of includes. A total of 100 includes is allowed. Duplicate includes are considered a configuration error.
> A hard limit of 30 seconds was set for resolving all files.

重複 include ができないという点から、極力階層的な include をするべきではないと思います。階層的な include をしてしまうと、どこで何を include しているか管理する必要が生まれます。上記で上げた例のように、flat な include 構成のほうが見通しがよいですね。

## Protected 環境変数

2つ目のトピックは環境変数について、です。
GitLabにはリポジトリおよびグループ単位で環境変数を設定することができます。

_:link: 参考：https://gitlab.com/help/ci/variables/README.md#protected-environment-variables_

この環境変数には`State (Protected or not) / Masked (Masked or not)`の2つのパラメータがあります。

- Protected：protected branches / protected tags でしか参照できない
- Masked：パイプライン上で内容を表示できない

<img width="489" alt="protected_var.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221423/c01c6182-b8b5-5597-57d2-e23acfcc8be3.png">

では、上記の画像のように設定した環境変数を、CIの script で echo してみましょう。各変数にはそれぞれ下記の値が入っています。

```bash
NORMAL_VAR=normal variable
PROTECTED_VAR=protected variable
MASKED_VAR=masked_variable
PROTECTED_MASKED_VAR=protected_and_masked variable
```

```yaml
  before_script:
    - echo ${NORMAL_VAR}
    - echo ${PROTECTED_VAR}
    - echo ${MASKED_VAR}
    - echo ${PROTECTED_MASKED_VAR}
```
https://gitlab.com/yo-C-ta/gitlab-ci-sample/blob/master/.gitlab-ci.yml#L44

これをプロテクトされていないブランチで実行すると、Protected 変数にはアクセスできず、Masked 変数は MASKED と表示されます。

```bash
22 $ echo ${NORMAL_VAR}
23 normal variable
24 $ echo ${PROTECTED_VAR}                        # PROTECTED_VAR は参照しても空文字になる
25 $ echo ${MASKED_VAR}
26 [MASKED]                                       # MASKED_VAR は[MASKED]と表示される
27 $ echo ${PROTECTED_MASKED_VAR}                 # PROTECTED_MASKED_VAR は空文字になる
28 $ if [ -z $PROTECTED_VAR ]; then exit 1; fi    # プロテクトブランチでないとjobを実行させないガードも可能
30 Running after script...
31 $ echo "after_script is not defined."
32 after_script is not defined.
35 ERROR: Job failed: exit code 1                 # 👈 ガードされてる
```
https://gitlab.com/yo-C-ta/gitlab-ci-sample/-/jobs/369358119

一方で、プロテクトされたブランチでは Masked 変数でない限り値を参照できます。 

```bash 
22 $ echo ${NORMAL_VAR}
23 normal variable                                # 参照できてる
24 $ echo ${PROTECTED_VAR}
25 protected variable                             # 参照できてる
26 $ echo ${MASKED_VAR}
27 [MASKED]                                       # MASKEDなのでプロテクトブランチでも参照できない
28 $ echo ${PROTECTED_MASKED_VAR}
29 [MASKED]                                       # MASKEDなのでプロテクトブランチでも参照できない
30 $ if [ -z $PROTECTED_VAR ]; then exit 1; fi    # プロテクトブランチなのでjobを実行できる
31 /bin/bash: line 103: [: protected: binary operator expected
32 $ echo "Somethig to deploy"
33 Somethig to deploy
35 Running after script...
36 $ echo "after_script is not defined."
37 after_script is not defined.
41 Job succeeded                                  # 👈 実行できてる
```
https://gitlab.com/yo-C-ta/gitlab-ci-sample/-/jobs/372974427

この機能を利用すると、プロテクトされたブランチで実行されるjobをコントロールすることができます。例えば、deploy job をこの Protected 変数に依存する形で定義しておけば、プロテクトされたブランチ上でしかジョブを行えないように制限することができます。

## Container registry

GitLab にはコンテナレジストリもあります。

*:link: 参考：https://gitlab.com/help/user/packages/container_registry/index.md*

WebUIも用意されています。
https://gitlab.com/yo-C-ta/gitlab-ci-sample/container_registry

CIの中で Docker イメージをビルドし、このレジストリにプッシュすることができます。サンプルプロジェクトでは下記のようにしてイメージを追加し、レジストリに追加しました。

```yaml
  script:
    - docker login -u "${CI_REGISTRY_USER}" -p "${CI_REGISTRY_PASSWORD}" ${CI_REGISTRY}
    - docker build --pull -t "${CI_REGISTRY_IMAGE}:${CI_COMMIT_REF_NAME}" .
    - docker push "${CI_REGISTRY_IMAGE}:${CI_COMMIT_REF_NAME}"
```
https://gitlab.com/yo-C-ta/gitlab-ci-ymls/blob/master/tagging.yml

この script コマンドでは、`CI_`ではじまる変数が多数使われていますが、これらは GitLab CI 内で与えられる predefined な環境変数のため、イメージをプッシュするために自分で変数を与える必要がありません。

一方で他のリポジトリから、このリポジトリのコンテナレジストリにアクセスし、イメージをプルするときは下記のパスでアクセスできます。

```
registry.gitlab.com/yo-c-ta/gitlab-ci-sample:1.0.0
```

CI/CDでイメージのやり取りが必要であれば、とても有用な機能だと思います。

## Environments

> Environments are places where code gets deployed, such as staging or production.

GitLab にはデプロイ環境も用意されています。
この機能を使用すると、これまでのデプロイの履歴が管理され、現在デプロイされているものの特定および追跡ができるようになります。

_:link: 参考：https://gitlab.com/help/ci/environments_

GitLabではこの機能についてもWebUIを用意しており、パイプラインと同じようにボタン1つで環境の開始・停止・ロールバックができるようになっています（パブリックなリポジトリでもデブロイ環境は外部から参照できないようなので、リンクを割愛します）。

gitlab-ci.yml のジョブに下記のように追加するだけで、可能です。

```yaml
  environment:
    name: stg/$CI_COMMIT_REF_NAME
```

_:link: 参考：https://gitlab.com/help/ci/yaml/README.md#environment_

環境変数と同様に Environments にも Protected Environments を設定することができ、その環境にデプロイができる人を制限することが可能です。

*:link: 参考：https://docs.gitlab.com/ee/ci/environments/protected_environments.html*

また、kubernetes クラスタにデプロイしたいんだ！とかあると思いますが、ちゃんと用意されていました。すみませんが、これに関しては未検証なので、ここでは触れません。

_:link: 参考：https://gitlab.com/help/user/project/clusters/index.md_

## Junit テストレポート

少しこれまでと観点は異なりますが、これもとても便利な機能です。
Junit xml のテストレポート表示機能はモダンな CI Saas であれば大体サポートされていると思います。
GitLabにもこの機能があり、パイプライン画面および、マージリクエスト画面で確認することができます。

*:link: 参考：https://gitlab.com/help/ci/junit_test_reports.md*

ここでは[gotestsum](https://github.com/gotestyourself/gotestsum)を使って junit xml ファイルを出力しました。
また、同時にカバレッジの計測を行っています。

```yaml
  script:
    - go get github.com/stretchr/testify/assert
    - go get gotest.tools/gotestsum
    - gotestsum --format short-verbose --junitfile unit-tests.xml -- -v -coverprofile=cover.out ${GO_TEST_TARGET}
    - go tool cover -html=cover.out -o coverage.html
  artifacts:
    paths:
      - coverage.html
    reports:
      junit: unit-tests.xml
```
https://gitlab.com/yo-C-ta/gitlab-ci-ymls/blob/master/test.yml

試しにfailするテストを作成し、各画面で結果を表示させてみましょう。

:point_down: パイプライン画面での表示

<img width="637" alt="job-failed-testrep.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221423/18f1d0b8-abcc-43e3-5795-d97fe6be57bd.png">

:point_down: マージリクエスト画面での表示

<img width="516" alt="MR.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221423/d6fad15e-2a77-2b5f-7f9a-223f95f3e327.png">

Junit テストレポートが表示できると、下記の嬉しさがあります。

- 煩雑なジョブの実行ログを展開しなくても、テスト結果を確認できる
  - ジョブやパイプラインに詳しくないメンバへの情報共有に有意義
  - テストケース一覧の中から失敗したものを優先して表示してくれるので、コケたテストを探す手間がない
- Junit xml を表示できる他ツールへのテスト結果連携ができる
  - report.xml は artifact の形で保存されており、ファイルパスを paths で指定するとxmlファイル自体を出力可能です

## まとめ

いやー、GitLabには本当にいろんな機能がありますね。今まで全然使ってなかったんだなと思い知りました :rolling_eyes: 

サンプルプロジェクトや実際の実務で触っていく中で、やはり Closed な環境で、チームがそこそこ大きくなってくると Silver/Premium くらいには課金しないとかゆいところに手が届かない印象を受けました（そりゃそうだよね）。

ただそこはチームの特性やスキルもあるので、課金せずともある程度の制約を課すこともできると思いますし、そもそもそんな制約がいらない洗練されたチームも存在するかもしれません（制約といっても、チームを縛るレベルではなく、人的ミスを混入させないためのポジティブな制約の意味も含んでいます）。

昨今、本当に色々なSaasが登場していますね。
それらすべてを試して細部に至るまで検証することは非常にたいへんなことです。
自分たちが求めるコアコンポーネントを絞り込み、そこにどれだけ力を入れているか、見極める力が求められますね :eyes: