Розширюємо та доповнюємо Kubernetes (огляд та відео доповіді)
8 квітня на конференції Saint HighLoad++ 2019, в рамках секції «DevOps та експлуатація», прозвучала доповідь «Розширюємо та доповнюємо Kubernetes», у створенні якої брали участь три співробітники компанії «Флант». У ньому ми розповідаємо про численні ситуації, в яких нам хотілося розширити та доповнити можливості Kubernetes, але для чого ми не знаходили готового та простого рішення. Необхідні рішення у нас з'явилися у вигляді Open Source-проектів, і їм також присвячено цей виступ.
За традицією раді уявити відео з доповіддю (50 хвилин, набагато інформативніше статті) і основне вичавлення в текстовому вигляді. Поїхали!
Ядро та доповнення в K8s
Kubernetes змінює галузь та підходи до адміністрування, які давно устоялися:
завдяки його абстракційМи оперуємо вже не такими поняттями, як налаштування конфіга або запуск команди (Chef, Ansible…), а користуємося угрупуванням контейнерів, сервісами тощо.
Ми можемо готувати програми, не замислюючись про нюанси тієї конкретного майданчика, на якій його буде запущено: bare metal, хмара одного з провайдерів і т.п.
З K8s як ніколи стали доступні кращі практики з організації інфраструктури: техніки масштабування, самовідновлення, стійкості до відмови і т.п.
Однак, зрозуміло, все не так гладко: з Kubernetes прийшли і свої нові виклики.
Кубернетес НЕ є комбайном, який вирішує усі проблеми всіх користувачів. Ядро Kubernetes відповідає тільки за набір мінімально необхідних функцій, що присутні в кожному кластері:
У ядрі Kubernetes визначається базовий набір примітивів – для угруповання контейнерів, керування трафіком тощо. Докладніше про них ми розповідали у доповіді 2-річної давності.
З іншого боку, K8s пропонує чудові можливості розширення доступних функцій, що допомагають закрити й інші. специфічні - Потреби користувачів. За доповнення в Kubernetes відповідають адміністратори кластерів, які мають встановити та налаштувати все необхідне для того, щоб їх кластер «набув потрібну форму» [для вирішення їх специфічних завдань]. Що ж це за такі доповнення? Розглянемо деякі приклади.
приклади доповнень
Встановивши Kubernetes, ми можемо здивуватися, що мережа, яка так необхідна для взаємодії pod'ів як у рамках вузла, так і між вузлами, сама по собі не працює. Ядро Kubernetes не гарантує потрібні зв'язки — натомість воно визначає мережевий інтерфейс (CNI) для сторонніх доповнень. Ми повинні встановити одне з таких доповнень, яке відповідатиме за конфігурацію мережі.
Близький приклад – рішення для зберігання даних (локальний диск, мережевий блоковий пристрій, Ceph…). Спочатку вони були в ядрі, але з появою CSI ситуація змінюється на аналогічну вже описаній: у Kubernetes інтерфейс, а його реалізація - у сторонніх модулях.
Оператори - Це цілий клас доповнень (до яких відноситься і згаданий cert-manager), вони визначають примітив(и) та контролер(и). Логіка їх роботи обмежена лише нашою фантазією і дозволяє перетворювати готові компоненти інфраструктури (наприклад, СУБД) на примітиви, працювати з якими набагато простіше (ніж із набором із контейнерів та їх налаштувань). Операторів написано безліч - нехай багато з них ще і не готові до production, це лише питання часу:
Метрики — ще одна ілюстрація, як у Kubernetes відокремили інтерфейс (Metrics API) від реалізації (сторонні доповнення, такі як Prometheus adapter, Datadog cluster agent…).
Для моніторингу та статистики, де на практиці потрібні не тільки Prometheus та Grafana, а й kube-state-metrics, node-exporter тощо.
І це далеко не повний список доповнень… Наприклад, ми в компанії «Флант» на кожен Kubernetes-кластер на сьогодні встановлюємо 29 доповнень (Всі вони загалом створюють 249 об'єктів Kubernetes). Простіше кажучи, ми не бачимо життя кластеру без додатків.
Автоматизація
Оператори призначені для автоматизації рутинних операцій, з якими ми повсякденно стикаємося. Ось приклади з життя, відмінним рішенням для яких буде написання оператора:
Є приватний (тобто потребує логіна) registry з образами додатку. Передбачається, що кожному pod'у прив'язується спеціальний секрет, що дозволяє аутентифікуватись у registry. Наше завдання - забезпечити знаходження цього секрету в namespace'і, щоб pod'и могли завантажувати образи. Додатків (кожному з яких потрібен секрет) може бути дуже багато, а самі секрети корисно регулярно оновлювати, тому варіант з розкладанням секретів руками відпадає. Тут і приходить на допомогу оператор: ми створюємо контролер, який чекатиме появи namespace'а і за цією подією додасть секрет в namespace.
Нехай за замовчуванням доступ із pod'ів до Інтернету заборонено. Але іноді він може вимагатися: логічно, щоб механізм дозволу доступу працював просто, не вимагаючи специфічних навичок, наприклад, за наявності певного лейбла в namespace'і. Як нам допоможе оператор? Створюється контролер, який очікує на появу лейбла в namespace'і і додає відповідний policy для доступу в інтернет.
Схожа ситуація: нехай нам потрібно додавати на вузол певний заплямуватиякщо на ньому є аналогічний лейбл (з якимось префіксом). Дії з оператором очевидні.
У будь-якому кластері треба вирішувати рутинні завдання, а правильно це робити – за допомогою операторів.
Підсумовуючи всі описані історії, ми дійшли висновку, що для комфортної роботи в Kubernetes потрібно: а) встановлювати доповнення, б) розробляти оператори (Для вирішення повсякденних адмінських завдань).
Як написати оператор для Kubernetes?
Загалом схема проста:
…але тут з'ясовується, що:
Kubernetes API – досить нетривіальна річ, яка потребує чимало часу для освоєння;
програмування теж не для кожного (мова Go обрана як краща, тому що для неї є спеціальний фреймворк — Operator SDK);
з фреймворком як аналогічна ситуація.
Підсумок: для написання контролера (оператора) доводиться витратити суттєві ресурси вивчення матчасти. Це було б виправдано для великих операторів — скажімо, для СУБД MySQL. Але якщо ми пригадаємо описані вище приклади (розкладання секретів, доступ pod'ів в інтернет…), які хочеться теж робити правильно, то ми зрозуміємо, що зусилля, що витрачаються, переважать потрібний зараз результат:
Загалом виникає дилема: витратити багато ресурсів і знайти правильний інструмент для написання операторів або діяти «по-старому» (але швидко). Для її вирішення — знаходження компромісу між цими крайнощами ми створили свій проект: shell-operator(див. також його недавній анонс на хабрі).
Shell-operator
Як він працює? У кластері є pod, у якому лежить Go-бінарник із shell-operator. Поруч із ним зберігається набір хуків(Докладніше про них - див. нижче). Сам shell-operator підписується на певні події у Kubernetes API, за фактом настання яких він запускає відповідні хуки.
Як shell-operator розуміє, які хуки за яких подій викликати? Цю інформацію передають shell-operator'у самі хуки і роблять вони це дуже просто.
Хук - це скрипт на Bash або будь-який інший файл, що виконується, який підтримує єдиний аргумент --config та у відповідь на нього видає JSON. Останній визначає, які об'єкти його цікавлять і які події (для цих об'єктів) слід реагувати:
Проілюструю реалізацію на shell-operator одного з наших прикладів - розкладання секретів для доступу до приватного Registry з образами програми. Вона складається із двох етапів.
Практика: 1. Пишемо хук
Насамперед у хуку обробимо --config, Вказавши, що нас цікавлять namespace'и, а конкретно - момент їх створення:
Першим кроком ми дізнаємося, який namespace був створений, а другим - створюємо через kubectl секрет цього простору імен.
Практика: 2. Збираємо образ
Залишилося передати створений хук shell-operator'у - як це зробити? Сам shell-operator поставляється у вигляді Docker-образу, так що наше завдання – додати хук до спеціального каталогу в цьому образі:
FROM flant/shell-operator:v1.0.0-beta.1
ADD my-handler.sh /hooks
це системний компонент, якому (як мінімум) потрібні права на те, щоб підписатися на події в Kubernetes і щоб розкладати секрети по namespace'ам, тому ми створюємо для хука ServiceAccount (і набір правил).
Результат – ми вирішили нашу проблему рідним для Kubernetes способом, створивши оператор для розкладання секретів.
Інші можливості shell-operator
Щоб обмежити об'єкти вибраного вами типу, з якими працюватиме хук, їх можна фільтрувати, відбираючи за певними лейблами (або за допомогою matchExpressions):
Передбачено механізм дедуплікації, який – за допомогою jq-фільтра – дозволяє перетворювати великі JSON'и об'єктів у маленькі, де залишаються тільки ті параметри, за зміною яких ми хочемо стежити.
При виклику хука shell-operator передає йому дані про об'єкт, які можуть бути використані для будь-яких потреб.
Події, при настанні яких викликаються хуки, не обмежені Kubernetes events: у shell-operator передбачена підтримка виклику хуків за часом (аналогічно crontab у традиційному планувальнику), а також спеціальної події onStartup. Всі ці події можуть комбінуватися і призначатися на той самий хук.
І ще дві особливості shell-operator:
Він працює асинхронно. З моменту отримання події Kubernetes (наприклад, створення об'єкта) у кластері могли відбутися й інші події (наприклад, видалення того самого об'єкта), і це необхідно враховувати в хуках. Якщо хук виконався з помилкою, то за умовчанням він буде повторно викликати до успішного завершення (цю поведінку можна змінити).
Він експортує метрики для Prometheus, за допомогою яких можна зрозуміти, чи працює shell-operator, дізнатися кількість помилок по кожному хуку та поточний розмір черги.
Підсумовуючи цю частину доповіді:
установка доповнень
Для комфортної роботи з Kubernetes була також згадана необхідність встановлення доповнень. Про неї я розповім на прикладі шляху нашої компанії до того, як ми це робимо зараз.
Роботу з Kubernetes ми розпочинали з кількох кластерів, єдиним доповненням у яких був Ingress. У кожний кластер його потрібно було ставити по-різному, і ми зробили кілька YAML-конфігурацій для різних оточень: bare metal, AWS…
Кластерів ставало більше – більше ставало і змін. Крім того, ми покращували самі ці конфігурації, внаслідок чого вони стали досить різноманітними:
Щоб упорядкувати все, ми почали зі скрипту (install-ingress.sh), який приймав аргументом тип кластера, в який будемо деплоїтись, генерував потрібну YAML-конфігурацію і викочував її в Kubernetes.
Якщо коротко, то подальший наш шлях і пов'язані з ним міркування були такі:
для роботи з YAML-конфігураціями потрібен шаблонизатор (на перших етапах це простий sed);
зі зростанням числа кластерів прийшла необхідність для автоматичного оновлення (найраніше рішення - поклали скрипт в Git, по cron'у його оновлюємо і запускаємо);
Такий скрипт був потрібний для Prometheus (install-prometheus.sh), проте він примітний тим, що вимагає набагато більше вступних даних, а також їх зберігання (по-хорошому централізоване і в кластері), причому деякі дані (паролі) можна було автоматично генерувати:
ризик викотити щось неправильне на зростаючу кількість кластерів постійно зростав, тому ми зрозуміли, що інсталяторам (тобто двом скриптам: для Ingress та Prometheus) знадобилося стейджування (кілька гілок у Git, кілька cron'ів на їх оновлення у відповідних: стабільних чи тестових кластерах);
с kubectl apply стало складно працювати, тому що він не є декларативним і вміє лише створювати об'єкти, але не приймати рішення щодо їх статусу/видаляти їх;
не вистачало деяких функцій, які ми на той момент зовсім не реалізували:
повноцінного контролю результату оновлення кластерів,
автоматичного визначення деяких параметрів (вступних для скриптів установки) на основі даних, які можна отримати з кластера (discovery),
його логічний розвиток у вигляді постійної discovery.
Весь цей накопичений досвід ми реалізували в рамках іншого свого проекту. addon-operator.
Addon-operator
В його основі вже згаданий shell-operator. Вся система виглядає так:
До хуків shell-operator'а додаються:
сховище values,
Helm-чарт,
компонент, який стежить за сховищем values і – у разі будь-яких змін – просить Helm перевикотити чарт.
Таким чином, ми можемо зреагувати на подію в Kubernetes, запустити хук, а з цього хука — внести зміни до сховища, після чого буде перевикачено чарт. У схемі, що вийшла, ми виділяємо набір хуків і чарт в один компонент, який називаємо модулем:
Модуль може бути безліч, а до них ми додаємо глобальні хуки, глобальне сховище values і компонент, який стежить за цим глобальним сховищем.
Тепер, коли в Kubernetes щось відбувається, ми можемо на це відреагувати за допомогою глобального хука та змінити щось у глобальному сховищі. Ця зміна буде помічена і викличе викочування всіх модулів у кластері:
Ця схема відповідає всім вимогам до встановлення доповнень, що були озвучені вище:
За шаблонизацію та декларативність відповідає Helm.
Питання автооновлення вирішено за допомогою глобального хука, який за розкладом ходить до registry і, якщо бачить там новий образ системи, перекочує її (тобто «сам себе»).
Зберігання налаштувань у кластері реалізовано за допомогою ConfigMap, В якому записані первинні дані для сховищ (при старті вони завантажуються в сховища).
Проблеми генерації паролів, discovery та continuous discovery вирішені за допомогою хуків.
Стейджування досягнуто завдяки тегам, які Docker підтримує із коробки.
Контроль результату здійснюється за допомогою метрик, якими ми можемо зрозуміти статус.
Вся ця система реалізована у вигляді єдиного бінарника на Go, який отримав назву addon-operator. Завдяки цьому схема виглядає простіше:
Головний компонент на цій схемі - набір модулів (Виділені сірим кольором внизу). Тепер ми можемо невеликими зусиллями написати модуль для потрібного доповнення та бути впевненими, що воно буде встановлене у кожен кластер, оновлюватиметься та реагуватиме на потрібні йому події у кластері.
"Флант" використовує addon-operator на 70+ Kubernetes-кластерах. Поточний статус - альфа-версія. Зараз ми готуємо документацію, щоб випустити бету, а поки що у репозиторії доступні приклади, на основі яких можна створити свій addon.
Де взяти самі модулі для addon-operator? Публікація своєї бібліотеки – наступний етап для нас, ми плануємо це зробити влітку.