3-way merge в werf: деплой в Kubernetes с Helm «на стероиди»

Случилось то, чего ние (не само ние) дълго чакахме: werf, нашата Open Source утилита за изграждане на приложения и тяхното доставяне в Kubernetes, сега поддържа прилагане на промени с помощта на 3-way-merge патчи! В допълнение, появи се възможност за адаптиране на съществуващи K8s ресурси в Helm релизи без повторно създаване на тези ресурси.

3-way merge в werf: деплой в Kubernetes с 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.

За работа с ресурсите API-то на Kubernetes предлага следните основни операции: create, patch, replace и delete. Предполага се, че с тях трябва да се конструира удобен непрекъснат внедрител на ресурси в кластера. Как?

Императивни команди kubectl

Първият подход за управление на обектите в Kubernetes е използването на императивни команди kubectl за създаване, промяна и изтриване на тези обекти. По-просто казано:

  • командата kubectl run може да стартира Deployment или Job:
    kubectl run --generator=deployment/apps.v1 ИМЕ_НА_DEPLOYMENT --image=IMAGE
  • командата kubectl scale — променя броя на репликите:
    kubectl scale --replicas=3 deployment/mysql
  • и т.н.

Такъв подход може да изглежда удобен на пръв поглед. Въпреки това, има проблеми:

  1. Трудно е да автоматизира.
  2. Как да отразиш конфигурацията в Git? Как да правим преглед на промените, които се случват в кластера?
  3. Как да осигурим възпроизводимост конфигурации при повторно стартиране?

Очевидно е, че този подход не се съчетава добре с съхранението заедно с кода на приложението и инфраструктурата като код (IaC; или дори GitOps като по-съвременната версия, която набира популярност в Kubernetes екосистемата). Затова по-нататъшното развитие на тези команди в kubectl не се случи.

Операции create, get, replace и delete

С първоначалното създаване всичко е просто: изпращаш манифеста в операция създай до kube api и ресурсът е създаден. YAML представянето на манифеста може да се съхранява в Git, а за създаване — да се използва командата kubectl create -f manifest.yaml.

С изтриването също е просто: слагаш същия manifest.yaml от Git в командата kubectl delete -f manifest.yaml.

Операция replace позволява напълно да се замени конфигурацията на ресурса с нова, без да се създава отново ресурсът. Това означава, че преди да направи промяна в ресурса, рационално е да поиска текущата версия с операцията get, да я промени и да я актуализира с операция replace. В kube apiserver е вградена оптимистична блокировка и, ако след операцията get обектът е променен, то операцията replace няма да премине.

За да се съхранява конфигурацията в Git и да се актуализира с replace, трябва да се извърши операция get, да се слее конфигурацията от Git с това, което сме получили, и да се извърши replace. Стандартно kubectl позволява да се използва командата kubectl replace -f manifest.yaml, където manifest.yaml — вече напълно подготвеният (в нашия случай — слетия) манифест, който трябва да бъде инсталиран. Получава се, че потребителят трябва да реализира сливане на манифестите, а това не е тривиално…

Също така е важно да се отбележи, че макар manifest.yaml да се съхранява в Git, не можем предварително да знаем, трябва ли да създаваме обект или да го актуализираме — това трябва да го прави потребителският софтуер.

Общо: можем ли да изградим непрекъснато разгръщане само с помощта на create, replace и delete, осигурявайки съхранение на конфигурацията на инфраструктурата в Git заедно с кода и удобен CI/CD?

В принцип можем… За това е необходимо да реализираме операция по сливане на манифестите и някаква обвивка, която:

  • проверява наличието на обект в клъстера,
  • извършва първоначално създаване на ресурса,
  • актуализира или премахва него.

При актуализиране трябва да се вземе предвид, че ресурсът е могъл да се промени от последния get и автоматично да обработва случая с оптимистична блокировка — да прави повторни опити за актуализиране.

Но защо да изобретяваме колелото, когато kube-apiserver предлага друг начин за актуализиране на ресурсите: операцията patch, която снимва част от описаните проблеми от потребителя?

Patch

Тук вече стигнахме до патчовете.

Патчовете — това е основният начин за прилагане на промени към съществуващи обекти в Kubernetes. Операцията patch работи така, че:

  • на потребителя на kube-apiserver е необходимо да изпрати патч в JSON формат и да укаже обекта,
  • а apiserver сам ще разбере текущото състояние на обекта и ще го приведe в желаното състояние.

Оптимистична блокировка в този случай не е необходима. Тази операция е по-декларативна в сравнение с replace, въпреки че първоначално може да изглежда обратното.

Следователно:

  • с помощта на операцията създай създаваме обект по манифеста от Git,
  • чрез изтрий — премахваме, ако обектът вече не е необходим,
  • чрез patch — променяме обекта, като го приведем в вида, описан в Git.

Въпреки това, за да направите това, е необходимо да създадете правилен патч!

Как работят патчовете в Helm 2: 2-странно обединение

При първоначалната инсталация на релиз Helm се извършва операция създай за ресурсите на чарта.

При обновяване на релиза Helm за всеки ресурс:

  • сравнява патча между версията на ресурса от предишния чарт и текущата версия на чарта,
  • прилага този патч.

Такъв патч ще наричаме 2-странно обединен патч, тъй като в създаването му участват 2 манифеста:

  • манифест на ресурса от предишния релиз,
  • манифест на ресурса от текущия ресурс.

