«Спустись в кратер Екуль Снайфедльс, который тень Скартариса ласкает перед июльскими календами, отважный странник, и ты достигнешь центра Земли. Это я совершил, – Арне Сакнуссем».
Путешествие к центру земли. Жюль Верн

Ступая по тропе изучения новой технологии, порой полезно обернуться назад и оценить, насколько тернист был этот путь, сделать выводы и только после этого уверенно двигаться дальше. Так как Kubernetes — довольно сложная система, то и требования к уверенному знанию своего устройства и бережному отношению к конфигурациям у нее соответствующие. Как опытный путешественник не покоряет горные вершины без инструментов первой необходимости, так и работающий с Kubernetes инженер не обходится без приложений, страхующих его в повседневной работе.
В этой статье мы протестируем несколько утилит для валидации Kubernetes манифестов и, сравнив их между собой, попробуем ответить на вопрос — возможно ли избавиться от мисконфигураций на разных этапах подготовки деплоя приложения.
Меня зовут Артём, я DevOps-инженер SimbirSoft. Наверное, самым большим страхом при взаимодействии с любой кластерной системой я бы назвал вероятность обвалить всё и вся без возможности восстановления. Но обычно всё гораздо более приземленно. Незаданные лимиты и реквесты превращают приложения в черные ящики для шедулера Kubernetes, кто-то оставляет запуск контейнера от пользователя root. В комбинации с базовым образом Ubuntu и доступом к хостовой системе это даёт возможность злоумышленнику не просто запустить вместо бизнес-нагрузки вредоносный код, но и спокойно написать и отладить целый пет-проект для последующего трудоустройства в вашу организацию. Из таких еле заметных мелочей, как снежный ком, образуются проблемы, которые очень сложно разгрести на поздних этапах развития проекта, поэтому проверять всё нужно ещё до деплоя в кластер.
Осуществляются такие проверки с помощью валидаторов и линтеров. Валидаторы проверяют код (в нашем случае yaml манифесты) на соответствие спецификации Kubernetes, а линтеры – на ошибки синтаксиса и best-practice. С внедрением технологий контейнеризации и практик DevOps широкое применение получили системы CI/CD, позволяющие освободить разработчиков и инженеров от рутинных задач проверки, сборки и деплоя кода. Одной из таких систем является GitLab CI, на примере которого мы и будем внедрять валидаторы и линтеры Kubernetes.
Итак, меньше слов – больше дела.
Подопытные
Без проблемы не появится и решение. Поэтому был написан простенький манифест Kubernetes (parrot.yaml), запускающий веб-сервер Nginx, отображающий html-страничку с гифкой попугая, а также Service и Ingress, обеспечивающие доступ до приложения снаружи кластера. Далее по тексту будем называть наше «приложение» Parrot.

---
apiVersion: apps/v1
kind: Deployment
metadata:
name: bird
spec:
replicas: 3
selector:
matchLabels:
app.kubernetes.io/name: parrot-app
template:
metadata:
labels:
app.kubernetes.io/name: parrot-app
spec:
containers:
- name: bird
image: vregret/parrot:0.5
imagePullPolicy: Always
ports:
- name: http
protocol: TCP
containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: parrot
labels:
app: parrot
app.kubernetes.io/name: parrot-app
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: parrot-app
ports:
- port: 80
name: http
protocol: TCP
targetPort: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: parrot
labels:
app: parrot
app.kubernetes.io/name: parrot-app
spec:
rules:
- host: parrot.aperture.loc
http:
paths:
- path: /
pathType: ImplementationSpecific
backend:
service:
name: parrot
port:
number: 80
Так как сейчас развертывание рабочих нагрузок простыми манифестами некий моветон – был собран простенький helm чарт (parrot-app) под наше приложение. С добавлением обычных для репозитория Gitlab CI компонентов наш тестовый проект выглядит примерно так:

Все шаблоны для Job CI, включающих линтеры и валидаторы, мы будем хранить в .gitlab-ci/jobs/integration-kubernetes.yaml, благодаря чему впоследствии их можно будет вынести в отдельный репозиторий и ссылаться на них из .gitlab-ci.yml, обеспечив модульность нашего пайплайна.
Первые шаги
Итак, теперь мы готовы перейти к постепенной настройке нашего пайплайна и описанию утилит. Каждую из них будем сразу интегрировать в наш проект как для Raw манифеста, так и для Helm чарта приложения. Для удобства определим что каждый Stage нашего пайплайна – проверка отдельной утилитой.
1. Kubeconform
Описание: валидатор синтаксиса Kubernetes манифестов
Тюнинг параметров: возможность указать версию Kubernetes и json схему, согласно которым будет производиться проверка.
Возможность тестирования кластера: отсутствует
Замеченные особенности: отсутствует
Данная утилита является духовным наследником утилиты kubeval (которая, судя по официальному репозиторию в github, перестала развиваться и превратилась в kubeconform). Утилита предназначена для базовой проверки Kubernetes манифестов на основании их json схемы, которую заботливо тянет извне (мы можем переопределить источник схемы параметром -schema-location). При использовании с параметром -strict , kubeconform способна проверить манифест на наличие некорректных значений, выбивающихся из json схемы (достаточно удобно, если у инженера соскочила рука и в спецификации Pod внезапно оказалась таска Ansible). Установка и запуск достаточно тривиальны, по этому сразу опишем их в CI/CD.
# .gitlab-ci/jobs/integration-kubernetes.yaml
.kubeconform_lint:
image:
name: dtzar/helm-kubectl:3.7.2
variables:
KUBECONFORM_VERSION: "v0.6.1"
KUBECONFORM_RAW_PATH: ''
KUBECONFORM_HELM_PATH: ''
before_script:
# Install kubeconform
- wget https://github.com/yannh/kubeconform/releases/download/${KUBECONFORM_VERSION}/kubeconform-linux-amd64.tar.gz
- tar xf kubeconform-linux-amd64.tar.gz
- mv kubeconform /usr/local/bin
script:
- >
if [[ -z ${KUBECONFORM_HELM_PATH} ]]; then
kubeconform -strict -verbose ${KUBECONFORM_RAW_PATH};
elif [[ -z ${KUBECONFORM_RAW_PATH} ]]; then
helm dependency build ${KUBECONFORM_HELM_PATH};
helm template ${KUBECONFORM_HELM_PATH} | kubeconform -strict -verbose - ;
fi
# .gitlab-ci.yml
raw-01:
stage: kubeconform
variables:
KUBECONFORM_RAW_PATH: 'kube/raw/*'
tags:
- docker
allow_failure: true
extends: .kubeconform_lint
helm-01:
stage: kubeconform
variables:
KUBECONFORM_HELM_PATH: 'kube/helm/parrot-app'
tags:
- docker
allow_failure: true
extends: .kubeconform_lint
После прохождения пайплайна мы увидим примерно такой вывод:

Как мы видим, kubeconform справился с проверкой синтаксиса манифеста. Сам вывод валидатора можно менять с помощью аргумента –output=json,junit,tap,text. Однако давайте посмотрим в сторону Helm чарта.
Судя по приборам – всё отлично.

Однако при попытке применить данный helm чарт в Kubernetes получим следующую ошибку:

Выполним команду helm template и посмотрим на результаты рендера чарта.
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 3
readinessProbe:
{}
startupProbe:
null
Теперь взглянем на переменные чарта.
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 3
readinessProbe: {}
startupProbe:
Судя по всему, мы забыли указать значение для startupProbe, а значение readinessProbe сделали равным {}, что вызвало ошибку при проверке API Kubernetes. Данную проблему можно решить, используя конструкцию if в go-template Helm чарта, но было бы неплохо проверять такие моменты на этапе CI/CD. Для этого можно использовать kubectl apply –dry-run=server, если у нас в есть кластер, через API которого можно произвести проверку. Если такого нет, нужна утилита, которой мы бы смогли описать свои пожелания в виде правил. А значит – двигаемся дальше.
2. Conftest
Описание: утилита для написания тестов конфигураций.
Тюнинг параметров: файл конфигурации, файлы политик по которым происходит проверка.
Возможность тестировать кластера: отсутствует
Замеченные особенности: отсутствуют
Данная утилита не является в прямом смысле валидатором Kubernetes манифестов – её функционал распространяется заметно дальше и выходит за рамки статьи. Попробуем использовать её на благо нашего пайплайна.
# .gitlab-ci/jobs/integration-kubernetes.yaml
.conftest_lint:
image:
name: dtzar/helm-kubectl:3.7.2
variables:
CONFTEST_VERSION: "0.40.0"
CONFTEST_RAW_PATH: ''
CONFTEST_HELM_PATH: ''
CONFTEST_POLICY_PATH: ".policy/"
before_script:
# Install confest
- wget https://github.com/open-policy-agent/conftest/releases/download/v${CONFTEST_VERSION}/conftest_${CONFTEST_VERSION}_Linux_x86_64.tar.gz
- tar xf conftest_${CONFTEST_VERSION}_Linux_x86_64.tar.gz
- mv conftest /usr/local/bin
script:
- >
if [[ -z ${CONFTEST_HELM_PATH} ]]; then
conftest test ${CONFTEST_RAW_PATH} -p ${CONFTEST_POLICY_PATH};
elif [[ -z ${CONFTEST_RAW_PATH} ]]; then
helm dependency build ${CONFTEST_HELM_PATH};
helm template ${CONFTEST_HELM_PATH} | conftest test -p ${CONFTEST_POLICY_PATH} - ;
fi
# .gitlab-ci.yml
raw-02:
stage: conftest
variables:
CONFTEST_RAW_PATH: 'kube/raw/*'
CONFTEST_POLICY_PATH: ".policy/"
tags:
- docker
allow_failure: true
extends: .conftest_lint
helm-02:
stage: conftest
variables:
CONFTEST_HELM_PATH: 'kube/helm/parrot-app'
CONFTEST_POLICY_PATH: ".policy/"
tags:
- docker
allow_failure: true
extends: .conftest_lint
Примеры для написания своих политик есть в репозитории, а мы попробуем исправить недочеты предыдущего линтера и добавим еще правило для проверки наличия лейбла author (дабы знать героев в лицо, конечно же).
#./.policy/kubernetes.rego
package kubernetes
is_deployment {
input.kind = "Deployment"
}
#./.policy/deployment.rego
package main
import data.kubernetes
containers = input.spec.template.spec.containers[_]
name = input.metadata.name
kind = input.kind
required_labels {
input.metadata.labels[
"author"
]
}
livenessProbe {
containers.livenessProbe != {}
containers.livenessProbe != null
}
readinessProbe {
containers.readinessProbe != {}
containers.readinessProbe != null
}
startupProbe {
containers.startupProbe != {}
containers.startupProbe != null
}
deny[msg] {
kubernetes.is_deployment
not required_labels
msg = sprintf("%s - %s: Missing requirements labels", [kind, name])
}
deny[msg] {
kubernetes.is_deployment
containers.livenessProbe
not livenessProbe
msg = sprintf("%s - %s: %s in livenessProbe body ", [kind, name, containers.livenessProbe])
}
deny[msg] {
kubernetes.is_deployment
containers.readinessProbe
not readinessProbe
msg = sprintf("%s - %s: %s in readinessProbe body ", [kind, name, containers.readinessProbe])
}
deny[msg] {
kubernetes.is_deployment
containers.startupProbe
not startupProbe
msg = sprintf("%s - %s: %s in startupProbe body ", [kind, name, containers.startupProbe])
}
Здесь мы проверяем наличие probes и null или {} в нужных точках Yaml файла + наличие необходимого лейбла. Вариантов вывода результатов у conftest также довольно много (---output=stdout, json, tap, table, junit, github), но воспользуемся стандартным. На выходе CI/CD видим красивое отображение нашей «заплатки».

