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

Путешествие к центру земли. Жюль Верн

Ступая по тропе изучения новой технологии, порой полезно обернуться назад и оценить, насколько тернист был этот путь, сделать выводы и только после этого уверенно двигаться дальше. Так как 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

  • URL: https://github.com/yannh/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 после валидации манифеста parrot.yaml
Вывод kubeconform после валидации манифеста parrot.yaml

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

Судя по приборам – всё отлично.

Вывод результаты kubeconform при проверке helm чарта
Вывод результаты kubeconform при проверке helm чарта

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

Ошибка при применении проверенного helm чарта  (для детального рассмотрения откройте картинку в новой вкладке)
Ошибка при применении проверенного helm чарта (для детального рассмотрения откройте картинку в новой вкладке)

Выполним команду 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

  • URL: https://github.com/open-policy-agent/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 видим красивое отображение нашей «заплатки».

Результат работы conftest
Результат работы conftest

Но зачем при написании политики мы указали, что проверять пробы на некорректное заполнение нужно только после проверки их наличия? Ведь отсутствие проверок работоспособности наших сервисов – само по себе преступление и не соответствует best practice! Однако в этой статье мы пытаемся разложить всё по полочкам, и описанные выше инструменты с задачей валидирования конфигураций справились. Для проверок на лучшие практики лучше использовать специальные инструменты, о которых мы как раз и поговорим далее.

3. Kube-score

  • URL: https://github.com/zegl/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
Вывод работы kube-score

Как мы видим, kube-score отметил проблемные места в нашем манифесте, затронув отсутствие лимитов и реквестов для CPU, Memory и Ephemeral Storage. Сам вывод может меняться в зависимости от аргумента командной строки --output-format (доступны варианты human, json, sarif  и ci), что позволяет распарсить последующий вывод и выводить информацию в Merge Request.

Все найденные нами ошибки мы поправим далее по тексту, а сейчас перейдем к следующей утилите.

4. Kubelinter

  • URL: https://github.com/stackrox/kube-linter 

  • Описание: линтер манифестов и 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

Посмотрим, как данная утилита справилась с проверкой.

Вывод утилиты Kubelinter
Вывод утилиты Kubelinter

Стоит отметить не совсем удобный, но подробный и подверженный парсингу вывод утилиты. 

Также, как можно заметить по коду CI, – это первая утилита в нашем обзоре, которая способна проверять Helm чарты самостоятельно, без их предварительного рендеринга через helm template, что достаточно приятно. Отдельного внимания заслуживают umbrella (зонтичные) чарты – при их использовании, перед валидацией необходимо использовать команду helm dependency build. 

Теперь мы переходим к более увесистым и любопытным утилитам, «тяжеловесам» нашего разбора.

5. Polaris

  • URL: https://github.com/FairwindsOps/polaris 

  • Описание: линтер синтаксиса Kubernetes манифестов и Helm чартов + проверка на соответствие лучшим практикам.

  • Тюнинг параметров: файл конфигурации, добавление собственных проверок, исключение из проверки сущностей с помощью аннотаций.

  • Возможность тестирования кластера: через CLI и admission webhook.

  • Замеченные особенности: была замечена проблема при одновременной работе версий 7.0.1 Polaris и >3.7.2 у Helm, на данный момент в версии 7.3.2 Polaris проблема исчезла.

Polaris предоставляет возможность использовать себя как CLI утилиту для проверки манифестов, дает возможность настраивать свои правила проверок, а также может быть запущен как Admission Controller для валидации приходящих в Kubernetes кластер конфигураций. Является проектом-участником CNCF. Также в наличии имеется симпатичный дашборд для отображения «погоды» в кластере:

Polaris Dashboard
Polaris Dashboard

Ну а мы переходим к адаптации данной утилиты для нашего 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

А теперь посмотрим, как данная утилита справилась с нашим заданием.

Вывод утилиты Polaris в нашем CI
Вывод утилиты Polaris в нашем CI

Помимо красивого и читаемого вывода (тип которого тоже изменяется через аргумент --format=pretty/json/yaml), здесь стоит обратить внимание на то, что несмотря на посредственные параметры нашего манифеста – проверка линтером посчиталась успешной. Связано это с тем, что в аргумент --set-exit-code-below-score мы передали значение 70. Повышая и понижая данное значение, мы можем регулировать то, при каком пороговом количестве очков линтинга Polaris посчитает манифест небезопасным и завершится ошибкой. Также стоит заметить, что конфигурация для Polaris представляет собой длинный yaml файл, что не всегда удобно, если хочется проигнорировать или изменить важность какого-нибудь этапа линта. Впрочем, ничего не мешает вытягивать его из отдельного репозитория в рамках CI.

6. Datree

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 после проверки манифеста в CI
Вывод Datree после проверки манифеста в CI

Как мы видим, утилита успешно справилась с задачей и даже направила нам ссылку на просмотр политик. Там нас встретит окно входа в аккаунт Datree, но об этом далее по тексту. Также, как и у некоторых предыдущих утилит, можно менять вывод программы (--output=simple/yaml/json/xml/JUnit). 

Мы же, не отвлекаясь, переходим к настоящему монстру валидации Kubernetes манифестов, да и самих кластеров – Kubescape.

7. Kubescape

  • URL: https://github.com/kubescape/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
Вывод работы Kubescape
Вывод работы Kubescape

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

Работа над ошибками

Таким образом, мы рассмотрели семь утилит для проверки Kubernetes конфигураций, а наш неадекватный пайплайн превратился в нечто подобное:

Пайплайн Gitlab CI после тестирования наших манифестов
Пайплайн Gitlab CI после тестирования наших манифестов

Теперь попробуем внести в наш манифест все необходимые исправления, дабы каждый из наших валидаторов разрешил нам последующий деплой. Некоторые из них требуют наличие в конфигурации 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

Листинг исправленного манифеста

От себя замечу, что даже просто работа по удовлетворению всех хотелок линтеров – весьма занимательное и познавательное занятие. Всем советую, даже если не хочется их потом вносить в свой пайплайн.

Визуализация ощущений после завершения пайплайна
Визуализация ощущений после завершения пайплайна

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

Пайплайн CI со всеми пройденными проверками
Пайплайн CI со всеми пройденными проверками

Казалось бы, всё: мы получили некоторое понимание по работе утилит, можно просто выбрать одну и добавить в свой пайплайн, или улучшить всё это, например, сделав отельный 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 чарта.

После развертывания деплой ненадежной версии приложения будет блокироваться:

Пример работы Polaris Kubernetes Admission webhook
Пример работы Polaris Kubernetes Admission webhook

Теперь попробуем провернуть подобную процедуру с 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 Kubernetes Admission webhook
Пример работы Datree Kubernetes Admission webhook

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

Datree Dashboard
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.

Комментарии (0)