この記事では conftest というツールを使って、Kubernetes のマニフェストをテストする方法を紹介します。conftest のバージョンは v0.11.0 で確認しています。
マニフェストのテスト
本番環境向けの Kubernetes のマニフェストでは様々な点を考慮する必要があります。考慮する点は環境や組織ごとに違うと思いますが、例えば以下のようなものが挙げられます。
- privileged の利用を禁止したい
- 新しい API Version を利用することを推奨する
- livenessProbe / readinessProbe の設定を推奨する
- resource request / limit の設定を推奨する
この記事では conftest というツールを使って、これらのポリシーを定義し、マニフェストがそれを満たしているかテストする方法を紹介します。
なお、PodSecurityPolicy や open-policy-agent/gatekeeper のような Kubernetes の API Server 側でポリシーをチェックする仕組みも存在します。conftest は API Server 側でチェックする仕組みと比べ、単独で動作するため CI などに組み込みやすいという特徴があります。一方 API Server 側のチェックに比べると強制力は弱いので、組み合わせて利用する形になるかと思います。
conftest とは
conftest は YAML や JSON などの構造化データに対してユニットテストを記述できるツールです。以下のような特徴があります。メインの開発者 @garethr さんは、kubetest や kubeval など他にも Kubernetes 関連のツールを作っている方です。
- YAML、JSON など構造化データに対するポリシーが書ける
- 汎用的なツールで Kubernetes に特化しているわけではない
- ポリシーはOpen Policy Agent (OPA) で使われているポリシー言語 Rego で記載する
- ポリシー自体にもユニットテストがかける (
opa test
)
- ポリシー自体にもユニットテストがかける (
- brew の tap が用意されていてインストールが簡単
- ポリシーはOCI 準拠のレジストリに格納することもできる
- ポリシーを社内で共有するといったことがやりやすい
- チェックは必須の deny, 任意の warn の 2 つに別れていて CI にも導入しやすい
- deny のチェックに引っかかったときだけ、コマンドの戻り値が 1 になる
- クラスタ上のデータは参照できないので、Ingress の host 名の衝突などはチェックできない
conftest を試してみる
インストール
Mac の Homebrew で簡単にインストールが可能です。
brew tap instrumenta/instrumenta
brew install conftest
テスト対象のマニフェスト
テスト対象のマニフェストを用意します。hello-deploy.yaml
として保存します。
apiVersion: extensions/v1beta1 # API Version が古い
kind: Deployment
metadata:
name: hello
spec:
selector:
matchLabels:
run: hello
template:
metadata:
labels:
run: hello
spec:
containers:
- image: nginx:1.17.3
name: multi
securityContext: # privileged は禁止したい
privileged: true
ポリシーを記載する
ここでは例として以下の 2 点をチェックしたいと思います。
- Deployment の API Version が最新 (
apps/v1
)であること- 必須のチェックとはせずに警告だけを出す (warn)
- privileged を利用していないこと
- 必須のチェックとする (deny)
ポリシーは Open Policy Agent (OPA) で使われているポリシー言語 Rego で記載します。Rego 自体の文法については長くなるため、この記事では扱いません。文法の詳細はOPA の公式ドキュメント をご覧ください。
conftest では deny
, warn
の 2 種類のルールを使ってチェックを行います。deny
の場合は一つでも引っかかればコマンドの戻り値が 1 (エラー)となりますが、warn
の場合は戻り値には影響しません。そのため CI などで必須チェックとしたいものは deny
、任意でチェックするものは warn
にすることができます。なお fail-on-warn
というオプションで warn
の場合も戻り値を 1 にすることもできます。
conftest のデフォルトではポリシーはチェック対象のファイルと同じディレクトリの policy/
にある全てのファイルを読み込みます。以下を policy/mypolicy.rego
として保存します。
package main
# deprecated な deployment のバージョンリスト
deprecated_deployment_version = [
"extensions/v1beta1",
"apps/v1beta1",
"apps/v1beta2"
]
# 最新の API Version が使われているかのチェック
warn[msg] {
input.kind == "Deployment"
input.apiVersion == deprecated_deployment_version[i]
msg = "最新の APIVersion apps/v1 を指定してください"
}
# privileged が使われているかのチェック
deny[msg] {
input.kind == "Deployment"
input.spec.template.spec.containers[_].securityContext.privileged == true
msg = "privileged はセキュリティ上の理由で許可されていません"
}
ポリシーでテストする
以下のように conftest test
でテスト対象を指定して、ポリシーを満たしているかテストします。
$ conftest test hello-deploy.yaml
WARN - hello-deploy.yaml - 最新の APIVersion apps/v1 を指定してください
FAIL - hello-deploy.yaml - privileged はセキュリティ上の理由で許可されていません
マニフェストを修正して、テストが通るようになったことを確認します。
$ conftest test hello-deploy.yaml
# 問題ない場合は何も表示されない
OCI レジストリでポリシーを管理する
conftest は Docker Registry など OCI 準拠のレジストリを使ってポリシーを管理することができます。複数の環境でポリシーを共有したい場合に便利でしょう。
ローカルに Docker Registry を立て試してみます。(実際に利用する場合は Docker Hub など外部のレジストリを使うことになります)
# Docker Registry をローカルに立てる
$ docker run -it --rm -p 5000:5000 registry:2.7.1
policy
ディレクトリがあるディレクトリで、policy を OCI レジストリにアップロードします。v0.11.0 では成功しても特に何も表示されません。
$ conftest push localhost:5000/mypolicy:v0.1 policy
他のディレクトリに移動してアップロードした policy ファイルを取得できるか試してみます。
# 別の temp ディレクトリに移動
$ cd $(mktemp -d)
# pull する
$ conftest pull localhost:5000/mypolicy:v0.1
# ポリシーファイルが取得できている
$ ls policy
mypolicy.rego
ポリシーの情報は conftest.toml
という設定ファイルで記述することもできます。
[[policies]]
repository = "localhost:5000/mypolicy"
tag = "v0.1"
以上のファイルを置いた状態で、conftest に --update
オプションを付けて実行すると自動的にレジストリからポリシーを取得してテストを実行してくれます。
$ conftest test --update hello-deploy.yaml
WARN - hello-deploy.yaml - 最新の APIVersion apps/v1 を指定してください
FAIL - hello-deploy.yaml - privileged はセキュリティ上の理由で許可されていません
ポリシーのユニットテストを記載する
ポリシーが複雑になってくると、ポリシー自体のユニットテストを記載したくなるかもしれません。OPA 自体の仕組みを使ってユニットテストを書くことが可能です。詳しくは公式ドキュメントのHow Do I Test Policies?をご覧ください。
$ opa test -v .
data.main.test_deprecated_deployment: PASS (491ns)
data.main.test_deprecated_deployment#01: PASS (354ns)
data.main.test_deprecated_deployment#02: PASS (348ns)
data.main.test_deprecated_deployment_allowed: PASS (253ns)
data.main.test_privileged_deployment: PASS (243ns)
data.main.test_privileged_deployment_allowed: PASS (239ns)
--------------------------------------------------------------------------------
PASS: 6/6
ルール名に test_
という prefix がついたものがテストとして扱われます。慣例的にファイル名にも _test.rego
とつける場合が多いようですが、必須ではありません。以下は先程のポリシーに対するユニットテストの例になります。
package main
test_deprecated_deployment {
warn["最新の APIVersion apps/v1 を指定してください"] with input as {"kind": "Deployment", "apiVersion": "extensions/v1beta1"}
}
test_deprecated_deployment {
warn["最新の APIVersion apps/v1 を指定してください"] with input as {"kind": "Deployment", "apiVersion": "apps/v1beta1"}
}
test_deprecated_deployment {
warn["最新の APIVersion apps/v1 を指定してください"] with input as {"kind": "Deployment", "apiVersion": "apps/v1beta2"}
}
test_deprecated_deployment_allowed {
# 最新の API Version の場合は warn が出ない
not warn["最新の APIVersion apps/v1 を指定してください"] with input as {"kind": "Deployment", "apiVersion": "apps/v1"}
}
test_privileged_deployment {
deny["privileged はセキュリティ上の理由で許可されていません"] with input as
{
"kind": "Deployment",
"spec": {
"template": {
"spec": {
"containers": [
{
"name": "app1",
"image": "nginx",
},
{
"name": "app2",
"image": "nginx",
"securityContext": { # privileged が使われている
"privileged": true,
}
}
],
}
}
}
}
}
test_privileged_deployment_allowed {
not deny["privileged はセキュリティ上の理由で許可されていません"] with input as
{
"kind": "Deployment",
"spec": {
"template": {
"spec": {
"containers": [
{
"name": "app1",
"image": "nginx",
},
{
"name": "app2",
"image": "nginx",
}
],
}
}
}
}
}