Підхід GitOps як сучасна практика для CD з Kubernetes

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Мене звати Володимир Шинкар. Я працюю DevOps Інженером в компанії Intellias. Також є активним учасником підрозділу Centre of Excellence в компанії.

Загалом DevOps досвіду у мене більше 6 років. В ІТ сфері працюю близько 8 років. За цей час я успішно мігрував, розгорнув і консультував понад 15 проєктів в різних сферах. Моє знайомство з GitOps почалося не так давно. До цього, працюючи з Kubernetes кластером та Kubernetes в цілому, ми використовували CI для доставки коду. Користувалися різними інструментами, на кшталт helm чи kubectl, і постійно стикалися з певними проблемами, які виникали через невідповідність стейту на самому кластері. З часом ми перейшли на GitOps і вже успішно використали цей підхід на кількох проєктах.

У цій статті ми розглянемо принципи GitOps, поговоримо про безпеку Pipelines та їхню реалізацію, а також подивимось на загрози в Git-і та правила зберігання секретів. Крім того, проаналізуємо push та pull підходи для налаштування CD, а в кінці статті у Демо я покажу як це застосовується на практиці.

Що таке GitOps і як з ним працювати?

GitOps — це еволюція infrastructure as code підходу. Він допомагає суттєво покращити ефективність роботи розробників, в тому числі DevOps інженерів. GitOps — це також операційна модель, яка базується на багаторічному досвіді інженерів та містить набір принципів та методів, які пришвидшують усі процеси, в тому числі розгортання та роботи. Працюючи з GitOps, варто пам’ятати, що всі зміни, конфіги та системи мають бути версіоновані. Крім того, код також повинен мати версію, до прикладу, теги чи бренчі. Це допоможе вам зберігати стейт кожного середовища і відповідно легше робити оновлення чи ролбеки.

Принципи GitOps

У роботі з GitOps можна виділити чотири головні принципи:

1. Декларативність. Ваша система має бути описана декларативно. Ви маєте описувати всі зміни, які робите, щоб в подальшому змогти їх легко прочитати та порівняти.

2. Версіонування. Їх ми зберігаємо в коді, тобто в git-і. Таким чином в будь-який час можна переглянути, що в нас є в мастер гілці. Це буде відображенням того, що в нас є в системі.

3. Автоматизоване розгортання. Кожна затверджена зміна повинна автоматично розгортатися в певній системі. Це може бути Kubernetes кластер, конфігураційні файли, застосунок чи налаштування якогось елементарного сертифікат-менеджера.

4. Системні агенти. Це агенти, які встановлені у вашій системі і відповідають за розгортання змін. Вони перевіряють версії, порівнюють стани з попереднім та поточним і сповіщають вас про певні проблеми, конфлікти чи помилки в синтаксисі. Таким чином ви завжди отримуєте зворотний зв’язок.

Переваги GitOps

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

  • Продуктивність. Система завжди надає нам зворотний зв’язок. Цей процес відбувається безперервно. Тобто кожного разу, коли ми робимо якусь зміну, система каже нам «все ок» або «в процесі», або «щось пішло не так». Так ми розуміємо, що відбувається після того, як ми зробили зміну, та зменшуємо час на деплоймент.
  • Краща якість роботи. Нові інженери та розробники не мають прямого доступу до системи. Натомість вони керують тільки маніфестами, які зберігаються в репозиторії. GitOps допомагає їм працювати з кластером, не маючи до нього прямого доступу.
  • Стабільність. Ми завжди маємо аудит того, що відбувається в системі. Ми бачимо кожну зміну та її історію — хто що робив, хто що накоїв. Маючи GitOps, ми також можемо отримати SOC 2 сертифікацію. Тобто за допомогою GitOps інфраструктура відповідає певним Security Compliance.

