GitLab CI/CD 実践[マルチステージ編]:複数ジョブをつなぐパイプライン設計

はじめに

前回の記事では、単一のジョブをステップごとに実行するシンプルなパイプラインを題材に、GitLab CI/CDの基本的な仕組みを確認しました。しかし、実際の開発現場では、アプリケーションのビルド、テスト、デプロイなど複数の処理を組み合わせ、段階的に実行することが求められます。今回はその実践編として、複数のジョブをつなぎ合わせてステージごとに実行するマルチステージパイプラインの設計方法を紹介します。さらに、作成したパイプラインを効率的に管理・運用するための実行管理のポイントについても解説します。

前提条件

本記事では、あらかじめ以下の環境が構築されていることを前提とします。環境の詳細な構築手順については、環境構築編の記事をご参照ください。

  • GitLab(Self-Managed版)
    • LinuxサーバーにOmnibusパッケージでインストール済み
    • Community Edition(無償版)を利用
    • 自己署名証明書を利用し、内部DNSによる名前解決が可能
  • Gitlab Runner
    • OpenShiftクラスター上にデプロイ済み
    • Kubernetes Executorを利用し、CI/CDジョブをPodとして実行可能
    • レジストリ認証情報をリンクしたServiceAccountを利用可能
      • ex. oc secrets link gitlab-runner-sa gitlab-registry-secret –for=pull
  • OpenShiftクラスター
    • 閉域環境(インターネット非接続)に構築
    • 踏み台サーバーからocコマンドで操作可能
  • GitLab Container Registry
    • コンテナイメージのpush / pullに利用
    • 自己署名証明書のため、CA証明書をRunner / ノードに登録済み
  • 踏み台サーバー
    • 内部DNSサーバー、NFSサーバーを兼任
    • 外部インターネットへの接続が可能
    • ocコマンドでOpenShiftを操作可能
    • podmanでコンテナイメージの push / pull が可能

これらの環境を利用してGitLab CI/CDパイプラインを実行し、ビルドからデプロイまでの流れを検証できます。

また、本記事では以下のようにテスト用のプロジェクトとブランチを用意して検証します。

テスト用プロジェクトの作成

任意の名称で新規作成(例: test-project)。このプロジェクトでジョブを検証します。

テスト用ブランチの作成

本記事ではmulti-stage-testブランチを作成して使用していますが、mainなど任意のブランチでも検証可能です。

マルチステージパイプラインの作成方法

ここでは、実際にGitLab上でマルチステージパイプラインを作成し、アプリケーションを OpenShiftクラスターにデプロイする一連の流れを確認します。今回のサンプルでは、「イメージのビルド」と「Kubernetes へのデプロイ」をそれぞれ独立したステージとして定義します。

ディレクトリ構成

まず、プロジェクトのディレクトリ構成は以下のとおりです。

ソースコードや設定ファイルに加え、CI/CDの定義ファイル(.gitlab-ci.yml)とKubernetes のマニフェスト(deploy.yaml)を配置しています。

.
├── .gitlab-ci.yml
├── Dockerfile
├── README.md
├── deploy.yaml
├── nginx.conf
└── web
    └── index.html

各ファイルの役割

ファイル名概要
DockerfileベースとなるNginxイメージにアプリの静的ファイルとNginx設定を組み込み、ポート8080で待ち受けるコンテナを作成します。
nginx.confヘルスチェック用の/healthzエンドポイントとトップページでindex.htmlを返す設定、使用ポートを定義しています。
web/index.htmlデプロイ成功を確認するための簡単なHTMLページです。
deploy.yamlOpenShift上にDeploymentを作成するためのマニフェストです。後述のジョブで${IMAGE}:${TAG}の部分を置換し、実際にビルドしたコンテナイメージを指定します。
.gitlab-ci.ymlCI/CD パイプラインの定義です。buildステージでイメージのビルドとプッシュを行い、deployステージでKubernetesへ適用します。

Dockerfile

このアプリケーションでは、ベースイメージにnginx:1.29-alpineを利用しています。閉域環境のため、本記事では事前にこのイメージをGitLab Container Registryにpushし、そのレジストリを参照する形でFROMを指定しています。

FROM registry.gitlab.local.example.com/root/test-project/nginx:1.29-alpine

