Всичко започна с това, че тимлидерът на един от нашите екипи разработчици поиска в тестов режим да пуснем тяхното ново приложение, което преди това беше подложено на контейнеризация. Пуснах го. Около 20 минути по-късно получих молба да обновя приложението, защото бяха добавили наистина важна функция. Обнових го. Още след няколко часа… ами, и сами се досещате какво се случи по-нататък…
Аз, да си призная, съм доста мързелив (признавал ли съм това преди? не?), и, като се има предвид, че тимлидерите имат достъп до Jenkins, където е целият ни CI/CD, си помислих: нека сами разгръщат, колкото си поискат! Спомних си един виц: дай на човек риба и той ще бъде сити един ден; наречи го Сити и той ще бъде сити цял живот. И продължих да правя работа, която да умее да разгръща в куер контейнер с приложението всяка успешно сглобена версия и да предава в него всякакви стойности ENV (дядо ми, — филолог, бивш преподавател по английски, — сега щеше да завърти пръст на слепоочието си и да ме погледне много изразително, четейки това изречение).
И така, в бележката ще разкажа как научих:
- Динамично да обновявам задачи в Jenkins от самата задача или от други задачи;
- Да се свързвам с облачната конзола (Cloud shell) от нода с инсталиран агент на Jenkins;
- Да разгръщам работна натовареност (workload) в Google Kubernetes Engine.
Всъщност, наистина, малко преувеличавам. Предполага се, че поне част от инфраструктурата ви е в облака на Google, а следователно, вие сте негов потребител и, разбира се, имате GCP акаунт. Но бележката не е за това.
Това е поредната ми шпаргалка. Такива бележки искам да пиша само в един случай: когато имам задача, първоначално не знам как да я реша, решението не се гуглира в готов вид, затова го гугля на части и в крайна сметка решавам задачата. И за да не се налага в бъдеще, когато забравя как съм го направил, отново да гугля всичко на части и да компилирам в едно, пиша си такива шпаргалки.
Отказ: 1. Бележката беше написана "за себе си", не претендира за best practice . С удоволствие ще прочета предложения "а по-добре беше да се направи така" в коментарите.
2. Ако приложната част на бележката считаме за сол, то, както и всички моите предишни бележки, тази е слабо солен разтвор.
Динамично актуализиране на настройките на задачите в Jenkins
Предвиждам вашия въпрос: какво общо има динамичното актуализиране на джобовете? Въведох ръчно стойността на стринговия параметър и напред!
Отговарям: наистина съм мързелив, не обичам, когато се оплакват: Миша, деплойът се проваля, всичко е изгубено! Започваш да гледаш, а там има печатна грешка в стойността на някой параметър за стартиране на задачата. Затова предпочитам да правя всичко максимално защитено. Ако има възможност да ограничиш потребителя от директно въвеждане на данни, давайки вместо това списък със стойности за избор, то аз организирам избора.
Планът е следният: създаваме задача в Jenkins, в която преди стартиране да можеш да избереш версия от списък, да посочиш стойности за параметрите, предавани в контейнера чрез ENV, след което то изгражда контейнера и го качва в Container Registry. Оттам контейнерът се стартира в Kubernetes като workload с параметри, зададени в джобовете.
Процесът на създаване и настройка на задачата в Jenkins няма да разглеждаме, това е оффтопик. Ще предположим, че задачата е готова. За реализиране на актуализирания списък с версии ни трябват две неща: вече съществуващ списък-източник с валидни номера на версии и променлива от тип Choice parameter в задачата. В нашия пример нека променливата носи името BUILD_VERSION, нека не задълбаваме в нея. Но на списъка-източник нека обърнем внимание.
Вариантите не са много. Сетих се за два на момента:
- Да използвам Remote access API, което Jenkins предлага на своите потребители;
- Да запитам съдържанието на отдалечена папка в репозитория (в нашия случай това е JFrog Artifactory, но това не е същински проблем).
Jenkins Remote access API
Според установената прекрасна традиция ще предпочета да избегна пространни обяснения.
Ще си позволя само свободен превод на част от първия абзац :
Jenkins предоставя API за отдалечен машинно-разбираем достъп до своя функционал. Отдалеченият достъп се предлага в стил, подобен на REST. Това означава, че няма единна точка за достъп до всички възможности, а вместо това се използва URL формат "…/api/", където "…" обозначава обектът, към който се прилагат възможностите на API.
С други думи, ако задачата за деплой, за която говорим в момента, е достъпна на адрес http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build, API свистулките за тази задача са достъпни на адреса http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/
Следващото, което имаме, е изборът в какъв вид да получим изхода. Нека се съсредоточим върху XML, тъй като API позволява филтриране само в този случай.
Нека просто да опитаме да получим списък на всички стартирания на задачата. Интересува ни само името на билдa (displayName) и неговия резултат (резултат):
http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]Успя ли?
Сега ще филтрираме само тези стартирания, които в крайна сметка имат резултат SUCCESS. Използваме аргумента &exclude и като параметър ще му предадем пътя до стойност, различна от SUCCESS. Да-да. Двойното отрицание е потвърждение. Изключваме всичко, което не ни интересува:
http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]&exclude=freeStyleProject/allBuild[result!='SUCCESS'] Скриншот на списъка на успешните

