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