27 мая ў галоўнай зале канферэнцыі DevOpsConf 2019, якая праходзіць у рамках фестывалю , у рамках секцыі «Бесперапынная пастаўка», прагучаў даклад «werf – наш інструмент для CI/CD у Kubernetes». У ім распавядаецца пра тых праблемах і выкліках, з якімі сутыкаецца кожны пры дэплоі ў Kubernetes, а таксама аб нюансах, якія могуць быць прыкметныя не адразу. Разбіраючы магчымыя шляхі рашэння, мы паказваем, як гэта рэалізавана ў Open Source-інструменте .
З моманту выступу наша ўтыліта (раней вядомая як dapp) пераадолела гістарычную мяжу ў 1000 зорак на GitHub - Мы спадзяемся, што расце супольнасць яе карыстальнікаў спросціць жыццё шматлікім DevOps-інжынерам.

Такім чынам, уяўляем (~47 хвілін, значна інфарматыўныя артыкулы) і асноўны выцісканне з яго ў тэкставым выглядзе. Паехалі!
Дастаўка кода ў Kubernetes
Гаворка ў дакладзе пойдзе больш не пра werf, а пра CI/CD у Kubernetes, маючы на ўвазе, што наш софт спакаваны ў Docker-кантэйнеры (пра гэта я расказваў у ), а K8s будзе выкарыстоўвацца для яго запуску ў production (пра гэта - у ).
Як выглядае дастаўка ў Kubernetes?
- Ёсць Git-рэпазітар з кодам і інструкцыямі для яго зборкі. Дадатак збіраецца ў Docker-выява і публікуецца ў Docker Registry.
- У тым жа рэпазітары ёсць інструкцыі і аб тым, як прыкладанне дэплоіць і запускаць. На стадыі дэплою гэтыя інструкцыі адпраўляюцца ў Kubernetes, які атрымлівае патрэбную выяву з registry і запускае яго.
- Плюс, звычайна есць тэсты. Некаторыя з іх можна выконваць пры публікацыі выявы. Таксама можна (па тым жа інструкцыям) разгарнуць копію прыкладання (у асобнай прасторы імёнаў K8s або асобным кластары) і запускаць тэсты тамака.
- Нарэшце, патрэбна CI-сістэма, якая атрымлівае падзеі з Git'а (ці націскі кнопак) і выклікае ўсе пазначаныя стадыі: build, publish, deploy, test.

