WHITEPLUS TechBlog

株式会社ホワイトプラスのエンジニアによる開発ブログです。

CircleCIのセットアップワークフローを使った差分ビルドで効率的にCIを回す

こんにちは。ホワイトプラスでエンジニアをやっている古賀です。

lenet ではCIの一つとしてCircleCI を使っており、静的解析・フォーマットチェック・ユニットテスト(フロントエンド・バックエンド)などを実行しています。

CIにpush するたびに全てのテストを実行すると、変更差分に関係のないテストが実行されてしまい、コードベースが大きくなると非効率な面が際立ちます。

昨年従量課金プランへ移行するのに合わせて、主にモノレポで採用されている差分ビルド(変更差分に関係あるテストのみ回す)を取り入れ、CIの効率化を図っています。

ここでは、CircleCIのセットアップワークフローを使って差分ビルドを行う方法を紹介します。

セットアップワークフローとは

  • 簡単に言うと、ユーザーが動的に設定ファイルを生成し、それに基づいて実行内容を決定できるというものです。
  • この機能以前も、設定ファイルに書かれた内容を条件分岐によって実行内容を変えることはできましたが、より柔軟に実行内容を設定することができます。
  • 使用するにはドキュメントに記載の通り、CircleCI Web上でEnable dynamic config using setup workflows を有効にし、.circleci/config.yml に setup:true を追加します。
  • これでセットアップワークフローが使えるようになったので、次は設定ファイルの動的生成を実装します。

設定ファイルの動的生成

  • lenet のフロントエンドアプリケーションは、ディレクトリを分けて管理されています。
  • CIでどのような処理を行うかはアプリケーション毎に異なるため、それぞれにCirlceCI設定ファイルを用意して、モジュール管理者が自由に設定できるようにしたいですね。
  • そして、変更があったコードが属するモジュールのCirlceCI設定ファイルに基づいてCIを実行する、というのがやりたいことです。
.
├── app-1
│   └── .circleci
│       └── config.yml
├── app-2
│   └── .circleci
│       └── config.yml
└── app-3
    └── .circleci
        └── config.yml
  • これは設定ファイル分割とパスフィルタリングを組み合わせたもので、CircleCI support ページで紹介されている circle-makotom/circle-advanced-setup-workflow を参考にしました。
  • やっていることは大きく3つあり、「変更差分の検知」「設定ファイルのマージ」「ワークフローの実行依頼」です。実装概要を以下に説明します。

step1. 変更差分の検知

  • プロジェクトルートにある .circleci/config.ymlworkflows を以下のように記述します。
  • これは、インラインOrbで定義している config-splitting/setup-dynamic-config ジョブを呼び出し、必要なパラメータを渡しています。
    • base-revision:ファイル差分を検出する比較元ブランチ名
    • shared-config:共通設定を定義したファイルパス
    • modules:変更差分を検出したいモジュールパス
workflows:
  setup-workflow:
    jobs:
      - config-splitting/setup-dynamic-config:
          base-revision: origin/master
          shared-config: .circleci/shared-config.yml
          modules: |
            app-1
            app-2
            app-3
  • 呼び出された config-splitting/setup-dynamic-config では、origin/master と作業ブランチの差分ファイルを git diff --name-only で取得し、modules に設定されたディレクトリパスに該当するかどうかを調べ、該当するディレクトリパスを一時ファイルに書き出します。
  • 例えば、app-1app-2 に差分ファイルがあった場合、以下のような中身を持つ一時ファイルが生成されます。
app-1
app-2

step2. 設定ファイルのマージ

  • 次に、一時ファイルに書き込まれた各ディレクトリパスの末尾に、/.circleci/config.yml を追記し、更に先ほど shared-configパラメータで指定した共通設定ファイルのパスを追記します。
app-1/.circleci/config.yml
app-2/.circleci/config.yml
.circleci/shared-config.yml
  • そして、YAMLファイルのマージツールであるyqを使って、一時ファイルに書かれたファイルをマージし、設定ファイルを生成します。
  • マージの例を見てみましょう。