Також використання GitOps у роботі може надати вашому проєкту додаткові переваги:

  • Надійність. Крім того, що GitOps надає можливість робити rollback, він також дозволяє вам бачити з якою швидкістю йде деплоймент. Кожна ваша зміна одразу попадає в кластер. Якщо вам знадобиться відновити кластер, це займе лічені хвилини.
  • Послідовність. Ви завжди робите зміни в одному місці, у вас є один репозиторій, який застосовує зміни до всіх концептів. Це і моніторинг, і логування, і сервери застосунків, і безпека — все знаходиться в одному репозиторії. P.S. Так можна розділяти застосунки розробників, щоб покращити досвід співпраці між командами та розділити повноваження до кластера.
  • Безпека та аудит. Git гарантує постійний контроль змін. Він також дає можливість підписувати зміни, тобто затверджувати їх за допомогою різних воркфловів, перевірок, аудитів безпеки та pull/merge запитів, які повинні перевірятися іншими людьми.

Безпека пайплайнів

Безпека ваших пайплайнів починається з обмеження доступу до кластера. Доступ до нього має бути тільки у DevOps або infrastructure інженерів. Інші члени команди чи користувачі інфраструктури, яким потрібно буде розміщувати застосунки чи додаткові системні ресурси, повинні отримати доступ тільки до репозиторію, в межах якого вони повинні працювати.Те саме стосується і сторонніх сервісів. Наприклад, час від часу нам може знадобитися надати доступ таким програмам, як terraform cloud або platform9, чи spotlight. Якщо на їхньому боці станеться якийсь інцидент чи виникне проблема з безпекою, зловмисники зможуть отримати доступ і до вашого акаунту. Тому потрібно переглянути практику надання повного доступу таким системам. Натомість надавайте їм тільки read-only доступ.

Крім того, при розробці застосунків, скажімо Node.js, ми часто використовуємо сторонні бібліотеки, які не завжди походять від офіційного виробника. Це може бути якийсь розробник, якому захотілось щось зробити. Він опублікував свій модуль і ви його собі встановили. А цей модуль мав в собі зловмисне програмне забезпечення і ваш код було доставлено разом з ним, а це призвело до втрати чи шифруванню даних. Це може бути дуже небезпечно і може призвести до великих фінансових втрат.

Ще одну проблему з безпекою становить застаріла CI система. Простим прикладом може бути Jenkins server, в якому ви маєте безліч плагінів. Їх ніхто не оновлює, бо раптом щось не те зробиш і все зламається. А старі плагіни мають чимало слабких місць, які можуть бути використані зловмисниками і про які всі насправді знають. Тим не менш, не оновлені вчасно плагіни досі складають серйозну безпекову проблему на багатьох проєктах. Тому краще обмежити публічний доступ до СІ системи та лімітувати внутрішній трафік.

Також є ще поширена практика використовувати один СІ сервер на кілька команд. Припустимо, що є аутсорс компанія, яка має 10 проєктів. Для того, щоб не створювати 10 серверів і 10 СІ систем під кожен проєкт, компанія використовує один сервер на всіх. Таким чином ми даємо доступ багатьом розробникам до СІ системи, щоб вони могли самі створювати собі джоби, тощо. Таке трапляється часто, але робити це небажано.Отже, проаналізувавши згадані небезпеки, можна сказати, що найчастіше проблеми виникають через так званий людський фактор. Хтось зробив щось не так, щось виконав не там де треба, чи поставив Cron джоби і все виконалось на продакшені замість стейджа. Наприклад, кілька років тому був інцидент з Amazon S3, коли працівник виконав не той Ansible скрипт не там де треба. І через нього перестав працювати цілий S3 сервіс в одному з регіонів.

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

  1. Не надавайте доступ до продакшену. Дайте своїй команді можливість робити зміни у власних гілках, а в основну гілку тільки через Pull requests. Якщо зміни для продакшн середовища, то обов’язково з ревю процесом від іншого інженера, тощо. Розділяйте відповідальність, щоб додати більше пунктів контролю.
  2. Кожен коміт завжди повинен перевірятися на статичний аналіз будь-якими лінтерами чи іншими перевірками. В Python я нерідко використовую елементарні перевірки на довжину стрічки, на правильність використання методів, на складність методів тощо.
  3. Додавайте етапи перевірки. Включайте не тільки лінтери, але також розгортайте ваш код на sandbox-і. Ви можете запустити Docker контейнер, перевірити чи все відбулося і видалити його.
  4. Використовуйте сторонні системи, які допоможуть вам перевіряти ваш код на відповідність вимогам. Наприклад, такі системи можуть перевірити ваші плагіни на наявність слабких місць і надати вам зворотний зв’язок.

