CircleCIで設定ファイルを分割してpath-filteringも適用する方法

## 概要

本記事では [CircleCI](https://circleci.com/) の設定ファイルを分割する方法を紹介します。
さらに [path-filtering orb](https://circleci.com/developer/orbs/orb/circleci/path-filtering) を適用して対応するファイルを変更したときだけジョブが実行されるようにしています。
記事内に出てくる設定ファイルは次の repository に置いてあります。
https://github.com/algas/circleci-split-example

## 対象読者

- CircleCI をすでに使っている人

CircleCI 自体の基本的な使い方は本記事では説明しません。
基本的な使い方を知りたい方は公式サイトもしくは他の記事をご参照ください。

## 背景

CircleCI の設定ファイルが肥大化して困っていませんか？
現時点では CircleCI の設定ファイルを標準機能を使って分割することができないので、プロジェクトが大きくなるとともに設定ファイルも大きくなってしまいます。

公式の [FAQ](https://circleci.com/docs/2.0/faq/#is-it-possible-to-split-the-configyml-into-different-files) には次のように書かれています。

> Is it possible to split the config.yml into different files?
> Splitting config.yml into multiple files is not yet supported.

しかし、設定を工夫することで分割された設定ファイルを CircleCI に読み込ませることができます。
本記事では分割された設定ファイルを CircleCI に適用する方法を紹介します。

## ファイル構成

CircleCI の設定ファイルを複数のアプリケーションもしくはライブラリごとに分割したいものとします。
ここでは foo と bar という2つの package があると考えます。
(実際にはこれより多くの package が repository に含まれることを想定しています)

- packages:
  - foo: (your app1)
  - bar: (your app2)
- .circleci:
  - config.yml (main config)
- workflows: (splitted config files)
  - _base.yml (config template)
  - packages:
    - foo.yml (config for package "foo")
    - bar.yml (config for package "bar")

`worklows/packages/` 以下に各 package に対応した設定ファイルをそれぞれ置きます。

## 解説

CircleCI のデフォルト設定では `.circleci/config.yml` が呼び出されます。
`.circleci/config.yml` は次のような内容です。

```config.yml
version: 2.1
setup: true
orbs:
  path-filtering: circleci/path-filtering@0.0.2
jobs:
  setup:
    docker:
      - image: cimg/go:1.18.3 # yq のために golang の image を使います
    steps:
      - checkout
      - run:
          name: Install yq4 # yaml ファイルをマージするために yq4 をインストールします
          command: |
            go install github.com/mikefarah/yq/v4@latest
            yq --version
      - run:
          name: Merge config files # workflows 以下の yaml ファイルをマージします 
          command: |
            mkdir -p /tmp/workspace
            yq eval-all '. as $item ireduce ({}; . * $item )' ./workflows/_base.yml ./workflows/packages/*.yml > /tmp/workspace/merged.yml
            cat /tmp/workspace/merged.yml
      - persist_to_workspace:
          root: /tmp/workspace
          paths:
            - merged.yml

workflows:
  filter:
    jobs:
      - setup
      - path-filtering/filter:
          requires:
            - setup
          pre-steps:
            - attach_workspace:
                at: /tmp/workspace
          base-revision: main
          config-path: /tmp/workspace/merged.yml
          mapping: |
            packages/foo/.* run-foo true
            packages/bar/.* run-bar true
```

`setup` という job では設定ファイルをマージするために [yq (v4)](https://mikefarah.gitbook.io/yq/) をまずインストールします。次に、インストールした yq を使って `workflows/` 以下に置いてある設定ファイルを CI 実行時に動的に1つのファイル (`/tmp/workspace/merged.yml`) にマージして config.yml から呼び出します。
`yq eval-all` というコマンドを使って YAML をマージする方法は公式リファレンスに書かれています。
https://mikefarah.gitbook.io/yq/operators/reduce#merge-all-yaml-files-together

ここではテンプレートファイル `_base.yml` に各 package の設定ファイルをマージするようにしています。

```_base.yml
version: 2.1

parameters:

jobs:

workflows:
```

```foo.yml
parameters:
  run-foo:
    type: boolean
    default: false

jobs:
  foo:
    docker:
      - image: cimg/base:2021.04
    steps:
      - checkout
      - run:
          name: The First Foo Step
          command: |
            echo 'Hello World!'
            echo 'foo package'

workflows:
  foo:
    when: << pipeline.parameters.run-foo >>
    jobs:
      - foo
```

マージした設定ファイルは `persist_to_workspace` を使って呼び出し先の job (`path-filtering/filter`) でも使えるようにします。

`setup` の次に `path-filtering/filter` の job が実行されます。`requires` に `setup` を指定することで `setup` の後に `path-filtering/filter` が実行されるようになっています。さらに `pre-steps` に `attach_workspace` を設定してマージしたファイルを参照できるようにしています。

`path-filtering/filter` は job を実行する条件を設定するための orb です。ここでは `packages/foo` 以下のファイルが変更されたときだけ `workflows/packages/foo` に書いてある `foo` の job が実行されるように設定しています。これは `.circleci/config.yml` の `path-filtering/filter` の `mapping` に書いてあります。`bar` も `foo` と同じ設定になっています。

path-filtering orb の詳細は公式リファレンスを参照してください。
https://circleci.com/developer/orbs/orb/circleci/path-filtering

## その他の設定

- 実運用するには CircleCI と (GitHub などの) Git repository を連携します
Git repository へのコードの反映に連動して CI が実行されます。連携方法自体はここには書きません。
- CircleCI で dynamic config を有効にする必要があります
"Project Settings" > "Advanced" > "Enable dynamic config using setup workflows" を ON にします。
https://circleci.com/docs/2.0/dynamic-config/#getting-started-with-dynamic-config-in-circleci

## 本記事に書かなかったこと

- path-filtering を使わない場合の CircleCI のファイル分割の方法
[continuation orb](https://circleci.com/developer/orbs/orb/circleci/continuation) を使えばできるはず

## 参考文献

- https://circleci.com/docs/2.0/using-dynamic-configuration/#execute-specific-workflows-or-steps-based-on-which-files-are-modified

## まとめ

- CircleCI で設定ファイルを分割する方法を紹介しました。
- path-filtering を使って特定のファイルを変更したときだけ CI の job が実行される設定も解説しました。