И просто за забава ще се уверим, че филтърът не ни е подведен (филтрите никога не лъжат!) и ще изведем списък на „неуспешните“:
http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]&exclude=freeStyleProject/allBuild[result='SUCCESS'] Скриншот на списъка на неуспешните

Списък на версиите от папка на отдалечен сървър
Има и втори начин да получим списък на версиите. Този ми харесва дори повече от извикването на API на Jenkins. Защото, ако приложението е успешно компилирано, значи е било опаковано и поставено в хранилището в съответната папка. Типа, хранилището по подразбиране е склад за работни версии на приложения. Нека да запитаме какви версии са налични. Отдалечената папка ще бъде curl’вана, grep’вана и awk’вана. Ако някой се интересува от
Команда в един ред
Обърнете внимание на две неща: предавам удостоверения за свързване в заглавката и не ми трябват всички версии от папката, а само тези, които са създадени през последния месец. Променете командата в съответствие с вашите реалности и нужди:
curl -H "X-JFrog-Art-Api:VeryLongAPIKey" -s http://arts.myre.po/artifactory/awesomeapp/ | sed 's/a href=// | grep "$(date +%b)-$(date +%Y)|$(date +%b --date='-1 month')-$(date +%Y)" | awk '{print $1}' | grep -oP '>K[^/]+ )Настройка на задания и конфигурационен файл на задачата в Jenkins
С източника на списъка с версии се справихме. Нека сега интегрираме получения списък в задачата. За мен очевидно решение беше да добавя стъпка в задачата за изграждане на приложението. Стъпка, която да се изпълнява при резултат „успех“.
Отваряме настройките на задачата за изграждане и скролваме до самото дно. Натискаме бутонията: Add build step -> Conditional step (single). В настройките на стъпката избираме условието Current build status, задаваме стойността SUCCESS, изпълнявано действие при успех Run shell command.
И сега най-интересното. Конфигурациите на задачите Jenkins се съхраняват в файлове. В XML формат. По пътя http://път-до-заданието/config.xml Съответно, можете да изтеглите файла с конфигурацията, да го редактирате по необходимия начин и да го поставите обратно на мястото, от което сте го взели.
Помните ли, по-горе се споразумяхме, че за списъка с версии ще създадем параметър BUILD_VERSION?
Нека изтеглим файла с конфигурацията и да погледнем вътре. Просто за да се уверим, че параметърът е на мястото си и в действителност е от желания вид.
Скриншот под спойлера.
Вашият предоставен фрагмент config.xml трябва да изглежда така. С изключение на това, че съдържанието на елемента choices все още липсва