Загрози в Git-і

В інтернеті є чимало прикладів, коли Лінус Торвальд комітає щось у відкритий репозиторій, чим викликає подив і захоплення всіх, хто це бачить. Адже це сам Лінус Торвальд! Насправді, у більшості випадків це ніякий не Торвальд, а звичайна атака, коли зловмисники користується незахищеним доступом до репозиторію і вдають із себе когось іншого. Щоб вберегтися від цього ми можемо увімкнути обов’язковий підпис кожної зміни і мати GPG сертифікат, яким ми підписуємо кожен свій «git push». Таким чином, якщо ми побачимо, що підпис відрізнявся від попереднього, ми можемо його або скасувати, або принаймні відстежити спробу шахрайства.

Ще одна потенційна загроза виникає тоді, коли розробники переписують історію і виконують форс пуш. Через це можуть з’явитися проблеми з порівнянням версій, можуть не співпадати git log чи просто виникнути великі merge конфлікти, які не завжди буде легко вирішити. Щоб не допускати такого, ми можемо заборонити форс пуш у мейн бенчу чи мастер. Також, про всяк випадок, варто робити бекап репозиторіїв.

І третій пункт — ми можемо розгортати наші Git репозиторії за допомогою GitOps. Terraform підтримує Git провайдера і ми можемо описати в ньому те, яку структуру та налаштування повинен мати репозиторій. Часто користувачі не задумуються над безпекою в репозиторію і просто її ігнорують. А ми натомість форсуємо кожен репозиторій. Тобто навіть, якщо хтось з адмін правами піде і вимкне якісь галочки чи поміняє якісь параметри репозиторію, при наступному виконанні Terraform коду всі зміни будуть відновлені.

Зберігання секретів

Більшість із нас стикалася з ситуацією, коли потрібно зберегти пароль в СІ системі. Що ми робимо в таких випадках? Додаємо пароль в environment variable нашої Pipeline чи в Jenkins секрети, чи навіть Amazon секрети. І в кожному разі створюємо потенційно небезпечну ситуацію. Адже варто вам добавити print або echo в гілку, чи зробити пост реквест на endpoint — все, ваші секрети перестають бути лише вашими. Надаючи доступ розробникам до репозиторію, ви в той самий момент надаєте їм можливість побачити ваші секрети.То як варто зберігати секрети? Написати на папірці або ж записати пароль на флешку та замкнути у сейфі. Це — найбезпечніше. Інший варіант — Vault (HashiCorp Vault). Це крута штука, якою ми користувалися неодноразово, але вона несе з собою досить великий багаж налаштувань. Amazon Secret Manager — це теж непогана опція. Ми можемо її використовувати багато де, але в будь-якому випадку нам прийдеться цей секрет спочатку отримати. Ми ж все одно авторизовуємо CI Pipeline на те, щоб витягнути цей секрет. В результаті він буде розшифрований і CI система буде мати його у відкритому вигляді. Звісно, це спрацьовує не у всіх системах. Якщо вона бачить цей секрет, вона його сховає, але, на жаль, у випадку з post на endpoint вона цього не зробить. Тобто Secret Manager — це зручно, але його краще інтегрувати в Kubernetes і уникнути того, щоб використовувати плейн секрети безпосередньо в репозиторій.Моїм улюбленим інструментом є Sealed Secret. Це, по суті, також шифрований файл, який ви зберігаєте в Git-і, але, щоб його застосувати, вам потрібно створити агент Kubernetes кластерів, який буде вичитувати ваші секрети з репозиторія. Після цього він вже на стороні кластера їх розшифрує. Це такий односторонній спосіб того, щоб доставляти секрети на кластері.

