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 tag. У нас ёсць registry з чынам, тэгіраваным як 1.0. У Kubernetes ёсць stage і production, куды гэта выява выпампаваны. У Git мы робім коміты і ў нейкі момант ставім тэг 2.0. Збіраны яго па інструкцыям з рэпазітара і змяшчаем у registry з тэгам 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.

Недахоп у тым, што зараз не атрымаецца вызначыць, што за коміт выпампаваны на 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 перазапусціцца кантэйнер, для яго загрузіцца новы вобраз такі, што яшчэ нікім не тэставаўся. Гэта забівае ідэю immutable infrastructure.
  3. Сіне-зялёны. Адзін registry пачаў перапаўняцца - загружаем выявы ў іншы. Тая ж самая праблема, што ў папярэднім спосабе: у які момант можна чысціць той registry, што пачаў перапаўняцца?
  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. вынік працы розных кантролераў (і планавальніка).

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

Такі падыход называецца 2-way merge. Ён выкарыстоўваецца, напрыклад, у Helm.

Ёсць яшчэ і 3-way merge, які адрозніваецца тым, што:

  • параўноўваючы last-applied и новы, мы глядзім, што было выдалена;
  • параўноўваючы новы и жыць, мы глядзім, што дададзена або зменена;
  • суміраваны патч накладваем на жыць.

Мы дэплоім 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

Дадаць каментар