Cloud Intelligence™Cloud Intelligence™

Cloud Intelligence™

Kubernetesで使うValidating Admission Policy:応用編

By Eyal ZekariaJun 5, 20238 min read

このページはEnglishDeutschEspañolFrançaisItalianoPortuguêsでもご覧いただけます。

前回の記事では、Validating Admission Webhooks(VAW)を用いてKubernetesクラスタにDynamic Admission Control(DAC)を実装する際の課題を取り上げました。あわせて、新しいValidation Admission Policy(VAP)リソースをご紹介し、シンプルなユースケースを通じて手軽さをお伝えしました。

今回は、VAPのもう一歩踏み込んだ機能として、次の2つに焦点を当てます。

  1. リソースのマッチングとフィルタリング
  2. ポリシーのパラメータ化

最後に、VAWに代えてVAPを採用する際の判断ポイントと、移行のコツについても簡単に触れます。

前回および今回のサンプルコードはすべて、本記事のために用意したGitHubリポジトリでご覧いただけます。

リソースのマッチングとフィルタリング

前回のユースケースでは、特定の1種類のリソース(Deployment)だけにポリシーを適用する方法をご紹介しました。リソース固有のバリデーションには適したやり方です。

では、命名規則の強制のように、クラスタ全体に汎用的なポリシーを適用したい場合はどうすればよいでしょうか。

選択肢はいくつかあります。まず明示的な方法として、検証したいリソースの種類ごとにマッチングルールを作成する手があります。

apiVersion: admissionregistration.k8s.io/v1alpha1
kind: ValidatingAdmissionPolicy
metadata:
  name: "demo-policy.example.com"
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
      - apiGroups:   ["apps"]
        apiVersions: ["v1"]
        operations:  ["CREATE", "UPDATE"]
        resources:   ["deployments", "statefulsets"]
      - apiGroups:   ["autoscaling"]
        apiVersions: ["v2"]
        operations:  ["CREATE", "UPDATE"]
        resources:   ["horizontalpodautoscalers"]
  validations:
    - expression: "!object.metadata.name.contains('demo') || object.metadata.namespace == 'demo'"

同じAPIグループ・バージョンに属するリソースには共通ルールを使い回せますが、このリストはすぐに膨れ上がり、メンテナンスの手間も発生します(特に新しいリソースタイプをクラスタに導入するたびに見直しが必要です)。

もう一つの方法は、いったんすべてのリソースをマッチさせたうえで、検証対象から外したいものを選んで除外するアプローチです。リソースのフィルタリングは2つの階層で行えます。

  1. ValidatingAdmissionPolicyリソース内 — .spec.matchConditionsを使う方法
  2. ValidatingAdmissionPolicyBindingリソース内 — .spec.matchResources.excludeResourcesを使う方法

1つ目は公式ドキュメントに記載がありますが、2つ目はあまり詳しく触れられていません。.spec.matchConditionsCommon Expression Language(CEL)を用いて、ルールにすでにマッチしたリクエスト(つまりバインディングとポリシーのマッチ制約を通過したリクエスト)をさらに絞り込みます。

一方、リソース除外(.spec.matchResources.excludeResources)は、リソースがポリシーに到達する前の段階で実行され、リソースのマッチングと同じ構文で除外リストを定義します。

次の例は、CELのマッチ条件を使い、APIグループcoordination.k8s.ioLeaseリソースと、APIグループrbac.authorization.k8s.ioのあらゆるリソースをスキップするポリシーです。

apiVersion: admissionregistration.k8s.io/v1alpha1
kind: ValidatingAdmissionPolicy
metadata:
  name: "demo-policy.example.com"
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
      - apiGroups:   ["*"]
        apiVersions: ["*"]
        operations:  ["CREATE", "UPDATE"]
        resources:   ["*"]
  matchConditions:
    - name: 'exclude-leases' # Each match condition must have a unique name
      expression: '!(request.resource.group == "coordination.k8s.io" && request.resource.resource == "leases")' # Match non-lease resources.
    - name: 'rbac' # Skip RBAC requests.
      expression: 'request.resource.group != "rbac.authorization.k8s.io"'
  validations:
    - expression: "!object.metadata.name.contains('demo') || object.metadata.namespace == 'demo'"

続いて、同じようなリクエストをマッチ除外でフィルタリングするバインディングの例です。

