Mindez ott kezdődött, hogy egyik fejlesztőcsapatunk csapata kérte, hogy tesztelési céllal tegyem elérhetővé az új alkalmazást, amelyet a múlt héten konténerizálásnak vetettek alá. Megtettem. Körülbelül 20 perccel később érkezett a kérés, hogy frissítsem az alkalmazást, mert egy nagyon szükséges dolgot befejeztek. Frissítettem. Még pár órával később... nos, már sejtitek, mi történt ezután...
Bevallom, elég lusta vagyok (ezt korábban már megemlítettem? nem?), és figyelembe véve azt a tényt, hogy a csapatvezetők férhetnek hozzá a Jenkinshez, ahol az összes CI/CD-nk található, azt gondoltam: hagyja, hogy ő telepítsen, amit csak akar! Eszembe jutott egy vicc: adj egy halat az embernek, és egy napig jóllakik; nevezd el az embert Jólakiknak, és egész életében jóllakott lesz. És elmentem munkát készíteni, amely képes arra, hogy bármely sikeresen összeállított verzióval telepítse a kubernetes konténert az alkalmazással és bármely értéket átadjon neki. ENV (A nagyapám, — aki a múltban angolt tanított, — most valószínűleg az ujjával a halántékát tekerné, és kifejezően nézne rám, miután elolvasta ezt a mondatot.).
Tehát a bejegyzésben arról fogok mesélni, hogyan tanultam meg:
- Dinamikusan frissíteni a feladatokat a Jenkinsszel a maga feladaton vagy más feladatokból;
- Csatlakozni a felhő konzolhoz (Cloud shell) egy telepített Jenkins ügynököt futtató csomópontról;
- Munkaterhelést (workload) telepíteni a Google Kubernetes Engine-be.
Valójában egy kicsit csúnyán beszélek. Feltételezhető, hogy az infrastruktúrájuk legalább egy része a Google felhőjében található, tehát felhasználója annak, és természetesen van GCP fiókja. De a bejegyzés nem erről szól.
Ez egy újabb segédlet, amit írtam. Ilyen bejegyzéseket csak egy esetben szeretnék írni: ha egy feladat áll előttem, nem tudom azonnal megoldani, a megoldást nem találtam meg kész formában, tehát apró részekre bontva kutattam, és végül megoldottam a feladatot. És annak érdekében, hogy a jövőben, amikor elfelejtem, hogyan csináltam, ne kelljen újra minden részletet megkeresnem és összeszednem, ilyen segédleteket írok magamnak.
Figyelem: 1. A bejegyzés „magamnak” íródott, nem vágyom arra, hogy best practice legyek. Szívesen olvasnék más lehetőségeket a kommentek között, hogy „de így kellett volna csinálni”.
2. Ha a bejegyzés alkalmazott részét sóként vesszük, akkor ez — mint az összes korábbi bejegyzésem — gyenge sóoldat.
A Jenkins feladatok beállításainak dinamikus frissítése
Már előre látom a kérdésedet: mi köze van a dinamikus frissítésnek a feladathoz? Kézzel írtam be egy string paraméter értékét, és már mehetek is!
Válaszolok: valóban lusta vagyok, nem szeretem, amikor panaszkodnak: Misi, a telepítés összeomlik, minden elveszett! Elkezded nézni, és ott van egy elírás egy futtató paraméter értékében. Ezért előnyben részesítem, ha mindent a lehető legbiztosabban csinálok. Ha van lehetőség megfosztani a felhasználót attól, hogy közvetlenül adatokat adjon meg, és inkább felsorolást biztosítani a választáshoz, akkor megszervezem a választást.
A terv a következő: létrehozunk egy feladatot a Jenkinsben, ahol a futtatás előtt egy listából ki lehet választani a verziót, és meg lehet adni az értékeket a paraméterekhez, amelyek a tárolóba kerülnek ENV, ezután összegyűjti a tárolót és feltölti a Container Registry-be. Innen a tárolót elindítja a kubernetesben mint workload a feladatban megadott paraméterekkel.
A Jenkins feladat létrehozásának és beállításának folyamatát nem vizsgáljuk, ez offtopik. Abból indulunk ki, hogy a feladat már készen áll. A dinamikusan frissíthető verziólistához szükségünk van két dologra: egy már meglévő forráslistára, amely előzetes érvényességű verziószámokat tartalmaz, és egy Choice parameter -ra a feladatban. A mi példánkban legyen a változó neve BUILD_VERSION, ezen nem állunk meg részletesebben. De a forráslistánál nézzük meg közelebbről.
A lehetőségek nem túl sokfélék. Nekem hirtelen két dolog jutott eszembe:
- A Jenkins által a felhasználók számára kínált Remote access API használata;
- A távoli repository mappa tartalmának lekérése (a mi esetünkben ez a JFrog Artifactory, ami nem lényeges).
Jenkins Remote access API
Követve a klasszikus hagyományt, elkerülném a hosszas magyarázatokat.
Megengedem magamnak, hogy csak egy szabad fordítást adjak az API :
A Jenkins API-t biztosít a funkcióihoz való távoli, gép által értelmezett hozzáféréshez. A távoli hozzáférés REST-szerű stílusban érhető el. Ez azt jelenti, hogy nincs egyetlen belépési pont az összes lehetőséghez, hanem helyette ".../api/" formájú URL-t használnak, ahol "…" egy olyan objektumot jelent, amelyre alkalmazni lehet az API lehetőségeit.
Más szóval, ha a jelenleg tárgyalt telepítő feladat elérhető az alábbi címen http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build, az API-trükkök ehhez a feladathoz a következő címen érhetők el http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/
Következő lépésként van lehetőségünk megválasztani, milyen formátumban szeretnénk a kimenetet. Maradjunk az XML-nél, mivel az API ebben az esetben lehetővé teszi a szűrést.
Próbáljuk meg így megszerezni az összes feladatindítás listáját. Csak a build nevét érdekel minket (displayName) és az eredményét (eredmény):
http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]Sikerült?
Most szűrjük meg azokat a futásokat, amelyek eredménye végül SUCCESS. Használjunk egy érvet &exclude és paraméterként adjuk meg az érték útját, ami nem egyenlő SUCCESS. Igen-igen. A kettős tagadás – az állítás. Kizárunk mindent, ami nem érdekel minket:
http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]&exclude=freeStyleProject/allBuild[result!='SUCCESS'] Képernyőkép a sikeres