Тут ёсць некалькі важных заўваг:
- Паколькі ў нас нязменная інфраструктура (immutable infrastructure), выява прыкладання, што выкарыстоўваецца на ўсіх этапах (staging, production і да т.п.), павінен быць адзін. Больш падрабязна пра гэта і з прыкладамі я расказваў .
- Паколькі мы прытрымліваемся падыходу інфраструктура як код (IaC), код прыкладання, інструкцыі для яго зборкі і запуску павінны ляжаць менавіта ў адным рэпазітары. Больш падрабязна пра гэта - гл. .
- Ланцужок дастаўкі (delivery) мы звычайна бачым так: дадатак сабралі, пратэставалі, рэлізнулі (этап release) і ўсё - дастаўка адбылася. Але ў рэчаіснасці ж карыстач атрымлівае тое, што вы выкацілі, ня тады, калі вы гэта даставілі ў production, а калі ен змог туды зайсці і гэты production працаваў. Таму я лічу, што ланцужок дастаўкі заканчваецца толькі на этапе эксплуатацыі (бег), а калі казаць дакладней, то нават у той момант, калі код прыбралі з production (замяніўшы яго на новы).
Вернемся да пазначанай вышэй схемы дастаўкі ў Kubernetes: яе вынайшлі не толькі мы, але і літаральна кожны, хто займаўся дадзенай праблемай. Па сутнасці гэты патэрн зараз называюць GitOps (падрабязней пра тэрмін і якія стаяць за ім ідэямі можна прачытаць ). Паглядзім на этапы схемы.
Стадыя зборкі (build)
Здавалася б, што можна расказаць у 2019 годзе пра зборку Docker-вобразаў, калі ўсе ўмеюць пісаць Dockerfile'ы і запускаць. docker build?.. Вось нюансы, на якія хацелася б звярнуць увагу:
- Вага выявы мае значэнне, таму выкарыстоўвайце , Каб пакінуць у вобразе толькі сапраўды патрэбнае для працы прыкладання.
- Колькасць пластоў трэба мінімізаваць, аб'ядноўваючы ланцужкі з
RUN-каманд па сэнсе. - Аднак гэта дадае праблем адладцы, паколькі пры падзенні зборкі даводзіцца адшукваць тую патрэбную каманду з ланцужка, які выклікаў праблему.
- Хуткасць зборкі важная, таму што мы хочам хутка выкочваць змены і глядзець на вынік. Напрыклад, не жадаецца перазбіраць залежнасці ў бібліятэках мовы пры кожнай зборцы прыкладання.
- Часцяком з аднаго Git-рэпазітара патрабуюцца шмат вобразаў, што можна вырашыць наборам з Dockerfile'ов (ці найменнымі стадыямі ў адным файле) і Bash-скрыптам з іх паслядоўнай зборкай.
Гэта была толькі вярхушка айсберга, з якой сутыкаюцца ўсе. Але ёсць і іншыя праблемы, а ў прыватнасці:
- Часцяком на стадыі зборкі нам трэба нешта прымантаваць (напрыклад, закэшаваць вынік каманды тыпу apt'а ў іншую дырэкторыю).
- Мы хочам анзибль замест таго, каб пісаць на shell'е.
- Мы хочам збіраць без Docker (навошта нам дадатковая віртуальная машына, у якой трэба ўсё для гэтага наладжваць, калі ўжо ёсць кластар Kubernetes, у якім можна запускаць кантэйнеры?).
- Паралельная зборка, якую можна разумець па-рознаму: розныя каманды з Dockerfile (калі выкарыстоўваецца multi-stage), некалькі комітаў аднаго рэпазітара, некалькі Dockerfile'ов.
- Размеркаваная зборка: мы хочам збіраць нешта ў pod'ах, якія з'яўляюцца "эфемернымі", т.я. у іх знікае кэш, а значыць - яго трэба захоўваць дзесьці асобна.
- Нарэшце, вяршыню жаданняў я назваў аўтамагіяй: ідэальна было б зайсці ў рэпазітар, набраць нейкую каманду і атрымаць гатовы вобраз, сабраны з разуменнем таго, як і што правільна зрабіць. Зрэшты, асабіста я не ўпэўнены, што ўсе нюансы так можна прадугледзець.
І вось ёсць праекты:
- - зборшчык ад кампаніі Docker Inc (ужо інтэграваны ў актуальныя версіі Docker), якая спрабуе вырашыць усе гэтыя праблемы;
- - зборшчык ад Google, які дазваляе збіраць без Docker;
- - Спроба CNCF зрабіць аўтамагію і, у прыватнасці, цікавае рашэнне з rebase для пластоў;
- і яшчэ куча іншых утыліт, такіх як , ...
… і паглядзіце, колькі ў іх зорак на GitHub. Гэта значыць, з аднаго боку, docker build ёсць і можа нешта зрабіць, але ў рэчаіснасці-то пытанне да канца не вырашана - Доказам гэтаму і служыць паралельнае развіццё альтэрнатыўных зборшчыкаў, кожны з якіх вырашае нейкую частку праблем.
Зборка ў werf
Так мы падабраліся да (раней як dapp) - Open Source-ўтыліце кампаніі «Флант», якую мы робім ужо шмат гадоў. Пачыналася ўсё гадоў 5 таму з Bash-скрыптоў, якія аптымізуюць зборку Dockerfile'аў, а апошнія 3 гады вядзецца паўнавартасная распрацоўка ў рамках аднаго праекту са сваім Git-рэпазітаром (спачатку на Ruby, а потым на Go, а заадно і перайменавалі). Якія пытанні зборкі вырашаны ў werf?

Зафарбаваныя сінім праблемы ўжо рэалізаваны, паралельная зборка зроблена ў рамках аднаго хаста, а выдзеленыя жоўтым пытанні плануем дарабіць да канца лета.
Стадыя публікацыі ў 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.

Праблема такога падыходу ў тым, што мы спачатку паставілі тэг, а толькі потым пратэставалі і выкацілі. Чаму?
Другі варыянт - 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.

Сапраўдны ж мінус у тым, што няма падтрымкі 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-выявы.

Вернемся да схемы і замест коммітаў будзем выкарыстоўваць такія сігнатуры, г.зн. тэгіраваць вобразы сігнатурамі.

Цяпер, калі запатрабуецца, напрыклад, з'merge'іць змены з рэлізу ў master, мы можам рабіць сапраўдны merge commit: у яго будзе іншы ідэнтыфікатар, але тая ж сігнатура. З такім жа ідэнтыфікатарам мы выкацім выяву і на production.
Недахоп у тым, што зараз не атрымаецца вызначыць, што за комит выпампаваны на production - кантрольныя сумы працуюць толькі ў адзін бок Гэтая праблема вырашаецца дадатковым пластом з метададзенымі - падрабязней распавяду далей.
Тэгіраванне ў werf
У werf мы пайшлі яшчэ далей і рыхтуемся зрабіць размеркаваную зборку з кэшам, які не захоўваецца на адной машыне… Такім чынам, у нас збіраюцца Docker-выявы двух тыпаў, мы завём іх этап и малюнак.
У Git-рэпазітары werf захоўваюцца спецыфічныя інструкцыі для зборкі, якія апісваюць розныя этапы зборкі.beforeInstall, ўсталёўваць, beforeSetup, ўстаноўка). Першы stage-лад мы збіраем з сігнатурай, вызначанай як кантрольная сума першых крокаў. (Падрабязнасці пазней).

