Підготовка до зими 2024-2025: частина 1 – ДБЖ, інвертори, та акумулятори
0 (0)

29 Червня 2024

Перший пост цієї “серії” (яка взагалі не планувалась як серія) – я написав ще у 2022, коли вперше довелося почати розбиратись з електрикою і тим, як забезпечити електрохарчування вдома під час відключень:

  1. Підготовка до зими 2022-2023: інтернет, електрика, опалення, їжа та вода (опублікований 1 січня 2023)
  2. Підготовка до зими 2023-2024: електрохарчування (опублікований 13 серпня 2023)

Окремо варто згадати пост Linux: збереження заряду батареї ноутбуку – як визначити споживану потужність ноутбука, і як її можна зменшити.

Цей пост я почав писати в кінці жовтня – на початку листопада 2023 року, коли готувався до зими, але недописав, та й зима пройшла без відключень.

Втім, раптом питання стало дуже актуальним влітку 2024 року – тож давайте повернемось до цієї теми.

Отже, про що будемо говорити:

  • згадаємо що таке вольти/ампери/вати
  • як порахувати потужність електроприладів вдома і прикинути необхідний запас енергії
  • як ми цей запас можемо тримати – які типи зарядних станцій та інших подібних рішень є
  • розглянемо питання, пов’язані з ДБЖ та інверторами – на що звертати увагу при виборі, як правильно користуватись
  • і поговоримо про акумулятори – які типи є, їхні характеристики, плюси-мінуси, і розглянемо питання зарядки

Відразу хочу нагадати, що я не електрик, а звичайний айтішник.

І цей пост – не “повний гайд” з точними рекомендаціями що вибрати і як налаштувати, а більше має на меті дати загальну інформацію по основним питанням.

Note: чому “електрохарчування”а не електроживлення? Див. Визначення «електрохарчування».

Вольти, вати, ампери

Трохи детальніше намагався розібрати в Підготовка до зими 2023-2024: електрохарчування, але давайте згадаємо:

  • Вольтинапруга – по суті схожа з величиною тиску води в трубі – чим він вище, тим с більшою силою йде вода з крана, тобто з якою “силою” електрони штовхаються через провідник. Позначається як В або V.
  • Ампер: сила струму – можна порівняти з кількістю води, що протікає через трубу за одиницю часу, або з кількістю даних, що передається через мережевий канал за одиницю часу (наприклад, кількість пакетів або бітів). В цьому випадку ампери визначають, скільки електричних зарядів проходить через провідник за одиницю часу.
  • Ват: потужність – можна порівняти з кількістю води в літрах, яке виллється з крана. Позначається як W (Вт).

Наприклад, якщо знаємо, що батареї EcoFlow працюють на 50 вольтах, а споживана потужність під час зарядки 2000 ват – то отримуємо силу струму зарядки у 2000/50 == 40 ампер. Але про це все будемо детальніше говорити далі.

Або навпаки: якщо зарядний пристрій працює на 12 вольтах і видає 10 ампер сили струму – то він передає 120 ват, а якщо 24 вольти на тих жеж 10 амперах – то вже 240 ват.

Ще досить гарне пояснення – побачив колись давно десь в коментарях на Ютубі, і мені воно прям дуже подобається:

– Напруга (V): ширина річки
– Сила струму (A): швидкість течії води в річці
Тому одну і ту ж потужність може дати й широка ріка (висока напруга, Вольт) з повільною течією (слабкий струм, Ампер) – і вузька річка (низька напруга, Вольт) зі швидкою течією (сильний струм, Ампер).
Чим швидше ріка і чим вона ширше – тим більше води (Ват) за одиницю часу.

Тобто:

  • Напруга (V) – ширина річки: широка річка може передати більше води при тій же швидкості течії (силі струму)
    • хоча ширина річки тут не дуже вдала аналогія, бо напруга (в вольтах) описує різницю електричних потенціалів, що штовхає електрони через провідник, подібно до того, як різниця висот штовхає воду вниз по річці
  • Сила струму (A) – швидкість течії води в річці:
    • умовна річка шириною в 10 метрів при швидкості течії води в 10 метрів на секунду передає 100 кубометрів води в секунду
    • умовна річка шириною в 100 метрів при швидкості течії  води в 1 метр на секунду також передає 100 кубометрів води в секунду
  • Потужність (W) – кількість води за одиницю часу: потужність визначається як кількість енергії, переданої (або спожитої) за одиницю часу, і в цій аналогії це кількість води, що проходить через річку за певний проміжок часу (наші 100 кубометрів в секунду)

Потужність можна отримати за допомогою формули P=V×I, де P – потужність в ватах, V – напруга в вольтах, а I – сила струму в амперах.

Рахуємо потужність приладів вдома

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

Але є така ось табличка, аби мати уявлення про потужність різних приладів:

Отже, що нам знадобиться? Рахуємо те, що має працювати постійно, тобто телевізор, ігрову приставку або ігровий ПК пропускаємо.

В моєму випадку це:

  • холодильник: 80 ват (маємо на увазі пусковий струм до х10 від номінального – але про це поговоримо, коли будемо розглядати інвертори та ДБЖ)
  • газовий котел для опалення і гарячої води для душа/кухні (у нас в ЖК свої скважини, насоси та генератори для них): 130 ват (і теж має пусковий струм, хоча тут, мабуть, поменше, бо нема компресора, як у холодильнику – тільки двигун насоса)
  • і “куточок користувача”:
    • настільна лампа: 8 ват
    • колонки: 10 ват
    • роутер: 18 ват
    • медіаконвертер (оптика => ethernet): 12 ват
    • монітор: 40 ват
    • ноутбук: 60 ват

Заміряти можна ватметром, наприклад мій холодильник:

Але холодильник і котел працюють не постійно, а вмикаються та вимикаються при потребі, коли змінюється температура. Давайте вважати, що вони працюють 50/50 від загального часу.

Тоді холодильник і котел працюючи одночасно споживають ~210 ват, умовно кажучи, годину через годину. Значить в розрахунок беремо 100 ват/годину.

Тепер додаємо решту – “куточок користувача”, і тут маємо ще ~150 ват/годин.

Отже, разом це 250 ват/годину – і тепер ми можемо рахувати необхідний запас.

Самий довгий блекаут 2022/2023 був три доби, а на добу нам потрібно:

  • холодильник і котел: працюють цілодобово, по 100 Вт/г – на добу потрібен запас у 2400 ват/годин
  • “куточок користувача”: працюють умовних 14 годин на добу, по 150 Вт/г – на добу потрібен запас у 2100 ват/годин

Тобто разом необхідний добовий запас енергії – 4500 ват/годин. Хоча взимку 2022/2023 я обходився без холодильника – м’ясо з морозильника вивішував в пакеті за вікном.

Тепер можна поговорити про те, як цей запас отримувати та зберігати.

Варіанти енергозабезпечення: зарядні станції, акумулятори, ДБЖ

Ну і тут вибір на всі смаки:

  • зарядні станції типу EcoFlow/Bluetti: найкращій варіант з точки зору безпеки та обслуговування, бо “it just works”, але і найдорожчий
  • саморобні станції від майстрів: дешевші від EcoFlow, прості в обслуговуванні, але є ризики що китайська начинка накриється, і станція перестане працювати, або можливі проблем з батареями (і навіть пожежа)
  • ДБЖ та акумулятори: більш складний варіант з точки зору зборки самої системи, більш небезпечний ніж EcoFlow (але безпечніший за саморобні станції), проте дешевше, ніш EcoFlow
  • окремо зарядне, інвертор та акумулятори: найскладніший з точку зору зборки системи та її обслуговування, і найбільш небезпечний при порушенні правил використання

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

Найбільші ризики використання всіх подібних приладів – це пожежа, і ці ризики треба мати на увазі, бо ми багато в новинах чуємо про черговий випадок вибухів на балконах.

Наприклад я собі “від гріха подалі” ще у 2022 купив два вогнегасники:

Ще – якщо дійдуть руки, буде час та натхнення – то хочу зробити з Adruino датчики тепла (міряти температуру на балконі, це стоять зарядні станції та акумулятори, і температуру самих акумуляторів), та датчик диму. Хоча можна просто купити готові рішення від того ж Ajax Systems.

Окей. Тепер давайте розберемо ці системи детальніше.

Зарядні станції EcoFlow/Bluetti/etc

Я їх не розглядав в минулих записах, бо в мене їх не було 🙂

Але в цьому році я все ж купив собі EcoFlow DELTA Max 2000 (2016 Вт·год). Обійшлася вона мені в ~59.000 грн (брав на Rozetka), і я вважаю, що воно того варте.

В мене Li-ion батареї, а аналогічна станція з LiFePo4 буде коштувати близько 100.000 (це зараз, при курсі долара ~40 грн). Про батареї будемо говорити окремо і детальніше далі, але в принципі LiFePo4 варті свої грошей.

Коли я себе вговорював купити її, то останнім було – “Блін, ти собі на день народження подарував Samsung S23 Ultra за 50.000 грн! І це просто телефон – а тут питання нормального функціювання!“.

Тобто – да, воно коштує грошей, і не мало, проте ви просто його вмикаєте – і спокійно собі живете.

Переваги:

  • простота: воно просто працює, і нічого в принципі від вас не вимагає
  • безпека: це, мабуть, максимальний рівень безпеки в плані як вибуху/пожежі, так і випадкового замикання дротів або удару током вас, як юзера
  • надійність: це, мабуть, максимальний рівень безпеки надійності з точки зору поломки – якщо правильно використовувати та не перенавантажувати
  • швидкість зарядки: це дуже важливий момент, і про нього будемо говорити далі, але щодо EcoFlow – то він свої 2000 Вт/год “заливає” за півтори години – і це дуже круто
  • мобільний додаток: керування параметрами та відображення даних по споживанню/запасу енергії – дуже зручно, дуже корисно
  • компактність: для розміщення такого девайсу знадобиться набагато менше місця, ніж для звичайних акумуляторів на той самий об’єм енергії

Недоліки:

  • фактично, він тут один – це ціна, інших поки не побачив
  • ще до недоліків можна додати, що ані станція, ані мобільний застосунок не попереджають про низький заряд батареї – станція просто мовчки виключається

Але EcoFlow має купу метрик, з якими ми можемо зробити крутий моніторинг з алертами – див. EcoFlow: моніторинг з Prometheus та Grafana.

Щодо шумності: EcoFlow DELTA Max доволі шумна, і включає кулери як під час зарядки, так і під час використання. Спати з такою станцією в одній кімнаті буде не дуже зручно (навряд чи можливо взагалі), тож у випадку однокімнатної квартири-студії треба або вимикати станцію на ніч, або ставити десь в кладовку чи на балкон, а враховуючи відносну пожежобезпечність такого приладу – цілком собі варіант тримати десь там.

Але слідкуйте за температурою, особливо якщо балкон на сонячній стороні, бо в мене станція іноді нагрівається до +45, і починає видавати попередження – доводиться ставити вентилятор. Або закривайте вікна балкона, наприклад Сонцезахисною плівкою.

Мобільний додаток для EcoFlow виглядає ось так – тут підключений ігровий ПК, монітор, роутер, колонки:

Налаштування девайсу:

І під час зарядки – 2000 ват (це на початку, потім зменшується сила струма на батареї, і, відповідно, споживана потужність):

EcoFlow: рекомендації з використання

Кілька рекомендацій по використанню EcoFlow, хоча це відноситься і до інших аналогічних девайсів.

Треба мати на увазі, що деградація батарей – це зменшення їх ємності. 800 циклів в документації EcoFlow – це зменшення ємності до 80% від початкової, а не повністю вихід з ладу. До того ж зараз наче можна легко замінити батареї на нові.

Мінімальний та максимальний рівень заряду

Має сенс виставити обмеження на мінімальний та максимальний рівень заряду – 20% мінімум, і 90% максимум. В такому випадку станція не буде повністю висаджувати батареї, що для них не є ОК, і не буде повністю їх заряджати. Хоча я впевнений, що контролер самої станції і так має потрібні обмеження – але зайвим не буде.

Не тримати постійно на зарядці

Друге, що я роблю – це не тримаю її на зарядці постійно, бо по документації вона розрахована на 800 циклів заряду/розряду (це при Li-ion батареях, для LiFePo4 це здається 3000 циклів). Так – доводиться робити додаткові рухи руками, і перемикати розетки/подовжувачі, коли вимикають/вмикають світло – зато проживе станція довше, бо світло вимикається кілька раз на добу, і кілька раз на добу вмикається зарядка батарей.

Хоча просто тримати її ввімкненою зручно, бо станція вміє працювати як UPS, тобто коли в мережі є енергія – станція живить прилади від мережі, а як в мережі пропадає – то переключається на батареї.

Не перенавантажувати станцію

Ну і, звісно, не можна перенавантажувати станцію. В неї заявлена видача у 2000 ват, є режим X Boost до 4600 ват. Тож не варто підключати стиральну машинку, кавоварку і чайник одночасно.

Вимикати інвертор

Також майте на увазі, що вбудований інвертор станції (взагалі будь-який інвертор) також споживає енергію, навіть якщо не підключено ніяких приладів, і споживає близько 20-30 ват на годину – тобто він з’їсть ~500 ват запасу за добу, навіть працюючи вхолосту. Тому можете або вимикати AC вручну, або налаштувати автовимкнення (дефолт – 12 годин).

Наприклад, з включеним AC при заряді в 34% станція видає 1 день роботи (без приладів):

А з відключеним – 99 годин:

Зменшувати швидкість зарядки

Можна зменшити максимальне навантаження станції під час зарядки батарей (2000 ват/годину в моїй).

Аби включити опцію власного налаштування – на задній панелі треба переключити Fast – Slow/Custom:

Після чого в мобільному застосунку стане доступним налаштування AC charge speed:

Це зменшить струм заряду, і батареї будуть почуватись краще.

Саморобні зарядні станції від “умільців”

Я не скажу, що рекомендував би такий варіант, бо тут занадто багато “як пощастить”: як пощастить з руками майстра, як пощастить з компонентами (Китай/не Китай і т.д.).

Але в цілому – варіант робочий, і в мене одне таке чудо є:

Заявлені такі ж самі 2000 Вт/годин, але реально вдається накопичити близько 1300 – тестував як раз на ігровому ПК, який споживає ~300 ват, і працює близько 4 годин.

В середині це чудо виглядає так:

Синя плата справа – це BMS, Battery Management System, яка контролює заряд акумуляторних батарей – рівень їх заряду/розряду, і додатково може вміти в захист від замикань, перенавантаження тощо.

Переваги:

  • швидкість зарядки: зазвичай такі станції роблять з потужними зарядками, наприклад в мене вона заряджається за пару годин
  • компактність: конкретно ця більша за EcoFlow, але це все ще менше місця, ніж 2 акумулятори і ДБЖ, плюс тут все в одному корпусі
  • ціна: на Olx такі системи продаються за ціною в районі 20.000 грн за 2000 Вт/г, що набагато дешевше, ніж EcoFlow, який, нагадаю, коштує майже 60.000 за 2.000 Вт/г

Недоліки:

  • надійність: Китай 🤷‍♂️ Man Shrugging Emoji іноді інвертор починає пищати під час роботи, і я поки не зрозумів чому (5 коротких звукових сигналів – хтось, може, в курсі?)
  • безпека: тут знов-таки покладаємось на руки майстра – наскільки він все правильно зробив (якість пайки, сама схема роботи), наскільки якісні компоненти використовував (а враховуючи факт, що такі станції намагаються зробити не надто дорогими – то можливі нюанси)
  • шум: під час зарядки гудить, як літак на форсажі – набагато гучніша за EcoFlow

ДБЖ та зовнішні акумулятори

Ще один варіант – це купити окремо акумулятор, і окремо до нього – ДБЖ.

Наприклад, в мене є ось такий комплект:

Тут ДБЖ CyberPower CPS1000E та AGM-акумулятор на 72 Ампер/години.

Про типи і ємність акумуляторів теж будемо говорити далі.

Але цей сетап більш резервний, і купувався ще у 2022, коли вибору особливо не було, бо тоді скупали просто все, що з’являлось.

Переваги:

  • ну, воно працює… в принципі це, мабуть, все
  • ціна: насправді, не впевнений, чи заносити це в плюси, чи в мінуси, але в порівнянні з EcoFlow цей варіант буде дешевшим (хоча в порівнянні з EcoFlow, мабуть, будь-що буде дешевшим 🙂 )
    • мені сам ДБЖ обійшовся у 2022 році в 23.000 грн, зараз такий коштує від 16.000 (на Розетці) до тих жеж 23.500 гривень
    • акумулятор – якщо брати ті ж умовні 2.000 ват/годин (200 ампер/годин на 12 вольт, тобто 1 шт), то за Li-ion це буде близько 20.000 грн, а за LiFePo4 – 35-50 тисяч гривень
  • надійність: через те, що тут два компоненти, то і говорити про них треба окремо, але щодо самого ДБЖ – то CyberPower наче доволі відома компанія, тож тут можна поставити плюс
  • безпека: при умовах правильного використання більш безпечне рішення в порівнянні в саморобними станціями, але залежить від самого ДБЖ та акумуляторів, але точно менш безпечне за EcoFlow

Недоліки:

  • шум: ДБЖ (принаймні цей) доволі шумний під час зарядки, в кімнаті не поставиш (приблизно на рівні EcoFlow)
  • надійність: а тут вже про акумулятор, і тут багато чого “It depends” – і виробник акумулятору, і те, наскільки ДБЖ правильно заряджає акумулятор(и), і температура в місці, де акумулятор стоїть – бо вони не люблять ані холод, ані жару
  • безпека: і тут знов-таки дуже багато залежить від акумулятора і умов експлуатації, тому запишу в мінуси, бо ризики вибуху/пожежі акумулятора є
  • швидкість зарядки: багато залежить від ДБЖ та акумуляторів, але скоріш за все це буде в кілька разів довше (якщо не в 10 раз), ніж той же EcoFlow
  • компактність: це система, яку треба один раз поставити, і забути, бо з місця на місце просто так не перенесеш – треба розбирати компоненти, і переносити окремо

Окремо зарядне, інвертор та акумулятори

В мене такої системи нема, але в Телеграм-чаті RTFM після публікації першого посту хтось скидав фотки системи вдома, де стоїть акумулятор, окремо до нього підключається зарядне, і окремо – інвертор.

Переваги:

  • ціна: єдина, мабуть, перевага такого рішення – це ціна, і то не впевнений

Недоліки:

  • простота: ніякої, бо маєте, по-перше, дуже ретельно вибрати всі компоненти – і акумулятор, і відповідний зарядний пристрій, по-друге – це все з’єднується окремо, і маєте або перемикати постійно вручну – або знов-таки мати мороку з якимись додатковими компонентами контролю/перемикання
  • безпека: трохи є, але гірше за інші рішення, бо дуже багато “It depends” – як правильно виберете компоненти, надійність компонентів, їхній моніторинг і так далі
  • компактність: і знов мінус, бо в рішення з ДБЖ+акумулятор у вас принаймні зарядне+інвертор в одному корпусі, а тут – купа проводів

Знайшов фотки з чату в Телеграмі:

І ще окремо сам зарядний пристрій:

Імхо – дуже так собі рішення, бо надто багато геморою.

Втім, на Youtube-каналі @izmailinvertor є багато відео, де людина з прямими руками розказує про такі системи.

