ProHoster > блог > адміністраванне > 3-way merge у werf: дэплой у Kubernetes з Helm «на пазіцыі, метадалагічнай»
3-way merge у werf: дэплой у Kubernetes з Helm «на пазіцыі, метадалагічнай»
Здарылася тое, чаго мы (і не толькі мы) доўга чакалі: werf, наша Open Source-ўтыліта для зборкі прыкладанняў і іх дастаўкі ў Kubernetes, зараз падтрымлівае прымяненне змяненняў з дапамогай 3-way-merge-патчаў! У дадатак да гэтага, з'явілася магчымасць adoption'а існуючых K8s-рэсурсаў у Helm-рэлізы без перастварэння гэтых рэсурсаў.
Калі зусім сцісла, то ставім WERF_THREE_WAY_MERGE=enabled - атрымліваем дэплой «як у kubectl apply», сумяшчальны з існуючымі інсталяцыямі на Helm 2 і нават крыху больш.
Але давайце пачнем з тэорыі: што ўвогуле такое 3-way-merge-патчы, як людзі прыйшлі да падыходу з іх генерацыяй і чаму яны важныя ў CI/CD-працэсах з інфраструктурай на базе Kubernetes? А пасля гэтага - паглядзім, што ж уяўляе сабой 3-way-merge у werf, якія рэжымы выкарыстоўваюцца па змаўчанні і як гэтым кіраваць.
Што такое 3-way-merge-патч?
Такім чынам, пачнем з задачы выкату рэсурсаў, апісаных у YAML-маніфестах, у Kubernetes.
Для працы з рэсурсамі Kubernetes API прапануе такія асноўныя аперацыі: create, patch, replace і delete. Мяркуецца, што з іх дапамогай трэба сканструяваць зручны бесперапынны выкат рэсурсаў у кластар. Як?
Імператыўныя каманды kubectl
Першы падыход да кіравання аб'ектамі ў Kubernetes - выкарыстанне імператыўных каманд kubectl для стварэння, змены і выдаленні гэтых аб'ектаў. Прасцей кажучы:
камандай kubectl run можна запусціць Deployment або Job:
kubectl run --generator=deployment/apps.v1 DEPLOYMENT_NAME --image=IMAGE
камандай kubectl scale - памяняць колькасць рэплік:
kubectl scale --replicas=3 deployment/mysql
і г.д.
Такі падыход можа здацца зручным з першага позірку. Аднак ёсць праблемы:
Яго цяжка аўтаматызаваць.
Як адлюстраваць канфігурацыю у Git? Як рабіць review зменаў, якія адбываюцца з кластарам?
Як забяспечыць ўзнаўляльнасць канфігурацыі пры перазапуску?
...
Зразумела, што такі падыход дрэнна спалучаецца з захоўваннем разам з кодам прыкладання і інфраструктуры як кода (IaC; ці нават GitOps як больш сучаснага варыянту, які набірае папулярнасць у Kubernetes-экасістэме). Таму далейшага разьвіцьця гэтыя каманды ў kubectl не атрымалі.
Аперацыі create, get, replace і delete
З першасным стварэннем усё проста: адпраўляем маніфест у аперацыю create у kube api і рэсурс створаны. YAML-прадстаўленне маніфесту можна захоўваць у Git, а для стварэння - выкарыстоўваць каманду kubectl create -f manifest.yaml.
С выдаленнем таксама проста: падстаўляем той жа manifest.yaml з Git у каманду kubectl delete -f manifest.yaml.
Аперацыя replace дазваляе цалкам замяніць канфігурацыю рэсурса на новую, без перастварэння рэсурса. Гэта азначае, што перад тым, як рабіць змену ў рэсурс, лагічна запытаць бягучую версію аперацыяй get, змяніць яе і абнавіць аперацыяй replace. У kube apiserver убудаваны optimistic locking і, калі пасля аперацыі get аб'ект памяняўся, то аперацыя replace не пройдзе.
Каб захоўваць канфігурацыю ў Git і абнаўляць з дапамогай replace, трэба рабіць аперацыю get, смерціць канфіг з Git'а з тым, што мы атрымалі, і выконваць replace. Штатна kubectl дазваляе толькі карыстацца камандай kubectl replace -f manifest.yaml, Дзе manifest.yaml - ужо цалкам падрыхтаваны (у нашым выпадку - смяртэжаны) маніфест, які патрабуецца ўсталяваць. Атрымліваецца, карыстачу неабходна рэалізаваць merge маніфестаў, а гэтая справа нетрывіяльная…
Таксама варта адзначыць, што хаця manifest.yaml і захоўваецца ў Git, мы не можам ведаць загадзя, трэба ствараць аб'ект або абнаўляць яго - гэта павінен рабіць карыстацкі софт.
Разам: ці можам мы пабудаваць бесперапынны выкат толькі з дапамогай create, replace і delete, забяспечыўшы захоўванне канфігурацыі інфраструктуры ў Git'е разам з кодам і зручны CI/CD?
У прынцыпе, можам… Для гэтага спатрэбіцца рэалізаваць аперацыю merge маніфестаў і нейкую абвязку, якая:
правярае наяўнасць аб'екта ў кластары,
выконвае першаснае стварэнне рэсурсу,
абнаўляе ці выдаляе яго.
Пры абнаўленні трэба ўлічыць, што рэсурс мог памяняцца з часу апошняга get і аўтаматычна апрацоўваць выпадак optimistic locking - рабіць паўторныя спробы абнаўлення.
Аднак навошта вынаходзіць ровар, калі kube-apiserver прапануе іншы спосаб абнаўлення рэсурсаў: аперацыю patch, якая здымае з карыстальніка частку апісаных праблем?
пластыр
Вось мы і дабраліся да патчаў.
Патчы - гэта асноўны спосаб прымянення змяненняў да існуючых аб'ектаў у Kubernetes. Аперацыя patch працуе так, што:
карыстачу kube-apiserver патрабуецца паслаць патч у JSON-выглядзе і паказаць аб'ект,
а apiserver сам разбярэцца з бягучым станам аб'екта і прывядзе яго да патрабаванага ўвазе.
Optimistic locking у дадзеным выпадку не патрабуецца. Гэтая аперацыя больш дэкларатыўная ў параўнанні з replace, хаця спачатку можа здацца наадварот.
Такім чынам:
з дапамогай аперацыі create мы ствараем аб'ект па маніфесце з Git'а,
з дапамогай delete - выдаляем, калі аб'ект больш не патрабуецца,
з дапамогай patch - Змяняем аб'ект, прыводзячы яго да выгляду, апісанаму ў Git.
Аднак, каб гэта зрабіць, неабходна стварыць правільны патч!
Як працуюць патчы ў Helm 2: 2-way-merge
Пры першай усталёўцы рэлізу Helm выконвае аперацыю create для рэсурсаў чарта.
Пры абнаўленні рэлізу Helm для кожнага рэсурсу:
лічыць патч паміж версіяй рэсурсу з мінулага чарта і бягучай версіяй чарта,
ужывае гэты патч.
Такі патч мы будзем зваць 2-way-merge patch, таму што ў яго стварэнні ўдзельнічаюць 2 маніфесты:
маніфест рэсурсу з папярэдняга рэлізу,
маніфест рэсурсу з бягучага рэсурса.
Пры выдаленні аперацыя delete у kube apiserver выклікаецца для рэсурсаў, якія былі абвешчаныя ў мінулым рэлізе, але не абвешчаныя ў бягучым.
Падыход з 2 way merge patch мае праблему: ён прыводзіць да расінхрона рэальнага стану рэсурсу ў кластары і маніфесту ў Git.
Ілюстрацыя праблемы на прыкладзе
У Git, у чарце захоўваецца маніфест, у якім поле image у Deployment мае значэнне ubuntu:18.04.
Карыстальнік праз kubectl edit памяняў значэнне гэтага поля на ubuntu:19.04.
Пры паўторным дэплоі чарта Helm не генеруе патч, таму што поле image у папярэдняй версіі рэлізу і ў бягучым чарце аднолькавыя.
Пасля паўторнага дэплою image застаецца ubuntu:19.04, хоць у чарце напісана ubuntu:18.04.
Мы атрымалі рассінхранізацыю і страцілі дэкларатыўнасць.
Што такое сінхранізаваны рэсурс?
Наогул кажучы, поўнае адпаведнасць маніфеста рэсурсу ў які працуе кластары і маніфеста з Git атрымаць немагчыма. Таму што ў рэальным маніфесце могуць быць службовыя анатацыі/лэйблы, дадатковыя кантэйнеры і іншыя дадзеныя, якія дадаюцца і выдаляюцца з рэсурсу дынамічна нейкімі кантролерамі. Гэтыя дадзеныя мы не можам і не жадаем трымаць у Git. Аднак мы жадаем, каб пры выкаце тыя палі, якія мы відавочна паказалі ў Git'е, прымалі адпаведныя значэнні.
Атрымліваецца такое агульнае правіла сінхранізаванага рэсурсу: пры выкаце рэсурсу можна мяняць або выдаляць толькі тыя палі, якія відавочна прапісаны ў маніфесце з Git'а (ці былі прапісаны ў папярэдняй версіі, а зараз выдалены).
3-way-merge patch
Асноўная ідэя 3-way-merge patch: генеруем патч паміж апошняй ужытай версіяй маніфэсту з Git'а і мэтавай версіяй маніфэсту з Git'а з улікам бягучай версіі маніфэсту з працуючага кластара. Выніковы патч павінен адпавядаць правілу сінхранізаванага рэсурсу:
новыя палі, дададзеныя ў мэтавую версію, дадаюцца з дапамогай патча;
раней існавалыя палі ў апошняй ужытай версіі і не існыя ў мэтавай - абнуляюцца з дапамогай патча;
палі ў бягучай версіі аб'екта, адрозныя ад мэтавай версіі маніфеста, - абнаўляюцца з дапамогай патча.
Менавіта па такім прынцыпе генеруе патчы. kubectl apply:
апошняя прымененая версія маніфеста захоўваецца ў анатацыі самога аб'екта,
мэтавая - бярэцца з паказанага YAML-файла,
бягучая - з працуючага кластара.
Цяпер, калі разабраліся з тэорыяй, пара расказаць, што ж мы зрабілі ў werf.
Ужыванне змен у werf
Раней werf, як і Helm 2, выкарыстоўваў 2-way-merge-патчы.
Патч для рамонту
Для таго, каб перайсці на новы від патчаў – 3-way-merge, – першым крокам мы ўвялі так званыя repair-патчы.
Пры дэплоі выкарыстоўваецца стандартны 2-way-merge-патч, але werf дадаткова генеруе такі патч, які б сінхранізаваў рэальны стан рэсурсу з тым, што напісана ў Git (ствараецца такі патч з выкарыстаннем таго ж правілы сінхранізаванага рэсурсу, апісанага вышэй).
У выпадку ўзнікнення рассінхрона, у канцы дэплою карыстальнік атрымлівае WARNING з адпаведным паведамленнем і патчам, які трэба прымяніць, каб прывесці рэсурс да сінхранізаванаму ўвазе. Таксама гэты патч запісваецца ў адмысловую анатацыю werf.io/repair-patch. Мяркуецца, што карыстач рукамі сам прыменіць гэты патч: werf яго прымяняць не будзе прынцыпова.
Генерацыя repair-патчаў - гэта часовая мера, якая дазваляе выпрабаваць на справе стварэнне патчаў па прынцыпе 3-way-merge, але аўтаматычна гэтыя патчы не ўжываць. На дадзены момант такі рэжым працы ўключаны па змаўчанні.
3-way-merge patch толькі для новых рэлізаў
Пачынаючы з 1 снежня 2019 г. beta- і alpha-версіі werf пачынаюць па змаўчанні выкарыстоўваць паўнавартасныя 3-way-merge-патчы для ўжывання змен толькі для новых Helm-рэлізаў, якія выкочваюцца праз werf. Ужо існуючыя рэлізы працягнуць выкарыстоўваць падыход з 2-way-merge + repair-патчамі.
Дадзены рэжым працы можна ўключыць відавочна настройкай WERF_THREE_WAY_MERGE_MODE=onlyNewReleases ужо зараз.
Заўвага: фіча з'яўлялася ў werf на працягу некалькіх рэлізаў: у альфа-канале яна стала гатовай з версіі v1.0.5-alpha.19, а ў бэта-канале - з v1.0.4-beta.20.
3-way-merge patch для ўсіх рэлізаў
Пачынальна з 15 снежня 2019 г. beta- і alpha-версіі werf пачынаюць па змаўчанні выкарыстоўваць паўнавартасныя 3-way-merge-патчы для ўжывання змен для ўсіх рэлізаў.
Дадзены рэжым працы можна ўключыць відавочна настройкай WERF_THREE_WAY_MERGE_MODE=enabled ужо зараз.
Як быць з аўтамаштабаванне рэсурсаў?
У Kubernetes існуе 2 тыпу аўтамаштабавання: HPA (гарызантальны) і VPA (вертыкальны).
Гарызантальны аўтаматычна выбірае колькасць рэплік, вертыкальны - колькасць рэсурсаў. І колькасць рэплік, і патрабаванні да рэсурсаў указваюцца ў маніфесце рэсурсу (гл. spec.replicas або spec.containers[].resources.limits.cpu, spec.containers[].resources.limits.memory и іншыя).
Праблема: калі карыстач сканфігуруе рэсурс у чарце так, што ў ім будуць паказаныя вызначаныя значэнні па рэсурсах ці рэплікам і для дадзенага рэсурсу будуць уключаныя аўтаскейлеры, то пры кожным дэплоі werf будзе скідаць гэтыя значэнні ў тое, што запісана ў маніфесце чарта.
Рашэнняў у праблемы два. Для пачатку лепш за ўсё адмовіцца ад відавочнага ўказання аўтамаштабаваных значэнняў у маніфесце чарта. Калі ж гэты варыянт па нейкіх прычынах не падыходзіць (напрыклад, таму што ў чарце зручна задаць пачатковыя абмежаванні рэсурсаў і колькасць рэплік), то werf прапануе наступныя анатацыі:
werf.io/set-replicas-only-on-creation=true
werf.io/set-resources-only-on-creation=true
Пры наяўнасці такой анатацыі werf не будзе скідаць адпаведныя значэнні пры кожным дэплоі, а толькі ўсталюе іх пры першапачатковым стварэнні рэсурса.
Больш падрабязна - гл. у дакументацыі праекта па HPA и VPA.
Забараніць выкарыстанне 3-way-merge patch
Карыстальнік пакуль можа забараніць выкарыстанне новых патчаў у werf з дапамогай зменнай асяроддзя WERF_THREE_WAY_MERGE_MODE=disabled. Аднак пачынаючы з 1 сакавіка 2020 года гэтая забарона перастане працаваць і магчыма будзе толькі выкарыстанне 3-way-merge-патчаў.
Adoption рэсурсаў у werf
Засваенне метаду ўжывання змен 3-way-merge-патчамі дазволіла нам адразу рэалізаваць такую фічу, як adoption існых у кластары рэсурсаў у Helm-рэліз.
Helm 2 мае праблему: нельга дадаць у маніфесты чарта рэсурс, які ўжо існуе ў кластары, без перастварэння з нуля гэтага рэсурсу (гл. #6031, #3275). Мы навучылі werf прымаць існуючыя рэсурсы ў выпуску. Для гэтага трэба ўсталяваць на бягучую версію рэсурсу з працуючага кластара анатацыю (напрыклад, з дапамогай kubectl edit):
"werf.io/allow-adoption-by-release": RELEASE_NAME
Цяпер рэсурс трэба апісаць у чарце і пры наступным дэплоі werf'ом рэлізу з адпаведным імем існуючы рэсурс будзе прыняты ў гэты рэліз і застанецца пад яго кіраваннем. Больш за тое, у працэсе прыняцця рэсурсу ў рэліз werf прывядзе бягучы стан рэсурсу з працуючага кластара да стану, апісанаму ў чарце, выкарыстоўваючы тыя ж 3-way-merge-патчы і правіла сінхранізаванага рэсурсу.
Заўвага: ўстаноўка WERF_THREE_WAY_MERGE_MODE не ўплывае на adoption рэсурсаў - у выпадку adoption заўсёды выкарыстоўваецца 3-way-merge-патч.
Спадзяюся, пасля гэтага артыкула стала больш зразумела, што такое 3-way-merge-патчы і чаму да іх прыйшлі. З практычнага пункта гледжання развіцця праекту werf іх рэалізацыя стала яшчэ адным крокам на шляхі паляпшэння Helm-падобнага дэплою. Зараз можна забыцца пра праблемы з сінхранізацыяй канфігурацыі, якія часта ўзнікалі пры выкарыстанні Helm 2. Разам з тым, была дададзеная новая карысная фіча adoption'а ўжо выпампаваных Kubernetes-рэсурсаў у Helm-рэліз.
У Helm-падобным дэплоі па-ранейшаму застаюцца некаторыя праблемы і цяжкасці, такія як выкарыстанне Go-шаблонаў, і мы працягнем іх вырашаць.
Інфармацыю аб метадах абнаўлення рэсурсаў і adoption'е можна таксама знайсці на гэтай старонцы дакументацыі.
Helm 3
Асобнай заўвагі вартая якая выйшла літаральна на днях новая мажорная версія Helm – v3, – якая таксама выкарыстоўвае 3-way-merge-патчы і пазбаўляецца ад Tiller. Новая версія Helm патрабуе міграцыі ужо існуючых установак, каб сканвертаваць іх у новы фармат захоўвання рэлізаў.
Werf са свайго боку на дадзены момант ужо пазбавіўся ад выкарыстання Tiller, пераключыўся на 3-way-merge і дадаў шмат іншага, пры гэтым застаўшыся сумяшчальным з ужо існуючымі інсталяцыямі на Helm 2 (ніякіх скрыптоў міграцыі выконваць не трэба). Таму, пакуль werf не пераключаны на Helm 3, карыстачы werf не губляюць асноўных пераваг Helm 3 перад Helm 2 (у werf яны таксама ёсць).
Тым не менш, пераключэнне werf на кодавую базу Helm 3 непазбежна і адбудзецца ў найбліжэйшай будучыні. Меркавана гэта будзе werf 1.1 ці werf 1.2 (на дадзены момант, галоўная версія werf – 1.0; падрабязней пра прыладу версіявання werf гл. тут). За гэты час Helm 3 паспее стабілізавацца.