werf — наш інструмент для CI/CD у Kubernetes (огляд та відео доповіді)

27 травня у головному залі конференції DevOpsConf 2019, що проходить у рамках фестивалю РІТ++ 2019, в рамках секції «Безперервне постачання», прозвучала доповідь «werf – наш інструмент для CI/CD у Kubernetes». У ньому розповідається про тих проблем і викликів, з якими стикається кожен при депло в Kubernetes, а також про нюанси, які можуть бути помітні не одразу. Розбираючи можливі шляхи вирішення, ми показуємо, як це реалізовано в Open Source-інструменті werf.

З моменту виступу наша утиліта (раніше відома як dapp) подолала історичний рубіж у 1000 зірок GitHub — ми сподіваємося, що зростаюча спільнота її користувачів спростить життя багатьом DevOps-інженерам.

werf — наш інструмент для CI/CD у Kubernetes (огляд та відео доповіді)

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

Доставка коду в Kubernetes

Мова у доповіді піде більше не про werf, а про CI/CD у Kubernetes, маючи на увазі, що наш софт упакований у Docker-контейнери (про це я розповідав у доповіді 2016 року)а K8s буде використовуватися для його запуску в production (про це - в 2017 році).

Яка доставка в Kubernetes?

  • Є Git-репозиторій з кодом та інструкціями для його збирання. Програма збирається в Docker образ і публікується в Docker Registry.
  • У тому ж репозиторії є інструкції про те, як додаток деплоїти і запускати. На стадії деплою ці інструкції відправляються Kubernetes, який отримує потрібний образ з registry і запускає його.
  • Плюс, зазвичай, є тести. Деякі з них можна виконувати під час публікації образу. Також можна (за тими ж інструкціями) розгорнути копію програми (в окремому просторі імен K8s або окремому кластері) та запускати тести там.
  • Нарешті, потрібна CI-система, яка отримує події з Git'а (або натискання кнопок) і викликає всі ці стадії: build, publish, deploy, test.

werf — наш інструмент для CI/CD у Kubernetes (огляд та відео доповіді)

Тут є кілька важливих зауважень:

  1. Оскільки у нас незмінна інфраструктура (Immutable infrastructure), образ програми, що використовується на всіх етапах (staging, production і т.п.), має бути один. Докладніше про це і з прикладами я розповідав тут.
  2. Оскільки ми слідуємо підходу інфраструктура як код (IaC), код програми, інструкції для його складання та запуску повинні лежати саме в одному репозиторії. Докладніше про це див. тій же доповіді.
  3. Ланцюжок доставки (Delivery) ми зазвичай бачимо так: програму зібрали, протестували, релізнули (Етап release) і все – доставка відбулася. Але насправді користувач отримує те, що ви викотили, НЕ тоді, коли ви це доставили у production, а коли він зміг туди зайти і цей production працював. Тому я вважаю, що ланцюжок доставки закінчується лише на етапі експлуатації (бігти)а якщо говорити точніше, то навіть у той момент, коли код прибрали з production (замінивши його на новий).

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

Стадія збирання (build)

Здавалося б, що можна розповісти у 2019 році про складання Docker-образів, коли всі вміють писати Dockerfile'и та запускати docker build?.. Ось нюанси, на які хотілося б звернути увагу:

  1. Вага образу має значення, тому використовуйте багатоступінчастий, щоб залишити в образі тільки дійсно необхідне роботи програми.
  2. кількість шарів треба мінімізувати, поєднуючи ланцюжки з RUN-команд за змістом
  3. Однак це додає проблем налагодження, оскільки при падінні збірки доводиться відшукувати ту потрібну команду з ланцюжка, який викликав проблему.
  4. Швидкість складання важлива, тому що ми хочемо швидко викочувати зміни та дивитися на результат. Наприклад, не хочеться перезбирати залежності в бібліотеках мови при кожній збірці програми.
  5. Найчастіше з одного Git-репозиторію потрібні багато образів, що можна вирішити набором з Dockerfile'ів (або іменованими стадіями в одному файлі) та Bash-скриптом з їхньою послідовною збіркою.

