Rethinking Continuous Delivery [ukr]

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

Ми в MacPaw практикуємо підхід сервісних команд. І як одна з таких команд, на наші плечі лягає відповідальність за доставку програм у різних середовищах, від тестових до продакшну. У цій доповіді я розповім, як ми намагаємося уніфікувати підхід до доставки програм у середовища з різноманітним стеком та різноманітними підходами до розробки. В доповіді буде розглянуто такі підходи, як GitOps, динамічні середовища, та доставка програм на базі повідомлень.

Андрій Насінник
MacPaw
  • 15 років в індустрії
  • Розробляє/проєктує/адмініструє/конфігурує для MacPaw
  • Намагається програмувати на Rust та Haskell
  • Любить функціональне програмування та no-code підхід
  • Twitter, Github, Linkedin

Транскрипція доповіді

Вітаю всіх. Як вже зазначено, я працюю в компанії MacPoW. Сьогодні ми будемо говорити про Continuous Delivery. Я працюю як інженер, Site Reliability Engineer, тобто, здебільшого, ми працюємо з Kubernetes. Ось мій Twitter. Окрім того, крім роботи з Kubernetes, у мене є досвід у розробці. І якби я комерційно писав і на PHP, і на Node.js, і на Go, то буваю як на той, так і на той бік барикад. Так, сьогодні ми говоримо про Delivery, конкретно бекенд сервісів. Тобто тут ми не будемо обговорювати фронтенд сервіси, не будемо говорити про мобільні або десктопні застосунки. Хоча якраз. MacPoW займається в основному розробкою десктопу для Mac.

І коли ми говоримо про CD, ми маємо на увазі саме цю зелену частинку на нашому відомому слайді про CI-CD. Тобто всі інші етапи, ми вважаємо не деплойментом, тобто це ми вважаємо CI. А CD вважаємо конкретно цю штуку деплойментом. Так. Чому ж у нас такий стек? І чому ми взагалі подумали, що нам треба по-іншому подивитися на Delivery? У нас використовується GitHub. У нас є багато різних проєктів. І різні проєкти використовують дуже різні підходи. Деякі проєкти використовують Single Repo, в плані один компонент знаходиться в одному репозиторії, він там розробляється і він деплоїться. Інші проєкти можуть використовувати Multi Repo. Де в одному репозиторії розробляється кілька мікросервісів, сервісів, хто як називає, і хто як вважає свій сервіс сервісом чи мікросервісом. Ось. Крім того, це все деплоїться на Kubernetes, як я вже сказав раніше.

Абсолютно всі наші сервіси деплоюються на Kubernetes. Крім того, окрім сервісів, які розробляють наші команди, нам потрібно мати багато інших компонентів інфраструктурних. Наприклад, деякі моніторинги, системи, які допомагають нам їх розгортати, а також складові, які відповідають за обробку сертифікатів і так далі. Ми намагаємося утримувати всі ці складові в одному репозиторії та доставляти їх за допомогою GitOps-підходу. Конкретно для GitOps-підходу ми використовуємо FluxCE. Всі інші додатки ми доставляємо іншим чином, не використовуючи GitOps. Ми не використовуємо GitOps для їхньої доставки. Ми використовуємо класичну схему з CI-підходом.

У нас є різні системи CI. Зараз найпопулярнішою є GitHub Actions, яка замінює Azure Pipelines. По суті, вони дуже схожі, навіть якщо дивитися на синтаксис, оскільки обидві належать Microsoft. Крім того, ми використовуємо Jenkins на деяких проектах, і був випадок використання системи CI під назвою Drone, яка є цікавою. Майже всі наші проекти мають схожий пайплайн для бекенд-проектів. Хоча всередині вони можуть відрізнятися, оскільки можуть бути написані різними мовами програмування. Ми використовуємо JS, Go, Java та Python для написання сервісів. Кожна команда може працювати з різним стеком, і в деяких мультирепозиторіях можуть бути навіть різні сервіси на різних мовах програмування.

