Conftestを用いたCIでのポリシーチェックの紹介

こんにちは。この記事は、Merpay Tech Openness Month 2020 の4日目の記事です。メルペイのSREチームに所属している@yuharaです。
メルペイのサービスはGCP上で稼働しマイクロサービスを構成しています。各マイクロサービスはGKE上で動き、Cloud SpannerやCloud Pub/SubといったGCPのクラウドリソースもマイクロサービス毎に独立したプロジェクトを持ち各チームで管理しています。
これらは、GCPのリソースであればTerraformで使われるHCLで記述され、またGKE上のKubernetesリソースはYAML Manifestとして宣言的に記述されGitHub上のリポジトリで管理し、各マイクロサービスチームのディベロッパーが要件に応じて作成・変更していきます。
今回、これらのTerraformやKubernetesに関わるHCLやYAMLファイルに対して、必要な設定がなされているかをCIでチェックし、安全にリリースを行うための取り組みについて紹介します。

Production Readiness Checklist

メルカリとメルペイでは、サービスを安全にリリースするために、Production Readiness Checklistを作成しています。Production Readiness ChecklistではマイクロサービスがProduction Readyな状態かを確認するためのチェックリストであり、KubernetesのManifestを例にあげると、アプリケーションの安全な停止のためにpreStopが設定されているかなど様々な項目があり、リリース前に各マイクロサービスの開発チームが手動でチェックすることになっていました。
これらのチェックをCIで自動化し、設定が不十分であればディベロッパーにフィードバックし、より安全に開発およびリリースするための取り組みとしてOpen Policy Agentを利用し始めました。

Open Policy Agentとは

Open Policy Agent(以下OPA)とはOSSの汎用ポリシーエンジンで、現在CNCFでincubatingのプロジェクトです。OPAではRegoというポリシー記述言語でポリシーを書きます。OPAではこのRegoで書かれたポリシーとJSONなどの構造化データに従って評価し結果を返します。
OPAをデーモンやライブラリとしてアプリケーションに統合することで、アプリケーションからポリシーを切り離すことができます。
また、Kubernetes環境においてはAdmission Controllerと連携しWebhook先をOPAにすることでポリシーチェックを行うGatekeeperや、コマンドラインからYAMLなどのデータをテストするConftestなどがあり、今回はCIで利用するためConftestを紹介したいと思います。

なぜConftestを使い始めたか

ConftestはInstrumentalで開発されていましたが、2020年5月にOPAコミュニティ傘下に加わりました。ConftestはRegoで記述されたポリシーを元にYAMLやJSONにとどまらず、HCLやDockerfile、VCLなどといった様々な形式のファイルに対応しコマンドラインから簡単にテストを行うことができます。
YAMLやJSONのチェックであれば他にもKubevalといったツールもありますし、Go言語などでもバリデーション用のライブラリが存在します。今回 私達がConftestを使い始めた理由は下記の2点です。

  • GCPのクラウドリソースはHCL形式のTerraformで管理しており、他にもDockerfileやVCLなどの様々な形式のファイルに対応している
  • Kubernetesでは同じOPAのGatekeeperと組み合わせて、Regoで書いたポリシーを用いてCIとKubernetesのAPI Serverの両方の側面でチェックすることが可能

こういった観点からConftestを使い始めました。(ちなみにConftestとGatekeeperには完全な互換性は無く、ConstraintTemplateというCRDの形にRegoを置き換えるなど少し工夫が必要です。)

どのように使っているか

では具体的にConftestを使ったポリシーチェックを紹介していきます。一例としてProduction Readiness Checklistの中に、変動するワークロードに対応するためにHorizontalPodAutoscalerが設定されAuto Scaleするようになっていることをチェックする項目があります。
例えばこのケースでHPAの設定として、spec.minReplicasが設定されていることをテストするポリシーを書いてみます(spec.minReplicasは省略可能でデフォルトが1であるため、明示的に3以上に指定されていることをチェックするというシナリオです。)

package hpa

description := "This is a production readiness check item, please check https://~"

violation_min_replicas_is_not_set[{"msg": msg, "description": description}] {
  input.kind = "HorizontalPodAutoscaler"
  satisfied := [good | good := input.spec.minReplicas >= 3]
  not any(satisfied)
  msg := sprintf("minReplicas in HorizontalPodAutoscaler %s must be set", [input.metadata.name])
}

package名は任意です。同じpackage内では変数を参照することができます。またdescriptionは後で述べますがポリシー違反時に返却する情報としてmetadataを自由に付与できますが、そこに乗せる情報として定義しています。
次に、ルールを記述していきます。ルール名には violation, deny, warn が用いられますがdenyはGatekeeperにてviolationに変更されており、ポリシーの併用を考慮するならばConftestにおいてもviolationを使うと良いでしょう。Conftestの実行時にviolationもしくはdenyルールに該当する場合はExit codeが1を返しますが、warnの場合は0を返します。
また、Conftestの内部的にはdata.<package名>.<ルール名>という形で扱われるため、ルールが増えた場合のトレース時などに見分けがつくようにviolation_xxxのような名称でルールごとに分けることも可能です。

定義したルールの中で違反する条件を記述していきます。ここではinput(入力データ)に対してkindHorizontalPodAutoscalerでかつspec.minReplicasが3以上に設定されていることをチェックしています。もしspec.minReplicasが明示的に指定されていなかったり3未満であったりする場合は条件にマッチし違反となります。