Висновки

Тож якщо обирати рішення для дому – то, звісно, якщо є гроші – найкращим варіантом будуть станції по типу EcoFlow.

Другий варіант – це ДБЖ+зовнішні акумулятори. З недоліків – швидкість зарядки і компактність, але має бути дешевшим.

Варіант з саморобними зарядними станціями з Olx особисто я не рекомендував би, бо віри в надійність і безпеку мало. Хоча так – сам вдома таку станцію тримаю, але купував, бо на той час не було грошей, економив, а викинути тепер жалко – “запас карман нє тянєт”.

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

ДБЖ та інвертори

Мабуть, окремо варто трохи поговорити про різницю між ними, і як їх вибирати.

Отже, ДБЖ – це “all inclusive” система, де в одному корпусі ви маєте і зарядний пристрій для акумуляторів, і інвертор. Зарядний пристрій, власне, заряджає батареї, а інвертор – “розряджає”, тобто передає струм з них на побутові прилади, при потребі збільшуючи струм з 12/24/etc вольт до звичних 220, і перетворюючи його з постійного на змінний.

Інвертор жеж – це тільки перетворення постійного струму 12 вольт з акумулятору на 220 вольт змінного струму, і нічого більше. Хіба що якийсь додатковий захист від замикань/перенавантаження тощо.

І не забуваємо, що будь-який інвертор, навіть в EcoFlow частину енергії витрачає на перетворення струму, приблизно 15%. Тобто, якщо батарея на 1000 ват/годин – то реальної ємності буде 850.

Вибір ДБЖ/інвертора

При виборі ДБЖ є три основних параметри, на які треба звертати увагу – це вихідна та зарядна потужність, і синусоїда.

Вихідна потужність ДБЖ та інверторів

На початку ми порахували, що разом в мене вдома споживається 250 ват/на годину, тож мінімально ДБЖ має бути з запасом хоча б х2, тобто 500 ват  – мій CyberPower CPS1000E видає до 700 ват, а EcoFlow – до 2000.

Але тут треба враховувати нюанс з системами типу холодильника, у яких дуже високе споживання при старті двигуна/компресора – від 800 до 1500 ват, тому від CyberPower його захарчувати не вийде, бо або спрацює захист і ДБЖ виключиться – або захист не спрацює, і ДБЖ згорить.

Вольт-ампери та вати

Також потужність ДБЖ часто вказується у вольт-амперах (ВА, також позначається як “повна потужність”), а споживана потужність приладів зазвичай в ватах (“активна потужність”). В такому випадку, аби перевести потужність в вольт-амперах в вати – вольт-ампери множимо на коефіцієнт потужності приладу, зазвичай це 0.6 – 0.9, але має бути вказаний в документації приладу. Можна просто брати середнє значення – 0.85.

Зарядна потужність ДБЖ

І оце дуже важливий момент, про який будемо говорити далі, бо згадуючи зиму 2022/2023, коли світло іноді бувало по кілька годин на добу – то за ці кілька годин вам потрібно повністю зарядити ваші акумулятори.

При цьому є нюанс і з самими акумуляторами, які не дуже люблять високі токи, і якщо якийсь AGM кожен день по кілька раз заряджати на 10 амперах – то спасибі він вам не скаже. Втім, це знов-таки окрема тема, про яку говоритимо далі.

Типи ДБЖ

ДБЖ поділяються на різні типи – в залежності від задачі, для якої проектувались:

  • резервні ДБЖ (Off-Line, Standby): самі прості, призначені виключно для підстрахування на кілька хвилин – поки світло є, то живлення передається напряму з мережі і паралельно заряджаються акумулятори, а коли вмикається напруга в мережі – то вмикається живлення від акумуляторів
  • лінійно-інтерактивні (Line-Interactive): поки в мережі є напруга – то передають її на прилади і паралельно згладжують коливання напруги в електромережі, тобто працюють як стабілізатори напруги, при вимиканні світла або перепадах напруги – перемикаються на роботу від батарей
  • інверторні або ДБЖ безперервної дії (Online): найбільш просунуті системи, які постійно перетворюють струм з мережі в постійний струм, вирівнюють будь-які коливання, перетворюють назад в змінний, а потім передають на прилади

Форма вихідної напруги

ДБЖ можуть видавати чисту або модифіковану синусоїду змінного струму, і деякі прилади вимагають саме чисту – наприклад, двигун холодильника, газовий котел або медична апаратура.

Дуже важлива характеристика ДБЖ, яку треба мати на увазі.

Непоганий матеріал на цю і інші теми по ДБЖ – Що потрібно знати про джерела безперебійного живлення (ДБЖ).

Рекомендації по використанню інверторів

Коротко про те, як правильно використовувати інвертори:

  • при включенні:
    • у виключеному стані інвертор підключаємо до батареї
    • включаємо інвертор, чекаємо, поки він “заведеться”
    • підключаємо прилади
  • при виключенні:
    • відключаємо прилади
    • виключаємо інвертор
    • відключаємо від батареї

Див. трохи більше деталей у відео Как правильно включать – выключать инвертор 12-220В.

Акумулятори

Тему про акумулятори, мабуть, варто було б винести взагалі окремим постом, бо тема досить велика і місцями складна.

Але давайте спробуємо відносно стисло про неї поговорити.

Отже, що ми маємо мати на увазі при виборі акумулятора:

  • тип: кислотні, AGM, гелеві, LiFePo4 – вибір великий, і важливий
  • ємність: як правильно розрахувати наскільки вам вистачить акумулятору
  • швидкість заряду: як вибрати знов-таки тип акумулятору і ДБЖ для нього

Типи акумуляторів

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

Основні характеристики акумуляторів:

  • кількість циклів заряду-розряду: чим більше виконується циклів перезарядки – тим більше деградує (втрачає ємність батарея)
  • рівень саморозряду: як швидко акумулятор розряджається без підключених приладів
  • стійкість до глибокого розряду: наскільки деградує батарея при розряді до мінімальних значень
  • стійкість до перезаряду:
    • здатність акумулятора не перегріватися при надмірній зарядці, що впливає на його перегрів і може призвести до займання або вибуху
    • рівень деградації батареї під час зарядки високим струмом, особливо під час завершення зарядки
  • температурний режим: допустима/комфортна температура навколишнього середовища
  • чутливість до зарядного пристрою: різні типи акумуляторів мають різні характеристики процесу заряджання, і ДБЖ має це враховувати
  • вартість
  • ефект пам’яті“: втрата ємності акумулятора при неповному розряді перед наступною зарядкою
  • швидкість зарядки: власне, як швидко можна зарядити акумулятор – залежить від максимальної сили струму і типу акумулятора

Absorbent Glass Mat (AGM) акумулятори

Також є свинцево-кислотними, але електроліт всередині знаходиться в абсорбованому стані, тому вони герметичні і їх можна встановлювати в будь-яке положення окрім “догори ногами”.

Характеристики:

  • кількість циклів заряду-розряду: 300-500
  • рівень саморозряду: низький (1-3% на місяць)
  • стійкість до глибокого розряду: середня
  • стійкість до перезаряду: середня
  • температурний режим: від -20°C до +50°C (оптимально 20-25°C)
  • чутливість до зарядного пристрою: середня
  • вартість: середня
  • ефект пам’яті“: відсутній
  • швидкість зарядки: середня (можна заряджати до 20% від ємності на годину)

Переваги:

  • велика кількість циклів перезаряджання і тривалий термін служби
  • низький рівень саморозряду
  • немає “ефекту пам’яті” (можна заряджати у будь-який час, не чекаючи повної розрядки)
  • швидко заряджаються

Недоліки:

  • погано переносять перезаряджання (високий струм наприкінці зарядки), тому треба мати відповідний ДБЖ
  • обмежена кількість циклів заряду-розряду
  • відносно важкі (бо свинець)

Гелеві та мультигелеві акумулятори

За характеристиками подібні до AGM, але в якості електроліту використовується гель.

Характеристики:

  • кількість циклів заряду-розряду: 500-800
  • рівень саморозряду: низький (1-2% на місяць)
  • стійкість до глибокого розряду: висока
  • стійкість до перезаряду: висока
  • температурний режим: від -20°C до +55°C (оптимально 20-25°C)
  • чутливість до зарядного пристрою: висока
  • вартість: вище середньої
  • ефект пам’яті“: відсутній
  • швидкість зарядки: низька (можна заряджати 10-15% від ємності на годину)

Переваги:

  • довго можуть бути розрядженими
  • пристосовані під циклічний характер роботи з глибоким розрядом
  • допустимість короткострокових глибоких розрядів

Недоліки:

  • висока вартість
  • чутливість до коротких замикань
  • повільна зарядка
  • більша чутливість до температур, хоча працюють при температурі від -30 до +50
  • чутливі до зарядного пристрою

LiFePO4

Нове покоління акумуляторів – літій-залізо-фосфатні. Також герметичні, зокрема використовується в електромобілях.

Характеристики:

  • кількість циклів заряду-розряду: 2000-5000
  • рівень саморозряду: дуже низький (менше 1% на місяць)
  • стійкість до глибокого розряду: дуже висока
  • стійкість до перезаряду: висока
  • температурний режим: від -20°C до +60°C (оптимально 15-35°C)
  • чутливість до зарядного пристрою: висока (потребує спеціального зарядного пристрою або просунутий ДБЖ)
  • вартість: висока
  • ефект пам’яті“: відсутній
  • швидкість зарядки: дуже висока (можна заряджати до 1C, тобто струм заряду може дорівнювати ємності акумулятора, деякі моделі підтримують ще швидшу зарядку)

Переваги:

  • велика кількість циклів перезаряджання і тривалий термін служби – набагато більший, ніж у AGM та гелевих акумуляторів
  • низький рівень саморозряду
  • широкий діапазон робочих температур (від -15 до +60°C)
  • не бояться великих струмів (можна швидше заряджати)
  • висока швидкість заряджання – і за рахунок можливості подачі більш високого струму, ніж у AGM та гелевих акумуляторів, і за рахунок самої технології

Недоліки:

  • висока вартість в порівнянні з AGM та гелевими акумуляторами
  • потребують спеціальної системи управління зарядкою

Інші типи літій-іонних акумуляторів

Окрім LiFePO4 є і інші типи літій-іонних акумуляторів:

  • Li-ion (літій-кобальт оксид, LiCoO2): найпоширеніший тип, використовується в смартфонах, ноутбуках
  • LiMn2O4 (літій-марганець оксид): використовується в електроінструментах, медичному обладнанні
  • NMC (літій-нікель-марганець-кобальт): поширений в електромобілях та портативній електроніці
  • NCA (літій-нікель-кобальт-алюміній): використовується в електромобілях Tesla та деяких портативних пристроях.

Висновки

  • Свинцево-кислотні AGM: надійні, доступна ціна, хороший вибір для більшості домашніх систем
  • Гелеві акумулятори: підходять там, де необхідна глибока розрядка і довготривала робота без підзарядки, підходять для систем з частими і довгими відключеннями електроенергії
  • LiFePO4: все “най-” – найдовший термін служби, найшвидша зарядка, найкраща безпечність

Рекомендації по експлуатації акумуляторів

  • правильна зарядка:
    • використовуйте зарядні пристрої, призначені для конкретного типу акумулятора
    • уникайте перезаряду, особливо для свинцево-кислотних акумуляторів
    • для літій-іонних акумуляторів підтримуйте рівень заряду між 20% і 80%
  • температурний режим: зберігайте і експлуатуйте акумулятори в рекомендованому температурному діапазоні, особливо, якщо акумулятори десь на балконі, який влітку нагрівається від сонця
  • глибина розряду:
    • для свинцево-кислотних акумуляторів уникайте глибокого розряду (нижче 50%)
    • LiFePO4 акумулятори краще переносять глибокий розряд, але краще не розряджати нижче 20%
  • регулярне використання: періодично використовуйте акумулятори, не залишайте їх повністю зарядженими або розрядженими на тривалий час
  • зберігання: при тривалому зберіганні підтримуйте частковий заряд (40-60%)

Рівень розряду акумулятора та його ємність

Важливий момент, котрий треба враховувати при розрахунках: розряджати акумулятор бажано максимум до 30-40% його ємності, хоча це дуже залежить від типу – гелеві цього не люблять, AGM трохи краще переживає глибокий розряд, а LiFePO4 краще інших.

Давайте порахуємо скільки реальної (або корисної) ємності буде в акумуляторі.

Наприклад, маємо AGM 12 вольт на 100 ампер/годин – отримуємо 1200 ват/годин повної ємності.

Від цих 1200 віднімаємо хоча б 30% заряду, який потрібно залишати, аби батареї в акумуляторі почувались краще – і вже маємо 800 ват/годин.

І додатково від цих 800 Вт/г віднімаємо ще втрати на роботу інвертора – це відсотків 15, і в залишку корисної або реальної ємності вже маємо 680 ват/годин.

За цим треба або слідкувати самому (якщо використовуємо схему з окремим інвертором+зарядне+акумулятор), або сам ДБЖ має відключатись автоматично при низькому заряді батареї (і буде добре, якщо ДБЖ досить інтелектуальний, аби враховувати тип акумулятору).

Зарядка акумуляторів та важливість вибору правильного ДБЖ

Отже, від того, який саме у вас тип акумулятора дуже сильно залежить те, як він має заряджатись.

Дуже корисне відео на цю тему – Чи можна заряджати LiFePo4 акуми автомобільними зарядками?

Ще один важливий нюанс при виборі акумулятору та ДБЖ – це скільки часу буде займати зарядка.

Час заряду залежить від:

  • типу акумулятора: різні типи мають різний рівень швидкості зарядки, і у LiFePo4 найшвидша зарядка
  • максимальний/рекомендований струм заряду:
    • свинцево-кислотні AGM, мультигель та гель мають рекомендований рівень в 0.1С (1/10 ємності), тобто акумулятор в 100 ампер-годин можна заряджати максимум на 10 амперах, в такому випадку повний цикл зарядки займе 10 годин
    • LiFePo4 можна заряджати на струмі 0.5С – 1С, тобто акумулятор на 100 ампер-годин можна заряджати струмом від 50 ампер, і повний цикл зарядки займе 2 години

При перевищені струму заряду батарея може перегрітись і вибухнути (проте вас покажуть по телевізору).

На струм заряду може впливати вбудована плата BMS, про яку згадував вище, бо вона може мати власні обмеження. Втім, не варто покладатись тільки на неї (тим більш, її може і не бути), а читати документацію до ДБЖ та акумулятора.

Схеми підключення акумуляторів

Див. Паралельне та послідовне з’єднання АКБ.

Паралельне підключення

Позначається на акумуляторах як P (paralel).

При такому підключенні всі плюси та всі мінуси батарей підключаються до плюса на ДБЖ. В такому випадку загальна ємність сумується, але напруга лишається такою ж самою:

Тобто, отримаємо 200 ампер/годин і 12 вольт.

Дозволяє використовувати акумулятори з різною ємністю, але може призвести до нерівномірного розряду акумуляторів.

Послідовне підключення

Позначається на акумуляторах як S (serial).

При такому підключенні мінус одного акумулятора підключається до плюса наступного. В результаті ємність залишається такою ж самою, але їхня напруга сумується:

Тобто, отримаємо 100 ампер/годин і 24 вольт.

Вимагає точного підбору акумуляторів за ємністю та станом.

12 вольт vs 24 вольти: а для чого?

Схема підключення залежить від того, який ДБЖ у вас використовується, бо ДБЖ на 12 вольт не видасть високої потужності. 1000-2000 ват потужності для таких ДБЖ, мабуть, максимум.

Наприклад, у нас є кавоварка, яка працює з напругою 220 вольт і споживає 1000 ват/годин. Маємо інвертор, до якого підключено акумулятор на 12 вольт.

В такому випадку аби забезпечити необхідну потужність у 1000 ват від інвертора до кавоварки нам потрібна сила струму у 1000W/220V=4.54 ампери. Але сила струму від акумулятора до інвертора вже буде 1000W/12V, тобто 83 ампери. Відповідно, великі втрати на передачу енергії, і потрібні більш товсті кабелі. А при використанні інвертора на 24 вольти – це було б 41.6 ампери.

Це дуже грубий приклад, але основна ідея така.

То що в результаті? І трохи про розетки.

Отже, якщо повернутись до початку цього посту:

  • у нас є 4500 ват-годин на добу споживання
  • ми хочемо зарядити акумулятори за умовні 2 години

Чому 2 години? Бо, по-перше, говорять, що цією зимою в найгіршому варіанті світло буде 4 години на добу, але давайте ми будемо песимістами х2, і припускати, що світло буде лише 2 години на добу.

По-друге – EcoFlow свої 2 кіловат/години заряджає менш ніж за 2 години, і хочеться побудувати щось хоча б приблизно таке ж саме.

Що ми можемо зробити, аби забезпечити себе хоча б на добу?

Варіант 1 – купити пару EcoFlow. Дорого, але надійно.

Варіант 2 – купувати ДБЖ та акумулятори. З урахування специфіки різних типів АКБ, якщо ми хочемо заряджати швидко – то нам потрібні LiFePo4.

4500 ват-годин запасу – це 375 ампер/годин при 12 вольтах – тобто, 2 акуми по 200 ампер-годин. І це якщо рахувати тільки повну ємність – без втрат на перетворення і “залишковий запас” акумулятора, аби не розряджати його повністю.

А аби їх зарядити за 2 години – нам потрібен ДБЖ/зарядне, яке буде видавати 100 ампер! І таке зарядне потрібне на кожен акумулятор окремо!

Таке, звісно, реалізувати можна, але я не зустрічав таких зарядних пристроїв. До того ж, аби витримати таку силу струму будуть потрібні ну прям дуже товсті кабелі.

Тому єдине, що можна зробити – це підключити акумулятори послідовно, аби мати напругу у 24 вольти (в EcoFlow, наприклад, батареї працюють на 48 вольтах – як раз для того, аби знизити необхідну силу струму).

А маючи 24 вольти – ми можемо заряджати їх при струмі в 50 ампер, що вже більш реально, і такі зарядні пристрої знайти можна.

Тоді будемо мати 50  ампер * 24 вольти == 1200 ват потужності, а повна зарядка акумуляторів займе ~4 години. А насправді навіть більше, бо насправді “профіль зарядки” виглядає не рівномірно. Див. ось цей момент у відео – там людина малює графіки заряду.

В такому випадку, якщо ми беремо собі ліміт в 50 ампер – то варіанти будуть такі:

  • зарядне на 50 ампер + 2 акумулятори по 200 А/г послідовно: отримуємо ~2000 ват/годин, зарядка 4 години
  • зарядне на 50 ампер + 2 акумулятори по 100 А/г послідовно: отримуємо ~1000 ват/годин, зарядка 2 години

І аби забезпечити свої 4500 ват/годин запасу на добу, нам потрібно або два комплекти з двома акумуляторами по 200 А/г – які будуть заряджатись 4 години, або 4 комплекти з двома акумуляторами по 100 А/г – які будуть заряджатись за 2 дві години.

Але навіть якщо ми підемо на зборку 4-х комплектів – нам їх потрібно заряджати одночасно!

А маючи 1200 ват потужності через розетку на зарядку одного такого блоку батарей в розетці будемо мати 1200/220 = 5.4 ампер, що в принципі нормально, бо стандартно розетки розраховані на 16 ампер максимум – і це треба мати на увазі.

