Расширяем и дополняем Kubernetes (обзор и видео доклада)

jmb5aprqc5kp9-ywf0_xusebrp8.png

8 апреля на конференции Saint HighLoad++ 2018, в рамках секции «DevOps и эксплуатация», прозвучал доклад «Расширяем и дополняем Kubernetes», в создании которого участвовали три сотрудника компании «Флант». В нём мы рассказываем о многочисленных ситуациях, в которых нам хотелось расширить и дополнить возможности Kubernetes, но для чего мы не находили готового и простого решения. Необходимые решения у нас появились в виде Open Source-проектов, и им тоже посвящено это выступление.

По традиции рады представить видео с докладом (50 минут, гораздо информативнее статьи) и основную выжимку в текстовом виде. Поехали!

Ядро и дополнения в K8s


Kubernetes меняет отрасль и подходы к администрированию, которые давно устоялись:

  • Благодаря его абстракциям, мы оперируем уже не такими понятиями, как настройка конфига или запуск команды (Chef, Ansible…), а пользуемся группировкой контейнеров, сервисами и т.п.
  • Мы можем готовить приложения, не задумываясь о нюансах той конкретной площадки, на которой оно будет запущено: bare metal, облако одного из провайдеров и т.п.
  • С K8s как никогда стали доступны лучшие практики по организации инфраструктуры: техники масштабирования, самовосстановления, отказоустойчивости и т.п.


Однако, разумеется, всё не так гладко: с Kubernetes пришли и свои — новые — вызовы.

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

j4bi2rovwoaeg30vtxmqvkf3gr0.png

В ядре Kubernetes определяется базовый набор примитивов — для группировки контейнеров, управления трафиком и так далее. Подробнее о них мы рассказывали в докладе 2-летней давности.

ri3rbhiadlucbkmjxoi7evenkoc.png

С другой стороны, K8s предлагает замечательные возможности по расширению доступных функций, что помогают закрыть и другие — специфичные — потребности пользователей. За дополнения в Kubernetes отвечают администраторы кластеров, которые должны установить и настроить всё необходимое для того, чтобы их кластер «обрёл нужную форму» [для решения их специфичных задач]. Что же это за дополнения такие? Рассмотрим некоторые примеры.

Примеры дополнений


Установив Kubernetes, мы можем удивиться, что сеть, столь необходимая для взаимодействия pod’ов как в рамках узла, так и между узлами, сама по себе не работает. Ядро Kubernetes не гарантирует нужные связи — вместо этого, оно определяет сетевой интерфейс (CNI) для сторонних дополнений. Мы должны установить одно из таких дополнений, которое и будет отвечать за конфигурацию сети.

y1dzkhba60lopgq_nokdcisaols.png

Близкий пример — решения для хранения данных (локальный диск, сетевое блочное устройство, Ceph…). Изначально они были в ядре, но с появлением CSI ситуация меняется на аналогичную уже описанной: в Kubernetes интерфейс, а его реализация — в сторонних модулях.

Среди прочих примеров:

  • Ingress-контроллеры (их обзор см. в нашей недавней статье).
  • cert-manager:

    jdbeocyiociiucegto-own0o6g0.gif

  • Операторы — это целый класс дополнений (к которым относится и упомянутый cert-manager), они определяют примитив (ы) и контроллер (ы). Логика их работы ограничена только нашей фантазией и позволяет превращать готовые компоненты инфраструктуры (например, СУБД) в примитивы, работать с которыми гораздо проще (чем с набором из контейнеров и их настроек). Операторов написано огромное множество — пусть многие из них ещё и не готовы к production, это лишь вопрос времени:

    uuypgi9fy-7ot0uho2vh27uq7nq.png

  • Метрики — очередная иллюстрация, как в Kubernetes отделили интерфейс (Metrics API) от реализации (сторонние дополнения, такие как Prometheus adapter, Datadog cluster agent…).
  • Для мониторинга и статистики, где на практике нужны не только Prometheus и Grafana, но и kube-state-metrics, node-exporter и т.п.