もう少し詳しく見ていきましょう。

  satisfied := [good | good := input.spec.minReplicas >= 3]
  not any(satisfied)

上記の記述では、inputデータのspec.minReplicasが 3以上であればtrueと評価され、good という変数に代入され、Pipeの左側のオブジェクトに変数goodを渡しています。そして satisfied に [] で括られた配列として代入します。
もしminReplicasが3未満に設定されている場合は satisfied := [ false ] となり、minReplicasが設定されていない場合は評価されないため satisfied := [ ] という空の配列になります。any関数で配列のいずれかの要素にtrueがあった場合はtrueを返しますが、そうでない場合はfalseを返します。最後にnot は否定を表すため最終的にtrueとなり条件にマッチする形になります。

  msg := sprintf("minReplicas in HorizontalPodAutoscaler %s must be set", [input.metadata.name])

そして最後にルールに一致する場合に、msgにフィードバックとして返すメッセージを記述しています。

このポリシーを実際にConftestでテストするには以下のようなコマンドを実行します。

conftest test --policy policy --namespace hpa --input yaml --output yaml sample-hpa.yaml

オプションの詳細はドキュメントに記載があります。--policyでRegoファイルを配置しているディレクトリを指定し、--namespaceでパッケージ名を指定しています。--inputで入力ファイルの形式を指定し、--outputで出力結果の形式を指定しています。

--outputがYAMLとJSONで少し異なり、ポリシー違反がある場合はYAMLの場合はmsgのみの出力に対し、JSONではmetadataとして個別に設定した値が加えられています。
このようにしてCIでチェックを行い、違反がある場合にはディベロッパーに対して適切な情報を付与してフィードバックを行うことが可能となります。下記は現時点で最新の v0.20.0の時の出力例です。

$ conftest test --policy policy --namespace hpa --input yaml --output yaml sample-hpa.yaml 
FAIL - sample-hpa.yaml - minReplicas in HorizontalPodAutoscaler sample must be set

1 test, 0 passed, 0 warnings, 1 failure
$ conftest test --policy policy --namespace hpa --input yaml --output json sample-hpa.yaml 
[
        {
                "filename": "sample-hpa.yaml",
                "warnings": [],
                "failures": [
                        {
                                "msg": "minReplicas in HorizontalPodAutoscaler sample must be set",
                                "metadata": {
                                        "description": "This is a production readiness check item, please check https://~"
                                }
                        }
                ],
                "successes": []
             }
]

ポリシーの開発について

Regoのポリシーを開発するにあたり、OPAではポリシーのテストを行うためのフレームワークが用意されています。先程のspec.minReplicasをチェックするポリシーを例にテストコードを書くと下記のようになります。テストケースとしてspec.minReplicasが正しく3以上に設定されているケースと、3未満のケース、spec.minReplicasが指定されていないケースの3パターンです。

package hpa

msg := "minReplicas in HorizontalPodAutoscaler sample must be set"

test_min_replicas_is_set {
 not violation_min_replicas_is_not_set[{"msg": msg, "description": description}] with input as
   {"kind": "HorizontalPodAutoscaler","metadata": {"name": "sample"}, "spec": {"maxReplicas": 10, "minReplicas": 3}}
}

test_min_replicas_less_than_three {
  violation_min_replicas_is_not_set[{"msg": msg, "description": description}] with input as
    {"kind": "HorizontalPodAutoscaler", "metadata": {"name": "sample"}, "spec": {"maxReplicas": 10, "minReplicas": 2}}
}

test_min_replicas_is_not_set {
 violation_min_replicas_is_not_set[{"msg": msg, "description": description}] with input as
   {"kind": "HorizontalPodAutoscaler", "metadata": {"name": "sample"}, "spec": {"maxReplicas": 10}}
}

opa testコマンドでテストを実行すると、test_から始まるルールに対して実行し3つのテストに全てPASSしていることがわかります。

$ opa test -v hpa.rego hpa_test.rego 
data.hpa.test_min_replicas_less_than_three: PASS (1.03581ms)
data.hpa.test_min_replicas_is_not_set: PASS (550.321µs)
data.hpa.test_min_replicas_is_set: PASS (481.894µs)
--------------------------------------------------------------------------------
PASS: 3/3

また、--coverage --format=jsonオプションを追加することで、テストカバレッジをレポートすることも可能です。上記のテストケースでは100%という結果でした。

$ opa test --coverage --format=json hpa.rego hpa_test.rego | jq .coverage
100

このように、Regoのポリシー開発においてもテストコードを書きCIによってカバレッジなどを含むレポートをフィードバックすることで、開発プロセスをスピートアップし効率的に行うことが可能となります。

おわりに

Regoでポリシーを書き、Conftestを用いてCIでポリシーチェックを行う取り組みを紹介しました。今回はKubernetesのYAML Manifestを例に取り上げましたが、初めにも述べたように様々な形式のファイルに対してポリシーチェックを行うことができます。
最初は少しRegoは難しいと感じるかもしれませんが、Playgroundも用意されていて簡単に試すこともできますし、Conftestのリポジトリ上にもいくつかのExampleがあるので興味がある方は参考にしてみると良いと思います。
最後まで読んで頂きありがとうございました。

  • X
  • Facebook
  • linkedin
  • このエントリーをはてなブックマークに追加