はじめに

本記事ではCircleCIのジョブ内から他のパイプライン(※)を実行(キック)する方法について紹介します。
1つのパイプラインでCI/CDの処理を完結させるだけでなく、異なるリポジトリにあるパイプラインを連携させることでより柔軟で効率的なCI/CDのワークフローを実現できます。

※「他のパイプライン」… ここでは「CircleCIパイプラインから呼び出される別のCircleCIパイプライン」のことを指します。主に複数のGitHubリポジトリのそれぞれでCircleCIのパイプラインが構築されている状況を想定しています。

前提条件

  • CircleCIのAPIトークン
    パイプラインAからパイプラインBにアクセスするため、BのAPIトークンを発行し、Aから利用できるようにする(後述)
  • CircleCIプロジェクトの環境設定
    CircleCIの設定ファイル(.circleci/config.yml)が各リポジトリで正しく設定されていること。

イメージ

① パイプラインAのジョブ2からパイプラインBを実行
② パイプラインBの実行中、パイプラインA(のジョブ2)は待機
③ パイプラインAのジョブ2がパイプラインBの実行結果(成功 or 失敗)を取得
  成功だったらパイプラインAのジョブ3以降を続行し、失敗だったらパイプラインAも失敗で終了させる

CircleCIの REST API について

CircleCIでは REST API を使用してパイプラインのトリガーや管理を自動化できます。以下の仕様でHTTPリクエストを送ることでパイプラインを実行できます。

  • パイプラインの起動
    • POSTリクエストで送信する
    • Gitブランチや設定パラメーターを指定することで任意のパイプラインを開始できる
  • 認証
    • HTTPヘッダーにCircleCIユーザーが発行したAPIトークン(後述)を含める
  • パラメーター設定
    トリガー時にカスタムパラメーターを指定でき、設定ファイル内でそのパラメーターに基づいた処理の分岐が可能

実装手順

1. APIトークンの準備

最初にパイプラインAPIを利用するためのAPIトークンを取得します。

  1. CircleCIのユーザー設定画面(User Settings)にアクセス
  2. 「Personal API Token」のセクションで新しいトークンを発行する
  3. キック元のCircleCIプロジェクトで環境変数(Settings > Environment Variables)を新規作成する
    • 変数名を CIRCLECI_API_TOKEN_B とし、値に発行したAPIトークンを入れる

注意
APIトークンは2種類あり、「CircleCIプロジェクト」から発行するものと「ユーザーアカウント」から発行するものがありますが、ユーザーアカウントから発行してください。現在の CircleCI API v2 では「CircleCIプロジェクトから発行したAPIトークン」は利用できません(”Project not found.” と出力される)

2. 設定ファイルの作成

次にCircleCIの設定ファイル(.circleci/config.yml)を作成します。
以下はパイプラインA用のGitHubリポジトリ circleci-multi-repo-pipeline-a.circleci/config.yml の例です。

version: 2.1

executors:
  default:
    docker:
      - image: cimg/base:stable

jobs:
  trigger-pipeline-b:
    executor: default
    steps:
      - run:
          name: Trigger Pipeline in Repository B
          command: |
            export CIRCLECI_PRJ_SLUG="github/t-asaeda/circleci-multi-repo-pipeline-b"
            RESPONSE=$(curl -X POST <https://circleci.com/api/v2/project/$CIRCLECI_PRJ_SLUG/pipeline> \\
            -H "Circle-Token: $CIRCLECI_API_TOKEN_B" \\
            -H "Content-Type: application/json" \\
            -d '{ "branch": "main" }')

            PIPELINE_ID=$(echo "$RESPONSE" | jq -r '.id')

            # パイプラインBが完了するまで待機
            while true; do
              STATUS=$(curl -s -H "Circle-Token: $CIRCLECI_API_TOKEN_B" \\
                "<https://circleci.com/api/v2/pipeline/${PIPELINE_ID}/workflow>" | jq -r '.items[0].status')

              if [ "$STATUS" = "success" ]; then
                echo "Pipeline succeeded."
                break
              elif [ "$STATUS" = "failed" ] || [ "$STATUS" = "error" ] || [ "$STATUS" = "failing" ]; then
                echo "Pipeline failed with status: $STATUS"
                exit 1
              else
                echo "Pipeline status: $STATUS. Waiting..."
                sleep 10
              fi
            done

workflows:
  version: 2
  trigger-workflow:
    jobs:
      - trigger-pipeline-b

