Политики управления рабочими нагрузками в Kubernetes

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!


Control by Matthias-Haker

В Kubernetes с помощью политик можно предотвратить развертывание определенных рабочих нагрузок в кластере. 

Команда разработки Kubernetes aaS VK Cloud Solutions перевела статью о том, как применять такие политики с помощью статических инструментов, например conftest, и кластерных операторов, например Gatekeeper.

Когда нужны строгие политики


Чаще всего строгие политики применяют в кластере для соблюдения нормативных требований. Но есть несколько практик, которые не регулируются требованиями, но рекомендуются для администраторов кластеров:

  • не запускать привилегированные поды;
  • не запускать поды как root-пользователь;
  • не указывать лимиты ресурсов;
  • не использовать тег latest для образа контейнера;
  • не включать дополнительные Linux capabilities по умолчанию.

Кроме того, вам могут понадобиться специальные общие политики, например:

  • все рабочие нагрузки должны иметь метки project и app;
  • все рабочие нагрузки должны использовать образы контейнеров из определенного реестра контейнеров, например my-company.com.

Существует и третья категория проверок, которые иногда нужно применить в качестве политики, чтобы избежать сбоев в работе сервисов. Например, когда два разных сервиса не должны использовать одно и то же имя хоста ingress.

Мы расскажем о применении политик для рабочих нагрузок Kubernetes с помощью внекластерных и внутрикластерных решений. Эти политики отклоняют рабочие нагрузки, которые не соответствуют заданным условиям.

За пределами кластера это достигают с помощью статических проверок YAML-манифестов перед их отправкой в кластер. Внутри кластера используют проверяющие контроллеры допуска, которые вызываются в составе API-запроса перед сохранением манифеста в базе данных.

По ходу статьи вам может пригодиться этот Git-репозиторий.

Ситуация: Deployment, не соответствующий требованиям


Рассмотрим следующий YAML-манифест:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: http-echo
  labels:
    app: http-echo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: http-echo
  template:
    metadata:
      labels:
        app: http-echo
    spec:
      containers:
      - name: http-echo
        image: hashicorp/http-echo
        args: ["-text", "hello-world"]
      - name: http-echo-1
        image: hashicorp/http-echo:latest
        args: ["-text", "hello-world"]

Из одного образа этот Deployment создает под, состоящий из двух контейнеров. В первом контейнере не указан никакой тег, во втором — тег latest. Фактически оба контейнера используют последнюю версию образа hashicorp/http-echo. Это не самый лучший вариант, и вам необходимо запретить в кластере такое развертывание. Лучше всего прикрепить к образу контейнера тег, например hashicorp/http-echo:0.2.3.

Давайте посмотрим, как можно обнаружить нарушение политики с помощью статической проверки. Поскольку нам нужно, чтобы ресурс не попал в кластер, эту проверку нужно проводить:

  • в качестве Git pre-commit — до передачи ресурса в Git;
  • в конвейере CI/CD — до мерджа ветки в основную;
  • в конвейере CI/CD — до передачи ресурса в кластер.

Применение политик с помощью Conftest


Conftest — это бинарный инструмент и фреймворк для тестирования конфигурационных данных, который можно использовать для проверки манифестов в Kubernetes. Тесты описываются с помощью специализированного языка запросов Rego.

Для установки Conftest следуйте инструкциям на сайте проекта. На момент написания статьи доступна версия Conftest 0.19.0.

Определим две политики:

package main

deny[msg] {
  input.kind == "Deployment"
  image := input.spec.template.spec.containers[_].image
  not count(split(image, ":")) == 2
  msg := sprintf("image '%v' doesn't specify a valid tag", [image])
}

deny[msg] {
  input.kind == "Deployment"
  image := input.spec.template.spec.containers[_].image
  endswith(image, "latest")
  msg := sprintf("image '%v' uses latest tag", [image])
}

Догадаетесь, что они проверяют? Оба правила применяются только к Deployment и предназначены для извлечения имени образа из раздела spec.container. Первое правило контролирует, есть ли в образе заданный тег.

package main

deny[msg] {
  input.kind == "Deployment"
  image := input.spec.template.spec.containers[_].image
  not count(split(image, ":")) == 2
  msg := sprintf("image '%v' doesn't specify a valid tag", [image])
}

deny[msg] {
  input.kind == "Deployment"
  image := input.spec.template.spec.containers[_].image
  endswith(image, "latest")
  msg := sprintf("image '%v' uses latest tag", [image])
}

