概要
業務でCircleCIを使うことになったのだが、設定ファイルをいい感じに分割する方法が見つからなかったので自作したという話。前置きに興味がない人はYAML Bundlerの紹介まで読み飛ばしてOK。
問題提起
CircleCIの設定はconfig.yml
に記述するのだが、モノレポで開発している場合は肥大化しがち。このような場合にはDynamic Configurationという仕組みが使える。このDynamic Configurationというのは以下のような二段構えになっていて、
-
config.yml
の記述に従い、第二段階用の設定ファイル(ファイル名は任意だがこの記事ではmain.yml
)の生成・パラメータの受け渡しを行う - 第一段階で生成した設定ファイルとパラメータに従って必要な処理を実行
第二段階用の設定ファイルは任意の方法で生成してよいから、好きなだけファイルを分割しておいてこの段階で統合すればよいという寸法。ただし任意の方法といっても、以下の二つが定番パターンのようだ。それぞれのメリット・デメリットを挙げる。
ciercleci config pack
- メリット
- circleci/circleci-cliなどのdockerイメージにインストール済みのコマンドなので、CIの中で使いやすい1
- デメリット
yq
- メリット
- cimg/baseなどのdockerイメージにインストール済みのコマンドなので、CIの中で使いやすい
- デメリット
- 自由度は高いがyqコマンド自体に習熟する必要がある
YAML Bundlerの紹介
この記事では第三の選択肢としてYaml Bundlerというorbを紹介する。これを使うとYAMLファイルの中で!include
というタグを使い、他のYAMLファイルの内容を取り込むことができる。YAMLの書き方が分かる人ならすぐ使えるし、自由度もそれなりに高いはず。
使い方
go・pyというディレクトリでそれぞれアプリケーションを開発中のモノレポを想定する。YAML以外のコードもここで見られる。
.
├── .circleci
│ ├── config.yml
│ ├── main.yml
│ ├── go.yml
│ └── py.yml
├── go
│ ├── go.mod
│ ├── main.go
│ └── main_test.go
└── py
├── main.py
└── test.py
config.yml
はこんな感じ。yaml-bundler/bundleが!include
タグを処理するcommand。
version: 2.1
setup: true
orbs:
yaml-bundler: dr666m1/yaml-bundler@0.0.5
continuation: circleci/continuation@0.3.1
jobs:
setup:
executor: yaml-bundler/default
steps:
- checkout
- yaml-bundler/bundle: # !includeタグを処理して上書き
filepath: .circleci/main.yml
- continuation/continue:
configuration_path: .circleci/main.yml
workflows:
setup:
jobs:
- setup
main.yml
はこんな感じ。filepathとjsonpath3を指定して他のYAMLファイルを読み込んでいる。
version: 2.1
jobs: !include
- filepath: ./go.yml
jsonpath: $.jobs
- filepath: ./py.yml
jsonpath: $.jobs
workflows: !include
- filepath: ./go.yml
jsonpath: $.workflows
- filepath: ./py.yml
jsonpath: $.workflows
取り込まれる側のgo.yml
py.yml
は以下の通り。YAMLファイルとして有効な形式であれば、どんなファイルでも取り込み可能4。
jobs:
go-test:
docker:
- image: cimg/go:1.18
steps:
- checkout
- run:
working_directory: go
command: go test
workflows:
go-test:
jobs:
- go-test
jobs:
py-test:
docker:
- image: cimg/python:3.11
steps:
- checkout
- run:
command: python -m unittest ./py/test.py
workflows:
py-test:
jobs:
- py-test
最終的に!include
タグの処理が完了したmain.yml
はこんな感じ。
version: 2.1
jobs:
go-test:
docker:
- image: cimg/go:1.18
steps:
- checkout
- run:
working_directory: go
command: go test
py-test:
docker:
- image: cimg/python:3.11
steps:
- checkout
- run:
command: python -m unittest ./py/test.py
workflows:
go-test:
jobs:
- go-test
py-test:
jobs:
- py-test
基本的な使い方は以上!
差分があるディレクトリに応じてworkflowを実行する応用例もあった方がよい気がするから、ここにコードを置いておく。
また、ここまでで触れられなかった!include
タグの詳しい使い方も折り畳み部分に書いておく。
!includeタグの詳しい使い方
# main.yml (before)
version: !include # filepath・jsonpathを指定する最も基本的な使い方
filepath: ./constants.yml
jsonpath: '$.version'
executors: !include ./executors.yml # ファイル全体を読み込む場合は一行で書ける
jobs:
main-job:
executor: my-executor
# 配列で指定すると結果が統合される(配列同士だと配列になる)
steps: !include
- filepath: ./steps1.yml
- filepath: ./steps2.yml
sub-job:
executor: my-executor
steps:
- run: echo 'this is only step of sub-job'
# 配列で指定すると結果が統合される(map同士だとmapになる)
workflows: !include
- filepath: ./main-workflow.yml
- filepath: ./sub-workflow.yml
---
# constants.yml
version: 2.1
---
# executors.yml
my-executor:
docker:
- image: cimg/base:current
---
# steps1.yml
- run: echo "this is first step of steps1.yml"
- run: echo "this is second step of steps1.yml"
---
# steps2.yml
- run: echo "this is only step of steps2.yml"
---
# main-workflow.yml
main-workflow:
jobs:
- main-job
---
# sub-workflow.yml
sub-workflow:
jobs:
- sub-job
---
# main.yml (after)
version: 2.1
jobs:
main-job:
executor: my-executor
# 配列の配列ではなくフラットな配列になる
steps:
- run: echo "this is first step of steps1.yml"
- run: echo "this is second step of steps1.yml"
- run: echo "this is only step of steps2.yml"
sub-job:
executor: my-executor
steps:
- run: echo "this is only step of sub-job"
workflows:
main-workflow:
jobs:
- main-job
sub-workflow:
jobs:
- sub-job
最後に
最後まで読んだけど期待していたものと違った、という方はこちらも読んでみてください。
自分より先に素敵なorbを作っていた方の記事です。
-
circleci/circleci-cli orbを使うのが公式推奨の方法だと思う。 ↩
-
たとえば単一のリポジトリで複数のアプリケーションを開発しているときに、app1のcommands・jobs・workflowsをapp1.ymlというファイルで管理したい、という要望には応えられないと思われ。 ↩
-
jsonpath-ngというパッケージを利用しているので、詳しい説明はそちらを見るとよさげ。 ↩