Тобто в одну розетку ми можемо максимум одночасно включити 2 комплекти – сила струму через розетку буде близько 10 ампер.

А включати одночасно 2 EcoFlow в один блок розеток або навіть в одній кімнаті все ж не варто: один EcoFlow при зарядці споживає до 2000 ват, тобто 9 ампер з розетки, до якої від підключений.

Це грубі розрахунки, але плюс-мінус виходить так.

Підсумки і варіанти

А тепер давайте порахуємо вартість варіантів на ~2000 Вт/годину (але пам’ятаємо, що ми нарахували необхідний добовий запас в 4500 Вт/г).

Строк служби далі – доволі умовна одиниця, бо батарея не вмре повністю, а просто втратить частину своєї ємності.

EcoFlow на 2000 Вт/годин з Li-ion батареями

Найкращій варіант з усіх точок зору, окрім ціни:

    • ціна: ~60.000 гривень
    • строк служби: враховуючи заявлені 800 циклів заряду, при щоденних відключеннях електрики пропрацює ~2-3 роки
    • плюси:
      • просто працює
      • компактні, можна перенести
      • швидко заряджається
    • мінуси: ціна

Якщо ж брати відразу з LiFePo4 батареями (нагадаю – близько 100.000 гривень на сьогодні) – то це гарантовано років 5 і більше роботи.

Або за 105.000 взагалі відразу взяти EcoFlow DELTA Pro на 3600 Вт/годин з тими ж LiFePo4.

Ну і не EcoFlow єдиним – подібних рішень багато. Просто в мене вона є, тому пишу про неї.

ДБЖ з зовнішнім LiFePo4 акумулятором на 200 ампер/годин (~2000 Вт/годин)

Непоганий варіант, але не транспортабельний і за ціною може вийти не набагато дешевше за EcoFlow. Втім, прослужить скоріш за все довше за EcoFlow з Li-ion:

    • ціна:
      • ДБЖ: 10-15 тисяч гривень
      • акумулятор: від 35 до 50 тисяч
    • строк служби: враховуючи ~3000 циклів заряду, при щоденних відключеннях електрики – пропрацює ~4-5 років
    • плюси:
      • трохи дешевший за EcoFlow
      • при правильно підібраному зарядному пристрої – швидко заряджається
      • достатньо надійно і безпечно
    • мінуси:
      • швидкість заряду: залежить від зарядного, але навряд чи вам вдасться “залити” його повністю за 2-3 години, навіть використовуючи LiFePo4 (або брати менші за ємністю акумулятори в більшій кількості – а це вплине на загальну вартість системи)
      • не можна просто так перенести в інше приміщення
      • потребує додаткових знань, додаткового обслуговування, моніторингу, обережності

Можна замість LiFePo4 взяти AGM-акумулятори – вийде в пару раз дешевше, але вони і заряджатись будуть в кілька раз довше, і строк служби буде в кілька раз меншим.

Приклад ДБЖ – LPE-B-PSW-1500VA+, і акумулятора – Kepworth LiFePO4 12V/200AH.

Зарядні станції від “майстрів” на 2000 Вт/годин

Найбільш небезпечний, проте самий бюджетний варіант:

    • ціна: від 20-25 тисяч гривень
    • строк служби: на 1-2 роки можна розраховувати, але навряд чи більше (скоріш менше, і тут вже саме про те, скілька така станція пропрацює взагалі, а не втрата ємності батареї)
    • плюси:
      • компактні, можна перенести
      • швидко заряджається
    • мінуси:
      • надійність – може зламатись в будь-який момент
      • безпека і пожежонебезпечність: відносно до EcoFlow або ДБЖ з LiFePo4 акумулятором доволі (дуже?) небезпечне рішення
      • шумні

В цілому, це, мабуть, все.

І капелька про павербанки

Ну і павербанки ніхто не відміняв.

Це мій запас, який зробив ще в кінці минулого року:

Тут:

  • дві зарядні станції Kseni по 160.000 mAh (до них інвертор на 500 ват)
  • два павербанка FutureSolar по 160.000 mAh (до них два інвертори на 150 ват кожен – телевізор від них працює без проблем)
  • один павербанк ChinaNoName на 60.000 mAh
  • два павербанка ChinaNoName по 50.000 mAh
  • два павербанка Baseus по 30.000 mAh
  • два павербанка Xiomi по 20.000 mAh

Живити через інвертор від павербанок всякі ноутбуки/монітори/світильники можна спокійно, а от холодильник і котел – вже тільки від ДБЖ/EcoFlow. Втім тут ще пам’ятаємо, що сам інвертор буде з’їдати частину енергії (хоча ноутбук можна напряму, якщо потужність павербанка дозволяє).

Як рахувати ємність павербанка?

Тут вже зовсім коротенько.

Ємність на всіх банках вказується з розрахунком на 3.7 вольти, але по факту на виході більшість видає 5 вольт. Якщо через автомобільний “прикурювач” – то там 12 вольт:

Формула:

мА/г * Вольти / 1000

Тепер рахуємо:

  • павербанк з 60.000 номінальної ємності при 3.7 вольти – це 60.000 (міліампер/годин) * 3.7 вольти == 222 ват-години
  • той же павербанк при 5 вольтах на виході – це вже 222 (ват-години) / 5 (вольт) == 44.400 mAh
  • а він жеж, живлячи інвертор на 12 вольтах – 18.500 mAh

Див. How many times can I recharge a cell phone with a power bank?

Такі діла.

P.S. Як завжди – дякую Артему aka @artygan за допомогу в деяких питаннях)

Loading

AWS: RDS з IAM database authentication, EKS Pod Identities та Terraform
0 (0)

27 Червня 2024

Готуємось мігрувати базу даних нашого Backend API з DynamoDB до AWS RDS з PostgreSQL, і нарешті вирішив спробувати що ж таке AWS RDS IAM database authentication, який з’явився здається ще десь у 2021.

IAM database authentication, як, в принципі, можна здогадатись з назви, дозволяє нам виконувати аутентифікацію в RDS за допомогою AWS IAM, а не логіна-пароля з самого сервера баз даних.

Втім, авторизація – тобто перевірка які саме доступи є у юзера в базі/базах, залишаються за самим сервером БД, бо IAM нам тільки дасть доступ до самого інстансу RDS.

Тож що будемо робити:

  • спочатку руками спробуємо як працює RDS IAM database authentication, і як вона конфігуриться
  • потім перейдемо до автоматизації з Terraform, і заодно поглянемо на те, як працює AWS EKS Pod Identities
  • напишемо код на Python, який буде запускатись в Kubernetes Pod з SericeAccount та підключатись до RDS використовуючи RDS IAM database authentication
  • поговоримо про проблеми при використанні RDS IAM database authentication та автоматизації з Terraform

Тестую на RDS, який створюється для Grafana, тому подекуди будуть імена з “monitoring“/”grafana“.

Як працює RDS IAM database authentication?

Документація – IAM database authentication for MariaDB, MySQL, and PostgreSQL.

Загальна ідея – замість паролю до RDS використовується IAM-токен для IAM Role або IAM User, до яких підключено IAM Policy, яка описує ID Aurora-кластеру або RDS-інстансу та ім’я користувача.

Але, нажаль, на цьому роль IAM завершується, бо доступи та права в самому сервері баз даних створюються і керуються як і раніше, тобто через CREATE USER та GRANT PERMISSIONS.

IAM database authentication та Kubernetes ServiceAccount

Відносно Kubernetes Pod я, чесно кажучи, очікував трохи більшого, бо мені здавалось, що просто використовуючи IAM Role та Kubernetes ServiceAccount можна буде взагалі без паролю підключатись до RDS – як ми це робимо з доступом до інших ресурсів в AWS через AWS API.

Але з RDS схема виглядає трохи інакше:

  • створюємо інстанс RDS з параметром IAM authentication == true
  • створюємо IAM Role з IAM Policy
  • в PostgreSQL/MariaDB створюємо відповідного користувача, включаємо йому аутентифікацію через IAM
  • в Kubernetes створюємо ServiceAccount з цією роллю
  • підключаємо цей ServiceAccount до Kubernetes Pod
  • в Pod, використовуючи IAM Role з ServiceAccount, генеруємо IAM RDS Token для доступу для RDS
  • і вже з цим токеном підключаємось до серверу RDS

Давайте спочатку спробуємо руками, а потім глянемо, як це зробити з Terraform – бо там є свої нюанси.

RDS IAM authentication: перевірка

Отже, маємо вже створений RDS PostgreSQL з Password and IAM database authentication:

Для серверу вже маємо дефолтного master-юзера та пароль в SecretsManager – він знадобиться для додавання нового користувача.

Знаходимо ID інстансу – буде потрібен в IAM Policy:

Створення IAM Policy

Далі нам потрібна IAM Policy, яка буде дозволяти доступ юзеру до цього інстансу RDS.

Переходимо в IAM > Policy, створюємо нову політику, див. документацію Creating and using an IAM policy for IAM database access:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "rds-db:connect",
            "Resource": "arn:aws:rds-db:us-east-1:492***148:dbuser:db-UZM***3SA/db_test"
        }
    ]
}

Тут ми Allow виконати дію rds-db:connect до сервера бази даних в Resource використовуючи ім’я користувача db_test, і цього ж користувача db_test ми потім додамо з CREATE USER на самому сервері БД.

Зверніть увагу, що інстанс RDS вказується не як його ім’я – а саме як його ID – db-XXXYYYZZZ.

Зберігаємо політику:

Політику можемо підключити напряму до свого юзера AWS, або використати IAM Role.

З роллю спробуємо пізніше, коли будемо підключати Kubernetes Pod, а зараз для перевірки схеми в цілому давайте використаємо звичайного IAM User.

Знаходимо необхідного IAM User та додаємо пермішени:

Вибираємо Attach policies directly, знаходимо нашу IAM Policy:

Наступний крок – додати користувача в RDS.

PostgreSQL: створення database user

Документація – Creating a database account using IAM authentication.

Note: Make sure the specified database user name is the same as a resource in the IAM policy for IAM database access

Тобто при CREATE USER маємо вказати того самого db_test, який вказано в "Resource" нашої IAM Policy :

...
"Resource": "arn:aws:rds-db:us-east-1:492***148:dbuser:db-UZM***3SA/db_test"
...

Підключаємось з дефолтним юзером і паролем, які отримали при створенні сервера:

$ psql -h ops-monitoring-rds.***.us-east-1.rds.amazonaws.com -U master_user -d ops_monitoring_db

Створюємо нового користувача db_test, і підключаємо йому аутентифікацію через роль rds_iam:

ops_grafana_db=> CREATE USER db_test;
CREATE ROLE
ops_grafana_db=> GRANT rds_iam TO db_test;
GRANT ROLE

Для MariaDB це буде AWSAuthenticationPlugin.

Підключення з psql

Документація – Connecting to your DB instance using IAM authentication from the command line: AWS CLI and psql client.

Note: You cannot use a custom Route 53 DNS record instead of the DB instance endpoint to generate the authentication token.

Знаходимо URL ендпоінту серверу:

Задаємо змінну з адресою:

$ export RDSHOST="ops-monitoring-rds.***.us-east-1.rds.amazonaws.com"

З AWS CLI та командою aws rds generate-db-auth-token отримуємо токен – саме він і буде нашим паролем:

$ export PGPASSWORD="$(aws --profile work rds generate-db-auth-token --hostname $RDSHOST --port 5432 --region us-east-1 --username db_test)"

Перевіримо його:

$ echo $PGPASSWORD
ops-monitoring-rds.***.us-east-1.rds.amazonaws.com:5432/?Action=connect&DBUser=db_test&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=***%2F20240624%2Fus-east-1%2Frds-db%2Faws4_request&X-Amz-Date=20240624T142442Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&X-Amz-Security-Token=IQo***942

Підключаємось:

$ psql "host=$RDSHOST sslmode=require dbname=ops_grafana_db user=db_test password=$PGPASSWORD"
psql: error: connection to server at "ops-monitoring-rds.***.us-east-1.rds.amazonaws.com" (10.0.66.79), port 5432 failed: FATAL:  PAM authentication failed for user "db_test"

FATAL: PAM authentication failed for user “db_test”

В моєму випадку помилка виникла, бо я спершу генерив токен з “--region us-west-2“, а сервер знаходиться в us-east-1 (привіт, copy-paste з документації 🙂 ).

Тобто помилка виникає саме через помилки в налаштуваннях доступу – або в IAM Policy вказано інший username, або при CREATE USER вказано інше ім’я, або токен згенеровано для іншої IAM-ролі.

Перегенеримо токен, пробуємо ще раз:

$ psql "host=$RDSHOST sslmode=require dbname=ops_grafana_db user=db_test password=$PGPASSWORD"
psql (16.2, server 16.3)
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
Type "help" for help.

ops_grafana_db=> 
ops_grafana_db=> \dt
                        List of relations
 Schema |            Name             | Type  |      Owner       
--------+-----------------------------+-------+------------------
 public | alert                       | table | ops_grafana_user
 public | alert_configuration         | table | ops_grafana_user
 public | alert_configuration_history | table | ops_grafana_user
...

При чому password=$PGPASSWORD можна не вказувати – psql сам зчитає змінну PGPASSWORD, див. Environment Variables.

dbname=ops_grafana_db тут – бо сервер створюється для Grafana, і це її база.

Окей – це перевірили, працює.

Тепер час Kubernetes та автоматизації з Terraform – і тут наші пригоди тільки починаються.

Terraform та AWS EKS Pod Identity з IAM database authentication

Давайте глянемо, як ця схема буде працювати з Kubernetes Pod та ServiceAccount.

Детальніше про нову схему роботи з Pod ServiceAccounts та IAM писав в AWS: EKS Pod Identities – заміна IRSA? Спрощуємо менеджмент IAM доступів, але досі в production не використовував – як раз потестуємо як його взагалі готувати.

Отже, що нам потрібно:

  • IAM Role з IAM Policy
  • в Trusted Policy цієї IAM Role будемо мати pods.eks.amazonaws.com
  • роль додамо до кластеру через EKS IAM API
  • створимо Kubernetes Pod та ServiceAccount
  • в поді запустимо код на Python, який буде підключатись до RDS

Тобто, Kubernetes Pod для аутентифікації в AWS API буде використовувати IAM Role з Kubernetes ServiceAccount, потім, використовуючи цю роль, від AWS API отримає AWS RDS Token, і вже з цим токеном підключиться до RDS.

Створення AWS EKS Pod Identity з Terraform

Для AWS EKS Pod Identity є модуль eks-pod-identity, візьмемо його.

В Terraform описуємо aws_iam_policy_document з доступом до RDS:

data "aws_iam_policy_document" "monitoring_rds_policy" {
  statement {
    effect = "Allow"

    actions = [
      "rds-db:connect"
    ]
    resources = [
      "arn:aws:rds-db:us-east-1:${data.aws_caller_identity.current.account_id}:dbuser:${module.monitoring_rds.db_instance_resource_id}/test_user"
    ]
  }
}

Policy нова, і юзера використаємо теж нового – test_user.

В ${data.aws_caller_identity.current.account_id} ми маємо AWS Account ID:

data "aws_caller_identity" "current" {}

В ${module.monitoring_rds.db_instance_resource_id} – ID нашого RDS-інстансу, який створюється за допомогою модулю terraform-aws-modules/rds/aws з параметром iam_database_authentication_enabled:

module "monitoring_rds" {
  source  = "terraform-aws-modules/rds/aws"
  version = "~> 6.7.0"

  identifier = "${var.environment}-monitoring-rds"
  ...
  # DBName must begin with a letter and contain only alphanumeric characters
  db_name  = "${var.environment}_grafana_db"
  username = "${var.environment}_grafana_user"
  port     = 5432

  manage_master_user_password          = true
  manage_master_user_password_rotation = false

  iam_database_authentication_enabled = true
  ...
}

Далі з terraform-aws-modules/eks-pod-identity/aws описуємо EKS Pod Identity Association, де використовуємо aws_iam_policy_document.monitoring_rds_policy, який зробили вище:

module "grafana_pod_identity" {
  source  = "terraform-aws-modules/eks-pod-identity/aws"
  version = "~> 1.2.1"

  name = "${var.environment}-monitoring-rds-role"

  attach_custom_policy    = true
  source_policy_documents = [data.aws_iam_policy_document.monitoring_rds_policy.json]

  associations = {
    atlas-eks = {
      cluster_name    = data.aws_eks_cluster.eks.name
      namespace       = "${var.environment}-monitoring-ns"
      service_account = "eks-test-sa"
    }
  }
}

В namespace задаємо ім’я, в якому буде створено ServiceAccount для Pod, а в service_account – власне ім’я ServiceAccount.

data.aws_eks_cluster.eks.name отримується з data "aws_eks_cluster":

# get info about a cluster
data "aws_eks_cluster" "eks" {
  name = local.eks_name
}

Деплоїмо, і перевіряємо IAM:

Та Pod Identity associations в AWS EKS:

Тепер маємо IAM Role, до якої підключена IAM Policy, яка надає доступ юзеру test_user до RDS-інстансу з ID db-UZM***3SA, та маємо встановлений зв’язок між ServiceAccount з іменем eks-test-sa в Kubernetes-кластері та цією IAM-роллю.

Python, PostgreSQL та IAM database authentication

Що має відбуватись далі:

  • створимо Kubernetes Pod
  • створимо ServiceAccount з іменем eks-test-sa
  • напишемо код на Python, який буде:
    • використовуючи ServiceAccount та пов’язану з ним IAM Role підключатись до AWS API
    • отримає AWS RDS Token
    • з цим токеном підключиться до RDS

Знов заходимо на RDS з мастер-юзером, і створюємо нового користувача test_user (як вказано в IAM Policy) з роллю rds_iam:

ops_grafana_db=> CREATE USER test_user;
CREATE ROLE
ops_grafana_db=> GRANT rds_iam TO test_user;
GRANT ROLE
ops_grafana_db=> GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO test_user;
GRANT

Описуємо ServiceAccount eks-test-sa та Kubernetes Pod з цим ServiceAccount в namespace=ops-monitoring-ns:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: eks-test-sa
  namespace: ops-monitoring-ns
---
apiVersion: v1
kind: Pod
metadata:
  name: eks-test-pod
  namespace: ops-monitoring-ns
spec:
  containers:
    - name: ubuntu
      image: ubuntu
      command: ['sleep', '36000']
  restartPolicy: Never
  serviceAccountName: eks-test-sa

Деплоїмо:

$ kk apply -f eks-test-rds-irsa.yaml
serviceaccount/eks-test-sa created
pod/eks-test-pod created

Підключаємось в под:

$ kk exec -ti eks-test-pod -- bash
root@eks-test-pod:/# 

Встановлюємо python-boto3 для отримання токену та python3-psycopg2 для роботи з PostgreSQL:

root@eks-test-pod:/# apt update && apt -y install vim python3-boto3

Пишемо код:

#!/usr/bin/python3

import boto3
import psycopg2

DB_HOST="ops-monitoring-rds.***.us-east-1.rds.amazonaws.com"
DB_USER="test_user"
DB_REGION="us-east-1"
DB_NAME="ops_grafana_db"

client = boto3.client('rds')

# using Kubernetes Pod ServiceAccount's IAM Role generate another AWS IAM Token to access RDS
db_token = client.generate_db_auth_token(DBHostname=DB_HOST, Port=5432, DBUsername=DB_USER, Region=DB_REGION)