Nos, csak a szórakozás kedvéért győződjünk meg arról, hogy a szűrő nem tévedett (a szűrők sosem hazudnak!), és megmutatjuk a «nem-sikeres» lista:
http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]&exclude=freeStyleProject/allBuild[result='SUCCESS'] Képernyőkép a nem-sikeres

A távoli szerver mappájából származó verziók listája
Van egy másik módja is a verziók listájának megszerzésére. Ez még tetszik is jobban, mint a Jenkins API elérése. Mivel ha az alkalmazás sikeresen összeállt, akkor azt becsomagolták és a megfelelő mappába helyezték a tárolóba. Olyan, mint a tároló alapértelmezett tárolója az alkalmazások funkcionális verzióinak. Kérdezzük meg, hogy milyen verziók vannak itt tárolva. A távoli mappát curl-lel, grep-pel és awk-kal fogjuk elérni. Ha valakit érdekel egy egysoros megoldás, az spoiler alatt található.
Egysoros parancs
Figyeljünk két dologra: a fejlécekben átadom a csatlakozási adatokat, és nem szükséges, hogy az összes verzió ott legyen, csak azokat válogatom ki, amelyeket egy hónapon belül hoztak létre. Szerkeszd a parancsot a saját valóságodnak és szükségeidnek megfelelően:
curl -H "X-JFrog-Art-Api:VeryLongAPIKey" -s http://arts.myre.po/artifactory/awesomeapp/ | sed 's/a href=//g' | grep "$(date +%b)-$(date +%Y)|$(date +%b --date='-1 month')-$(date +%Y)" | awk '{print $1}' | grep -oP '>K[^/]+')Feladatok beállítása és a feladat konfigurációs fájlja a Jenkinsben
Rendben van a verziók listájának forrása. Most integráljuk a megszerzett listát a feladatba. Számomra egyértelmű döntés volt, hogy lépést adjunk a feladatban az alkalmazás összeállításához. Egy lépés, amely a «siker» eredmény esetén hajtódik végre.
Nyissuk meg a feladat beállításait és görgessünk le egészen a végéig. Kattintsunk a gombokra: Add build step -> Conditional step (single). A lépés beállításaiban válasszuk az Jelenlegi build állapotfeltételt, állítsuk be az értéket SUCCESS, tegyük meg a kikérendő tevékenységet siker esetén Futtass shell parancsot.
És most jön a legérdekesebb. A Jenkins feladatok konfigurációi fájlokban tárolódnak. XML formátumban. Az útvonalon http://út-változó-hoz/task/config.xml Ezért letölthetjük a konfigurációs fájlt, a szükséges módon módosíthatjuk, majd visszahelyezhetjük a helyére, ahonnan elvettük.
Ne feledje, hogy fentebb megállapodtunk abban, hogy egy paramétert hozunk létre a verziók listájához. BUILD_VERSION?
Töltsük le a konfigurációs fájlt, és nézzük meg, hogy mi van benne. Csak hogy megbizonyosodjunk arról, hogy a paraméter a helyén van és valóban a megfelelő formában.
Képernyőkép a spoiler alatt.
Az Ön config.xml töredékének ugyanúgy kell kinéznie. Azzal a feltétellel, hogy a choices elem tartalma egyelőre hiányzik.