Щодо пайплайну, перші етапи, позначені сірим або синім кольором, відповідають команді розробки, яка працює з конкретним репозиторієм. Це, в основному, код-лінтінг та статична валідація коду для виявлення помилок. SAST - це етап для команди безпеки, який також включає статичну валідацію, але з фокусом на безпеку. Юніт тести входять до відповідальності розробницької команди і є частиною пайплайну. Після цього відбувається збірка іміджу. Команда SiteRealityBuild інженерів часто бере участь у написанні Docker-файлів для збірки іміджу та його доставки. Проте ми вважаємо, що вірно, коли за це відповідає команда розробки, а не команда доставки, яка доставляє цей артефакт на середовище.

Також ми використовуємо сканування на вразливості, щоб перевірити, чи є вразливості в кожному іміджі, який зібрався. Ці кроки - це етапи, описані в пайплайні. Останній етап також описаний в пайплайні і відповідає конкретній команді. Ми плануємо виокремити його від типового пайплайну, який описується нашими утилітами GitHub Actions, Azure Pipeline чи Jenkins. Щодо деплою, ми використовуємо класичний підхід через push. Наша додаток намагається викликати API Kubernetes і створювати ресурси в Kubernetes. Для цього ми використовуємо Helm, який є дуже популярним інструментом.

У Helm може бути створено різноманітні об'єкти. Наприклад, бази даних і бакети також можуть бути створені за допомогою маніфестів Kubernetes. Таким чином, багато речей можна описати безпосередньо в Helm. Проте редагування цих значень не завжди безпечно, оскільки можливе перейменування бази даних або створення більше баз даних, ніж дозволяється. Це може створити проблеми з безпекою, коли команда, яка має доступ до репозиторію, може впливати на інфраструктуру.

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

Окрім того, імперативний підхід, де все виконується крок за кроком, може бути неефективним. Якщо який-небудь етап не вдається, потрібно перезапустити весь процес. Це також може бути даремно витратною операцією, особливо коли розглядаються фіксації вразливостей для проектів у фриз режимі. Іншою проблемою є безпека, оскільки агент, який використовується в GitHub Actions, повинен мати доступ до Kubernetes кластера. Враховуючи, що Helm може створювати різноманітні маніфести в будь-якому namespace, це може призвести до потенційної безпекової загрози, якщо доступ до репозиторію надається особам з доступом до продакшена.

Рішенням наших проблем є перехід від імперативного підходу до декларативного. Ми плануємо використовувати GitOps, де конфігурація знаходиться в Git репозиторії, і зміни автоматично відображаються в інфраструктурі. Це спростить процес оновлення Helm та інші аспекти доставки, зменшуючи вплив на безпеку та ефективність процесу. І нам доведеться тримати одного агента, який може їсти приблизно так само, як і сам цей проект ще більше. І це дуже неефективно. То як ми це вирішуємо? Ми вирішили вирішувати це точно так само, як ми робимо зараз з інфраструктурою. GitOps-підходом. Одна з реалізацій GitOps-підходу, яка не у нас, це Argo. А підніміть руку ті, хто використовує Argo. Круто. А хто використовує Flux? Вау. Окей. Так, у нас не Argo, тому що вона нам не підійшла з кількох причин.

Це одне з існуючих у Argo, яке для нас насправді критичне. Це пост-рендерінг. Тобто, коли Argo відмальовує ваші маніфести, нам потрібно деколи робити зміни в цих маніфестах. Це рідко може бути пов'язано з конкретно нашими додатками. Тому що ми можемо вносити зміни в сам хартчарт, і проблем з цим не буде. Але ми доставляємо не тільки наші додатки. Ми доставляємо Prometheus, який розробляє інша команда і так далі. І змінювати їх не можемо. Ну, або можемо через пул-реквести. Ми так часто робимо і намагаємося робити через пул-реквести. Але це інколи займає багато часу, і інколи може не підніматися взагалі, поки цю існ не виправлять. Нам це не підходить.