# アプリ静的ファイル
COPY web/ /usr/share/nginx/html/

# Nginx 設定(ポート8080で待受)
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 8080

CMD ["nginx", "-g", "daemon off;"]

このDockerfileでは、web/index.htmlを/usr/share/nginx/html/に配置し、Nginxをポート8080で起動することで、ブラウザやcurlからアクセスするとindex.htmlが返されるシンプルな構成になっています。

nginx.conf

このnginx.confはポート8080で待ち受け、/healthzに200を返すヘルスチェック用エンドポイントを定義しています。

ドキュメントルート/usr/share/nginx/htmlに配置したindex.htmlをトップページとして返し、存在しないパスもindex.htmlにフォールバックするシンプルな設定です。

server {
  listen 8080 default_server;
  server_name _;

  # ヘルスチェック用(200/ok)
  location = /healthz {
    add_header Content-Type text/plain;
    return 200 "ok\n";
  }

  root  /usr/share/nginx/html;
  index index.html;

  location / {
    try_files $uri $uri/ /index.html;
  }
}

web/index.html

デプロイの動作確認用に用意したシンプルな静的ファイルです。

Nginxのドキュメントルートに配置され、ブラウザやcurlでアクセスするとトップページとして返されます。パイプラインの挙動を検証したい場合は、このファイルを編集してpushすることで、新しいイメージがビルドされ、再デプロイ後に変更内容を確認できます。

<!doctype html>
<html lang="ja">
  <head><meta charset="utf-8"><title>Demo App</title></head>
  <body>
    <h1>Deploy Success v1.0.0.</h1>
  </body>
</html>

deploy.yaml

アプリケーションをOpenShiftクラスター上にデプロイするためのDeploymentマニフェストです。spec.template.spec.containers[0].imageには ${IMAGE}:${TAG}を指定しており、CI/CDパイプライン実行時にビルドしたイメージ名へ置換されます。

コンテナはポート8080を公開し、/healthzへのHTTP応答をreadinessProbeとして利用することで、Pod が正常に起動しているかを判定します。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: multi-stage-demo-deploy
  namespace: gitlab-runner-test
spec:
  replicas: 1
  selector:
    matchLabels:
      app: multi-stage-demo
  template:
    metadata:
      labels:
        app: multi-stage-demo
    spec:
      serviceAccountName: gitlab-runner-sa
      imagePullSecrets:
        - name: gitlab-registry-secret
      containers:
        - name: multi-stage-demo-app
          image: ${IMAGE}:${TAG}
          imagePullPolicy: Always
          ports:
            - containerPort: 8080
          readinessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 3
            periodSeconds: 5

.gitlab-ci.yml

この.gitlab-ci.ymlは 2ステージ(build → deploy)構成でdindを使ってイメージをビルド→レジストリへpush →OpenShiftへのデプロイまでを自動化します。まず定義全体を示し、その後に実行フローを解説します。

stages:
  - build
  - deploy

default:
  tags: [devsecops-runner]

build_image:
  stage: build
  image: registry.gitlab.local.example.com/root/registry-project/docker:28.3.3
  services:
    - name: registry.gitlab.local.example.com/root/registry-project/docker:28.3.3-dind
      alias: docker
      entrypoint: ["/bin/sh","-lc"]
      command:
        - >
          cp /etc/gitlab-runner/certs/gitlab.local.example.com.crt /usr/local/share/ca-certificates/ca.crt &&
          update-ca-certificates ;
          exec dockerd-entrypoint.sh
          --tls=false
          --host=unix:///var/run/docker.sock
          --host=tcp://0.0.0.0:2375
  variables:
    DOCKER_HOST: tcp://docker:2375
    DOCKER_TLS_CERTDIR: ""
  before_script:
    - cp /etc/gitlab-runner/certs/gitlab.local.example.com.crt /usr/local/share/ca-certificates/ca.crt && update-ca-certificates
    - docker login "$CI_REGISTRY" -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD"
    # IMAGE_VERSION が定義されていればそれを利用、なければ CI_COMMIT_SHORT_SHA
    - export IMAGE_TAG="${IMAGE_VERSION:-$CI_COMMIT_SHORT_SHA}"
    - export IMAGE_FULL="${CI_REGISTRY_IMAGE}/demo:${IMAGE_TAG}"
  script:
    - docker build -t "$IMAGE_FULL" .
    - docker push "$IMAGE_FULL"
    - printf "IMAGE_FULL=%s\n" "$IMAGE_FULL" > build.env
  artifacts:
    reports:
      dotenv: build.env

