巨大な .circleci/config.yml を分割した話

こんにちは。 SRE の @suzuki-shunsuke です。

6000 行を超える巨大な .circleci/config.yml を分割してメンテナンス性を改善した話を紹介します。

背景

我々 SRE は日々 Developer Productivity の改善に取り組んでいます。 その取り組みの一環で Developer の方から直接フィードバックをもらう機会がありました。 その中で Monorepo の .circleci/config.yml が大きすぎて修正するのが大変という意見をもらいました。 弊社では様々なサービスを一つのリポジトリで管理する Monorepo というアーキテクチャを採用しており、 CircleCI で全てのサービスのテストやビルド・デプロイなどを行っています。 それ故に .circleci/config.yml は 6000 行を超えるものになっており、とても見通しが悪く、新しくサービスを追加する際などもどうすればいいのか分かりにくいという問題がありました。 そこで .circleci/config.yml を分割し、改善に取り組む事にしました。

対応方針

CircleCI は設定ファイルの分割をサポートしていません。 なので分割した設定ファイルをコマンドによってマージして .circleci/config.yml を生成し、分割したファイルと .circleci/config.yml を両方コミットするという戦略を取ることにしました。

circleci config pack

.circleci/config.yml をマージするツールとしては公式の circleci config pack コマンドがあります。 公式のツールなので安心感があります。

https://circleci.com/docs/2.0/local-cli/#packing-a-config

このツールを使うのも当然有力ですが、ファイル名やディレクトリ構成などの制約が結構強いです。

$ circleci config pack <src>

のように一つのディレクトリ及びその子ディレクトリに設定ファイルを集約させる形になります。 また、 設定ファイルのパスがそのまま command や job の名前に反映されます。 これは制約に従えばコードの記述量を減らせるメリットがある反面、ディレクトリ構成などがかなり制限されます。 新しく設定ファイルを書く場合はまだいいかもしれませんが、今回は既存の .circleci/config.yml と等価な(つまり job 名などが変わらない) 設定ファイルを生成したかったため、 余計に厳しいと感じました。

また、 circleci config pack だと workflow の job のリストを分割出来ません。 弊社の monorepo だと同じ workflow ですべてのサービスの build や test の job が実行されているため、 workflow の jobs の定義も結構大きくて見通しが悪く、分割したいと思いました。

.circleci/config.yml がそこまで大きくない場合はいいかもしれませんが、 6000 行規模で様々なサービスの job や command などがあるコードの分割には些か厳しいと思いました。

circleci-config-merge

circleci config pack の制約が弊社のニーズに合わないと感じたので、マージするツールを OSS として作りました。

https://github.com/suzuki-shunsuke/circleci-config-merge

このツールはマージするファイルの一覧を引数として渡し、マージされた .circleci/config.yml を標準出力として出力します。

$ circleci-config-merge merge <file> <file> ... > .circleci/config.yml

特にディレクトリ構成やファイル名の制約はないのと、 workflow の job のリストを分割できます。 Go のバイナリを GitHub Releases からダウンロードしてくれば使えます。 分割されたファイルのフォーマットは .circleci/config.yml と同じです。

サンプル

以降で circleci-config-merge を使った場合のディレクトリ構成、 .circleci/config.yml の更新方法などを 説明しますが、サンプルリポジトリも用意したのでそちらも参照してください。

https://github.com/suzuki-shunsuke/example-circleci-config-merge

ディレクトリ構成

各サービスのディレクトリ直下に circleci というディレクトリを作ってそこに YAML を置きました。

.circleci/base-config.yml # どのサービスにも属さないものとかは .circleci/*.yml とかに置いておく
service-1/
  circleci/
    config.yml
service-2/
  circleci/
    workflow.yml # ファイルをどう分割するかは自由
    jobs.yml
...

.circleci/config.yml の更新方法

.circleci/config.yml を更新したくなったら、分割したファイルを更新して make circleci を実行します。

$ make circleci

このコマンドによって分割されたファイルの一覧を取得し、 circleci-config-merge によって .circleci/config.yml を生成します。

そして .circleci/config.yml と分割したファイルをコミットします。

CI

CI で .circleci/config.yml をテストします。

  • .circleci/config.yml を直接更新していないか
  • .circleci/config.yml をコミットし忘れてないか
  • 分割したファイルをコミットし忘れてないか

CI の中で circleci-config-merge を実行して生成した結果と .circleci/config.ymlYAML として比較しています。 比較には dyff というツールを使っています。

circleci-config-merge は workflow の job のリストを job 名でソートする

.circleci/config.yml の workflow の job のリストの順序には意味はありません。 しかし、 順序が違うと YAML の比較が難しくなります。 そこで circleci-config-merge は workflow の job のリストを job 名でソートしています。

結果

.circleci/config.yml をサービスごとに分割した結果、60 個ほどのファイルに分割されました。 サービスごとに分割されているので、各サービスの developer は分割されたものだけを基本的に見ればよく、 かなり見通しが良くなりました。また新しいサービスを追加する際も既存の設定のコピペがしやすくなりました。 設定の scaffold も今はありませんが、将来的にはできるかもしれません。

むすび

SRE が Developer Productivity の改善に取り組んでいる事例として、 circleci-config-merge を使った .circleci/config.yml の分割を紹介しました。 今回 Developer から直接フィードバックをもらうことで Developer が求めていることを実現できたのは非常に良かったなと思います。