Як все відбувається? За допомогою kubeseаl ви шифруєте ваш секрет і додаєте його в Git репозиторій. Відповідно, ви навіть можете його опублікувати, адже його дуже важко розшифрувати. Ви налаштовуєте агент на те, щоб він контролював ваші секрети з певного репозиторію. І, як тільки він бачить, що з’явився чи змінився ресурс з назвою SealedSecret, він автоматично створює чи оновлює аналогічний дешифрований Secret в Kubernetes кластері. Таким чином Kubernetes кластер може отримувати доступ до секретів, не беручи участь у СІ процесі. Він буде просто поза ним.

Ще одним корисним інструментом є Git Crypt. Його можна застосовувати поверх Sealed Secret. Ви просто вказуєте, які файли повинні бути шифровані і ці файли можуть бути розшифровані тільки в тому місці чи на тій машині, в якої є decryption key. Це, по суті, приватний ключ. Якщо ви просто завантажите (git clone) репозиторій, то ті файли, які були позначені Git Crypt-ом будуть зашифровані й будуть доступні тільки тим, хто має ключ шифрування.

Push та pull підходи для налаштування CD

Наразі існує два головних підходи до налаштування CD — Push-based і Pull-based.​ Давайте розглянемо їх трохи детальніше.

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

У Push-based підході у вас є репозиторій, система автоматизації — СІ система чи Jenkins, чи будь-який інший СІ сервер. Ви також маєте певний інструмент — Kubectl, Helm, Terraform тощо. І, звісно, у вас є Kubernetes та image репозиторій. Вам потрібно створити застосунок і після цього дати Kubernetes кластеру команду: «візьми цей image за допомогою деплойменту» тощо. Це є прямий Push-based підхід. Чому Push-based? Бо ви напряму кажете: «СІ система, візьми і додай собі ці зміни».

Тепер давайте розглянемо Рull-based підхід.Push-based базується на децентралізований системі доступів. Тобто СІ система має доступ тільки до репозиторіїв та до AWS ECR чи будь-якого іншого Docker репозиторію, чи до репозиторію артефактів. Кожний етап розділений і ніхто не заходить за його межі. У випадку, якщо хтось втрутиться, він зможе отримати доступ чи тільки до Image Repo, чи до якогось іншого кроку. До кластера доступ є тільки у DevOps/Infrastructure інженерів, а основні зміни можуть попадати тільки після проходження всіх етапів перевірок та попадання в основну гілку і відповідно з репозиторіїв, які дозволені в кластер агенті.Можна розглянути Pull-based систему на основі Agro-CD. Це агент, який допомагає нам застосовувати зміни в Kubernetes кластері. За допомогою Agro-CD ми можемо отримати Multicluster архітектуру. Скажімо, ми маємо якийсь мастер кластер, в якого є централізований моніторинг, логування чи деплоймент. Ви додаєте інші агенти в інші кластери і вказуєте, що ваш мастер знаходиться там. Це все налаштовується елементарним встановленням агента. Для того, щоб налаштовувати кілька кластерів одночасно використовується Apps pattern. Це, по суті, така тавтологія. Це набір застосунків, який має в собі інші застосунки. Звісно, що Agro-CD повинен мати доступ до тих чи інших кластерів, але сертифікат і key повинен бути збережений в ньому.

В Agro-CD ми налаштовуємо елементарні речі, наприклад, назву застосунку, namespace завжди має бути agrocd. Тобто ми вказуємо Git репозиторій і, відповідно, вказуємо шлях, де лежить наш Helm чарт чи наші yaml файли. Ми також маємо finalizer в кожного ресурсу, який створений Agro-CD. Що він нам дає? Коли ми видаляємо застосунок, ми можемо отримати orphan ресурси. Тобто застосунок створив за собою деплоймент, Horizontal Pod Autoscaler, купу сервісів, секретів і різних ресурсів. Якщо ми просто видалимо застосунок, то вони так і лишаться неконтрольовані. Тобто, додаючи finalizer, Agro-CD отримує сповіщення про те, що цей застосунок повинен бути видалений. Тоді він спочатку видаляє всі ресурси, які були створені цим застосунком, а після того вже видаляє цей застосунок із кластера.

