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

Ступая по тропе изучения новой технологии, порой полезно обернуться назад и оценить, насколько тернист был этот путь, сделать выводы и только после этого уверенно двигаться дальше. Так как 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 кластер конфигураций. Является проектом-участником 
Ну а мы переходим к адаптации данной утилиты для нашего 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.
ссылка на оригинал статьи https://habr.com/ru/articles/730318/
Добавить комментарий