При изтриване операцията изтрий в kube apiserver се извиква за ресурси, които бяха обявени в предишния релиз, но не са обявени в текущия.

Подходът с 2-странно обединение има проблем: той води до дисинхронизация на реалното състояние на ресурса в клъстера и манифеста в Git.

Илюстрация на проблема на примера

  • В Git, в чарта се съхранява манифест, в който полето изображение на Deployment има стойност ubuntu:18.04.
  • Потребителят през kubectl edit променя стойността на това поле на ubuntu:19.04.
  • При повторно деплойване на чарта Helm не генерира патч, тъй като полето изображение в предишната версия на релиза и в текущия чарт са идентични.
  • След повторното деплойване изображение остава ubuntu:19.04, въпреки че в чарта е написано ubuntu:18.04.

Получихме дисинхронизация и загубихме декларативността.

Какво е синхронизиран ресурс?

Общо взето, пълно съвпадение на манифеста на ресурса в работещия клъстер и манифеста от Git не може да бъде постигнато. Защото в реалния манифест могат да има служебни анотации/етикети, допълнителни контейнери и други данни, добавяни и изтривани от ресурса динамично от определени контролери. Тези данни не можем и не искаме да съхраняваме в Git. Въпреки това желаем при внедряване полетата, които явно сме указали в Git, да приемат съответните стойности.

Резултатът е такова общо правило за синхронизирания ресурс: при внедряване на ресурса могат да се променят или изтриват само онези полета, които явно са записани в манифеста от Git (или са били записани в предишната версия, а сега са изтрити).

3-странно обединен патч

Основна идея 3-странно обединен патч: генерира патч между последната приложена версия на манифеста от Git и целевата версия на манифеста от Git, като се взема предвид текущата версия на манифеста от работещия клъстер. Финалният патч трябва да отговаря на правилото за синхронизиран ресурс:

  • новите полета, добавени в целевата версия, се добавят с помощта на патч;
  • Полята, които вече съществуват в последно приложената версия и не съществуват в целевата, се нулират с помощта на патча;
  • Полята в текущата версия на обекта, които се различават от целевата версия на манифеста, се актуализират с помощта на патча.

Точно по този принцип генерира патчи kubectl apply:

  • последно приложената версия на манифеста се запазва в анотацията на самия обект,
  • целевата се взима от посочения YAML файл,
  • текущата - от работещия клъстер.

Сега, след като се запознахме с теорията, е време да разкажем какво направихме в werf.

Прилагане на промените в werf

По-рано werf, подобно на Helm 2, използваше 2-way-merge патчи.

Repair patch

За да преминем към новия вид патчи — 3-way-merge, първата стъпка беше да въведем така наречените repair патчи.

При деплой се използва стандартен 2-way-merge патч, но werf допълнително генерира такъв патч, който синхронизира реалното състояние на ресурса с това, което е написано в Git (създава се такъв патч с помощта на същото правило за синхронизирания ресурс, описано по-горе).

В случай на разсинхрон, в края на деплоя потребителят получава WARNING със съответно съобщение и патч, който трябва да се приложи, за да се доведе ресурсът до синхронизирано състояние. Този патч също така се записва в специална анотация werf.io/repair-patch. Предполага се, че потребителят ръчно сам ще приложи този патч: werf няма да го приложи принципно.

Генерирането на repair патчи е временно решение, което позволява да се тестува създаването на патчи по принципа на 3-way-merge, но тези патчи не се прилагат автоматично. В момента този режим на работа е включен по подразбиране.

3-way-merge патч само за нови релизи

Започвайки от 1 декември 2019 г., бета и алфа версии на 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 патч за всички релизи

От 15 декември 2019 г. бета и алфа версиите на 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 патч

Потребителят все още може да забрани използването на новите патчи в werf с помощта на променлива на средата WERF_THREE_WAY_MERGE_MODE=disabled. Въпреки това, от 1 март 2020 г. тази забрана ще спре да функционира и ще бъде възможно само използването на 3-way-merge патчи.

Приемане на ресурси в werf

Усвояването на метода за прилагане на промени с 3-way-merge патчи ни позволи веднага да реализираме такава функция, като приемане на съществуващите ресурси в кластера в 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 не оказва влияние върху приемането на ресурси — в случай на приемане винаги се използва 3-way-merge пач.

Подробности — в документацията.

Изводи и бъдещи планове

Надявам се, след тази статия стана по-ясно какво представляват 3-way-merge пачовете и защо са били въведени. От практическа гледна точка за развитието на проекта werf, тяхната реализация е още една стъпка напред в подобряването на Helm-подобно разгръщане. Сега може да забравим за проблемите с синхронизацията на конфигурацията, които често възникваха при използване на Helm 2. В същото време, беше добавена нова полезна функция за приемането на вече изтеглени Kubernetes ресурси в Helm релиза.

В Helm-подобното разгръщане все още остават някои проблеми и трудности, като използването на Go шаблони, и ще продължим да ги решаваме.

Информация за методите за актуализиране на ресурси и приемането може също да се намери на тази страница с документация.

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 ще успее да се стабилизира.

P.S.

Прочетете също в нашия блог:

Източник: habr.com

Купете надежден хостинг за сайтове с защита от DDoS, VPS VDS сървъри 🔥 Купете надежден хостинг за сайтове с защита от DDoS, VPS VDS сървъри | ProHoster