И это далеко не полный список дополнений… Например, мы в компании «Флант» на каждый Kubernetes-кластер на сегодняшний день устанавливаем 29 дополнений (все они в общей сложности создают 249 объектов Kubernetes). Проще говоря, мы не видим жизни кластера без дополнений.

Автоматизация


Операторы созданы для автоматизации рутинных операций, с которыми мы повседневно сталкиваемся. Вот примеры из жизни, отличным решением для которых будет написание оператора:

  1. Есть приватный (т.е. требующий логина) registry с образами для приложения. Предполагается, что каждому pod’у привязывается специальный секрет, позволяющий аутентифицироваться в registry. Наша задача — обеспечить нахождение этого секрета в namespace’е, чтобы pod’ы могли скачивать образы. Приложений (каждому из которых нужен секрет) может быть очень много, а сами секреты полезно регулярно обновлять, так что вариант с раскладыванием секретов руками отпадает. Тут и приходит на помощь оператор: мы создаём контроллер, который будет дожидаться появления namespace’а и по этому событию добавит секрет в namespace.
  2. Пусть по умолчанию доступ из pod’ов к интернету запрещён. Но иногда он может требоваться: логично, чтобы механизм разрешения доступа работал просто, не требуя специфичных навыков, — например, по наличию определённого лейбла в namespace’е. Как нам тут поможет оператор? Создаётся контроллер, который ожидает появления лейбла в namespace’е и добавляет соответствующий policy для доступа в интернет.
  3. Схожая ситуация: пусть нам потребовалось добавлять на узел определённый taint, если на нём есть аналогичный лейбл (с каким-то префиксом). Действия с оператором очевидны…


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

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

Как написать оператор для Kubernetes?


В целом схема проста:

qtjo7ujblxe_kbzr05hw0ixow1y.png

…, но тут выясняется, что:

  • Kubernetes API — достаточно нетривиальная вещь, которая требует немало времени для освоения;
  • программирование тоже не для каждого (язык Go выбран как предпочтительный, потому что для него есть специальный фреймворк — Operator SDK);
  • с фреймворком как таковым аналогичная ситуация.


Итог: для написания контроллера (оператора) приходится потратить существенные ресурсы для изучения матчасти. Это было бы оправдано для «больших» операторов — скажем, для СУБД MySQL. Но если мы вспомним описанные выши примеры (раскладывание секретов, доступ pod’ов в интернет…), которые хочется тоже делать правильно, то мы поймём, что затрачиваемые усилия перевесят нужный сейчас результат:

znw39bssazgqnsoe9mrsh8e7o6o.png

В общем, возникает дилемма: потратить много ресурсов и обрести правильный инструмент для написания операторов или действовать «по старинке» (но быстро). Для её решения — нахождения компромисса между этими крайностями — мы создали свой проект: shell-operator (см. также его недавний анонс на хабре).

Shell-operator


Как он работает? В кластере есть pod, в котором лежит Go-бинарник с shell-operator. Рядом с ним хранится набор хуков(подробнее о них — см. ниже). Сам shell-operator подписывается на определённые события в Kubernetes API, по факту наступления которых он запускает соответствующие хуки.

Как shell-operator понимает, какие хуки при каких событиях вызывать? Эту информацию передают shell-operator’у сами хуки и делают они это очень просто.

Хук — это скрипт на Bash или любой другой исполняемый файл, который поддерживает единственный аргумент --config и в ответ на него выдаёт JSON. Последний определяет, какие объекты его интересуют и на какие события (для этих объектов) следует реагировать:

6m7feilxyvybractmb6aoxj-olm.png

Проиллюстрирую реализацию на shell-operator одного из наших примеров — раскладывание секретов для доступа к приватному registry с образами приложения. Она состоит из двух этапов.

Практика: 1. Пишем хук


Первым делом в хуке обработаем --config, указав, что нас интересуют namespace’ы, а конкретно — момент их создания:

[[ $1 == "--config" ]] ; then
  cat << EOF
{
  "onKubernetesEvent": [
    {
      "kind": "namespace",
      "event": ["add"]
    }
  ]
}
EOF
…


Как будет выглядеть логика? Тоже довольно просто:

…
else
  createdNamespace=$(jq -r '.[0].resourceName' $BINDING_CONTEXT_PATH)
  kubectl create -n ${createdNamespace} -f - << EOF
Kind: Secret
...
EOF
fi


Первым шагом мы узнаём, какой namespace был создан, а вторым — создаём через kubectl секрет для этого пространства имён.

Практика: 2. Собираем образ


Осталось передать созданный хук shell-operator’у — как это сделать? Сам shell-operator поставляется в виде Docker-образа, так что наша задача — добавить хук в специальный каталог в этом образе:

FROM flant/shell-operator:v1.0.0-beta.1
ADD my-handler.sh /hooks


Останется собрать его и push’нуть:

$ docker build -t registry.example.com/my-operator:v1 .
$ docker push registry.example.com/my-operator:v1


Финальный штрих — задеплоить образ в кластер. Для этого напишем Deployment:

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: my-operator
spec:
  template:
    spec:
      containers:
      - name: my-operator
        image: registry.example.com/my-operator:v1 # 1
      serviceAccountName: my-operator              # 2


В нём нужно обратить внимание на два момента:

  1. указание только что созданного образа;
  2. это системный компонент, которому (как минимум) нужны права на то, чтобы подписаться на события в Kubernetes и чтобы раскладывать секреты по namespace’ам, поэтому мы создаём для хука ServiceAccount (и набор правил).


Результат — мы решили нашу проблему родным для Kubernetes способом, создав оператор для раскладывания секретов.

Другие возможности shell-operator


Чтобы ограничить объекты выбранного вами типа, с которыми будет работать хук, их можно фильтровать, отбирая по определённым лейблам (или с помощью matchExpressions):

"onKubernetesEvent": [
  {
    "selector": {
      "matchLabels": {
        "foo": "bar",
       },
       "matchExpressions": [
         {
           "key": "allow",
           "operation": "In",
           "values": ["wan", "warehouse"],
         },
       ],
     }
     …
  }
]


Предусмотрен механизм дедупликации, который — с помощью jq-фильтра — позволяет преобразовывать большие JSON’ы объектов в маленькие, где остаются только те параметры, за изменением которых мы хотим следить.

При вызове хука shell-operator передаёт ему данные про объект, которые могут использоваться для любых нужд.

События, при наступлении которых вызываются хуки, не ограничены Kubernetes events: в shell-operator предусмотрена поддержка вызова хуков по времени (аналогично crontab в традиционном планировщике), а также специального события onStartup. Все эти события могут комбинироваться и назначаться на один и тот же хук.

И ещё две особенности shell-operator:

  1. Он работает асинхронно. С момента получения события Kubernetes (например, создание объекта) в кластере могли произойти и другие события (например, удаление того же объекта), и это необходимо учитывать в хуках. Если хук выполнился с ошибкой, то по умолчанию он будет повторно вызываться до успешного завершения (это поведение можно изменить).
  2. Он экспортирует метрики для Prometheus, с помощью которых можно понять, работает ли shell-operator, узнать количество ошибок по каждому хуку и текущий размер очереди.


Подводя итог этой части доклада:

v2aazotslqg8mvbgb4wbkrwz1do.png

Установка дополнений


Для комфортной работы с Kubernetes была также упомянута необходимость установки дополнений. О ней я расскажу на примере пути нашей компании к тому, как мы делаем это сейчас.

Работу с Kubernetes мы начинали с нескольких кластеров, единственным дополнением в которых был Ingress. В каждый кластер его требовалось ставить по-разному, и мы сделали несколько YAML-конфигураций для разных окружений: bare metal, AWS…

Кластеров становилось больше — больше становилось и конфигураций. Кроме того, мы улучшали сами эти конфигурации, в результате чего они стали довольно разнородными:

sxbb3ndabtfctyjimx7lzwvg-sk.png

Чтобы привести всё в порядок, мы начали со скрипта (install-ingress.sh), который принимал аргументом тип кластера, в который будем деплоиться, генерировал нужную YAML-конфигурацию и выкатывал её в Kubernetes.