Но зачем при написании политики мы указали, что проверять пробы на некорректное заполнение нужно только после проверки их наличия? Ведь отсутствие проверок работоспособности наших сервисов – само по себе преступление и не соответствует best practice! Однако в этой статье мы пытаемся разложить всё по полочкам, и описанные выше инструменты с задачей валидирования конфигураций справились. Для проверок на лучшие практики лучше использовать специальные инструменты, о которых мы как раз и поговорим далее.
3. Kube-score
Описание: линтер манифестов Kubernetes + проверка на соответствие лучшим практикам
Тюнинг параметров: исключение сущности из валидации или активация опциональных тестов с помощью аннотаций (https://github.com/zegl/kube-score#ignoring-a-test) и аргумента cli --ignore-test
Возможность тестирования кластера: отсутствует
Замеченные особенности: отсутствуют
# .gitlab-ci/jobs/integration-kubernetes.yaml
.kubescore_lint:
image:
name: dtzar/helm-kubectl:3.7.2
variables:
KUBESCORE_VERSION: 1.16.1
KUBESCORE_RAW_PATH: ''
KUBESCORE_HELM_PATH: ''
before_script:
# Install kubescore
- wget https://github.com/zegl/kube-score/releases/download/v${KUBESCORE_VERSION}/kube-score_${KUBESCORE_VERSION}_linux_amd64.tar.gz
- tar xf kube-score_${KUBESCORE_VERSION}_linux_amd64.tar.gz
- mv kube-score /usr/local/bin
script:
- >
if [[ -z ${KUBESCORE_HELM_PATH} ]]; then
kube-score score ${KUBESCORE_RAW_PATH} --output-format ci ;
elif [[ -z ${KUBESCORE_RAW_PATH} ]]; then
helm dependency build ${KUBESCORE_HELM_PATH} ;
helm template ${KUBESCORE_HELM_PATH} | kube-score score --output-format ci - ;
fi
# .gitlab-ci.yml
raw-03:
stage: kube-score
variables:
KUBESCORE_RAW_PATH: 'kube/raw/*'
tags:
- docker
allow_failure: true
extends: .kubescore_lint
helm-03:
stage: kube-score
variables:
KUBESCORE_HELM_PATH: 'kube/helm/parrot-app'
tags:
- docker
allow_failure: true
extends: .kubescore_lint
После прохождения пайплайна kube-score отобразит такой вывод:

Как мы видим, kube-score отметил проблемные места в нашем манифесте, затронув отсутствие лимитов и реквестов для CPU, Memory и Ephemeral Storage. Сам вывод может меняться в зависимости от аргумента командной строки --output-format (доступны варианты human, json, sarif и ci), что позволяет распарсить последующий вывод и выводить информацию в Merge Request.
Все найденные нами ошибки мы поправим далее по тексту, а сейчас перейдем к следующей утилите.
4. Kubelinter
Описание: линтер манифестов и Helm чартов Kubernetes + проверка на соответствие лучшим практикам
Тюнинг параметров: возможность редактирования списка проверок с помощью файла настроек, выбор директорий с файлами, игнорирование сущностей через аннотации, составление собственных проверок.
Возможность тестирования кластера: отсутствует
Замеченные особенности: не проверяет корректность согласно json k8s schema, что решается добавлением в пайплайн валидатора (например kubeconform).
Придерживаясь нашей традиции – запихнем и эту утилиту к нам в пайплайн.
# .gitlab-ci/jobs/integration-kubernetes.yaml
.kubelinter_lint:
image:
name: dtzar/helm-kubectl:3.7.2
variables:
KUBELINTER_VERSION: '0.4.0'
KUBELINTER_RAW_PATH: ''
KUBELINTER_HELM_PATH: ''
before_script:
# Install kube-linter
- wget https://github.com/stackrox/kube-linter/releases/download/${KUBELINTER_VERSION}/kube-linter-linux.tar.gz
- tar xf kube-linter-linux.tar.gz
- mv kube-linter /usr/local/bin
script:
- >
if [[ -z ${KUBELINTER_HELM_PATH} ]]; then
kube-linter lint ${KUBELINTER_RAW_PATH};
elif [[ -z ${KUBELINTER_RAW_PATH} ]]; then
helm dependency build ${KUBELINTER_HELM_PATH} ;
kube-linter lint ${KUBELINTER_HELM_PATH};
fi
# .gitlab-ci.yml
raw-04:
stage: kubelinter
variables:
KUBELINTER_RAW_PATH: 'kube/raw/*'
tags:
- docker
allow_failure: true
extends: .kubelinter_lint
helm-04:
stage: kubelinter
variables:
KUBELINTER_HELM_PATH: 'kube/helm/parrot-app'
tags:
- docker
allow_failure: true
extends: .kubelinter_lint
Посмотрим, как данная утилита справилась с проверкой.

Стоит отметить не совсем удобный, но подробный и подверженный парсингу вывод утилиты.
Также, как можно заметить по коду CI, – это первая утилита в нашем обзоре, которая способна проверять Helm чарты самостоятельно, без их предварительного рендеринга через helm template, что достаточно приятно. Отдельного внимания заслуживают umbrella (зонтичные) чарты – при их использовании, перед валидацией необходимо использовать команду helm dependency build.
Теперь мы переходим к более увесистым и любопытным утилитам, «тяжеловесам» нашего разбора.
5. Polaris
Описание: линтер синтаксиса Kubernetes манифестов и Helm чартов + проверка на соответствие лучшим практикам.
Тюнинг параметров: файл конфигурации, добавление собственных проверок, исключение из проверки сущностей с помощью аннотаций.
Возможность тестирования кластера: через CLI и admission webhook.
Замеченные особенности: была замечена проблема при одновременной работе версий 7.0.1 Polaris и >3.7.2 у Helm, на данный момент в версии 7.3.2 Polaris проблема исчезла.
Polaris предоставляет возможность использовать себя как CLI утилиту для проверки манифестов, дает возможность настраивать свои правила проверок, а также может быть запущен как Admission Controller для валидации приходящих в Kubernetes кластер конфигураций. Является проектом-участником CNCF. Также в наличии имеется симпатичный дашборд для отображения «погоды» в кластере:

Ну а мы переходим к адаптации данной утилиты для нашего CI/CD.
# .gitlab-ci/jobs/integration-kubernetes.yaml
.polaris_lint:
image:
name: dtzar/helm-kubectl:3.7.2
variables:
POLARIS_VERSION: 7.0.1
POLARIS_RAW_PATH: ''
POLARIS_HELM_PATH: ''
POLARIS_SCORE_LEVEL: 75
POLARIS_CONFIG: ''
before_script:
# Install polaris
- wget https://github.com/FairwindsOps/polaris/releases/download/${POLARIS_VERSION}/polaris_linux_amd64.tar.gz
- tar -xvzf ./polaris_linux_amd64.tar.gz
- mv ./polaris /bin/polaris
script:
- >
if [[ -z ${POLARIS_HELM_PATH} ]]; then
polaris audit --audit-path ${POLARIS_RAW_PATH} --only-show-failed-tests --set-exit-code-below-score ${POLARIS_SCORE_LEVEL} --format=pretty $(if [[ -z $POLARIS_CONFIG ]]; then echo ""; else echo --config $POLARIS_CONFIG; fi);
elif [[ -z ${POLARIS_RAW_PATH} ]]; then
polaris audit --helm-chart ${POLARIS_HELM_PATH} --only-show-failed-tests --set-exit-code-below-score ${POLARIS_SCORE_LEVEL} --format=pretty $(if [[ -z $POLARIS_CONFIG ]]; then echo ""; else echo --config $POLARIS_CONFIG; fi);
fi
# .gitlab-ci.yml
raw-05:
stage: polaris
variables:
POLARIS_RAW_PATH: './kube/raw'
POLARIS_SCORE_LEVEL: 75
POLARIS_CONFIG: './polaris-config.yaml'
tags:
- docker
allow_failure: true
extends: .polaris_lint
helm-05:
stage: polaris
variables:
POLARIS_HELM_PATH: './kube/helm/parrot-app'
POLARIS_SCORE_LEVEL: 70
tags:
- docker
allow_failure: true
extends: .polaris_lint
А теперь посмотрим, как данная утилита справилась с нашим заданием.

Помимо красивого и читаемого вывода (тип которого тоже изменяется через аргумент --format=pretty/json/yaml), здесь стоит обратить внимание на то, что несмотря на посредственные параметры нашего манифеста – проверка линтером посчиталась успешной. Связано это с тем, что в аргумент --set-exit-code-below-score мы передали значение 70. Повышая и понижая данное значение, мы можем регулировать то, при каком пороговом количестве очков линтинга Polaris посчитает манифест небезопасным и завершится ошибкой. Также стоит заметить, что конфигурация для Polaris представляет собой длинный yaml файл, что не всегда удобно, если хочется проигнорировать или изменить важность какого-нибудь этапа линта. Впрочем, ничего не мешает вытягивать его из отдельного репозитория в рамках CI.
6. Datree
Описание: линтер синтаксиса Kubernetes манифестов и Helm чартов + проверка на соответствие лучшим практикам.
Тюнинг параметров: policy-as-a-code, стандартные проверки на практики, пользовательские проверки, проверки на deprecated ресурсы и так далее.
Возможность тестирования кластера: через admission webhook.
Замеченные особенности: условно бесплатная, 1000 бесплатных проверок в месяц
Datree на самом деле уже не просто cli утилита, а целый проект, включающий в себя облачный дашборд, Admission Controller для Kubernetes и возможность мониторить сразу несколько кластеров, получая данные централизованно. Также есть возможность использовать её в GitOps подходе (совместно с ArgoCD, Flux и другими) и писать свои детальные правила валидации манифестов. К сожалению, за полный набор плюшек надо платить, но CLI инструментом можно пользоваться практически безвозмездно (1000 бесплатных проверок в месяц). Является проектом-участником CNCF.
Давайте приступим.
# .gitlab-ci/jobs/integration-kubernetes.yaml
.datree_lint:
image:
name: dtzar/helm-kubectl:3.7.2
variables:
DATREE_VERSION: 1.6.4-rc
DATREE_RAW_PATH: ''
DATREE_HELM_PATH: ''
before_script:
# Install datree
- wget https://github.com/datreeio/datree/releases/download/${DATREE_VERSION}/datree-cli_${DATREE_VERSION}_Linux_x86_64.zip
- unzip -n datree-cli_${DATREE_VERSION}_Linux_x86_64.zip
- mv datree /usr/local/bin
- datree config set offline local
script:
- >
if [[ -z ${DATREE_HELM_PATH} ]]; then
datree test ${DATREE_RAW_PATH} --no-record --ignore-missing-schemas ;
elif [[ -z ${DATREE_RAW_PATH} ]]; then
helm plugin install https://github.com/datreeio/helm-datree
helm dependency build ${DATREE_HELM_PATH} ;
helm datree test ${DATREE_HELM_PATH} --no-record --ignore-missing-schemas;
fi
# .gitlab-ci.yml
raw-06:
stage: datree
variables:
DATREE_RAW_PATH: 'kube/raw/*'
tags:
- docker
allow_failure: true
extends: .datree_lint
helm-06:
stage: datree
variables:
DATREE_HELM_PATH: 'kube/helm/parrot-app'
tags:
- docker
allow_failure: true
extends: .datree_lint
После выполнения пайплайна мы увидим вот такую картину:

Как мы видим, утилита успешно справилась с задачей и даже направила нам ссылку на просмотр политик. Там нас встретит окно входа в аккаунт Datree, но об этом далее по тексту. Также, как и у некоторых предыдущих утилит, можно менять вывод программы (--output=simple/yaml/json/xml/JUnit).
Мы же, не отвлекаясь, переходим к настоящему монстру валидации Kubernetes манифестов, да и самих кластеров – Kubescape.
7. Kubescape
Описание: линтер синтаксиса Kubernetes манифестов и Helm чартов + проверка на соответствие лучшим практикам.
Тюнинг параметров: настройки через аргументы командной строки
Возможность тестирования кластера: через cli
Замеченные особенности: отсутствуют
Данная утилита позволяет сканировать ваши конфигурации, кластера Kubernetes, описывает, какими CIS практиками это обусловлено, и заботливо выводит информацию о них в выводе. Также она позволяет генерировать отчеты в различных форматах, в том числе pdf и html. Похожим инструментом, только не для Kubernetes, а для UNIX подобных систем я бы назвал lynis (да простят меня специалисты по информационной безопасности за такое сравнение). Является проектом-участником CNCF.
Бежим пробовать!
# .gitlab-ci/jobs/integration-kubernetes.yaml
.kubescape_lint:
image:
name: dtzar/helm-kubectl:3.7.2
variables:
KUBESCAPE_VERSION: 'v2.0.183'
KUBESCAPE_RAW_PATH: ''
KUBESCAPE_HELM_PATH: ''
KUBESCAPE_WARNING_SEVERITY: "high"
before_script:
# Install kubescape
- wget https://github.com/kubescape/kubescape/releases/download/${KUBESCAPE_VERSION}/kubescape-ubuntu-latest
- apk add curl gcompat --no-cache
- mv kubescape-ubuntu-latest /usr/local/bin/kubescape
- chmod +x /usr/local/bin/kubescape
script:
- >
if [[ -z ${KUBESCAPE_HELM_PATH} ]]; then
kubescape scan ${KUBESCAPE_RAW_PATH} --severity-threshold ${KUBESCAPE_WARNING_SEVERITY};
elif [[ -z ${KUBESCAPE_RAW_PATH} ]]; then
helm dependency build ${KUBESCAPE_HELM_PATH} ;
kubescape scan ${KUBESCAPE_HELM_PATH} --severity-threshold ${KUBESCAPE_WARNING_SEVERITY};
fi
# .gitlab-ci.yml
raw-07:
stage: kubescape
variables:
KUBESCAPE_RAW_PATH: 'kube/raw/*'
KUBESCAPE_WARNING_SEVERITY: "high"
tags:
- docker
allow_failure: true
extends: .kubescape_lint
helm-07:
stage: kubescape
variables:
KUBESCAPE_HELM_PATH: 'kube/helm/parrot-app'
KUBESCAPE_WARNING_SEVERITY: "high"
tags:
- docker
allow_failure: true
extends: .kubescape_lint

Как можно увидеть, вывод достаточно подробный. Также его можно представить в других форматах (аргумент –format = pretty-printer/json/junit/Prometheus/pdf/html/sarif) или поставить некий уровень «очков» проверки через аргумент --severity-threshold (что отсылает нас к Polaris). Этой же утилитой мы способны через CLI проверить и наш кластер на наличие проблем с настройкой.
Работа над ошибками
Таким образом, мы рассмотрели семь утилит для проверки Kubernetes конфигураций, а наш неадекватный пайплайн превратился в нечто подобное:

Теперь попробуем внести в наш манифест все необходимые исправления, дабы каждый из наших валидаторов разрешил нам последующий деплой. Некоторые из них требуют наличие в конфигурации PodDisruptionBudget и NetworkPolicy, в данной статье мы опустим их листинг, так как знающие люди их и без линтеров не игнорируют, а время читателя не резиновое. Также для работы нашего приложения необходимо осуществить его пересборку, убрав большую часть проблем на этапе сборки самого образа (привилегированные порты, запуск от привилегированного пользователя и другое). Аналогичные манифесту изменения внесем и в наш Helm чарт.
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: bird
labels:
author: "IvanIvanov"
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 2
maxUnavailable: 0
selector:
matchLabels:
app.kubernetes.io/name: parrot-app
template:
metadata:
labels:
app.kubernetes.io/name: parrot-app
spec:
containers:
- name: bird
image: vregret/parrot-uprivileged:0.1
imagePullPolicy: Always
securityContext: #https://snyk.io/blog/10-kubernetes-security-context-settings-you-should-understand/
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 10000
runAsGroup: 10000
capabilities: #https://man7.org/linux/man-pages/man7/capabilities.7.html
drop:
- "all"
seccompProfile:
type: RuntimeDefault
seLinuxOptions:
level: "s0:c123,c456"
resources:
requests:
memory: "64Mi"
cpu: "250m"
ephemeral-storage: "5Mi"
limits:
memory: "64Mi"
cpu: "250m"
ephemeral-storage: "10Mi"
ports:
- name: http
protocol: TCP
containerPort: 8080
livenessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 5
periodSeconds: 3
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 3
volumeMounts:
- mountPath: /var/cache/nginx
name: cache
- mountPath: /var/run
name: pid
volumes:
- name: cache
emptyDir:
sizeLimit: 5Mi
- name: pid
emptyDir:
sizeLimit: 1Mi
---
apiVersion: v1
kind: Service
metadata:
name: parrot
labels:
app: parrot
app.kubernetes.io/name: parrot-app
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: parrot-app
ports:
- port: 8080
name: http
protocol: TCP
targetPort: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: parrot
labels:
app: parrot
app.kubernetes.io/name: parrot-app
spec:
rules:
- host: parrot.aperture.loc
http:
paths:
- path: /
pathType: ImplementationSpecific
backend:
service:
name: parrot
port:
number: 8080
Листинг исправленного манифеста
От себя замечу, что даже просто работа по удовлетворению всех хотелок линтеров – весьма занимательное и познавательное занятие. Всем советую, даже если не хочется их потом вносить в свой пайплайн.

На выходе получаем вот такую милую глазу картину:

Казалось бы, всё: мы получили некоторое понимание по работе утилит, можно просто выбрать одну и добавить в свой пайплайн, или улучшить всё это, например, сделав отельный DevSecOps репозиторий, собрав отдельный образ под линтинг манифестов, и дергать его из других репозиториев через Triggered pipelines.
Но что, если «собака-подозревака» в нашей голове хочет большего, если доверия к пайплайну нет, так как разработчики имеют kube-config до продуктового кластера, если хитрые инженеры не хотят запоминать правила написания манифестов отдавая всю работу CI/CD, чем снижают коэффициент Time-to-market? Давайте успокоим себя еще несколькими способами, активно используемыми в сообществе DevOps — рассмотрим сущность Admission Controller Kubernetes и подход Shift-left.
Валидация с помощью Kubernetes Admission Controller и использование подхода Shift-Left
«Everything about this place just doesn't feel right».
The Count of Tuscany. Dream Theater
Итак, если не копипастить документацию Kubernetes, Admission Controller — это сущность в API Kubernetes, позволяющая проверять или изменять (validation или mutation соответственно) запросы к API от клиентов. В нашем случае мы рассмотрим Polaris и Datree как сущности, которые можно установить в кластер Kubernetes в качестве Admission Webhook серверов. Они будут пропускать через три сита себя применяемые конфигурации и принимать решение о том, стоит ли их пропустить/изменить. Согласно RoadMap Kubescape, данная функция скоро появится и у него.
Начнем, пожалуй, с Polaris — с ним мы пройдем по всем граблям в развертывании Admission Webhook сервера. Основная часть граблей связана с тем, что все запросы (webhook) к API Kubernetes должны осуществляться с использованием tls, а значит, мы должны не просто поставить в Kubernetes приложение, но сформировать csr запрос на сертификат, заверить его во внутреннем удостоверяющем центре Kubernetes и используя полученный от УЦ сертификат, поднять Polaris. Все манипуляции мы будем проводить локально в кластере, созданном через Kind, который также отлично можно использовать и в рамках CI/CD, и как плацдарм для обкатки различных решений. Дабы не тратить ваше время, приведу примерный скрипт для развертывания Polaris Admission controller в Kubernetes:
#!/bin/bash
set -e
# Добавляем репозиторий Helm
helm repo add fairwinds-stable https://charts.fairwinds.com/stable
helm repo update
mkdir polaris-certs
# Генерируем ключ
openssl genrsa -out ./polaris-certs/polaris-webhook.key 2048
# Создаем конфигурацию запроса сертификата
cat<<EOF > ./polaris-certs/polaris-webhook.conf
[ req ]
default_bits = 2048
prompt = no
default_md = sha256
req_extensions = req_ext
distinguished_name = dn
[ dn ]
O = system:nodes
CN = system:node:polaris-webhook.polaris.svc.cluster.local
[ req_ext ]
subjectAltName = @alt_names
[ alt_names ]
DNS.1 = polaris-webhook
DNS.2 = polaris-webhook.polaris
DNS.3 = polaris-webhook.polaris.svc
DNS.4 = polaris-webhook.polaris.svc.cluster.local
EOF
# … генерируем запрос на сертификат …
openssl req -new -key ./polaris-certs/polaris-webhook.key -out ./polaris-certs/polaris-webhook.csr -config ./polaris-certs/polaris-webhook.conf
# … на базе созданного запроса формируем запрос (каламбур) и направляем его в Kubernetes …
cat <<EOF | kubectl apply -f -
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata:
name: polaris-webhook
spec:
request: $(cat ./polaris-certs/polaris-webhook.csr | base64 | tr -d "\n")
signerName: kubernetes.io/kubelet-serving
usages:
- digital signature
- key encipherment
- server auth
EOF
# … помогаем Kubernetes аппрувнуть запрос и выпустить сертификат …
kubectl certificate approve polaris-webhook
# … забираем сертификат …
kubectl get csr polaris-webhook -o jsonpath='{.status.certificate}' | base64 --decode > ./polaris-certs/polaris-webhook.crt
# Применяем сертификат в Kubernetes и развертываем Polaris передав ему также CA Bundle Kubernetes
kubectl create ns polaris
kubectl -n polaris create secret tls polaris-webhook --cert=./polaris-certs/polaris-webhook.crt --key=./polaris-certs/polaris-webhook.key
helm upgrade --install polaris fairwinds-stable/polaris -n polaris -f values.yaml --set webhook.caBundle=$(kubectl get configmaps -n kube-system extension-apiserver-authentication -o=jsonpath='{.data.client-ca-file}' | base64 | tr -d "\n")

Сразу замечу, что сталкивался с забавным моментом. Так как применение Validation Admission Webhook Configuration происходит мгновенно, последующее развертывание подов Polaris может не произойти из-за попытки API Kubernetes обратиться к Polaris для проверки этих самых подов.
Решение достаточно простое – удалить сущность validatingwebhookconfiguration из кластера, подождать старта подов и после этого применить повторно. В идеале – этот момент, а точнее последовательность деплоя, нужно автоматизировать в Iaac репозитории или через Helm hooks. Правила, которыми можно настраивать Polaris, можно явно задать в values.yaml Helm чарта.
После развертывания деплой ненадежной версии приложения будет блокироваться:

Теперь попробуем провернуть подобную процедуру с Datree. Данная система уже не требует такой подготовки и устанавливается гораздо более просто, выполняя под капотом все вышеописанные действия.
#!/bin/bash
set -e
helm repo add datree-webhook https://datreeio.github.io/admission-webhook-datree
helm repo update
helm install -n datree datree-webhook datree-webhook/datree-admission-webhook --debug \
--create-namespace \
--set datree.token=dba0xxxx-xxxx-xxxx-xxxx-4acaf52axxxx \
--set datree.clusterName=$(kubectl config current-context)
В datree.token мы должны передать токен, получаемый при регистрации и оформлении подписки Datree. В данном примере мы используем тестовый токен, выданный для тестирования на ограниченный срок.
Вывод в консоли при попытке применить наш плохой манифест будет выглядеть примерно так:

Обратим внимание на консоль – Datree автоматически формирует ссылку, по которой можно перейти в Datree Dashboard и посмотреть отчет по данной проверке, а также политики и другие «ништяки» данной системы.

Таким образом, мы получили возможность обезопасить кластер Kubernetes от мисконфигураций непосредственно при обращении к нему – это здорово и позволяет спать чуть более спокойно. Однако есть ещё способ проверки манифестов, позволяющий ускорить развертывание приложений и не позволяющий попасть неправильным манифестам не только в Kubernetes, но и в git репозиторий. Немного поговорим про shift-left подход.
Основной смысл и цель подхода — это смещение «влево» (ваш кэп), то есть дать возможность IT-специалистам проверять код и конфигурации до их попадания в систему контроля версий. Реализуется это различными способами, но в ходе изучения данной темы я чаще всего наблюдал использование утилиты pre-commit, которая позволяет настраивать git hooks и внедрять проверку кода различными способами в зависимости от этапов работы с git репозиторием. Она обладает хорошей документацией, которая помогает во всём разобраться и писать свои хуки. Наша же задача – попросить данную утилиту сформировать hook, который бы проверял измененные файлы перед коммитом в системе контроля версий. В качестве утилиты для проверки кода будем использовать kube-score, предварительно установленную на компьютере инженера.
Настройка Pre-commit производится добавлением в корень репозитория файла .pre-commit-config.yaml.
repos: #https://pre-commit.com/#advanced
- repo: local
hooks:
- id: kubernetes-validation-kube-score
name: kubernetes-validation-kube-score
description: Validation kubernetes manifests via kube-score
entry: ./.lint.sh
language: script
types_or:
- text
verbose: true
files: "^kube/"
Предположим, что по принятым нами стандартам оформления репозиториев, манифесты приложения располагаются в каталоге kube/. Стоит учитывать и то, что в нашем репозитории есть как манифест, так и Helm чарт приложения, а значит, для утилит которые не способны сами шаблонизировать чарт, изменение какого-нибудь template/deployment.yaml повлечет ошибки в валидации go template, который используется Helm. Чтобы исправить сложившуюся ситуацию, был составлен небольшой скрипт, который позволяет отличать манифесты от чартов и при необходимости проверки чарта – шаблонизировать его для проверки. Данный скрипт располагается в корне проекта и будет вызываться хуком git. Также ничего не мешает в конфигурации pre-commit указывать git репозиторий со всеми необходимыми конфигурациями и скриптами, что упрощает централизацию и переиспользование. Примерный листинг скрипта:
#!/bin/bash
set -e
# Поиск чартов в репозитории (по наличию файла Chart.yaml) что позволяет найти как обычные чарты так и umbrella чарты (правда для которых необходимо использовать helm dependency build)
CHARTS_LOCATIONS=$(find . -name "Chart.yaml" -type f | xargs dirname | sed 's/.\///')
# Получение пути до измененного файла (также отображается в git status)
FILE=$1
its_chart=false
# Сравнение пути до измененного файла с путями до Helm чартов
for i in $CHARTS_LOCATIONS
do
if [[ $(echo $FILE | xargs dirname) == $i* ]]
then
its_chart=true
target_helm_chart=$i
break
fi
done
# Непосредственно проверка в зависимости от типа ресурса
if $its_chart
then
echo "$FILE - It's a part of Helm chart"
helm template $target_helm_chart | kube-score score -
else
echo "$FILE - It's a Kubernetes raw manifest"
kube-score score $FILE
fi
Соглашусь с тем, что данный скрипт имеет определенные недостатки, но для проверки работы pre-commit подходит неплохо. Само применение настроек и hook происходит командой pre-commit install в корне репозитория (это единственное действие, которое необходимо совершить инженеру после клонирования репозитория и установки pre-commit и линтера для включения проверок). Теперь при попытке закоммитить невалидный манифест в систему контроля версий мы будем получать ошибку и информацию о причинах.

Если не пытаться идти по граблям, то у утилиты kubelinter уже есть готовая pre-commit конфигурация, позволяющая использовать данную утилиту при shift-left подходе.
Таким образом, мы можем применять различные способы валидации конфигураций Kubernetes и использовать их ещё до внесения в систему контроля версий. Однако серебряной пулей данный способ также не является – команда git commit --no-verify позволяет сделать commit без какой-либо валидации, и это не есть плохо, так как невозможно заставить человека делать всё правильно.
Если провести краткое сравнение приведенных инструментов между собой, то образуется примерно такая картина:
Название | Валидация | Best-practice | Возможность настройки | Многообразие вывода результатов | Kubernetes Admission webhook | Бесплатность | Тестирование кластера |
Kubeconform | + | - | + | + | - | + | - |
Conftest | + | + | + | + | - | + | - |
Kubelinter | - | + | + | - | - | + | - |
Kube-score | - | + | + | + | - | + | - |
Polaris | + | + | + | + | + | + | + |
Datree | + | + | + | + | + | ~ | + |
Kubescape | - | + | ~ | + | - | + | + |
Выводы
Все приведенные инструменты и способы призваны лишь снизить риск появления мисконфигураций в ваших кластерах и помочь администраторам и разработчикам разобраться в том, как делать всё правильно и убрать из головы тот самый страх, о котором было написано в начале нашего путешествия. Каждый из приведенных в статье инструментов имеет свои плюсы и минусы, работа над каждым из них активно ведется компаниями и сообществами, появляются новые.
Не стоит списывать со счетов и проверенные временем инструменты, например, yamllint. Порой не совсем корректно написанный YAML может повлиять на работу линтера (передаю пламенный привет kube-score и ошибке [CRITICAL] networking.k8s.io/v1/Ingress: (/) No service match was found, эти два часа были незабываемы).
От себя дам краткую характеристику по всем инструментам:
Kubeconform – удобное средство проверки синтаксиса.
Polaris – удобное и бесплатное решение для реализации “самозащиты” кластера.
Kubelinter, Kubescape и Kube-score — наиболее въедливые утилиты по поиску плохих практик.
Datree — централизованное, пусть и не бесплатное средство для централизованной проверки как в CI, так и в самом Kubernetes.
Наиболее интересный вариант использования получился с использованием сразу нескольких утилит подряд, например yamllint + Kubeconform + Kube-score. Таким образом покрывается большая площадь возможных проблем, а манифесты становятся «чистыми и мягкими».
Буду рад комментариям о вашем опыте использования валидаторов для Kubernetes и решения проблем с некорректными конфигурациями. Всё же вся наша жизнь – это школа и перестать учиться значит потерять себя.
Спасибо за внимание!

Полезные материалы о полном цикле разработки и методологии DevOps мы также публикуем в наших соцсетях – ВКонтакте и Telegram.