Сам же AppProject — це, по суті, логічна одиниця, в якій ми описуємо структуру застосунка. Ми вказуємо куди вона має розгортатися, точніше на який кластер. Також namespace, який буде дозволений і в який відбудеться розгортання. Ми також зазначаємо репозиторії, з яких можливі зміни. І основне — права, які ми даємо тому чи іншому проєкту. Припустимо, в нас є кілька проєктів, які мають свої репозиторії і над якими працюють різні девелопери. Але нам треба якось їх обмежити. За допомогою APP Project ми створюємо їм певні обмеження, наприклад, дозволяємо їм деплоїти тільки в одному namespace. В останньому пункті ClusterResourceWhitelist ми додаємо те, що можна робити в межах цього проєкту. Ми там вказуємо, до прикладу, що тільки для певного namespace цей проєкт зможе працювати. Якщо ми захочемо створити якусь кластер роль, ми просто цього зробити не зможемо. Таким чином ми захищаємо себе від несанкціонованого доступу з боку розробників. Якщо вони захочуть розширити свої права в межах кластеру, вони просто цього зробити не зможуть.

Демо

Висновки

GitOps — це хороший спосіб покращити процес доставки змін та застосунків в Kubernetes кластер. Але варто врахувати, що він підійде не всім. Його потрібно впроваджувати комплексно. Тобто вам потрібно підготувати зміни до Git Workflow, до інфраструктури, до CI системи чи навіть зовсім від неї відмовитися (!). Також потрібно буде провести тренінг розробникам, а то й розробити додаткову документацію. І, звісно, пояснити клієнту для чого ви це робите. Вставновити ArgoCD чи FluxCD агента це 5 хвилин, а стабільна і налагоджена робота на всіх етапах виконання автоматизації може тривати й кілька місяців!

Все ж я рекомендую кожному, хто працює чи планує працювати з Kubernetes, спробувати реалізувати POC GitOps, хоча б на Minikube. За цим майбутнє!

Дякую всім хто дочитав до цього моменту і пам’ятайте «With great power comes great responsibility!».

👍НравитсяПонравилось9
В избранноеВ избранном4
LinkedIn
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

чи є можливість розшарити репи які використовуються в демо?

Ми ж все одно авторизовуємо CI Pipeline на те, щоб витягнути цей секрет.

для таких случаев вешается роль на EC2

Це у випадку, якщо ваша CI система, або ранери хоститься у вашому AWS акаунті. А якщо у вас все онсайт, то IAM роль не буде куди добавити.

Як все відбувається? За допомогою kubeseаl ви шифруєте ваш секрет і додаєте його в Git репозиторій. Відповідно, ви навіть можете його опублікувати, адже його дуже важко розшифрувати. Ви налаштовуєте агент на те, щоб він контролював ваші секрети з певного репозиторію. І, як тільки він бачить, що з’явився чи змінився ресурс з назвою SealedSecret, він автоматично створює чи оновлює аналогічний дешифрований Secret в Kubernetes кластері. Таким чином Kubernetes кластер може отримувати доступ до секретів, не беручи участь у СІ процесі. Він буде просто поза ним.

Может, я не так понял идею... Если ключи генерируются автоматически внутри кластера, то что будет, если его снести? Это автоматически сделает все секреты в git’е невалидными, т.к. ключи уже на небесах?

Привіт, дякую за питання.
Ключ шифрування можна забекапити для майбутнього відновлення у випадку втрати кластера.
Більше мона дізнатися тут:
github.com/...​offline-with-a-backup-key

