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について初めて聞いたという方は以下の記事でも紹介しているので興味があればぜひ!

kenfdev.hateblo.jp

Conftest

ConftestはYAMLあるいはJSONで定義された設定ファイルに対してテストを書けるというツールです。

面白いのは、テストに使うのがOpen Policy AgentのRegoというポリシー用の言語だという点です。上に紹介した記事にも書いていますが、さらに気になる人は公式サイトでもRegoについて詳しく書かれているのでぜひ読んでみてください。

www.openpolicyagent.org

インストール

Macにインストール。僕はbrewを使いましたが、様々な方法でインストールできますので、READMEをチェックしましょう!

brew tap instrumenta/instrumenta
brew install conftest

インストールはこれで完了です!

テストを書く

ではテストを書いてみましょう!KubernetesYAMLがテストできるってところがハイライトされがちですけど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オプションで変更可能とのこと。

なので、packagemainにしましょう。

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.versionversionにアクセスできます。

conftestを実行してみます!

$ conftest test config.yml
config.yml
   Use version 2.1 or higher

2.0config.ymlに指定しているので怒られてますね!

CircleCI純正のイメージしか使っちゃいけない

こんなユースケースがあるかどうかはさておき、使うコンテナのimageにポリシーを設けたい場合の例になります。

必ずimagecircleci/から始まっているかどうかをチェックしてみます!

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のドキュメントにも載っているので、どういうものがあるかチェックしておくといざとなったら使えて便利です!

www.openpolicyagent.org

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と言って公式ドキュメントでも説明があるので要チェックです。

www.openpolicyagent.org

また、僕が以前書いた記事にもANDとOR、そしてルール内から別のルールを呼び出すことについて触れていますので興味があればぜひ参考にしてみてください!

kenfdev.hateblo.jp

上のやり方では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"というように評価することで、taglatestかどうかチェックすることができます。

それでは、テストを実行してみましょう!

$ 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は学んでおいて損は無いんじゃないかなと思っています。

今回のコードは以下のリポジトリに置いてます。

github.com

まとめ

  • YAML, JSONの設定ファイルに対してテストが書けるconftestを試した
  • conftestは宣言的にOpen Policy AgentのRegoを使ってテストを書ける
  • どこがルール違反になっているか、場所も教えてくれるとさらにうれしい
  • 全部ポリシーをパスしてたらもうちょっとうれしい出力がほしい(笑)
  • 今後に期待したい!!!

参考

Conftestを使ったテストの例 github.com

Open Policy Agentのチュートリアル www.openpolicyagent.org