Megértette? Nos, írjunk egy szkriptet, amely a sikeres build esetén fog futni.
A szkript letölti a verziók listáját, letölti a konfigurációs fájlt, beleírja a szükséges helyre a verziók listáját, majd visszahelyezi. Igen. Minden rendben. Írja bele a verziók listáját az XML-be a helyre, ahol már van verziók listája (ez majd a szkript első futása után lesz). Tudom, hogy a világban még mindig léteznek heves regex rajongók. Én nem tartozom közéjük. Kérem, telepítse a arra a gépre, ahol a konfigurációt szerkesztik. Úgy gondolom, ez nem olyan nagy ár, hogy elkerüljük az XML sed-del való szerkesztését.
A spoiler alatt bemutatom a kódot, amely a fent leírt sorozatot teljes egészében végrehajtja.
A verziók listáját írjuk a konfigurációba, amely egy távoli szerver mappából származik.
#!/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.xmlHa az Jenkins-ből származó verziók megszerzésének módszere tetszik jobban, és olyan lusta vagy, mint én, akkor a spoiler alatt ugyanaz a kód található, de a lista az Jenkins-ből származik:
A verziók listáját írjuk a konfigurációba Jenkins-ből.
Csak vegye figyelembe: nálam a build neve a sorszám és a verziószám kettősponttal elválasztva. Ezért az awk levágja a felesleges részt. Ezt a sort módosítsa az Ön igényeinek megfelelően.
#!/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.xmlElvileg, ha tesztelte a fentiek alapján írt kódot, akkor a telepítési feladatban már meg kell jelennie a verziók legördülő listájának. Így néz ki körülbelül a képernyőkép a spoiler alatt.
Helyesen kitöltött verziólista.

Ha minden működött, akkor másolja be a szkriptet a Futtass shell parancsot és mentse a módosításokat.
Csatlakozás a Cloud shellhez.
A gyűjtők nálunk a konténerekben vannak. Az alkalmazások szállítási eszközeként és a konfigurációk menedzserként az Ansible-t használjuk. Tehát, amikor a konténerek összeszereléséről van szó, három lehetőség jut eszembe: Docker telepítése Docker-en belül, Docker telepítése az Ansible-ral rendelkező gépre, vagy konténerek építése a felhő konzolon. A Jenkins pluginokról ebben a megjegyzésben hallgassunk. Emlékszel?
Úgy döntöttem: ha a konténereket „do-it-yourself” módon is fel lehet építeni a felhő konzolon, akkor minek bonyolítani? Tartsuk tisztán, igaz? Azt szeretném, ha a Jenkins-szel építeném a konténereket a felhő konzolon, majd onnan dobnám őket a kubernetesbe. Ráadásul a Google infrastruktúráján belül nagyon erős csatornák vannak, ami kedvezően fogja befolyásolni a telepítési sebességet.
A felhő konzolhoz való csatlakozáshoz két dolog szükséges: gcloud és hozzáférési jogok a Google Cloud API azzal a VM példánnyal, ahonnan a csatlakozást végrehajtják.
Azok számára, akik nem a Google felhőből szeretnének csatlakozni
A Google lehetővé teszi az interaktív hitelesítés kikapcsolását a szolgáltatásainak. Ez lehetővé teszi, hogy a konzolhoz akár egy kávéfőzőről is csatlakozzunk, ha az *nix alapon működik, és önállóan is van konzolja.
Ha szükséged van arra, hogy részletesebben foglalkozzam ezzel a kérdéssel ebben a megjegyzés keretein belül — írjatok kommentben. Ha elegendő szavazat összegyűlik — frissítem ezt a témát.
A legegyszerűbb mód a jogok megadására — a webes felületen keresztül.
- Állítsa le azt a VM példányt, ahonnan a jövőben a felhő konzolhoz csatlakozni fog.
- Nyissa meg a Példány részleteit, és kattintson a Módosítani kell.
- Az oldal alján válassza ki a példány hozzáférési jogosultsági területét Teljes hozzáférés az összes Cloud API-hoz.
Képernyőkép