Спасибо. Т.к. статья имеет определённый уклон в технологиях, позволю себе то же самое). Поэтому для себя как пользователя Azure и .NET’чика в данный момент я бы точно такое не использовал по причине слишком большой сложности и сомнительного реального профита. Причин несколько, и все проистекают из того, что секреты секретам — рознь:
1.1. Managed Identities, через которые будут (и должны) настраиваться, грубо говоря, 90% всех доступов, не содержат секретов в принципе, и задаются через взаимные ссылки на ресурсы в IaC. Даже DevOps’ы не увидят секретов, если их нет. Мы отдаём менеджмент на откуп облака, но если даже облако взломают, то за какой-то конкретный сектор безопасности волноваться уже нет смысла, поэтому можно это делать со спокойной душой — облака в любом случае в среднем круче, чем вы (мы).
1.2. Секреты, которые всё же нужно будет использовать явно (грубо говоря — 10%), можно писать в Key Vault. При этом в подавляющем большинстве приложений брать секреты из Key Vault можно либо через декларативные ссылки на него в обычной конфигурации, либо через ссылки на него же в Azure App Configuration. Соответственно, записывать секреты будем:
1.2.1. Через тот же IaC, если они берутся из внутренних ресурсов.
1.2.2. Из secure variables, если нет нативного доступа к ресурсам — только через голые секреты и больше никак. Мало того, сами variable groups могут быть подвязаны к конкретному Key Vault’у. Насколько я понял, в статье указана похожая схема для AWS, и приводится в пример, что из CI/CD пайплайна можно будет их заэкспоузить, и это вроде как проблема. Но я не вижу здесь проблемы, и вот почему: к IaC, CI/CD пайплайнам и переменным на проде, если Вы не даёте туда лезть разработчикам, у них и так не будет доступа, кроме разве что кода, который вы же (DevOps’ы) и зааппрувите (по крайней мере, в Azure DevOps доступы даже к variable group’ам можно настроить гранулярно). Таким образом, единственные люди, от которых может что-то утечь — это вы и есть, и что у вас будут в кластере права читать секреты, что где-то ещё, уже не составляет никакой разницы.
1.3. Если говорить конкретно про AKS (Кубер), то для него есть AAD Pod Identity, что является вариацией п.1.1.
1.4. В Automation Account`ах есть encrypted variables, плюс в разных типах ресурсов может быть ещё что-то своё, но с точки зрения статьи разработчиков там и не ноги не будет — только DevOps’ы.
При этом в целом вероятность «потерять» серкет в целом плюс-минус одинаковая, и через Azure Key Vault даже меньше, т.к. там включён soft delete по дефолту, а через SealedSecret забыли сделать бэкап, кто-то случайно удалил кластер, и всё.
Плюс в том же документе по Вашей ссылке указано, что SealedSecret всё же не предназначены для долгосрочного хранения в репо, что сильно ограничивает область применения. И в ином случае хорошо бы делать парный rotation для ключей и сразу всех секретов каждый месяц.
Поэтому в случае Azure я пока против того, чтобы городить подобный огород.

GitOps звісно не всім потрібен. Якщо ви не бачите в ньому потребу, то й не потрібно його інтегровувати. І я радий що є інші точки зору. Не може все бути ідеальним :)

А стосовно

AAD Pod Identity,

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

Також хочу добавити що є різні моделі відповідальності на проекті між розробниками та системними інженерами (чи просто девопсами).
В прикладі який я приводив на демо, це коли розробники мають свій репозиторій з хелм чартом або просто yaml файлом і воно його ж самі розробляють і несуть відповідальність на секрети (third-pary провайдери, api токени, та інше) так і за та що попаде на продакшин.

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

Як на мене то GitOps ОК лише там де є активна розробка нового проекту або вяла підтримки існуючого. Як тільки проект переходить в стан активної експлуатації з постійною доробкою і переробкою, то починаються проблеми які вирішуються ускладненням різних процесів розробки і все стає не дуже ОК (поріг входу, збільшення часу на розробку та тестування).
Відчуття що не вистачає винесення тригерів деплоя на більш мануальний рівень (також можливо що я про щось не здогадуюсь, тому все ускладнюю))).

Поріг входу можливо буде вищий для DevOps/Infrastructure інженерів, так як прийдеться розбиратися з новою системою. Тестування можна виконувати на дев середовищі, до якого в іженера може бути повний доступ, щоб не робити 100 комітів, щоб відтестувати ту чи іншу фічу і вже на вищий стейджах доставляти тільки кодом.

Мануальний тригер також присутнійю Ось на демо я демонтрую як можна швидко запустити процес оновлення з git-а:
youtu.be/Qhy1lxKbLHM?t=1019

Для розробників GitOps навпаки зменшує поріг входу, тому, що їм не потрібно розбиратися з кластером. Їх робота буде тільки в оновленні yaml файлів в репозиторії на основі інших прикладів.
Проблема може бути тільки в розумінні «як це працює», але це вже індивідуально.

Подписаться на комментарии