Vse se je začelo, ko je vodja ene izmed naših razvijalskih skupin prosil, da v testnem okolju objavimo njihovo novo aplikacijo, ki je bila pred tem podvržena kontejnerizaciji. Objavil sem jo. Približno po 20 minutah sem prejel prošnjo za posodobitev aplikacije, ker so dodali zelo potrebno funkcionalnost. Posodobil sem jo. Še po dveh urah ... no, verjetno že predvidevate, kaj se je zgodilo naprej ...
Moram priznati, da sem dokaj len (saj sem se že prej tega spomnil? ne?), in ob upoštevanju dejstva, da imajo vodje dostop do Jenkins-a, kjer imamo vse CI/CD, sem pomislil: naj kar sami nameščajo, kolikor jim paše! Spomnil sem se šale: daj človeku ribo in sit bo en dan; pokliči ga Sit in sit bo vse življenje. In šel sem izdelovati nalogo, ki bi znala nameščati v kuber konteiner z aplikacijo katere koli uspešno sestavljene različice in prenesti vanj poljubne vrednosti ENV (moj dedek, filolog, nekdanji učitelj angleščine, bi zdaj vrtel prst okoli ušesa in me zelo izrazito pogledal, ko bi prebral to poved).
Torej, v opombi bom opisal, kako sem se naučil:
- Dinamiko posodabljanja nalog v Jenkins-u iz same naloge ali iz drugih nalog;
- Povezovanje z oblačno konzolo (Cloud shell) z nodi, na kateri je nameščen agent Jenkins;
- Namestitev delovne obremenitve (workload) v Google Kubernetes Engine.
Dejansko, seveda nekoliko pretiram. Predpostavlja se, da je vsaj del infrastrukture v Googlovem oblaku, kar pomeni, da ste njegov uporabnik in seveda, da imate račun GCP. Ampak opomba ni o tem.
To je še en moj priročnik. Takšne opombe želim pisati le v enem primeru: pred mano je bila naloga, sprva nisem vedel, kako jo rešiti, rešitev se ni ponudila v vseh podrobnostih, zato sem jo iskal po delih in na koncu nalogo rešitev. In da v prihodnosti, ko bom pozabil, kako sem to naredil, ne bom moral znova iskat po kosih in vse združiti, pišem takšne priročnike.
Izjava: 1. Opomba je bila napisana 'za se', ne da bi bila na mestu best practice ni. Z veseljem bom prebral predloge 'bolje bi bilo narediti tako' v komentarjih.
2. Če praktični del opombe štejem za sol, potem, kot vse moje prejšnje opombe, je tudi ta - raztopina z malo soli.
Dinamično ažuriranje postavki poslova u Jenkinsu
Predviđam vaše pitanje: kakve veze ima dinamično ažuriranje posla? Ručno unesem vrednost string parametra i idemo dalje!
Odgovaram: stvarno sam lenj, ne volim kada se žale: Miša, deploy se ruši, sve je propalo! Počnem da gledam, a tamo je greška u vrednosti nekog parametra koji pokreće posao. Zato preferiram sve raditi maksimalno otpornim na greške. Ako postoji mogućnost da se korisniku oduzme mogućnost direktnog unosa podataka, umesto toga pružam listu vrednosti za izbor.
Plan je sledeći: kreiramo posao u Jenkinsu, pre pokretanja može se iz liste odabrati verzija, navesti vrednosti za parametre koji se prožimaju u kontejner kroz ENV, zatim se kontejner gradi i šalje u Container Registry. Od tamo se kontejner pokreće u kubernetesu kao workload sa parametrima koji su zadati u poslu.
Proces kreiranja i podešavanja posla u Jenkinsu nećemo razmatrati, to je izvan teme. Polazićemo od pretpostavke da je posao spreman. Za implementaciju ažurirane liste verzija potrebne su nam dve stvari: već postojeća izvorna lista sa unapred validnim brojevima verzija i promenljiva tipa Choice parameter u poslu. U našem primeru neka promenljiva nosi ime BUILD_VERSION, nećemo se detaljno zadržavati na njoj. Ali se zadržimo malo više na izvornoj listi.
Opcija nije toliko mnogo. Odmah mi padaju na pamet dve:
- Koristiti Remote access API koji Jenkins nudi svojim korisnicima;
- Zahtevati sadržaj udaljene mape repozitorijuma (u našem slučaju to je JFrog Artifactory, što nije presudno).
Jenkins Remote access API
Prema lepom običaju, preferiram da izbegnem dugotrajna objašnjenja.
Dopustite mi samo slobodan prevod dela prvog paragrafa :
Jenkins pruža API za udaljeni mašinski razumljiv pristup svojoj funkcionalnosti. Udaljeni pristup se nudi u REST-ličnom stilu. To znači da ne postoji jedinstvena tačka ulaza za sve mogućnosti, već se koristi URL oblika ".../api/", gde "…" označava objekat na koji se primenjuju mogućnosti API-ja.
Drugim rečima, ako je posao za deploy o kojem trenutno govorimo dostupan na adresi http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build, API zvoni za to nalogo dostopni na naslovu http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/
Nato imamo izbiro, v kakšni obliki prejeti izhod. Osredotočimo se na XML, saj API le v tem primeru omogoča filtriranje.
Poskusimo le tako pridobiti seznam vseh izvajanj naloge. Zanima nas samo ime zbirke (displayName) in njen rezultat (result):
http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]Je uspelo?
Sedaj bomo filtrirali le tista izvajanja, ki so na koncu z rezultatom SUCCESS. Uporabimo argument &exclude in kot parameter mu posredujemo pot do vrednosti, ki ni enaka SUCCESS. Da, da. Dvojna negacija — to je trditev. Izključimo vse, kar nas ne zanima:
http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]&exclude=freeStyleProject/allBuild[result!='SUCCESS'] Posnetek seznama uspešnih

