Майструємо завдання на деплой у GKE без плагінів, смс та реєстрації. Одним вічком заглядаємо Jenkins'у під піджак

Все почалося з того, що тимлід однієї з наших команд розробників попросив у тестовому режимі виставити назовні їх новий додаток, який напередодні був підданий контейнеризації. Я виставив. Приблизно через 20 хвилин надійшло прохання оновити програму, тому що там допила дуже потрібну штуку. Я обновив. Ще за пару годин… ну, ви й так здогадуєтеся, що сталося далі…

Я, зізнатися, досить лінивий (я ж раніше в цьому зізнавався? ні?), і, враховуючи той факт, що тимліди мають доступ у Jenkins, в якому у нас весь CI/CD, подумав: так нехай він сам деплоїть, скільки заманеться ! Згадав анекдот: дай людині рибу і він буде ситий день; назви людину Сит і він буде Сит все життя. І пішов майструвати джобуяка б вміла деплоїти в кубер контейнер з додатком будь-якої успішно зібраної версії і передавати в нього будь-які значення ENV (мій дідусь, - філолог, викладач англійської в минулому, - зараз би покрутив пальцем біля скроні і дуже виразно подивився б на мене, прочитавши цю пропозицію).

Отже, у замітці я розповім про те, як я навчився:

  1. Динамічно оновлювати завдання в Jenkins'і із самого завдання або з інших завдань;
  2. Підключатися до хмарної консолі (Cloud shell) з ноди із встановленим агентом Jenkins'а;
  3. Деплоїти робоче навантаження (workload) у Google Kubernetes Engine.


Насправді, я, звичайно, дещо лукавлю. Передбачається, що хоча б частина інфраструктури у вас у кутовій хмарі, а, отже, ви її користувач і, зрозуміло, у вас є обліковий запис GCP. Але нотатка не про це.

Це моя чергова шпаргалка. Такі нотатки мені хочеться писати лише в одному випадку: переді мною стояло завдання, я спочатку не знав, як його вирішити, рішення не нагуглилось у готовому вигляді, тому я його гуглив частинами і в результаті завдання вирішив. І для того, щоб у майбутньому, коли я забуду, як я це зробив, мені не довелося знову все гуглити шматками і компілювати воєдино, я пишу собі такі шпаргалки.

Відмова від відповідальності: 1. Нотатка писалася «для себе», на роль краща практика не претендує. Із задоволенням вважаю варіанти «а краще було зробити так» у коментарях.
2. Якщо прикладну частину нотатки вважати сіллю, то, як і всі мої попередні нотатки, ця – слабосольовий розчин.

Динамічне оновлення налаштувань завдань у Jenkins

Передбачаю ваше запитання: а до чого тут взагалі динамічне оновлення джоби? Вписав ручками значення рядкового параметра та вперед!

Відповідаю: я правда лінивий, не люблю, коли скаржаться: Мишко, деплой фарбується, все пропало! Починаєш дивитися, а там помилка у значенні якогось параметра запуску завдання. Тому волію все робити максимально фулпруфно. Якщо є можливість позбавити користувача можливості вводити дані безпосередньо, давши натомість список значень для вибору, то я організовую вибір.

План такий: створюємо завдання в Jenkins, в якому перед запуском можна було б зі списку вибрати версію, вказати значення для параметрів, що передаються в контейнер через ENV, Далі воно збирає контейнер і гартує його в Container Registry. Далі звідти контейнер запускається у кубері як навантаження із параметрами, заданими в джобі.

Процес створення та налаштування завдання в Jenkins'і розглядати не будемо, це офтопік. Виходитимемо з того, що завдання готове. Для реалізації оновлюваного списку з версіями, нам потрібні дві речі: вже наявний список-джерело з апріорі валідними номерами версій та змінна типу Choice parameter у завданні. У нашому прикладі нехай змінна носитиме ім'я BUILD_VERSION, на ній зупинятись докладно не будемо. А ось на списку-джерелі давайте зупинимося докладніше.