Второе правило контролирует, что заданный тег — не latest.

package main

deny[msg] {
  input.kind == "Deployment"
  image := input.spec.template.spec.containers[_].image
  not count(split(image, ":")) == 2
  msg := sprintf("image '%v' doesn't specify a valid tag", [image])
}
 
deny[msg] {
  input.kind == "Deployment"
  image := input.spec.template.spec.containers[_].image
  endswith(image, "latest")
  msg := sprintf("image '%v' uses latest tag", [image])
}

Два блока  deny подтверждают несоответствие, если их значение оказывается true.

Обратите внимание: если блоков deny несколько, Conftest проверяет их независимо друг от друга, и общий результат — нарушение хотя бы в одном из блоков.

Теперь сохраним файл как check_image_tag.rego и выполним conftest для манифеста deployment.yaml:

$ conftest test -p conftest-checks test-data/deployment.yaml
FAIL – test-data/deployment.yaml - image 'hashicorp/http-echo' doesn't specify a valid tag
FAIL – test-data/deployment.yaml - image 'hashicorp/http-echo:latest' uses latest tag
2 tests, 0 passed, 0 warnings, 2 failures

Отлично, он обнаружил оба нарушения.

Поскольку Conftest —  статический бинарный инструмент, можно запустить проверку до отправки YAML-файла в кластер.

Если вы уже используете конвейер CI/CD для внесения изменений в кластер, то можно добавить еще одно действие, которое проверяет все ресурсы на соответствие политикам Conftest.

Но помешает ли это кому-нибудь отправить Deployment с тегом latest? Конечно, любой человек с необходимыми правами может создать рабочую нагрузку в вашем кластере и пропустить действия конвейера CI/CD.

Если вы можете успешно выполнить kubectl apply -f deployment.yaml, то можно игнорировать Conftest, и кластер будет запускать образы с тегом latest.

Как помешать кому-либо обойти ваши политики? К статической проверке можно добавить динамические, развернутые внутри кластера, чтобы отклонить ресурс после его отправки в кластер.

API Kubernetes 


Давайте вспомним, что происходит, когда вы создаете такой под в кластере:

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  containers:
  - name: sise
    image: learnk8s/app:1.0.0
    ports:
    - containerPort: 8080

Далее вы разворачиваете его командой:

$kubectl apply -f pod.yaml

Потом происходит следующее:
  1. Определение YAML отправляется на сервер API и сохраняется в etcd.
  2. Планировщик закрепляет под за узлом.
  3. kubelet извлекает спецификации пода и создает его.

По крайней мере, таково общее описание. Давайте подробнее рассмотрим, как работает первый пункт:
  1. kubectl apply -f deployment.yaml используется для отправки запроса в Control Plane для развертывания трех реплик.
  2. API получает запрос и данные JSON — kubectl преобразовал ресурс YAML в JSON.
  3. Сервер API сохраняет определение объекта в базе данных.
  4. Определение YAML теперь хранится в etcd.

Но действительно ли под капотом всё так просто? Что произойдет, если в YAML будет опечатка? Что мешает вам отправлять неработающие ресурсы в etcd?

Когда вы набираете kubectl apply, в дело вступает двоичный файл kubectl. Он:
  1. проверяет ресурс со стороны клиента — нет ли очевидной ошибки;
  2. преобразует данные YAML в JSON;
  3. считывает конфигурационные данные из KUBECONFIG;
  4. отправляет запрос с полезными данными на kube-apiserver.

Когда kube-apiserver получает запрос, он не сразу сохраняет его в etcd. Сначала ему нужно убедиться в легитимности инициатора запроса. Иными словами, он должен проверить подлинность запроса.

У вас есть разрешение на создание ресурсов после аутентификации? Ведь аутентификация и авторизация — это не одно и то же. Доступ к кластеру не означает, что вы можете создавать или считывать все ресурсы.

Для авторизации часто применяется управление доступом на основе ролей (RBAC). С его помощью можно назначать отдельные разрешения и ограничивать действия пользователя или приложения. На этом этапе аутентификацию и авторизацию выполняет kube-apiserver.

API-сервер часто рассматривают как единый блок. Но это не так, на самом деле он состоит из нескольких компонентов:



