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'а можна зробити свій власний.

Go? Bash! Зустрічайте shell-operator (огляд та відео доповіді з KubeCon EU'2020)

представляємо відео з доповіддю (~23 хвилини англійською, помітно інформативнішою за статтю) і основне вижимання з нього в текстовому вигляді. Поїхали!

Ми у «Фланті» постійно все оптимізуємо та автоматизуємо. Сьогодні мова йтиме про ще одну захоплюючу концепцію. Зустрічайте: cloud-native shell-скриптинг!

Втім, почнемо з контексту, в якому все це відбувається, — з Kubernetes.

Kubernetes API та контролери

API в Kubernetes можна представити у вигляді файлового сервера з директоріями під кожен тип об'єктів. Об'єкти (ресурси) на цьому сервері представлені файлами YAML. Крім того, сервер має базовий API, що дозволяє робити три речі:

  • отримувати ресурс з його kind'у та імені;
  • змінювати ресурс (при цьому сервер зберігає лише «правильні» об'єкти — усі некоректно сформовані чи призначені для інших директорій відкидаються);
  • стежити за ресурсом (у цьому випадку користувач одразу отримує його поточну/оновлену версію).

Таким чином, Kubernetes виступає таким собі файловим сервером (для YAML-маніфестів) з трьома базовими методами (так, взагалі-то є й інші, але ми їх поки що опустимо).

Go? Bash! Зустрічайте shell-operator (огляд та відео доповіді з KubeCon EU'2020)

Проблема в тому, що сервер може лише зберігати інформацію. Щоб змусити її працювати, потрібний контролер — друге за важливістю та фундаментальністю поняття у світі 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'ом, і користувач нарешті отримує оновлений (поточний) статус.

Go? Bash! Зустрічайте shell-operator (огляд та відео доповіді з KubeCon EU'2020)

Shell-operator

Виходить, що в основі Kubernetes лежить спільна робота різних контролерів (оператори Kubernetes також контролери). Виникає питання, як створити свій оператор із мінімальними зусиллями? І тут на допомогу приходить розроблений нами shell-operator. Він дозволяє системним адміністраторам створювати власні оператори, використовуючи звичні методи.

Go? Bash! Зустрічайте shell-operator (огляд та відео доповіді з KubeCon EU'2020)

Простий приклад: копіювання секретів

Давайте розглянемо найпростіший приклад.

Припустимо, у нас є кластер 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 і т.д. Такі файли ми називаємо хуками (гачки).

Go? Bash! Зустрічайте shell-operator (огляд та відео доповіді з KubeCon EU'2020)

Shell-operator підписується на події Kubernetes та запускає ці хуки у відповідь на ті з подій, що нам потрібні.

Go? Bash! Зустрічайте shell-operator (огляд та відео доповіді з KubeCon EU'2020)

Яким чином 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:

Go? Bash! Зустрічайте shell-operator (огляд та відео доповіді з KubeCon EU'2020)

function __config__() {
  cat << EOF
    configVersion: v1
    kubernetes:
    - name: src_secret
      apiVersion: v1
      kind: Secret
      nameSelector:
        matchNames:
        - mysecret
      namespace:
        nameSelector:
          matchNames: ["default"]
      group: main
EOF

В результаті хук запускатиметься при зміні секрету-джерела (src_secret) та отримувати наступний binding context:

Go? Bash! Зустрічайте shell-operator (огляд та відео доповіді з KubeCon EU'2020)

Як бачите, в ньому міститься ім'я та об'єкт цілком.

Слідкуємо за просторами імен

Тепер потрібно підписатися на namespaces. Для цього вкажемо наступне binding configuration:

- name: namespaces
  group: main
  apiVersion: v1
  kind: Namespace
  jqFilter: |
    {
      namespace: .metadata.name,
      hasLabel: (
       .metadata.labels // {} |  
         contains({"secret": "yes"})
      )
    }
  group: main
  keepFullObjectsInMemory: false

Як бачите, у конфігурації з'явилося нове поле з ім'ям jqFilter. Як натякає його назва, jqFilter відфільтровує всю зайву інформацію і створює новий об'єкт JSON з полями, які цікавлять нас. Хук із такою конфігурацією отримає наступний binding context:

Go? Bash! Зустрічайте shell-operator (огляд та відео доповіді з KubeCon EU'2020)

Він містить у собі масив filterResults для кожного простору імен у кластері. Бульова змінна hasLabel показує, чи лейбл прикріплений до даного простору імен. Селектор keepFullObjectsInMemory: false говорить про те, що немає потреби тримати повні об'єкти в пам'яті.

Відслідковуємо секрети-мети

Ми підписуємося на всі Secret'и, у яких задана інструкція managed-secret: "yes" (це наші цільові dst_secrets):

- name: dst_secrets
  apiVersion: v1
  kind: Secret
  labelSelector:
    matchLabels:
      managed-secret: "yes"
  jqFilter: |
    {
      "namespace":
        .metadata.namespace,
      "resourceVersion":
        .metadata.annotations.resourceVersion
    }
  group: main
  keepFullObjectsInMemory: false

У цьому випадку jqFilter відфільтровує всю інформацію за винятком простору імен та параметра resourceVersion. Останній параметр був переданий інструкції при створенні секрету: він дозволяє порівнювати версії секретів та підтримувати їх у актуальному стані.

Хук, налаштований подібним чином, отримає три контексти прив'язки, описані вище. Їх можна уявити як свого роду знімок (знімок) кластера.

Go? Bash! Зустрічайте shell-operator (огляд та відео доповіді з KubeCon EU'2020)

На основі цієї інформації можна розробити базовий алгоритм. Він перебирає всі простори імен:

  • якщо hasLabel має значення true для поточного простору імен:
    • порівнює глобальний секрет з локальним:
      • якщо вони однакові - нічого не робить;
      • якщо вони відрізняються - виконує kubectl replace або create;
  • якщо hasLabel має значення false для поточного простору імен:
    • переконується, що Secret відсутня у цьому просторі імен:
      • якщо локальний Secret присутній - видаляє його за допомогою kubectl delete;
      • якщо локального Secret не виявлено — нічого не робить.

Go? Bash! Зустрічайте shell-operator (огляд та відео доповіді з KubeCon EU'2020)

Реалізація алгоритму на Bash ви можете завантажити в нашому репозиторії з прикладами.

Ось так ми змогли створити простий контролер 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):

Go? Bash! Зустрічайте shell-operator (огляд та відео доповіді з KubeCon EU'2020)

Як зробити так, щоб вони перейшли на новий ConfigMap (v.2)? Відповідь проста: скористатися template'ом. Давайте додамо інструкцію з контрольною сумою в розділ template конфігурації Deployment'а:

Go? Bash! Зустрічайте shell-operator (огляд та відео доповіді з KubeCon EU'2020)

В результаті в усіх pod'ах буде прописана ця контрольна сума, і вона буде такою самою, як у Deployment'a. Тепер потрібно просто оновлювати інструкцію при зміні ConfigMap. І shell-operator дуже доречний у цьому випадку. Все що потрібно – це запрограмувати хук, який підпишеться на ConfigMap та оновить контрольну суму.

Якщо користувач вносить зміни до ConfigMap, shell-operator їх помітить та перерахує контрольну суму. Після чого у гру вступить магія Kubernetes: оркестратор уб'є pod, створить новий, дочекається, коли той стане Readyі перейде до наступного. В результаті Deployment синхронізується та перейде на нову версію ConfigMap.

Go? Bash! Зустрічайте shell-operator (огляд та відео доповіді з KubeCon EU'2020)

Приклад 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-серверу та синхронізації бажаного та спостережуваного станів кластера.

Go? Bash! Зустрічайте shell-operator (огляд та відео доповіді з KubeCon EU'2020)

Приклад 3: моніторинг кластерної мережі

Як відомо, використання ping'а є найпростішим способом моніторингу мережі. У цьому прикладі ми покажемо, як реалізувати такий моніторинг за допомогою shell-operator.

Насамперед потрібно підписатися на вузли. Shell-operator'у потрібні ім'я та IP-адреса кожного вузла. З їх допомогою він пінгуватиме ці вузли.

configVersion: v1
kubernetes:
- name: nodes
  apiVersion: v1
  kind: Node
  jqFilter: |
    {
      name: .metadata.name,
      ip: (
       .status.addresses[] |  
        select(.type == "InternalIP") |
        .address
      )
    }
  group: main
  keepFullObjectsInMemory: false
  executeHookOnEvent: []
schedule:
- name: every_minute
  group: main
  crontab: "* * * * *"

Параметр 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 у конфігурації прив'язки.

Go? Bash! Зустрічайте shell-operator (огляд та відео доповіді з KubeCon EU'2020)

Можна створювати будь-яку кількість черг/хуків та їх усіляких комбінацій. Наприклад, одна черга може працювати із двома хуками, або навпаки.

Go? Bash! Зустрічайте shell-operator (огляд та відео доповіді з KubeCon EU'2020)

Все, що потрібно зробити, – відповідним чином налаштувати поле queue у конфігурації прив'язки. Якщо не вказано ім'я черги, хук запускається в черзі за промовчанням (default). Подібний механізм черг дозволяє повністю вирішити всі проблеми управління ресурсами під час роботи з хуками.

Висновок

Ми розповіли, що таке shell-operator, показали, як з його допомогою можна швидко та без особливих зусиль створювати оператори Kubernetes, та навели кілька прикладів його використання.

Детальна інформація про shell-operator, а також короткий посібник щодо його використання доступні у відповідному репозиторії на GitHub. Не соромтеся звертатися до нас із запитаннями: обговорити їх можна у спеціальній Telegram-групі (російською) або в цьому форумі (англійською).

А якщо сподобалося – ми завжди раді новим issues/PR/зіркам на GitHub, де, до речі, можна знайти й інші цікаві проекти. Серед них варто особливо виділити addon-operator, який є старшим братом для shell-operator. Ця утиліта використовує чарти Helm для встановлення доповнень, вміє доставляти оновлення та стежити за різними параметрами/значеннями чартів, контролює процес інсталяції чартів, а також може модифікувати їх у відповідь на події у кластері.

Go? Bash! Зустрічайте shell-operator (огляд та відео доповіді з KubeCon EU'2020)

Відео та слайди

Відео з виступу (~23 хвилини):


Презентація доповіді:

PS

Читайте також у нашому блозі:

Джерело: habr.com

Додати коментар або відгук