Unit Test好きにはたまらない!Conftestで設定ファイルのテストをしてみた
KubeConでOpen Policy Agent関連の発表を追っていたところ、面白そうなプレゼンを発見しました!
Unit Testing Your Kubernetes Configuration with Open Policy Agent
僕はUnit Test大好き人間なのでUnit Testと聞くだけで興奮するのですが、それにさらにOpen Policy Agentが関わっているときたら放っておけません!
もしOpen Policy Agentについて初めて聞いたという方は以下の記事でも紹介しているので興味があればぜひ!
Conftest
ConftestはYAMLあるいはJSONで定義された設定ファイルに対してテストを書けるというツールです。
面白いのは、テストに使うのがOpen Policy AgentのRegoというポリシー用の言語だという点です。上に紹介した記事にも書いていますが、さらに気になる人は公式サイトでもRegoについて詳しく書かれているのでぜひ読んでみてください。
インストール
Macにインストール。僕はbrewを使いましたが、様々な方法でインストールできますので、READMEをチェックしましょう!
brew tap instrumenta/instrumenta brew install conftest
インストールはこれで完了です!
テストを書く
ではテストを書いてみましょう!KubernetesのYAMLがテストできるってところがハイライトされがちですけどYAML, JSONならなんでもOKです!
ということでせっかくなのでチュートリアルには無いCircleCIのYAMLを対象にしました!
テストを書くにあたって重要なのはRegoという言語を使うということです。
Regoはポリシーに特化した言語で、ちょっとトリッキーなところもありますが、宣言的で読みやすい言語だと思います。
例えばCircleCIの以下のようなconfigがあったとします。
version: 2.0 jobs: build: docker: - image: circleci/buildpack-deps steps: - checkout test: docker: - image: kenfdev/golang:1.12 steps: - checkout workflows: version: 2 build_and_test: jobs: - build - test
これを.circleci/config.yml
に置きます。
conftest
を導入すると何ができるかというと、このYAMLの記述内容に対してポリシーを書くことができます。
具体的にはこんなポリシーを割り当てることができます
version: 2.1
以上しか使っちゃいけない- CircleCI純正のイメージしか使っちゃいけない
- コンテナのイメージに
latest
を使っちゃいけない
などなどです。
実際にこのポリシーを適用したテストを順番に書いてみます。
conftest
はデフォルトでpolicy
ディレクトリ配下の.rego
ファイルを見てくれる様なので、circleci.rego
というファイルを作ります。
touch policy/circleci.rego
この中にルールを書いていきます。
package
を指定
Regoのpackage
はデフォルトでmain
が使われます。これは--namespace
オプションで変更可能とのこと。
なので、package
はmain
にしましょう。
package main
version: 2.1
以上しか使っちゃいけない
「CircleCIのOrbs機能を使いたいから、Versionを必ず2.1以上にしたい」というポリシーがあるかもしれません。その場合に書けるポリシーは以下のとおり。
deny[msg] { input.version < 2.1 msg = "Use version 2.1 or higher" }
deny
になる条件を探すので、「どうなっているとルール違反か?」というマインドが必要です。input
というのはRegoで特別な変数で、conftest
の場合ここにテスト対象のYAMLあるいはJSONが入っています。なのでinput.version
でversion
にアクセスできます。
conftest
を実行してみます!
$ conftest test config.yml config.yml Use version 2.1 or higher
2.0
をconfig.yml
に指定しているので怒られてますね!
CircleCI純正のイメージしか使っちゃいけない
こんなユースケースがあるかどうかはさておき、使うコンテナのimage
にポリシーを設けたい場合の例になります。
必ずimage
がcircleci/
から始まっているかどうかをチェックしてみます!
deny[msg] { docker_images := input.jobs[_].docker[_].image not startswith(docker_images, "circleci/") msg = "Only use official CircleCI images" }
Regoのちょっとトリッキーな書き方をさっそく取り入れてみました。
docker_images := input.jobs[_].docker[_].image
これを言語化すると「jobs
の中に含まれる複数のキー(build
, test
)の中にあるdocker
という配列の中のimage
キーの値」になります。
こうやって書くことでOPAがよしなに順番に拾ってくれます。そして、それらがcircleci/
で始まってなかったらアウトにします。
not startswith(docker_images, "circleci/")
ここで使っているstartswith
はRegoに予め組み込まれているビルトイン関数です。OPAのドキュメントにも載っているので、どういうものがあるかチェックしておくといざとなったら使えて便利です!
「circleci/
で始まっていないもの」を探すので、先頭にnot
をつけている点もお忘れなく!
実行してみます!
$ conftest test config.yml config.yml Only use official CircleCI images Use version 2.1 or higher
kenfdev/golang:1.12
を使っているので怒られていますね!
コンテナのイメージにlatest
を使っちゃいけない
またまたimage
に関連するポリシーです。安定したCIを回すためにimage
に必ずタグを指定するというポリシーが必要になるかもしれません。latest
を使っていたらルール違反にしましょう。
deny[msg] { docker_images := input.jobs[_].docker[_].image tag_is_latest(split(docker_images, ":")) msg = "Do not use `latest` container image tags" } # helpers tag_is_latest(images) { count(images) < 2 } tag_is_latest([_, tag]) { tag == "latest" }
docker_images
はさっきと同じですが、2行目がまたちょっとトリッキーです。latest
を検知する方法は以下の2つの観点にしています。
:
で分割した場合に要素が2個より小さい:
で分割して末端側(tag側)の値がlatest
になっている
いずれかにヒットしたらルール違反にします!ここでポイントなのは、「いずれか」ということ。つまりOR条件になります。Regoのルール内の行は全てANDになり、同じルール名のルール(この場合tag_is_latest
)を並べるとORになるという特性があります。これはIncremental Definitionsと言って公式ドキュメントでも説明があるので要チェックです。
また、僕が以前書いた記事にもANDとOR、そしてルール内から別のルールを呼び出すことについて触れていますので興味があればぜひ参考にしてみてください!
上のやり方ではtag_is_latest
というルールを別途2つ作って、それぞれの判定条件を書きました。
:
で分割した場合に要素が2個より小さい
tag_is_latest(images) { count(images) < 2 }
images
にはsplit(docker_images, ":")
で分割された値が入ってきます。今回の例であれば下のようになります。
["circleci/buildpack-deps"] # 要素数は1つ ["kenfdev/golang", "1.12"] # 要素数は2つ
タグが無い場合のデフォルトがlatest
になるので、分割された要素の数が2より小さいかどうかをチェックします。
:
で分割して末端側(tag側)の値がlatest
になっている
tag_is_latest([_, tag]) { tag == "latest" }
また新たな記法が引数の場所([_, tag]
)に登場しました。これは、いい感じに配列の値を分割代入させる書き方です。
例えば下のように書くとイメージしやすいと思います。
[name, tag] = ["circleci/buildpack-deps", "latest"]
name
にはcircleci/buildpack-deps
が対応し、tag
にはlatest
が対応して代入されるようなイメージです。でも、name
には今回は特に興味がないので、上の例では[_, tag]
と書いています。そして、ルール内ではtag == "latest"
というように評価することで、tag
がlatest
かどうかチェックすることができます。
それでは、テストを実行してみましょう!
$ conftest test config.yml config.yml Only use official CircleCI images Do not use `latest` container image tags Use version 2.1 or higher
image: circleci/buildpack-deps
がタグを指定していないので、ちゃんと怒られていますね!ちなみにimage: circleci/buildpack-deps:latest
としてもちゃんと怒られます。
conftest
でテストができました!念の為ポリシーの全体像も載せておきます。
# .circleci/policy/circleci.rego package main deny[msg] { input.version < 2.1 msg = "Use version 2.1 or higher" } deny[msg] { docker_images := input.jobs[_].docker[_].image not startswith(docker_images, "circleci/") msg = "Only use official CircleCI images" } deny[msg] { docker_images := input.jobs[_].docker[_].image tag_is_latest(split(docker_images, ":")) msg = "Do not use `latest` container image tags" } # helpers tag_is_latest(images) { count(images) < 2 } tag_is_latest([_, tag]) { tag == "latest" }
config.ymlの修正
怒られっぱなしなのもちょっと悲しいので、config.yml
を直しましょう!
全てのポリシーを満たすために下のように修正しました。
version: 2.1 # 2.1に変更 jobs: build: docker: - image: circleci/buildpack-deps:jessie # latest使うのやめた steps: - checkout test: docker: - image: circleci/golang:1.12 # circleciのイメージに変更した steps: - checkout workflows: version: 2 build_and_test: jobs: - build - test
それではテスト実行です!
$ conftest test config.yml
config.yml
ちょっとわかりづらいんですけど、何も怒られなくなりましたね!
いい感じにconftest
で設定ファイルに対してテストが書けることがわかりました。Regoへの慣れは必要ですが、今後Open Policy Agentも様々な場所で使われていくことになりそうなので、Regoは学んでおいて損は無いんじゃないかなと思っています。
今回のコードは以下のリポジトリに置いてます。
まとめ
- YAML, JSONの設定ファイルに対してテストが書ける
conftest
を試した conftest
は宣言的にOpen Policy AgentのRegoを使ってテストを書ける- どこがルール違反になっているか、場所も教えてくれるとさらにうれしい
- 全部ポリシーをパスしてたらもうちょっとうれしい出力がほしい(笑)
- 今後に期待したい!!!
参考
Conftestを使ったテストの例 github.com
Open Policy Agentのチュートリアル www.openpolicyagent.org