Рассмотрим, как взаимодействуют эти компоненты:
  1. HTTP-обработчик принимает и обрабатывает HTTP-запросы.
  2. Затем API верифицирует вызывающую сторону, спрашивая: «Вы пользователь кластера?»

    Но даже если вы авторизовались, это не гарантирует доступ ко всем ресурсам. Возможно, вы можете создавать поды, но не секреты. На этом этапе ваша учетная запись пользователя проверяется на соответствие правилам RBAC. 

А после этих проверок можно наконец сохранить определение пода в etcd? Не спешите. kube-apiserver работает как конвейер. Запрос проходит ряд компонентов и только потом сохраняется в базе данных.

Авторизация и аутентификация — это первые два компонента, но они не единственные. До попадания в базу данных объект перехватывают контроллеры допуска:


На этом этапе можно выполнить дальнейшие проверки текущего ресурса. А в Kubernetes по умолчанию включено множество контроллеров допуска.

Все контроллеры, включенные в minikube, можно проверить с помощью команды kubectl -n kube-system describe pod kube-apiserver-minikube. Результат должен содержать флаг --enable-admission-plugins и список контроллеров.

Для примера давайте посмотрим на контроллер допуска NamespaceLifecycle.

Проверяющие контроллеры допуска


Контроллер допуска NamespaceLifecycle не позволяет создавать поды в еще не существующих пространствах имен.

Под с пространством имен можно определить следующим образом:
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
  namespace: does-not-exist
spec:
  containers:
  - name: sise
    image: learnk8s/app:1.0.0
    ports:
    - containerPort: 8080

Определение YAML допустимо, поэтому после проверки kubectl запрос отправляется в кластер:
$ kubectl apply -f pod-namespaced.yaml

Предположим, вы прошли проверку подлинности и авторизованы. Тогда запрос попадает на проверку к контроллеру допуска NamespaceLifecycle. Пространство имен
does-not-exist
не существует и в конечном счете отклоняется. 

Кроме того, контроллер допуска NamespaceLifecycle останавливает запросы, которые могут удалить пространства имен defaultkube-system и kube-public.

Контроллеры, которые проверяют действия и ресурсы, относятся к категории проверяющих (validating). Есть и другая категория контроллеров — изменяющие (mutatuing).

Изменяющие контроллеры допуска


Изменяющие контроллеры могут проверять запрос и изменять его. Пример такого контроллера — DefaultStorageClass.

Предположим, вам нужно создать Persistent Volume Claim (PVC):

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 3Gi

В консоли это выглядит так:
$ kubectl apply -f pvc.yaml

Если проверить список Persistent Volume Claims с помощью kubectl get pvc, то можно заметить, что у тома есть свойство Bound и он относится к «стандартному» классу хранилища.
NAME    STATUS VOLUME       CAPACITY ACCESS MODES STORAGECLASS 
my-pvc  Bound  pvc-059f2da2 3Gi      RWO          standard
AGE       
3s

Но вы не задавали никакой «стандартный» StorageClass в файле YAML. Так ведь?
Определение YAML для Persistent Volume Claim можно просмотреть с помощью:
$ kubectl get pvc my-pvc -0 yaml

Если присмотреться к определению, то можно заметить, что оно содержит несколько дополнительных полей:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-pvc
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 3Gi
  storageClassName: standard
  volumeMode: Filesystem
  volumeName: pvc-059f2da2-a216-42b7-875e-e7da327605dd

Имя standard не закодировано в API. Напротив, имя StorageClass по умолчанию включается в spec.storageClassName. Имя StorageClass по умолчанию (default) можно извлечь из кластера следующим образом:
$ kubectl get storageclass
NAME                 PROVISIONER                 RECLAIMPOLICY   
standard (default)   k8s.io/minikube-hostpath    Delete  
VOLUMEBINDINGMODE   AGE        
Immediate           8m

Если имя StorageClass по умолчанию «aws-ebs», то контроллер допуска DefaultStorageClass включил бы его вместо имени «standard».

В Kubernetes есть несколько изменяющих и проверяющих контроллеров допуска. Полный список можно найти в официальной документации.

Пройдя проверку контроллеров допуска, запрос сохраняется в etcd. То есть мы можем расширить структуру API-сервера еще несколькими компонентами:


  1. Изменяющие контроллеры допуска вызываются первыми.
  2. Измененный ресурс направляется на проверку соответствия схеме. Здесь API проверяет корректность ресурса. Под все еще выглядит как под? Все ли нужные поля на месте?
  3. На последнем этапе в игру вступают проверяющие контроллеры допуска. После этого компонента ресурс сохраняется в etcd.

