Перейти к содержимому
Hogin Hogin
Назад

ValidatingAdmissionPolicy: переезжаем с вебхуков на CEL прямо в API-сервере

9 мин чтения

Каждый kubectl apply на Pod с политикой disallow-latest-tag сегодня уходит через сеть в отдельный под Kyverno или Gatekeeper и возвращается обратно — и так на каждый ресурс, каждый раз, пока кластер жив. ValidatingAdmissionPolicy (VAP) убирает этот прыжок: CEL-выражение компилируется и выполняется прямо внутри kube-apiserver, без внешнего webhook. Это не экспериментальная фича — VAP стабилен с Kubernetes 1.30, а Kyverno 1.17 уже депрекейтит классические ClusterPolicy в пользу CEL-нативных ValidatingPolicy. Разбираем, когда VAP полностью заменяет вебхук, а когда без него всё ещё не обойтись.

Содержание

Открыть содержание

Почему это не игнорировать в 2026

VAP прошёл обычный для Kubernetes путь взросления: alpha в 1.26, beta в 1.28, GA в 1.30. То есть на подавляющем большинстве продовых кластеров он уже доступен из коробки, без включения feature gate. Параллельно происходит второе, более важное для практики движение: Kyverno официально сворачивает старую модель политик. С версии 1.17 ClusterPolicy и CleanupPolicy помечены deprecated в пользу CEL-нативных ValidatingPolicy и DeletingPolicy, а удаление запланировано на v1.20 (октябрь 2026, источник). Если вы пишете и поддерживаете политики на Kyverno сегодня, миграция на CEL — это не «можно попробовать в свободное время», а горизонт в пару релизов.

Задержка — не абстрактная метрика для графиков. Каждый вебхук встраивается в критический путь kubectl apply, helm upgrade, реконсиляции контроллера — то есть в путь, по которому CI/CD ждёт ответа, прежде чем считать деплой успешным. Даже если сам под Kyverno отвечает за единицы миллисекунд, к этому добавляются TLS-хендшейк, сериализация объекта в JSON туда и обратно, и риск, что под окажется недоступен именно в момент пиковой нагрузки на кластер.

Где выполняется проверка: вебхук vs CEL в апи-сервере

Разница на диаграмме — не косметика. Webhook-путь требует, чтобы под контроллера был Ready, прошёл TLS-хендшейк и успел ответить в пределах timeoutSeconds — иначе решает failurePolicy (Fail отклонит запрос, Ignore пропустит его непроверенным, что на практике означает дыру в защите ровно тогда, когда она нужнее всего). CEL в апи-сервере ничего этого не знает: правило скомпилировано в байт-код ещё при kubectl apply и выполняется в том же процессе, который уже держит объект в памяти для остальных этапов admission-цепочки.

Модель CEL-выражений: что видит правило

ValidatingAdmissionPolicy даёт CEL-выражению четыре именованные переменные — от них зависит, что вообще можно написать в expression, и это стоит понимать до того, как переносить первую реальную политику.

Что доступно CEL-выражению в ValidatingAdmissionPolicy

Область действия правила задаётся отдельно от логики — в matchConstraints.resourceRules (какие apiGroups, resources, operations) и опционально в objectSelector/namespaceSelector (по labels). Это тот же принцип, что match в Kyverno или matchLabels в Gatekeeper Constraint, просто ближе к нативному API Kubernetes.

Каждое выражение в validations[].expression обязано вернуть bool. Никакого «выполнения с побочными эффектами» — компилятор CEL проверяет типы уже при kubectl apply, а не при первом сработавшем запросе, так что опечатка в имени поля объекта роняется сразу. Более того, начиная с 1.30 апи-сервер умеет делать type checking по установленным CRD и сообщать о потенциальных проблемах прямо в status.typeChecking политики — то есть можно поймать «это поле не существует у этого типа ресурса» ещё до того, как правило кого-то заблокирует или молча пропустит всё подряд.

Kyverno ValidatingPolicy vs голый ValidatingAdmissionPolicy

Здесь легко перепутать два разных решения с одинаковым CEL внутри.

Голый ValidatingAdmissionPolicy — родной API Kubernetes, никакого Kyverno или Gatekeeper в кластере не нужно вообще. Пишете CEL руками, применяете kubectl apply, отладка — через kubectl describe и логи апи-сервера. Полный контроль, ноль дополнительных компонентов и точек отказа, но и ноль привычных удобств: нет kyverno test для локального прогона в CI, нет PolicyReport с готовой сводкой нарушений по всему кластеру, нет общей библиотеки готовых правил под этот конкретный формат — всё пишется с нуля или копируется из чужих репозиториев.

ValidatingPolicy в Kyverno — CEL-нативный CRD, пришедший на смену ClusterPolicy, — это надстройка поверх той же CEL-модели, но с прежним тулингом вокруг Kyverno: kyverno test для прогона политики против фикстур в CI, PolicyReport с агрегированным аудитом по кластеру, единый CLI для отладки. Ключевая деталь — Kyverno умеет автоматически сгенерировать нативный ValidatingAdmissionPolicy и ValidatingAdmissionPolicyBinding из ValidatingPolicy, когда правило укладывается в возможности CEL, и по-прежнему маршрутизирует запрос через собственный webhook, если правило выходит за эти границы. Оператору не нужно решать это вручную на каждую политику — движок сам выбирает путь исполнения и делает это прозрачно.

Вебхук остаётся обязательным ровно в трёх случаях, которые CEL не выражает в принципе, а не потому что кто-то поленился их реализовать:

Таблица сравнения

ПараметрWebhook (Kyverno ClusterPolicy)CEL (ValidatingAdmissionPolicy)
Где выполняетсяотдельный под, сетевой вызоввнутри kube-apiserver
Задержка admissionсеть + сериализация на каждый запросin-process, без сетевого прыжка
Точка отказапод должен быть Ready, влияет failurePolicyнет отдельного компонента, который может упасть
Mutation / generation❌ (нужен webhook или MutatingAdmissionPolicy)
Внешние вызовы (cosign, API)
Проверка типов правилаво время выполнениястатически, при apply (status.typeChecking)
Локальное тестированиеkyverno test, PolicyReportkubectl, логи апи-сервера (в Kyverno-обёртке — тот же kyverno test)
Параметризацияпеременные Kyvernoparams + paramRef
Требует компонент в кластереда (под Kyverno/Gatekeeper)нет

Что нужно, чтобы перевести политику на CEL

Берём disallow-latest-tag — ту же политику, что разбиралась в сравнении Kyverno и OPA Gatekeeper, — и переписываем как нативный ValidatingAdmissionPolicy без единого пода Kyverno в кластере.

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: disallow-latest-tag
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["pods"]
    namespaceSelector:
      matchExpressions:
        - key: kubernetes.io/metadata.name
          operator: NotIn
          values: ["kube-system"]
  validations:
    - expression: >
        object.spec.containers.all(c, !c.image.endsWith(':latest'))
      message: "Теги :latest запрещены. Укажите точный тег или SHA-дайджест."
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: disallow-latest-tag-binding
spec:
  policyName: disallow-latest-tag
  validationActions: ["Deny"]

ValidatingAdmissionPolicy описывает правило, область действия и исключения (здесь — kube-system, где системные образы часто держат другой цикл выпуска тегов), а ValidatingAdmissionPolicyBinding — включает правило и задаёт режим (Deny — блокировать запрос, Warn — вернуть предупреждение клиенту, но пропустить, Audit — только записать в AdmissionReview, вообще без влияния на запрос). Разделение на два объекта позволяет держать одно правило и несколько привязок с разным validationActions для разных окружений — например, Audit в staging, пока не убедились, что правило не ловит ложных срабатываний, и Deny в production.

kubectl apply -f disallow-latest-tag-vap.yaml
kubectl get validatingadmissionpolicies
# NAME                   AGE
# disallow-latest-tag    12s

kubectl get validatingadmissionpolicy disallow-latest-tag \
  -o jsonpath='{.status.typeChecking.expressionWarnings}'
# (пусто — CEL-выражение прошло проверку типов без замечаний)

Разница в задержке видна без специального бенчмарк-стенда — достаточно замерить время между отправкой запроса и ответом апи-сервера до и после миграции:

for i in {1..20}; do
  t0=$(date +%s%N)
  kubectl run "probe-$i" --image=nginx:1.27.0 --restart=Never --dry-run=server >/dev/null
  t1=$(date +%s%N)
  echo "$(( (t1 - t0) / 1000000 )) ms"
done

--dry-run=server прогоняет запрос через весь admission-путь, включая политику, но не создаёт ресурс — удобно гонять пробы в цикле, не засоряя namespace тестовыми Pod. На локальном kind-кластере вебхук-путь у Kyverno добавляет заметный сетевой прыжок к каждому запросу; на CEL в апи-сервере этот прыжок пропадает полностью, и порядок задержки падает с десятков миллисекунд до единиц. Точные цифры зависят от сети, ресурсов узла и загрузки пода контроллера, но направление стабильно в любой среде: меньше сетевых прыжков — меньше задержка, и разброс (p99) сокращается заметнее, чем медиана.

Как проверить, что политика работает

kubectl run test --image=nginx:latest --restart=Never
# Error from server: ValidatingAdmissionPolicy 'disallow-latest-tag' with binding
# 'disallow-latest-tag-binding' denied request: Теги :latest запрещены.
# Укажите точный тег или SHA-дайджест.

kubectl run ok --image=nginx:1.27.0 --restart=Never
# pod/ok created

kubectl run legacy --image=nginx:latest --restart=Never -n kube-system
# pod/legacy created  — namespaceSelector исключил kube-system, как и задумано

Сообщение об ошибке приходит прямо от апи-сервера, без упоминания вебхука или пода-контроллера — политика теперь часть самого Kubernetes API, а не внешней надстройки над ним. Если нужно быстро посмотреть, сколько запросов правило заблокировало за последнее время без включения Deny в проде, начните с validationActions: ["Audit"] и смотрите структурированные записи AdmissionReview в аудит-логе апи-сервера — это тот же механизм, которым Kyverno сам заполняет PolicyReport, только без промежуточного CRD.

Итог

Для простых validation-правил — «запрещённый тег», «обязательный label», «лимиты CPU заданы» — CEL в апи-сервере теперь новый дефолт: меньше задержка, меньше движущихся частей, меньше поводов держать failurePolicy: Ignore в проде «на всякий случай». Kyverno остаётся там, где CEL принципиально не дотягивается — mutation, generation ресурсов и проверки с внешним вызовом вроде верификации подписи образа. Разумный путь — не выбирать один инструмент раз и навсегда, а мигрировать validation-правила по мере депрекейта ClusterPolicy в Kyverno 1.17–1.20, оставив webhook только под то, что CEL действительно не умеет.


Поделиться:

Следующая статья
Renovate: автообновление зависимостей, которое не бесит