apiVersion: admissionregistration.k8s.io/v1alpha1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: "demo-binding-test.example.com"
spec:
  policyName: "demo-policy.example.com"
  validationActions: [Deny]
  matchResources:
    excludeResourceRules:
      - apiGroups:   ["rbac.authorization.k8s.io"]
        apiVersions: ["*"]
        operations:  ["*"]
        resources:   ["*"]
      - apiGroups:   ["coordination.k8s.io"]
        apiVersions: ["*"]
        operations:  ["*"]
        resources:   ["leases"]
    namespaceSelector:
      matchLabels:
        environment: demo

どちらもほぼ同じ結果が得られますが、可能であればexcludeResourceRulesをおすすめします。フィルタリングがより早い段階で行われるため、無駄にCEL式を実行せずに済むからです(Kubernetes APIサーバーでCELを実行する際のコスト予算については前回の記事で取り上げました)。さらに、リソースのマッチングと同じ構文を使うため、可読性や理解のしやすさという点でもメリットがあると感じています。

ただし、matchConditionsはCEL式でフィルタリングを行い、バリデーション式と同じオブジェクト(objectoldObjectrequestparams)にアクセスできます。そのため、excludeResourceRulesでは扱えない条件、たとえばユーザーやグループ、その他オブジェクトの任意の属性によるフィルタリングまで実現できます。

apiVersion: admissionregistration.k8s.io/v1alpha1
kind: ValidatingAdmissionPolicy
metadata:
 name: "demo-policy.example.com"
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
      - apiGroups:   ["*"]
        apiVersions: ["*"]
        operations:  ["CREATE", "UPDATE"]
        resources:   ["*"]
  matchConditions:
    - name: 'exclude-kubelet-requests'
      expression: '!("system:nodes" in request.userInfo.groups)' # Allow requests made by non-node users.
  validations:
    - expression: 'object.metadata.name.startsWith("demo-")'

結論としては、両者を組み合わせて使うのがおすすめです。特定のリソース(APIグループ、バージョン、操作、リソースタイプ)を除外したいときはexcludeResourceRulesを、特定のユーザーやグループからのリクエストなど、より高度なフィルタリングが必要なときはmatchConditionsを、と使い分けるとよいでしょう。

ポリシーのパラメータ化

ポリシーをさらに強力にしたいときは、パラメータ化が有効です。組み合わせごとにポリシーを定義し直さなくても、パラメータ値だけを変えてポリシーを再利用できます。要は、設定をポリシー本体から切り離すという考え方です。

パラメータを使うには、クラスタ運用者がパラメータを定義・保持するためのCustomResourceDefinition(CRD)を作成する必要があります。多少の追加作業は発生しますが、必須ではないので、必要に応じて使うかどうかを判断してください。

以下は、必要に応じて簡単に変更・拡張できるシンプルなCRDの例です。

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: parameters.example.com
spec:
  group: example.com
  names:
    kind: Parameters
    plural: parameters
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
        type: object
        properties:
          maxReplicas:
            type: integer
    served: true
    storage: true
  scope: Cluster

このCRDがあれば、整数型のmaxReplicasというカスタムフィールドを1つ持つParameters型のリソースを作成できます。

apiVersion: example.com/v1
kind: Parameters
metadata:
  name: demo-parameters
maxReplicas: 3

これが用意できたら、ポリシー側で.spec.paramKindに(CRDで定義した)正しいAPIバージョンとkindを指定し、バリデーション式の中でパラメータを参照できるようにします。

apiVersion: admissionregistration.k8s.io/v1alpha1
kind: ValidatingAdmissionPolicy
metadata:
  name: "demo-policy.example.com"
spec:
  failurePolicy: Fail
  paramKind:
    apiVersion: example.com/v1
    kind: Parameters
  matchConstraints:
    resourceRules:
      - apiGroups:   ["apps"]
        apiVersions: ["v1"]
        operations:  ["CREATE", "UPDATE"]
        resources:   ["deployments"]
  validations:
    - expression: "object.spec.replicas <= params.maxReplicas"
      messageExpression: "'Deployments cannot have more than ' + string(params.maxReplicas) + ' replicas, this one has ' + string(object.spec.replicas)"

messageExpressionもパラメータを参照する形に書き換えており、ポリシー内に値をハードコーディングせずに済んでいる点に注目してください。