Но что если вам нужна специализированная проверка или ресурсы нужно изменять в соответствии с вашими правилами? Контроллеры допуска поддерживают возможности расширения.

Вы можете использовать стандартные контроллеры допуска Kubernetes или подключить свои собственные.

Контроллеры доступа MutationAdmissionWebhook и ValidationAdmissionWebhook являются программируемыми — они ничего не делают сами. Компонент можно зарегистрировать на вебхуке Mutation или Validation, и эти контроллеры будут вызывать его, когда запрос оказывается на этапе допуска.



Можно написать компонент, который проверяет, использует ли текущий под образ контейнера, взятый из частного реестра. Можно зарегистрировать его как часть ValidationAdmissionWebhook и принимать или отклонять запросы в зависимости от образа контейнера.

Именно это и делает Gatekeeper: он регистрируется в кластере как компонент и проверяет запросы.

Применение политик посредством Gatekeeper


С помощью Gatekeeper администратор Kubernetes может внедрять политики, обеспечивающие соблюдение заданных требований в кластере. Gatekeeper регистрируется в качестве контроллера с помощью вебхука проверки в API Kubernetes.



Любой ресурс, отправленный в кластер, перехватывается и проверяется на соответствие списку действующих политик.

Кроме того, Gatekeeper использует родные концепции Kubernetes вроде определения пользовательских ресурсов (CRD), поэтому политики управляются как ресурсы Kubernetes. Чтобы узнать больше по этой теме, ознакомьтесь с документом Google Cloud.

Внутри Gatekeeper использует Open Policy Agent (OPA) для реализации основного механизма политик, а сами политики написаны на языке Rego, которым пользуется Conftest.

Теперь опробуем Gatekeeper на практике. Вам понадобится доступ к кластеру Kubernetes с правами администратора, который можно настроить с помощью minikube. Если вы работаете на Windows, можно воспользоваться нашим удобным руководством по установке minikube и Docker.

После настройки коммуникации kubectl с кластером настройте Gatekeeper:
$ kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/master/deploy/gatekeeper.yaml

Проверьте корректность настройки:
$ kubectl -n gatekeeper-system describe svc gatekeeper-webhook-service
Name:              gatekeeper-webhook-service
Namespace:         gatekeeper-system
Labels:            gatekeeper.sh/system=yes
Annotations:       ...
Type:              ClusterIP
IP:                10.102.199.165
Port:              <unset>  443/TCP
TargetPort:        8443/TCP
Endpoints:         172.18.0.4:8443
# more output ...

Этот сервис вызывается API Kubernetes во время обработки запроса на этапе проверки допуска. Теперь все ваши поды, Deployments, сервисы и т. д. перехватываются и тщательно проверяются Gatekeeper.

Определение многократно используемых политик с помощью ConstraintTemplate


В Gatekeeper нужно сначала создать политику с использованием пользовательского ресурса ConstraintTemplate.

Рассмотрим пример. Это определение ConstraintTemplate отклоняет любое развертывание, использующее тег latest:

apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: k8simagetagvalid
spec:
  crd:
    spec:
      names:
        kind: K8sImageTagValid
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8simagetagvalid

        violation[{"msg": msg, "details":{}}] {
          image := input.review.object.spec.template.spec.containers[_].image
          not count(split(image, ":")) == 2
          msg := sprintf("image '%v' doesn't specify a valid tag", [image])
        }

        violation[{"msg": msg, "details":{}}] {
          image := input.review.object.spec.template.spec.containers[_].image
          endswith(image, "latest")
          msg := sprintf("image '%v' uses latest tag", [image])
        }

Эта политика похожа на предыдущую, которую мы использовали с Conftest. 

Взгляните:

package main

deny[msg] {
  input.kind == "Deployment"
  image := input.spec.template.spec.containers[_].image
  not count(split(image, ":")) == 2
  msg := sprintf("image '%v' doesn't specify a valid tag", [image])
}

deny[msg] {
  input.kind == "Deployment"
  image := input.spec.template.spec.containers[_].image
  endswith(image, "latest")
  msg := sprintf("image '%v' uses latest tag", [image])
}


И все же тут есть несколько тонких важных различий. Объект ввода имеет вид input.review.object, а не input, и здесь нет необходимости заявлять его тип.

apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: k8simagetagvalid
spec:
  crd:
    spec:
      names:
        kind: K8sImageTagValid
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8simagetagvalid

        violation[{"msg": msg, "details":{}}] {
          image := input.review.object.spec.template.spec.containers[_].image
          not count(split(image, ":")) == 2
          msg := sprintf("image '%v' doesn't specify a valid tag", [image])
        }

        violation[{"msg": msg, "details":{}}] {
          image := input.review.object.spec.template.spec.containers[_].image
          endswith(image, "latest")
          msg := sprintf("image '%v' uses latest tag", [image])
        }

Правило deny переименовано в «нарушение» (violation).

В Conftest используется сигнатура правила Deny[msg] {...}, а в Gatekeeper — violation[{"msg": msg}] {...}.

apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: k8simagetagvalid
spec:
  crd:
    spec:
      names:
        kind: K8sImageTagValid
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8simagetagvalid

        violation[{"msg": msg, "details":{}}] {
          image := input.review.object.spec.template.spec.containers[_].image
          not count(split(image, ":")) == 2
          msg := sprintf("image '%v' doesn't specify a valid tag", [image])
        }

        violation[{"msg": msg, "details":{}}] {
          image := input.review.object.spec.template.spec.containers[_].image
          endswith(image, "latest")
          msg := sprintf("image '%v' uses latest tag", [image])
        }

Блок с правилом о нарушении имеет специфическую сигнатуру — объект с двумя свойствами.
Первое свойство, msg — строка, второе — объект details, который включает произвольные свойства и который можно дополнить любым значением. В нашем случае это пустой объект.

Оба используются как возвращаемые значения:
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: k8simagetagvalid
spec:
  crd:
    spec:
      names:
        kind: K8sImageTagValid
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8simagetagvalid

       violation[{"msg": msg, "details":{}}] {
          image := input.review.object.spec.template.spec.containers[_].image
          not count(split(image, ":")) == 2
          msg := sprintf("image '%v' doesn't specify a valid tag", [image])
        }

       violation[{"msg": msg, "details":{}}] {
          image := 
input.review.object.spec.template.spec.containers[_].image
          endswith(image, "latest")
          msg := sprintf("image '%v' uses latest tag", [image])
        }

Теперь создаем ConstraintTemplate:
$kubectl apply -f templates/check_image_tag.yaml
constrainttemplate.templates.gatekeeper.sh/k8simagetagvalid created