Крім того, Argo трошки інакше працює з іміджами. І це я розповім трошки пізніше, тому що це є одним з наших рішень. Так, ми використовуємо Flux. Flux має цю фічу пост-рендерінгу. Він вміє нативно працювати з Helm. Тобто, коли Flux працює з Helm, він робить Helm-інстал точно так само, якби ви зробили Helm-інстал з консолі. Argo працює не так. Argo працює, він робить Helm-темплейт. І після цього починає виконувати свої хуки, які можуть працювати так, як працюють хуки Helm. Але їх потрібно маркувати різними анотаціями. І це не зовсім той самий підхід, якби ви зробили Helm-інстал. І нам часто це не підходить, тому що, наприклад, всі міграції у нас виконуються хуками. І їх би треба було переписувати під Argo стиль, цей Argo War of Clovers.

Так, але як ми таки доставляємо наші додатки? І так. Чому ми взагалі хочемо мати цей GitOps-підхід? І як він виправить нашу проблему з тим, що нам не потрібно було б ходити в 30 репозиторіїв і все вправляти? По-перше, ми виносимо цей проект в наш репозиторій з нашою інфраструктурою. І він знаходиться в відповідальності нашої команди, тобто команди SREA. Вся інфраструктура описана в одному місці і не описана в окремому проекті. І вона тепер не залежить від цього CI-степа. Тобто нам не потрібно чекати, поки команда розробки прийде і перевірить, що всі їхні лінтери пройшли успішно. І ми можемо спокійно змінювати які-небудь наші компоненти.

Наприклад, ми хочемо включити всюди шифрування в базах даних. База даних у нас — маніфест в Kubernetes. Нам би довелося внести зміни в Helium Values, який вміє цей маніфест створювати і відтворювати, внести значення, яке б сказало: «шифруй свою базу даних». Тепер ми можемо змінити це в нашому репозиторії, задиплаїти, і це розгорнеться на всіх проектах, які розгорнуті у нас. Тобто одним комітом вирішити ту саму проблему, яку раніше вирішували тридцятьма комітами. Крім того, нам не потрібно чекати на апрув від команди. Звісно, ми це обговорюємо. Інколи ми навіть в нашому репозиторії робимо такий гібридний підхід, дозволяючи командам контрибютити нам, роблячи опис їхнього проекту. Їх кодери для папочки, яка описує їхній проект. І тоді вони можуть контрибютити і самі відповідати за цей кусочок, який є їхнім проектом. Також це допомагає нам трошки в наслідуванні. Тому що у нас є багато, наприклад, PHP, і він використовується в десяти репозиторіях. Ми можемо мати одну, наприклад, конфігурацію PHP-FPM, і її успадковувати для всіх. І змінювати її в одному місці, а не змінювати у всіх цих десяти місцях. І це декларативно. Тобто це один раз ви описали маніфест, він зберігається в такому стані. І кожен раз Kubernetes намагається вашу інфраструктуру відкотити до стану цього маніфеста. Тобто він намагається, навіть якщо в Kubernetes він має такий, зберігає стан і робить постійний реконсайл. Якщо у вас збережений там цей маніфест, то система буде намагатися його розгорнути постійно, навіть якщо там є якась помилка. Так, щодо безпеки. Flux замість того, щоб ви з агента аплаїли ваші маніфести, Flux це робить замість вас. Він розгорнутий в самому кластері. Він витягує своїм контролером ваш репозиторій і робить аплаї з середини кластера.

Тобто у вас вже немає цієї проблеми, коли якийсь агент, на якого може крутитися якийсь код, до якого може отримати доступ. Доступ хакера вже, так би мовити, запущений всередині кластера, і отримати доступ до кластера стало складніше. І репозиторій тепер один, а не їх там 30, і з цих 30 вже складніше отримати доступ. Так, і як же ми взагалі доставляємо наші додатки? Наш пайплайн, в суті, залишився такий самий. Крок розгортання. Ми просто видалили звідти. І останнім нашим кроком, відкритим для сканера безпеки, було завантаження Docker Image. Ми завантажуємо Docker Image в реєстр. Сам Flux має кілька контролерів. Один з них - Image Update Controller. Цей контролер Image Update вміє читати дані з вашого реєстру. Він переглядає дані з вашого реєстру. Якщо там з'являється оновлення вашої версії, наприклад, це видно тут, це для нашого стейджингу. Для стейджингу ми використовуємо суфікс rc. У нас для стейджингу суфікс rc вказує на реліз-кандидата, наприклад, версія може виглядати так: 1.0.0.0-rc.25. Flux приходить, дивиться, "Ого, з'явився, у мене тут розгорнуто 24-й реліз-кандидат, з'явився 25-й реліз-кандидат", і автоматично оновлює нашу систему.

