3-странно сливане в werf: внедряване в Kubernetes с Helm «на стероиди»

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

3-странно сливане в 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.

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

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

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

  • с командата kubectl run може да стартира Deployment или Job:
    kubectl run --generator=deployment/apps.v1 ИМЕ НА РАЗГРЪЩАНЕ --image=ИЗОБРАЖЕНИЕ
  • с командата kubectl scale — променя броя на репликите:
    kubectl scale --replicas=3 deployment/mysql
  • и т.н.

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

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

Ясно е, че този подход се съчетава слабо с съхраняването на кода на приложението и инфраструктурата като код (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 е внедрено оптимистично заключване , и ако след операцията 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 сам ще се справи с текущото състояние на обекта и ще го доведе до необходимия вид.

Оптимистичното заключване в този случай не е необходимо. Тази операция е по-декларативна в сравнение с 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 пачове.

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 г. 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 патч

Потребителят все още може да забрани използването на новите патчи в 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 ще приведe текущото състояние на ресурса от работещия клъстер в състоянието, описано в чарта, използвайки същите 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