Це була лише верхівка айсберга, з якою стикаються усі. Але є й інші проблеми, зокрема:

  1. Найчастіше на стадії збирання нам потрібно щось примонтувати (наприклад, закешувати результат команди типу apt'а до сторонньої директорії).
  2. Ми хочемо Неможливо замість того, щоб писати на shell'і.
  3. Ми хочемо збирати без Docker (навіщо нам додаткова віртуальна машина, в якій треба все для цього налаштовувати, коли вже є кластер Kubernetes, у якому можна запускати контейнери?).
  4. Паралельна збірка, яку можна розуміти по-різному: різні команди з Dockerfile (якщо використовується multi-stage), кілька коммітів одного репозиторію, кілька Dockerfile'ів.
  5. Розподілене складання: ми хочемо збирати щось у pod'ах, які є «ефемерними», т.к. у них пропадає кеш, а значить його треба зберігати десь окремо.
  6. Зрештою, вершину бажань я назвав автомагією: ідеально було б зайти до репозиторію, набрати якусь команду та отримати готовий образ, зібраний з розумінням того, як і що правильно зробити. Втім, особисто я не впевнений, що всі нюанси можна так передбачити.

І ось є проекти:

  • moby/buildkit - Складальник від компанії Docker Inc (вже інтегрований в актуальні версії Docker), яка намагається вирішити всі ці проблеми;
  • каніко - Складальник від Google, що дозволяє збирати без Docker;
  • Buildpacks.io - Спроба CNCF зробити автомагію і, зокрема, цікаве рішення з rebase для шарів;
  • і ще купа інших утиліт, таких як будівництво, genuinetools/img...

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

Складання в werf

Так ми підібралися werf (раніше відомої як dapp) — Open Source-утиліта компанії «Флант», яку ми робимо вже багато років. Починалося все років 5 тому з Bash-скриптів, які оптимізують збірку Dockerfile'ів, а останні 3 роки ведеться повноцінна розробка в рамках одного проекту зі своїм Git-репозиторієм (спочатку на Ruby, а потім переписали на Go, а заразом і перейменували). Які питання зборки вирішено у werf?

werf — наш інструмент для CI/CD у Kubernetes (огляд та відео доповіді)

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

Стадія публікації у registry (publish)

Набрали docker push… — що може бути складним у тому, щоб завантажити образ у registry? І тут постає питання: «Який тег поставити образу?» Виникає він через те, що в нас є Gitflow (або інша стратегія Git'а) і Kubernetes, а індустрія прагне того, щоб те, що відбувається в Kubernetes, було те, що робиться в Git. Адже Git – наше єдине джерело правди.

Що у цьому складного? Гарантувати відтворюваність: від комміту в Git, який за своєю природою незмінний (Immutable), до образу Docker, який повинен зберігатися таким самим.

Нам також важливо визначати походження, тому що ми хочемо розуміти, з якого комміту було зібрано програму, запущену в Kubernetes (тоді ми зможемо робити diff'и та подібні речі).

Стратегії тегування

Перша - це простий git тег. У нас є registry з тегованим чином як 1.0. У Kubernetes є stage і production, куди цей образ викачується. У Git ми робимо комміти і в якийсь момент ставимо тег 2.0. Збираємо його за інструкціями з репозиторію і поміщаємо до registre з тегом 2.0. Викочуємо на stage і, якщо все добре, потім на production.

werf — наш інструмент для CI/CD у Kubernetes (огляд та відео доповіді)

Проблема такого підходу в тому, що ми спочатку поставили тег, а потім протестували і викотили. Чому? По-перше, це просто нелогічно: ми видаємо версію софту, який ще навіть не перевіряли (не можемо зробити інакше, тому що для того, щоб перевірити, потрібно поставити тег). По-друге, такий шлях не поєднується з Gitflow.

Другий варіант - git commit + tag. У master-гілці є тег 1.0; йому registry — образ, розгорнутий на production. Крім того, у Kubernetes-кластері є контури preview та staging. Далі ми прямуємо Gitflow: в основній гілці для розробки (develop) робимо нові фічі, внаслідок чого з'являється коміт з ідентифікатором #c1. Ми його збираємо та публікуємо в registry, використовуючи цей ідентифікатор (#c1). З таким самим ідентифікатором викочуємо на preview. Аналогічно робимо із коммітами #c2 и #c3.

Коли зрозуміли, що фічі достатньо, починаємо все стабілізувати. У Git створюємо гілку release_1.1 (на базі #c3 з develop). Збирати цей реліз не потрібно, т.к. це було зроблено на попередньому етапі. Тому можемо просто викотити її на staging. Виправляємо баги в #c4 і аналогічно викочуємо на staging. Паралельно в той же час йде розробка develop, куди періодично забираються зміни з release_1.1. В якийсь момент отримуємо зібраний та викачений на staging коміт, яким ми задоволені (#c25).

Тоді ми робимо merge (з fast-forward'ом) релізної гілки (release_1.1) у master. Ставимо на цей коміт тег із новою версією (1.1). Але цей образ вже зібраний у registry, тому, щоб не збирати його ще раз, ми просто додаємо другий тег на існуючий образ (тепер він у registry має теги #c25 и 1.1). Після цього викочуємо його на production.

Є недолік, що на staging викачали один образ (#c25), а на production - як би інший (1.1), але ми знаємо, що «фізично» це той самий образ з registry.

werf — наш інструмент для CI/CD у Kubernetes (огляд та відео доповіді)

Справжній мінус у тому, що немає підтримки merge commit'ів, треба робити fast-forward.

Можна піти далі і зробити трюк… Розглянемо приклад простого Dockerfile:

FROM ruby:2.3 as assets
RUN mkdir -p /app
WORKDIR /app
COPY . ./
RUN gem install bundler && bundle install
RUN bundle exec rake assets:precompile
CMD bundle exec puma -C config/puma.rb

FROM nginx:alpine
COPY --from=assets /app/public /usr/share/nginx/www/public

Побудуємо з нього файл за таким принципом, що візьмемо:

  • SHA256 від ідентифікаторів використовуваних образів (ruby:2.3 и nginx:alpine), які є контрольними сумами їхнього вмісту;
  • всі команди (RUN, CMD і т.п.);
  • SHA256 від доданих файлів.

… та візьмемо контрольну суму (знову SHA256) від такого файлу. Це сигнатура всього, що визначає вміст Docker-образу.

werf — наш інструмент для CI/CD у Kubernetes (огляд та відео доповіді)

Повернемося до схеми та замість коммітів будемо використовувати такі сигнатури, тобто. тегувати образи сигнатурами.

werf — наш інструмент для CI/CD у Kubernetes (огляд та відео доповіді)

Тепер, коли потрібно, наприклад, з'merge'ити зміни з релізу в master, ми можемо робити справжній merge commit: у нього буде інший ідентифікатор, але та сама сигнатура. З таким же ідентифікатором ми викотимо образ і на виробництво.

Недоліком є ​​те, що тепер не вдасться визначити, що за коміт викачено на production — контрольні суми працюють лише в один бік. Ця проблема вирішується додатковим шаром із метаданими — докладніше розповім далі.

Тегування у werf

У werf ми пішли ще далі і готуємося зробити розподілене складання з кешем, який не зберігається на одній машині… Отже, у нас збираються Docker-образи двох типів, ми називаємо їх етап и зображення.

У Git-репозиторії werf зберігаються специфічні інструкції для збирання, що описують різні етапи збирання (beforeInstall, встановлювати, beforeSetup, установка). Перший stage-образ ми збираємо з сигнатурою, визначеною як контрольна сума перших кроків. Потім додаємо вихідний код, для нового stage-образу ми рахуємо його контрольну суму… Ці операції повторюються для всіх етапів, в результаті чого ми отримуємо набір з stage-образів. Потім робимо фінальний image-образ, що містить також метадані про його походження. І вже цей образ ми тегуємо у різний спосіб (подробиці пізніше).

werf — наш інструмент для CI/CD у Kubernetes (огляд та відео доповіді)

Нехай після цього з'являється новий коміт, в якому змінили код програми. Що станеться? Для зміни коду буде створено патч, підготовлений новий stage-образ. Його сигнатура буде визначена як контрольна сума старого stage-образу та нового патчу. З цього образу буде сформовано й новий фінальний image-образ. Аналогічна поведінка відбуватиметься за змін на інших етапах.

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

werf — наш інструмент для CI/CD у Kubernetes (огляд та відео доповіді)

Очищення registry

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

Які стратегії очищення?

  1. Можна просто нічого не чистити. Іноді справді простіше трохи заплатити за зайве простір, ніж розплутувати величезний клубок із тегів. Але це працює лише до певного моменту.
  2. Повне скидання. Якщо видалити всі образи і перезбирати лише актуальні в CI-системі, може виникнути проблема. Якщо на production перезапуститься контейнер, для нього завантажиться новий образ, такий, що ще ніким не тестувався. Це вбиває ідею неабиякого infrastructure.
  3. Синьо-зелений. Один registre почав переповнюватися - завантажуємо образи в інший. Така сама проблема, що в попередньому способі: в який момент можна очищати той резерв, який почав переповнюватися?
  4. По часу. Видаляти всі образи старше 1 місяця? Але обов'язково знайдеться сервіс, який не оновлювався цілий місяць.
  5. вручну визначати, що можна видаляти.

По-справжньому життєздатних варіанти два: не чистити або комбінація з blue-green + вручну. В останньому випадку йдеться про наступне: коли ви розумієте, що настав час почистити registry, створюєте новий і додаєте все нові образи в нього протягом, наприклад, місяця. А через місяць дивіться, які pod'и в Kubernetes, як і раніше, використовують старий registry, і переносіть їх теж у новий registry.

До чого ми прийшли у werf? Ми збираємо:

  1. Git head: всі теги, всі гілки, - припускаючи, що все, що протеговано в Git, нам потрібно і в образах (а якщо ні, то треба видалити в самому Git'е);
  2. всі pod'и, які викачані зараз у Kubernetes;
  3. старі ReplicaSet'и (те, що нещодавно було викачено), а також плануємо сканувати Helm-релізи та відбирати останні образи там.

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

Стадія деплою (deploy)

Надійна декларативність

Перший момент, на який хотілося б звернути увагу в деплої, — викочування оновленої конфігурації ресурсів, оголошеної декларативно. Оригінальний документ YAML з описом Kubernetes-ресурсів завжди сильно відрізняється від результату, що реально працює в кластері. Тому що Kubernetes додає до конфігурації:

  1. ідентифікатори;
  2. службову інформацію;
  3. безліч значень за замовчуванням;
  4. секцію із поточним статусом;
  5. зміни, зроблені в рамках роботи admission webhook;
  6. результат роботи різних контролерів (та планувальника).

Тому, коли з'являється нова конфігурація ресурсу (new), ми можемо просто взяти і перезаписати нею поточну, «живу», конфігурацію (жити). Для цього нам доведеться порівняти new з минулою застосованою конфігурацією (last-applied) і накотити на жити отриманий патч.

Такий підхід називається 2-way merge. Він використовується, наприклад, у Helm.

Є ще й 3-way merge, який відрізняється тим, що:

  • порівнюючи last-applied и newми дивимося, що було видалено;
  • порівнюючи new и житими дивимося, що додано або змінено;
  • сумований патч накладаємо на жити.

Ми деплоїмо 1000+ програм з Helm, тому фактично живемо з 2-way merge. Однак у нього є низка проблем, які ми вирішили своїми патчами, які допомагають Helm'у нормально працювати.

Реальний статус викату

Після того, як за черговою подією, наша CI-система згенерувала нову конфігурацію для Kubernetes, вона передає її на застосування (apply) в кластер - за допомогою Helm або kubectl apply. Далі відбувається вже описаний N-way merge, на що Kubernetes API схвально відповідає CI-системі, а та – своєму користувачеві.

werf — наш інструмент для CI/CD у Kubernetes (огляд та відео доповіді)

Однак є величезна проблема: адже успішне застосування не означає успішний викочування. Якщо Kubernetes зрозумів, що зміни треба застосувати, застосовує його — ми ще не знаємо, що вийде в результаті. Наприклад, оновлення та рестарт pod'ів у frontend'і може пройти успішно, а в backend'і - ні, і ми отримаємо різні версії запущених образів програми.

Щоб все робити правильно, у цій схемі напрошується додаткова ланка - спеціальний трекер, який отримуватиме від Kubernetes API інформацію про статус і передаватиме її для подальшого аналізу реального стану речей. Ми створили Open Source-бібліотеку на Go kubedog (Див. її анонс тут), - яка вирішує цю проблему і вбудована у werf.

Поведінка цього трекера на рівні werf налаштовується за допомогою анотацій, які ставляться на Deployments або StatefulSets. Головна інструкція fail-mode - розуміє такі значення:

  • IgnoreAndContinueDeployProcess - ігноруємо проблеми викочування цього компонента і продовжуємо деплою;
  • FailWholeDeployProcessImmediately - Помилка в цьому компоненті зупиняє процес деплою;
  • HopeUntilEndOfDeployProcess - Сподіваємося, що цей компонент запрацює до кінця деплою.

Наприклад, така комбінація з ресурсів та значень анотації fail-mode:

werf — наш інструмент для CI/CD у Kubernetes (огляд та відео доповіді)

Коли деплоїм вперше, база даних (MongoDB) ще може бути не готова - Deployment'и впадуть. Але можна дочекатися моменту, щоб вона запустилася, і деплой все ж таки пройде.

Є ще дві анотації для kubedog в werf:

  • failures-allowed-per-replica - кількість дозволених падінь на кожну репліку;
  • show-logs-until — регулює момент, до якого werf показує (в stdout) логи з усіх котованих pod'ів. За умовчанням це PodIsReady (щоб ігнорувати повідомлення, які нам навряд чи потрібні, коли на pod починає приходити трафік), проте допустимі також значення ControllerIsReady и EndOfDeploy.

Що ми хочемо від деплою?

Крім уже описаних двох пунктів нам хотілося б:

  • бачити логі - причому тільки потрібні, а не всі поспіль;
  • відслідковувати прогрес, тому що якщо job "мовчки" висить кілька хвилин, важливо розуміти, що там відбувається;
  • мати автоматичний відкат на випадок, якщо щось пішло негаразд (а тому критично знати реальний статус деплою). Викочування має бути атомарним: або він проходить до кінця, або все повертається до колишнього стану.

Підсумки

Нам як компанії для реалізації всіх описаних нюансів на різних етапах доставки (build, publish, deploy) достатньо CI-системи та утиліти werf.

Замість висновку:

werf — наш інструмент для CI/CD у Kubernetes (огляд та відео доповіді)

За допомогою werf ми непогано просунулися у вирішенні великої кількості проблем DevOps-інженерів і будемо раді, якщо ширша спільнота хоча б спробує цю утиліту у справі. Досягти хорошого результату разом буде простіше.

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

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

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

PS

Інші доповіді про Kubernetes у нашому блозі:

Джерело: habr.com

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