Варіантів не така вже й безліч. Мені відразу на думку прийшли два:

  • використовувати Remote access API, який пропонує Jenkins своїм користувачам;
  • Запитувати вміст віддаленої папки репозиторію (у нашому випадку це JFrog Artifactory, що не важливо).

Jenkins Remote access API

За прекрасною традицією волію уникнути розлогих пояснень.
Дозволю собі лише вільний переклад шматка першого абзацу першої сторінки документації з 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 лише у разі дозволяє використовувати фільтрацію.

Давайте просто спробуємо отримати список всіх запусків завдання. Нас цікавить тільки ім'я збирання (відображуване ім'я) та її результат (результат):

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']

Скріншот списку успішних
Майструємо завдання на деплой у GKE без плагінів, смс та реєстрації. Одним оком заглядаємо Jenkins'у під піджак

Ну і просто для пустощів переконаємося, що фільтр нас не обдурив (фільтри ж ніколи не брешуть!) і виведемо список «не-успішних»:

http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]&exclude=freeStyleProject/allBuild[result='SUCCESS']

Скріншот списку не-успішних
Майструємо завдання на деплой у GKE без плагінів, смс та реєстрації. Одним оком заглядаємо Jenkins'у під піджак

Список версій із папки на віддаленому сервері

Є й другий спосіб отримати список версій. Він мені подобається навіть більше, ніж звернення до 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 поки що відсутня
Майструємо завдання на деплой у GKE без плагінів, смс та реєстрації. Одним оком заглядаємо Jenkins'у під піджак

Переконалися? Ну все, пишемо скрипт, який виконуватиметься у разі успішного складання.
Скрипт отримуватиме список версій, завантажуватиме файл конфігурації, писатиме в нього в потрібне нам місце список версій, а потім кластиме його назад. Так. Все вірно. Писати список версій у XML'ку на те місце, де вже є список версій (буде в майбутньому, після першого запуску скрипта). Я знаю, що у світі ще живуть люті любителі регулярних виразів. Я до них не належу. Встановіть, будь ласка, xmlstarler на ту машину, де редагуватиметься конфіг. Мені здається, це не така вже й велика плата за те, щоб уникнути редагування 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

За ідеєю, якщо ви протестували код, написаний на основі прикладів вище, то в завдання на деплой у вас вже повинен з'явитися список, що випадає з версіями. Ось приблизно на скріншоті під спойлером.

Коректно заповнений список версій
Майструємо завдання на деплой у GKE без плагінів, смс та реєстрації. Одним оком заглядаємо Jenkins'у під піджак

Якщо все відпрацювало, то копіпастіть скрипт у Run shell command та зберігайте зміни.

Підключення до Cloud shell

Складачі у нас у контейнерах. Як засіб доставки додатків та менеджера конфігурацій ми використовуємо Ansible. Відповідно, коли мова заходить про складання контейнерів, варіантів на думку спадає три: встановити Docker в Docker'і, встановити Docker на машину з Ansible'ом, або збирати контейнери в хмарній консолі. Про плагіни для Jenkins ми домовилися у цій нотатці мовчати. Пам'ятаєте?

Я вирішив: ну, якщо контейнери «з коробки» можна збирати в хмарній консолі, то навіщо городити город? Keep it clean, правда? Хочу збирати контейнери Jenkins'ом у хмарній консолі, а потім звідти ж куляти їх у кубер. Тим більше, що всередині інфраструктури у гугла дуже жирні канали, що сприятливо позначиться на швидкості деплою.

Для підключення до хмарної консолі потрібні дві речі: gcloud та права доступу до Google Cloud API для того екземпляра ВМ, з якою це саме підключення здійснюватиметься.

Для тих, хто планує підключатися взагалі не з гуглової хмари
Google допускає можливість відключення інтерактивної авторизації у своїх сервісах. Це дозволить підключатися до консолі хоч з кавоварки, коли вона під *nix'ами і в неї самої є консоль.

Якщо є потреба у тому, щоб я висвітлив це питання докладніше в рамках цієї нотатки — пишіть у коментарях. Набереться достатня кількість голосів — напишу апдейт із цієї теми.

