ProHoster > Блог > адміністрування > Go? Bash! Зустрічайте shell-operator (огляд та відео доповіді з KubeCon EU'2020)
Go? Bash! Зустрічайте shell-operator (огляд та відео доповіді з KubeCon EU'2020)
Цього року головна європейська конференція з Kubernetes – KubeCon + CloudNativeCon Europe 2020 – була віртуальною. Втім, така зміна формату не завадила нам виступити із давно запланованою доповіддю «Go? Bash! Meet the Shell-operator», присвяченим нашому Open Source-проекту shell-operator.
У цій статті, написаній за мотивами виступу, подано підхід до спрощення процесу створення операторів для Kubernetes і показано, як з мінімальними зусиллями за допомогою shell-operator'а можна зробити свій власний.
представляємо відео з доповіддю (~23 хвилини англійською, помітно інформативнішою за статтю) і основне вижимання з нього в текстовому вигляді. Поїхали!
Ми у «Фланті» постійно все оптимізуємо та автоматизуємо. Сьогодні мова йтиме про ще одну захоплюючу концепцію. Зустрічайте: cloud-native shell-скриптинг!
Втім, почнемо з контексту, в якому все це відбувається, — з Kubernetes.
Kubernetes API та контролери
API в Kubernetes можна представити у вигляді файлового сервера з директоріями під кожен тип об'єктів. Об'єкти (ресурси) на цьому сервері представлені файлами YAML. Крім того, сервер має базовий API, що дозволяє робити три речі:
отримувати ресурс з його kind'у та імені;
змінювати ресурс (при цьому сервер зберігає лише «правильні» об'єкти — усі некоректно сформовані чи призначені для інших директорій відкидаються);
стежити за ресурсом (у цьому випадку користувач одразу отримує його поточну/оновлену версію).
Таким чином, Kubernetes виступає таким собі файловим сервером (для YAML-маніфестів) з трьома базовими методами (так, взагалі-то є й інші, але ми їх поки що опустимо).
Проблема в тому, що сервер може лише зберігати інформацію. Щоб змусити її працювати, потрібний контролер — друге за важливістю та фундаментальністю поняття у світі Kubernetes.
Розрізняють два основні типи контролерів. Перший бере інформацію з Kubernetes, обробляє її відповідно до вкладеної логіки та повертає до K8s. Другий бере інформацію з Kubernetes, але, на відміну від першого типу, змінює стан деяких зовнішніх ресурсів.
Давайте розглянемо докладніше процес створення Deployment'у в Kubernetes:
Deployment Controller (що входить до kube-controller-manager) отримує інформацію про Deployment і створює ReplicaSet.
ReplicaSet на основі цієї інформації створює дві репліки (два pod'а), але ці pod'и ще не заплановані.
Планувальник планує pod'и та додає в їх YAML'и інформацію про вузли.
Kubelet'и вносять зміни у зовнішній ресурс (скажімо, Docker).
Потім вся ця послідовність повторюється у зворотному порядку: kubelet перевіряє контейнери, обчислює статус pod'а та відсилає його назад. Контролер ReplicaSet отримує статус та оновлює стан набору реплік. Те саме відбувається з Deployment Controller'ом, і користувач нарешті отримує оновлений (поточний) статус.
Shell-operator
Виходить, що в основі Kubernetes лежить спільна робота різних контролерів (оператори Kubernetes також контролери). Виникає питання, як створити свій оператор із мінімальними зусиллями? І тут на допомогу приходить розроблений нами shell-operator. Він дозволяє системним адміністраторам створювати власні оператори, використовуючи звичні методи.
Простий приклад: копіювання секретів
Давайте розглянемо найпростіший приклад.
Припустимо, у нас є кластер Kubernetes. У ньому є простір імен default з деяким Secret'ом mysecret. Крім цього, у кластері є й інші простір імен. До деяких із них прикріплений певний лейбл. Наша мета – скопіювати Secret у простори імен із лейблом.
Завдання ускладнюється тим, що в кластері можуть з'являтися нові простори імен, і деякі з них можуть мати цей лейбл. З іншого боку, при видаленні лейбла Secret також має видалятися. Крім того, сам Secret також може змінюватися: у цьому випадку новий Secret повинен бути скопійований у всі простори імен з лейблами. Якщо Secret випадково видаляється у якомусь просторі імен, наш оператор має його одразу відновити.
Тепер, коли завдання сформульовано, настав час приступити до її реалізації за допомогою shell-operator. Але спочатку варто сказати кілька слів про самий shell-operator's.
Принципи роботи shell-operator
Як і інші робочі навантаження у Kubernetes, shell-operator функціонує у своєму pod'і. У цьому pod'є в каталозі /hooks зберігаються файли, що виконуються. Це можуть бути скрипти на Bash, Python, Ruby і т.д. Такі файли ми називаємо хуками (гачки).
Shell-operator підписується на події Kubernetes та запускає ці хуки у відповідь на ті з подій, що нам потрібні.
Яким чином shell-operator дізнається, який хук і коли запускати? Справа в тому, що кожен хук має дві стадії. Під час старту shell-operator запускає усі хуки з аргументом --config - Це стадія конфігурування. А вже після неї хуки запускаються нормально — у відповідь на події, до яких вони прив'язані. В останньому випадку хук отримує контекст прив'язки (обов'язковий контекст) - дані у форматі JSON, докладніше про які ми поговоримо нижче.
Робимо оператор на Bash
Наразі ми готові до реалізації. Для цього нам потрібно написати дві функції (до речі, рекомендуємо бібліотеку shell_lib, яка сильно спрощує написання хуків на Bash):
перша необхідна стадії конфігурування — вона виводить контекст прив'язки;
друга містить основну логіку хука.
#!/bin/bash
source /shell_lib.sh
function __config__() {
cat << EOF
configVersion: v1
# BINDING CONFIGURATION
EOF
}
function __main__() {
# THE LOGIC
}
hook::run "$@"
Наступний крок — визначитися з тим, які нам потрібні об'єкти. У нашому випадку потрібно відстежувати:
секрет-джерело щодо наявності змін;
всі namespace'и в кластері, щоб знати, до яких із них прикріплений лейбл;
секрети-мети, щоб переконатися, що всі вони синхронізовані із секретом-джерелом.
Підписуємося на секрет-джерело
Binding configuration йому досить проста. Ми вказуємо, що нас цікавить Secret із назвою mysecret у просторі імен default:
Як бачите, у конфігурації з'явилося нове поле з ім'ям jqFilter. Як натякає його назва, jqFilter відфільтровує всю зайву інформацію і створює новий об'єкт JSON з полями, які цікавлять нас. Хук із такою конфігурацією отримає наступний binding context:
Він містить у собі масив filterResults для кожного простору імен у кластері. Бульова змінна hasLabel показує, чи лейбл прикріплений до даного простору імен. Селектор keepFullObjectsInMemory: false говорить про те, що немає потреби тримати повні об'єкти в пам'яті.
Відслідковуємо секрети-мети
Ми підписуємося на всі Secret'и, у яких задана інструкція managed-secret: "yes" (це наші цільові dst_secrets):
У цьому випадку jqFilter відфільтровує всю інформацію за винятком простору імен та параметра resourceVersion. Останній параметр був переданий інструкції при створенні секрету: він дозволяє порівнювати версії секретів та підтримувати їх у актуальному стані.
Хук, налаштований подібним чином, отримає три контексти прив'язки, описані вище. Їх можна уявити як свого роду знімок (знімок) кластера.
На основі цієї інформації можна розробити базовий алгоритм. Він перебирає всі простори імен:
якщо hasLabel має значення true для поточного простору імен:
порівнює глобальний секрет з локальним:
якщо вони однакові - нічого не робить;
якщо вони відрізняються - виконує kubectl replace або create;
якщо hasLabel має значення false для поточного простору імен:
переконується, що Secret відсутня у цьому просторі імен:
якщо локальний Secret присутній - видаляє його за допомогою kubectl delete;
якщо локального Secret не виявлено — нічого не робить.
Ось так ми змогли створити простий контролер Kubernetes, використавши 35 рядків YAML-конфігів та приблизно таку ж кількість коду на Bash! Завдання shell-operator полягає у тому, щоб зв'язати їх разом.
Втім, копіювання секретів — це не єдина сфера застосування утиліти. Ось ще кілька прикладів, які покажуть, на що він здатний.
Приклад 1: внесення змін до ConfigMap
Давайте розглянемо Deployment, що складається із трьох pod'ів. Pod'и використовують ConfigMap для зберігання певної конфігурації. Під час запуску pod'ів ConfigMap перебував у певному стані (назвемо його v.1). Відповідно, всі pod'и використовують саме цю версію ConfigMap.
Припустимо, що ConfigMap змінився (v.2). Однак pod'и використовуватимуть колишню версію ConfigMap (v.1):
Як зробити так, щоб вони перейшли на новий ConfigMap (v.2)? Відповідь проста: скористатися template'ом. Давайте додамо інструкцію з контрольною сумою в розділ template конфігурації Deployment'а:
В результаті в усіх pod'ах буде прописана ця контрольна сума, і вона буде такою самою, як у Deployment'a. Тепер потрібно просто оновлювати інструкцію при зміні ConfigMap. І shell-operator дуже доречний у цьому випадку. Все що потрібно – це запрограмувати хук, який підпишеться на ConfigMap та оновить контрольну суму.
Якщо користувач вносить зміни до ConfigMap, shell-operator їх помітить та перерахує контрольну суму. Після чого у гру вступить магія Kubernetes: оркестратор уб'є pod, створить новий, дочекається, коли той стане Readyі перейде до наступного. В результаті Deployment синхронізується та перейде на нову версію ConfigMap.
Приклад 2: робота з Custom Resource Definitions
Як відомо, Kubernetes дозволяє створювати кастомні типи (kinds) об'єктів. Наприклад, можна створити kind MysqlDatabase. Припустимо, цей тип має два metadata-параметри: name и namespace.
apiVersion: example.com/v1alpha1
kind: MysqlDatabase
metadata:
name: foo
namespace: bar
Ми маємо кластер Kubernetes з різними просторами імен, в яких ми можемо створювати бази даних MySQL. У цьому випадку shell-operator можна використовувати для відстеження ресурсів MysqlDatabase, їх підключення до MySQL-серверу та синхронізації бажаного та спостережуваного станів кластера.
Приклад 3: моніторинг кластерної мережі
Як відомо, використання ping'а є найпростішим способом моніторингу мережі. У цьому прикладі ми покажемо, як реалізувати такий моніторинг за допомогою shell-operator.
Насамперед потрібно підписатися на вузли. Shell-operator'у потрібні ім'я та IP-адреса кожного вузла. З їх допомогою він пінгуватиме ці вузли.
Параметр executeHookOnEvent: [] запобігає запуску хука у відповідь на будь-яку подію (тобто у відповідь на зміну, додавання, видалення вузлів). Однак він запускатиметься (і оновлювати список вузлів) за розкладом - щохвилини, як наказує поле schedule.
Тепер постає питання, як саме ми дізнаємося про проблеми на кшталт втрати пакетів? Давайте поглянемо на код:
function __main__() {
for i in $(seq 0 "$(context::jq -r '(.snapshots.nodes | length) - 1')"); do
node_name="$(context::jq -r '.snapshots.nodes['"$i"'].filterResult.name')"
node_ip="$(context::jq -r '.snapshots.nodes['"$i"'].filterResult.ip')"
packets_lost=0
if ! ping -c 1 "$node_ip" -t 1 ; then
packets_lost=1
fi
cat >> "$METRICS_PATH" <<END
{
"name": "node_packets_lost",
"add": $packets_lost,
"labels": {
"node": "$node_name"
}
}
END
done
}
Ми перебираємо список вузлів, отримуємо їхні імена та IP-адреси, пінгуємо та надсилаємо результати в Prometheus. Shell-operator вміє експортувати метрики до Prometheus, зберігаючи їх у файл, розташований згідно з дорогою, вказаною в змінній оточенні $METRICS_PATH.
Ось так можна зробити оператора для простого моніторингу мережі у кластері.
Механізм черг
Ця стаття була б неповною без опису ще одного важливого механізму, вбудованого в shell-operator. Уявіть, що він виконує певний хук у відповідь на подію у кластері.
Що станеться, якщо в цей же час у кластері станеться ще одне подія?
Чи запустить shell-operator ще один екземпляр хука?
А що, якщо у кластері одразу відбудуться, скажімо, п'ять подій?
Чи буде shell-operator обробляти їх паралельно?
А як щодо споживаних ресурсів, таких як пам'ять та CPU?
На щастя, у shell-operator є вбудований механізм черг. Усі події поміщаються у чергу і обробляються послідовно.
Проілюструємо це на прикладах. Припустимо, що у нас є два хуки. Перша подія дістається першому хуку. Після того, як його обробка завершена, черга рухається вперед. Наступні три події перенаправляються на другий хук — вони витягуються з черги і надходять до нього «пачкою». Тобто хук отримує масив подій — чи, точніше, масив контекстів прив'язки.
Також ці події можна об'єднати в одну велику. За це відповідає параметр group у конфігурації прив'язки.
Можна створювати будь-яку кількість черг/хуків та їх усіляких комбінацій. Наприклад, одна черга може працювати із двома хуками, або навпаки.
Все, що потрібно зробити, – відповідним чином налаштувати поле queue у конфігурації прив'язки. Якщо не вказано ім'я черги, хук запускається в черзі за промовчанням (default). Подібний механізм черг дозволяє повністю вирішити всі проблеми управління ресурсами під час роботи з хуками.
Висновок
Ми розповіли, що таке shell-operator, показали, як з його допомогою можна швидко та без особливих зусиль створювати оператори Kubernetes, та навели кілька прикладів його використання.
Детальна інформація про shell-operator, а також короткий посібник щодо його використання доступні у відповідному репозиторії на GitHub. Не соромтеся звертатися до нас із запитаннями: обговорити їх можна у спеціальній Telegram-групі (російською) або в цьому форумі (англійською).
А якщо сподобалося – ми завжди раді новим issues/PR/зіркам на GitHub, де, до речі, можна знайти й інші цікаві проекти. Серед них варто особливо виділити addon-operator, який є старшим братом для shell-operator. Ця утиліта використовує чарти Helm для встановлення доповнень, вміє доставляти оновлення та стежити за різними параметрами/значеннями чартів, контролює процес інсталяції чартів, а також може модифікувати їх у відповідь на події у кластері.