deploy_manifest:
  stage: deploy
  image: registry.gitlab.local.example.com/root/test-project/oc:4.18
  needs:
    - job: build_image
      artifacts: true
  variables:
    K8S_NAMESPACE: gitlab-runner-test
  before_script:
    - oc whoami
  script:
    - |
      : "${IMAGE_FULL:?IMAGE_FULL missing from build.env}"
      # IMAGE と TAG を分離
      IMAGE="${IMAGE_FULL%:*}"; TAG="${IMAGE_FULL##*:}"
      # ${IMAGE}:${TAG} を合成
      sed "s|\${IMAGE}:\${TAG}|${IMAGE_FULL}|g" deploy.yaml > deploy.rendered.yaml
      echo "---- rendered ----"
      cat deploy.rendered.yaml
      oc apply -f deploy.rendered.yaml

※IMAGE_VERSIONが未定義の場合はCI_COMMIT_SHORT_SHAがタグに使われます。この場合、短いコミットIDごとに新しいイメージが作成されるため、レジストリの容量管理や不要イメージの削除に注意してください。レジストリの容量管理についてはGitLab Container Registry: 応用編(可視性設定とガベージコレクション)をご参照ください。

補足: 閉域環境向けの準備

ベースイメージ登録

通常はDocker Hubから直接nginx:1.29-alpineを取得できますが、インターネット接続ができない環境では以下のように 事前にレジストリにpushしておきます。

$ podman login registry.gitlab.local.example.com
$ podman pull docker.io/library/nginx:1.29.1-alpine
$ podman tag docker.io/library/nginx:1.29.1-alpine \
registry.gitlab.local.example.com/root/test-project/nginx:1.29.1-alpine
$ podman push \
registry.gitlab.local.example.com/root/test-project/nginx:1.29.1-alpine

※Docker 利用環境ではpodman → dockerに置き換えてください。

ocコマンド用イメージ登録

通常はregistry.redhat.ioから直接ose-cli-rhel9:v4.18を取得できますが、インターネット接続ができない環境では以下のように 事前にレジストリにpushしておきます。

$ podman login registry.redhat.io
$ podman pull registry.redhat.io/openshift4/ose-cli-rhel9:v4.18
$ podman login registry.gitlab.local.example.com
$ podman tag registry.redhat.io/openshift4/ose-cli-rhel9:v4.18 \
  registry.gitlab.local.example.com/root/test-project/oc:4.18
$ podman push registry.gitlab.local.example.com/root/test-project/oc:4.18

※Docker利用環境ではpodman → dockerに置き換えてください。
※デプロイ先のコンテナ基盤がKubernetesである場合はoc → kubectlに置き換えてください。OpenShiftの場合もkubectlコマンドで基本的な操作は可能ですが、ocにはOpenShift特有の拡張機能が含まれています。

パイプラインの仕組み(実行フロー解説)

パイプラインは2ステージ構成になっています。

1. build ステージ(イメージのビルドとプッシュ)

  • ジョブbuild_imageが実行されます。
  • Docker-in-Docker(dind)環境を利用してdocker buildを実行し、GitLab Container Registryにイメージをpushします。
  • pushしたイメージのフルパス(IMAGE_FULL)をbuild.envとしてアーティファクトに保存します。

→ この情報が次のステージに引き渡されます。

2. deploy ステージ(マニフェストの適用)

  • ジョブdeploy_manifestが実行されます。
  • 先ほど保存したIMAGE_FULLを利用し、deploy.yaml内の${IMAGE}:${TAG}を実際のイメージ名に置換します。
  • oc apply -f deploy.rendered.yamlを実行してOpenShiftクラスターにDeploymentを作成します。
  • Podが立ち上がり、readiness probeによって/healthzが成功すればデプロイ完了です。

パイプラインの実行

作成したパイプラインを実際に動かしてみましょう。

