Hands-on DevOps · 第1回

Hands-on DevOps #1 — GitLab CI/CD コンポーネントとカタログ

この記事の原文は Zenn で公開しています。


TL;DR

  • 作るtemplates/ にコンポーネントを置き、spec:inputs で入力を型・デフォルト値・options・正規表現まで宣言する。不正な値はパイプラインが作られる前に拒否される。
  • 公開・利用 — セマンティックバージョンのタグを push すると release ジョブが CI/CD Catalog に公開し、別のプロジェクトは include: component@バージョン で取り込んで使う。@1@~latest のようなバージョン範囲で破壊的変更まで制御できる。
  • 検証方法 — 本記事のすべての出力は、実際の gitlab.com プロジェクト SEON.N/gitlab-ci-components-catalog(public)でパイプライン・リリース・CI Lint API を直接実行してキャプチャした。

概要

CI/CD パイプラインを最初に組むとき、多くの人は別プロジェクトの .gitlab-ci.yml をコピーして始める。とりあえず動くが、プロジェクトが増えるほど同じ設定があちこちに複製され、三つの問題が繰り返される。

  • 発見(discoverability) — 同じビルド・テスト・デプロイのジョブを誰かが既に作っているか知る手段がない。だからチームごとに似たパイプラインを毎回ゼロから書く。
  • 再利用(reusability)include で別ファイルを取り込むことはできる。しかしバージョンも入力値の検証もない。元ファイルが変わると、それを参照していたパイプラインが予告なく壊れる。
  • 共有(contribution) — よくできたパイプラインの部品を組織全体に安全に配布し、「ここにあるから使って」と知らせる標準的な経路がない。

CI/CD コンポーネントとカタログは、まさにこの三つを解くために登場した。一言で言えば、パイプライン設定を「コピペするコード片」から「バージョンの付いたパッケージ」へ変えたものだ。

ライブラリを使うときソースを丸ごとコピーせず npm install や Go モジュールでバージョンを指定して取り込むように、パイプラインの部品もバージョンを指定し、依存関係のように取り込んで使えるようにしたのがコンポーネントだ。そして、それらのコンポーネントを一か所で検索・発見できるよう集めたのが CI/CD Catalog だ。

