Бути девопсом – то, звичайно круто – всі ці клауди, Терраформи, сесуріті і прочі дуже цікаві штуки.
Але я давно хотів спробувати і щось більш “реальне”, щось таке, щоб можна було потримати в руках, і цими ж руками зібрати.
В минулому році, коли думав про чергову підготовку до зими (див. Підготовка до зими 2023-2024: електрохарчування), знов згадав свою ідею мати вдома пожежну сигналізацію – бо на балконі стоять акумулятори. І, звісно, можна купити готові рішення від Ajax Systems, але ж можна і зробити самому!
Аналогічно з системою відеоспостереження – можна купити готові рішення (власне, я так і зробив) – а можна зібрати власний солюшон, і це було б набагато цікавіше.
Тож врешті-решт я таки вирішив почати знайомитись з Arduino. До того ж досвід роботи з мікроконтролерами може знадобитись, якщо доведеться мати справу з дронами (if you know what I mean 😉 ).
Ну і само собою – хіба ж можна займатись такими штуками, і не написати про на RTFM? 🙂 Тож додаю нову рубрику, і сподіваюсь, що буду її періодично оновлювати.
Arduino: the very beginning
Раніше я не мав справ ані з мікроконтролерами взагалі (хоча в універі наче були пари по ним), ані з Adruino, тому на самому початку для мене це був досить темний ліс: я знав, що це якась маленька плата, з якою можна робити якісь штуки.
Тож самим першим кроком було загуглити якось на кшталт “adruino що можна зробити” і, чесно кажучи, навіть здивувався – скільки ж всього є під цю платформу.
Тож з того, що можна зробити – і що було б корисно вдома, аби мати більше мотивації:
система відеоспостереження
сигналізація пожежі/потопу
система автополиву квітів
робот-пилосос
система управління акваріумом
Та безліч всього іншого, бо датчиків для Arduino – просто море.
Як познайомитись з Arduino?
Знову-таки – я ніколи з ним не мав справу, тож треба було якось почати освоюватись в новому для себе світі.
і стартовий набір Super Arduino Starter Kit від Keyestudio – купував на цьому сайті, і взагалі класний сайт з купою всього – і документація, і всякі плати/датчики/корпуси
Взагалі стартових наборів теж дуже багато, я вибирав по принципу “побільше всього відразу”. Враховуючи вартість в 2-3 тисячі гривень – можна собі дозволити брати “максимальний” набір (в мене ще не самий максимальний), бо краще відразу мати під рукою все, аніж почати щось робити, потім зрозуміти, що тобі не вистачає якоїсь деталі, і чекати, поки на пошту прийде замовлення.
Щодо книги – вона дійсно прям дуже зайшла. Розказується як для маленьких, з картинками, досить детально і з самого-самого початку – як раз те, що мені потрібно.
Ще, звісно, є купа матеріалів у всяких блогах, на Youtube тощо – але до них поки не добрався.
Насправді я тільки от сьогодні дістав цей набір із шафи, хоча почав читати книгу і купив набір ще восени, але потім якось те-це, якісь справи-робота, і трохи підзабив на це діло.
Тож час розпаковувати його – і пробувати щось сконектити.
Arduino Starter Kit
Список компонентів в моєму наборі прям дуже широкий:
5 * Синій світлодіод
5 * Червоний світлодіод
5 * Жовтий світлодіод
1 * RGB світлодіод
8 * Резистор 220 Ом
5 * Резистор 10 кОм
5 * Резистор 1кОм
1 * 10K потенціометр
1 * Зумер (активний)
1 * Зумер (пасивний)
4 * Великий кнопковий перемикач
2 * Датчик нахилу
3 * Фоторезистор
1 * Датчик полум’я
1 * Датчик температури LM35
1 * Регістр зсуву 74HC595N
1 * 7-сегментний світлодіодний 1x модуль
1 * 7-сегментний світлодіодний 4х модуль
1 * 8 * 8 Світлодіодна матриця
1 * 2×16 РК-дисплей
1 * ІЧ-приймач
1 * ІЧ-пульт дистанційного керування
1 * Серводвигун
1 * Кроковий модуль драйвера
1 * Кроковий двигун
1 * Модуль джойстика
1 * Релейний модуль
1 * Датчик руху PIR
1 * Аналоговий датчик газу
1 * Модуль акселерометра ADXL345
1 * Ультразвуковий датчик HC-SR04
1 * Модуль годинника реального часу на DS3231
1 * Датчик температури і вологості DHT11
1 * Датчик вологості грунту
1 * RFID-модуль RC522
1 * RFID-карта
1 * RFID-ключ
40 * Конектор
1 * Макетна плата
10 * Перемички мама-мама
30 * Перемички тато-тато
1 * 6-елементна AA акумуляторна батарея
1 * Кабель USB
Arduino controller
Власне сама “ардуінка”, контролер – головна плата:
Забігаючи наперед – для Arduino використовується Wiring – фреймворк зі спрощеним C++, але начебто можна писати і на самій С++ (буде привід згадати її).
Підключення Arduino до комп’ютера
Ну, що – спробуємо його включити?)
Живлення може бути прямо від USB:
Ваааау!)))
“It works!” (c)
Тепер можна починати щось робити.
Тож в наступній частині ми встановимо IDE на Linux, і заставимо нашу ардуінку блимкати LED.
В цілому проблем з ним поки не маємо, але в будь-якому разі потрібен його моніторинг, для чого Karpeneter “з коробки” надає метрики, які можемо використати в Grafana та Prometheus/VictoriaMetrics алертах.
Тож що будемо робити сьогодні:
додамо збір метрик до VictoriaMetrics
подивимось які метрики нам можуть бути корисні
додамо Grafana Dashboard для WorkerNodes + Karpenter
Взагалі пост вийшов більше про Grafana, ніж про Karpenter, але в графіках в основному використовуються метрики саме від Karpenter.
Окремо треба буде створити алерти – але це вже іншим разом. Маючи уяву про доступні метрики Karpenter та Prometheus-запити для графіків в Grafana проблем з алертами не має бути.
Деплоїмо, і для перевірки таргету відкриваємо порт до VMAgent:
$ kk port-forward svc/vmagent-vm-k8s-stack 8429
Перевіряємо таргети:
Для перевірки метрик відкриваємо порт до VMSingle:
$ kk port-forward svc/vmsingle-vm-k8s-stack 8429
І шукаємо дані по запиту {job="karpenter"}:
Корисні метрики Karpenter
Тепер глянемо, які саме метрики нам можуть бути корисні.
Але перед тим, як розбиратись з метриками – давайте проговоримо основні поняття в Karpenter (окрім очевидних типу Node, NodeClaim або Pod):
controller: компонент Karpenter, який віподвідає за певний аспект його роботи, наприклад, Pricing controller відповідає за перевірку вартості інстансів, а Disruption Controller відповідає за керування процесом зміни стану WorkerNodes
reconciliation (“узгодження”): процес, коли Karpenter виконує узгодження бажаного стану (desired state) и реального (current state), наприклад – при появі Pod, для якого нема вільних ресурсів на існуючих WorkerNodes, Karpenter створить нову Node, на якій зможе запустись Pod, і його статус стане Running – тоді reconciliation процес статусу цього поду завершиться
consistency (“когерентність” або “узгодженість”): процес внутрішнього контролю і забезпечення відповідності необхідним параметрам (наприклад, перевірка того, що створена WorkerNode має диск розміром саме 30 GB)
disruption: процес зміни WorkerNodes в кластері, наприклад перестворення WorkerNode (для заміни на інстанс з більшою кількістю CPU або Memory), або видалення існуючої ноди, на якій нема запущених Pods
interruption: випадки, коли EC2 буде зупинено у зв’язку з помилками на hardware, виключення інстансу (коли робиться Stop або Terminate instance), або у випадку зі Spot – коли AWS “відкликає” інстанс; ці евенти йдуть на віподвідну SQS, звідки їх отримує Karpenter, щоб запустити новий інстанс на заміну
provisioner: компонент, який аналізує поточні потреби кластера, такі як запити на створення нових Pod, визначає, які ресурси потрібно створити (WorkerNodes), і ініціює створення нових (взагалі, Provisioner був замінений на NodePool, але окремі метрики по ньому залишились)
Тут я зібрав тільки ті метрики, які мені вважаються найбільш корисними в даний момент, але варто самому передивитись документацію Inspect Karpenter Metrics і є трохи більше деталей у документації Datadog:
Controller:
controller_runtime_reconcile_errors_total: кількість помилок при оновленні WorkerNodes (тобто в роботі Disruption Controller при виконанні операцій по Expiration, Drift, Interruption та Consolidation) – корисно мати графік або алерт
controller_runtime_reconcile_total: загальна кількість таких операції – корисно мати уяву про активність Karpenter і, можливо, мати алерт, якщо це відбувається надто часто
Сonsistency:
karpenter_consistency_errors: виглядає як корисна метрика, але в мене вона пуста (принаймні поки що)
Disruption:
karpenter_disruption_actions_performed_total: загальна кількість дій по disruption (видалення/перестворення WorkerNodes), в лейблах метрик вказується disruption method – корисно мати уяву про активність Karpenter і, можливо, мати алерт, якщо це відбувається надто часто
karpenter_disruption_eligible_nodes: загальна кількість WorkerNodes для виконання disruption (видалення/перестворення WorkerNodes), в лейблах метрик вказується disruption method
karpenter_disruption_replacement_nodeclaim_failures_total: загальна кількість помилок при створенні нових WorkerNodes на заміну старим, в лейблах метрик вказується disruption method
Interruption:
karpenter_interruption_actions_performed: кількість дій за повідомленнями про EC2 Interruption (з SQS) – можливо має сенс, але в мене за тиждень збору метрик такого не траплялось
Nodeclaims:
karpenter_nodeclaims_created: загальна кількість створенних NodeClaims з лейблами по причині створення та відповідним NodePool
karpenter_nodeclaims_terminated: аналогічно, але по видаленним NodeClaims
Provisioner:
karpenter_provisioner_scheduling_duration_seconds: можливо, має сенс моніторити, бо якщо цей показник буде рости але буде надто великим – то це може бути ознакою проблем; проте, в мене за тиждень хістограма karpenter_provisioner_scheduling_duration_seconds_bucket незмінна
Nodepool:
karpenter_nodepool_limit: ліміт CPU/Memory NodePool, заданий в його Provisioner (spec.limits)
karpenter_nodepool_usage: використання ресурсів NodePool – CPU, Memory, Volumes, Pods
Nodes:
karpenter_nodes_allocatable: інформація по існуючим WorkerNodes – тип, кількість CPU/Memory, Spot/On-Demand, Availability Zone, etc
можна мати графік по кількості Spot/On-Demand інстансів
можна використовувати для отримання даних по доступних ресурсах ЦПУ/пам’яті – sum(karpenter_nodes_allocatable) by (resource_type)
karpenter_nodes_created: загальна кількість створенних нод
karpenter_nodes_terminated: загальна кількість видалених нод
karpenter_nodes_total_pod_limits: загальна кількість всіх Pod Limits (окрім DaemonSet) на кожній WorkerNode
karpenter_nodes_total_pod_requests: загальна кількість всіх Pod Requests (окрім DaemonSet) на кожній WorkerNode
Pods:
karpenter_pods_startup_time_seconds: час від сторення поду до його переходу в статус Running (сума по всім подам)
karpenter_pods_state: досить корисна метрика, бо в лейблах має статус поду, на якій він ноді запущений, неймспейс тощо
Cloudprovider:
karpenter_cloudprovider_errors_total: кількість помилок від AWS
karpenter_cloudprovider_instance_type_price_estimate: вартість інстансів по типам – можна на дашборді виводити вартість compute-потужностей кластеру
Створення Grafana dashboard
Для Grafana є готовий дашборд – Display all useful Karpenter metrics, але він якось зовсім не інформативний. Втім, з нього можна взяти деякі графіки та/або запити.
Зараз в мене є власна борда для перевірки статусу та ресурів по кожній окремій WorkerNode:
В графіках цієї борди є Data Links на дашборду з деталями по на дашборду з інформацією по конкретному Pod:
Графіки ALB внизу будуються з логів в Loki.
Тож що зробимо: нову борду, на якій будуть всі WorkerNodes, а в Data Links графіків цієї борди зробимо лінки на перший дашборд.
Тоді буде непогана навігація:
загальна борда по всім WorkerNodes з можливістю перейти на дашборду з більш детальною інформацією по конкретній ноді
на борді по конкретній ноді вже буде інформація по подах на цій ноді, і data links на борду по конкретному поду
Планування дашборди
Давайте поміркуємо, що саме ми б хотіли бачити на новій борді.
Фільтри/змінні:
мати змогу бачити всі WorkerNodes разом, або обрати одну чи кілька окремо
мати змогу бачити ресурси по конкретним Namespaces або Applications (в моєму випадку кожен сервіс має власний неймспейс, тому використаємо їх)
Далі, інформація по нодам:
загальна інформаця по нодам:
кількість нод
кількість подів
кількість ЦПУ
кількість Мем
spot vs on-deman ratio
вартість всіх нод за добу
відсотки від allocatable використано:
cpu – від pods requested
mem – від pods requested
pods allocation
реальне використання ресурсів – графіки по нодам:
CPU та Memory подами
кількість подів – процент від максимума на ноді
створено-видалено нод (by Karpenter)
вартість нод
процент EBS used
network – in/our byes/sec
По Karpenter:
controller_runtime_reconcile_errors_total – загальна кількість помилок
karpenter_provisioner_scheduling_duration_seconds – час створення подів
karpenter_cloudprovider_errors_total – загальна кількість помилок
Looks like a plan?
Поїхали творити.
Створення дашборди
Робимо нову борду, задаємо основні параметри:
Grafana variables
Нам потрібні дві змінні – по нодам і неймпспейсам.
Ноди можемо вибрати з karpenter_nodes_allocatable, неймспейси отримати з karpenter_pods_state.
Створюємо першу змінну – node_name, включаємо можливість вибору All або Multi-value:
Створюємо другу змінну – $namespace.
Щоб вибирати неймспейси тільки з обраних в фільтрі нод – додаємо можливість фільтру по $node_name яку створили вище і використовуємо регулярку “=~” – якщо нод буде обрано кілька:
Переходимо до графіків.
Кількість нод в кластері
Запит – використовуємо фільтр под обраним нодам:
count(sum(karpenter_nodes_allocatable{node_name=~"$node_name"}) by (node_name))
Частина ресурсів зайнята системою то ДемонСетами – вони у karpenter_nodes_allocatable не враховються. Можна перевірити запитом sum(karpenter_nodes_system_overhead{resource_type="cpu"}).
Тому можемо вивести або загальну кількість – karpenter_nodes_allocatable{resource_type="cpu"} + karpenter_nodes_system_overhead{resource_type="cpu"}, або тільки дійсно доступну для наших workloads – karpenter_nodes_allocatable{resource_type="cpu"}.
Так як тут ми хочемо бачити саме загальну кількість – то давайте використаємо суму:
Окрім нод створенних самим Karpenter у нас є окрема “дефолтна” нода, яка створються при створенні кластеру – для всяких controllers. Вона теж Spot (поки що), тож рахуймо і її.
Формула буде такою:
загальна сума нод = всі спот від karpenter + 1 дефолтна / на загальну кількість нод
Сам запит:
sum(count(sum(karpenter_nodes_allocatable{node_name=~"$node_name", nodepool!="", capacity_type="spot"}) by (node_name)) + 1) / count(sum(karpenter_nodes_allocatable{node_name=~"$node_name"}) by (node_name)) * 100
CPU requested – % від загального allocatable
Запит беремо з дефолтної борди Karpenter, трохи підпилюємо під свої фільтри:
Метрика controller_runtime_reconcile_errors_total включає в себе і контролери від VictoriaMetrcis, тож виключаємо їх через {container!~".*victoria.*"}:
По-перше – у AWS є дефолтні метрики білінгу від CloudWatch, але наш проект користується кредитами від AWS і ці метрики пусті.
Тому скористаємось метриками від Karpenter – karpenter_cloudprovider_instance_type_price_estimate.
Щоб відобразити вартість серверверів нам треба вибрати кожен тип інстансу які використовуються і потім порахувати загальну вартість по кожному типу і іх кількості.
Що ми маємо:
дефолта нода: з типом Spot, але створюється не Karpenter – можемо її ігнорувати
ноди, створені Karpenter: можуть бути або spot або on-demand, і можуть бути різних типів (t3.medium. c5.large, etc)
Спочатку нам треба отримати кількість нод по кожному типу:
count(sum(karpenter_nodes_allocatable) by (node_name, instance_type,capacity_type)) by (instance_type, capacity_type)
Отримуємо 4 spot, і один інстанс без лейбли capacity_type – бо це з дефолтної нод-групи:
Можемо його виключити з {capacity_type!=""} – він у нас один, без скейлінгу, можемо не враховувати, бо це тільки для CriticalAddons.
Для кращої картини візьмемо більший проміжок часу, бо там був ще t3.small:
Далі, у нас є метрика karpenter_cloudprovider_instance_type_price_estimate, використовуючи яку нам треба порахувати вартість всіх інстансів по кожному instance_type і capacity_type.
Запит буде виглядати так (дякую ChatGPT):
sum by (instance_type, capacity_type) (
count(sum(karpenter_nodes_allocatable) by (node_name, instance_type, capacity_type)) by (instance_type, capacity_type)
* on(instance_type, capacity_type) group_left
avg(karpenter_cloudprovider_instance_type_price_estimate) by (instance_type, capacity_type)
)
Тут:
“внутрішній” запит sum(karpenter_nodes_allocatable) by (node_name, instance_type, capacity_type): рахується сума всіх CPU, memory тощо для кожної комбінації node_name, instance_type, capacity_type
“зовнішній” count(...) by (instance_type, capacity_type): результат попереднього запиту рахуємо з count, щоб отримати кількість кожної комбінації – отримуємо кількіть WorkerNodes кожного instance_type та capacity_type
другий запит – avg(karpenter_cloudprovider_instance_type_price_estimate) by (instance_type, capacity_type): повертає нам середню ціну по кожному instance_type та capacity_type
використовуючи * on(instance_type, capacity_type): множимо кількість нод с запита номер 2 (count(...)) на результат с запита номер 3 (avg(...)) по співпадаючим комбінаціям метрик instance_type та capacity_type
і самий перший “зовнішній” запит sum by (instance_type, capacity_type) (...): повертає нам суму по кожій комбінації
В результаті маємо такий графік:
Отже, що ми тут маємо:
4 інстанси t3.medium та 2 t3.small
загальна вартість всіх t3.medium на годину виходить 0.074, всіх t3.small – 0.017
Для перевірки порахуємо вручну.
Спочатку по t3.small:
{instance_type="t3.small", capacity_type="spot"}
Виходить 0.008:
І по t3.medium:
{instance_type="t3.medium", capacity_type="spot"}
Виходить 0.018:
Тож:
4 інстанси t3.medium по 0.018 == 0.072 usd/година
2 інстанси t3.small по 0.008 == 0.016 usd/година
Все сходиться.
Залишилось все це зібрати разом, і вивести загальную вартість всіх серверів за 24 години – використаємо avg() і результат помножимо на 24 години:
avg(
sum by (instance_type, capacity_type) (
count(sum(karpenter_nodes_allocatable{capacity_type!=""}) by (node_name, instance_type, capacity_type)) by (instance_type, capacity_type)
* on(instance_type, capacity_type) group_left
avg(karpenter_cloudprovider_instance_type_price_estimate) by (instance_type, capacity_type)
)
) * 24
І в результаті все у нас зараз виглядає так:
Йдемо далі – до графіків.
CPU % use by Node
Тут вже використаємо дефолтні метрики від Node Exporter – node_cpu_seconds_total, але вони мають лейбли instance у вигляді instance="10.0.32.185:9100", а не node_name або node як у метриках від Karpenter (karpenter_pods_state{node="ip-10-0-46-221.ec2.internal"}).
Тож щоб їх застосувати node_cpu_seconds_total з нашою змінною $node_name – додамо нову змінну node_ip, яку будемо формувати з метрики kube_pod_info з фільтром по лейблі node, де використовуємо нашу стару змінну node_name – щоб вибирати поди тільки з обраних в фільтрах нод.
Додаємо нову змінну, поки для перевірки не вимикаємо “Show on dashboard“:
І тепер можемо створити графік с запитом:
100 * avg(1 - rate(node_cpu_seconds_total{instance=~"$node_ip:9100", mode="idle"}[5m])) by (instance)
Але в такому випадку instance нам поверне результати як “10.0.38.127:9100” – а ми всюди використовуємо “ip-10-0-38-127.ec2.internal“. До того ж ми не зможемо додати data links, бо друга панель використовує формат ip-10-0-38-127.ec2.internal.
Тож ми можемо використати label_replace(), і переписати запит так:
перший – метрика, над якою будемо виконувати трансформацію (результат rate(node_cpu_seconds_total))
другий – лейбла, над якою ми будемо виконувати трансформацію – instance
третій – новий формат value для лейбли – “ip-${1}-${2}-${3}-${4}.ec2.internal“
четвертий – ім’я лейбли, з якої ми будемо тримувати дані за допомогою regex
І останнім описуємо сам regex “(.*)\\.(.*)\\.(.*)\\.(.*):9100“, за яким треба отрмати кожен октет з IP 10.0.38.127, а потім кожен результат відповідно записати у ${1}-${2}-${3}-${4}.
Тут нам треба виконати запит між двома метриками – karpenter_pods_state та karpenter_nodes_allocatable:
(
sum by (node) (kube_pod_info{node=~"$node_name", created_by_kind!="Job"})
/
sum by (node) (kube_node_status_allocatable{node=~"$node_name", resource="pods"})
) * 100
Або ми можемо виключити нашу дефолтну ноду “ip-10-0-41-2.ec2.internal” і відобразити тільки ноди самого Карпентеру додавши вибірку по karpenter_nodes_allocatable{capacity_type!=""} – бо нам тут більше цікаво наскільки зайняті ноди, які створено самим Karpenter під наші аплікейшени.
Але для цього нам знабиться метрика karpenter_nodes_allocatable, в якій ми можемо перевірити наявність лейбли capacity_type – capacity_type!="".
Проте karpenter_nodes_allocatable має лейблу node_name а не node як в попередніх двух, тому ми знову можемо додати label_replace, і зробити такий запит:
(
sum by (node) (kube_pod_info{node=~"$node_name", created_by_kind!="Job"})
/
on(node) group_left
sum by (node) (kube_node_status_allocatable{node=~"$node_name", resource="pods"})
) * 100
and on(node)
label_replace(karpenter_nodes_allocatable{capacity_type!=""}, "node", "$1", "node_name", "(.*)")
Тут в and on(node) ми використовуємо лейблу node в результатах запиту зліва (sum by()) і в результаті справа, щоб зі списку нод в karpenter_nodes_allocatable{capacity_type!=""} (тобто всі ноди, окрім нашої “дефолтної”) вибрати тільки ті, які є в результатх першого запиту:
EBS use % by Node
Тут вже простіше:
sum(kubelet_volume_stats_used_bytes{instance=~"$node_name", namespace=~"$namespace"}) by (instance)
/
sum(kubelet_volume_stats_capacity_bytes{instance=~"$node_name", namespace=~"$namespace"}) by (instance)
* 100
Nodes created/terminated by Karpenter
Для відображення активності автоскейлінгу додамо графік з двома запитами:
increase(karpenter_nodes_created[1h])
Та:
- increase(karpenter_nodes_terminated[1h])
Тут в функції increase() перевіряємо наскільки змінилося значення за годину:
А щоб позбутися цих “сходинок” – можемо додатково загорнути результат у функцію avg_over_time():
Grafana dashboard: фінальний результат
І все разом у нас тепер виглядає так:
Додавання Data links
Останім кроком буде додавання Data Links на графіки: потрібно додати лінку на іншу дашборду, по конкретній ноді.
Ця борда має такий URL: https://monitoring.ops.example.co/d/kube-node-overview/kubernetes-node-overview?var-node_name=ip-10-0-41-2.ec2.internal
Де в var-node_name=ip-10-0-41-2.ec2.internal задається ім’я ноди, по якій треба вивести дані:
Тож відкриваємо графік, знаходимо Data links:
Задаємо ім’я та URL – список всіх полів можна отримати по Ctrl+Space:
__field візьме дані з labels.node з результату запиту в панелі:
І сформує посилання у вигляді “https://monitoring.ops.example.co/d/kube-node-overview/kubernetes-node-overview?var-node_name=ip-10-0-38-110.ec2.internal“.
Ще з дуже цікавих новинок останнього re:Invent – це EKS Pod Identities: нова можливість керувати доступами подів до ресурсів AWS.
The current state: IAM Roles for Service Accounts
До цього ми використовували модель IAM Roles for Service Accounts, IRSA, де для того, щоб якомусь поду дати доступ до, наприклад, S3, ми створювали IAM Role з відповідною IAM Policy, налаштовували її Trust Policy – щоб дозволити виконувати AssumeRole тільки з відповідвідного кластеру, потім створювали Kubernetes ServiceAccount, в annotations якого вказували ARN цієї ролі.
За такою схемою ми мали декілька “error prone” моментів:
найбільш розповсюджена проблема, з якою і я стикався прям ну дуже багато раз – помилки в Trust Policy, де треба було вказувати OIDC кластеру
помилки в самому ServiceAccount, де можна було помилитись в ARN ролі
Проте тепер EKS Pod Identities дозволяє нам один раз створити IAM Role, ніяк її не обмежувати конкретним кластером, і підключати цю роль до подів (знову-таки – через ServiceAccount) прямо з AWS CLI, AWS Console чи через AWS API (Terraform, CDK, etc).
Як це виглядає:
в EKS додаємо новий контроллер – Amazon EKS Pod Identity Agent add-on
створюємо IAM Role, в Trust Policy якої тепер використовуємо Principal: pods.eks.amazonaws.com
і з AWS CLI, AWS Console чи через AWS API підключаємо цю роль напряму до потрібного ServiceAccount
Го тестити!
Створення IAM Role
Переходимо в IAM, створюємо роль.
В Trusted entity type вибираємо EKS і новий тип – EKS – Pod Identity:
В Permissions візьмемо вже існуючу політику на S3ReadOnly:
Задаємо ім’я ролі, і як раз тут і бачимо нову Trust Policy:
І давайте порівняємо її з Trust Policy для IRSA ролі:
Набагато простіше, а значить – менше варіантів для помилок, і взагалі простіше менеджити. До того ж, ми більше не зав’язані на Cluster OIDC Provider.
До речі, з EKS Pod Identities ми можемо використовувати і role session tags.
Окей, йдемо далі.
Amazon EKS Pod Identity Agent add-on
Переходимо до нашого кластеру, встановлюємо новий компонент – Amazon EKS Pod Identity Agent add-on:
Чекаємо хвилину – готово:
І поди цього контролера:
$ kk -n kube-system get pod | grep pod
eks-pod-identity-agent-d7448 1/1 Running 0 91s
eks-pod-identity-agent-m46px 1/1 Running 0 91s
eks-pod-identity-agent-nd2xn 1/1 Running 0 91s
Підключення IAM Role до ServiceAccount
Переходимо в Access, і клікаємо Create Pod Identity Association:
Вибираємо роль, яку створили вище.
Далі задаємо ім’я неймспейсу – або вибираємо зі списку існуючих, або вказуємо нове.
Аналогічно з ім’ям ServiceAccount – можна задати вже створенний SA, можно задати нове ім’я:
$ kubectl apply -f iam-sa.yaml
namespace/ops-iam-test-ns created
serviceaccount/ops-iam-test-sa created
pod/ops-iam-test-pod created
І пробуємо доступ:
$ kk -n ops-iam-test-ns exec -ti ops-iam-test-pod -- bash
bash-4.2# aws s3 ls
2023-02-01 11:29:34 amplify-staging-112927-deployment
2023-02-02 15:40:56 amplify-dev-174045-deployment
...
Але якщо ми спробуємо іншу операцію, на яку ми не підключали політику, наприклад – EKS, то отримаємо 403:
bash-4.2# aws eks list-clusters
An error occurred (AccessDeniedException) when calling the ListClusters operation: User: arn:aws:sts::492***148:assumed-role/EKS-Pod-Identities-test-TO-DEL/eks-atlas-eks--ops-iam-te-cc662c4d-6c87-44b0-99ab-58c1dd6aa60f is not authorized to perform: eks:ListClusters on resource: arn:aws:eks:us-east-1:492***148:cluster/*
Проблеми?
Наразі я бачу одну потенційну не проблему, але питання, яке варто мати на увазі: якщо раніьше ми налаштовували доступ на рівні сервісу, то з EKS Pod Identities це робиться на рівні управління кластером.
Тобо: в мене є сервіс, Backend API. В нього є власний репозиторій, в якому є каталог terrafrom, в якому створюються необхідні IAM-ролі.
Далі, є каталог helm, в якому маємо маніфест з ServiceAccount, в якому в анотаціях через змінні передається ARN цієї IAM ролі.
І на цьому все – мені (точніше – CI/CD пайплайну, який виконує деплой) потрібен доступ тільки до IAM, потрібен доступ в EKS на створення Ingress, Deployment та ServiceAccount.
Але тепер треба буде думати як давати доступ ще й до EKS на рівні AWS, бо треба буде виконувати додаткову операцію в AWS API на Create Pod Identity Assosiaction.
Тобто тепер ми можемо створювати графіки та/або алерти не тільки з дефолтних метрик самого CloudWatch – але й за допомогою конекторів для CloudWatch підключити збір метрик з Amazon Managed Service for Prometheus, звичайного Prometheus, Amazon OpenSearch Service, Amazon RDS для MySQL та PostgreSQL, CSV файлів з S3 бакетів і навіть з Microsoft Azure Monitor.
Виглядає це прям дуже круто, бо тепер можна буду переосмислили взагалі всю свою концепцію побудови моніторингу и observability на проекті.
Підключення метрик з vanilla Prometheus
Є Prometheus на голому EC2 інстансі, на якому відкрито порт 9090 – спочатку спробуємо тут, потім глянемо на VictoriaMetrcis в EKS.
Переходимо в CloudWatch Metrics, тепер маємо нову вкладку Multi source query:
Вибираємо Prometheus:
Задаємо параметри.
Поля логін-пароль обов’язкові, тож навіть якщо Prometheues не потребує аутентифікації – задаємо тут якісь значення.
Нижче можна налаштувати параметри мережі. Наприклад, якщо Prometheus доступний тільки всередені VPC – то тут можемо вибрати VPC і сабнети:
Клілкаємо Create data source – в CloudWatch запустить створення CloudFormation стеку, в якому створить Lambda-функції, які власне і будуть збирати дані з нашого дата-сорсу:
Повертаємось до CloudWatch, де тепер маємо новий дата-сорс:
Отже, поговоримо про дуже гучний запуск Amazon Q – нової системи від AWS, яка має допомогти нам, інженерам і не тільки, в роботі.
Сам Amazon його називає “AI-powered assistant”, по факту для нас, як інженерів, це просто chatbot, з яким ми можемо поговорити, щоб отримати допомогу у розв’язанні якихось проблем або для отримання рекомендації для налаштування сервісу. Для бізнесу ж він може мабуть багато іншого, але нам він цікавий, як асистент по роботі з AWS.
Під капотом Amazon Q використовує Amazon Bedrock, тож щоб краще розуміти що таке Q – давайте глянемо і на Bedrock.
Amazon Bedrock
Bedrock запустили у квітні 2023, але чомусь він не привернув до себе такої уваги, хоча сервіс дуже цікавий, і я постараюся написати про нього окремо.
Отже, AWS Bedrock – це managed сервіс від AWS, який дозволяє будувати ваші АІ-powered сервіси, використовуючи Foundation Models (FM) від Amazon, Мета, Amazon, Stability AI, та інших:
Для роботи з FL Bedrock надає єдиний API, і вам не потрібно будувати ніякої інфраструктури для запуску моделей.
Крім того, ви можете розширяти базу знань Bedrock за рахунок власних баз знань (“Knowledge base“). При цьому ваші дані не будуть об’єднані з самим FM, тобто повністю зберігається всяка privacy (включаючи підтримку стандартів GDPR, HIPAA).
Тож Amazon Q – це система поверх Bedrock (я поки не знайшов інформації, яка саме модель використовується, але, мабуть, Titan – бо це система від самого Amazon).
По факту це чат-бот, з яким ми можемо поговорити, і який вже нам доступний в AWS Console справа, де ми звикли бачити розділ допомоги:
Хоча місцями Q не може відповісти навіть на прості питання 🙂
При цьому і сам Q, і Bedrock заточені під приватність даних, тому начебто можна спокійно (нєт) підключати їх до корпоративних даних – наприклад, до Git-репозиторію, або Atlassian Confluence, і тоді Q при формуванні відповіді буде використовувати дані з цих джерел для формування відповідей.
Чому “нєт?” Бо поки система в Preview, і про неї вже пишуть, що:
Knowledge baseQ is “experiencing severe hallucinations and leaking confidential data,”
в сервісах Amazon – наприклад, CodeWhisperer (аналог GitHub Copilot?)
Amazon Q vs ChatGPT
Мабуть, чи найперше питання, яке приходить в голову.
Знову-таки, чисто моє IMHO:
Amazon Q – це про бізнес: якщо ChatGPT це так би мовити “AI-чатбот загального призначення”, то Q може тісно інтегруватися з вашим бізнесом – вашими даними, користувачами тощо
Amazon Q – це про безпеку даних: нам обіцяють дуже потужні інструменти по обмеженню доступів: наприклад, якщо до Q буде підключена Confluence, то при формуванні відповіді на запит юзера будуть перевірятись його права в Confluence, і не будуть використані дані, до яких він там не має доступу (цікаво, як це реалізовано і як воно буде працювати, але поки просто маємо на увазі)
Amazon Q – це про інтеграцію:
модель, яка використовується для відповідей навчалася на даних самого AWS, яких за 17 років існування набралось багато – в тому числі мабуть і якісь дані, котрих зазвичай нема у відкритому доступі, тому по ідеї – Q в деяких моментах може давати більш точні відповіді (знову-таки – може не відразу, поки воно ще в Preview)
Q інтегрований з самими сервісами AWS, і ви можете використовувати його прямо в QuickSighe, або, як вже казалось, в самій AWS Console
(оці три пункта звучать так, наче Amazon мені заплатили за цей пост :-D)
Amazon Q pricing
Як завжди у AWS – “всьо сложно” 🙂
По-перше, поки система в Preview – більша частина можливостей безкоштовні.
По-друге – доступні будуть два різних плана: Business і Builder.
Якщо коротко, то Business – це більше про якісь маркетингові штуки на кшталт чат-бота для допомоги співробітникам у вирішення якихось питань, або такий собі “internal Google” – але з доступом до внутрішніх баз даних.
А от Builder, судячи з документації – це як раз те, що буде цікаво нам, як інженерам, бо саме в цьому плані буде доступ до “over 17 years’ worth of AWS knowledge and experience building in the cloud, including best practices, well-architected patterns“.
Спробуємо потраблшутити з Amazon Q – створимо EC2, зламаємо нетворкінг, і спитаємо Q.
Створюємо інстанс, дозволяємо SSH:
Перевіряємо – доступ є:
$ ssh 16.170.217.131
The authenticity of host '16.170.217.131 (16.170.217.131)' can't be established.
...
Далі, редагуємо SecurityGroup – видаляємо доступ SSH:
І питаємо Amazon Q:
На що він відповідає, що може спробувати проаналізувати проблему з AWS VPC Reachability Analyzer. Спробуємо – переходимо за посиланням, і:
Ну, я все ж очікував, що Q прям зможе проаналузвати конфіг нетворкінгу EC2, і побачить, що там проблема в самій SecurityGroup. Але це я багато, мабуть, хочу)
Втім вже те, що він так інтегрується з системами типу VPC Reachability Analyzer – непогано, і далі, сподіваюсь, буде ще краще.
Проходимо аутентифікацію – клікаємо Use free with AWS Builder ID:
Відкриється вікно в браузері, там підтверджуємо код, і вказуємо свою пошту – можна будь-яку, не обов’язкового ту, яка використовується в AWS акаунті:
На пошту буде відправлено листа з кодом – підтверджуємо, і задаємо собі пароль:
Дозволяємо доступ:
І тепер маємо підключений Amazon Q в нашому VSCode:
Задаємо питання – і він просканує відкритий у редакторі код, і на основі нього видасть відповідь:
Але питання розуміє не завжди коректно: в цілому він має підтримувати контекст розмови і враховувати відкритий код, але ось маємо відкритий код Terraform і пов’язане питання перед цим, а він дає відповідь про AWS Console:
Але якщо трохи перебудувати питання – то він повернув більш валідну відповідь:
На доповіді на самому re:Invent показували демо роботи з IDE, і там був приклад, як Q допомагає створювати код для AWS CDK та Python, і там виглядає прям дуже круто, бо я влітку цього року намучився з ChatGPT, який постійно видавав приклади для старої версії CDK або старих версій бібліотек (див. AWS: CDK – створення EKS з Python та загальні враження від CDK).
Amazon Q Application
Ми також можемо створити Application в AWS Console, і там використати власні дата-сорси, на основі яких Q буде формувати відповіді.
Плюс, там жеж можемо створити і веб-інтерфейс для юзерів:
Створюємо:
Далі налаштовується Retriever – яким чином Q буде отримувати дані з дата-сорса:
І останнім налаштовується вже сам дата-сорс через вибір конекторів – а їх тут дуже багато:
Візьмемо Web crawler:
Тут вже налаштовується і аутентифікація – і саме тут вочевидь будуть перевірятись user permissions – те, про що я вписав на початку, що Amazon Q не дасть інформації юзеру, якщо він не має до неї доступу в самому дата-сорсі:
І решта налаштувань – там і VPC, і IAM, параметри синхронізації тощо:
Робимо синхронізацію з нашим дата-сорсом:
І далі вже можемо відкрити веб-інтерфейс, і початитись з ботом:
А якщо клікнути Deploy web experience – то треба буде налаштовувати SAML:
І воно наче і має працювати без деплою, в Web preview режимі, але в мене воно просто зависало на запиті:
Можливо тому, що не закінчена синхронізація з дата-сорсом, але процес синхронізації з RTFM затягнувся на кілька годин, хоча тут всього-то близько 1500 постів, плюс всякі медіафайли типу скріншотів. Мабуть, вже не дочекаюсь її завершення, але якщо після закінчення синхронізації запрацює – то цю частину тут оновлю.
Висновки та враження від Amazon Q
Я не сказав би, що сервіс прям “producation ready” – але він дійсно ще в Preview, а тому багато чого буде допилюватись/фікситись/апгрейдитись, і тут все ще є над чим попрацювати, бо наразі вона виглядає трохи сирою – Amazon явно поспішали випустити її хоча б в Preview, бо вся ця “гонка Generative AI” і вот ето вот всьо.
Але в цілому система виглядає доволі перспективною, особливо, як вона навчиться толком допомагати траблшутити якісь проблеми в AWS.
Ну і головне – її дійсно можна буде використовувати різними бізнесами, інтегруючи зі своїми системами та базами даних, і не переживати за всякий privacy complience – а з ChartGPT це зараз дуже велика проблема.
Що маємо: є у нас Kubernetes cluster, на якому скейлінгом WorkerNodes займається Karpenter, який для NodePool має параметр disruption.consolidationPolicy=WhenUnderutilized, тобто він буде намагитись “ущільніти” розміщеня подів на нодах так, щоб максимально ефективно використати ресурси CPU та Memory.
В цілому все працює, але це призводить до того, що досить часто перестворюються WorkerNodes, а це викликає “переселення” наших Pods на інші ноди.
Тож задача зараз зробити так, щоб скейлінг і процес consolidation не викликав перебоїв в роботі наших сервісів.
Загалом це тема не стільки про сам Karpenter, скільки про забезпечення стабільності роботи подів в Kubernetes загалом, але зараз я детально зайнявся цим питанням саме через Karpenter, тому будемо трохи говорити і про нього.
Karpenter Disruption Flow
Щоб краще розуміти, що відбувається з нашими подами, давайте коротко глянемо як Karpenter виводить з пулу WorkerNode. Див. Termination Controller.
Після того, як Karpenter виявив, що є ноди, які треба термінейтити, він:
ставить на такую ноду taintkarpenter.sh/disruption:NoSchedule щоб Kubernetes не створював нових подів на цій ноді
при необхідності створює нову ноду, на яку буде переносити поди з ноди, яка буде виведена з роботи (або викорстає ноду яка вже є, якщо вона може прийняти додаткові поди відповідно до їх requests)
після того, як з ноди всі поди окрім DaemonSets видалені, Karpenter видаляє відповідний NodeClaim
видаляє finalizer ноди, що дозволяє Kubernetes виконати видалення цієї ноди
Kubernetes Pod Eviction Flow
І коротко процес того, як сам Kubernetes виконує “виселення” поду:
API Server отримує Eviction request і виконує перевірку – чи можна цей под виселити (наприклад – чи не порушить його видалення обмежень якогось PodDisruptionBudget)
відмічає ресурс цього поду на видалення
kubelet починає процес gracefully shut down – тобто відправляє сигнал SIGTERM
Kubernetes видаляє IP цього поду зі списку ендпоінтів
якщо под не закінчив роботи на протязі заданого – то kubelet відправляє сигнал SIGKILL, щоб вбити процес негайно
kubelet відправляє сигнал API Server, що под можна видаляти зі списку об’єктів
Тож що ми можемо зробити з подами, щоб наш сервіс працював незалежно від роботи Karpenter і взагалі стабільно і “бєз єдіного разрива” (с) ?
мати мінімум по 2 поди на критичних сервісах
мати Pod Topology Spread Constraints, щоб Pods розміщались на різних WorkerNodes – тоді якщо вбивається одна нода з одним подом – інший под на іншій ноді залишиться живим
мати PodDisruptionBudget, щоб мінімум 1 под був завжди живий – це не дасть Karpenter виконати evict всіх подів відразу, бо він слідкує за додтриманням PDB
і щоб гарантовано не дати виконати Pod Eviction – можемо задати поду анотацію karpenter.sh/do-not-disrupt – тоді Karpenter буде ігнорувати таки поди (і, відповідно, ноди, на яких буде запущено такий под)
Kubernetes Deployment replicas
Саме просте і очевидне рішення – це мати як мінімум 2 одночасно працюючих поди.
Хоча це не гарантує, що Kubernetes не виконає їхній eviction одночасно, але це мінімальна умова для подальших дій.
Тож або виконуємо руками kubectl scale deployment --replicas=2, або оновлюємо поле replicas в Deployment/StatefulSets/ReplicaSet (див. Workload Resources):
Більш детально описував у Pod Topology Spread Constraints, але якщо коротко, то ми можемо задати правила розміщення Kubernetes Pod так, щоб вони знаходились на різних WorkerNodes. Таким чином коли Karpenter захоче вивести одну ноду з роботи – то у нас залишиться под на іншій ноді.
Проте ніхто не завадить Karpenter-у виконати drain обох нод відразу, тож і це не є 100% гарантією, але це друга умова для забезпечення стабільності роботи нашого сервісу.
Крім того, з Pod Topology Spread Constraints, ми можемо задати розміщення подів у різних Availabilty Zones, що є фактичного must have опцією при побудові High-Availabiltiy архітектури.
Тож додаємо до нашого деплойменту topologySpreadConstraints:
За допомогою PodDisruptionBudget ми можемо задати правило на мінімальну кількість доступних або максимальну кількість недоступних подів. Значення може бути як у виді числа, так і у виді відсотка від загальної кількості подів в replicas для Deployment/StatefulSets/ReplicaSet.
У випадку з Deployment в якому маємо два поди і який має topologySpreadConstraints по різним WorkerNodes це дасть гарантію того, що Karpenter не виконає Node Drain двох WorkerNdoes одночасно. Натомість він “переселить” спочатку один под, вб’є його ноду, а потім повторить процес для іншої ноди.
$ kk get pdb
NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS AGE
nginx-demo-pdb 50% N/A 1 21s
Анотація karpenter.sh/do-not-disrupt
Окрім налаштувань на стороні Kubernetes, ми можемо явно задати заборону на видаленя поду самому Karpenter через додавання анотації karpenter.sh/do-not-disrupt (раніше, до Beta, це були анотації karpenter.sh/do-not-evict та karpenter.sh/do-not-consolidate).
Це може знадобитись наприклад для подів, які мають бути запущені в одному екземплярі (як VictoriaMetircs VMSingle instance), і які небажано зупиняти.
Для цього в template цього поду додаємо annotation:
Але в процесі міграції в Kubernetes у нас з’явились Application Load Balancers, які вміють писати логи тільки в S3, і нам треба навчитись збирати логи і звідти.
Начебто і нічого складного, але по-перше є деякі нюанси, особливо з IAM та VPC, по-друге – я ніде не знаходив такої документації, тож довелося писати її самому.
В принципі, тут все майже однаково зі збором логів з CloudWatch Logs:
$ kk apply -f ingress-svc-deploy.yaml
deployment.apps/nginx-demo-deployment created
service/nginx-demo-service created
ingress.networking.k8s.io/example-ingress created
Перевіряємо:
$ kk get ingress
NAME CLASS HOSTS ADDRESS PORTS AGE
example-ingress alb example.com k8s-default-examplei-b48cab7a95-1519459738.us-east-1.elb.amazonaws.com 80 22s
Перевіряємо файли в корзині:
$ aws s3 ls s3://eks-alb-logs-test/AWSLogs/492***148/
2023-11-15 14:51:43 106 ELBAccessLogTestFile
Спочатку подумав, що Promtail за 3 секунди не встигає отримати лог з S3, і збільшив таймаут функції:
Але виявилось, що причина в іншому: Lambda Promtail запущена в VPC, і до S3 ходить через VPC Ednpoint (див. Terraform: створення EKS, частина 1 – VPC, Subnets та Endpoints), але в SecurityGtoup, яка підключена до цієї функції, Outbound доступ був дозволений тільки до адрес в приватних сабнетах цієї VPC, бо тоді налаштовувалась тільки передача логів в Grafana Loki:
А нам потрібно надати доступ до VPC енпоінту S3, і якби це був тип Interface Ednpoint – то в SecurityGroup лямбди можна було б задати SecurityGroup ID цього Interface Endpoint-у:
Але у Gateway такої опції нема.
Проте ми можемо використати Prefix List ID – знаходимо його:
Кілька корисних порад по використанню Liveness та Readiness Probes в Kubernetes – різниця між ними, та як правильно налаштовувати ці перевірки.
Якщо зовсім коротко, то:
livenessProbe: використовується Kubernetes, щоб знати, коли потрібно виконати restart поду
readinessProbe: використовується Kubernetes, щоб знати, коли контейнер готовий приймати трафік, тобто – коли відповідний Kubernetes Service може додавати цей под до своїх роутів
startupProbe: використовується Kubernetes, щоб знати, коли контейнер запустився і готовий до виконання перевірок з livenessProbe та readinessProbe
livenessProbe та readinessProbe почнуть виконуватись тільки після успішної перевірки startupProbe
Отже, livenessProbe використовується для визначення чи живий процес в поді, тоді як readinessProbe – чи готовий сервіс в поді приймати трафік, а startupProbe – коли починати виконувати livenessProbe та readinessProbe.
Основа цього посту три матеріали, які я колись зберіг, і досить користуюсь:
livenessProbe потрібна коли, наприклад, процесс застряг в deadlock і не може виконувати свої задачі. Інший приклад – якщо процес увійшов в infinite loop і використовує 100% CPU, при цьому не маючи змоги обробляти запити від клієнтів, бо він все ще підключений до лоад-балансінгу в мережі Kubernetes.
У випадку, коли у вас є readinessProbe, але немає livenessProbe – то такий под буде відключений від трафіку, але залишиться в статусі Running, і буде продовжувати займати ресурси CPU/Memory.
Запити до livenessProbe виконуються процесом kubelet на тій самій WorkerNode, де працює конейтенер, і після рестарту под буде створено на тій самій WorkerNode
Процес в конейнері має зупинятись з кодом помилки
livenessProbeне має бути інструментом реагування на помилки в роботі сервісу: натомість, процес має завершити роботу з кодом помилки, що призведе до зупинки контейнеру/поду, і створення нового.
livenessProbe використовується тільки для перевірки статусу самого процесу в контейнері.
Розділяйте Liveness та Readiness Probes
Розповсюджена практика використання одного ендпоінту для livenessProbe та readinessProbe, але для livenessProbe задавати вище значення failureThreshold, тобто – раніше відключати від трафіку і клієнтів, і якщо “діло зовсім погано” – то виконувати рестарт.
Але ці Probes мають різні призначення, а тому хоча й допустимо використовувати один ендпоінт, але краще мати різні перевірки. Крім того, якщо обидві перевірки будуть сфейлені, то Kubernetes виконає рестарт поду та його відключення від мережі одночасно, що може призвести до 502 помилок у клієнтів.
Уникайте залежностей
Поди не мають посилатись один на одного або на зовнішні сервіси під час виконання livenessProbe: ваш контейнер не має виконувати перевірки доступності серверу баз даних, бо якщо сервер БД впав, то рестарт вашого поду не допоможе вирішити цю проблему.
Замість цього, ви можете створити окремий ендпоінт для системи моніторингу, і виконувати такі перевірки там – задля алертів і дашбордів в Grafana.
Крім того, процес в контейнері не має падати, якщо не може отримати доступ до сервісу, від якого він залежить. Натомість, він має виконувати retry конекту, бо Kubernetes очікує, що поди можуть бути запущені в будь-якому порядку.
Корректна обробка SIGTERM
Процес в контейнері має коректно оброблювати сигнал SIGTERM – саме він відправляється від kubelet до контейнерів, коли їх треба перезапустити. Якщо на SIGTERM відповіді не було (бо процес “вісить”) – буде виконано SIGKILL.
readinessProbe потрібні, щоб не надсилати запити до подів, які ще запустились.
Наприклад, якщо процес старту вашого поду займає 2 хвилини (якийсь bootsrap процесу, особливо якщо це JVM, або завантаження якогось cache в пам’ять), а ви не маєте readinessProbe – то Kubernetes почне відправляти запроси, як тільки под перейде в статус Running, і вони будуть фейлитись.
Перевірка залежностей
На відміну від livenessProbe, в readinessProbe може бути сенс в перевірці доступності сервісів, від яких залежить под, бо у випадку коли сервіс не може виконати запит від клієнта, бо в нього немає конекта до бази даних – то пускати трафік на цей под не треба.
Втім пам’ятайте, що readinessProbe виконується постійно (кожні 15 секунд по дефолту), і на кожну таку перевірку буде виконуватись окремий запрос до бази даних.
Але в цілому це залежить від самого вашого застосунку. Наприклад, якщо ви можете віддавати відповіді з якогось локального кешу, то под може продовжувати роботу, а на виконання запитів з Write-операціями – повертати помилку 503.
startupProbe
Так як startupProbe виконується тільки на старті поду, то як раз тут можна виконати перевірки підключень до зовнішніх сервісів або доступу до кешу.
Наприклад, може бути корисним перевірити підключення до бази даних коли ви деплоїте новую версію Helm-чарту і маєте Kubernetes Deployment з Rolling Update, але в новій версії маєте помилку в URL або паролі до серверу бази даних.
Також startupProbe може бути корисна, щоб не збільшувати параметр initialDelaySeconds для livenessProbe та readinessProbe , а натомість вікласти їх запуск допоки не завершиться перевірка startupProbe, бо якщо livenessProbe при старті контейнера не встигне виконатись – то Kubernetes виконає рестарт поду, хоча він ще “прогрівається”.
Типи перевірок
В кожній Probe можемо використовувати перевірки по:
exec: виконати команду всередені контейнеру
httpGet: виконати запит HTTP GET
tcpSocket: відкрити TCP connect на порт
grpc: виконати запит gRPC на TCP порт
Параметри для Probes
Всі Probes мають параметри, якві дозволяють точно налаштувати час виконання перевірок:
initialDelaySeconds: затримка між стартом контейнеру та початком виконання перевірок
periodSeconds: як часто після initialDelaySeconds робити запити на перевірку стану
timeoutSeconds: як довго чекати відповіді на запит
failureThreshold: скільки має бути failed відповідей, щоб вважати перевірку не пройденою (або – скільки раз повторювати перевірку, перш ніж виконати рестарт поду або відключення від мережі)
successThreshold: аналогічно, але щоб вважати перевірку пройденою
В пості Prometheus: запуск Pushgateway у Kubernetes з Helm та Terraform писав про те, як для Prometheus додати Pushgateway, який дозволяє використовувати Push-модель замість Pull, тобто – експортер може відправити метрики прямо в базу замість того, щоб чекати, коли до нього прийде сам Prometheus або VMAgent.
У VictoriaMetrics з цим набагато простіше, бо ніякого Pushgateway не потрібно – VictoriaMetrics “з коробки” вміє приймати метрики, і це одна з тих чудовіих фіч, чому я вибрав VictoriaMetrics, і досі їй радуюсь.
Все є, і ніяких тобі додаткових дій з Pushgateway!
Push метрик з експортера
Для тесту напишемо простий експортер:
#!/usr/bin/env python3
from time import sleep
from prometheus_client import Counter, CollectorRegistry, push_to_gateway
# local metrics "storage"
registry = CollectorRegistry()
# register a metric
counter = Counter('counter_metric', 'Example metric', ["label_name"], registry=registry)
while True:
# increase metric's value to +1
counter.labels(label_name="label_value").inc()
# push to the VictoriaMetrics
push_to_gateway('localhost:8428/api/v1/import/prometheus', job='test_job', registry=registry)
# make sure this code is working :-)
print("pushed")
# wait before next run
sleep(15)
і відправляємо в localhost:8428/api/v1/import/prometheus – це я перевіряв на тестовому інтансі, де VictoriaMetrics запущена з Docker Compose, тому тут localhost
Перевіряємо в самій VictoriaMetrics:
prometheus_client, функції та grouping key
Трохи про те, як працює сам prometheus_client:
push_to_gateway() в registry будуть перезаписані всі метрики з однаковим набором labels (grouping key)
pushadd_to_gateway() перезапише метрики з однаковим іменем та grouping key
delete_from_gateway() видалить метрики з заданим job та grouping key
Починаючи з 0.32, API-ресурси Provisioner, AWSNodeTemplate та Machine будуть deprecated, а з версії 0.33 їх приберуть взагалі.
Замість них додані:
Provisioner => NodePool
AWSNodeTemplate => EC2NodeClass
Machine => NodeClaim
v1alpha5/Provisioner => v1beta1/NodePool
NodePool являється наступником Provisioner, і має параметри для:
налаштування запуску Pods та WorkerNodes – requirements подів до нод, taints, labels
налаштування того, як Karpenter буде розміщувати поди на нодах, та як буде виконувати deprovisioning зайвих нод
“Під капотом” NodePool буде створювати ресурси v1beta1/NodeClaims (які прийшли за зміну v1alpha5/Machine), і взагалі тут ідея приблизно як з Deployments та Pods: в Deployment (NodePool) ми описуємо template того, як буде створюватись Pod (NodeClaims). А v1beta1/NodeClaims в свою чергу являється наступником ресурсу v1alpha5/Machine.
Також була додана новая секція disruption – сюди були перенесені всі параметри, які відносяться до видалення зайвих нод та управління подами, див. Disruption.
v1alpha5/AWSNodeTemplate => v1beta1/EC2NodeClass
EC2NodeClass являється наступником AWSNodeTemplate, і має параметри для:
Лейбли karpenter.sh/do-not-evict та karpenter.sh/do-not-consolidate були об’єднані у нову лейблу karpenter.sh/do-not-disrupt, яку можна використовувати як для Pods, щоб заборонити Karpenetr виконувати Pod eviction, так і для WorkerNodes, щоб заборонити видалення цієї ноди.
Міграція 0.30.0 на v0.32.1
Далі я описую все досить детально і розглядаю різні варіанти, тож може скластися враження, що процес апгрейду досить геморний, бо буде трохи багато тексту, але насправді ні – все досить просто.
який приймає параметр version, який ми передаємо зі змінної var.helm_release_versions.karpenter
і маємо два ресурси kubectl_manifest – для karpenter_provisioner і karpenter_node_template
Процес міграції включає в себе:
оновлення IAM Role, яка використовується подами контролера Karpenter для управління EC2 в AWS:
замінити тег karpenter.sh/provisioner-name на karpenter.sh/nodepool (див. chore: Release v0.32.0) – стосується тільки ролі, яка була створена з Cloudformation, бо в Terraform модулі використовується інший Condition
додати IAM Policy iam:CreateInstanceProfile, iam:AddRoleToInstanceProfile, iam:RemoveRoleFromInstanceProfile, iam:DeleteInstanceProfile та iam:GetInstanceProfile
додавання нових CRD v1beta1, після чого Karpenter сам оновить ресурси Machine на NodeClaim
для міграції AWSNodeTemplate => EC2NodeClass та Provisioner => NodePool можна використати утіліту karpenter-convert
додати taint karpenter.sh/legacy=true:NoSchedule до існуючого Provisioner
Karpneter помітить всі ноди цього Provisioner як drifted
Karpenter запустить нові ЕС2, використовуючи новий NodePool
видалення нод:
створити NodePool аналогічний існуючому Provisioner
видалити існуючий Provisioner командою kubectl delete provisioner <provisioner-name> --cascade=foreground, в результаті чого Karpenter видалить всі його ноди виконавши node drain для всіх відразу, і поди, які перейдуть в стан Pending, запустить на нодах, які були створені з NodePool
ручна заміна:
створити NodePool аналогічний існуючому Provisioner
додати taint karpenter.sh/legacy=true:NoSchedule до старого Provisioner
по черзі вручну видалити всі його WorkerNopes з kubectl delete node
також змінились values – блок aws в settings тепер deprecated, див. values.yaml
Всі зміни начебто backward compatible (перевірив – відкатував версії), тобто можемо спокійно оновлювати існуючі ресурси один за одним – поламатись нічого не повинно.
Тож, що зробимо:
оновимо модуль Terraform
додамо CRD
оновимо Helm-чарт з Karpenter
задеплоїмо, перевіримо – старі ноди від старого Provisioner продовжать працювати, поки ми їх не вб’ємо
додамо нові NodePool та EC2NodeClass
перестворимо WorkerNodes
Поїхали.
Step 1: оновлення Terraform Karpenter module
Виконуємо terraform apply до всіх змін, щоб мати задеплоєну останню версію нашого коду.
У файлі karpenter.tf маємо виклик модуля і його версію:
module "karpenter" {
source = "terraform-aws-modules/eks/aws//modules/karpenter"
version = "19.16.0"
...
Karpenter Instance Profile
В самому модулі була додана нова змінна enable_karpenter_instance_profile_creation, яка визначає хто буде менеджити IAM Roles для WorkerNodes – Terraform, як було раніше, чи використати нову фічу від Karpenter. Якщо enable_karpenter_instance_profile_creation задати в true, то модуль просто додає ще один блок прав в IAM, див. main.tf.
Але тут “є нюанс” (с) в залежностях модулю Karpneter, і Helm-чарту: якщо enable_karpenter_instance_profile_creation включити в true (дефолтне значення false) – то модуль не створить resource "aws_iam_instance_profile", який далі використовується в чарті для Karpenter – параметр settings.aws.defaultInstanceProfile.
Тож тут два варіанти:
спочатку оновити тільки версії – модуля і чарта, але використати старі параметри і Provisioner
після апдейту – створити NodePool and EC2NodeClass, замінити параметри, і перестворити WorkerNodes
або обновити відразу все – і модуль/чарт, і параметри, і Provisioner замінити на NodePool, і задеплоїти все разом
Спочатку можна зробити пошагово, десь на Dev-кластері, щоб подивитись, як воно все пройде, а потім на Prod викатувати вже весь апдейт відразу.
Почнемо, звісно, з Dev.
Міняємо версію на v19.18.0 – див. Releases, і додаємо enable_karpenter_instance_profile_creation = true – але поки закоментимо:
module "karpenter" {
source = "terraform-aws-modules/eks/aws//modules/karpenter"
#version = "19.16.0"
version = "19.18.0"
cluster_name = module.eks.cluster_name
irsa_oidc_provider_arn = module.eks.oidc_provider_arn
irsa_namespace_service_accounts = ["karpenter:karpenter"]
create_iam_role = false
iam_role_arn = module.eks.eks_managed_node_groups["${local.env_name_short}-default"].iam_role_arn
irsa_use_name_prefix = false
# In v0.32.0/v1beta1, Karpenter now creates the IAM instance profile
# so we disable the Terraform creation and add the necessary permissions for Karpenter IRSA
#enable_karpenter_instance_profile_creation = true
}
...
Поки не деплоїмо, йдемо до чарту.
Step 2: оновлення Karpenter Helm chart
Версії чартів в мене зібрані в одній змінній – оновлюємо тут karpneter v0.30.0 на v0.32.1:
Далі у нас два основних апдейти – це CRD та values чарту.
Karpenter CRD Upgrade
CRD є в основному чарті Karpneter, але:
Helm does not manage the lifecycle of CRDs using this method, the tool will only install the CRD during the first installation of the helm chart. Subsequent chart upgrades will not add or remove CRDs, even if the CRDs have changed. When CRDs are changed, we will make a note in the version’s upgrade guide.
Тобто наступні запуски helm install не оновлять CRD, які були встановлені при першій інсталяції.
Тож варіантів тут (знову!) два – або просто вручну їх додати з kubectl, або встановити з додаткового чарту karpenter-crd, див. CRD Upgrades.
При чому чарт встановить і старі CRD v1alpha5, і нові v1beta1, тобто ми будемо мати такий собі “backward compatible mode” – зможемо використовувати і старий Provisioner, і одночасно додати новий NodePool.
З helm template можна перевірити що саме чарт karpenter-crd буде робити:
Але тут є нюанс: для встановлення нових CRD з чарту потрібно буде видалити вже існуючі CRD, а це призведе до того, що і існуючі Provisioner та Machine і відповідні WorkdeNodes будуть видалені.
Тож якщо це вже давно існуючий кластер, і ви хочете все зробити без downtime – то CRD встановлюємо руками.
Якщо кілька хвилин простою вам ОК – то краще вже робити з додатковим Helm-чартом, як й надалі буде все автоматично менеджити.
І запускаємо terraform apply – зараз у нас має оновитись тільки сам чарт – в IAM поки змін не буде, бо маємо enable_karpenter_instance_profile_creation == false.
Після деплою перевіряємо CRD:
$ kk get crd | grep karpenter
awsnodetemplates.karpenter.k8s.aws 2023-11-02T15:33:26Z
ec2nodeclasses.karpenter.k8s.aws 2023-11-03T11:20:07Z
machines.karpenter.sh 2023-11-02T15:33:26Z
nodeclaims.karpenter.sh 2023-11-03T11:20:08Z
nodepools.karpenter.sh 2023-11-03T11:20:08Z
provisioners.karpenter.sh 2023-11-02T15:33:26Z
Перевіряємо поди і ноди – все має залишитись, як було – той самий ресурс Machine, щоб був створений зі старого Provisiner, і та сама WokrderNode:
$ kk get machine
NAME TYPE ZONE NODE READY AGE
default-b6hdr t3.large us-east-1a ip-10-1-35-97.ec2.internal True 30d
Якщо все ОК – то переходимо до створення NodePool та EC2NodeClass.
Step 3: створення NodePool та EC2NodeClass
Спочатку давайте розберемося з IAM ролями 🙂 Але це стосується конкретного мого сетапу, бо якщо ви всі ноди створюєте з Karpenter, то цю частину можна скіпнути.
В Terraform модулі EKS у нас створюється Managed Node Group, в якій створюється IAM Role, яка потім використовується в InstanceProfile для всіх нод кластера.
Далі ця роль передається в модуль karpenter, і тому create_iam_role в модулі Карпентер стоїть в false – бо роль вже є:
Щоб в новому маніфесті з EC2NodeClass передати в поле spec.role ім’я замість iam_role_arn – шукаємо його в outputs.tf:
...
output "iam_role_name" {
description = "The name of the IAM role"
value = try(aws_iam_role.this[0].name, null)
}
output "iam_role_arn" {
description = "The Amazon Resource Name (ARN) specifying the IAM role"
value = try(aws_iam_role.this[0].arn, var.iam_role_arn)
}
...
В spec.amiFamily передаємо AmazonLinux v2, в spec.role – IAM Role для InstanceProfile – вона ж додана і до aws-auth ConfigMap нашого кластера (в модулі eks).
Додавання NodePool
Так як Provisioner/NodePools планувалось мати не один, то їхні параметри задані в variables – копіюємо саму змінну:
Як і з Provisioner – додаємо файл шаблону configs/karpenter-nodepool.yaml.tmpl – тут формат теж трохи змінився, наприклад labels тепер в блоці spec.template.metadata.labels а не spec.labels, як було в Provisioner, див. NodePools.
Тож тепер шаблон виглядає так:
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
name: ${name}
spec:
template:
metadata:
%{ if labels != null ~}
labels:
%{ for k, v in labels ~}
${k}: ${v}
%{ endfor ~}
%{ endif ~}
spec:
%{ if taints != null ~}
taints:
- key: ${taints.key}
value: ${taints.value}
effect: ${taints.effect}
%{ endif ~}
nodeClassRef:
name: default
requirements:
- key: karpenter.k8s.aws/instance-family
operator: In
values: ${jsonencode(instance-family)}
- key: karpenter.k8s.aws/instance-size
operator: In
values: ${jsonencode(instance-size)}
- key: topology.kubernetes.io/zone
operator: In
values: ${jsonencode(topology)}
# total cluster limits
limits:
cpu: 1000
memory: 1000Gi
disruption:
consolidationPolicy: WhenEmpty
consolidateAfter: 30s
в resource "helm_release" "karpenter" видаляємо з values aws.defaultInstanceProfile
в module "karpenter" включаємо enable_karpenter_instance_profile_creation в true
Тепер Terraform має:
додати права до ролі KarpenterIRSA
якщо для модулю karpenter не передавався параметр create_instance_profile == false – то видалиться module.karpenter.aws_iam_instance_profile, але в моєму випадку він все одно не використовувався
і додати kubectl_manifest.karpenter_nodepool["default"] та kubectl_manifest.karpenter_node_class
Деплоїмо, перевіряємо:
$ kk get nodepool
NAME NODECLASS
default default
$ kk get ec2nodeclass
NAME AGE
default 40s
І все ще маємо нашу стару Machine:
$ kk get machine
NAME TYPE ZONE NODE READY AGE
default-b6hdr t3.large us-east-1a ip-10-1-35-97.ec2.internal True 30d
Все – нам лишилось перестворити WorkderNodes, перемістити Поди, і після деплою на Staging та Production прибратись в коді – видалити все, що лишилось від версії 0.30.0.
The provided credentials do not have permission to create the service-linked role for EC2 Spot Instances
В якийсь момент в логах пішла помилка такого плана:
karpenter-5dcf76df9-l58zq:controller {“level”:”ERROR”,”time”:”2023-11-03T14:59:41.072Z”,”logger”:”controller”,”message”:”Reconciler error”,”commit”:”1072d3b”,”controller”:”nodeclaim.lifecycle”,”controllerGroup”:”karpenter.sh”,”controllerKind”:”NodeClaim”,”NodeClaim”:{“name”:”default-ttx86″},”namespace”:””,”name”:”default-ttx86″,”reconcileID”:”6d17cadf-a6ca-47e3-9789-2c3491bf419f”,”error”:”launching nodeclaim, creating instance, with fleet error(s), AuthFailure.ServiceLinkedRoleCreationNotPermitted: The provided credentials do not have permission to create the service-linked role for EC2 Spot Instances.”}
Але, по-перше – чому Spot? Звідки це?
Якщо глянути NodeClaim, який був створений для ціїє ноди, то там бачимо "karpenter.sh/capacity-type == spot":
$ kk get nodeclaim -o yaml
...
spec:
...
- key: karpenter.sh/nodepool
operator: In
values:
- default
- key: karpenter.sh/capacity-type
operator: In
values:
- spot
...
Хоча в документації сказано, що по-дефолту capacity-type має бути on-demand:
...
- key: "karpenter.sh/capacity-type" # If not included, the webhook for the AWS cloud provider will default to on-demand
operator: In
values: ["spot", "on-demand"]
...
А ми в NodePool його не вказували.
Якщо ж в NodePool вказати karpenter.sh/capacity-type явно:
...
requirements:
- key: karpenter.k8s.aws/instance-family
operator: In
values: ${jsonencode(instance-family)}
- key: karpenter.k8s.aws/instance-size
operator: In
values: ${jsonencode(instance-size)}
- key: topology.kubernetes.io/zone
operator: In
values: ${jsonencode(topology)}
- key: karpenter.sh/capacity-type
operator: In
values: ["on-demand"]
...
То все працює, як треба.
І по-друге – яких саме пермішенів йому не вистачає? Що за помилка “ServiceLinkedRoleCreationNotPermitted“?
Я спотами в AWS не користувався, тому довелось трохи погуглити, і відповідь знайшлась в документації Work with Spot Instances та гайді Using AWS Spot instances, де мова йде про IAM Role AWSServiceRoleForEC2Spot, яка має бути створена в AWS Account, щоб мати змогу створювати Spot-інстанси.
Трохи дивне рішення по-дефолту створювати Spot, тим більш в документації говориться навпаки. Крім того – в 0.30 все працювало і без явного налаштування karpenter.sh/capacity-type.
Окей, будемо мати на увазі – якщо користуємось виключно On Demand – то треба додавати в конфіг NodePool.
Step 3: оновлення WorkerNodes
Що нам лишилося – це переселити наші поди на нові ноди.
Насправді всі поди переїхали на нові ноди ще під час апдейту, але давайте зробимо, бо нам ще апдейтити інші кластери.
Тут маємо три варіанти, про які говорили на початку. Давайте пробувати робити це без даунтайму – з використанням drift (але без даунтайму – це якщо маєте мінімум по 2 поди на сервіс, і на додачу PodDisruptionBudget).
Що нам треба зробити – це додати taint до існуючого Provisioner, задеплоїти зміни, щоб taint додався до Nodes, і тоді Karpenter виконає Node Drain та створить нові ноди, щоб перемістити наші workloads.
Додаємо в наш шаблон configs/karpenter-provisioner.yaml.tmpl:
І вже всі ці зміни разом можна викатувати на інші Kubernetes кластери – тільки не забудьте оновити tfvars для цих кластерів (якщо маєте щось типу окремих envs/dev/dev-1-28.tfvars, envs/staging/staging-1-28.tfvars, envs/prod/prod-1-28.tfvars).
Rolling back the upgrade
Навряд чи це знадобиться, бо в принці особливих проблема не має бути, але я робив під час ре-тесту апгрейду, тож запишу:
міняємо версію module "karpenter" з нової 19.18.0 на стару 19.16.0
в module "karpenter" коментуємо опцію enable_karpenter_instance_profile_creation
в tfvars для helm_release_versions міняємо версію чарту Karpenter з нової v0.32.1 на стару v0.30.0
лишаємо resource "helm_release" "karpenter_crd"
в resource "helm_release" "karpenter" коментуюємо новий блок values, розкоментуємо старі values через set
коментуємо ресурси resource "kubectl_manifest" "karpenter_nodepool" та resource "kubectl_manifest" "karpenter_node_class"
у файлі configs/karpenter-provisioner.yaml.tmpl прибираємо Taint