GitLab の .gitlab-ci.yml を読み解いてみる（共通設定編）

本記事は [GitLab Advent Calendar 2022](https://qiita.com/advent-calendar/2022/gitlab) の 24 日目の記事です。（遅れた）

# はじめに
GitLab のソースは https://gitlab.com/gitlab-org/gitlab で公開され、開発されています。もちろん、ビルドや lint、テスト、テスト環境へのデプロイなどには GitLab CI/CD が利用されています。そこで、GitLab の中の人が書いた `.gitlab-ci.yml` を読むことで、パイプラインの構成に関する知見が得られるのではないかと考えました。

本記事では、GitLab の `.gitlab-ci.yml` のうち、パイプラインやジョブの共通設定をしている部分に関して紹介します。個別のジョブの実装に関しては、読むのに少し時間がかかりそうなので、また後日まとめる予定です。

# 準備
公式のプロジェクトでは、自分に編集権限がないため [Pipeline Editor](https://docs.gitlab.com/ee/ci/pipeline_editor/) が利用できなさそうです。便利のために、プロジェクトをフォークしておきます。本記事執筆開始時点での master ブランチのコミットは [`bfe7a336fd9b44a1624f2df6fed3387043414a99`](https://gitlab.com/gitlab-org/gitlab/-/tree/bfe7a336fd9b44a1624f2df6fed3387043414a99) です。以降の情報はこのコミットの内容に基づきます。

https://gitlab.com/gitlab-org/gitlab

# プロジェクトルートの `.gitlab-ci.yml`
https://gitlab.com/gitlab-org/gitlab/-/blob/bfe7a336fd9b44a1624f2df6fed3387043414a99/.gitlab-ci.yml

プロジェクトルートの `.gitlab-ci.yml` には、ジョブは定義されておらず、[Global Keywords](https://docs.gitlab.com/ee/ci/yaml/#global-keywords) のみが定義されていました。Global Keywords とその概要は次のとおりです。キーワードの並び順は、`.gitlab-ci.yml` での出現順で記載しています。

| キーワード | 概要 |
|---|---|
| stages | パイプラインのステージ名と順番を定義する。|
| default | ジョブレベルのキーワードのデフォルト値を定義する。|
| workflow | パイプラインが起動する条件を定義する。|
| variables | パイプラインのすべてのジョブで利用できる CI/CD Variables を定義する。|
| include | 他の YAML ファイルからのパイプライン定義をインポートする。|

include が最下部にあるのが意外です。よくあるプログラムの記法を考えると最上部に持って行きたくなりそうですが、上から順に解釈されるわけではないことと、`stages` や `variables` など全体に関わる情報を先に書き、具体的な処理は後に書く、という考え方でこのような順番になっているかもしれません。

少し、歴史を辿ってみます。

2019年4月に `Refactor .gitlab-ci.yml` という[コミット](https://gitlab.com/gitlab-org/gitlab/-/blob/931d84ac0029c1336799a54b7619fa02570ec40c/.gitlab-ci.yml) があり、ここで `include` が最下部に移ったようです。

[一つ前のコミット](https://gitlab.com/gitlab-org/gitlab/-/blob/aada4607b7a442044e4b09bf4ad2fd5feb7cc53e/.gitlab-ci.yml) では、ファイル上部でソースコード品質分析ジョブの定義だけが `include` で読み込まれており、他のジョブは全てプロジェクトルートの `.gitlab-ci.yml` に大量に記載されています。2019年4月のリファクタリングの過程で、それら大量のジョブを `.gitlab/ci` ディレクトリに移行するとともに、 `include` も最下部に移動したようです。

2018年2月 に [GitLab 10.5](https://about.gitlab.com/releases/2018/02/22/gitlab-10-5-released/) で `include` 機能が追加されたことを考えると、公式で1年間も活用されていないのは意外ですね。

## stages
12ステージ定義されています。各ステージの概要は 公式ドキュメント内の [CI configuration internals](https://docs.gitlab.com/ee/development/pipelines/internals.html#stages) に記載されていました。

| ステージ | 概要 |
|---|---|
| `sync` | `gitlab-org/gitlab` の変更を `gitlab-org/gitlab-foss` (プロプライエタリコードが除去されたプロジェクト)に同期する。|
| `prepare` | 後続のジョブで必要な artifact を準備する。 |
| `build-images` | 後続のジョブで必要な Dockerイメージ を準備する。 |
| `fixtures` | フロントエンドテストで必要な fixture (バックエンドからの応答を固定化するファイル)[^1] を生成する。 |
| `lint` | lint と 静的解析 を実行する。 |
| `test` | 大多数のテストと、DB/マイグレーション のテストを実行する。 |
| `post-test` | ビルドレポートや、test ステージのジョブのデータ（カバレッジなど）を収集する。|
| `review` | CNG イメージ[^2] をビルドしてデプロイし、[Review Apps](https://docs.gitlab.com/ee/development/testing_guide/review_apps.html) に対する E2E テストを実行する。 |
| `qa` | `review` ステージでデプロイされた Review App に対して QA タスクを実行する。 |
| `post-qa` | ビルドレポートや、`qa` ステージのジョブのデータ（Review App の性能レポートなど）を収集する。  |
| `pages` | 様々なレポートを GitLab Pages としてデプロイする。 |
| `notify` | 様々な失敗をslackに通知する。|

[^1]: "fixture" という用語が初見でした。解釈が間違っているかもしれません。
[^2]: [Cloud Native GitLab container image (CNG)](https://gitlab.com/gitlab-org/build/CNG); GitLab のコンポーネント（gitaly, sidekiq など）毎のコンテナイメージ

ステージ名は定義されていますが、ジョブ間の前後関係は [`needs`](https://docs.gitlab.com/ee/ci/yaml/#needs) キーワードで指定されているので、必ずしも定義したステージの順にジョブが実行されるわけではありません。[バックエンドコードのMRパイプラインの例](https://gitlab.com/gitlab-org/gitlab/-/pipelines/433316063) を参照してみましょう。（Group jobs By のトグルを Job Dependencies に設定し、Show Dependencies を ON にすると、ジョブの依存関係を可視化することができます。）例えば、`lint` ステージの多くのジョブは依存するジョブが存在しないため、パイプラインが起動するとすぐに実行可能なジョブとしてキューイングされます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/229637/b51fdfe2-16e9-cb41-399f-d2eef1e1e3e4.png)

実際には依存関係のないジョブが `stages` の指定によって直列で呼び出す作りにしてしまうと、パイプライン全体の実行時間が延びうるだけでなく、ジョブの結果が開発者にフィードバックされるまでのリードタイムも長くなってしまいます。したがって、例のように `needs` キーワードを活用し、できるだけジョブを並列実行可能にしておくと良いですね。

## default
`default` では、ジョブのデフォルトのイメージや、ジョブで利用する GitLab Runner のタグなど、いくつかの設定についてデフォルト値を定めることができます。
`default` の内容は次の通りです。
```yaml
# always use `gitlab-org` runners, however
# in cases where jobs require Docker-in-Docker, the job
# definition must be extended with `.use-docker-in-docker`
default:
  image: $DEFAULT_CI_IMAGE
  tags:
    - gitlab-org
  # All jobs are interruptible by default
  interruptible: true
  # Default job timeout set to 90m https://gitlab.com/gitlab-com/gl-infra/infrastructure/-/issues/10520
  timeout: 90m
```

- ジョブのデフォルトのイメージは環境変数 `DEFAULT_CI_IMAGE` で指定しています。
    - Global の `variables` で定義された[環境変数](https://gitlab.com/gitlab-org/gitlab/-/blob/bfe7a336fd9b44a1624f2df6fed3387043414a99/.gitlab-ci.yml#L92)です。
      ```yml
      DEFAULT_CI_IMAGE: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-${DEBIAN_VERSION}-ruby-${RUBY_VERSION}.patched-golang-${GO_VERSION}-node-16.14-postgresql-${PG_VERSION}:rubygems-3.2-git-2.36-lfs-2.9-chrome-${CHROME_VERSION}-yarn-1.22-graphicsmagick-1.3.36
      ```
    - 様々なソフトウェアのバージョンが変数化されており、各変数の値を変更するだけで Docker イメージを差し替えられるようになっています。
- GitLab Runner の タグは `gitlab-org` を基本とし、Docker-in-Docker (DinD) するときは `.use-docker-in-docker` を継承するように、と記載があります。
    - ジョブの処理の重さに応じて、スペックが異なる Runner を準備し利用するプラクティスがありますが、ここでは特に活用されていないようです。
- `interruptible: true` 
    - ジョブが完了する前に新しいパイプラインが開始されたとき、ジョブがキャンセルされます。無駄なジョブを起動しないための設定ですね。
    - `.gitlab/ci/setup.gitlab-ci.yml` において、`interruptible:false` の `dont-interrupt-me` ジョブが定義されています。[特定の条件](https://gitlab.com/gitlab-org/gitlab/-/blob/bfe7a336fd9b44a1624f2df6fed3387043414a99/.gitlab/ci/rules.gitlab-ci.yml#L2036)で起動したパイプラインを中断させないようにしています。
- ジョブのタイムアウトを 90分 に設定しています。
    - `.gitlab/ci/review-apps/dast.gitlab-ci.yml` においては、DAST の実行のために`timeout: 3h` として[オーバーライドしています。](https://gitlab.com/gitlab-org/gitlab/-/blob/bfe7a336fd9b44a1624f2df6fed3387043414a99/.gitlab/ci/review-apps/dast.gitlab-ci.yml#L31)

## workflow
`workflow` はパイプラインの動作を制御するための仕組みです。パイプライン全体の起動条件を定めたり、条件に応じてパイプライン全体で利用する環境変数を制御する等の機能を提供します。workflow の実装内容は [こちら](https://gitlab.com/gitlab-org/gitlab/-/blob/bfe7a336fd9b44a1624f2df6fed3387043414a99/.gitlab-ci.yml#L27) を参照してください。

workflow で気になった実装を以下にピックアップします。
- `workflow: name` を使い、パイプラインに名前を付与している。
  ```yml
  workflow:
    name: '$PIPELINE_NAME'
  ```
  - GitLab 15.5 から feature flag で導入され、15.7 でデフォルトで有効になった機能です。
  - Pipeline の一覧や詳細画面において、パイプラインの名前を表示できます。以下キャプチャの `Scheduled ruby 3 pipeline` , `merged_result MR pipeline` 等です。利点としては、ちょっとした情報が追加できるくらいでしょうか･･･？
    <img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/229637/afb54781-9ee9-5173-196b-091654a4b359.png">
  - `workflow: if` の条件と合わせて、特定条件のときにパイプライン名が変わるようにしています。
    ```yml: 例
    # For merge requests running exclusively in Ruby 3.0
    - if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:run-in-ruby3/'
      variables:
        RUBY_VERSION: "3.0"細分化され
        PIPELINE_NAME: 'Ruby 3 $CI_MERGE_REQUEST_EVENT_TYPE MR pipeline'
    # For (detached) merge request pipelines.
    - if: '$CI_MERGE_REQUEST_IID'
      variables:
        PIPELINE_NAME: '$CI_MERGE_REQUEST_EVENT_TYPE MR pipeline'
    ```
- 環境変数 `FORCE_GITLAB_CI` を設定すると、workflow の他の条件に一致しなくとも、パイプラインが起動される。
  - `workflow: if` に起動条件を用意していない、通常とは異なる開発フローが生じたとき、わざわざ`.gitlab-ci.yml` を更新しなくて済むような工夫ですね。
- 障害通知チャンネルの指定
  - 特定のパイプラインにおいて、パイプラインが失敗したときに slack チャンネルへ通知するための変数を追加しています。パイプラインの用途によって、通知するチャンネルを変更しているようです。
    ```yml
    # For `$CI_DEFAULT_BRANCH` branch, create a pipeline (this includes on schedules, pushes, merges, etc.).
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
      variables:
        CREATE_INCIDENT_FOR_PIPELINE_FAILURE: "true"
        NOTIFY_PIPELINE_FAILURE_CHANNEL: "master-broken"
    ```
   - 上記の変数は `.gitlab./ci/notify.gitlab-ci.yml` 内の `notify-pipeline-failure` ジョブで[利用されています。](https://gitlab.com/pipeline-sandbox/gitlab/-/blob/master/.gitlab/ci/notify.gitlab-ci.yml#L40-63)

## variables
`variables` では、パイプライン全体で利用できる環境変数を定義することができます。variables の実装内容は [こちら](https://gitlab.com/gitlab-org/gitlab/-/blob/bfe7a336fd9b44a1624f2df6fed3387043414a99/.gitlab-ci.yml#L90-157) を参照してください。

定義されている環境変数は、概ね以下のように分類できそうです。記載している環境変数は例として抜粋しています。
- **バージョンや環境、ツールの基本的な設定**
    - `PG_VERSION` : PostgreSQL のバージョン
    - `NODE_ENV:` : `node` 実行時の環境設定
    - `GIT_DEPTH` : git clone 時の深さ（コミット数） 
- **テストに関する設定**
    - `FLAKY_RSPEC_SUITE_REPORT_PATH` : テストの結果が不安定なテスト（flaky test）に関するレポートの格納パス
    - `JUNIT_RESULT_FILE` : JUnit の結果を格納するパス
- **データの扱いに関する設定**
    - `ELASTIC_URL` : おそらく、[Elasticsearch integration](https://docs.gitlab.com/ee/integration/advanced_search/elasticsearch.html) を試す際の接続先URLと思われます。
    - `FF_USE_FASTZIP` : cache の圧縮、展開を高速化するための設定
    - `DECOMPOSED_DB` : バックエンドの DB を分解した構成にするか否か
- **レビュー用リソースに関する設定**
    - `DOCS_REVIEW_APPS_DOMAIN` : ドキュメントレビュー用のドメイン
    - `REVIEW_APPS_DOMAIN` : Review Apps 用のドメイン
    - `REVIEW_APPS_GCP_REGION` : Review Apps デプロイ先リージョン
- **その他**
    - `SIMPLECOV` : ruby のカバレッジ計測用ツール SimpleCov を動かすか否か
    - `REGISTRY_HOST` : コンテナイメージの格納先ホスト

バージョンや環境定義を記載しておくのは良いアイデアだなと思いました。ジョブのイメージを差し替える場合に、`image:` キーワードの部分を書き換えるのではなく、グローバルの環境変数で差し替えるようにしておけば、パイプラインで利用している各種ソフトウェアのバージョンが一目瞭然になります。

テストに関する設定で、結果やレポートの格納パスが環境変数になっているのも参考にできそうです。ジョブ毎に `artifact` にパスをベタ書きするのではなく、事前に格納パスを環境変数で決めておくことで、結果を出力する部分と、後続でそれを利用する部分とで不整合の発生を防ぐことができます。テストに限らず、ビルド成果物を後続ジョブに渡すケースで活用できます。


## include
次の2つの場所からパイプライン定義を include しています。
1. 同プロジェクトの、GitLab CI/CD 用ディレクトリ内の `.gitlab-ci.yml` で終わるファイル群:  `.gitlab/ci/*.gitlab-ci.yml`
2. 別プロジェクト（gitlab-org/frontend/untamper-my-lockfile）にある 1つの YAML ファイル
    - `yarn.lock` の改ざんをチェックするジョブが１つ定義されているだけであり、それ以外の全てのジョブは 1. で定義されています。

include する YAML のファイル名末尾に `.gitlab-ci.yml` をつけるのは、CI用の YAML ファイルであることが明確になるのが良いと思いました。プロジェクト内に CI 用以外の YAML ファイルが存在する場合でも、ワイルドカードを使って一括で include しやすくなりますね。

# include で導入されるジョブ
先述したとおり、include される YAMLファイルのファイル名は `<任意名>.gitlab-ci.yml` の形式となっています。
```bash
$ cd .gitlab/ci/

$ ls -1 *.gitlab-ci.yml
as-if-jh.gitlab-ci.yml
build-images.gitlab-ci.yml
caching.gitlab-ci.yml
ci-templates.gitlab-ci.yml
dev-fixtures.gitlab-ci.yml
docs.gitlab-ci.yml
frontend.gitlab-ci.yml
glfm.gitlab-ci.yml
global.gitlab-ci.yml
graphql.gitlab-ci.yml
memory.gitlab-ci.yml
notify.gitlab-ci.yml
pages.gitlab-ci.yml
qa.gitlab-ci.yml
rails.gitlab-ci.yml
releases.gitlab-ci.yml
reports.gitlab-ci.yml
review.gitlab-ci.yml
rules.gitlab-ci.yml
setup.gitlab-ci.yml
static-analysis.gitlab-ci.yml
test-metadata.gitlab-ci.yml
vendored-gems.gitlab-ci.yml
workhorse.gitlab-ci.yml
yaml.gitlab-ci.yml
```

## ファイルの構成
ファイルはステージ別に分類されているわけではなく、目的や機能別に分類されているようです。例えば、`docs.gitlab-ci.yml`には、`lint` ステージでドキュメントの lint を実行するジョブと、`review` ステージでドキュメントレビュー用のデプロイを実行するジョブが含まれています。他にも `setup.gitlab-ci.yml` には、`prepare`, `sync`, `test` ステージのジョブが含まれています。

これまでの私の経験では、ステージ別にディレクトリを切り、そのステージ内で利用するジョブの YAML ファイルを管理する構成が多かったのですが、一連の関連するジョブ（例：APIテストの準備、実行、結果回収）を把握しづらく、修正時の影響確認などがしづらいと感じていました。上記の構成のように 目的/機能別に定義を分割することで、一連のジョブが把握しやすくなりそうです。

## rules.gitlab-ci.yml
`rules.gitlab-ci.yml` では、ジョブの実行条件を制御する [`rules` キーワード](https://docs.gitlab.com/ee/ci/yaml/#rules)が細分化されて定義されています。

構成要素は大別すると以下の3つです。
- `rules:if` 部分を1行だけ定義した YAML アンカー 用 hidden ジョブ
   ```yml
   .if-default-branch-or-tag: &if-default-branch-or-tag
     if: '$CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH || $CI_COMMIT_TAG'
   ```
- `rules:change` 部分を定義した YAML アンカー用 hidden ジョブ
  ```yml
  .code-backstage-qa-patterns: &code-backstage-qa-patterns
    - "{package.json,yarn.lock}"
    - ".browserslistrc"キーワード
    - "babel.config.js"
    - "jest.config.{base,integration,unit}.js"
    - ".csscomb.json"
  # (以下省略) 
  ```
- 上の2つの YAML アンカーを組み合わせて `rules` キーワードを構成する hidden ジョブ
  ```yml
  .setup:rules:cache-gems:
  rules:
    - <<: *if-not-canonical-namespace
      when: never
    - <<: *if-default-branch-or-tag
      changes: *code-backstage-qa-patterns
    - <<: *if-dot-com-gitlab-org-merge-request
      changes: [".gitlab/ci/setup.gitlab-ci.yml"]
      when: manual
      allow_failure: true
  ```

基本の条件を YAML アンカーとして定義しておき、それらの組み合わせで様々な`rules`のパターンを作り上げる、構造化された方式で実装されています。これだけ構造化をしてもなお `rules.gitlab-ci.yml` は 2200行近くに膨らんでおり、GitLab ほど規模なプロジェクトになると、ジョブの起動条件のパターンが大量かつ複雑であることを実感しました。 

小規模なプロジェクトの場合は、ここまで細かく構造化すると却ってメンテナンスしにくくなる可能性もあるので、開発規模とジョブ起動条件の重複度を鑑みて構造化するレベルを調整すべきでしょう。

## global.gitlab-ci.yml
`global.gitlab-ci.yml` では、ジョブのデフォルト設定群が定義されています。

### リトライ設定
ジョブのリトライ設定をする `retry` のデフォルト設定が hidden ジョブとして定義されています。デフォルトのリトライ回数 `2` と、[リトライの条件](https://docs.gitlab.com/ee/ci/yaml/#retrywhen)が設定されています。`.gitlab-ci.yml` において設定可能な条件には、`always`(ジョブ失敗時は常にリトライする)を除き、想定外の事象が生じている場合が条件として用意されています。そのため、ジョブ間でほとんど差異が無い設定項目になることが多く、このように共通化するのはメリットが大きいと思われます。
```yml
.default-retry:
  retry:
    max: 2  # This is confusing but this means "3 runs at max".
    when:
      - unknown_failure
      - api_failure
      - runner_system_failure
      - job_execution_timeout
      - stuck_or_timeout_failure
```
なお、`retry` は Global の `default` キーワード配下でも設定できるのですが、リトライさせたくないジョブがある場合にわざわざ `retry: 0` を書く必要が生じるため、別途定義しているものと推測しています。

### cache に関する設定
GitLab CI/CD には、ジョブ実行を効率化するためのキャッシュ機能があり、`cache` キーワードで制御します。キャッシュの利用例は [こちら](https://docs.gitlab.com/ee/ci/caching/index.html#common-use-cases-for-caches) を参照してください。

`global.gitlab-ci.yml` では、`cache` の設定である `key` や `path`, `policy` のみが YAMLアンカー用の hidden ジョブとして定義されており、キャッシュの基本設定を部品化しているようです。

```yml
.ruby-gems-cache: &ruby-gems-cache
  key: "ruby-gems-debian-${DEBIAN_VERSION}-ruby-${RUBY_VERSION}"
  paths:
    - vendor/ruby/
  policy: pull

.ruby-gems-cache-push: &ruby-gems-cache-push
  <<: *ruby-gems-cache
  policy: push  # We want to rebuild the cache from scratch to ensure stale dependencies are cleaned up.

.gitaly-ruby-gems-cache: &gitaly-ruby-gems-cache
  key: "gitaly-ruby-gems-debian-${DEBIAN_VERSION}-ruby-${RUBY_VERSION}"
  paths:
    - vendor/gitaly-ruby/
  policy: pull
```

そして、`rules` のときと同様、YAMLアンカーを組み合わせて `cache` の設定を組み立てています。
```yml
.setup-test-env-cache:
  cache:
    - *ruby-gems-cache
    - *gitaly-ruby-gems-cache
    - *gitaly-binaries-cache
    - *go-pkg-cache

.setup-test-env-cache-push:
  cache:
    - *ruby-gems-cache-push
    - *gitaly-ruby-gems-cache-push
    - *go-pkg-cache-push
```

### services に関する設定
主にテストに利用する [service コンテナ](https://docs.gitlab.com/ee/ci/services/)に関する設定と、対応する `variables` が[hiddenジョブとして部品化されています。](https://gitlab.com/gitlab-org/gitlab/-/blob/bfe7a336fd9b44a1624f2df6fed3387043414a99/.gitlab/ci/global.gitlab-ci.yml#L226-320) ジョブ名はすべて `.use` から始まっており、起動するソフトウェアに応じた名前が付けられています。以下に例を示します。
- `.use-pg11`: PostgreSQL 11 と、redis を起動する。
  ```yml
  .use-pg11:
    services:
      - name: postgres:11.6
        command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
      - name: redis:5.0-alpine
    variables:
      POSTGRES_HOST_AUTH_METHOD: trust
      PG_VERSION: "11"
  ```
- `.use-pg11-es7-ee`: PostgreSQL 11 と、ElasticSearch 7 と、redis を起動する。
  ```yml
  .use-pg11-es7-ee:
    services:
      - name: postgres:11.6
        command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
      - name: redis:5.0-alpine
      - name: elasticsearch:7.17.6
        command: ["elasticsearch", "-E", "discovery.type=single-node", "-E", "xpack.security.enabled=false"]
    variables:
      POSTGRES_HOST_AUTH_METHOD: trust
      PG_VERSION: "11"
  ```
- `.use-pg12-opensearch2-ee` : PostgreSQL 12 と、OpenSearch 2 と、redis を起動する。
  ```yml
  .use-pg12-opensearch2-ee:
    services:
      - name: postgres:12
        command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
      - name: redis:6.0-alpine
      - name: opensearchproject/opensearch:2.2.1
        alias: elasticsearch
        command: ["bin/opensearch", "-E", "discovery.type=single-node", "-E", "plugins.security.disabled=true"]
    variables:
      POSTGRES_HOST_AUTH_METHOD: trust
      PG_VERSION: "12"
  ```

service コンテナを使いたいジョブでは、必ず `.use` から始まる hidden ジョブを継承することで、`services` 部分を定義することなく使うことができます。ここでは、指定したソフトウェアのコンテナを最小設定で起動するだけの設定となっているため、ジョブ本体の実装からは切り離して考えやすくなっています。このように部品化することにより、様々なジョブで使い回すことが可能な設計になっているようです。

### 特殊なジョブ用のデフォルト実装
コンテナビルド用に kaniko や buildX(buildkit) を使う場合のデフォルト実装も、hiddenジョブで定義されていました。
以下は kaniko を利用する際に継承する hidden ジョブです。
```yml
.use-kaniko:
  image:
    name: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images:kaniko
    entrypoint: [""]
  before_script:
    - source scripts/utils.sh
    - mkdir -p /kaniko/.docker
    - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json
```

汎用的な作りであり、`image` と 認証情報の環境変数あたりを書き換えることで、別プロジェクトでも活用ができそうですね。

### その他
3つほど、`before_script` だけを定義した hidden ジョブがありました。その中で、`.default-utils-before-script` という hidden ジョブが気になったので紹介します。

`.default-utils-before-script` では `before_script として`5行のスクリプトが実装されています。
```
.default-utils-before_script:
  before_script:
    - echo $FOSS_ONLY
    - '[ "$FOSS_ONLY" = "1" ] && rm -rf ee/ qa/spec/ee/ qa/qa/specs/features/ee/ qa/qa/ee/ qa/qa/ee.rb'
    - export GOPATH=$CI_PROJECT_DIR/.go
    - mkdir -p $GOPATH
    - source scripts/utils.sh
```

1-4 行目では FOSS版（プロプライエタリコードを抜いたバージョン）かどうかで、特定のファイルを削除する処理と、環境変数 `GOPATH` に関する処理をしています。5行目では、`script/utils.sh` の内容を `source` コマンドで読み込んでいます。この `script/utils.sh` には20個程度のシェルスクリプトの関数が定義されていました。

`script/utils.sh` 内に定義されている関数の例として、echo用の汎用的な関数を以下に示します。それぞれの関数の名前に合わせて、文字列に色を付けて標準エラー出力に出力する処理が実装されています。

```shell
function echoerr() {
  local header="${2:-no}"

  if [ "${header}" != "no" ]; then
    printf "\n\033[0;31m** %s **\n\033[0m" "${1}" >&2;
  else
    printf "\033[0;31m%s\n\033[0m" "${1}" >&2;
  fi
}

function echoinfo() {
  local header="${2:-no}"

  if [ "${header}" != "no" ]; then
    printf "\n\033[0;33m** %s **\n\033[0m" "${1}" >&2;
  else
    printf "\033[0;33m%s\n\033[0m" "${1}" >&2;
  fi
}

function echosuccess() {
  local header="${2:-no}"

  if [ "${header}" != "no" ]; then
    printf "\n\033[0;32m** %s **\n\033[0m" "${1}" >&2;
  else
    printf "\033[0;32m%s\n\033[0m" "${1}" >&2;
  fi
}
```

上記のように、ユーティリティ的な関数をスクリプトとして作成しておき `before_script` にて `source` コマンドで読み込み、ジョブ内で利用できるようにするテクニックは、いくつかのテックブログで言及されています。例えば、https://threedots.tech/post/keeping-common-scripts-in-gitlab-ci/ ではスクリプトを別のGitリポジトリで管理し、`before_script` で `git clone` をして利用できるようにする手法が紹介されています。これらを参考にすることで、スクリプトの再利用性を高め、メンテナンス性を向上させることができるでしょう。

# まとめ
パイプライン全体のグローバルの設定や、YAML アンカー、hiddenジョブを活用することで、様々な設定を効率よく組み立てていると感じました。
一方で、かなり構造化された実装となっているため、初見では読み解き難い実装になっている印象もあります。プロジェクトの規模に応じて、今回紹介した内容を取捨選択して活用することをおすすめします。

後日、個別のジョブの実装に関しても読み解いてみたいと思いますので、乞うご期待。