Если вкратце, то дальнейший наш путь и связанные с ним рассуждения были таковы:

  • для работы с YAML-конфигурациями требуется шаблонизатор (на первых этапах это простой sed);
  • с ростом числа кластеров пришла необходимость для автоматического обновления (самое раннее решение — положили скрипт в Git, по cron’у его обновляем и запускаем);
  • схожий скрипт потребовался для Prometheus (install-prometheus.sh), однако он примечателен тем, что требует гораздо больше вводных данных, а также их хранение (по-хорошему — централизованное и в кластере), причём некоторые данные (пароли) можно было автоматически генерировать:

    u93soes0m24zohi4oy0ugk2uvcg.png

  • риск выкатить что-то неправильное на растущее число кластеров постоянно рос, поэтому мы поняли, что инсталляторам (т.е. двум скриптам: для Ingress и Prometheus) понадобилось стейджирование (несколько веток в Git, несколько cron’ов на их обновление в соответствующих: стабильных или тестовых — кластерах);
  • с kubectl apply стало сложно работать, потому что он не является декларативным и умеет только создавать объекты, но не принимать решения по их статусу/удалять их;
  • не хватало некоторых функций, которые мы на тот момент совсем не реализовали:
    • полноценного контроля результата обновления кластеров,
    • автоматического определения некоторых параметров (вводных для скриптов установки) на основе данных, что можно получить из кластера (discovery),
    • его логичного развития в виде continuous discovery.


Весь этот накопленный опыт мы реализовали в рамках другого своего проекта — addon-operator.

Addon-operator


В его основе — уже упомянутый shell-operator. Вся система же выглядит следующим образом:

К хукам shell-operator’а добавляются:

  • хранилище values,
  • Helm-чарт,
  • компонент, который следит за хранилищем values и — в случае каких-либо изменений — просит Helm перевыкатить чарт.


w88fkfrbrukitlgx_u5jzauwtzq.gif

Таким образом, мы можем среагировать на событие в Kubernetes, запустить хук, а из этого хука — внести изменения в хранилище, после чего будет перевыкачен чарт. В получившейся схеме мы выделяем набор хуков и чарт в один компонент, который называем модулем:

b93jqjumemaiju4cwcji2qnmaqw.png

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

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

adhhe-ml3wrrqjks5dniduqxecg.gif

Эта схема удовлетворяет всем требованиям к установке дополнений, что были озвучены выше:

  • За шаблонизацию и декларативность отвечает Helm.
  • Вопрос автообновления решён с помощью глобального хука, который по расписанию ходит в registry и, если видит там новый образ системы, перевыкатывает её (т.е. «сам себя»).
  • Хранение настроек в кластере реализовано с помощью ConfigMap, в котором записаны первичные данные для хранилищ (при старте они загружаются в хранилища).
  • Проблемы генерации паролей, discovery и continuous discovery решены с помощью хуков.
  • Стейджирование достигнуто благодаря тегам, которые Docker поддерживает из коробки.
  • Контроль результата производится с помощью метрик, по которым мы можем понять статус.


Вся эта система реализована в виде единственного бинарника на Go, который и получил название addon-operator. Благодаря этому схема выглядит проще:

ip2ncf-unok3h5azr8-icyby3hs.png

Главный компонент на этой схеме — набор модулей (выделены серым цветом внизу). Теперь мы можем небольшими усилиями написать модуль для нужного дополнения и быть уверенными, что оно будет установлено в каждый кластер, будет обновляться и реагировать на нужные ему события в кластере.

«Флант» использует addon-operator на 70+ Kubernetes-кластерах. Текущий статус — альфа-версия. Сейчас мы готовим документацию, чтобы выпустить бету, а пока в репозитории доступны примеры, на основе которых можно создать свой addon.

Где взять сами модули для addon-operator? Публикация своей библиотеки — следующий этап для нас, мы планируем это сделать летом.

Видео и слайды


Видео с выступления (около часа):

Презентация доклада:

https://speakerdeck.com/flant/rasshiriaiem-i-dopolniaiem-kubernetes

P.S.


Другие доклады в нашем блоге:
Возможно, вас также заинтересуют следующие публикации:

© Habrahabr.ru