# app-1/.circleci/config.yml
jobs:
  app1-job:
    docker:
      - image: cimg/base:2023.03
    steps:
      - common-echo
      - run:
          name: app1 echo
          command: echo 'app1-job'
workflows:
  app1-workflow:
    jobs:
      - app1-job


# app-2/.circleci/config.yml
jobs:
  app2-job:
    docker:
      - image: cimg/base:2023.03
    steps:
      - common-echo
      - run:
          name: app2 echo
          command: echo 'app2-job'
workflows:
  app2-workflow:
    jobs:
      - app2-job


# .circleci/shared-config.yml
version: 2.1
commands:
  common-echo:
    steps:
      - run:
          name: common-echo
          command: echo 'common-echo'


# マージ後のファイル
version: 2.1
commands:
  common-echo:
    steps:
      - run:
          name: common-echo
          command: echo 'common-echo'
jobs:
  app1-job:
    docker:
      - image: cimg/base:2023.03
    steps:
      - common-echo
      - run:
          name: app1 echo
          command: echo 'app1-job'
  app2-job:
    docker:
      - image: cimg/base:2023.03
    steps:
      - common-echo
      - run:
          name: app2 echo
          command: echo 'app2-job'
workflows:
  app1-workflow:
    jobs:
      - app1-job
  app2-workflow:
    jobs:
      - app2-job
  • ここまでで、設定ファイルの動的生成が完了しました。

step3. ワークフローの実行依頼

  • 最後に continuation という Orb を使って、CircleCI のワークフロー実行用APIに先ほど生成した設定ファイルを渡して叩きます。
  • すると、設定ファイルに記述された内容でワークフローが実行されます。
  • 以上が実装概要になります。

リソースの重複定義対策

  • yqコマンドで設定ファイルをマージする時、リソースの重複定義があった場合に統合されてしまいます。
  • 例えば workflows 定義に、同名のワークフローが複数のファイルに定義されていた場合、そのうちの1つだけが残り他の定義は無視されてしまいます。
# app-1/.circleci/config.yml
jobs:
  app1-job:
    docker:
      - image: cimg/base:2023.03
    steps:
      - run:
          name: app1 echo
          command: echo 'app1-job'
workflows:
  app1-workflow:
    jobs:
      - app1-job


# app-2/.circleci/config.yml
jobs:
  app2-job:
    docker:
      - image: cimg/base:2023.03
    steps:
      - run:
          name: app2 echo
          command: echo 'app2-job'
workflows:
  app1-workflow: # app-1/.circleci/config.yml の workflow と同じ名前
    jobs:
      - app2-job


# マージ後のファイル
jobs:
  app1-job:
    docker:
      - image: cimg/base:2023.03
    steps:
      - run:
          name: app1 echo
          command: echo 'app1-job'
  app2-job:
    docker:
      - image: cimg/base:2023.03
    steps:
      - run:
          name: app2 echo
          command: echo 'app2-job'
workflows:
  app1-workflow: # app-2/.circleci/config.yml のワークフローだけになってしまった
    jobs:
      - app2-job
  • これでは app-1/.circleci/config.yml で定義したワークフローが実行されなくなってしまいますね。
  • モジュール毎の設定ファイルが増えるほど、重複定義は起こりやすくなります。
  • これを防ぐために、lenet では重複チェック用のシェルスクリプトを作っています。
duplicateDefinition=$(xargs -a "$1" yq '. | to_entries[] | select(.value | type == "object") | .value | keys[]' | sort | uniq -d)

if [ -n "$duplicateDefinition" ]; then
  echo "Error: duplicate definition found: $duplicateDefinition"
  exit 1
fi

echo "No duplicates found."
  • step2の設定ファイルマージを行う前のファイルパスを $1 に渡してスクリプトを実行し、yqコマンドでYAMLファイルをパースします。
  • 重複チェック対象にするのは、workflowsjobs などの定型キーの子要素に当たるユーザ定義のリソース名で、同名のリソースが複数定義されていた場合にエラーメッセージを出力し処理を終了させています。
  • これで重複定義の心配なく、設定ファイルを書いていけるようになりました。

最後に

ホワイトプラスでは一緒に働く仲間を募集しています。 カジュアル面談もできますので、お気軽にご応募ください!