ジョブを1つ、その中のステップも1つだけのシンプルな例です。
キック対象のリポジトリ circleci-multi-repo-pipeline-b に対しcurlを使用してパイプラインBをキックする CircleCI API にPOSTリクエストを送っています。詳細は次の項で説明します。

3. APIリクエストの実装

APIリクエスト処理では、以下のポイントに注意します。

  • CircleCIプロジェクトのスラッグは「Project Settings > Overview > Project slug」で確認できます
  • 発行したAPIトークンを「Circle-Token」ヘッダーにセットし、正しく認証されるようにします
  • リクエストボディに、実行したいブランチ名やカスタムパラメーター(例:「triggered_by」)をJSON形式で含めます

このサンプルでは「APIリクエストとエラー処理」「パイプラインBの結果を待つ処理」の2つに処理が分かれます。
まず前半の「APIリクエストとエラー処理」についてですが、APIリクエストが失敗した場合はキック先のパイプラインIDが返ってこないので、以下のようにエラーでジョブを終了させるようにしています。

export CIRCLECI_PRJ_SLUG="github/t-asaeda/circleci-multi-repo-pipeline-b"
RESPONSE=$(curl -X POST <https://circleci.com/api/v2/project/$CIRCLECI_PRJ_SLUG/pipeline> \\
-H "Circle-Token: $CIRCLECI_API_TOKEN_B" \\
-H "Content-Type: application/json" \\
-d '{ "branch": "main" }')

PIPELINE_ID=$(echo "$RESPONSE" | jq -r '.id')

# パイプラインIDが返ってこない場合はエラーで終了させる
if [ -z "$PIPELINE_ID" ] || [ "$PIPELINE_ID" = "null" ]; then
  echo "Failed to trigger pipeline."
  exit 1
fi

次にパイプラインBの実行結果が返ってくるまでパイプラインAのジョブを待機する処理を入れます。

# パイプラインBが完了するまで待機
while true; do
  STATUS=$(curl -s -H "Circle-Token: $CIRCLECI_API_TOKEN_B" \\
    "<https://circleci.com/api/v2/pipeline/${PIPELINE_ID}/workflow>" | jq -r '.items[0].status')

  if [ "$STATUS" = "success" ]; then
    echo "Pipeline succeeded."
    break
  elif [ "$STATUS" = "failed" ] || [ "$STATUS" = "error" ] || [ "$STATUS" = "failing" ]; then
    echo "Pipeline failed with status: $STATUS"
    exit 1
  else
    echo "Pipeline status: $STATUS. Waiting..."
    sleep 10
  fi
done

パイプラインAからパイプラインBを実行。

パイプラインAのジョブでは以下のメッセージが出力されています。
“Pipeline status: running. Waiting…” … パイプラインBの結果待ち
“Pipeline succeeded.” … パイプラインBが正常に実行された

ユースケース

実際の使用シナリオとして以下のようなケースが考えられます。

  • マイクロサービスの分割管理
    それぞれのサービスを異なるリポジトリで管理している場合、あるサービスのビルドやテストが完了したタイミングで、依存する他サービスのパイプラインを自動的にキックすることで連携テストや統合デプロイが実現できます。
    私が所属するチームで開発・運用している社内サービスでも一部のマイクロサービスをこの方式でデプロイしており、手動デプロイの繰り返しを回避し、かつヒューマンエラーの予防にも貢献しています。
  • コードの再利用
    共通ライブラリのリポジトリで変更があった場合、その変更を反映するために複数のアプリケーションリポジトリのパイプラインを一斉に実行する、といったシナリオでも利用可能です。

注意点とトラブルシューティング

実装時に気を付けるべきポイントは以下の通りです:

  • APIトークンの管理
    APIトークンはCircleCIの Contexts やCircleCIプロジェクトの環境変数で安全に管理し、config.yml に直接記述しないようにしましょう。
  • エラーレスポンスの確認
    キックが失敗しても原因がわからない場合は、HTTPステータスコードやレスポンス内容をログに出力(echo)して問題箇所の特定を行います。
  • パラメーターの整合性
    設定ファイル内で受け取るパラメーターと、APIリクエストで送信するパラメーターが一致しているかを確認し、意図しない挙動にならないようにしましょう。

まとめ

本記事では、CircleCIのジョブ内から別のリポジトリのパイプラインをキックする方法について解説しました。
今回紹介したこの方法がお役に立てれば幸いです。