Крім того, він автоматично, як ми пам'ятаємо, оновлює наш стан, який записаний в нашому інфраструктурному репозиторії. В маніфестах. Він автоматично робить коміт у ваш репозиторій із зміною цієї версії. Таким чином, виглядає цей маніфест, який йде у ваш репозиторій і оновлює версію вашого образу. Ви отримаєте коміт у свій інфраструктурний репозиторій, і дані в вашому репозиторії будуть консистентними. Отже, так виглядає ця схема. Є 30 таких додатків. Реєстр. Зараз ми використовуємо Google Container Registry. Це частина Flux. Звісно, це не зовсім так просто, як зображено стрілочками, воно працює через Kubernetes маніфести, і тоді контролер обробляє кожен маніфест Kubernetes, але в суті виглядає так. Є Image Reflector, який витягує репозиторії з реєстру. Є Image Updater, який записує оновлену версію у ваш репозиторій. А сам Source витягує код з вашого репозиторію, а Helm, якщо бачить, що з репозиторію прийшла нова версія, новий коміт, виконує розгортання вашого додатка у просторі імен.

Але нам цього було недостатньо. Тому що продакшн - це добре, стейджинг - це добре, але команди повинні тестувати це десь, QA повинні тестувати це десь, нові фічі повинні розроблятися десь. Є проекти, де невелика кількість розробників і можливо достатньо двох середовищ, але кожна команда тестує свої фічі. Тому у нас є динамічні оточення. Коли ми робимо нову фічу у проекті, створюється запит на злиття. На момент створення запиту на злиття запускається цей CI, і створюється Docker Image нової версії. Цей Docker Image також розгортається у нашому кластері. Тобто старий пайплайн також розгортався при кожному запиті на злиття. Іноді ми застосовували фільтр до запитань на злиття: якщо у запиті на злиття є мітка deployed, тоді його розгортати. Але це не так просто реалізувати з GitOps, оскільки це не зовсім GitOps підхід. Ви не можете декларувати динамічні оточення, які можуть існувати 2 години, 1 годину, 3 дні - ви не можете вказати їх у вашому репозиторії, оскільки хтось повинен це робити, і це не зручно. Навіть якщо це робиться автоматично, це також не вихід, оскільки у вас може виникнути обмеження за частотою від вашого GitHub. GitHub має досить низькі обмеження за частотою, і коли ви вносите зміни до вашого інфраструктурного репозиторію з 30 репозиторіїв, це буде дуже дорого і не ефективно. Як нам виправити цю ситуацію? Ми все ж хочемо використовувати GitOps підхід і використовувати його для динамічних оточень також.

Нам потрібна система, яка могла б нам створити наше середовище динамічним, але вже всередині Kubernetes, тобто модель "pull", а не "push", коли ми створюємо, як зараз робить Helm, викликаючи Kubernetes, але все це в межах самого Kubernetes. І такі системи існують. Одна з таких систем - Trigger Mesh. Вона вміє створювати ресурси Kubernetes, але ще не повністю. І ось я саме шукаю рішення стосовно створення ресурсів Kubernetes, тому Trigger Mesh для нас не підходить. Існує ще одна система, Tekton. Це більше як повний конвеєр на Kubernetes. Але один з його компонентів, Tekton Triggers, спрямований саме на слухання HTTP-подій і створення будь-якого маніфесту Kubernetes на їхньому підґрунті. Наприклад, для створення бази даних. Це трошки дивний підхід, але це можливо. Він також не відповідає нашим вимогам, оскільки обмежує можливість використовувати лише свої маніфести.