Найпростіший спосіб дати права через веб-інтерфейс.

  1. Зупиніть екземпляр ВМ, з якого надалі буде здійснюватись підключення до хмарної консолі.
  2. Відкрийте Відомості екземпляра та натисніть Змінити.
  3. Внизу сторінки виберіть область дії доступу екземпляра Повний доступ до всіх Cloud API.

    Скріншот
    Майструємо завдання на деплой у GKE без плагінів, смс та реєстрації. Одним оком заглядаємо Jenkins'у під піджак

  4. Збережіть зміни та запустіть екземпляр.

Після закінчення завантаження ВМ, підключіться до неї SSH і переконайтеся, що підключення відбувається без помилки. Скористайтеся командою:

gcloud alpha cloud-shell ssh

Успішне підключення виглядає приблизно так
Майструємо завдання на деплой у GKE без плагінів, смс та реєстрації. Одним оком заглядаємо Jenkins'у під піджак

Деплой у GKE

Оскільки ми всіляко прагнемо повністю перейти на IaC (Infrastucture as a Code), докерфайли у нас зберігаються у гіті. Це з одного боку. А деплой в kubernetes описується yaml-файлом, який використовується тільки даним завданням, який сам по собі теж код. Це з іншого боку. Загалом, я до того, що план такий:

  1. Беремо значення змінних BUILD_VERSION та, опціонально, значення змінних, які будуть передані через ENV.
  2. Качаємо з гіта докерфайл.
  3. Генеруємо yaml для деплою.
  4. Заливаємо обидва ці файли по scp в хмарну консоль.
  5. Білимо там контейнер і пушаємо його в Container registry
  6. Застосовуємо файл деплою навантаження у кубер.

Давайте конкретніше. Раз заговорили про ENV, то припустимо, нам треба буде передавати значення двох параметрів: ПАРАМЕТР1 и ПАРАМЕТР2. Додаємо їхнє завдання на деплой, тип — Рядковий параметр.

Скріншот
Майструємо завдання на деплой у GKE без плагінів, смс та реєстрації. Одним оком заглядаємо Jenkins'у під піджак

Генерувати yaml будемо простим перенаправленням нудьгувати у файл. Передбачаються, зрозуміло, що у докерфайлі у вас присутні ПАРАМЕТР1 и ПАРАМЕТР2, що ім'я навантаження буде awesomeapp, а зібраний контейнер із додатком зазначеної версії лежить у Реєстр контейнерів по дорозі 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.

Чистимо домашню папку в хмарній консолі від старого докерфайлу:

gcloud alpha cloud-shell ssh --command="rm -f 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"

Запускаємо завдання, відкриваємо виведення консолі та сподіваємося побачити успішне складання контейнера.

Скріншот
Майструємо завдання на деплой у GKE без плагінів, смс та реєстрації. Одним оком заглядаємо Jenkins'у під піджак

А далі й успішний деплой зібраного контейнера

Скріншот
Майструємо завдання на деплой у GKE без плагінів, смс та реєстрації. Одним оком заглядаємо Jenkins'у під піджак

Я навмисне обійшов увагою налаштування Вхід. З однієї простої причини: якось налаштувавши його на навантаження із заданим ім'ям, він залишиться працездатним, скільки деплоїв із цим ім'ям не проводи. Ну і взагалі це трохи за рамками історії.

замість висновків

Всі наведені вище кроки, напевно, можна було не робити, а просто встановити якийсь плагін для Jenkins'а, їхній мууульйон. Але я чомусь не люблю плагіни. Ну, точніше, вдаюсь до них лише від безвиході.

А ще мені просто подобається розколупати якусь нову для мене тему. Текст вище - у тому числі і спосіб поділитися знахідками, які я зробив, вирішуючи описане на початку завдання. Поділитися з тими, хто, як і, зовсім не лютий вовк у девопсі. Якщо хоча б комусь мої знахідки допоможуть — задоволений.

Джерело: habr.com

Додати коментар або відгук