概要
KubernetesのマニフェストはYAMLで記載し、gitなどにコミットして管理する事が多いです。
その際に下記のような観点での事前テストを実施する事を考えてみました。
- マニフェストがYAMLとして正しいか
- マニフェストがチームで定めたルールにしたがっているか?
- 特定のラベルがついているか?またその値が正しいか?
ツール調査
要件を満たすために使えそうなOSSのツールを調べてみました。
Copper
https://copper.sh/
Cloud 66という会社が作ったマニフェストのテストツールです。
試食
Getting Startedを参考に試しに実行してみました。
ドキュメントには引数が-rules
などと書かれていますが、正しくは --rules
です
ルールはこのように書きます。
jsonpathで興味のあるリソースをフィルタして、assertを書くと言うようなもののようです。
rule ApiV1Only ensure {
fetch("$.apiVersion").first == "v1" // we only allow the use of v1 API functions
}
$ copper check --rules ../rules/v10only.cop --file kube-state-metrics.yaml
check --rules ../rules/v10only.cop --file kube-state-metrics.yaml
Validating part 0
ApiV1Only - PASS
Validating part 1
ApiV1Only - FAIL
Validating part 2
ApiV1Only - FAIL
Validating part 3
ApiV1Only - PASS
Validating part 4
ApiV1Only - FAIL
1つのファイルに複数のリソースが入っていると上記のように出力されます。リソース名を出して欲しいと思いました。
fetchに引っかからなければその後のassertも実行されないようです。
リソースを限定してテストする
DeploymentでapiVersionがこれというような指定もできます。
rule always ensure{
fetch("$[?(@['kind'] == 'Deployment')].spec.template.spec.containers..imagePullPolicy").first == "IfNotPresent"
}
上の例は複数あるimagePullPolicyのうち始めのものだけを比較していますが、本当は全ての要素を比較したいです。forEachみたいなものが見当たらず、よい書き方がわかりませんでした。
特定のラベルが付いているか?
個数を調べることで存在がわかるようです。
rule existLabel ensure {
fetch("$.metadata.labels['addonmanager.kubernetes.io/mode']")
.count == 1
}
ただし中身の検証をしようとすると
rule labelContents ensure {
fetch("$.metadata.labels['addonmanager.kubernetes.io/mode']").first == "Reconcile"
}
存在しないリソースの処理の際にランタイムエラーを起こしてしまいました。
$ copper check --rules ./rules/nolatest.cop --file kube-state-metrics.yaml
check --rules ./rules/nolatest.cop --file kube-state-metrics.yaml
Validating part 0
existLabel - PASS
labelContents - PASS
Validating part 1
existLabel - PASS
labelContents - PASS
Validating part 2
existLabel - PASS
labelContents - PASS
Validating part 3
existLabel - PASS
labelContents - PASS
Validating part 4
Runtime error: cannot call first on an empty array
しかし、テスト自体は成功とみなされるようです。エラーハンドリングに改良が必要そうです。
その他 気になった所
- && が使えると書いてあるが実際にはsyntax errorする
- 論理演算にかっこが使えない
- andが短絡評価にならないのでガードとして使えない
- fetchを変数に入れられない
ruby製でDSLを使っていると言う事で、実質rubyがフルセットで使えるのかと思ってソースコードをみたところ
copper/copper.treetop at master · cloud66/copper · GitHub
普通に独自言語のパーサーから作っているようで、柔軟な処理を書ける雰囲気ではありませんでした。(テスト記述言語としてそう言うことはやらせないというポリシーのようです。)
まとめ
独自言語でかつまだ完成度が低いので試行錯誤が辛いという印象です。
まだ発展途中のプロダクトのため、PRやIssueを積極的に行うことで使い物になるかもしれません。
Kubeval
kubernetesのマニフェストのYAMLファイルがYAMLとして正しいかと、KubernetesのAPIに準拠した内容かどうかを調べるツールです。
試食
wget https://github.com/garethr/kubeval/releases/download/0.6.0/kubeval-darwin-amd64.tar.gz
tar xf kubeval-darwin-amd64.tar.gz
cp kubeval /usr/local/bin
指示に従いcurlしました。 go getでもインストール出来ると思います。
OpenAPIのSchemaの取得先
--schema-location string Base URL used to download schemas. Can also be specified with the environment variable KUBEVAL_SCHEMA_LOCATION (default "https://raw.githubusercontent.com/garethr")
デフォルトはgithubにあるスキーマを見に行くようです。
しかし、v1.9.3までで更新が止まっています。
スキーマはローカルにおいてfile:// で参照することもできるため、ターゲットとなるKubernetesのバージョンのスキーマをローカルに置いて実行することを考えてみます。
同作者のこれ(GitHub - garethr/openapi2jsonschema: Convert OpenAPI definitions into JSON schemas for all types in the API)を使うとswagger.jsonをjsonschemaに変換することができるようです。
下記のように実行すると Kubernetesのリポジトリからschemaを作ることができました。
openapi2jsonschema https://raw.githubusercontent.com/kubernetes/kubernetes/master/api/openapi-spec/swagger.json
docker imageも用意されていました。 https://hub.docker.com/r/garethr/openapi2jsonschema/
openapi2jsonschemaはいくつかオプションがあります。
- standalone
- 定義を参照せずに全て展開するオプション
- 指定するとすごく遅くなりました
- strict
- 存在しないプロパティがあった場合にエラーとなるようなschemaを出力するオプション
- 指定すると終わらなくなった・・・
デフォルトはstandaloneのjsonschemaを参照しているようでしたが、standaloneではないjsonschemaでも動作する事を確認しました。
動作の様子
上記手法で生成したschemaを利用してコマンドを実行してみました。
$ kubeval manifests/* --schema-location file:///Users/inajob/work/_tmp/schema
The document manifests/kube-state-metrics.yaml contains a valid ServiceAccount
The document manifests/kube-state-metrics.yaml contains a valid ClusterRole
The document manifests/kube-state-metrics.yaml contains a valid ClusterRoleBinding
The document manifests/kube-state-metrics.yaml contains a valid Service
The document manifests/kube-state-metrics.yaml contains an invalid Deployment
---> spec.template.spec.containers.0.readinessProbe.httpGet.port: Invalid type. Expected: string, given: integer
The document manifests/kubernetes-dashboard.yaml contains a valid ServiceAccount
The document manifests/kubernetes-dashboard.yaml contains a valid ClusterRoleBinding
The document manifests/kubernetes-dashboard.yaml contains a valid Role
The document manifests/kubernetes-dashboard.yaml contains a valid RoleBinding
The document manifests/kubernetes-dashboard.yaml contains an invalid Service
---> spec.ports.0.targetPort: Invalid type. Expected: string, given: integer
The document manifests/kubernetes-dashboard.yaml contains an invalid Deployment
---> spec.template.spec.containers.0.livenessProbe.httpGet.port: Invalid type. Expected: string, given: integer
The document manifests/prometheus.yaml contains a valid ConfigMap
The document manifests/prometheus.yaml contains a valid ConfigMap
The document manifests/prometheus.yaml contains a valid ServiceAccount
The document manifests/prometheus.yaml contains a valid ClusterRole
The document manifests/prometheus.yaml contains a valid ClusterRoleBinding
The document manifests/prometheus.yaml contains an invalid Service
---> spec.ports.0.targetPort: Invalid type. Expected: string, given: integer
The document manifests/prometheus.yaml contains an invalid Deployment
---> spec.template.spec.containers.0.livenessProbe.httpGet.port: Invalid type. Expected: string, given: integer
---> spec.template.spec.containers.0.readinessProbe.httpGet.port: Invalid type. Expected: string, given: integer
The document _output/prometheus.yaml contains an invalid Ingress
---> spec.rules.0.http.paths.0.backend.servicePort: Invalid type. Expected: string, given: integer
make: *** [kubeval] Error 1
それっぽい出力が得られました。ポートの指定が数値になっているが文字列であるべき というエラーが出ていることがわかります。
まとめ
kubevalを使うとyamlとして正しいかに加えてKubernetesの定義するOpenAPIの仕様を満たしたmanifestかどうかをチェックできます。
検証にはopenapi2jsonschemaで変換したjsonschemaを使用しています。
デフォルトではGitHub - garethr/kubernetes-json-schema: A set of JSON schemas for various Kubernetes versions, extracted from the OpenAPI definitions を見に行くきますが、ここは1.9.3で更新が止まっています。そのためそれ以上のバージョンで検証するためには、自前でjsonschemaを用意する必要があります。
Kubetest
Kubernetesのマニフェストのユニットテストを実行するためのツールです。
試食
インストール
goなので簡単です。
$ go get github.com/garethr/kubetest
# github.com/garethr/skyhook
../../go-workspace/src/github.com/garethr/skyhook/skyhook.go:30:28: multiple-value skylark.ExecFile() in single-value context
../../go-workspace/src/github.com/garethr/skyhook/skyhook.go:46:24: not enough arguments in call to syntax.Parse
have (string, []byte)
want (string, interface {}, syntax.Mode)
../../go-workspace/src/github.com/garethr/skyhook/skyhook.go:62:14: thread.Push undefined (type *skylark.Thread has no field or method Push)
../../go-workspace/src/github.com/garethr/skyhook/skyhook.go:63:14: thread.Pop undefined (type *skylark.Thread has no field or method Pop)
とはいきませんでした・・、、skyhookがおかしい?
読む限り関数のインターフェースがあっていないようです。
付属のMakefileをつかってビルドすることに
$ make darwin
(略)
vendor/github.com/garethr/skyhook/skyhook.go:30:28: multiple-value skylark.ExecFile() in single-value context
vendor/github.com/garethr/skyhook/skyhook.go:46:24: not enough arguments in call to syntax.Parse
have (string, []byte)
want (string, interface {}, syntax.Mode)
vendor/github.com/garethr/skyhook/skyhook.go:62:14: thread.Push undefined (type *skylark.Thread has no field or method Push)
vendor/github.com/garethr/skyhook/skyhook.go:63:14: thread.Pop undefined (type *skylark.Thread has no field or method Pop)
make: *** [darwin] Error 2
それでもエラーです。
公式のtravisもエラーになっているからHEADは今ビルドできないようにみえます。
Travis CI - Test and Deploy Your Code with Confidence
Makeだとglide updateするようですが、これだと最新のパッケージがインストールされてしまいます。そして最新のものだと関数のインターフェースが違いコンパイルできません。
glide.lockに記載されたパッケージを利用してビルドする必要があります。
下記手順でインストール出来ました。
$ go get github.com/garethr/kubetest
(失敗する)
$ glide install
(略)
$ go get github.com/garethr/kubetest
(うまくいく)
コンパイル済みのバイナリもあります。素直にこちらを使うのもよいでしょう。
Releases · garethr/kubetest · GitHub
実行してみる
./tests
ディレクトリにテストを入れると自動で読み込みます。(指定することもできます)
ルールはこのように記述します。
def test_for_latest_image():
if spec["kind"] == "ReplicationController":
for container in spec["spec"]["template"]["spec"]["containers"]:
tag = container["image"].split(":")[-1]
assert_not_equal(tag, "latest", "should not use latest images")
test_for_latest_image()
$ kubetest _output/kube-state-metrics.yaml
FATA .latest.sky.swp:1:10: got float literal, want newline
拡張子関係なくtestsのファイルを読むらしい・・vimの一時ファイルを動かそうとしている・・消してやり直します。
$ kubetest _output/kube-state-metrics.yaml
WARN _output/kube-state-metrics.yaml "map[k8s-app:"kube-state-metrics"]" does not contain "addonmanager.kubernetes.io/mode"
それっぽく動いています。
しかしどのmanifestを処理しているか教えてくれない・・
どのリソースを処理しているかわかるようにする
1つのファイルに複数のリソースが定義してあるときは何がエラーなのかわかりにくいので、出力するようにします。
def test_for_latest_image():
print(spec["kind"] + " " + spec["metadata"]["name"])
if spec["kind"] == "ReplicationController":
for container in spec["spec"]["template"]["spec"]["containers"]:
tag = container["image"].split(":")[-1]
assert_not_equal(tag, "latest", "should not use latest images")
test_for_latest_image()
崩れたYAMLをテストしようとする
$ kubetest manifest/kube-state-metrics.yaml && echo "ok"
WARN The document manifest/kube-state-metrics.yaml does not contain any content
ClusterRole kube-state-metrics
ClusterRoleBinding kube-state-metrics
Service kube-state-metrics
Deployment kube-state-metrics
ok
妙なWARNが出るがコマンドはOKを返します。YAMLがvalidかはこのツールだけではわからないようです。
テスト定義のDSLについて
- Pythonのサブセットであるskylarkという言語で記述する。(https://github.com/google/skylark)
- Pythonとにているがフルセットではないので戸惑うことがある。マニュアルはしっかりしている。skylark/spec.md at master · google/skylark · GitHub
- テスト用の関数を定義した後,最後に実行するのが少しダサい。 Issueもできているのでそのうちよくなりそう Autodetect functions starting with test_ · Issue #2 · garethr/kubetest · GitHub
まとめ
いくつか気になるところがあるが、manifestが特定のルールにしたがっているかを柔軟に検証することができる良いツールであると感じました。
HEADがビルドできないのが気になります。
全体のまとめ
- Copper
- kubeval
- kubetest
をそれぞれ調査しました。どれも完璧というものではないですが、事前テストをすることでmanifestの品質を高めることができます。
参考
- kubetest, kubeval の作者のプレゼン資料