はじめに
今回は、Kubernetesのポリシー管理ツールであるKyvernoについて、公式が提供しているCLIツールを用いたCI(継続的インテグレーション)の実装を行いましたので、その知見を共有します。
ポリシーを本番環境に適用してから「あ、設定ミスで全部ブロックしちゃった」と気づくのは、SRE として最も避けたい事態かもしれません。事前に確認できればより安全になることは間違いないかと思います。 そこで、Kyverno CLI を導入し、Pull Request の段階でポリシーの挙動を自動検証する仕組みを構築しました。
Kyverno CLIとは
Kyverno CLIは、リソースに追加する前にポリシーの動作を検証およびテストするためのコマンドラインツールです。
これを利用することで、以下のメリットがあります。
- クラスタにapplyする前に構文エラーやロジックミスを防げる
- 既存のマニフェストに対してポリシーがどう作用するかを確認できる
- GitOpsフローの中にポリシーのテストを組み込める
インストール方法
インストールは、様々な方法が紹介されているので、公式ドキュメントを参考にしてみてください。
https://kyverno.io/docs/kyverno-cli/install/#install-via-krew
主なオプションと機能
Kyvernoは様々なコマンドが用意されていますが、ここでは大きく分けて2つのコマンドを紹介します。
test: テストファイルを用いたユニットテストの実行
test はユーザが用意したテストケースをもとに、特定のリソースセットを一つまたは複数のポリシーに対してテストし、事前に別のテストマニフェストファイルで宣言された望ましい結果と実際の結果を比較するために使用されます。
https://kyverno.io/docs/kyverno-cli/usage/test
そのため、テストケースを予め用意する必要があり、以下のようなkyverno-test.yamlを作成する必要があります。
apiVersion: cli.kyverno.io/v1alpha1
kind: Test
metadata:
name: require-labels-test
policies:
- <ここに適用したいPolicyファイルを記載>
resources:
- <ここに適用したいResourceを記載>
- <ここに適用したいResourceを記載>
## 上記で追加した情報を以下で詳細にテストケースとして記載することができる
results:
## passパターンとfailパターンなど想定したケースごとに用意する
- policy: <policy name>
rule: <rule name>
resources:
- <resource name>
kind: Pod
result: pass
- policy: <policy name>
rule: <rule name>
resources:
- <resource name>
kind: Pod
result: fail
これを特定のディレクトリ内(コマンドで指定するディレクトリ)に配置することによって、kyverno がkyverno-test.yamlという名前のファイルを検索し、見つかった場合はその中のテストを実行します。
使い方
基本的な使い方と結果は以下のようになります。
# ポリシーのテスト実行例
kyverno test /path/to/folderOfPoliciesOrTests
# ポリシー結果例(teamというラベルをvalidateする)
❯ kyverno test ./kyverno/policy/test
Loading test ( kyverno/policy/test/kyverno-test.yaml ) ...
Loading values/variables ...
Loading policies ...
Loading resources ...
Loading exceptions ...
Applying 1 policy to 2 resources with 0 exceptions ...
Checking results ...
│────│────────────────│────────────│─────────────────────────────────│────────│────────│
│ ID │ POLICY │ RULE │ RESOURCE │ RESULT │ REASON │
│────│────────────────│────────────│─────────────────────────────────│────────│────────│
│ 1 │ require-labels │ check-team │ v1/Pod/default/test-pod-valid │ Pass │ Ok │
│ 2 │ require-labels │ check-team │ v1/Pod/default/test-pod-invalid │ Pass │ Ok │
│────│────────────────│────────────│─────────────────────────────────│────────│────────│
Test Summary: 2 tests passed and 0 tests failed
apply: ポリシーをリソースに適用し、結果を確認する(ドライラン)
apply コマンドは、指定された入力リソースのセットを使用して、1つまたは複数のポリシーをドライランするために使用されます
https://kyverno.io/docs/kyverno-cli/usage/apply
これをCIの中で実行することによって「今からデプロイしようとしているマニフェストが、ポリシー違反をしていないか?」をチェックする(Lint的な使い方)のに適しています。
簡単な使い方
インストール後、基本的な使い方は以下のようになります。
# ポリシーのドライラン実行
kyverno apply /path/to/folderOfPolicies --resource=/path/to/resources/
## 結果
❯ kyverno apply ./kyverno/policy --resource ./kyverno/manifest/test-pod-valid.yaml
Applying 3 policy rule(s) to 1 resource(s)...
pass: 1, fail: 0, warn: 0, error: 0, skip: 0
補足ですが、入力リソースはリソースマニフェスト(1つまたは複数)であっても、実行中のKubernetesクラスターから取得したものであってもかまいません。
実行中のKubernetesクラスターから取得したものでチェックしたい場合は以下のように—cluster オプションを利用することで可能です。
## ポリシーのドライラン実行(実行中のKubernetesクラスターから取得)
kyverno apply /path/to/policy.yaml --cluster
また、実運用ではkustomize, helmなどのマニフェスト管理ツールを利用していることも多いかと思います。これに対応するために、リソースは標準入力からも渡すことができます
## ポリシーのドライラン実行(kustomizeでbuildしたリソース)
kustomize build nginx/overlays/envs/prod/ | kyverno apply /path/to/policy.yaml --resource -
今回導入した実装
実際にCIパイプラインに組み込むにあたり、「Unit Test」と「ドライラン(DryRun)」の2つのフェーズを実装しました。今回はGitHub Actionsを利用したCIパイプラインですが、Kyvernoの処理のフォーカスして説明します。
Unit Test
ポリシー自体のロジックが正しいかを検証するフェーズです。
実装方法
UnitTestは単純で、Kyvernoの test コマンドを使用しています。ポリシーファイルと対になるテスト定義ファイル(kyverno-test.yaml)を用意し、それに伴うテスト用のマニフェストを用意しました。コマンドについては以下のように実行しています。
—fail-only は結果がfailのものだけを出力します。これをPRにコメントするようにしています。
また、PRにコメントする兼ね合いで、出力(-o)はmarkdown形式で出力させています。
kyverno test --fail-only /path/to/policy -o markdown
運用上の考慮点:テストコードの視認性と管理
ポリシーの数が増えるにつれてファイルが乱立し、「どのファイルがどのポリシーのテストリソースなのか」がパッと見て判別できなくなってしまうことを考え、ポリシーごとにディレクトリを切る構成で管理するようにしました。
policies-config
└── kubernetes-policies
└── kyverno
├── policies
├── POD
├── pod-001
└── pod-002
└── ASM
└── test
├── POD
├── 001
├── kyverno-test.yaml
├── fail-1.yaml ## 失敗パターンのyaml
└── pass.yaml ## 成功パターンのyaml
└── 002
└── ASM
このように「1ポリシー = 1ディレクトリ」とし、その中にテスト用リソースを隠蔽することで、トップレベルのディレクトリがスッキリし、開発者が目的のポリシーにすぐに辿り着けるようになりました。
ドライラン (Apply)
実際にデプロイ予定のマニフェストファイルに対してポリシーを適用し、違反がないかを確認するフェーズです。
実装方法
apply コマンドを使用します。Gitリポジトリ上のマニフェストディレクトリを指定して実行します。
kyverno apply /tmp/policy.yaml \
--values-file "$values_file" \
--resource /tmp/manifests.yaml
運用上の考慮点:クラスタ情報への依存
ドライランを実装する上で最大の課題は、「クラスタ外(CI環境)では判定できないルールがある」という点でした。
今回の導入ケースとして、CIからKubernetesクラスタへのアクセスが不可能な環境だったため、このような制約が生まれました。
特に ApiCallを使用している場合、単なるファイルチェックでは動作しません。
当然、これらを解決するには --cluster フラグを使用し、実際のクラスタと通信する実装を構築することも考えましたが、工数兼ね合いで断念しました。
どう解決したか
今回は「まずはスモールスタート」ということで、--cluster オプションは使用せず、静的なチェックのみを行う運用としました。例として、Deploymentへのポリシーとしてdeploymentに紐づくHPAが存在することをチェックするというものです。
💡 --cluster オプションを利用することができる環境であれば、ApiCallは動作します。また、Namespace Selector(後述)も問題なく動作しました。
1.静的解析としての割り切り:
## hpa_listにmanifestファイルから検索した、リソースを取得
hpa_list=$(yq ea -N -o json -I=0 '[select(.kind == "HorizontalPodAutoscaler").spec.scaleTargetRef.name]' /tmp/manifests.yaml | tr -d '\n')
## 複数存在する場合はリスト型で保管される
> echo $hpa_list
["test-hpa-1", "test-hpa-2"]
2.Valuesファイルを作成する
yq -n '
.apiVersion = "cli.kyverno.io/v1alpha1" |
.kind = "Values" |
.metadata.name = "values" |
.globalValues.hpaList = '"$hpa_list"'
' > "$values_file"
こうすることで、values.yamlは以下のように作成されます。
apiVersion: cli.kyverno.io/v1alpha1
kind: Values
metadata:
name: values
globalValues:
hpaList:
- test-hpa-1
- test-hpa-2
3.上記のValuesを利用して、applyをすることにより、CIを実行することが可能になります
kyverno apply /tmp/policy.yaml \
--values-file "$values_file" \
--resource /tmp/manifests.yaml
このように、values.yamlにモックデータとして格納し、それをapply コマンドで利用することでクラスタ外(CI環境)では判定できないApiCall を利用しているポリシーに対して静的解析を可能にしました。
ポリシーを作る上でちょっと役に立つTIPS
最後に、実装中に気づいた、ポリシー作成時の小技を少し紹介します。
Warningを出す設定(policies.kyverno.io/scored)
Kubernetesのマニフェストを利用する場合、環境によって設定を変えたりするケースが多くあると思います。
例として、namespaceリソースにistio-injectionラベルが入ってる事をチェックするポリシーでは、システムによってはIstioが不要な場面もあるはずです。
そういったポリシーは、failureAction をAuditにすることでリソースがデプロイされるが、警告メッセージを出すことができますが、これをCI実行する場合、kyvernoCLIはポリシー違反(fail)と判断し、CIは失敗してしまいます。
こういったケースに対応できるように、warningでレポートする機能が存在します。この設定がpolicies.kyverno.io/scored: false で、デフォルトはtrueとなっています。
💡 なお、failureAction を Enforceにした場合、policies.kyverno.io/scored: false にしたとしても違反があればデプロイは拒否されます。あくまでレポート機能の一部と思った方が良いです。
対象のポリシー
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-labels
annotations:
policies.kyverno.io/scored: "false"
spec:
rules:
- name: check-istio-labels
match:
any:
- resources:
kinds:
- Namespace
validate:
failureAction: Audit
message: "istio-injection is required(when use istio)"
pattern:
metadata:
labels:
istio-injection: enabled
対象のマニフェスト
apiVersion: v1
kind: Namespace
metadata:
name: test-namespace-1
---
apiVersion: v1
kind: Namespace
metadata:
name: test-namespace-2
labels:
istio-injection: enabled
apply コマンドの結果
❯ kyverno apply ./kyverno/policy --resource ./kyverno/manifest
Applying 1 policy rule(s) to 2 resource(s)...
policy require-labels -> resource default/Namespace/test-namespace-1 failed:
1 - check-istio-labels validation error: istio-injection is required(when use istio)
pass: 1, fail: 0, warn: 1, error: 0, skip: 0
> echo $?
0
ここで、test-namespace-1 はポリシーを満たしていないため、failと判断されてますが、apply コマンドとしてはwarnで返し、コマンドが成功していることがわかります。
NamespaceSelector利用時の対処
ポリシーを作成する際、特定のNamespaceだけこのポリシーを適用したい場合が出てくると思います。そんな時に利用できるNamespaceSelector を紹介します。
NamespaceSelector はその名の通り、特定のNamespaceのみにポリシーを適用させたい時に利用します。
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-memory-limit
namespace: test-namespace-1
annotations:
pod-policies.kyverno.io/autogen-controllers: DaemonSet,Deployment,StatefulSet
spec:
rules:
- name: check-app
match:
any:
- resources:
kinds:
- Pod
namespaceSelector:
matchExpressions:
- key: istio-injection
operator: Exists
validate:
-- 以下省略 --
ただし注意が必要です。ApiCall 同様にローカルのマニフェストファイルに対して実行した場合、このNamespaceSelectorの条件は無視(スキップ)されてしまいます
- なぜ無視されるのか?
理由はシンプルで、「検証対象のYAMLファイル(PodやDeployment)には、Namespace自体のラベル情報が含まれていないから」です。
Kyverno CLIがローカルの deployment.yaml を読み込んだ際、そこに namespace: test-namespace-1 と書いてあったとしても、CLIは「test-namespace-1 というNamespaceにどんなラベルが付いているか」を知る由もありません。その結果、match 条件を満たしているか判断できず、対象外として扱われてしまいます。
- 解決策:どうテストするか
この問題を回避し、正しくテストを行うにはApiCallと同じアプローチでテスト用の変数(values.yaml)でNamespace情報をモックすることが必要です
apiVersion: cli.kyverno.io/v1alpha1
kind: Values
metadata:
name: values
namespaceSelector:
- name: test-namespace-1
labels:
istio-injection: true
## --values-fileで明示的に指定する
kyverno apply ./kyverno/policy --resource ./kyverno/manifest --values-file ./values.yaml
このように「このapply実行時は、対象のNamespaceがこのラベルを持っているとみなす」という情報を注入できます。
まとめ
今回は、Kyverno CLI を活用して、Pull Request の段階でポリシー違反を検知する CI の仕組みを紹介しました。
本番環境への適用前に検証を行う「Shift Left」を実現することで、SRE として最も恐れる「設定ミスによる全ブロック」や「デプロイ後の手戻り」を未然に防ぐことができます。
今回の実装のポイントは以下の3点です。
- Unit Test と DryRun の分離: ポリシー自体のロジック検証と、マニフェストへの適用検証を分けることで、切り分けが容易になる。
- ディレクトリ構成の工夫: 「1ポリシー=1ディレクトリ」でテストリソースを隠蔽し、管理コストを下げる。
- クラスタレスな検証:
yqやValuesファイルを活用することで、クラスタ接続がない CI 環境でも柔軟なテストが可能になる。
Kyverno CLI は導入のハードルが比較的低く、GitOps フローに組み込むことで得られる安心感は非常に大きいです。まずはスモールスタートで、重要なポリシーからテストを自動化してみてはいかがでしょうか。