まず、検証用プロジェクトに移動し、管理画面からCI/CD変数を設定します。[変数を追加]をクリックし、キーにIMAGE_VERSION、値にv1.0.0を入力します。なお、mainブランチなどの保護ブランチ以外で試す場合は、保存時に[変数の保護]のチェックを外してください。チェックが付いたままだと、featureブランチなど非保護ブランチでパイプライン実行時に環境変数を参照できなくなります。

次に、ソースコードをリポジトリにpushします。GitLabのプロジェクト画面から検証対象のブランチに切り替え、[編集] → [Web IDE]を選択して内部エディタを開きます。前述のファイル(.gitlab-ci.ymlやdeploy.yamlなど)を作成し、右側の[Source Control]アイコンからコミットメッセージを入力して[Commit and push]をクリックすれば、コードがブランチにpushされます。

ソースコードがpushされると、自動的にパイプラインが起動します。プロジェクトのサイドバーから [ビルド] > [パイプライン] を選択すると、実行中のパイプラインを確認できます。さらに、パイプラインIDをクリックすれば、各ジョブの実行状況やログを確認できます。

プロジェクトのサイドバーから[デプロイ] > [コンテナレジストリ]を選択すると、プロジェクト内レジストリのリポジトリ一覧が表示されます。今回ビルドしたイメージはdemoリポジトリに格納されています。

この例では、build_imageジョブでイメージをビルドしてレジストリにpushし、その結果生成されたIMAGE_FULL変数がbuild.envに保存されます。続くdeploy_manifestジョブでは、その変数を利用してマニフェスト内のプレースホルダーを置換し、oc applyコマンドでOpenShiftにデプロイできていることが確認できます。

デプロイ成功の確認

デプロイジョブの成功を確認した後、アプリのPodがRunning状態で起動していることを確認します。

$ oc get pod -n gitlab-runner-test -l app=multi-stage-demo
NAME                                       READY   STATUS    RESTARTS   AGE
multi-stage-demo-deploy-6874887f67-w7br8   1/1     Running   0          69s

Podが正常に起動すると、Pod内のNginxでweb/index.htmlが配信されます。以下のようにアクセスして動作を確認できます。
# Pod名を取得
$ POD=$(oc -n gitlab-runner-test get pod -l app=multi-stage-demo -o jsonpath='{.items[0].metadata.name}')

# ローカル18080 → Pod 8080 へ転送
$ oc -n gitlab-runner-test port-forward pod/$POD 18080:8080

# ブラウザからlocalhost:18080にアクセスし、index.htmlの内容が表示されることを確認
Deploy Success v1.0.0.

このメッセージが表示されれば、マルチステージパイプラインを通じて「ビルド→レジストリへのpush→OpenShiftへのデプロイ」が自動化できていることを確認できます。

これで、コードのpushをトリガーにビルドからデプロイまでが一連の流れで実行されることを、実際のパイプライン実行を通して確かめられます。

なお、すべての処理を1つのステージにまとめることも可能ですが、ビルドとデプロイを分けることで以下のメリットがあります。

  • 責務の分離:ビルドとデプロイを明確に分けることで、どの段階で失敗したのかを特定しやすくなります。
  • 効率的なリトライ:ビルドが成功していれば、デプロイだけを再実行できるため、再試行の効率が向上します。
  • 並列・拡張性の確保:将来的にテストステージを挟んだり、複数環境へのデプロイを分岐させたりといった拡張が容易になります。

このように、ステージを適切に分割することで、パイプラインの信頼性と運用効率を高めることができます。

まとめ

本記事では、複数のステージを組み合わせたパイプラインの設定方法を解説しました。
サンプルアプリケーションを題材に、ビルドとデプロイを別ステージに分けたマルチステージパイプラインを構築し、実際にコードのpushをトリガーにして一連の処理が自動で実行される流れを確認しました。
単一ステージに処理をまとめることも可能ですが、ステージを分割することで「どこで失敗したのかが明確になる」「ビルドが成功していればデプロイのみを再実行できる」といった運用上のメリットが得られることも示しました。これにより、パイプラインの信頼性や管理性が大きく向上します。

次回は、さらに複雑化したパイプライン定義ファイルを include / extends 機能 を活用して整理する方法を解説します。複数のジョブやステージが増えて管理が煩雑になったときに役立つ実践的なテクニックを取り上げ、よりスケーラブルなCI/CD運用に向けた一歩を紹介します。

ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

0人がこの投稿は役に立ったと言っています。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です