- Mentse a változtatásokat, és indítsa újra a példányt.
A VM indítása után csatlakozzon SSH-n keresztül, és győződjön meg arról, hogy a csatlakozás hiba nélkül történik. Használja a parancsot:
gcloud alpha cloud-shell ssh A sikeres csatlakozás körülbelül így néz ki

Telepítés GKE-be
Mivel mindent megteszünk, hogy teljesen átálljunk az IaC (Infrastruktúra mint kód) modellre, a dockerfile-okat a git-ben tároljuk. Ez az egyik oldal. A kubernetes telepítése yaml-fájllal van leírva, amelyet csak ez a feladat használ, amely önmagában szintén kódnak tekinthető. Ezzel együtt arra akarok kilyukadni, hogy a terv a következő:
- Vesszük a változók értékeit BUILD_VERSION és opcionálisan a változók értékei, amelyeket átadunk a ENV.
- Letöltjük a Dockerfile-t a gitből.
- YAML-t generálunk a telepítéshez.
- Mindkét fájlt SCP-n keresztül feltöltjük a felhő konzolba.
- Ott építjük a konténert és feltöltjük a Container registry-be.
- Alkalmazzuk a terhelés telepítési fájlját a kuberbe.
Legyünk konkrétabbak. Mivel szóba került a ENV, tegyük fel, hogy át kell adni két paraméter értékeit: PARAM1 és PARAM2. Hozzáadjuk őket a telepítéshez, típusa — String Parameter.
Képernyőkép

A YAML-t egyszerű átirányítással fogjuk generálni echo a fájlba. Nyilvánvalóan feltételezzük, hogy a Dockerfile-ban van PARAM1 és PARAM2, hogy a terhelés neve awesomeapp, és a megépített konténer az adott verzióval az Container registry Így meg kell találni a folyamatot, amely a gcr.io/awesomeapp/awesomeapp-$BUILD_VERSION, ahol $BUILD_VERSION éppen a legördülő listából lett választva.
Parancslistázás
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.yamlA Jenkins ügynöknek csatlakozás után gcloud alpha cloud-shell ssh interaktív mód nem érhető el, ezért a parancsokat a felhő konzolba adunk át a parancsparaméterrel.
Kitakarítjuk a felhő konzol otthoni mappáját a régi Dockerfile-tól:
gcloud alpha cloud-shell ssh --command="rm -f Dockerfile"A frissen letöltött Dockerfile-t a felhő konzol otthoni mappájába helyezzük SCP-vel:
gcloud alpha cloud-shell scp localhost:./Dockerfile cloudshell:~Összeállítjuk, címkézzük és feltöltjük a konténert a Container registry-be:
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"
Hasonló módon járunk el a telepítési fájl esetében is. Figyeljen rá, hogy az alábbi parancsokban kitalált klaszternevek használatára kerül sor, ahová a telepítés történik (awsm-cluster) és a projekt neve (awesome-project), ahol a klaszter található.
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"Elindítjuk a feladatot, megnyitjuk a konzol kimenetet és bízunk benne, hogy sikeres lesz a konténer build.
Képernyőkép

Aztán sikeresen telepítjük a felépített konténert.
Képernyőkép

Szándékosan figyelmen kívül hagytam a beállítást. Ingress. Egy egyszerű okból: miután egyszer beállítod egy megadott névvel, az működőképes marad, bármennyi telepítést is végezz ezzel a névvel. Egyébként is, ez kicsit kívül esik a téma keretein. workload Az összes fent említett lépést valószínűleg nem kellett volna megtenni, elegendő lett volna egy bármilyen Jenkins plugint telepíteni, több ezer kapható. De valamiért nem kedvelem a plugineket. Vagyis, csak végső megoldásként folyamodom hozzájuk.
Összegzések helyett
És egyszerűen csak szeretek beleásni magam egy új témába. A fenti szöveg – többek között – egy módot is jelent, hogy megosszam a felfedezéseimet, amelyeket a feladat megoldása során tettem. Megosztani azzal, aki, mint én, nem egy fenevad a devops világában. Ha akárkinek segítenek a felfedezéseim – elégedett leszek.
Készítünk egy feladatot a GKE-be való telepítéshez pluginek, SMS és regisztráció nélkül. Egy pillantást vetünk a Jenkins-re a zakó alatt.
Forrás: habr.com