Trigger Mesh вміє працювати із PubSub, а Tekton може працювати тільки з HTTP-подіями. Ми також розглядали Argo Events. Argo Events може працювати з різними системами нотифікацій, включаючи як HTTP-події, так і події від GCR. Є нюанси із GCR-подіями, але Argo Events з ними впорається. Він може створити будь-який ресурс Kubernetes у момент приходу події. Окрім того, крім створення, потрібно розуміти, що саме створювати. Наприклад, у нас є образ. Однак інформації про цей Docker-образ, яка приходить разом з подією, недостатньо для розгортання середовища. Тому ми вирішили узгодити, як саме називати цей шлях імеджа з нашою розробницькою командою.

Тут ми використовуємо стандартний підхід у GCR. Далі ми домовились, що першою частиною буде назва проекту. Наприклад, у нашому випадку, це CleanMyMac. Другою частиною буде компонент, наприклад, аналітика. А останньою частиною буде сам образ. Це необхідно, оскільки для деяких компонентів, наприклад, у PHP, потрібні різні образи для обробки HTTP або повідомлень з Redis.

Крім того, нам треба було домовитися про технічний ідентифікатор, оскільки нам потрібно зрозуміти, який саме образ та подія створені для якого середовища. Тому ми домовились, що першою частиною буде номер pull request. Це дозволяє нам розуміти, яке саме середовище ми повинні розгорнути з цим образом. Останньою частиною є унікальний ідентифікатор для того, щоб всі образи були унікальні в реєстрі і не переписували один одного. Це необхідно також через політики Kubernetes, які можуть впливати на оновлення образів.

Як це реалізується на Argo? В Argo є event bus, що піднімає саму велику системи подій. Там, на жаль, вона реалізована за замовчуванням. Крім того, є event source, яка слухає саме систему, від якої ви хочете отримати подію. У нашому випадку це GCR. Є сенсор, який опрацьовує цю подію. Ось такі речі є фічею Flux, що дозволяє populate ваші середовища, а точніше, sabbatical замінювати змінні в маніфесті. Це дуже зручно, коли у вас схожі маніфести і ви хочете декларувати різні кастомізації через sabbatical.

Ось так описується обробка саме цієї події. Тут у нас величезний GQ, оскільки велика частина має більше чотирьох. Це особливість роботи з GCR. В PubSub він зберігає конкретно зашифрований меседж в Base64. Його доводиться обробляти кожен раз. Зазвичай в самому Argo це можна робити було би на етапі EventSource трошки раніше, і цього не потрібно було б робити в сенсорі. Але він цього не вміє, не розуміє GQ на етапі EventSource, що не дуже мені подобається. Це, як називається, дизайнний підхід в Argo, коли в одному місці використовують GQ, а в іншому іншу бібліотеку, яка не вміє тих самих фіч. І ми не можемо це зробити на попередньому етапі.

Ми вирізаємо цей великий image, він зберігається в полі. За допомогою GQ ми розбиваємо його на всі ці значення. З PR я видалив, на жаль. Потім фільтруємо за тим, чи це pull request. Наприклад, якщо там вливається 1.0.0.rc25, то нам не потрібно створювати preview.env. Розуміємо, що це імідж для стейджингу. Також можна фільтрувати компоненти, які не хочемо згортати. В описі triggers можна буде створювати Kubernetes ресурс. Перш ніж створити його, треба взяти дані і наповнити ними цей Kubernetes ресурс. Для цього в Argo Sensory є template, який вміє витягувати дані з події різними способами, такими як JSON-пас або gotemplate.

Крім того, він може взяти дані для запису або використовувати gotemplate для редагування, тобто здійснювати різні операції для формування необхідного ключа. Якщо вам потрібно змінити не значення, але ключ в маніфесті, то потрібен параметр на рівні об'єкта template, а не об'єкта source і ресурсу. Операція patch необхідна для того, щоб... Коли у нас є проект з кількома компонентами, які пушаться асинхронно, і повідомлення приходять від GCR асинхронно. Наприклад, спочатку ви отримуєте повідомлення про оновлення аналітичного сервісу, а потім ваше середовище повинно містити три сервіси. Коли ви отримуєте цю подію, вона відразу створює маніфест. Потім ви отримуєте іншу подію, і змінюється лише один image. Ви не можете його просто перетворити, бо це його знищить.