CI/CD コンポーネント(component) とは、GitLab が定義する「再利用可能な単一のパイプライン構成単位」だ。include で取り込んで使う点は従来と同じだが、決定的に違う点が二つある。

  1. 型のある入力(**spec:inputs** — コンポーネントは string/number/boolean/array 型、デフォルト値、options(enum)、regex(正規表現)による検証を宣言できる。不正な値はパイプラインが作られる前に拒否される。
  2. セマンティックバージョニング + カタログ — コンポーネントはセマンティックバージョンでリリースされ、CI/CD Catalog で検索・発見される。

従来の include と比べると次のとおり。

方式取り込むもの型入力バージョン/カタログ
include:local同じ repo のファイルなしなし
include:project別プロジェクトのファイル(ref)なしなし
include:remoteURL のファイルなしなし
include:templateGitLab 提供テンプレートなしなし
**include:component**コンポーネント(spec + job)ありあり(Catalog)

表を一言でまとめるとこうだ。include:localinclude:projectinclude:remote が「ファイルをそのままコピーしてくる」のに対し、include:component は「バージョンと入力契約(spec)が明示された依存関係を取り込む」ものだ。取り込む対象が単なるファイルではなく、入力仕様とバージョンが保証された部品である点が決定的な違いだ。

CI/CD コンポーネントと CI/CD Catalog は GitLab 17.0(2024-05-16)で GA になった。それ以前は experimental/beta 段階だった。本記事では GitLab.com(常に最新バージョン)を使う。

どこで使うか

コンポーネントが真価を発揮するのは「同じことを複数のプロジェクトで繰り返すとき」だ。実務でよく出てくるユースケースは次のとおり。

こんな場面でコンポーネントでこう解く
セキュリティスキャンやリントを全リポジトリに同じように適用したいスキャンコンポーネントを一度作ってカタログに載せれば、各プロジェクトは include 数行で同じ検査を受ける
デプロイ手順(Cloud Run、Kubernetes など)を標準化したい環境名・イメージタグを inputs で受けるようにして、同じコンポーネントをチームごとに値だけ変えて再利用する
プラットフォームチームが全社標準パイプラインを強制したいコンポーネントをグループ単位でまとめてカタログとして提供すれば、標準の強制(ガバナンス)と重複排除(DRY)を同時に得られる
ライブラリのアップグレードのように破壊的変更を抑えたい利用側は @1 のような部分バージョンで非破壊的な更新だけ自動で受けるか、特定バージョンに固定する

GitLab が GA を発表した際に挙げた代表例も Google Cloud Run デプロイコンポーネントであり、セキュリティスキャン・ビルド・リント・デプロイのように「チームごとに同じことを繰り返していたジョブ」を組織共通の部品に変えるのが、最もよくある導入動機だ。

バージョン履歴と発展の方向

コンポーネントとカタログは 17.0 で GA になった後も止まっていない。GitLab は四半期ごとのリリースでこの領域に機能を着実に積み上げている。

バージョン追加・変更されたこと
Beta(2023-12)CI/CD Catalog ベータ公開
17.0(2024-05)コンポーネント + カタログ正式 GA
18.0release ジョブの標準イメージが release-cliglab に移行。release-cli は deprecated となり、削除は 20.0 予定(それまでは glab が無ければ自動フォールバック)
18.5プロジェクトあたりのコンポーネント上限が 30 → 100
18.6–18.7コンポーネントコンテキスト式 — コンポーネントが自身の名前・バージョンなどのメタデータにアクセス
18.9カタログリソースの使用状況(usage)分析を導入
19.0(2026-05)メンテナ向けの詳細なコンポーネント使用状況 — どのプロジェクトがどのバージョンを使っているか追跡

流れを見れば方向は明確だ。中核となる構文(spec:inputs、セマンティックバージョニング、release、カタログ)は GA 以降そのまま安定して維持され、その上に運用の利便機能(使用状況分析、使用状況、コンテキスト式)と上限の引き上げが継続的に加えられている。

つまり一度覚えれば長く使える安定した基盤でありながら、同時に GitLab が四半期ごとに積極的に育て続けている機能でもある。本記事のハンズオンは gitlab.com(常に最新)で実行したので、キャプチャした挙動は既に最新バージョン基準だ。

利用できる範囲(プラン・提供形態)も広い。コンポーネント・カタログ・spec:inputs は以下の範囲ですべて動作する。

  • プラン(Tier): Free・Premium・Ultimate — 全プラン
  • 提供形態(Offering): GitLab.com(SaaS)・GitLab Self-Managed・GitLab Dedicated — 全提供形態

無料プランやセルフホストでも、コンポーネントを作り・公開し・取り込んで使う中核の動作はそのまま使える。ただし二点だけ覚えておけばよい。第一に、19.0 の「詳細なコンポーネント使用状況」は Premium 以上限定だ。第二に、セルフホストインスタンスのカタログは公開済みコンポーネントが 0 個の状態で始まるため、自分で公開するか GitLab.com からミラーして埋める必要があり、インスタンスのバージョンが 17.0 以上である必要がある。

アーキテクチャ / 構成図

ecosystem

コンポーネントプロジェクト(templates/*.yml + .gitlab-ci.yml + README/LICENSE)が release を通じてカタログにバージョンを公開し、任意の利用側プロジェクトが include: component@バージョン で取り込んで使う。

動作の流れ

lifecycle

(1) コンポーネントを書いて main に push →(2)self-test パイプラインがコンポーネントを実際に実行 →(3)セマンティックバージョンのタグ →(4)release ジョブが公開 →(5)カタログにバージョン登録 →(6)利用側が include。タグのパイプラインでも self-test が先に走るため、壊れたコンポーネントは公開されない。

include の解決と入力検証

resolution

include: component@バージョン はパイプラインの 生成時点 に解決される: バージョン解決(tag/SHA/partial/~latest)→ テンプレート fetch → 入力検証(type/options/regex)→ $[[ inputs.x ]] 補間 → ジョブのマージ。検証段階で弾かれると、runner が起動する前にパイプラインが失敗する。

前提条件

このハンズオンはローカルの glabgitcurlpython3 だけを使う。glab は既にインストール・認証済みのものを使い、無ければ下表のとおり用意する(コンテナで使うなら registry.gitlab.com/gitlab-org/cli イメージを docker run)。

ツールバージョンmacOSWindowsLinux
GitLab17.0+—(SaaS 利用)
glab1.40+brew install glabwinget install glab.glabパッケージマネージャ、または docker run --rm -it registry.gitlab.com/gitlab-org/cli:latest
git2.30+標準搭載標準/winget install Git.Git標準/ディストリのパッケージ
curl7.x+標準搭載標準搭載標準搭載
トークンapi + write_repository scope の PAT、または glab auth login同じ同じ
Runnerプロジェクトに shared/group runner を有効化(パイプライン実行用)同じ同じ

glab/git/curl は OS に関係なく同じコマンドを使う。以下のハンズオンのコマンドは三者共通で、差が出る箇所だけ別途記す。

核心となる概念

1) コンポーネントのディレクトリ構造

コンポーネントプロジェクトは最上位の templates/ ディレクトリにコンポーネントを置く。

├── templates/
│   ├── greeting.yml          # 単一ファイルのコンポーネント
│   └── my-other/             # ディレクトリ型のコンポーネント
│       └── template.yml      # このファイルだけが配布される
├── LICENSE.md
├── README.md
└── .gitlab-ci.yml

単一ファイルは templates/<name>.yml、複雑なコンポーネントは templates/<name>/template.yml になる。ディレクトリ型では template.yml だけが配布され、残りのファイル(ビルド/テスト用)は配布されない。

2) spec:inputs — 型のある入力

コンポーネントファイルは二つの YAML ドキュメントに分かれる。--- の上が spec(入力宣言)、下が実際のジョブ定義だ。

spec:
  inputs:
    stage:
      type: string      # string(既定)/ number / boolean / array
      default: test      # default があれば任意、無ければ必須
    style:
      options: [plain, banner]   # 許可値のホワイトリスト(enum)
    version:
      regex: ^v?\d+\.\d+\.\d+$    # 正規表現による検証(RE2)
---
# ジョブ定義で $[[ inputs.NAME ]] として補間する

核心は 「なぜ」 だ。入力検証はパイプラインの 生成時点(設定を fetch するとき)に行われる。つまり不正な入力は runner が起動する前に即座に拒否され、コストと時間を節約する。一つのパイプラインは最大 20 個の input を受け取れる。

3) 補間 $[[ inputs.x ]]

CI 変数 $VAR とは異なり、入力の補間は $[[ inputs.name ]] 構文を使う。ジョブの 名前 にも、スクリプト にも、配列要素 $[[ inputs.arr[0] ]] にも使える。補間は設定 fetch 時点で一度だけ評価され、パイプライン全体で固定される。

4) バージョン参照

コンポーネントは次の優先順位で参照する。

  1. コミット SHA@e3262fdd...
  2. タグ@v1.0.0(カタログ公開にはセマンティックバージョンのタグが必要)
  3. ブランチ@main
  4. 部分バージョン / latest@1.2@1@~latest

~latest はリリース済みの最新バージョン(プレリリースを除く)を指すため、破壊的変更が自動的に入り込み得る。運用では固定バージョンか @1 のような部分バージョンを推奨する。

5) include パスと release

include:
  - component: $CI_SERVER_FQDN/<project-path>/<component-name>@<version>

$CI_SERVER_FQDN は GitLab ホストの FQDN を指す定義済み変数だ。インスタンスが違っても同じ設定が動く。そして、カタログにバージョンを表示させるには必ず **release** キーワード でリリースを作る必要がある(Releases API ではない)。

ハンズオン手順

ステップ 1 — プロジェクト作成と clone

このステップは、コンポーネントを入れる空のプロジェクト(器)を作るステップだ。カタログに公開するにはプロジェクトに description・README・templates/ のコンポーネントが必要なので、まずその枠組みから用意する。まだコンポーネントは無く、空のリポジトリを受け取って作業ディレクトリだけ整える。

説明を明確にするため API で description まで入れて作成した(カタログは description が必須)。glab repo create でも可能だ。

# トークンは glab 設定から取得するか、PAT を直接 export する
GITLAB_TOKEN=$(glab config get token --host gitlab.com)
REPO_NAME="gitlab-ci-components-catalog"
DESC="Reusable GitLab CI/CD components (greeting, semver-guard) published to the CI/CD Catalog. Hands-on samples."

# API で作成(または: glab repo create "SEON.N/${REPO_NAME}" --public --description "${DESC}")
curl -s --request POST --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
  --header "Content-Type: application/json" \
  --data "{\"name\":\"${REPO_NAME}\",\"path\":\"${REPO_NAME}\",\"namespace_id\":<your-namespace-id>,\"visibility\":\"public\",\"description\":\"${DESC}\"}" \
  "https://gitlab.com/api/v4/projects"

git clone "https://oauth2:${GITLAB_TOKEN}@gitlab.com/SEON.N/${REPO_NAME}.git" "/tmp/${REPO_NAME}"
cd "/tmp/${REPO_NAME}"
git symbolic-ref HEAD refs/heads/main
mkdir -p templates

出力

created: SEON.N/gitlab-ci-components-catalog | id 83321559 | vis public
Cloning into '/tmp/gitlab-ci-components-catalog'...
warning: You appear to have cloned an empty repository.

補足: サービスアカウントや CI 環境は SSH 鍵が無いことがあるので、git push は https://oauth2:${GITLAB_TOKEN}@... 形式にする。トークンの scope は api(作成)と write_repository(push)が必要だ。

ステップ 2 — greeting コンポーネント(入力 4 種 + 補間)

最初のコンポーネントを書くステップだ。コンポーネントの二つの核心である「型のある入力」と「補間」を一度に見せるため、入力 4 種を受け取ってジョブ名とスクリプトに差し込む greeting を作る。入力によって動作が変わるコンポーネントの、最も小さな例だ。

templates/greeting.ymlstring/boolean/options/default をすべて使い、ジョブ名とスクリプトに補間する。

spec:
  inputs:
    stage:
      type: string
      default: test
    name:
      type: string
      description: "Who to greet. Required: no default."
    style:
      type: string
      default: plain
      options: [plain, banner]
    shout:
      type: boolean
      default: false
---
"greeting $[[ inputs.name ]]":          # ジョブ名にも補間
  stage: $[[ inputs.stage ]]
  image: alpine:3.20
  variables:
    GREET_NAME: "$[[ inputs.name ]]"
    GREET_STYLE: "$[[ inputs.style ]]"
    GREET_SHOUT: "$[[ inputs.shout ]]"
  script:
    - |
      MSG="Hello, ${GREET_NAME}!"
      if [ "${GREET_SHOUT}" = "true" ]; then
        MSG="$(echo "${MSG}" | tr '[:lower:]' '[:upper:]')"
      fi
      if [ "${GREET_STYLE}" = "banner" ]; then
        LINE="$(echo "${MSG}" | sed 's/./=/g')"
        printf '%s\n%s\n%s\n' "${LINE}" "${MSG}" "${LINE}"
      else
        echo "${MSG}"
      fi

ファイル: templates/greeting.yml

補足: 複数行スクリプトは - | のブロックスカラーで書く。- echo "OK: ..." のように書くと、YAML の plain scalar 内の : (コロン+スペース)がマッピング区切りと解釈されてパースエラーになる(後述のトラブルシューティング参照)。

ステップ 3 — semver-guard コンポーネント(regex)+ .gitlab-ci.yml

入力検証(regex)を見せる二つ目のコンポーネントを作り、同時にこのプロジェクトが自分のコンポーネントを自分でテスト(self-test)し、タグから release するように .gitlab-ci.yml を組むステップだ。コンポーネントと「公開パイプラインの骨格」を一緒に揃える。

templates/semver-guard.yml は正規表現で入力を検証する。

spec:
  inputs:
    stage:
      type: string
      default: test
    version:
      type: string
      regex: ^v?\d+\.\d+\.\d+$
---
semver-guard:
  stage: $[[ inputs.stage ]]
  image: alpine:3.20
  variables:
    INPUT_VERSION: "$[[ inputs.version ]]"
  script:
    - |
      set -e
      echo "Validating version '${INPUT_VERSION}'"
      echo "${INPUT_VERSION}" | grep -Eq '^v?[0-9]+\.[0-9]+\.[0-9]+$'
      echo "OK - '${INPUT_VERSION}' is a valid semantic version"

プロジェクト自身の .gitlab-ci.yml は (a) コンポーネントを現在の SHA で self-test し、(b) タグから release を作る。

stages: [test, release]

include:
  - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/greeting@$CI_COMMIT_SHA
    inputs:
      name: GitLab
      style: banner
      shout: true
  - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/semver-guard@$CI_COMMIT_SHA
    inputs:
      version: v1.0.0

create-release:
  stage: release
  image: registry.gitlab.com/gitlab-org/cli:latest
  rules:
    - if: $CI_COMMIT_TAG
  script:
    - echo "Creating release for tag $CI_COMMIT_TAG"
  release:
    tag_name: $CI_COMMIT_TAG
    description: "Release $CI_COMMIT_TAG of the components."

ファイル: .gitlab-ci.yml

補足: self-test に @$CI_COMMIT_SHA を使うと、たった今 push したコミットに入っているコンポーネントを、まさにその時点で取り込んで検証する。つまり「自分自身を自分のパイプラインでテストする」パターンだ。push 前にはローカルで YAML 文法を検証した。

python3 - <<'PY'
import yaml
for f in ["templates/greeting.yml","templates/semver-guard.yml",".gitlab-ci.yml"]:
    print("OK", f, len(list(yaml.safe_load_all(open(f)))), "doc")
PY
# OK templates/greeting.yml 2 doc
# OK templates/semver-guard.yml 2 doc
# OK .gitlab-ci.yml 1 doc

ステップ 4 — push して self-test パイプラインを実行(実際の出力)

書いたコンポーネントが実際に動くか確認するステップだ。main に push すると self-test パイプラインが、たった今作ったコンポーネントをそのコミットのまま実行する。公開する前に壊れたコンポーネントをふるい落とす関門だ。

git add .
git commit -m "Add greeting and semver-guard reusable CI/CD components"
git push -u origin main
# main への push が self-test パイプラインをトリガーする

パイプラインとジョブの状態を API で確認する。

PID=83321559
curl -s --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
  "https://gitlab.com/api/v4/projects/${PID}/pipelines?per_page=1"

実際の出力(ジョブ一覧)

14845634397 | semver-guard     | test | success | 8.4 s
14845634396 | greeting GitLab  | test | success | 10.3 s

ジョブ名が greeting GitLab である点に注目。"greeting $[[ inputs.name ]]" の補間が効いている。ジョブログ(trace)は次のとおり。

実際の出力(greeting ジョブ trace)

$ MSG="Hello, ${GREET_NAME}!" # collapsed multi-line command
==============
HELLO, GITLAB!
==============

実際の出力(semver-guard ジョブ trace)

$ set -e # collapsed multi-line command
Validating version 'v1.0.0'
OK - 'v1.0.0' is a valid semantic version

補足: style: banner(バナー出力)と shout: true(大文字化)が両方反映され、HELLO, GITLAB! がボックスで出力された。boolean 入力がスクリプト内で "true" という文字列に補間されたことも確認できる。

ステップ 5 — カタログリソース登録 + バージョン公開(実際の出力)

検証済みのコンポーネントをカタログに載せ、「バージョンで利用できる」状態にするステップだ。まずプロジェクトをカタログリソースとして登録し、セマンティックバージョンのタグを push して release ジョブがそのバージョンをカタログに公開するようにする。ここから他のプロジェクトが検索・利用できる。

プロジェクトを「カタログリソース」として表示する。UI では Settings > General > Visibility の「CI/CD Catalog project」トグルだが、自動化には GraphQL catalogResourcesCreate を使う(REST はまだ無い、issue 463043)。

curl -s --request POST "https://gitlab.com/api/graphql" \
  --header "Authorization: Bearer ${GITLAB_TOKEN}" \
  --header "Content-Type: application/json" \
  --data '{"query":"mutation { catalogResourcesCreate(input: { projectPath: \"SEON.N/gitlab-ci-components-catalog\" }) { errors } }"}'
# => {"data":{"catalogResourcesCreate":{"errors":[]}}}

これでセマンティックバージョンのタグを push すると、タグのパイプラインが self-test の後に release ジョブを実行する。

git tag v1.0.0
git push origin v1.0.0

実際の出力(タグパイプラインのジョブ)

14845637416 | create-release   | release | success
14845637415 | semver-guard     | test    | success
14845637414 | greeting GitLab  | test    | success

実際の出力(create-release ジョブ trace)

$ echo "Creating release for tag $CI_COMMIT_TAG"
Creating release for tag v1.0.0
• Creating or updating release  repo=SEON.N/gitlab-ci-components-catalog tag=v1.0.0
✓ Release created:	url=https://gitlab.com/SEON.N/gitlab-ci-components-catalog/-/releases/v1.0.0
✓ Release succeeded after 0.77 seconds.

create-release ジョブログ — registry.gitlab.com/gitlab-org/cli(glab)イメージで release 生成

カタログにバージョンが登録されたか GraphQL で確認する。

curl -s --request POST "https://gitlab.com/api/graphql" \
  --header "Authorization: Bearer ${GITLAB_TOKEN}" --header "Content-Type: application/json" \
  --data '{"query":"{ ciCatalogResource(fullPath: \"SEON.N/gitlab-ci-components-catalog\") { name webPath versions { count nodes { name } } } }"}'

出力

{"data":{"ciCatalogResource":{"name":"gitlab-ci-components-catalog",
  "webPath":"/SEON.N/gitlab-ci-components-catalog",
  "versions":{"count":1,"nodes":[{"name":"v1.0.0"}]}}}}

CI/CD Catalog に公開された v1.0.0(Components: greeting, semver-guard)

補足: release ジョブが使うイメージは現在の公式ドキュメント基準で registry.gitlab.com/gitlab-org/cli(glab)だ。以前の release-cli から変わった。release キーワード無しでタグだけ push してもカタログにバージョンは作られない。

ステップ 6 — 利用側の include 検証(CI Lint API、実際の出力)

公開したコンポーネントを他のプロジェクトが実際に取り込んで使えるか確認するステップだ。runner を起動せず、CI Lint API だけでバージョン参照と入力検証が意図どおり動くかを見る。

別のプロジェクトが公開済みのバージョンを取り込めるか CI Lint API(POST /projects/:id/ci/lint)で検証する。runner 無しで include の解決と入力検証まで確認できる。

PID=83321559
curl -s --request POST --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
  --header "Content-Type: application/json" \
  --data '{"include_jobs":true,"content":"stages: [test]\ninclude:\n  - component: gitlab.com/SEON.N/gitlab-ci-components-catalog/greeting@v1.0.0\n    inputs:\n      name: World\n      style: plain"}' \
  "https://gitlab.com/api/v4/projects/${PID}/ci/lint"

出力(バージョン参照 3 種)

ref @v1.0.0     -> valid=True  jobs=['greeting World']  errors=[]
ref @~latest    -> valid=True  jobs=['greeting World']  errors=[]
ref @1.0.0      -> valid=False errors=["Component '.../greeting@1.0.0' - content not found"]

タグが v1.0.0 なので include も @v1.0.0(または @~latest)でなければならない。@1.0.0(接頭辞 v なし)はタグ名と異なるため “content not found” になる。次に semver-guard の正規表現入力を検証する。

実際の出力(regex 入力の通過/拒否)

=== valid version v2.3.4 (regex pass) ===
valid: True | jobs: ['semver-guard'] | errors: []

=== invalid version 'not-a-version' (regex reject) ===
valid: False | errors: ['`.../semver-guard@v1.0.0`: `version` input: provided value does not match required RegEx pattern']

カタログ UI の semver-guard — version は regex で検証される入力

補足: 不正なバージョン文字列は、ジョブが実行される前のパイプライン生成段階で正規表現検証によって弾かれる。これがコンポーネントの入力の型/正規表現の核心的な価値だ。

応用ハンズオン

基本の一周 — コンポーネントを作り(ステップ 1〜3)、self-test で検証し(ステップ 4)、カタログに公開し(ステップ 5)、利用側が取り込めるか確認(ステップ 6)— はここまでだ。以下は実務でよく使う応用パターンだ: 別プロジェクトでの実際の利用、バージョン範囲による破壊的変更の制御、複数コンポーネントの組み合わせ。

ステップ 7 — 利用側プロジェクトで実際に取り込む(usage)

コンポーネントを作ったプロジェクトではなく、別のプロジェクトinclude して利用するのがカタログの目的だ。そこで別の 利用側プロジェクト(public)を一つ作り、実際に取り込んで使った。

利用側プロジェクトの .gitlab-ci.yml は、コンポーネントをバージョンで取り込むだけでよい。

stages: [test]

include:
  - component: $CI_SERVER_FQDN/<your-namespace>/gitlab-ci-components-catalog/greeting@v1.0.0
    inputs:
      name: Consumer
      style: banner
  - component: $CI_SERVER_FQDN/<your-namespace>/gitlab-ci-components-catalog/semver-guard@v1.0.0
    inputs:
      version: v3.1.4

実際の出力(利用側パイプラインのジョブ)

greeting Consumer | test | success
semver-guard      | test | success

利用側プロジェクトのパイプライン — カタログの greeting・semver-guard を include して実行

ジョブ名が greeting Consumer である点から、補間が利用側の入力(name: Consumer)で効いていることが確認できる。つまりコンポーネントは一度公開すれば、任意のプロジェクトが自分の入力で取り込んで使う。

補足: カタログはコンポーネントごとの使用数を集計する。GraphQL で照会する。

glab api graphql -f query='{ ciCatalogResource(fullPath:"<group>/<project>"){ versions{ nodes{ name components{ nodes{ name last30DayUsageCount } } } } } }'

ただし last30DayUsageCount は利用直後にはすぐ反映されない(実測では同日の再照会でも 0 だった)。使用状況は集計が更新された後に確認するのが正確だ。

ステップ 8 — v2.0.0 リリースとバージョン範囲(破壊的変更の制御)

破壊的変更は MAJOR で知らせる。greeting の既定の styleplain から banner に変え(既存の利用側の既定出力が変わる変更)、v2.0.0 をリリースした。

# templates/greeting.yml: style の default を plain -> banner に変更(breaking)
git commit -am "greeting v2.0.0: change default style to banner (breaking change)"
git tag v2.0.0
git push origin main v2.0.0

タグのパイプラインが self-test の後 release まで通れば、カタログには二つのバージョンが共存する。

実際の出力(カタログのバージョン)

{"versions":{"count":2,"nodes":[{"name":"v2.0.0"},{"name":"v1.0.0"}]}}

これで利用側がどの参照を使うかによって取り込むバージョンが分かれる。CI Lint で検証した。

実際の出力(バージョン参照の解決)

@v1.0.0  -> valid     正確なタグ
@v2.0.0  -> valid     正確なタグ
@1       -> valid     1.x 範囲の最新 = v1.0.0
@2       -> valid     2.x 範囲の最新 = v2.0.0
@~latest -> valid     リリース済みの最新(プレリリース除く)= v2.0.0
@1.0.0   -> invalid   content not found(タグは v1.0.0)

肝心なのは運用戦略だ。破壊的変更の v2.0.0 が出ても、@1 で固定した利用側は 1.x を受け続けて安全だ。一方 @~latest を使っていた利用側は v2.0.0 が自動で入り込み、動作が変わる(ここでは既定出力が banner に)。だから運用パイプラインは @1 のような部分バージョンか固定バージョンを使い、@~latest は避けるのが安全だ。

ステップ 9 — コンポーネントの組み合わせ(複数コンポーネントを一つのパイプラインに)

コンポーネントの真価は「組み立て」にある。単一目的のコンポーネントを複数、ステージとして束ねて標準パイプラインを作る。ここでは lint コンポーネントをもう一つ作り、self-test を lint → test の流れで構成した。

templates/lint.yml:

spec:
  inputs:
    stage:
      type: string
      default: lint
---
component-lint:
  stage: $[[ inputs.stage ]]
  image: alpine:3.20
  script:
    - |
      echo "Linting component templates..."
      for f in templates/*.yml; do echo "checking ${f}"; done
      echo "OK - lint passed"

.gitlab-ci.yml はステージを [lint, test, release] にし、既存の greeting・semver-guard(test)の前に lint(lint ステージ)を追加する。

stages: [lint, test, release]

include:
  - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/lint@$CI_COMMIT_SHA
    inputs:
      stage: lint
  # greeting, semver-guard はステップ 3 と同じ(test ステージ)

実際の出力(パイプラインのグラフ)

lint → test の 2 段で組み合わせた self-test パイプライン

lint ステージの component-lint が先に通ってから、test ステージの greetingsemver-guard が走る。各コンポーネントは独立してバージョン管理・公開されるが、利用側でこのようにステージとして組み合わせ、一つの標準パイプラインを作る — これが「組織標準パイプラインカタログ」の核心だ。

検証

確認項目コマンド/方法期待結果
コンポーネント動作self-test パイプラインgreeting/semver-guard ジョブ success
補間ジョブ名greeting GitLab
カタログ公開GraphQL ciCatalogResourceversions.count == 1v1.0.0
リリースGET /projects/:id/releasestag_name: v1.0.0
利用側の解決CI Lint @v1.0.0valid=true、ジョブのマージ
入力検証CI Lint、不正な値valid=false、RegEx エラー

失敗時: パイプラインが起動しないならプロジェクトの runner 設定(shared/group runner)と .gitlab-ci.yml の文法(glab ci lint)を、カタログにバージョンが出ないなら (1) カタログリソース登録の有無、(2) release キーワードの使用有無、(3) description/README の有無を見る。

本番適用(Production)

  • 権限モデル・トークンのローテーション: 公開には Owner 権限と api+write_repository scope が必要だ。CI ではできるだけ CI_JOB_TOKEN やグループアクセストークンを使い、PAT は有効期限を設定して定期的にローテーションする。トークンに有効期限が無いと特に危険なので、必ず有効期限・ローテーション方針を設ける。
  • ガバナンス: コンポーネントプロジェクトをグループ単位でまとめ、標準パイプラインを強制する。セマンティックバージョン(MAJOR.MINOR.PATCH)で破壊的変更を知らせ、利用側は @1(部分バージョン)で非破壊的な更新だけ自動で受けるか、固定バージョンを使う。
  • **\~latest** の注意: 便利だが破壊的変更が自動で入り込む。運用パイプラインは固定バージョンを推奨する。
  • セキュリティのハードニング: コンポーネントが受ける入力に regex/options をかけ、任意コマンド注入の余地を減らす。イメージタグを入力で受けるときも正規表現で制限する。
  • モニタリング・可観測性との連携: リリース/パイプラインの失敗を通知につなぐ。例: パイプライン失敗時に webhook → 通知チャンネル。(運用環境の可観測性スタック、例: Prometheus/Loki/Tempo と連携)パイプラインのステータスバッジやカタログのバージョン数をダッシュボードの指標として出す。
  • 障害復旧 runbook: 誤ったバージョンを公開したら (1) パッチバージョンを上げて再公開、(2) 利用側は直前の正常バージョンに固定、(3) ~latest を使っていた利用側が壊れたら直ちに固定バージョンへ切り替える。

よくある間違いとトラブルシューティング

症状原因修正
include 時に content not foundタグが v1.0.0 なのに @1.0.0 で参照include のバージョンを実際のタグ名に一致(@v1.0.0)、または @~latest/@1 を使う
YAML パースエラー expected <block end>plain scalar のスクリプトに : (コロン+スペース)が含まれる(echo "OK: ..."複数行スクリプトは block scalar で書き、コロン+スペースを plain scalar に置かない
カタログにバージョンが出ないプロジェクトがカタログリソース未登録catalogResourcesCreate GraphQL または UI トグルを有効化
カタログ登録はできたがバージョン 0release キーワード無しでタグだけ pushパイプラインに release: ジョブを追加してタグを再 push
パイプラインがまったく起動しないプロジェクトに有効な runner が無い(gitlab.com 無料枠はアカウント検証が必要な場合あり)shared/group runner を有効化、グループの shared_runners_setting を確認
誤ったトークンで 401セルフホスト用または期限切れトークンの使用glab config get token --host gitlab.com で正しいホストのトークンを使う

さらに進む

  • **array** 入力とインデックス: $[[ inputs.servers[0].host ]] のような構造化入力を使う(セグメントあたり最大 5 インデックス)。
  • コンポーネントのテスト強化: self-test に、不正な入力が実際に拒否されるか表明する negative test を追加し、壊れたコンポーネントが公開されないようにする。
  • CI/CD Catalog の検索: https://gitlab.com/explore/catalog で公開コンポーネントを探索し、再利用パターンを学ぶ。

後片付け(cleanup)

このハンズオンは git push と release だけを使い、追加のインフラを立てない。gitlab.com のパイプラインはプロジェクトの CI 分(minutes)を少量消費する。

# ハンズオン終了後の片付け(必要なら):
# - プロジェクト全体の削除はユーザー承認後のみ。保持する場合はトークンだけ回収。
# - git push に使った一時トークンは有効期限/ローテーション方針に従って管理する。
# - ローカル作業ディレクトリの掃除: rm -rf /tmp/gitlab-ci-components-catalog

コスト/CI 分の注意: カタログ公開自体に追加コストは無い。ただしパイプライン実行は CI 分(minutes) を消費する。大きな self-test を繰り返しトリガーしないよう rules で不要な実行を減らす。

参考資料