In preprosto za zabavo se prepričajmo, da nas filtriranje ni prevaralo (filtri nikoli ne lagajo!) in izpisali bomo seznam "ne-uspešnih":
http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]&exclude=freeStyleProject/allBuild[result='SUCCESS'] Posnetek seznama ne-uspešnih

Seznam različic iz mape na oddanem strežniku
Obstaja tudi drugi način za pridobitev seznama različic. Ta mi je še bolj všeč kot klic API-ja Jenkins. No, ker če je aplikacija uspešno zgrajena, pomeni, da so jo spakirali in shranili v repozitorij v ustrezno mapo. Tako, repozitorij je po privzetku shramba delovnih različic aplikacij. Tako. No in vprašajmo ga, katere različice so na voljo. Oddano mapo bomo curlali, grepali in awkali. Če nekoga zanima enovrstčna ukazna vrstica, je pod spoilerjem.
Ukazna vrstica
Upoštevajte dve stvari: v glavi posredujem potrebne podatke za povezavo in ne potrebujem prav vseh različic iz mape, izbiram le tiste, ki so bile ustvarjene v zadnjem mesecu. Uredite ukaz v skladu s svojimi realnostmi in potrebami:
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[^/]+' )Nastavitev nalog in datoteka s konfiguracijo naloge v Jenkinsu
S seznamom različic smo se spoprijeli. Zdaj bomo pridobljen seznam vključili v nalogo. Zame je bilo očitna rešitev dodati korak v nalogi za gradnjo aplikacije. Korak, ki se izvrši v primeru rezultata „uspeh“.
Odprimo nastavitve naloge za gradnjo in pomikamo se do dna. Pritisnemo na gumbe: Dodaj korak gradnje -> Pogojski korak (enojno). V nastavitvah koraka izberemo pogoj Trenutni status gradnje, nastavimo vrednost SUCCESS, izvajanje dejanja v primeru uspeha Izvedi ukaz v lupini.
In now the most interesting part. Jenkins job configurations are stored in files. In XML format. At the path http://path-to-job/config.xml Accordingly, you can download the configuration file, edit it as needed, and put it back in its original place.
Remember, earlier we agreed that we would create a parameter for the list of versions. BUILD_VERSION?
Let's download the configuration file and take a look inside. Just to make sure that the parameter is present and is indeed in the correct format.
Screenshot under the spoiler.
Your provided fragment of config.xml should look the same. With the exception that the content of the choices element is currently absent.

Got it? Well, let's write a script that will run in case of a successful build.
The script will obtain the list of versions, download the configuration file, write the list of versions to the necessary place in it, and then put it back. Yes. That's right. Writing the list of versions in the XML file where there is already a list of versions (which will be added in the future, after the first run of the script). I know, there are still hardcore fans of regular expressions out there. I don't belong to them. Please install on the machine where the config will be edited. It doesn't seem like a big price to pay to avoid editing XML using sed.
Under the spoiler, I provide the code that executes the above sequence in its entirety.
We write the list of versions from the folder on the remote server to the config
#!/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.xmlIf you preferred the version with obtaining versions from Jenkins and you are just as lazy as I am, then under the spoiler is the same code, but the list is from Jenkins:
We write the list of versions from Jenkins to the config
Just keep in mind: my build name consists of a sequence number and a version number separated by a colon. Accordingly, awk cuts off the unnecessary part. Modify this line to suit your needs.
#!/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.xmlIn theory, if you have tested the code written based on the examples above, then in the deploy job you should already have a dropdown list with versions. This is approximately how it looks in the screenshot under the spoiler.
Correctly filled list of versions