Няхай пасля гэтага з'яўляецца новы коміт, у якім змянілі толькі код прыкладання. Што адбудзецца? Для змен кода будзе створаны патч, падрыхтаваны новы stage-выява. Яго сігнатура будзе вызначана як кантрольная сума старога stage-выявы і новага патча. З гэтай выявы будзе сфарміраваны і новы фінальны image-вобраз. Аналагічныя паводзіны будуць адбывацца пры зменах на іншых этапах.
Такім чынам, stage-выявы - кэш, які можна захоўваць размеркавана, а ўжо ствараныя з яго image-выявы загружаюцца ў Docker Registry.

Ачыстка registry
Гаворка пойдзе не пра выдаленне пластоў, якія засталіся віслымі пасля выдаленых тэгаў, - гэта стандартная магчымасць самога Docker Registry. Гаворка пра сітуацыю, калі назапашваецца мноства Docker-тэгаў і мы разумеем, што некаторая іх частка нам больш не патрабуецца, а месца яны займаюць (і/ці мы за яго плацім).
Якія ёсць стратэгіі ачысткі?
- Можна проста нічога не чысціць. Часам сапраўды прасцей крыху заплаціць за лішнюю прастору, чым разблытваць велізарны клубок з тэгаў. Але гэта працуе толькі да вызначанага моманту.
- Поўны скід. Калі выдаліць усе выявы і перасабраць толькі актуальныя ў CI-сістэме, тое можа паўстаць праблема. Калі на production перазапусціцца кантэйнер, для яго загрузіцца новы вобраз такі, што яшчэ нікім не тэставаўся. Гэта забівае ідэю immutable infrastructure.
- Сіне-зялёны. Адзін registry пачаў перапаўняцца - загружаем выявы ў іншы. Тая ж самая праблема, што ў папярэднім спосабе: у які момант можна чысціць той registry, што пачаў перапаўняцца?
- Па часе. Выдаляць усе вобразы старэйшыя за 1 месяц? Але абавязкова знойдзецца сервіс, які не абнаўляўся цэлы месяц…
- ўручную вызначаць, што можна выдаляць.
Па-сапраўднаму жыццяздольных варыянту два: не чысціць ці ж камбінацыя з blue-green + уручную. У апошнім выпадку прамова аб наступным: калі вы разумееце, што сітавіна пачысціць registry, ствараеце новы і дадаеце ўсё новыя выявы ў яго на працягу, напрыклад, месяца. А праз месяц гледзіце, якія pod'ы ў Kubernetes па-ранейшаму выкарыстаюць стары registry, і пераносьце іх таксама ў новы registry.
Да чаго мы прыйшлі ў werf? Мы збіраем:
- Git head: усе тэгі, усе галінкі, - мяркуючы, што ўсё, што пратэгавана ў Git, нам трэба і ў выявах (а калі не, то трэба выдаліць у самім Git'е);
- усе pod'ы, якія выкачаныя зараз у Kubernetes;
- старыя ReplicaSet'ы (тое, што нядаўна было выпампавана), а таксама плануем сканаваць Helm-рэлізы і адбіраць апошнія выявы тамака.
… і які робіцца з гэтага набору whitelist — спіс выяў, якія мы не будзем выдаляць. Усё астатняе чысцім, пасля чаго знаходзім сіроцкія stage-вобразы і выдаляем іх таксама.
Стадыя дэплоя (deploy)
Надзейная дэкларатыўнасць
Першы момант, на які хацелася б звярнуць увагу ў дэплоі, - выкат абноўленай канфігурацыі рэсурсаў, аб'яўленай дэкларатыўна. Арыгінальны YAML-дакумент з апісаннем Kubernetes-рэсурсаў заўсёды моцна адрозніваецца ад выніку, рэальна які працуе ў кластары. Таму што Kubernetes дадае ў канфігурацыю:
- ідэнтыфікатары;
- службовую інфармацыю;
- мноства значэнняў па змаўчанні;
- секцыю з бягучым статутам;
- змены, зробленыя ў рамках працы admission webhook;
- вынік працы розных кантролераў (і планавальніка).
Таму, калі з'яўляецца новая канфігурацыя рэсурсу (новы), мы не можам проста ўзяць і перазапісаць ёю бягучую, «жывую», канфігурацыю (жыць). Для гэтага нам давядзецца параўнаць новы з мінулай ужытай канфігурацыяй (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-сістэме, а тая – свайму карыстачу.

Аднак ёсць вялікая праблема: бо паспяховае прымяненне не азначае паспяховы выкат. Калі Kubernetes зразумеў, што за змены трэба прымяніць, прымяняе яго - мы яшчэ не ведаем, што атрымаецца ў выніку. Напрыклад, абнаўленне і рэстарт pod'аў ва frontend'е можа прайсці паспяхова, а ў backend'е - не, і мы атрымаем розныя версіі запушчаных вобразаў прыкладання.
Каб усё рабіць правільна, у гэтай схеме напрошваецца дадатковае звяно - спецыяльны трэкер, які будзе атрымліваць ад Kubernetes API інфармацыю аб статусе і перадаваць яе для далейшага аналізу рэальнага становішча рэчаў. (гл. яе анонс ), - Якая вырашае гэтую праблему і ўбудаваная ў werf.
Паводзіны гэтага трэкера на ўзроўні werf наладжваецца з дапамогай анатацый, якія ставяцца на Deployments ці StatefulSets. Галоўная анатацыя fail-mode - разумее наступныя значэння:
-
IgnoreAndContinueDeployProcess- ігнаруем праблемы выкату гэтага кампанента і працягваем дэплой; -
FailWholeDeployProcessImmediately- памылка ў гэтым кампаненце спыняе працэс дэплою; -
HopeUntilEndOfDeployProcess- спадзяемся, што гэты кампанент запрацуе да канца дэплою.
Напрыклад, такая камбінацыя з рэсурсаў і значэнняў анатацыі fail-mode:

Калі дэплоім ўпершыню, база дадзеных (MongoDB) яшчэ можа быць не гатовая – Deployment'ы ўпадуць. Але можна дачакацца моманту, каб яна запусцілася, і дэплой усё ж пройдзе.
Ёсць яшчэ дзве анатацыі для kubedog у werf:
-
failures-allowed-per-replica- Колькасць дазволеных падзенняў на кожную рэпліку; -
show-logs-until- Рэгулюе момант, да якога werf паказвае (у stdout) логі з усіх выкочваюцца pod'ов. Па змаўчанні гэтаPodIsReady(каб ігнараваць паведамленні, якія нам ці наўрад патрэбныя, калі на pod пачынае прыходзіць трафік), аднак дапушчальныя таксама значэнніControllerIsReadyиEndOfDeploy.
Што яшчэ мы жадаем ад дэплою?
Апроч ужо апісаных двух пунктаў нам хацелася б:
- бачыць логі - прычым толькі патрэбныя, а не ўсё запар;
- адсочваць прагрэс, таму што калі job "моўчкі" вісіць некалькі хвілін, важна разумець, што там адбываецца;
- мець аўтаматычны адкат на выпадак, калі нешта пайшло не так (а таму крытычна ведаць рэальны статут дэплою). Выкат павінен быць атамарным: ці ён праходзіць да канца, ці ўсё вяртаецца да ранейшага стану.
Вынікі
Нам як кампаніі для рэалізацыі ўсіх апісаных нюансаў на розных этапах дастаўкі (build, publish, deploy) дастаткова CI-сістэмы і ўтыліты .
Замест заключэння:

З дапамогай werf мы нядрэнна прасунуліся ў рашэнні вялікай колькасці праблем DevOps-інжынераў і будзем рады, калі шырэйшае супольнасць хоць бы паспрабуе гэтую ўтыліту ў справе. Дабіцца добрага выніку разам будзе прасцей.
Відэа і слайды
Відэа з выступу (~47 хвілін):

Прэзентацыя даклада:
PS
Іншыя даклады пра Kubernetes у нашым блогу:
- «» (Дзмітрый Сталяроў; 27 красавіка 2019 на «Страчцы»);
- «» (Андрэй Паловаў; 8 красавіка 2019 г. на Saint HighLoad++);
- «» (Дзмітрый Сталяроў; 8 лістапада 2018 на HighLoad++);
- «» (Дзмітрый Сталяроў; 28 мая 2018 на RootConf);
- «» (Дзмітрый Сталяроў; 7 лістапада 2017 на HighLoad++);
- «» (Дзмітрый Сталяроў; 6 чэрвеня 2017 на RootConf).
Крыніца: habr.com