Для цього потрібна саме операція patch, яка залишить ті зміни з попереднього і, як би, зробить такий собі абсурд. Абсурд от із цього маніфеста і додасть технічно цього нового компонента для вашого image. І так, як ми створюємо сам компонент? Сам компонент ми створюємо якраз флаксовою кастомізацією. Флаксова кастомізація, схоже, є Helm(багато хто використовує Helm). Але кастомізація - це кастомізація. Кастомізація - це також темплейтер, тільки більш плейновий. Вона сама вбудована в Kubernetes і може виконувати темплейтизацію. Так само вона вміє робити темплейт з різними конфігураціями.

У нашому випадку наш preview.env описаний в іншій директорії, але насправді він наслідує повністю ваш staging.env. Тобто там вносяться тільки певні зміни, які вам потрібні, наприклад, менше ресурсів тощо. Далі є цей представник. Він є так званим пост-рендерером, пост-білдом, який нас дуже врятував і якого нам не вистачає в Vargo. Він вміє оновлювати, якраз оцими кавичками, вносити зміни по всіх маніфестах. Наприклад, image-tech може бути в worker або в server, і нам потрібно оновити його в двох місцях. Для цього ми використовуємо цей substitution і він допомагає оновлювати їх.

Отак виглядає наш повноцінний flow. З'явився ArgoEvent. Тепер він також слухає наш registry. Потрапляє в source. Далі сенсор потрапляє в цей Helm Controller, який, по суті, є Customization Controller. Вони однакові. По факту вони вже аплають, просто один Helm Release робить, а інший - Customization Apply. Насправді у нас в оригіналі використовується Customization, який в середині має Helm Release. І тоді у нас створюється динамічна кількість environments. Тобто для кожного відкритого pull request ми розгортаємо environment і команда може його тестувати. Потім, звісно, їх треба попідтирати, це ми вирішуємо за допомогою кронджобу, нічого складного. Це, в принципі, те, що дозволяє нам позбутися deployment з агентів і не робити зміни в 30 репозиторіях, що значно полегшує нашу роботу з добавленням нових проєктів і підтримкою існуючих. Але крім цього у нас є, звісно, деякі проблеми.

Перша проблема - це нам потрібно повідомляти наших розробників про те, що було виявлено в цих preview env'ах. І це не завжди зручно зробити, тому що наші евенти - вони асинхронні. І ми можемо отримати в один момент відповідь від Kubernetes, що успішно розгорнувався сервіс-аналітик такої версії, а потім починає розгортатися ще й core-сервіс, просто тому, що, наприклад, була черга на CI і він більше білдився, і через це відставав. І QA може пройти, почати вже щось тестувати і сказати, от тут не вилетіла версія, або там якась бага не працює. З асинхронними івентами це прийдеться якось вирішувати. Одне з рішень, над яким ми думаємо, це використовувати в GitHub те API, яке називається deployment. Ми думали перенести все-таки той кол, який слухає сенсор, не в Registry, а в GitHub, і слухати цей GitHub-івент, і вже на його основі створювати всі image, так як в ньому вже можна розмістити інформацію про всі image, які вам потрібно вилити на ваш environment. І ще, окрім цього, наша команда розробки іноді має дуже складний флоу, і після preview env'а може виникнути ситуація, що вони там проганяють якісь тести автоматично і хочуть, щоб після цього deployment'а у них був якийсь сигнал, щоб вони могли продовжити свій CI-степ, зробити якісь тести і, наприклад, навіть автоматично мерджити. Тобто на деяких проєктах у нас є автоматичний мердж. Це також із цим підходом поки що ламається, але ми також плануємо це вирішити, просто відправляючи в GitHub додатковий хук, на який вони зможуть підписатися і обробляти ці евенти. Це все. Дякую за увагу.

Увійти
Або поштою
Увійти
Або поштою
Реєстрація через e-mail
Реєстрація через e-mail
Забули пароль?