# connect to RDS using the token as a password
conn = psycopg2.connect(database=DB_NAME,
                        host=DB_HOST,
                        user=DB_USER,
                        password=db_token,
                        port="5432")

cursor = conn.cursor()

cursor.execute("SELECT * FROM dashboard_provisioning")

print(cursor.fetchone())

conn.close()

В принципі він досить простий – підключаємось до AWS, отримуємо токен, підключаємось до RDS.

Запускаємо, перевіряємо результат:

root@eks-test-pod:/# ./test-rds.py 
(1, 1, 'default', '/var/lib/grafana/dashboards/default/nodeexporter.json', 1719234200, 'c2ef5344baf3389f5238679cd1b0ca68')

Трохи про те, що саме відбувається “під капотом”:

  • Kubernetes Pod має ServiceAccount
  • ServiceAccount через Pod Identity associations пов’язаний з IAM Role ops-monitoring-rds-role
  • IAM Role ops-monitoring-rds-role має IAM Policy з Allow на rds-db:connect
  • Kubernetes Pod використовує IAM Role з ServiceAccount для аутентифікації та авторизації в AWS
  • після чого в Python з boto3 та client.generate_db_auth_token отримує RDS Token
  • і використовує його для підключення для PostgreSQL

На самому RDS ми вже маємо створеного юзера test_user з rds_iam та доступом до баз даних.

Про те, як саме працює Kubernetes ServiceAccounts та токени на рівні Kubernetes Pod див. в AWS: EKS, OpenID Connect та ServiceAccounts (тільки там описано ще без Pod Identity associations, але механізм той самий).

Виглядає, як робочий варіант – але залишився ще один нюанс.

Terraform та IAM RDS Authentication: складнощі

Вся наша схема з Terraform наче робоча – але ми руками створювали test_user та давали йому пермішени.

І тут ще один недолік схеми RDS та IAM database authentication – бо нам все одно потрібно створювати юзера в сервері БД.

А звідси випливає ще одна проблема – як це робити з Terraform?

Я не став вже витрачати на це час, бо нам в принципі це не дуже актуально, бо буде лише кілька юзерів і їх можна зробити руками, а поточну автоматизацію це не блокує.

Але з часом, коли проект виросте, то це питання все одно доведеться вирішувати.

Отже, які проблеми і варіанти вирішення маємо:

  • ми можемо створити PostgreSQL (чи MariaDB) юзерів прямо з коду Terraform використовуючи PostgreSQL provider, і виконавши local-exec або використавши resource "postgresql_grant"
    • див. приклади в AWS RDS IAM Authentication with Terraform та grant privileges and permissions to a role via terrafrom is not working
    • але для цього потрібен доступ до самого RDS, який знаходиться в приватній мережі VPC, а тому для CI/CD потрібен мережевий доступ до VPC, що, в принципі, можливо, якщо запускати GitHub Runners (в нашому випадку) в Kubernetes, чиї WorkerNodes мають доступ до приватних сабнетів – але зараз ми використовуємо Runners самого GitHub, і у них цього доступу нема
  • другий варіант – використовувати AWS Lambda, яка буде запускатись в VPC з доступом до RDS, і створювати юзерів

Обидів схеми цілком робочі, і колись, можливо, опишу реалізацію одного з них (скоріш за все другого).

Але на даний момент не бачу сенсу витрачати на це час.

Висновки

А висновки насправді трохи неоднозначні.

Сама ідея з RDS IAM database authentication виглядає дуже цікаво, але той факт, що токен для RDS і звичайний токен аутентифікації в AWS API для IAM Role являють собою різні сутності трохи ускладнює реалізацію, бо якби до RDS можна було конектитись просто використовуючи ServiceAccount та IAM Role – це дуже спростило б використання.

Крім того, чомусь очікував, що й авторизація буде робитись на рівні IAM – тобто, прямо в IAM Policy можна буде вказати хоча б бази даних, до яких будуть доступи. Але це залишається на рівні сервера БД.

Друга проблема полягає в тому, що нам все одно доводиться створювати юзера в RDS і видавати йому права, що знов-таки створює додаткові складнощі в автоматизації.

Втім, в цілому свою задачу RDS IAM database authentication виконує – нам дійсно не потрібно створювати якийсь Kubernetes Secret з паролем для бази даних і маунтити його до Kubernetes Pod, а достатньо підключити ServiceAccount, а отримання токену ми вже “перекладаємо на плечі девелоперів” – тобто виконуємо на рівні коду, а не Kubernetes.

І, думаю, ми все ж будемо цей механізм використовувати у нас в Production.

Loading

Kubernetes: контейнери та “загублений” SIGTERM
0 (0)

22 Червня 2024

Маємо API-сервіс в Kubernetes, який періодично видає 502, 503, 504 помилки.

Почав його дебажити, і виявив дивну штуку – в логах не було повідомлень про отриманий SIGTERM, а тому спочатку пішов розбиратись з Kubernetes – чому він його не відправляє?

The Issue

Отже, як це виглядає.

Маємо Kubernetes Pod:

$ kk get pod
NAME                          READY   STATUS    RESTARTS   AGE
fastapi-app-89d8c77bc-8qwl7   1/1     Running   0          38m

Читаємо його логи:

$ ktail fastapi-app-59554cddc5-lgj42
==> Attached to container [fastapi-app-59554cddc5-lgj42:fastapi-app]

Вбиваємо його:

$ kk delete pod -l app=fastapi-app
pod "fastapi-app-6cb6b46c4b-pffs2" deleted

І що ми бачимо в його логах? А нічого!

...
fastapi-app-6cb6b46c4b-9wqpf:fastapi-app [2024-06-22 11:13:27 +0000] [9] [INFO] Application startup complete.
==> Container left (terminated) [fastapi-app-6cb6b46c4b-pffs2:fastapi-app]
==> New container [fastapi-app-6cb6b46c4b-9qtvb:fastapi-app]
==> New container [fastapi-app-6cb6b46c4b-9qtvb:fastapi-app]
fastapi-app-6cb6b46c4b-9qtvb:fastapi-app [2024-06-22 11:14:15 +0000] [8] [INFO] Starting gunicorn 22.0.0
...
fastapi-app-6cb6b46c4b-9qtvb:fastapi-app [2024-06-22 11:14:16 +0000] [9] [INFO] Application startup complete.

Тут:

  1. сервіс запустився – “Application startup complete
  2. под вмер – “Container left
  3. новий под запустився – “New container” і “Starting gunicorn

А ось, як це має виглядати в нормальному випадку:

...
fastapi-app-59554cddc5-v7xq9:fastapi-app [2024-06-22 11:09:54 +0000] [8] [INFO] Waiting for application shutdown.
fastapi-app-59554cddc5-v7xq9:fastapi-app [2024-06-22 11:09:54 +0000] [8] [INFO] Application shutdown complete.
fastapi-app-59554cddc5-v7xq9:fastapi-app [2024-06-22 11:09:54 +0000] [8] [INFO] Finished server process [8]
fastapi-app-59554cddc5-v7xq9:fastapi-app [2024-06-22 11:09:54 +0000] [1] [ERROR] Worker (pid:8) was sent SIGTERM!
fastapi-app-59554cddc5-v7xq9:fastapi-app [2024-06-22 11:09:54 +0000] [1] [INFO] Shutting down: Master
==> Container left (terminated) [fastapi-app-59554cddc5-v7xq9:fastapi-app]

Тобто тут Gunicorn отримує SIGTERM, і коректно завершує свою роботу.

Якого біса?

Давайте розбиратись.

Kubernetes та процес Pod termination

Як відбувається процес зупинки подів?

Детальніше писав у Kubernetes: NGINX/PHP-FPM graceful shutdown – избавляемся от 502 ошибок, тут дуже коротко:

  1. виконуємо kubectl delete pod
  2. kubelet на відповідній WorkerNode отримує від API-серверу команду на зупинку поду
  3. kubelet відправляє контейнеру сигнал SIGTERM – до процесу з PID 1, тобто першому процесу, який запущено при створенні контейнеру
  4. якщо контейнер не зупинився протягом terminationGracePeriodSeconds – то відправляється SIGKILL

Тобто наш процес Gunicorn має отримати SIGTERM, записати це в лог, і почати запиняти свої воркери.

Натомість він не отримує нічого і просто вмирає.

А чому?

PID 1 та SIGTERM в контейнерах

Глянемо, що у нас з процесами в цьому поді :

root@fastapi-app-6cb6b46c4b-9qtvb:/app# ps aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.0  0.0   2576   948 ?        Ss   11:14   0:00 /bin/sh -c gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app
root           8  0.0  1.3  31360 27192 ?        S    11:14   0:00 /usr/local/bin/python /usr/local/bin/gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app
root           9  0.2  2.4 287668 49208 ?        Sl   11:14   0:04 /usr/local/bin/python /usr/local/bin/gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app

І бачимо, що у нас тут PID 1 – це процес /bin/sh, який через -c запускає gunicorn.

Тепер давайте в поді запустимо strace, і подивимось які сигнали він отримує:

root@fastapi-app-6cb6b46c4b-9pd7r:/app# strace -p 1
strace: Process 1 attached
wait4(-1, 

Виконуємо kubect delete pod – але додамо time, аби заміряти час на виконання команди:

$ time kk delete pod fastapi-app-6cb6b46c4b-9pd7r
pod "fastapi-app-6cb6b46c4b-9pd7r" deleted

real    0m32.222s

32 секунди…

А що в strace?

root@fastapi-app-6cb6b46c4b-9pd7r:/app# strace -p 1
strace: Process 1 attached
wait4(-1, 0x7ffe7a390a3c, 0, NULL)      = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
--- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=0, si_uid=0} ---
wait4(-1,  <unfinished ...>)            = ?
command terminated with exit code 137

Отже, що тут відбувалось:

  1. kubelet відправив сигнал SIGTERM процесу з PID 1 – SIGTERM {si_signo=SIGTERM} – і PID 1 мав би передати цей процес своїм дочірнім процесам, зупинити їх, а потім завершитись сам
  2. але процес не завершився – і kubelet почекав дефолтні 30 секунд, аби под коректно завершив свою роботу – див. Pod phase
  3. після чого вбив контейнер, і процес завершився з “terminated with exit code 137

Зазвичай код 137 – це про OutOfMemory Killer, коли процес вбивається з SIGKILL, але в нашому випадку OOMKill не було – а просто відбувся SIGKILL через те, що процеси в поді не завершились вчасно.

Добре – а куди подівся наш SIGTERM?

Давайте виконаємо сигнали напряму з контейнеру – спочатку kill -s 15, SIGTERM, потім kill -s 9, SIGKILL:

root@fastapi-app-6cb6b46c4b-r9fnq:/app# kill -s 15  1
root@fastapi-app-6cb6b46c4b-r9fnq:/app# ps aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.0  0.0   2576   920 ?        Ss   12:02   0:00 /bin/sh -c gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app
root           7  0.0  1.4  31852 27644 ?        S    12:02   0:00 /usr/local/bin/python /usr/local/bin/gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app
...
root@fastapi-app-6cb6b46c4b-r9fnq:/app# kill -s 9  1
root@fastapi-app-6cb6b46c4b-r9fnq:/app# ps aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.0  0.0   2576   920 ?        Ss   12:02   0:00 /bin/sh -c gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app
root           7  0.0  1.4  31852 27644 ?        S    12:02   0:00 /usr/local/bin/python /usr/local/bin/gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app

Шо? Як?

Чому ігнорується SIGTERM? А тим більш SIGKILL, який має бути “non ignorant signal” – див. man signal:

The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.

Linux kill() та PID 1

Бо PID 1 в Linux є особливим процесом, бо це перший процес, який запускається системою, і він має бути “захищений” від “випадкового вбивства”.

Якщо ми глянемо на man kill, то там це явно вказано, а також сказано про хендлери сигналів до процесу:

The only signals that can be sent to process ID 1, the init process, are those for which init has explicitly installed signal handlers. This is done to assure the system is not brought down accidentally

Перевірити які хендлери є у нашого процесу – тобто, які сигнали наш процес може перехопити і обробити – можна з файлу /proc/1/status:

root@fastapi-app-6cb6b46c4b-r9fnq:/app# cat /proc/1/status | grep SigCgt
SigCgt: 0000000000010002

SigCgt – це сигнали, які процес може перехопити і обробити сам. Решта будуть або проігноровані, або опрацьовані з SIG_DFL handler, а SIG_DFL handler ігнорує сигнали для PID 1, у якого нема власного хендлера.

Спитаємо ChatGPT які саме це сигнали:

(взагалі можна і самому перевести, якщо цікаво – див. наприклад How can I check what signals a process is listening to? або How to read bitmask for the signals)

Отже, що ми маємо:

  • процес /bin/sh з PID 1
  • PID 1 є спеціальним процесом
  • перевірка PID 1 вказує нам, що він “розпізнає” тільки сигнали SIGHUP і SIGCHLD
  • SIGTERM та SIGKILL будуть ним проігноровані

Але як жеж тоді зупиняється контейнер?

Docker stop та сигнали

Процес зупинки контейнера в Docker (або Containerd) не відрізняється від того, як це в Kubernetes, бо по факту kubelet просто передає команди на container runtime. В AWS Kubernetes це зараз containerd.

Але задля простоти давайте виконаємо це локально з Docker.

Отже, запускаємо контейнер з того самого образу, який тестили в Kubernetes:

$ docker run --name test-app 492***148.dkr.ecr.us-east-1.amazonaws.com/fastapi-app-test:entry-2     
[2024-06-22 14:15:03 +0000] [7] [INFO] Starting gunicorn 22.0.0
[2024-06-22 14:15:03 +0000] [7] [INFO] Listening at: http://0.0.0.0:80 (7)
[2024-06-22 14:15:03 +0000] [7] [INFO] Using worker: uvicorn.workers.UvicornWorker
[2024-06-22 14:15:03 +0000] [8] [INFO] Booting worker with pid: 8
[2024-06-22 14:15:03 +0000] [8] [INFO] Started server process [8]
[2024-06-22 14:15:03 +0000] [8] [INFO] Waiting for application startup.
[2024-06-22 14:15:03 +0000] [8] [INFO] Application startup complete.

Пробуємо його зупинити через відправку SIGKILL до PID 1 – нічого не змінилося, він ігнорує сигнал:

$ docker exec -ti test-app sh -c "kill -9 1"
$ docker ps
CONTAINER ID   IMAGE                                                                   COMMAND                  CREATED              STATUS              PORTS     NAMES
99bae6d55be2   492***148.dkr.ecr.us-east-1.amazonaws.com/fastapi-app-test:entry-2   "/bin/sh -c 'gunicor…"   About a minute ago   Up About a minute             test-app

Пробуємо зупинити з docker stop – і знов дивимось на час:

$ time docker stop test-app
test-app

real    0m10.234s

І статус контейнера:

$ docker ps -a
CONTAINER ID   IMAGE                                                                        COMMAND                  CREATED              STATUS                        PORTS                                                                                                                                  NAMES
cab29916f6ba   492***148.dkr.ecr.us-east-1.amazonaws.com/fastapi-app-test:entry-2        "/bin/sh -c 'gunicor…"   About a minute ago   Exited (137) 52 seconds ago

Той жеж код 137, тобто контейнер зупинився з SIGKILL.

Але як, якщо сигнал відправляється до PID 1, котрий його ігнорує?

Я не знайшов цього в документації до docker kill, але ми можемо вбити процеси контейнеру двома шляхами:

  1. вбити всі дочірні процеси в самому контейнері – і тоді parent (PID 1) вмре сам
  2. вбити всю групу процесів на хості через їхній SID (Session ID) – що знову-таки призведе до того, що PID 1 проігнорує сигнал, але вмре сам, бо померли всі його дочірні процеси

Ще раз глянемо на процеси в контейнері:

root@cddcaa561e1d:/app# ps aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.0  0.0   2576  1408 ?        Ss   15:58   0:00 /bin/sh -c gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app
root           7  0.1  0.0  31356 26388 ?        S    15:58   0:00 /usr/local/bin/python /usr/local/bin/gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app
root           8  0.5  0.1  59628 47452 ?        S    15:58   0:00 /usr/local/bin/python /usr/local/bin/gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app

root@cddcaa561e1d:/app# pstree -a
sh -c gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app
  └─gunicorn /usr/local/bin/gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app
      └─gunicorn /usr/local/bin/gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app

Вбити PID 1 ми не можемо, бо він нас ігнорує – але ми можемо вбити PID 7!

Тоді він за собою вб’є PID 8, бо це його дочірній процес, а коли PID 1 виявить, що більше нема його дочірніх процесів – то він вмре сам, і контейнер зупиниться:

root@cddcaa561e1d:/app# kill 7

І логи контейнеру:

...
[2024-06-22 16:02:54 +0000] [7] [INFO] Handling signal: term
[2024-06-22 16:02:54 +0000] [8] [INFO] Shutting down
[2024-06-22 16:02:54 +0000] [8] [INFO] Error while closing socket [Errno 9] Bad file descriptor
[2024-06-22 16:02:54 +0000] [8] [INFO] Waiting for application shutdown.
[2024-06-22 16:02:54 +0000] [8] [INFO] Application shutdown complete.
[2024-06-22 16:02:54 +0000] [8] [INFO] Finished server process [8]
[2024-06-22 16:02:54 +0000] [7] [ERROR] Worker (pid:8) was sent SIGTERM!
[2024-06-22 16:02:54 +0000] [7] [INFO] Shutting down: Master

Але так як поди/контейнери вмирають з кодом 137, то вони були вбиті з SIGKILL, бо коли Docker або інший container runtime не може зупинити процес з PID 1 з SIGKILL – то він відправляє SIGKILL всім процесам в контейнері.

Тобто:

  1. спочатку відправляється SIGTEM до PID 1
  2. через 10 секунд відправляється SIGKILL до PID 1
  3. якщо і це не допомогло – то SIGKILL відправляється всім процесам в контейнері

Наприклад, це можна зробити передавши Session ID (SID) команді kill.

Знаходимо основний процес контейнеру:

$ docker inspect --format '{{ .State.Pid }}' test-app
629353

Виконуємо ps j -A:

$ ps j -A      
   PPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND
 ...
 629333  629353  629353  629353 ?             -1 Ss       0   0:00 /bin/sh -c gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app
 629353  629374  629353  629353 ?             -1 S        0   0:00 /usr/local/bin/python /usr/local/bin/gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app
 629374  629375  629353  629353 ?             -1 S        0   0:00 /usr/local/bin/python /usr/local/bin/gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app

Бачимо наш SID – 629353.

І вбиваємо всю групу:

$ sudo kill -9 -- -629353

Окей.

Це все добре і дуже цікаво.

Але можна обійтися без цих костилів?

“Правильний” запуск процесів в контейнері

Врешті-решт, давайте глянемо на наш Dockerfile:

FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENTRYPOINT gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app

Дивимось документацію Docker – Shell and exec form:

INSTRUCTION [“executable”,”param1″,”param2″] (exec form)
INSTRUCTION command param1 param2 (shell form)

Отже, в нашому випадку використовується як раз shell form – і в результаті в ролі PID 1 маємо саме /bin/sh, який через -c викликає Gunicorn.

Якщо ж ми перепишемо в exec form:

FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENTRYPOINT ["gunicorn", "-w", "1", "-k", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:80", "app:app"]

І запустимо контейнер з такого образу, то будемо мати тільки Gunicorn процеси:

root@e6087d52350d:/app# ps aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.6  0.0  31852 27104 ?        Ss   16:13   0:00 /usr/local/bin/python /usr/local/bin/gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app
root           7  2.4  0.1  59636 47556 ?        S    16:13   0:00 /usr/local/bin/python /usr/local/bin/gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app

Які вже нормально хендлять сигнали SIGTERM:

root@e6087d52350d:/app# cat /proc/1/status | grep SigCgt
SigCgt: 0000000008314a07

І тепер, якщо ми відправимо SIGTERM до PID 1 – то контейнер нормально завершить свою роботу:

root@e6087d52350d:/app# kill 1

І його логи:

...
[2024-06-22 16:17:20 +0000] [1] [INFO] Handling signal: term
[2024-06-22 16:17:20 +0000] [7] [INFO] Shutting down
[2024-06-22 16:17:20 +0000] [7] [INFO] Error while closing socket [Errno 9] Bad file descriptor
[2024-06-22 16:17:20 +0000] [7] [INFO] Waiting for application shutdown.
[2024-06-22 16:17:20 +0000] [7] [INFO] Application shutdown complete.
[2024-06-22 16:17:20 +0000] [7] [INFO] Finished server process [7]
[2024-06-22 16:17:20 +0000] [1] [ERROR] Worker (pid:7) was sent SIGTERM!
[2024-06-22 16:17:21 +0000] [1] [INFO] Shutting down: Master

Ну і поди в Kubernetes тепер будуть зупинятись нормально – і швидко, бо не будуть чекати grace period.

Корисні посилання

Loading

Kubernetes: моніторинг Events з kubectl та Grafana Loki
0 (0)

21 Червня 2024

В Kubernetes окрім метрик та логів з контейнерів ми маємо змогу отримувати інформацію про роботу компонентів за допомогою Kubernetes Events.

В евентах зазвичай зберігається інформація про статус подів (створення, Evict, kill, ready або not-ready статус подів), про WorkerNodes (статус серверів), про роботу Kubernetes Scheduler (неможливість запуску поду, тощо).

Типи Kubernetes Events

В цілому, всі ці евенти можна поділити на такі типи:

  • Failed events: коли виникає проблема з маніфестом або образом, з якого потрібно створити контейнер (ImagePullBackOff, CrashLoopBackOff)
  • Eviction events: коли под видаляється, бо на WorkerNode мало ресурсів (Node-pressure Eviction), або ноду треба видалити, і автоскейлер (наприклад, Karpenter) виконує node drain (API-initiated Eviction)
  • Scheduler events: проблеми с запуском поду на WorkerNode, наприклад – коли Scheduler не може знайти ноду з достатніми ресурсами, щоб задовольнити Pod requests
  • Volume events: проблеми з підключенням PersistentVolume до поду (FailedAttachVolume, FailedMount)
  • Node events: проблеми в роботі WorkerNodes (NodeNotReady)

Kubernetes Events та kubectl

Отримати евенти можемо просто з kubectl – або зробивши kubectl describe pod <POD_NAME> чи kubectl decsribe deploy <DEPLOY_NAME>:

Або з kubectl get events, якому можна додати параметр --watch:

Також існує цікавий плагін podevents, який додає час евенту.

Встановити можна з krew:

$ kubectl krew install podevents

І запускаємо, передавши ім’я поду:

Kubernetes Events та Grafana Loki

Для роботи з евентами є багато рішень:

  • sloop – активний, система візуалізації евентів з можливістю фільтрації та пошуку
  • kspan – активний, створює OpenTelemetry Spans з евентів, які потім можна перевіряти в системах типу Jaeger
  • kubernetes-event-exporter – активний, вміє відправляти евенти, мабуть, в усе, що взагалі існує – і AWS SQS/SNS, і Opsgenie, і Slack, і Loki – можливо, він буде наступний в моєму кластері, коли поточне рішення стане недостатнім
  • Grafana Agent (Grafana Alloy) – теж вміє працювати з евентами, і писати їх у вигляді логів в Loki
  • eventrouter – в архіві з 2022
  • kubewatch – в архіві з 2022

Але, як на мене, то простіше всього їх мати у вигляді логів, і потім з Loki RecordingRules створювати метрики, а з них графіки в Grafana та/або алерти.

Для цього є дуже проста система max-rocket-internet/k8s-event-logger, яка слухає Kubernetes API, отримує всі евенти, і записує їх у вигляді лога в JSON.

Встановлюється з Helm-чарту:

$ helm repo add deliveryhero https://charts.deliveryhero.io/
$ helm -n monitoring install k8s-event-logger deliveryhero/k8s-event-logger

Створить Pod, який власне і буде читати events:

$ kubectl -n ops-monitoring-ns get pods -l "app.kubernetes.io/name=k8s-event-logger"
NAME                                READY   STATUS    RESTARTS   AGE
k8s-event-logger-5b548d6cc4-r8wkl   1/1     Running   0          68s

І писати їх у свій output:

$ kubectl -n ops-monitoring-ns logs -l "app.kubernetes.io/name=k8s-event-logger"
{"metadata":{"name":"backend-api-deployment-7fdfbb755-tjv2j.17daa9e0264e6139","namespace":"prod-backend-api-ns","uid":"1fa06477-62c9-4324-8823-7f2801fc26af","resourceVersion":"110778929","creationTimestamp":"2024-06-20T08:43:07Z","managedFields":[{"manager":"kubelet","operation":"Update","apiVersion":"v1","time":"2024-06-20T08:43:07Z",
...

Який потім попаде в Promtail, а звідти в Loki:

В цілому, на цьому і все.

Тепер, маючи історію евентів, буде набагато простіше дебажити якісь проблеми з подами або нодами.

Loading

AWS: Karpenter та SSH для Kubernetes WorkerNodes
0 (0)

19 Червня 2024

Маємо AWS EKS кластер з WorkerNodes/EC2, які створюються за допомогою Karpenter.

Процес створення інфраструктури, кластеру та запуск Karpenter описаний у попередніх постах:

Чого прям ну дуже не вистачає в цій системі – це доступу до серверів по SSH, без якого почуваєшся… Ну, наче DevOps, а не Infrastructure Engineer. Короче – доступ по SSH іноді прям треба, але – сюрпрайз – Karpenter з коробки не дає можливості додати ключ на ноди, які він менеджить.

Хоча, здавалося б – в чому проблема в EC2NodeClass передати ключ, як це робиться в Terraform resource "aws_instance" з параметром key_name?

Але – ОК. Нема, то й нема. Можливо, додадуть пізніше.

Натомість в документації Can I add SSH keys to a NodePool? пропонується використати або AWS Systems Manager Session Manager, або AWS EC2 Instance Connect, або “the old school way” – додати публічну частину ключа через AWS EC2 User Data, і підключатись через bastion-хост або VPN.

Тож що будемо робити сьогодні – по черзі спробуємо всі три рішення, спочатку кожне будемо робити руками, потім дивитись як його додати в нашу автоматизацію, і потім вирішимо який варіант буде найпростішим.

Варіант 1: AWS Systems Manager Session Manager та SSH на EC2

AWS Systems Manager Session Manager використовується для менеджменту EC2-інстансів. Взагалі він вміє досить багато, наприклад – слідкувати за патчами і апдейтами для пакетів, які встановлені на інстансах.

Зараз він нас цікавить тільки як система, яка дозволить виконати SSH на Kubernetes WorkerNode.

Для роботи потребує SSM-агента, який по дефолту є на всіх інстансах з Amazon Linux AMI.

Знаходимо ноди, які створені Kaprneter (у нас є окрема лейбла для них):

$ kubectl get node -l created-by=karpenter
NAME                          STATUS   ROLES    AGE     VERSION
ip-10-0-34-239.ec2.internal   Ready    <none>   21h     v1.28.8-eks-ae9a62a
ip-10-0-35-100.ec2.internal   Ready    <none>   9m28s   v1.28.8-eks-ae9a62a
ip-10-0-39-0.ec2.internal     Ready    <none>   78m     v1.28.8-eks-ae9a62a
...

Отримуємо Instance ID:

$ kubectl get node ip-10-0-34-239.ec2.internal -o json | jq -r ".spec.providerID" | cut -d \/ -f5 
i-011b1c0b5857b0d92

AWS CLI: TargetNotConnected when calling the StartSession operation

Пробуємо підключитись – і отримуємо помилку “TargetNotConnected“:

$ aws --profile work ssm start-session --target i-011b1c0b5857b0d92

An error occurred (TargetNotConnected) when calling the StartSession operation: i-011b1c0b5857b0d92 is not connected.

Або через AWS Console:

Але і тут маємо помилку підключення – “SSM Agent is not online“:

Помилка виникає через те, що:

  • або в IAM-ролі, яка підключена до інстансу, нема дозволу на SSM
  • або EC2 запущено в приватній мережі, і агент не може підключитись до зовнішнього ендпоінту

SessionManager та політики для IAM

Перевіряємо – знаходимо IAM Role:

І підключені до неї політики – про SSM нема нічого:

Редагуємо політику – поки що руками, потім зробимо в коді Terraform:

Підключаємо AmazonSSMManagedInstanceCore:

І за хвилину-дві пробуємо ще раз:

SessionManager та VPC Endpoint

Інша можлива причина проблем підключення SSM-агенту до AWS – нема доступу з інстансу до ендпоінтів SSM:

  • ssm.region.amazonaws.com
  • ssmmessages.region.amazonaws.com
  • ec2messages.region.amazonaws.com

Якщо сабнет приватний, і має ліміти на зовнішні підключення – то можливо треба створити VPC Endpoint для SSM.

Див. SSM Agent is not online та Troubleshooting Session Manager.

AWS CLI: SessionManagerPlugin is not found

Але після фіксу IAM при підключенні з робочої машини з AWS CLI маємо помилку “SessionManagerPlugin is not found“:

$ aws --profile work ssm start-session --target i-011b1c0b5857b0d92

SessionManagerPlugin is not found. Please refer to SessionManager Documentation here: http://docs.aws.amazon.com/console/systems-manager/session-manager-plugin-not-found

Встановлюємо його локально – див. документацію Install the Session Manager plugin for the AWS CLI.

Для Arch Linux є пакет aws-session-manager-plugin в AUR:

$ yay -S aws-session-manager-plugin

І тепер можемо підключитись:

$ aws --profile work ssm start-session --target i-011b1c0b5857b0d92

Starting session with SessionId: arseny-33ahofrlx7bwlecul2mkvq46gy
sh-4.2$ 

Залишилось додати це в автоматизацію.

Terraform: EKS module та додавання IAM Policy

Для модулю EKS політику можемо додати через параметр iam_role_additional_policies – див. node_groups.tf і в прикладах AWS EKS Terraform module:

В модулі версії 20.0 ім’я параметру змінилось – iam_role_additional_policies => node_iam_role_additional_policies, але у нас поки що версія 19.21.0, і роль додається таким чином:

...
module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 19.21.0"

  cluster_name    = local.env_name
  cluster_version = var.eks_version
  ...
  vpc_id                   = local.vpc_out.vpc_id
  subnet_ids               = data.aws_subnets.private.ids
  control_plane_subnet_ids = data.aws_subnets.intra.ids

  manage_aws_auth_configmap = true

  eks_managed_node_groups = {
      ...
      # allow SSM
      iam_role_additional_policies = {
        AmazonSSMManagedInstanceCore = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
      }
...

Відключаємо те, що робили руками, деплоїмо Terraform, перевіряємо – політика додана:

І підключення працює:

$ aws --profile work ssm start-session --target i-011b1c0b5857b0d92

Starting session with SessionId: arseny-pt7d44xp6ibvqcezj2oqjaxv5q
sh-4.2$ bash
[ssm-user@ip-10-0-34-239 bin]$ pwd
/usr/bin

Варіант 2: AWS EC2 Instance Connect та SSH на EC2

Інший варіант підключення – через EC2 Instance Connect. Документація – Connect to your Linux instance with EC2 Instance Connect.

Також потребує агента, який також по дефолту є на Amazon Linux.

Для інстансів в приватних мережах для підключення потребує EC2 Instance Connect VPC Endpoint.

SecurityGroup та SSH

Для Instance Connect через ендпоінт потрібен доступ до порта 22, SSH (на відміну від SSM, який відкриває підключення через самого агента).

Відкриваємо порт для всіх адрес у VPC:

EC2 Instance Connect VPC Endpoint

Переходимо в VPC > Endpoints, створюємо ендпоінт:

Вибираємо тип EC2 Instance Connect Endpoint, саму VPC, та SecurityGroup:

Вибираємо Subnet – у нас більшість ресурсів в us-east-1a, тому візьмемо її, аби не ганяти зайвий cross-AvailabilityZone трафік (див. AWS: Cost optimization – обзор расходов на сервисы и стоимость трафика в AWS):

Кілька хвилин чекаємо статус Active:

І підключаємось з AWS CLI вказуючи --connection-type eice, бо інстанси в приватній мережі:

$ aws --profile work ec2-instance-connect ssh --instance-id i-011b1c0b5857b0d92 --connection-type eice
...
[ec2-user@ip-10-0-34-239 ~]$ 

Terraform: EC2 Instance Connect, EKS та VPC

Для Terraform тут буде потрібно в модулі EKS додавати node_security_group_additional_rules для доступу по SSH, і для VPC створювати EC2 Instance Connect Endpoint, бо у нас VPC та EKS створюються окремо.

...
module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 19.21.0"

  cluster_name    = local.env_name
  cluster_version = var.eks_version

  ...
      # allow SSM
      iam_role_additional_policies = {
        AmazonSSMManagedInstanceCore = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
      }
      ...
  }

  node_security_group_name    = "${local.env_name}-node-sg"
  cluster_security_group_name = "${local.env_name}-cluster-sg"

  # to use with EC2 Instance Connect
  node_security_group_additional_rules = {
    ingress_ssh_vpc = {
      description = "SSH from VPC"
      protocol    = "tcp"
      from_port   = 22
      to_port     = 22
      cidr_blocks      = [local.vpc_out.vpc_cidr]
      type        = "ingress"
    }
  }

  node_security_group_tags = {
    "karpenter.sh/discovery" = local.env_name
  }
  ...
}
...

Якщо створювали руками, як описано вище – то з SecurityGroup видаляємо правило з SSH, і деплоїмо з Terraform.

Для VPC EC2 Ednpoint я не знайшов, як це зробити через модуль Антона Бабенко terraform-aws-modules/vpc, але можна зробити окремим ресурсом через aws_ec2_instance_connect_endpoint:

resource "aws_ec2_instance_connect_endpoint" "example" {
  subnet_id = module.vpc.private_subnets[0]
  security_group_ids = ["sg-0b70cfd6019c635af"]
}

Втім, тут треба передавати SecurityGroup ID з кластеру, а кластер у нас створюється після VPC, тому виникає проблема “куриця-яйце”.

Взагалі з Instance Connect виглядає якось трохи більш складно, чим з SSM, бо більше змін в коді, ще й в різних модулях.

Втім – варіант робочий, і якщо ваша автоматизація дозволяє – то можна використовувати його.

Варіант 3: дідовський спосіб з SSH Public Key через EC2 User Data

Ну і самий старий і, можливо, простий варіант – це самому створити SSH-ключ, і додавати його публічну частину на EC2 при створені інстансу.

З недоліків тут те, що додавати таким чином багато ключів буде складно, та й взагалі EC2 User Data іноді може вилізти боком, але якщо потрібно додати тільки один ключ, якийсь “супер-адмін” на крайній випадок – то цілком валідний варіант.

Тим більш, якщо у вас є VPN до VPC (див.Pritunl: запуск VPN в AWS на EC2 з Terraform) – то підключення буде ще простішим.

Створюємо ключ:

$ ssh-keygen 
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/setevoy/.ssh/id_ed25519): /home/setevoy/.ssh/atlas-eks-ec2
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/setevoy/.ssh/atlas-eks-ec2
Your public key has been saved in /home/setevoy/.ssh/atlas-eks-ec2.pub
...

Публічну частину можемо зберігати в репозиторії – копіюємо її:

$ cat ~/.ssh/atlas-eks-ec2.pub 
ssh-ed25519 AAA***VMO setevoy@setevoy-wrk-laptop

Далі трохи костилів: EC2NodeClass у нас створюється з Terraform через ресурс kubectl_manifest. Найпростіший варіант, який поки що прийшов в голову – це додати публічний ключ в variables, і потім використати в kubectl_manifest.

Пізніше мабуть перенесу такі ресурси в окремий Helm-чарт, і зроблю більш красиво.

Поки що створюємо нову змінну:

variable "karpenter_nodeclass_ssh" {
  type        = string
  default = "ssh-ed25519 AAA***VMO setevoy@setevoy-wrk-laptop"
  description = "SSH Public key for EC2 created by Karpenter"
}

В конфіг EC2NodeClass додаємо spec.userData:

resource "kubectl_manifest" "karpenter_node_class" {
  yaml_body = <<-YAML
    apiVersion: karpenter.k8s.aws/v1beta1
    kind: EC2NodeClass
    metadata:
      name: default
    spec:
      amiFamily: AL2
      role: ${module.eks.eks_managed_node_groups["${local.env_name_short}-default"].iam_role_name}
      subnetSelectorTerms:
        - tags:
            karpenter.sh/discovery: "atlas-vpc-${var.environment}-private"
      securityGroupSelectorTerms:
        - tags:
            karpenter.sh/discovery: ${local.env_name}
      tags:
        Name: ${local.env_name_short}-karpenter
        environment: ${var.environment}
        created-by: "karpneter"
        karpenter.sh/discovery: ${local.env_name}
      userData: |
        #!/bin/bash
        mkdir -p ~ec2-user/.ssh/
        touch ~ec2-user/.ssh/authorized_keys
        echo "${var.karpenter_nodeclass_ssh}" >> ~ec2-user/.ssh/authorized_keys
        chmod -R go-w ~ec2-user/.ssh/authorized_keys
        chown -R ec2-user ~ec2-user/.ssh       
  YAML

  depends_on = [
    helm_release.karpenter
  ]
}

Якщо використовується не Amazon Linux – то міняємо ec2-user на потрібного.

Майте на увазі, що зміни в EC2NodeClass призведуть до перестворення всіх інстансів, і що ваші сервіси сконфігуровані для стабільної роботи, див. Kubernetes: забезпечення High Availability для Pods.

Деплоїмо, перевіряємо:

$ kk get ec2nodeclass -o yaml
...
    userData: #!/bin/bash\nmkdir -p ~ec2-user/.ssh/\ntouch ~ec2-user/.ssh/authorized_keys\necho
      \"ssh-ed25519 AAA***VMO setevoy@setevoy-wrk-laptop\" >> ~ec2-user/.ssh/authorized_keys\nchmod -R go-w
      ~ec2-user/.ssh/authorized_keys\nchown -R ec2-user ~ec2-user/.ssh       \n
...

Чекаємо, коли Karpenter заскейлить якусь нову WorkerNode, і пробуємо SSH:

$ ssh -i ~/.ssh/hOS/atlas-eks-ec2 [email protected]
...
[ec2-user@ip-10-0-39-73 ~]$ 

Готово.

Висновки

  • AWS SessionManager: виглядає як найпростіший варіант з точку зору автоматизації, рекомендований самим AWS, але треба подумати, як робити той же scp через нього (хоча це начебто можливо через додаткові костилі – див. .SSH and SCP with AWS SSM)
  • AWS EC2 Instance Connect: прикольна фіча від Амазону, але якось більш геморно в автоматизації, тому не наш варіант
  • “дідовський” SSH: ну, старе – перевірене 🙂 але я не дуже люблю User Data, бо іноді може призвести до проблем з запуском інстансів; втім – теж простий з точки зору автоматиазції, і дає звичний SSH без додаткових тєлодвіженій

Loading

Pritunl: запуск VPN в AWS на EC2 з Terraform
0 (0)

31 Травня 2024

Колись вже трохи писав про Pritunl – Pritunl: запуск VPN в Kubernetes.

Повернемось до цієї теми ще раз, але цього разу на EC2 в AWS, без Kubernetes.

Отже, що треба – це запустити якийсь VPN-сервіс для проекту, або мати доступ до всяких Kubernetes API/Kubernetes WorkerNodes/AWS RDS у приватних мережах.

Вибір тут, в принципі, є – і AWS VPN, і ванільний OpenVPN, і багато іншого.

Але я Pritunl користувався вже в декількох проектах, він має приємний інтерфейс, основні можливості VPN доступні у Free версії – тож вай нот?

Що таке Pritunl

Фактично, Pritunl – це обгортка над звичайним OpenVPN сервером. Повністю сумісний, використовує однакові конфіги, і так далі.

Вміє в інтеграцію з AWS VPC – https://pritunl.com/vpc, але мені не дуже хочеться, щоб хтось автоматом міняв таблиці маршрутизації.

Сетап мережі в AWS у нас доволі простий, і поки що все можна менеджити самому – більше контролю, більше розуміння, що може піти не так.

Ну і плюс ця інтеграція наче доступна тільки в Enerprize – Pritunl Pricing.

У Pritunl дві основні концепції – Organization  та Server:

  • Server описує конфіг для OpenVPN – порти, роути, DNS
  • Organization описує юзерів
  • Organization підключається до Server

Далі, юзер завантажує файл .ovpn, і підключається будь-яким VPN-клієнтом. Наскільки пам’ятаю, навіть дефолтний клієнт на macOS працював з ним без проблем.

Pritunl та Terraform

На попередньому проекті ми мали Pritunl в Kubernetes, але мені ця ідея якось не дуже подобається, бо, імхо, VPN має бути окремим сервісом.

Якщо говорити про Terraform, то є цікавий Pritunl Provider – але йому потрібен API ключ, який в Pritunl доступний тільки в Enerprize.

Ще є готовий код з Terraform тут – Pritunl VPN, але мені якось простіше підняти власну EC2 у власній VPC.

І ще нагуглив такий готовий модуль – AWS VPN (Pritunl) Terraform Module, виглядає наче робочим рішенням.

Проте будемо робити більш дідовським методом:

  • є звичайна AWS VPC з кількома приватними сабнетами
  • у публічному сабнеті з Terraform запустимо звичайний EC2
  • через AWS EC2 user_data встановимо і запустимо Pritunl
  • і вручну налаштуємо йому юзерів, сервери, роути

Роутинг має бути таким: всі пакети, які йдуть у VPC – відправляються через VPN, а решта – через звичайне підключення.

Terraform: створення EC2

Отже, спершу нам потрібно запустити EC2, на якій буде працювати Pritunl.

Для цього EC2 нам потрібні AWS AMI, SSK Key, Security Group, VPC ID, і відразу створимо AWS Route53 запис.

Отримання VPC ID

VPC ID отримуємо через terraform_remote_state, детальніше писав у Terraform: terraform_remote_state – отримання outputs інших state-файлів:

data "terraform_remote_state" "vpc" {
  backend = "s3"
  config = {
    bucket         = "tf-state-backend-atlas-vpc"
    key            = "${var.environment}/atlas-vpc-${var.environment}.tfstate"
    region         = var.aws_region
    dynamodb_table = "tf-state-lock-atlas-vpc"
  }
}

У цьому outputs маємо VPC ID та ID публічних сабнетів:

$ terraform output
...
vpc_id = "vpc-0fbaffe234c0d81ea"
...
vpc_public_subnets_cidrs = tolist([
  "10.0.0.0/20",
  "10.0.16.0/20",
])
vpc_public_subnets_ids = [
  "subnet-01de26778bea10395",
  "subnet-0efd3937cadf669d4",
]

І далі цей data resource використовуємо у locals:

locals {
  # get VPC info
  vpc_out = data.terraform_remote_state.vpc.outputs
}

Хоча в принципі теж можна зробити просто з data "aws_vpc".

EC2 SSH Key

Використовуємо key_pair.

Створюємо сам ключ:

$ ssh-keygen
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/setevoy/.ssh/id_ed25519): /home/setevoy/.ssh/atlas-vpn
...

Публічну частину можемо зберігати в репозиторії – створюємо каталог, і копіюємо її:

$ mkdir ssh
$ cp /home/setevoy/.ssh/atlas-vpn.pub ssh/

Описуємо ресурс aws_key_pair:

resource "aws_key_pair" "vpn_key" {
  key_name   = "atlas-vpn-key"
  public_key = file("${path.module}/ssh/atlas-vpn.pub")
}

AWS Secuirty Group

Знаходимо домашній/робочий IP:

$ curl ifconfig.me
178.***.***.52

Описуємо SecurityGroup – дозволяємо SSH тільки з цього IP, у vpc_id використовуємо local.vpc_out.vpc_id.

Додаємо порти – 80 для Let’s Encrypt, який використовується Pritunl, 443 – для доступу до його адмінки, тут знов тільки з мого IP, 10052 UPD – для клієнтів VPN:

resource "aws_security_group" "allow_ssh" {
  name        = "allow_ssh"
  description = "Allow SSH inbound traffic"
  vpc_id      = local.vpc_out.vpc_id

  ingress {
    description = "SSH Arseny home"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["178.***.***.52/32"]
  }

  ingress {
    description = "Pritunl Admin Arseny home"
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["178.***.***.52/32"]
  }

  ingress {
    description = "Pritunl Lets Encrypt"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }    

  ingress {
    description = "Pritunl VPN port"
    from_port   = 10052
    to_port     = 10052
    protocol    = "udp"
    cidr_blocks = ["0.0.0.0/0"]
  } 

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "${var.project_name}-${var.environment}-allow_ssh"
  }
}

AWS AMI

Використовуючи data "aws_ami", знайдемо AWS AMI з Ubuntu.

Я спочатку пробував Pritunl запустити на Amazon Linux, але той yum і окремі репозиторії – це якась біда, на Ubuntu завелось без проблем:

data "aws_ami" "ubuntu" {
  most_recent = true

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-*-22.04-amd64-server-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  owners = ["099720109477"]  # Canonical's official AWS account ID for Ubuntu AMIs
}

Але використовуючи data "aws_ami" майте на увазі, що коли вийде якась обнова, то AWS створить новий AMI, і при наступному запуску вашого Terraform-коду він підтягне новий ID, і запропонує перестворити відповідну EC2.

Тому, можливо, краще просто знайти AMI ID вручну і занести в variables. Див. Find an AMI та Amazon EC2 AMI Locator.

AWS Elastic IP

Аби мати одну і ту ж саму адресу, зробимо її окремим ресурсом:

resource "aws_eip" "vpn_eip" {
  domain = "vpc"
}

AWS Route53 VPN record

Відразу створимо запис в DNS.

В variables.tf задаємо ID зони в Route53 та її ім’я:

variable "route53_ops_zone" {
  type = object({
    name = string
    id = string
  })
  default = {
    name = "ops.example.co"
    id = "Z02***OYY"
  }
}

І в main.tf описуємо сам запис:

resource "aws_route53_record" "vpn_dns" {
  zone_id  = var.route53_ops_zone.id
  name     = "vpn.${var.route53_ops_zone.name}"
  type     = "A"
  ttl      = 300
  records = [aws_eip.vpn_eip.public_ip]
}

Тепер у нас буде запис виду “vpn.ops.example.co IN A <EC2_EIP>.

AWS EC2 та установка Pritunl

І, врешті-решт, описуємо сам EC2, використовуючи ресурси, які створили вище:

  • ami – беремо з data.aws_ami.amazon_linu
  • key_name – беремо з aws_key_pair.vpn_key.key_name
  • vpc_security_group_ids – з SG, яку створюємо тут же
  • subnet_id, де створювати EС2 – беремо з local.vpc_out.vpc_public_subnets_ids

Тут же відразу додаємо встановлення Pritunl – див. документацію [Other Providers] Ubuntu 22.04, але вона місцями крива, тому, можливо, краще зробити установку руками після створення інстансу.

Ну, або таки додати в user_data – принаймні на момент написання з кодом нижче це працювало.

У випадку проблем з EC2 user_data – перевіряйте лог /var/log/cloud-init.log, і спробуйте запустити скрипт вручну – він має бути у файлі типу /var/lib/cloud/instance/scripts/part-001.

Майте на увазі, що user_data викликається тільки при створенні інстансу:

resource "aws_instance" "vpn" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = var.vpn_ec2_instance_type
  key_name      = aws_key_pair.vpn_key.key_name

  vpc_security_group_ids = [aws_security_group.allow_ssh.id]
  subnet_id     = local.vpc_out.vpc_public_subnets_ids[0]

  user_data = <<-EOF
              #!/bin/bash
              echo 'deb http://repo.pritunl.com/stable/apt jammy main' > /etc/apt/sources.list.d/pritunl.list
              echo 'deb https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/6.0 multiverse' > /etc/apt/sources.list.d/mongodb-org-6.0.list
              apt-key adv --keyserver hkp://keyserver.ubuntu.com --recv 7568D9BB55FF9E5287D586017AE645C0CF8E292A
              wget -qO - https://www.mongodb.org/static/pgp/server-6.0.asc | sudo apt-key add -
              apt update
              apt --assume-yes upgrade
              apt -y install wireguard wireguard-tools
              ufw disable
              apt -y install pritunl mongodb-org
              systemctl enable mongod pritunl
              systemctl start mongod pritunl
              EOF

  tags = {
    Name = "Pritunl VPN"
  }
}