最後に、作成したParametersリソースを名前で参照するようにバインディングを更新します。

apiVersion: admissionregistration.k8s.io/v1alpha1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: "demo-binding-test.example.com"
spec:
  policyName: "demo-policy.example.com"
  validationActions: [Deny]
  paramRef:
    name: "demo-parameters"
  matchResources:
    namespaceSelector:
      matchLabels:
        environment: demo

これらをすべてクラスタに適用したうえで、設定したレプリカ数を超えるDeploymentを作成しようとすると、想定どおり失敗します。

$ k create deployment nginx — image=nginx — replicas=6
error: failed to create deployment: deployments.apps "nginx" is forbidden: ValidatingAdmissionPolicy 'demo-policy.example.com' with binding 'demo-binding-test.example.com' denied request: Deployments cannot have more than 3 replicas, this one has 6

このように、異なるParametersリソースを用意してそれぞれを別のリソースや名前空間にバインドすれば、パラメータ化されたポリシーを使い回せるようになります。

VAWとVAP、どちらを選ぶか

VAPはVAWに比べて導入がはるかに容易なため、いずれVAWを置き換える、あるいは少なくともKubernetesでクラスタ内バリデーションを始めるときの標準になっていく可能性は高いと考えられます。

KubernetesでのCELサポートは、クラスタ運用者の使い勝手を向上させるさまざまな可能性を切り開きました。Mutating Admission Policyもまた、CELによって実現が現実味を帯びてきた機能の一つです。VAPは成熟と一般提供(GA)に向け、今後も継続的に注力・開発されていくと見てよいでしょう。

とはいえ、VAPはまだVAWを完全に置き換えられるレベルには達していません。VAWであれば、シンプルなリクエスト属性のチェックから、外部システムの状態を確認したうえでリクエストを許可するような処理まで、コードに落とし込めるあらゆるカスタムバリデーションロジックを実装できます。さらに、バリデーションサーバーを自前で書く必要がある分、技術や言語を自由に選べる柔軟性もあります(VAPのようにCELに縛られません)。

また、VAPは(v1.26以降)依然としてAlpha段階にあるため、主要クラウドのマネージドKubernetesサービスを利用しているユーザーにとっては、現時点で本格的に採用できる選択肢とは言えません。EKSAKSはAlpha機能をサポートしておらず、GKEはサポートしているもののテスト用途に限られます(Alphaクラスタは30日で期限切れになります)。つまり、たとえ技術的に可能でも、現時点ではVAPを組織の唯一のクラスタ内バリデーション手段として全面採用するのは避けたほうが無難です。

VAWからVAPへ移行するためのヒント

すでにVAPがベータまたはGAに到達したタイミングでこの記事を読んでいて、現行ソリューションからの移行を検討しているという方もいらっしゃるかもしれません。幸い、これら2つの機能は排他的ではないため、移行期間中は両者を並行して運用できます。

VAWとVAPはリソースのマッチング方法が似ているので、VAPを作成する手順は比較的シンプルです。スタート時に役立つヒントをいくつかご紹介します。

  • ValidatingWebhookConfigurationリソースの.webhooks.rulesと同じ内容を、.spec.matchConstraints.resourceRulesに持つvalidatingAdmissionPolicyを作成しましょう。
  • 現在使っている言語(おそらくRego)からCELへバリデーション式を移行します。これが圧倒的に時間のかかる作業ですが、ChatGPTを活用するのがおすすめです。ポリシーのルールは比較的単純なものが多いので、かなり良い結果を返してくれます。
  • ValidatingAdmissionPolicyBindingを作成し、.spec.validationActionsWarnに設定します。これでクライアントへエラーは返しつつ、APIリクエストはブロックしない状態にできます。
  • .spec.matchResourcesでポリシーをリソースにバインドする方法を決めます。シンプルに名前空間ラベルでマッチさせる方法もあれば、より高度なマッチ式を使う方法もあります。それに合わせて名前空間にラベルを付けてください。
  • 管理対象の名前空間で、有効・無効なリソースの作成を試し、返ってくる(あるいは返ってこない)警告を確認しながら式を磨き込んでいきましょう。
  • ポリシーに自信が持てたら、バインディングの.spec.ValidationActionsDenyに変更し、本格的に有効化します。
  • 従来のシステム側で重複しているポリシーは削除しましょう。

参考リソース