Можно запустить kubectl describe, чтобы направить запрос к шаблону из кластера:
$ kubectl describe constrainttemplate.templates.gatekeeper.sh/k8simagetagvalid
Name:         k8simagetagvalid
Namespace:
Labels:       <none>
Annotations:  kubectl.kubernetes.io/last-applied-configuration:            {"apiVersion":"templates.gatekeeper.sh/v1beta1","kind":"ConstraintTemplate","metadata":               {"annotations":{},"name":"k8simagetagvalid"},"spec"...
API Version:  templates.gatekeeper.sh/v1beta1
Kind:         ConstraintTemplate
# more output ...

Однако ConstraintTemplate нельзя использовать для проверки развертываний. Это просто определение политики, которая может быть реализована только путем создания Constraint.

Создание ограничения


Constraint — это способ показать: «Я хочу применить эту политику к кластеру». ConstraintTemplates можно сравнить с книгой рецептов. Рецептов печенья и выпечки может быть сотни, но ими сыт не будешь. Чтобы испечь торт, нужно выбрать рецепт и смешать ингредиенты. Ограничения — это частный случай ConstraintTemplate.

Рассмотрим пример. Следующее ограничение Constraint использует ранее определенный ConstraintTemplate (рецепт) K8sImageTagValid:

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sImageTagValid
metadata:
  name: valid-image-tag
spec:
  match:
    kinds:
      - apiGroups: ["apps"]
        kinds: ["Deployment"]


Обратите внимание: Constraint ссылается на ConstraintTemplate и на то, к какому типу ресурсов его следует применить.

Объект spec.match определяет рабочие нагрузки, к которым применяется ограничение. Здесь у нас указано, что оно применяется к группе API apps, тип Deployment. Поскольку эти поля являются массивами, для них можно указать несколько значений и расширить проверки до StatefulSets, DaemonSets и т. д.

Сохраните вышеуказанное содержимое в новый файл и назовите его check_image_tag_constraint.yaml. Выполните kubectl apply, чтобы создать ограничение:
$ kubectl apply -f check_image_tag_constraint.yaml
k8simagetagvalid.constraints.gatekeeper.sh/valid-image-tag created

С помощью kubectl describe убедитесь, что ограничение было создано:
$ kubectl describe k8simagetagvalid.constraints.gatekeeper.sh/valid-image-tag
Name:         valid-image-tag
Namespace:
Labels:       <none>
Annotations:  kubectl.kubernetes.io/last-applied-configuration:
         {"apiVersion":"constraints.gatekeeper.sh/v1beta1","kind":"K8sImageTagValid","metadata:
                 {"annotations":{},"name":"valid-image-tag"},"spec":...
API Version:  constraints.gatekeeper.sh/v1beta1
Kind:         K8sImageTagValid
Metadata:
  Creation Timestamp:  2020-07-01T07:57:23Z
# more output...


Тестируем политику


Теперь давайте протестируем Deployment с двумя образами контейнеров:
$ kubectl apply -f deployment.yaml
Error from server ([denied by valid-image-tag] image 'hashicorp/http-echo' doesn't specify a valid tag
[denied by valid-image-tag] image 'hashicorp/http-echo:latest' uses latest tag): error when creating
"test-data/deployment.yaml": admission webhook "validation.gatekeeper.sh" denied the request:
[denied by valid-image-tag] image 'hashicorp/http-echo' doesn't specify a valid tag
[denied by valid-image-tag] image 'hashicorp/http-echo:latest' uses latest tag

Развертывание отклоняется политикой Gatekeeper. Обратите внимание, что проверка встроена в API Kubernetes. Ее нельзя пропустить или обойти.

Внедрение политик Gatekeeper в кластере с существующими рабочими нагрузками может быть сложным, так как сопряжено с угрозой срыва развертывания критически важных рабочих нагрузок из-за несоответствия требованиям.

Но Gatekeeper позволяет устанавливать ограничения в режиме пробного выполнения с указанием enforcementAction: dryrun в спецификации:
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sImageTagValid
metadata:
  name: valid-image-tag
spec:
  enforcementAction: dryrun
  match:
    kinds:
      - apiGroups: ["apps"]
        kinds: ["Deployment"]

В этом режиме политика не мешает развертыванию рабочих нагрузок, но регистрирует и фиксирует все нарушения в поле Violations команды kubectl describe:
$ kubectl describe k8simagetagvalid.constraints.gatekeeper.sh/valid-image-tag
Name:         valid-image-tag
Namespace:
Labels:       <none>
Annotations:  kubectl.kubernetes.io/last-applied-configuration:
....
  Total Violations:  2
  Violations:
    Enforcement Action:  dryrun
    Kind:                Deployment
    Message:             image 'hashicorp/http-echo' doesn't specify a valid tag
    Name:                http-echo
    Namespace:           default
    Enforcement Action:  dryrun
    Kind:                Deployment
    Message:             image 'hashicorp/http-echo:latest' uses latest tag
    Name:                http-echo
    Namespace:           default
Events:                  <none>

Убедившись, что все манифесты соответствуют требованиям, можно убрать режим «пробного выполнения» и активно предотвращать нарушения.

А можно ли написать политику, согласно которой у Deployment должно быть две метки: project по текущему проекту и app для имени приложения?

Политика по обеспечению единообразных меток


Сначала нужно применить политику как проверку conftest:

package main

deny[msg] {
  input.kind == "Deployment"

  required := {"app", "project"}
  provided := {label | input.metadata.labels[label]}
  missing := required - provided

  count(missing) > 0
  msg = sprintf("you must provide labels: %v", [missing])
}

Давайте рассмотрим политику подробнее. required — это набор с двумя компонентами: app и project. Эти метки должны быть представлены во всех развертываниях.

package main

deny[msg] {
  input.kind == "Deployment"

  required := {"app", "project"}
  provided := {label | input.metadata.labels[label]}
  missing := required - provided

  count(missing) > 0
  msg = sprintf("you must provide labels: %v", [missing])
}

providedизвлекает набор меток, указанный во входных параметрах.

package main

deny[msg] {
  input.kind == "Deployment"

  required := {"app", "project"}
  provided := {label | input.metadata.labels[label]}
  missing := required - provided

  count(missing) > 0
  msg = sprintf("you must provide labels: %v", [missing])
}

Затем выполняется операция разности множеств и создается новое множество, содержащее метки, которые присутствуют в required, но отсутствуют в provided.

 package main

deny[msg] {
  input.kind == "Deployment"

  required := {"app", "project"}
  provided := {label | input.metadata.labels[label]}
  missing := required - provided

  count(missing) > 0
  msg = sprintf("you must provide labels: %v", [missing])
}

Если количество элементов в этом множестве больше 0, то правило нарушено. Это достигается за счет использования функции count() для проверки количества элементов в множестве missing. Выполните Conftest, указав эту политику, и вы увидите ошибку:

$ conftest test -p conftest-checks/check_labels.rego test-data/deployment.yaml
FAIL - test-data/deployment.yaml - you must provide labels: {"project"}
1 test, 0 passed, 0 warnings, 1 failure

Проблему можно исправить, добавив метки project и app:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: http-echo
  labels:
    app: http-echo
    project: test
spec:
  replicas: 2
  selector:
    matchLabels:
      app: http-echo
  template:
    metadata:
      labels:
        app: http-echo
    spec:
      containers:
      - name: http-echo
        image: hashicorp/http-echo
        args: ["-text", "hello-world"]
        ports:
        - containerPort: 5678

      - name: http-echo-1
        image: hashicorp/http-echo:latest
        args: ["-text", "hello-world"]
        ports:
        - containerPort: 5678

Что происходит, если развернуть ресурс непосредственно в кластере? Политику можно будет обойти. Давайте добавим ту же политику в Gatekeeper.

Сначала создайте ConstraintTemplate:

apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: k8srequiredlabels
spec:
  crd:
    spec:
      names:
        kind: K8sRequiredLabels
      validation:
        # Schema for the `parameters` field
        openAPIV3Schema:
          properties:
            labels:
              type: array
              items: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequiredlabels

       violation[{"msg": msg, "details": {"missing_labels": missing}}] {
          provided := {label | input.review.object.metadata.labels[label]}
          required := {label | label := input.parameters.labels[_]}
          missing := required - provided
          count(missing) > 0
          msg := sprintf("you must provide labels: %v", [missing])
        }

Из приведенного выше манифеста видно, как указать входные параметры для шаблона constraint, чтобы настроить его поведение во время выполнения.

Возвращаясь к нашему сравнению ConstraintTemplate с рецептом, с помощью входных параметров можно выбирать определенные ингредиенты рецепта. Однако эти ингредиенты должны соответствовать некоторым правилам.

Например, можно разрешить только массив строк в качестве входных данных для функции ConstraintTemplate. Можно описать входные параметры по схеме OpenAPIV3 в объекте validation в ConstraintTemplate:

apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: k8srequiredlabels
spec:
  crd:
    spec:
      names:
        kind: K8sRequiredLabels
      validation:
        # Schema for the `parameters` field
        openAPIV3Schema:
          properties:
            labels:
              type: array
              items: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequiredlabels

        violation[{"msg": msg, "details": {"missing_labels": missing}}] {
          provided := {label | input.review.object.metadata.labels[label]}
          required := {label | label := input.parameters.labels[_]}
          missing := required - provided
          count(missing) > 0
          msg := sprintf("you must provide labels: %v", [missing])
        }

Эта схема определяет, что шаблон constraint должен иметь только один параметр, labels, который представляет собой массив строк. Все предоставленные входные параметры доступны ограничению через атрибут input.parameters.

Сохраните приведенный выше манифест в файл check_labels.yaml, а затем выполните kubectl apply, чтобы создать шаблон constraint:

$ kubectl apply -f check_labels.yaml
constrainttemplate.templates.gatekeeper.sh/k8srequiredlabels created

Чтобы использовать политику из ConstraintTemplate, нужен Constraint. Создайте новый файл check_labels_constraints.yaml со следующим содержимым:

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
  name: deployment-must-have-labels
spec:
  match:
    kinds:
      - apiGroups: ["apps"]
        kinds: ["Deployment"]
  parameters:
    labels: ["app", "project"]

Выполните kubectl apply, чтобы создать ограничение:
$ kubectl apply -f check_labels_constraints.yaml
k8srequiredlabels.constraints.gatekeeper.sh/deployment-must-have-labels created

На этом этапе в вашем кластере создано два ограничения:
  1. Первое ограничение проверяет, использует ли развертывание образ с допустимым тегом.
  2. Второе ограничение проверяет, использует ли развертывание две метки: app и project.

Теперь попробуйте создать развертывание, описанное в начале статьи:
$ kubectl apply -f deployment.yaml
Error from server ([denied by deployment-must-have-labels] you must provide labels: {"project"}
[denied by valid-image-tag] image 'hashicorp/http-echo' doesn't specify a valid tag
[denied by valid-image-tag] image 'hashicorp/http-echo:latest' uses latest tag): error when creating
"deployment.yaml": admission webhook "validation.gatekeeper.sh" denied the request:
[denied by deployment-must-have-labels] you must provide labels: {"project"}
[denied by valid-image-tag] image 'hashicorp/http-echo' doesn't specify a valid tag
[denied by valid-image-tag] image 'hashicorp/http-echo:latest' uses latest tag

Создать развертывание не удается, поскольку у него нет необходимой метки project и оно не использует допустимый тег образа. Готово!

Правильный манифест развертывания, которое будет успешно развернуто, имеет две метки (app и project), а также образы с тегами использования:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: http-echo
  labels:
    app: http-echo
    project: test
spec:
  replicas: 2
  selector:
    matchLabels:
      app: http-echo
  template:
    metadata:
      labels:
        app: http-echo
    spec:
      containers:
      - name: http-echo
        image: hashicorp/http-echo:0.2.3
        args: ["-text", "hello-world"]
        ports:
        - containerPort: 5678

      - name: http-echo-1
        image: hashicorp/http-echo:0.2.1
        args: ["-text", "hello-world"]
        ports:
        - containerPort: 5678

Вы проверили Deployment с помощью статической и динамической проверки. Статическая на ранних этапах может быстро выявить нарушение правил, и при этом никто не обойдет политику, отправляя ресурсы непосредственно в кластер.

Заключение


И Conftest, и Gatekeeper используют язык Rego для определения политик, именно поэтому эти два инструмента становятся привлекательным решением для применения внекластерных и внутрикластерных проверок соответственно.

Но, как мы видели выше, чтобы политика Conftest Rego работала с Gatekeeper, необходимо внести несколько изменений. Проект konstraint призван в этом помочь. В его основе лежит идея, что ваш источник истины — это написанная на Rego политика для Conftest, согласно которой генерируются ресурсы ConstraintTemplate и Constraint для Gatekeeper.

Konstraint автоматизирует ручные действия, связанные с преобразованием политики, написанной для Conftest, в политику, которая работает в Gatekeeper.

Кроме того, konstraint упрощает тестирование шаблонов constraint и создание ограничений в Gatekeeper.

Conftest и Gatekeeper — это не единственные решения для применения внекластерных и внутрикластерных политик. Привлекательность этих двух решений заключается в том, что для применения политик можно использовать Rego для обоих инструментов. Можно даже пойти дальше и реализовать подмножество соответствующих политик внутри кластера и за его пределами.

Такой же уровень применения политик достигается с помощью аналогичного решения polaris, в котором реализованы функциональные возможности применения внекластерной и внутрикластерной политики.

Мы развиваем наш собственный Kubernetes aaS, о нем рассказывали в этой статье. И у нас тоже есть Gatekeeper, который позволяет реализовать управление рабочими нагрузками. Будем признательны, если вы его протестируете и дадите обратную связь. Для тестирования пользователям при регистрации начисляем 3000 бонусных рублей.


Что еще почитать:
  1. Рабочие узлы Kubernetes: много маленьких или несколько больших?
  2. Устранение неполадок в Kubernetes: в каком направлении двигаться, если что-то идет не так
  3. Telegram-канал с новостями о Kubernetes
Источник: https://habr.com/ru/company/vk/blog/646463/


Интересные статьи

Интересные статьи

В этой статье мы рассмотрим, как с помощью Open Source-утилиты werf собрать Docker-образ простого приложения и развернуть его в кластере Kubernetes, а также с легкостью накатывать изменения в его коде...
Обычно вы можете запустить HAProxy Kubernetes Ingress Controller как pod внутри Kubernetes-кластера. Как pod, он имеет доступ к другим pod, потому что они используют внутреннюю сеть Kubernetes-кластер...
Привет, меня зовут Андрей Щукин, я помогаю крупным компаниям мигрировать сервисы и системы в Облако КРОК. Вместе с коллегами из компании Southbridge, которая проводит в учебном центре «Слёрм»...
29 ноября прошла конференция @Kubernetes, организованная Mail.ru Cloud Solutions. Конференция выросла из митапов @Kubernetes — и стала четвёртым событием серии. Мы собрали в Mail.ru Group бол...
Kubernetes в значительной мере упрощает эксплуатацию приложений. Он забирает на себя ответственность за развертывание, масштабирование и отработку отказов, а декларативная природа описания ре...