Додаємо підключення Elastic IP до цього інстансу:

resource "aws_eip_association" "vpn_eip_assoc" {
  instance_id   = aws_instance.vpn.id
  allocation_id = aws_eip.vpn_eip.id
}

Terraform Outputs

Додамо outputs, аби потім простіше було шукати всякі ID:

output "vpn_ec2_id" {
  value = aws_instance.vpn.id
}

output "vpn_eip" {
  value = aws_eip.vpn_eip.public_ip
}

output "aws_ami_id" {
  value = data.aws_ami.ubuntu.id
}

output "vpn_dns" {
  value = aws_route53_record.vpn_dns.name
}

Робимо terraform init, terraform plan, terraform apply:

...
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:

ec2_public_ip = "3.83.69.105"
vpn_ec2_id = "i-0ea1407cb7ff8690f"

Перевіряємо інстанс:

Перевіряємо SSH до нього:

$ ssh -i ~/.ssh/atlas-vpn [email protected]
...
[ec2-user@ip-10-0-3-26 ~]$ sudo -s
[root@ip-10-0-3-26 ec2-user]#

Перевіряємо сам Pritunl на сервері:

root@ip-10-0-1-25:/home/ubuntu# systemctl status pritunl
● pritunl.service - Pritunl Daemon
     Loaded: loaded (/etc/systemd/system/pritunl.service; enabled; vendor preset: enabled)
     Active: active (running) since Fri 2024-05-31 13:04:08 UTC; 55s ago
   Main PID: 3812 (pritunl)
      Tasks: 19 (limit: 2328)
     Memory: 99.7M
        CPU: 1.318s
     CGroup: /system.slice/pritunl.service
             ├─3812 /usr/lib/pritunl/usr/bin/python3 /usr/lib/pritunl/usr/bin/pritunl start
             └─4174 pritunl-web

May 31 13:04:08 ip-10-0-1-25 systemd[1]: Started Pritunl Daemon.

Тепер можна переходити до його налаштування.

Pritunl inital setup

Документація – Configuration.

Підключаємось на EC2, виконуємо pritunl setup-key:

root@ip-10-0-1-25:/home/ubuntu# pritunl setup-key
074d9be70f1944d7a77374cca09ff8dc

Відкриваємо vpn.ops.example.co:443, на помилку ERR_CERT_AUTHORITY_INVALID поки не звертаємо уваги – Let’s Encrypt згенерить сертифікат після налаштувань Pritunl.

Передаємо setup-key, адресу MongoDB можна лишити по дефолту:

Чекаємо апдейту MongoDB:

Коли відкриється вікно логіна – на сервері виконуємо pritunl default-password:

root@ip-10-0-1-25:/home/ubuntu# pritunl default-password
[local][2024-05-31 13:12:41,687][INFO] Getting default administrator password
Administrator default password:
  username: "pritunl"
  password: "1rueBHeV9LIj"

І логінимось:

Генеруємо новий пароль, яким будемо користуватись вже постійно:

$ pwgen 12 1
iBai1Aisheat

І задаємо основні параметри Pritunl – тут тільки логін/пароль та адреси:

Якщо забули новий пароль – можна скинути з pritunl reset-password.

Error getting LetsEncrypt certificate check the logs for more information.

При проблемах з Let’s Ecnrypt – перевіряємо лог /var/log/pritunl.log, там все буде:

root@ip-10-0-1-25:/home/ubuntu# tail -f /var/log/pritunl.log
  File "/usr/lib/pritunl/usr/lib/python3.9/site-packages/pritunl/handlers/settings.py", line 1112, in settings_put
    acme.update_acme_cert()
  File "/usr/lib/pritunl/usr/lib/python3.9/site-packages/pritunl/acme.py", line 73, in update_acme_cert
    cert = get_acme_cert(settings.app.acme_key, csr, cmdline)
  File "/usr/lib/pritunl/usr/lib/python3.9/site-packages/pritunl/acme.py", line 45, in get_acme_cert
    certificate = acmetiny.get_crt(
  File "/usr/lib/pritunl/usr/lib/python3.9/site-packages/pritunl/acmetiny.py", line 138, in get_crt
    raise ValueError("Challenge did not pass for {0}: {1}".format(domain, authorization))
ValueError: Challenge did not pass for vpn.ops.example.co: {'identifier': {'type': 'dns', 'value': 'vpn.ops.example.co'}, 'status': 'invalid', 'expires': '2024-06-07T13:32:30Z', 'challenges': [{'type': 'http-01', 'status': 'invalid', 'error': {'type': 'urn:ietf:params:acme:error:dns', 'detail': 'DNS problem: NXDOMAIN looking up A for vpn.ops.example.co - check that a DNS record exists for this domain; DNS problem: NXDOMAIN looking up AAAA for vpn.ops.example.co - check that a DNS record exists for this domain', 'status': 400}, 'url': 'https://acme-v02.api.letsencrypt.org/acme/chall-v3/357864308812/RHhMwA', 'token': 'KZLx4dUxDmow5uMvfJdwbgz5bY4HG0tTQOW2m4UvFBg', 'validated': '2024-05-31T13:32:30Z'}]}
  acme_domain = "vpn.ops.example.co"

Домен новий – Let’s Encrypt про нього ще не знає.

Чекаємо кілька хвилин, і пробуємо ще раз.

Успішна реєстрація сертифікату в логах має виглядати так:

[INFO] Found domains: vpn.ops.example.co
[INFO] Getting directory...
[INFO] Directory found!
[INFO] Registering account...
[INFO] Registered!
[INFO] Creating new order...
[INFO] Order created!
[INFO] Verifying vpn.ops.example.co...
[INFO] vpn.ops.example.co verified!
[INFO] Signing certificate...
[INFO] Certificate signed!
[INFO] Settings changed, restarting server...

Створення Pritunl Organization та юзерів

Додаємо організацію – через неї будемо групувати юзерів, бо Groups в безплатний версії недоступні:

Додаємо юзера:

Email, Pin – опціональні, зараз не потрібні:

Створення Pritunl Server та роути

Див. Server configuration.

Переходимо до Servers, додаємо новий:

В DNS Server задаємо адресу DNS нашої VPC.

В Port задаємо порт, який відкривали на AWS EC2 SecurityGroup.

Virtual Network – пул, з якого будуть виділятись адреси клієнтам. Я тут використовую 172.*, бо простіше відрізнити від решти – дома 192.*, VPC 10.*.

Підключаємо створену раніше Організацію:

Стартуємо сервер:

Налаштуємо роути – щоб через VPN йшли запити тільки в VPC:

І видаляємо дефолтний роут в 0.0.0.0/0:

Linux OpenVPN – підключення до серверу

Переходимо в Users, клікаємо Download profile:

Розпаковуємо його:

$ tar xfpv test-user.tar 
org-all_test-user_org-all-serv.ovpn

І підключаємося за допомогою звичайного OpenVPN клієнта:

$ sudo openvpn --config org-all_test-user_org-all-serv.ovpn

У випадку помилки “ERROR: Cannot open TUN/TAP dev /dev/net/tun: No such device” на Linux – спробуйте перезавантажитись. В мене ядро було оновлене, і давно не ребутався.

Перевіряємо локальні роути:

$ route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         192.168.3.1     0.0.0.0         UG    600    0        0 wlan0
0.0.0.0         192.168.3.1     0.0.0.0         UG    1002   0        0 enp2s0f0
10.0.0.0        172.16.0.1      255.255.0.0     UG    0      0        0 tun0
172.16.0.0      0.0.0.0         255.255.255.0   U     0      0        0 tun0
...

Все гуд – в Інтернет, 0.0.0.0, ходимо старим маршрутом, через домашній роутер, а у VPC, 10.0.0.0 – через 172.16.0.1, наш VPN.

Спробуємо:

$ traceroute 1.1.1.1
traceroute to 1.1.1.1 (1.1.1.1), 30 hops max, 60 byte packets
 1  _gateway (192.168.3.1)  1.617 ms  1.550 ms  1.531 ms
 ...
 9  one.one.one.one (1.1.1.1)  17.265 ms  17.246 ms  18.600 ms

Окей, через домашній роутер.

І до якогось серверу в AWS VPC:

$ traceroute 10.0.42.95
traceroute to 10.0.42.95 (10.0.42.95), 30 hops max, 60 byte packets
 1  172.16.0.1 (172.16.0.1)  124.407 ms  124.410 ms  124.417 ms
 ...

Через VPN.

І навіть SSH до інстансів в приватній мережі працює:

$ ssh -i test-to-del.pem [email protected]
...
ubuntu@ip-10-0-45-127:~$

Чудово.

Linux Systemd та Pritunl/OpenVPN autostart

Давайте зробимо, аби конект був постійно.

Створюємо каталог:

$ sudo mkdir /etc/pritunl-client

Переносимо конфіг:

$ sudo mv org-all_test-user_org-all-serv.ovpn /etc/pritunl-client/work.ovpn

Пишемо самий простий файл /etc/systemd/system/pritunl-org.service:

[Unit]
Description=Pritunl Work

[Service]
Restart=always
WorkingDirectory=/etc/pritunl-client/

ExecStart=/usr/bin/openvpn --config work.ovpn

ExecStop=killall openvpn

[Install]
WantedBy=multi-user.target

І перевіряємо:

$ systemctl start pritunl-org.service
==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-units ====
Authentication is required to start 'pritunl-org.service'.

Ще раз роути:

$ route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         192.168.3.1     0.0.0.0         UG    100    0        0 enp2s0f0
0.0.0.0         192.168.3.1     0.0.0.0         UG    600    0        0 wlan0
0.0.0.0         192.168.3.1     0.0.0.0         UG    1002   0        0 enp2s0f0
10.0.0.0        172.16.0.1      255.255.0.0     UG    0      0        0 tun0

Все є.

Додаємо в автостарт:

$ systemctl enable pritunl-org.service
==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-unit-files ====
Authentication is required to manage system service or unit files.
Authenticating as: root
Password: 
==== AUTHENTICATION COMPLETE ====
Created symlink /etc/systemd/system/multi-user.target.wants/pritunl-org.service -> /etc/systemd/system/pritunl-org.service.

Готово.

Loading

Renovate: GitHub та Helm Charts versions management
0 (0)

29 Травня 2024

Depndabot (див. Dependabot: GitHub та Terraform versions management) цікавий тим, що досить швидко і просто конфігуриться, але те, що він досі не вміє в Helm-чарти (хоча feature request був відкритий ще в 2018) робить його трохи useless для нас.

Отже, натомість давайте глянемо на Renovate, який прям дуже цінується всіма, хто має справу з менеджментом версій.

Що Renovate вміє?

  • як і Dependabot, може запускатись з майже будь-яким хостингом – GitHub, Gitlab, Bitbucket тощо
  • можемо запускати як self-hosted на власних GitHub Actions Runner
  • можемо запускати в Kubernetes

Вміє перевіряти прям безліч систем – Terraform, Helm, Kubernetes manifest – перевіряти images і їхні апдейти, Dockerfiles. Див. Supported Managers.

Виводить дуже детальну інформацію по змінам які пропонує, і має власну дашборду.

Для GitHub найпростіший шлях інтеграції – через Renovate GitHub App.

Хоча в заголовку цього поста я вказав “Helm Charts”, але з коробки і з дефолтними параметрами Renovate виконає перевірку просто всього, що є в репозиторії і має якісь versions та dependencides.

Ну і коли я писав, що Dependabot “швидко і просто конфігуриться“, то у випадку з Renovate це взагалі фактично робиться в кілька кліків і працює просто з коробки.

Підключення Renovate до GitHub

Переходимо на сторінку Renovate GitHub App, клікаємо Install, вибираємо в які репозиторії його підключити.

Я поки для тестів додам тільки в один репозиторій з нашим моніторингом де маємо Terraform та Helm:

Дозволяємо доступ:

Реєструємось на https://developer.mend.io – далі тут будуть дашборди з деталями перевірок:

Переходимо до репозиторію, і маємо відкритий Pull Request для ініціалізації Renovate:

Ііі… В принципі – це все 🙂

Налаштування Renovate

В цьому PR маємо створений файл renovate.json з мінімальним конфігом:

Крім того, Renovate відразу визначив, які пакети в цьому репозиторії є:

І відразу визначає, що треба оновити:

А на сторінці репозиторію на developer.mend.io побачимо всі деталі перевірки:

Тепер можемо додати трохи свої параметрів, яких прям дуже багато, бо Renovate дозволяє дуже гнучко налаштувати ваші перевірки – див. всі на Configuration Options.

Наприклад, додамо розклад запуску, лейбли та будемо PR відразу асайнити на мене:

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": [
    "config:recommended"
  ],
  "labels": ["dependencies"],
  "assignees": ["arseny-zinchenko"]
}