If everything worked out, then copy and paste the script into Izvedi ukaz v lupini and save the changes.
Connection to Cloud shell
Sestavljalci so pri nas v kontejnerjih. Kot sredstvo za dostavo aplikacij in upravljalec konfiguracij uporabljamo Ansible. Tako da, ko gre za sestavljanje kontejnerjev, pridejo v misli tri možnosti: namestiti Docker v Dockerju, namestiti Docker na stroj z Ansiblejem, ali pa sestavljati kontejnerje v oblačnem konzoli. O vtičnikih za Jenkins smo se dogovorili, da v tem zapisku molčimo. Se spomnite?
Odločil sem se: no, saj kontejnerje "iz škatle" lahko sestavljamo v oblačni konzoli, zato zakaj si komplicirati življenje? Naj bo čisto, a ne? Želim sestavljati kontejnerje z Jenkinsom v oblačni konzoli in potem jih od tam izstreliti v Kubernetes. Še posebej, ker znotraj infrastrukture Googla imamo res zelo zmogljive povezave, kar bo pozitivno vplivalo na hitrost namestitve.
Za povezovanje z oblačno konzolo potrebujemo dve stvari: gcloud in pravice dostopa do Google Cloud API za tisto VM instanco, s katero bo to povezovanje izvedeno.
Za tiste, ki nameravajo povezovati izven Googlovega oblaka
Google dovoljuje možnost izklopa interaktivne avtentikacije v svojih storitvah. To bo omogočilo povezovanje s konzolo tudi s kavnega aparata, če je ta pod *nix in ima sama konzolo.
Če obstaja potreba, da ta vprašanje podrobneje osvetlim v okviru tega zapiska — pišite v komentarjih. Če se zbere dovolj glasov — bom napisal posodobitev na to temo.
Najenostavnejši način za dodelitev pravic je preko spletnega vmesnika.
- Ustavite VM instanco, s katere bo pozneje potekalo povezovanje z oblačno konzolo.
- Odprite Podrobnosti instance in kliknite Spremeni.
- Na dnu strani izberite obseg dostopa instance Poln dostop do vseh Cloud API.
Zaslon

- Shranjene spremembe in zaženite instanco.
Po končanju nalaganja VM se povežite z njo preko SSH in se prepričajte, da se povezava odvija brez napak. Uporabite ukaz:
gcloud alpha cloud-shell ssh Uspešna povezava izgleda približno tako

Namestitev v GKE
Ker si neprestano prizadevamo popolnoma preiti na IaC (Infrastructure as Code), imamo Docker datoteke shranjene v Gitu. To je ena stran. Namestitev v Kubernetes pa opisuje yaml datoteka, ki se uporablja samo za to nalogo, ki je sama po sebi tudi nekakšna koda. To je druga stran. Na splošno, želim povedati, da je načrt tak:
- Vzemi vrednosti spremenljivk BUILD_VERSION in, optionally, the variable values that will be passed through ENV.
- We pull the Dockerfile from Git.
- We generate the YAML for deployment.
- We upload both of these files via SCP to the cloud console.
- We build the container there and push it to the Container registry
- We apply the deployment configuration file in Kubernetes.
Let's be more specific. Since we are talking about ENV, let's assume we need to pass the values of two parameters: PARAM1 in PARAM2. We add their assignment to the deployment, type — String Parameter.
Zaslon

We will generate the YAML by simply redirecting it echo to a file. It is assumed, of course, that your Dockerfile contains PARAM1 in PARAM2, that the name of the load will be awesomeapp, and that the built container with the specified version is located in Container registry at the path gcr.io/awesomeapp/awesomeapp-$BUILD_VERSION, gde $BUILD_VERSION was just selected from the dropdown list.
Listing of commands
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.yamlTo the Jenkins agent after connecting via gcloud alpha cloud-shell ssh the interactive mode is not available, so we pass commands to the cloud console using the parameter —command.
We clean the home directory in the cloud console from the old Dockerfile:
gcloud alpha cloud-shell ssh --command="rm -f Dockerfile"We place the freshly downloaded Dockerfile in the home directory of the cloud console using SCP:
gcloud alpha cloud-shell scp localhost:./Dockerfile cloudshell:~We build, tag, and push the container to the 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"
We act similarly with the deployment file. Please note that the commands below use fictional names for the cluster where the deployment is taking place (awsm-cluster) and the project name (awesome-project), where the cluster is located.
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"Запускамо задачу, відкриваємо вивід консолі і сподіваємось побачити успішне збирання контейнера.
Zaslon

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

Я навмисно обійшов увагою налаштування Ingress. З однієї простої причини: одного разу налаштувавши його на workload з заданим ім'ям, він залишиться працездатним, скільки б деплоїв з цим ім'ям не проводили. І взагалі, це трохи за межами історії.
Namesto zaključkov
Усі наведені вище кроки, мабуть, можна було не робити, а просто встановити якийсь плагін для Jenkins, їх мільйон. Але я чомусь не люблю плагіни. Ну, точніше, вдаюсь до них тільки від безвиході.
А ще мені просто подобається розкривати якусь нову для мене тему. Текст вище — це також спосіб поділитися знахідками, які я зробив, вирішуючи описане на самому початку завдання. Поділитися з тими, хто, як і я, зовсім не є грубим вовком у девопсі. Якщо хоча б комусь мої знахідки допоможуть — буду радий.
Izvor: habr.com