Убедени ли сте? Добре, пишем скрипт, който ще се изпълнява при успешна компилация.
Скриптът ще получава списък с версии, ще изтегля файла с конфигурацията, ще записва в него на нужното ни място списъка с версии, а след това ще го поставя обратно. Да. Всичко е вярно. Ще записваме списъка с версии в XML файла на мястото, където вече има списък с версии (ще бъде в бъдеще, след първото стартиране на скрипта). Знам, че в света все още съществуват любители на регулярни изрази. Аз не съм един от тях. Моля, инсталирайте на машината, на която ще се редактира конфигът. Мисля, че това не е толкова голяма цена, за да избегнем редактиране на XML с помощта на sed.
Под спойлера предоставям кода, който изпълнява описаната по-горе последователност изцяло.
Записваме в конфигурацията списъка с версии от папка на отдалечен сървър
#!/bin/bash
############## Скачиваем конфиг
curl -X GET -u username:apiKey http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_k8s/config.xml -o appConfig.xml
############## Удаляем и заново создаем xml-элемент для списка версий
xmlstarlet ed --inplace -d '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]/a[@class="string-array"]' appConfig.xml
xmlstarlet ed --inplace --subnode '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]' --type elem -n a appConfig.xml
xmlstarlet ed --inplace --insert '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]/a' --type attr -n class -v string-array appConfig.xml
############## Читаем в массив список версий из репозитория
readarray -t vers < <( curl -H "X-JFrog-Art-Api:Api:VeryLongAPIKey" -s http://arts.myre.po/artifactory/awesomeapp/ | sed 's/a href=//' | grep "$(date +%b)-$(date +%Y)|$(date +%b --date='-1 month')-$(date +%Y)" | awk '{print $1}' | grep -oP '>K[^/]+' )
############## Пишем массив элемент за элементом в конфиг
printf '%sn' "${vers[@]}" | sort -r |
while IFS= read -r line
do
xmlstarlet ed --inplace --subnode '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]/a[@class="string-array"]' --type elem -n string -v "$line" appConfig.xml
done
############## Кладем конфиг взад
curl -X POST -u username:apiKey http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_k8s/config.xml --data-binary @appConfig.xml
############## Приводим рабочее место в порядок
rm -f appConfig.xmlАко предпочитате варианта с получаване на версиите от Jenkins и сте толкова мързеливи, колкото мен, то под спойлера е същият код, но списък от Jenkins:
Записваме в конфигурацията списъка с версии от Jenkins
Обаче имайте предвид, че името на компилацията ми се състои от последователен номер и номер на версия, разделени с двоеточие. Съответно, awk отрязва ненужната част. Променете тази стринга под вашите нужди.
#!/bin/bash
############## Скачиваем конфиг
curl -X GET -u username:apiKey http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_k8s/config.xml -o appConfig.xml
############## Удаляем и заново создаем xml-элемент для списка версий
xmlstarlet ed --inplace -d '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]/a[@class="string-array"]' appConfig.xml
xmlstarlet ed --inplace --subnode '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]' --type elem -n a appConfig.xml
xmlstarlet ed --inplace --insert '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]/a' --type attr -n class -v string-array appConfig.xml
############## Пишем в файл список версий из Jenkins
curl -g -X GET -u username:apiKey 'http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]&exclude=freeStyleProject/allBuild[result!=%22SUCCESS%22]&pretty=true' -o builds.xml
############## Читаем в массив список версий из XML
readarray vers < <(xmlstarlet sel -t -v "freeStyleProject/allBuild/displayName" builds.xml | awk -F":" '{print $2}')
############## Пишем массив элемент за элементом в конфиг
printf '%sn' "${vers[@]}" | sort -r |
while IFS= read -r line
do
xmlstarlet ed --inplace --subnode '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]/a[@class="string-array"]' --type elem -n string -v "$line" appConfig.xml
done
############## Кладем конфиг взад
curl -X POST -u username:apiKey http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_k8s/config.xml --data-binary @appConfig.xml
############## Приводим рабочее место в порядок
rm -f appConfig.xmlПо принцип, ако сте тествали кода, написан на базата на горепосочените примери, то в задачата за деплой вече трябва да имате падащо меню със версии. Ето как изглежда на скриншота под спойлера.
Коректно попълнен списък с версии

Ако всичко е проработило, копирайте скрипта в Run shell command и запазете промените.
Свързване с Cloud shell
Нашите контейнери се съхраняват в контейнери. За доставка на приложенията и управление на конфигурации използваме Ansible. Следователно, когато става въпрос за изграждане на контейнери, на ума ми идват три варианта: да инсталирам Docker в Docker, да инсталирам Docker на машина с Ansible или да изграждам контейнери в облачната конзола. За плъгини за Jenkins решихме да мълчим в този запис. Помните ли?
Реших: ако контейнерите могат да се изграждат 'из кутията' в облачната конзола, защо да създавам усложнения? Keep it clean, нали? Искам да изграждам контейнери с Jenkins в облачната конзола и после да ги разтоварвам в Kubernetes. Особено като се има предвид, че в инфраструктурата на Google каналите са много мощни, което благоприятно ще се отрази на скоростта на деплоя.
За свързване с облачната конзола са необходими две неща: gcloud и права за достъп до Google Cloud API за конкретната ВМ, от която ще осъществявате свързването.
За тези, които планират да се свързват изцяло не от облака на Google
Google допуска възможността за деактивиране на интерактивната авторизация в своите услуги. Това ще позволи свързване с конзолата дори от кафеавтомат, стига тя да е под *nix и да има собствена конзола.
Ако имате нужда да разгледам този въпрос по-подробно в рамките на този запис — пишете в коментарите. Ако събера достатъчно гласове — ще напиша обновление по тази тема.
Най-простият начин да предоставите права — чрез уеб интерфейса.
- Спрете ВМ, от която впоследствие ще се свързвате с облачната конзола.
- Отворете детайлите на ВМ и натиснете Да промените.
- Най-долу на страницата изберете обхвата на достъп на ВМ Пълен достъп до всички Cloud API.
Скриншот

- Запишете промените и стартирайте ВМ отново.
След като ВМ се зареди, свържете се с нея по SSH и се уверете, че свързването става без грешка. Използвайте командата:
gcloud alpha cloud-shell ssh Успешното свързване изглежда горе-долу така

Деплой в GKE
Тъй като се стремим да преминем напълно на IaC (Infrastructure as Code), нашите Docker файлове се съхраняват в Git. От една страна. А деплой в Kubernetes се описва с YAML файл, който се използва само от тази задача, който също по същество е код. От друга страна. В общи линии, казвам, че планът е следният:
- Вземаме стойностите на променливите BUILD_VERSION и по желание, стойности на променливите, които ще бъдат предадени чрез ENV.
- Изтегляме Dockerfile от Git.
- Генерираме YAML за деплой.
- Качваме и двата файла чрез SCP в облачната конзола.
- Създаваме контейнер там и го пускаме в Container registry
- Прилагаме файла за деплой на натоварване в Kubernetes.
Нека да бъдем по-конкретни. Тъй като заговорихме за ENV, нека да предположим, че ще трябва да предадем стойности на два параметъра: PARAM1 и PARAM2. Добавяме ги за задача на деплой, тип — String Parameter.
Скриншот

Ще генерираме YAML чрез просто пренасочване echo във файла. Предполага се, разбира се, че в Dockerfile имате PARAM1 и PARAM2, че името на натоварването ще бъде awesomeapp, а събран контейнер с приложението на посочената версия се намира в Container registry по пътя gcr.io/awesomeapp/awesomeapp-$BUILD_VERSION, където $BUILD_VERSION както беше избрано от падащото меню.
Изброяване на командите
touch deploy.yaml
echo "apiVersion: apps/v1" >> deploy.yaml
echo "kind: Deployment" >> deploy.yaml
echo "metadata:" >> deploy.yaml
echo " name: awesomeapp" >> deploy.yaml
echo "spec:" >> deploy.yaml
echo " replicas: 1" >> deploy.yaml
echo " selector:" >> deploy.yaml
echo " matchLabels:" >> deploy.yaml
echo " run: awesomeapp" >> deploy.yaml
echo " template:" >> deploy.yaml
echo " metadata:" >> deploy.yaml
echo " labels:" >> deploy.yaml
echo " run: awesomeapp" >> deploy.yaml
echo " spec:" >> deploy.yaml
echo " containers:" >> deploy.yaml
echo " - name: awesomeapp" >> deploy.yaml
echo " image: gcr.io/awesomeapp/awesomeapp-$BUILD_VERSION:latest" >> deploy.yaml
echo " env:" >> deploy.yaml
echo " - name: PARAM1" >> deploy.yaml
echo " value: $PARAM1" >> deploy.yaml
echo " - name: PARAM2" >> deploy.yaml
echo " value: $PARAM2" >> deploy.yamlСлед свързването, агентът на Jenkins не разполага с интерактивен режим, затова предаваме командите в облачната конзола чрез параметъра gcloud alpha cloud-shell ssh —command Почистваме домашната папка в облачната конзола от стария Dockerfile:.
gcloud alpha cloud-shell ssh --command="rm -f Dockerfile"
Слагаме новоизтегления Dockerfile в домашната папка на облачната конзола чрез SCP:gcloud alpha cloud-shell scp localhost:./Dockerfile cloudshell:~
Създаваме, тагваме и пушваме контейнера в Container registry:gcloud alpha cloud-shell ssh --command="docker build -t awesomeapp-$BUILD_VERSION ./ --build-arg BUILD_VERSION=$BUILD_VERSION --no-cache" gcloud alpha cloud-shell ssh --command="docker tag awesomeapp-$BUILD_VERSION gcr.io/awesomeapp/awesomeapp-$BUILD_VERSION" gcloud alpha cloud-shell ssh --command="docker push gcr.io/awesomeapp/awesomeapp-$BUILD_VERSION"
По аналогичен начин процедираме с файла за деплой. Обърнете внимание, че в долните команди се използват измислени имена на клъстери, където става деплой (
awsm-cluster) и името на проекта (awesome-project), където се намира клъстерът.), където се намира клъстера.
gcloud alpha cloud-shell ssh --command="rm -f deploy.yaml"
gcloud alpha cloud-shell scp localhost:.\/deploy.yaml cloudshell:~
gcloud alpha cloud-shell ssh --command="gcloud container clusters get-credentials awsm-cluster --zone us-central1-c --project awesome-project &&
kubectl apply -f deploy.yaml"Стартираме задачата, отваряме изхода от конзолата и се надяваме да видим успешна сборка на контейнера.
Скриншот

След това и успешен деплой на събраните контейнери
Скриншот

Умишлено пропуснах настройката Ingress. По една проста причина: след като бъде настроена с workload посочено име, тя ще остане работеща, независимо колко деплоя с това име ще се извършат. Иначе казано, това е малко извън рамките на историята.
Вместо изводите
Всички изброени стъпки, вероятно, можеше да не се правят, а просто да се инсталира някой плъгин за Jenkins, има безброй. Но по някаква причина не обичам плъгини. По-скоро, прибягвам до тях само от безизходица.
И също така просто ми харесва да разучавам нова за мен тема. Текстът по-горе е и начин да споделя находките, които направих, решавайки описаната в самото начало задача. Да споделя с тези, които не са лоши в DevOps. Ако поне на някого моите находки помогнат - ще бъда доволен.
Източник: habr.com