По дефолту, Renovate має ліміт у 2 PR на годину. Аби збільшити цей ліміт – у файлі renovate.json додаємо prHourlyLimit:

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": [
    "config:recommended"
  ],
  "labels": ["dependencies"],
  "assignees": ["arseny-zinchenko"],
  "prHourlyLimit": 10
}

Зберігаємо, пушимо і мержимо цей PR:

І маємо відкриті PR:

Деталі по конкретному PR:

Renovate Dependency Dashboard та GitHub Issues

Окремо можемо включити створення Issues для всіх PR, які буде створювати Renovate.

Переходимо в Settings репозиторію – включаємо Issues:

Тепер, коли Renovate буде відкривати PR з апдейтами – він створить GitHub Issue з деталями по апдейту:

 

Ну в цілому на цьому все.

“It (just) works!” (c)

Можливо, потім додам ще якісь деталі по конфігурації, коли буду налаштовувати інші репозиторії.

Корисні посилання

Loading

Dependabot: GitHub та Terraform versions management
0 (0)

29 Травня 2024

З часом, коли проект росте, то рано чи пізно постане питання про апгрейд версій пакетів, модулів, чартів.

Робити це вручну, звісно, можна – але тільки до якоїсь межі, бо врешті-решт ви просто фізично не зможете моніторити та оновлювати все.

Для автоматизації таких процесів існує багато рішень, але найчастіше зустрічаються два – Renovate та Dependabot.

За результатами опитування в UkrOps Slack, Renovate набрав набагато більше голосів, і в принципі він вміє більше, ніж Dependabot.

З іншої сторони – Dependabot вже є в GitHub репозиторіях, доступний у всіх тарифних планах, тож якщо ви використовуєте GitHub – то для налаштування Dependabot вам просто потрібно додати води додати файл конфігурації. Хоча, трохи забігаючи наперед – Renovate налаштовується ще простіше, але про це в наступному пості – Renovate: GitHub та Helm Charts versions management.

Взагалі, Dependabot можна мати майже на всіх платформах – GitHub, Github Enterprise, Azure DevOps, GitLab, BitBucket та AWS CodeCommit, див. How to run Dependabot.

Але – це для мене був прям surprize-surprize – Dependabot не вміє в Helm-чарти. Хоча з Terraform працює, і вже є в деяких наших репозиторіях з Python-кодом, тож для початку давайте глянемо на нього.

Знов-таки, забігаючи наперед – Renovate мені зайшов набагато більше, і ми будемо використовувати його.

Налаштування Dependabot

Отже, як це працює:

  • в репозиторії створюється файл конфігурації Dependabot
  • в ньому описується що саме він має перевіряти – бібліотеки pip, модулі Terraform тощо
  • описується що саме цікавить – secrity updates, або versions updates
  • при знаходженні апдейтів – Dependabot створює Pull Request, в якому додає деталі по апдейту
  • Profit!

Тож що будемо робити:

  • маємо GitHub репозиторій для моніторингу
  • в ньому маємо Terraform
  • налаштуємо перевірку і створення PR

Документація – Dependabot quickstart guide, Configuration options for the dependabot.yml file.

Див. також Supported repositories and ecosystems – які саме системи підтримує Dependabot.

Dependabot та Terraform

Що можемо моніторити з Dependabot в контексті Terraform – це версії провайдерів та модулів.

Наприклад, маємо два файли – versions.tf, де задаються версії провайдерів, і файл lambda.tf, де використовуємо кілька модулів – terraform-aws-modules/security-group/aws, terraform-aws-modules/lambda/aws і інші:

Тепер, щоб Dependabot почав моніторити версії в них – створюємо каталог .github, і в ньому файл dependabot.yml:

У файлі задаємо параметри:

version: 2
updates:
  - package-ecosystem: "terraform"
    directory: "/terraform"
    schedule:
      interval: "daily"
      time: "09:00"
      timezone: "Europe/Kyiv"
    assignees:
    - arseny-zinchenko
    reviewers:
    - arseny-zinchenko
    open-pull-requests-limit: 10

В принципі, тут все зрозуміло з назв параметрів:

  • package-ecosystem: так як цей конфіг для Terraform, то вказуємо його
  • directory: файли Terraform у директорії terraform в корні репозиторію
  • schedule: розклад перевірок – при цьому при першому додаванні файлу dependabot.yml він запустить перевірку відразу, і є можливість запускати вручну
  • assignees та reviewers: відразу створюємо PR на мене
  • open-pull-requests-limit: по дефолту Dependabot відкриває максимум 5 PR, можна збільшити за допомогою цього параметру

Пушимо в репозиторій, і перевіряємо статус:

В репозиторії переходимо в Insights > Dependency graph > Dependabot, і бачимо, що перевірка запустилась:

За хвилину маємо відкриті пул-реквести:

При цьому, в коментах він додає трохи деталей по апдейту – Release notes, Changelog, etc:

Правда, чомусь не всюди.

Наприклад, апдейт для модулю Lambda створився без деталей:

А от Renovate робить це набагато краще.

Dependabot та GitHub Secrets

Ще один нюанс – це сікрети, які доступні Dependabot.

У нас при PR зі змінами в директорії terraform запускається GitHub Actions Workflow, який виконує перевірки з Terraform (див. GitHub Actions: деплой Terraform з review запланованих змін).

Цей workflow знаходиться в окремому репозиторії, і для доступу до нього у викликаючий worfklow передається GitHub Deploy Key через GitHub Actions Secrets.

В GitHub Actions джобі, яка запустилась від Dependabot, цей степ сфейлився:

Хоча сам workflow передає всі сікрети через secrets: inherit:

...
jobs:

  terraform-test:
    # call the Reusable Workflow file
    uses: ORG_NAME/atlas-github-actions/.github/workflows/call-terraform-check-and-plan.yml@master
    with:
      aws-iam-role: ${{ vars.AWS_IAM_ROLE }}
      aws-env: ${{ vars.AWS_ENV }}
      pr-num: ${{ github.event.pull_request.number }}
      environment: ops
      slack-channel: '#cicd-devops'      
    secrets:
      inherit

Але для Dependabot ці сікрети необхідно задавати окремо – не в Actions secrets and variables > Actions, а в Actions secrets and variables > Dependabot:

Додаємо йому новий сікрет – і тепер перевірка працює:

Dependabot та приватні registries/repositories

Серед іншого, у нас є власні модулі Terraform, які зберігаються в приватному репозиторії.

При доступі до них – Dependabot сфейлить перевірку з помилкою “Dependabot can’t access ORG_NAME/atlas-tf-modules“:

Варіант перший – додати цей репозиторій або інший registry явно в файлі dependabot.yml – див. Configuring private registries.

Варіант другий – це просто клікнути Grant access, що відкриє доступ до репозиторію для всіх репозиторіїв в організації.

Або зробити вручну – переходимо в Settings організації > Code security > Global settings, і в Grant Dependabot access to private repositories додаємо доступ до потрібного репозиторію:

Ручний запуск Dependabot

Тепер, як додали доступ – повертаємось до репозиторію, переходимо в Insights > Dependency graph > Dependabot, клікаємо Check for updates:

І перевірка запущена:

В цілому, на цьому все. Тепер будемо мати апдейти для Terraform без необхідності самому підписуватись на всі репозиторії.

Хоча, ще раз – Renovate дійсно краще. Див. Renovate: GitHub та Helm Charts versions management.

Loading

Helm: UPGRADE FAILED: another operation (install/upgrade/rollback) is in progress
0 (0)

24 Травня 2024

Іноді під час деплою Helm-чартів може з’являтись помилка “UPGRADE FAILED: another operation (install/upgrade/rollback) is in progress“:

Виникати може через те, що попередній деплой не відбувся через помилки в чарті, або втрачений зв’язок між білд-машиною та Kubernets-кластером.

Перевіряємо статус релізу з ls --all:

$ helm -n dev-backend-api-ns ls --all
NAME            NAMESPACE               REVISION        UPDATED                                 STATUS          CHART           APP VERSION
dev-backend-api dev-backend-api-ns      590             2024-05-23 09:11:51.332096671 +0000 UTC pending-upgrade kraken-0.1.0    1.16.0     

І бачимо, що дійсно – маємо “pending-upgrade” замість “deployed“.

Також можна глянути з helm history – що там відбувалося до цього і який статус зараз:

І знов бачимо той самий статус “Preparing upgrade” замість “Upgrade complete“.

Окей, давайте фіксити.

Перший варіант – the hard way – просто видалити реліз з helm uninstall, і передеплоїти з helm upgrade --install, але це призведе до видалення всіх ресурсів, які були створені цим чартом.

Інший варіант – зробити helm rollback до попереднього стабільного деплою.

В цьому кейсі це був 587 – Upgrade complete.

Виконуємо:

$ helm -n dev-backend-api-ns rollback dev-backend-api 587
Rollback was a success! Happy Helming!

Перевіряємо статус тепер:

$ helm -n dev-backend-api-ns ls --all
NAME            NAMESPACE               REVISION        UPDATED                                         STATUS          CHART           APP VERSION
dev-backend-api dev-backend-api-ns      591             2024-05-23 13:01:36.905101349 +0300 EEST        deployed        kraken-0.1.0    1.16.0

Або:

$ helm -n dev-backend-api-ns status dev-backend-api
NAME: dev-backend-api
LAST DEPLOYED: Thu May 23 13:01:36 2024
NAMESPACE: dev-backend-api-ns
STATUS: deployed
REVISION: 591
TEST SUITE: None

Перезапускаємо джобу в GitHub Actions – і все працює.

Loading

AWS: VPC Flow Logs, NAT Gateways, та Kubernetes Pods – детальний обзор
0 (0)

1 Травня 2024

Маємо відносно великі витрати на AWS NAT Gateway Processed Bytes, і стало цікаво що ж саме процеситься через нього.

Здавалося б, все просто – включи собі VPC Flow Logs, да подивись, що до чого. Але як діло стосується AWS Elasitc Kubernetes Service та NAT Gateways, то все трохи цікавіше.

Отже, про що будемо говорити:

  • що таке NAT Gateway у AWS VPC
  • що таке NAT та Source NAT
  • включимо VPC Flow Logs, і розберемося з тим, що саме в них пишеться
  • і розберемося з тим, як знайти Kubernetes Pod IP у VPC Flow Logs

Схема нетворкінгу досить стандартна:

  • AWS EKS кластер
  • VPC
    • публічні сабнети для Load Balancers та NAT Gateways
    • приватні для Kubernetes Worker Nodes
    • окремі сабнети для баз даних
    • окремі сабнети для Kubernetes Control Plain

Створення VPC для кластеру описано у Terraform: створення EKS, частина 1 – VPC, Subnets та Endpoints.

AWS NAT Gateway Pricing

Документація – Amazon VPC pricing.

Отже, коли ми використовуємо NAT Gateway, то платимо за:

  1. за кожну годину роботи NAT Gateway
  2. за гігабайти, які він оброблює

Година роботи NAT Gateway коштує $0.045, тобто в місяць це буде:

0.045*24*30
32.400

32 долари.

Є варіант з використання NAT Instance замість NAT Gateway, але тоді мусимо мати справу з його менеджментом – і створення інстансу, і його апдейти, і конфігурація.

Амазон предоставляє AMI для цього – але вони давно не оновлюються, і не будуть.

Крім того, Terraform-модуль terraform-aws-modules/vpc/aws працює тільки з NAT Gateway, тому, якщо ви хочете використати NAT Instance – то маєте ще й автоматизацію писати під нього.

Отже – скіпаємо варіант з NAT Instance, і використовуємо NAT Gateway – як рішення, яке повністю підтримується і менеджиться Амазоном та VPC-модулем для Terraform.

Щодо вартості трафіку: платимо ті ж самі $0.045, але вже за кожен гігабайт. При чому рахується весь processed трафік – тобто і outbound (egress, TX – Transmitted), і inbound (ingress, RX – Recieved).

Отже, коли ви відправляєте один гігабайт даних в S3-бакет, а потім завантажуєте його назад на EC2 в приватній мережі – ви платите 0.045 + 0.045 долари.

Що таке NAT?

Давайте згадаємо, що таке NAT взагалі, і як він працює на рівні пакетів і архітектури мережі.

NAT – Network Address Translation – виконує операції над заголовками TCP/IP пакетів, міняючи (translate) адресу відправника або отримувача, дозволяючи мережевий доступ з або до машин, які не мають власного публічного IP.

Знаємо, що є декілька типів NAT:

  • Source NAT: пакет “виходить” з приватної мережі, і NAT перед відправкою в Internet заміняє source IP пакету на власний (SNAT)
  • Destination NAT: пакет “входить” в приватну мережу з Inernet, і NAT перед відправкою всередину мережі заміняє destination IP пакету з власного на приватну IP всередині мережі (DNAT)

Окрім того, є Static NAT, Port Address Translation (PAT), Twice NAT, Multicast NAT.

Нас зараз цікавить саме Source NAT, і далі ми будемо в основному розглядати саме його і те, як пакет потрапляє з VPC до інтернету.

Якщо відобразити це схемою, то вона буде виглядати так:

  1. Ініціація запиту з EC2: сервіс на EC2 з Private IP 10.0.1.5 генерує запит до External Server з IP 203.0.113.5
    1. ядро операційної системи EC2 створює пакет
      • source IP: 10.0.1.5
      • packet source IP: 10.0.1.5
      • destintation IP: 203.0.113.5
      • packet destination IP: 203.0.113.5
  2. Маршрутизація пакета: мережевий інтерфейс на EC2 включений в Private Subnet, і має Route Table, яка підключена до цього сабнету
    1. ядро операційної системи визначає, що destintation IP не належить до VPC, і переадресує пакет до NAT GW Private IP 10.0.0.220
      • source IP: 10.0.1.5
      • packet source IP: 10.0.1.5
      • destination IP: 10.0.0.220
      • packet destination IP: 203.0.113.5
  3. Обробка пакета NAT Gateway: пакет приходить на мережевий інтерфейс NAT GW, який має адресу 10.0.0.220
    1. NAT Gateway зберігає запис про походження пакету з IP 10.0.1.5:10099 => 203.0.11.443 у своїй NAT-таблиці
    2. NAT GW змінює source IP з 10.0.1.5 на адресу свого інтерфейсу у публічній мережі з IP 77.70.07.200 (власне, сама операція SNAT), і пакет відправляється в Інтернет
      • source IP: 77.70.07.200
      • packet source IP: 10.0.1.5
      • destination IP: 203.0.113.5
      • packet destination IP: 203.0.113.5

Що таке NAT Table?

NAT-таблиця зберігається в пам’яті NAT Gateway та використовується, аби прийняти пакет від External Server до нашої EC2, коли він буде слати відповідь, і переадресувати його до відповідного серверу в приватній мережі.

Схематично його можна відобразити так:

Отримуючи відповідь від 203.0.113.5 до себе на 77.70.07.200 і порт 20588, NAT Gateway по таблиці знаходить відповідного адресата – IP 10.0.1.5 і порт 10099.

Добре. Тепер, як згадали що таке NAT – давайте включимо VPC Flow Logs, і розберемося з записами, які він створює.

Див. The Network Address Translation Table.

Налаштування AWS VPC Flow Logs

Див. також AWS: VPC Flow Logs – знайомство та аналітика з CloudWatch Logs Insights.

VPC Flow Logs можна налаштувати вручну в панелі AWS:

Або, якщо використовуєте Terraform модуль terraform-aws-modules/vpc, то задати параметри в ньому:

...
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.5.2"

  name = local.env_name
  cidr = var.vpc_params.vpc_cidr

  ...

  enable_flow_log = var.vpc_params.enable_flow_log

  create_flow_log_cloudwatch_log_group = true
  create_flow_log_cloudwatch_iam_role  = true

  flow_log_max_aggregation_interval         = 60
  flow_log_cloudwatch_log_group_name_prefix = "/aws/${local.env_name}-flow-logs/"
  flow_log_log_format = "$${region} $${vpc-id} $${az-id} $${subnet-id} $${instance-id} $${interface-id} $${flow-direction} $${srcaddr} $${dstaddr} $${srcport} $${dstport} $${pkt-srcaddr} $${pkt-dstaddr} $${pkt-src-aws-service} $${pkt-dst-aws-service} $${traffic-path} $${packets} $${bytes} $${action}"
  #flow_log_cloudwatch_log_group_class       = "INFREQUENT_ACCESS"

}
...

Виконуємо terraform apply, і маємо логи у VPC з власним форматом:

VPC Flow Logs – формат

У flow_log_log_format описується формат того, як лог буде записаний, а саме – які поля в ньому будуть.

Я завжди використовую custom format з додатковою інформацією, бо дефолтний формат може бути недостатньо інформативним, особливо про роботі через NAT Gateways.

Всі поля є у документації Logging IP traffic using VPC Flow Logs.

Для Terraform, екрануємо записи з ${...} через додатковий $.

Вартість VPC Flow Logs в CloudWatch Logs

flow_log_cloudwatch_log_group_class дозволяє задати клас Standard або Infrequent Access, і Infrequent Access буде дешевшим, але він має обмеження – див. Log classes.

В моєму випадку, я планую збирати логи до Grafana Loki через CloudWatch Log Subscription Filter – тому потрібен Standard. Але подивимось – може налаштую через S3 бакет, і тоді, мабуть, можна буде використати Infrequent Access.

Бо насправді витрати на логування трафіку досить помітні.

Наприклад, у невеликій VPC, де в Kubernetes крутиться наш Backend API, моніторинг (див. VictoriaMetrics: створення Kubernetes monitoring stack з власним Helm-чартом) та кілька інших сервісів, після включення VPC Flow Logs вартість CloudWatch почала виглядати так:

Тож майте це на увазі.

VPC Flow Logs в CloudWatch Logs vs AWS S3

Зберігання логів в CloudWatch Logs буде дорожчим – але дає можливість виконувати запити у CloudWatch Logs Insights.

Крім того, як на мене, то налаштування збору логів до Grafana Loki простіше через CloudWatch Subscription Filters, аніж робити через S3 – просто менше головної болі з IAM.

Про Loki та S3 – див. Grafana Loki: збираємо логи AWS LoadBalancer з S3 за допомогою Promtail Lambda.

Про Loki та CloudWatch – див. Loki: збір логів з CloudWatch Logs з використанням Lambda Promtail.

Втім, поки що тримаю Flow Logs в CloudWatch Logs, а як закінчу розбиратись з тим, звідки йде трафік – то подумаю про використання S3, і звідти вже буду збирати до Grafana Loki.

VPC Flow Logs та Log Insights

Окей – отже, маємо налаштовані VPC Flow Logs в CloudWatch Logs.

Що нас особливо цікавить – це трафік через NAT Gateway.

Використовуючи кастомний формат логів – в Logs Insights можемо зробити такий запит:

parse @message "* * * * * * * * * * * * * * * * * * *" 
| as region, vpc_id, az_id, subnet_id, instance_id, interface_id, 
| flow_direction, srcaddr, dstaddr, srcport, dstport, 
| pkt_srcaddr, pkt_dstaddr, pkt_src_aws_service, pkt_dst_aws_service, 
| traffic_path, packets, bytes, action 
| filter (dstaddr like "10.0.5.175") | stats sum(bytes) as bytesTransferred by interface_id, flow_direction, srcaddr, srcport, dstaddr, dstport, pkt_srcaddr, pkt_dstaddr, pkt_src_aws_service, pkt_dst_aws_service, bytes
| sort bytesTransferred desc
| limit 10

Тут ми робимо фільтр по запитам, які у dstaddr мають Private IP нашого NAT Gateway:

Іноді pkt_src_aws_service або pkt_dst_aws_service не вказані, і тоді не дуже зрозуміло що за трафік.

Можна перевірити на сайті https://ipinfo.io – там може бути вказане ім’я хоста, і тоді ясно, що це, наприклад, S3-ендпоінт:

Flow Logs ingress vs egress

Ми знаємо, що це ingress – це вхідний трафік (RX, Received), а egress – вихідний трафік (TX, Transmitted).

Але вхідний та вихідний до чого? VPC, Subnet або ENI – Elastic Network Interface?

Читаємо документацію Logging IP traffic using VPC Flow Logs:

  • flow-direction: The direction of the flow with respect to the interface where traffic is captured. The possible values are: ingress | egress.

Тобто, відносно до мережевого інтерфейсу: якщо на інтерфейс EC2 або NAT Gateway (який під капотом є звичайним EC2) приходить трафік – то це ingress, якщо виходить з інтерфейсу – то egress.

Різниця srcaddr vs pkt-srcaddr та dstaddr vs pkt-dstaddr

У нас є чотири поля, які вказують на адресатів.

При цьому для source та destination у нас є два різних типи полів – з pkt-, або без.

В чому різниця:

  • srcaddr – “поточна” маршрутизація:
    • адреса вхідного трафіку – звідки прийшов пакет, або:
    • адреса інтерфейсу, який відправляє трафік
  • dstaddr – “поточна” маршрутизація:
    • адреса “пункту призначення” пакета у вихідному трафіку, або
    • адреса мережевого інтерфейсу для вхідного трафіку
  • pkt-srcaddr: “оригінальна” адреса появи пакету
  • pkt-dstaddr: “оригінальна” адреса “пункту призначення” пакету

Аби краще зрозуміти ці поля і взагалі структуру записів у Flow Logs – давайте розглянемо кілька прикладів з документації.

Flow Logs та приклади записів

Отже, маємо EC2 інстанс в приватній мережі, який робить запити до якогось зовнішнього сервісу через NAT Gateway.

Що ми побачимо в логах?

Приклади взяті з документації Traffic through a NAT gateway, і додав трохи схем, аби візуально було простіше зрозуміти.

Дивитись будемо на реальні дані:

  • маємо EC2 інстанс в приватному сабнеті:
    • Elastic Network Interface: eni-0467f85cabee7c295
    • Private IP: 10.0.36.132
  • маємо NAT Gateway:
    • Elastic Network Interface: eni-0352f8c82da6aa229
    • Private IP: 10.0.5.175
    • Public IP: 52.54.3.183

На EC2 запущено curl в циклі з запитом на 1.1.1.1:

root@ip-10-0-36-132:/home/ubuntu# watch -n 1 curl https://1.1.1.1

Формат VPC Flow Log той самий, що був вище, а для перевірки в CloudWatch Logs Insights будемо використовувати такий запит:

parse @message "* * * * * * * * * * * * * * * * * * *" 
| as region, vpc_id, az_id, subnet_id, instance_id, interface_id, 
| flow_direction, srcaddr, dstaddr, srcport, dstport, 
| pkt_srcaddr, pkt_dstaddr, pkt_src_aws_service, pkt_dst_aws_service, 
| traffic_path, packets, bytes, action 
| filter (interface_id = "eni-0352f8c82da6aa229" AND srcaddr = "10.0.36.132") | stats sum(bytes) as bytesTransferred by instance_id, interface_id, flow_direction, srcaddr, dstaddr, pkt_srcaddr, pkt_dstaddr
| sort bytesTransferred desc

Тут робимо виборку по записам з мережевого інтерфейсу NAT Gateway та Private IP нашого EC2:

Отже, в результатах у нас буде “instance_id, interface_id, flow_direction, srcaddr, dstaddr, pkt_srcaddr, pkt_dstaddr

NAT Gateway Elastic Network Interface records

Спочатку глянемо записи, які стосуються мережевого інтерфейсу NAT Gateway.

Від EC2 через NAT GW до Remote server

Перший приклад запису в Flow Logs відображає інформацію з мережевого інтерфейсу NAT Gateway, де записано проходження пакету від EC2 в приватній мережі до зовнішнього серверу:

При роботі з VPC Flow Logs головне пам’ятати, що записи робляться для кожного інтерфейсу.

Тобто, якщо ми робимо curl 1.1.1.1 з EC2-інстансу – то отримаємо два записи у Flow Log:

  1. з Elastic Network Interface на самому EC2
  2. з Elastic Network Interface на NAT Gateway

В цьому прикладі ми бачимо запис з інтерфейсу NAT Gateway, бо:

  • поле instace-id пусте (NAT GW хоч і є EC2, але це все ж Amazon-managed сервіс)
  • flow-directioningress, пакет прийшов на інтерфейс NAT Gateway
  • в полі dstaddr бачимо Private IP нашого NAT GW
  • і поле pkt-dstaddr не співпадає з dstaddr – в pkt-dstaddr у нас адреса “кінцевого отримувача”, а пакет прийшов на dstaddr – NAT Gateway

Від NAT Gateway до Remote Server

В другому прикладі бачимо запис про пакет, який було відправлено з NAT Gateway до Remote Server:

  • flow-directionegress, пакет відправлено з інтерфейсу NAT Gateway
  • srcaddr та pkt-srcaddr однакові
  • dstaddr та pkt-dstaddr однакові

Від Remote Server до NAT Gateway

Далі – наш Remote Server відправляє відповідь до нашого NAT Gateway:

  • flow-directioningress, пакет прийшов на інтерфейс NAT Gateway
  • srcaddr та pkt-srcaddr однакові
  • dstaddr та pkt-dstaddr однакові

Від Remote Server через NAT Gateway до EC2

Запис про пакет від Remote Server до нашого EC2 через NAT Gateway:

  • flow-directionegress, пакет відправлено з інтерфейсу NAT Gateway
  • srcaddr та pkt-srcaddr різні – в srcaddr маємо NAT GW IP, а в pkt-srcaddr – Remote Server
  • dstaddr та pkt-dstaddr однакові, з IP нашого EC2

EC2 Network Interface records

І пара прикладів записів у Flow Logs, які відносяться до EC2 Elastic Network Interface.

Від EC2 до Remote Server

Відправка пакета з EC2 до Remote Server:

  • instance_id не пустий
  • flow-directionegress, бо запис с інтерфейсу EC2, який відправляє пакет до Remote Server
  • srcaddr та pkt-srcaddr однакові, з Private IP цього EC2
  • поля dstaddr та pkt-dstaddr – теж однакові, з адресою Remote Server

Від Remote Server до EC2

Відправка пакета з Remote Server до EC2:

  • instance_id не пустий
  • flow-directioningress, бо запис с інтерфейсу EC2, який отримує пакет від Remote Server
  • srcaddr та pkt-srcaddr однакові, з адресою Remote Server
  • поля dstaddr та pkt-dstaddr – теж однакові, з Private IP цього EC2

VPC Flow Logs, NAT, Elastic Kubernetes Service та Kubernetes Pods

Окей – ми побачили, як знайти інформацію по трафіку через NAT Gateway з EC2-інстансів.

А як щодо Kubernetes Pods?

Тут ситуація ще цікавіша, бо маємо різні типи мережевої комунікації:

  • Worker Node to Pod
  • Worker Node to ClusterIP
  • Pod to ClusterIP Service
  • Pod to Pod на одній Worker Node
  • Pod to Pod на різних Worker Node
  • Pod to External Server

Поди мають IP адреси з пулу VPC CIDR, і ці IP підключаються до WorkerNode як Secondary Private IP (або беруться з підключених префіксів /28 у випадку з VPC CNI Prefix Assignment Mode – див. AWS: VPC Prefix та максимальна кількість подів на Kubernetes WorkerNodes).

При комунікації Pod to Pod, якщо вони в одній VPC, то використовуються їхні IP/WorkerNode Secondary Private IP. Але якщо вони знаходяться на одній WorkerNode – то пакет піде через віртуальні мережеві інтерфейси, а не через “фізичний” інтерфейс на WorkerNode/EC2, і, відповідно, ми цей трафік у Flow Logs не побачимо взагалі.

А от коли Pod відправляє трафік до зовнішнього ресурсу – то по дефолту плагін VPC CNI транслює (міняє) Pod IP на WorkerNode Primary Private IP, і, відповідно, у Flow Logs ми не побачимо IP поду, який шле трафік через NAT Gateway.

Тобто, у нас на рівні ядра операційної системи WorkerNode/EC2 виконується один SNAT, а потім на NAT Gateway – ще один.

Виключення – якщо под запускається з hostNetwork: true.

Документація – SNAT for Pods.

Давайте перевіримо.

Трафік з Pod до Pod, та VPC Flow Logs

Запустимо два поди. Додамо їм antiAffinity та topologyKey (див. Kubernetes: Pods та WorkerNodes – контроль розміщення подів на нодах), аби вони запустились на двох різних WorkerNodes:

apiVersion: v1
kind: Pod
metadata:
  name: ubuntu-pod1
  labels:
    app: ubuntu-app
    pod: one
spec:
  containers:
  - name: ubuntu-container1
    image: ubuntu
    command: ["sleep"]
    args: ["infinity"]
    ports:
    - containerPort: 80
  affinity:
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchLabels:
            app: ubuntu-app
        topologyKey: "kubernetes.io/hostname"
---
apiVersion: v1
kind: Pod
metadata:
  name: ubuntu-pod2
  labels:
    app: ubuntu-app
    pod: two
spec:
  containers:
  - name: ubuntu-container2
    image: ubuntu
    command: ["sleep"]
    args: ["infinity"]
    ports:
    - containerPort: 80
  affinity:
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchLabels:
            app: ubuntu-app
        topologyKey: "kubernetes.io/hostname"

Деплоїмо, і на першому встановлюємо curl, а на другому – NGINX.

Тепер маємо:

  • ubuntu-pod1:
    • Pod IP: 10.0.46.182
    • WorkerNode IP: 10.0.42.244
  • ubuntu-pod2:
    • Pod IP: 10.0.46.127
    • WorkerNode IP: 10.0.39.75

На другому стартуємо NGINX, і з першого поду запускаємо curl в циклі на IP другого поду:

root@ubuntu-pod1:/# watch -n 1 curl 10.0.46.127

І за хвилину перевіряємо Flow Logs с запитом:

parse @message "* * * * * * * * * * * * * * * * * * *" 
| as region, vpc_id, az_id, subnet_id, instance_id, interface_id, 
| flow_direction, srcaddr, dstaddr, srcport, dstport, 
| pkt_srcaddr, pkt_dstaddr, pkt_src_aws_service, pkt_dst_aws_service, 
| traffic_path, packets, bytes, action 
| filter (dstaddr = "10.0.46.127" AND dstport = 80) | stats sum(bytes) as bytesTransferred by instance_id, interface_id, flow_direction, srcaddr, dstaddr, dstport, pkt_srcaddr, pkt_dstaddr
| sort bytesTransferred desc

В srcaddr у нас Primary Private IP з WorkerNode, на якій запущено ubuntu-pod-1, а в pkt_srcaddr – IP самого Pod, який робить запити.

Трафік з Pod до External Server через NAT Gateway, та VPC Flow Logs

Тепер, нічого не міняючи, запустимо з того ж ubuntu-pod1 curl на 1.1.1.1, і подивимось логи:

В першому записі бачимо:

  • eni-0352f8c82da6aa229 – інтерфейс NAT Gateway
  • flow-directioningress, інтерфейс отримав пакет
  • srcaddr 10.0.42.244 – адреса WorkerNode, де запущений ubuntu-pod1
  • dstaddr 10.0.5.175 – пакет для NAT Gateway
  • pkt_dstaddr 1.1.1.1 – і пакет призначається для Remote Server

Далі, у другому записі:

  • той же мережевий інтерфейс, NAT GW
  • але вже egress – пакет вийшов з інтерфейсу
  • srcaddr 10.0.5.175 – пакет з NAT GW

І третій запис:

  • інстанс i-023f37c7aad6fc69d – там, де наш Pod
  • трафік egress – пакет вийшов з інтерфейсу
  • srcaddr 10.0.42.244 – пакет з Private IP цієї WorkerNode
  • і dstaddr 1.1.1.1 – пакет для Remote Server

Але ми ніде не бачимо IP самого Kubernetes Pod.

Kubernetes Pod, hostNetwork: true та VPC Flow Logs

Давайте передеплоїмо ubuntu-pod1 з hostNetwork: true:

apiVersion: v1
kind: Pod
metadata:
  name: ubuntu-pod1
  labels:
    app: ubuntu-app
    pod: one
spec:
  hostNetwork: true
  containers:
  - name: ubuntu-container1
    image: ubuntu
    command: ["sleep"]
    args: ["infinity"]
    ports:
    - containerPort: 80
  affinity:
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchLabels:
            app: ubuntu-app
        topologyKey: "kubernetes.io/hostname"

Деплоїмо, і перевіряємо IP самого Pod та IP його WorkerNode:

$ kubectl describe pod ubuntu-pod1
Name:             ubuntu-pod1
...
Node:             ip-10-0-44-207.ec2.internal/10.0.44.207
...
Status:           Running
IP:               10.0.44.207
...

Обидва IP однакові, відповідно, якщо зробимо з цього поду curl 1.1.1.1 – то у Flow Logs будемо бачити IP пода (а фактично – IP тієї Worker Node, на якій запущено цей Pod).

Але використання hostNetwork: true ідея погана (безпека, можливі проблеми з TCP-портами тощо), тому можемо зробити інакше.

AWS EKS та Source NAT for Pods

Якщо ми відключимо SNAT for Pods у VPC CNI нашого кластеру, то SNAT буде виконуватись тільки на NAT Gateway у VPC, а не двічі – спочатку на WorkerNode, а потім на NAT Gateway.

Див. AWS_VPC_K8S_CNI_EXTERNALSNAT та AWS_VPC_K8S_CNI_EXCLUDE_SNAT_CIDRS.

І, відповідно, в логах ми будемо бачити реальні IP наших подів.

Оновлюємо конфігурацію VPC CNI:

$ kubectl set env daemonset -n kube-system aws-node AWS_VPC_K8S_CNI_EXTERNALSNAT=true

Повертаємо конфіг для ubuntu-pod-1 без hostNetwork: true, передеплоїмо, і глянемо логи з таким запитом:

parse @message "* * * * * * * * * * * * * * * * * * *" 
| as region, vpc_id, az_id, subnet_id, instance_id, interface_id, 
| flow_direction, srcaddr, dstaddr, srcport, dstport, 
| pkt_srcaddr, pkt_dstaddr, pkt_src_aws_service, pkt_dst_aws_service, 
| traffic_path, packets, bytes, action 
| filter (srcaddr = "10.0.37.171" OR pkt_srcaddr = "10.0.37.171") | stats sum(bytes) as bytesTransferred by instance_id, interface_id, flow_direction, srcaddr, dstaddr, pkt_srcaddr, pkt_dstaddr
| sort bytesTransferred desc

Маємо два записи:

Перший запис – з інтерфейсу NAT Gateway, який отримав пакет від Pod з IP 10.0.37.171 для Remote Server 1.1.1.1:

Другий запис – з інтерфейсу EC2, який робить запит до Remote Server, тільки тепер у нас pkt_srcaddr не такий же, як srcadd (як було на схемі “З EC2 до Remote Server” вище), а має запис про IP нашого Kubernetes Pod:

І ось тепер ми зможемо відслідкувати який саме Kubernetes Pod шле або отримує трафік через NAT Gateway з таблиць DynamoDB або S3-корзин.

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

Корисні посилання

(ох, цей кайф, коли закриваєш купу вкладок в браузері…)

Loading