Vše začalo tím, že týmový vedoucí jednoho z našich vývojových týmů požádal o dočasné vystavení jejich nové aplikace, která byla nedávno containerizována. Učil jsem ji. Přibližně po 20 minutách přišla žádost o aktualizaci aplikace, protože tam dodali velmi potřebnou funkci. Aktualizoval jsem. Ještě po pár hodinách… snad už víte, co se dělo dál…
Přiznám se, jsem dost líný (už jsem se tím dříve přiznal? ne?), a vzhledem k tomu, že týmoví vedoucí mají přístup k Jenkinsu, ve kterém máme celé CI/CD, jsem si řekl: ať to nasazuje sám, jak chce! Vzpomněl jsem si na vtip: dej mu rybu a on bude sytý jeden den; nazvi ho Sytý a on bude sytý po celý život. A šel jsem. vytvořit job, který by uměl nasazovat do Kubernetes kontejner s aplikací jakékoli úspěšně sestavené verze a předávat jí jakékoli hodnoty. ENV (můj dědeček, — filolog, bývalý učitel angličtiny, — by teď otáčel prstem u spánku a velmi výmluvně by se na mě podíval poté, co by přečetl tuto větu).
Takže v tomto článku se podělím o to, jak jsem se naučil:
- Dynamicky aktualizovat úkoly v Jenkinsu přímo z úkolu nebo z jiných úkolů;
- Připojit se k cloudové konzoli (Cloud shell) z uzlu s nainstalovaným agentem Jenkins;
- Nasazovat pracovní zátěž (workload) do Google Kubernetes Engine.
Ve skutečnosti, samozřejmě, trochu přeháním. Předpokládá se, že alespoň část infrastruktury máte v Google cloudu, a tudíž jste jeho uživatel a samozřejmě máte účet GCP. Ale tento článek není o tom.
Toto je moje další příručka. Takové poznámky chci psát jen v jedné situaci: stála přede mnou úloha, zprvu jsem nevěděl, jak ji vyřešit, řešení jsem nenašel hotové na Googlu, takže jsem si ho hledal po částech a nakonec úkol vyřešil. A abych si do budoucna, když zapomenu, jak jsem to udělal, nemusel znovu všechno googlit po kouskách a skládal to dohromady, píšu si takové příručky.
Upozornění: 1. Tento článek byl psán "pro sebe", a nemá nárok na roli best practice rád si přečtu varianty "ale bylo by lepší udělat to tak" v komentářích.
2. Pokud by se aplikovaná část poznámky považovala za sůl, pak, jako všechny mé předchozí poznámky, tato – je slabým solným roztokem.
Dynamické aktualizace nastavení úloh v Jenkins.
Předvídám vaši otázku: a co má dynamická aktualizace úlohy společného? Zadám ručně hodnotu řetězcového parametru a jedu dál!
Odpovídám: opravdu jsem líný, nemám rád, když si lidé stěžují: Míšo, nasazení se rozpadá, všechno je ztraceno! Začnete zkoumat a tam je překlep v hodnotě nějakého parametru spouštění úlohy. Proto se snažím vše dělat maximálně bezchybně. Pokud je možné zbavit uživatele možnosti zadávat údaje přímo, místo toho nabídnu seznam hodnot k výběru.
Plán je následující: vytvořme úlohu v Jenkins, kde by bylo možné před spuštěním vybrat verzi ze seznamu, uvést hodnoty parametrů předávaných do kontejneru přes ENV, pak se kontejner sestaví a nahrává do Container Registry. Odtud je kontejner spouštěn v Kubernetes jako workload s parametry zadanými v úloze.
Proces vytvoření a nastavení úlohy v Jenkins nebudeme zvažovat, to je off-topic. Budeme vycházet z toho, že úloha je připravena. K realizaci aktualizovatelného seznamu s verzemi potřebujeme dvě věci: již existující zdrojový seznam s apriorně platnými čísly verzí a proměnnou typu Choice parameter v úloze. V našem příkladu ať proměnná nese jméno BUILD_VERSION, na ní se podrobněji nezastavíme. Ale na zdrojovém seznamu se zastavíme podrobněji.
Možností není zase tolik. Hned mě napadly dvě:
- Použít Remote access API, které Jenkins nabízí svým uživatelům;
- Žádat o obsah vzdálené složky repozitáře (v našem případě to je JFrog Artifactory, což není podstatné).
Jenkins Remote access API
Podle zavedené skvělé tradice se raději vyhnu rozsáhlým vysvětlením.
Dovolím si jen volný překlad části prvního odstavce :
Jenkins poskytuje API pro vzdálený strojově srozumitelný přístup k jeho funkčnosti. Vzdálený přístup je nabízen ve stylu podobném REST. To znamená, že neexistuje jednotný vstupní bod ke všem funkcím, ale místo toho se používá URL typu "…/api/", kde "…" představuje objekt, na který se vztahují možnosti API.
Jinými slovy, pokud je úkol na nasazení, o kterém právě mluvíme, k dispozici na adrese http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build, pak jsou API-endpointy pro tento úkol k dispozici na adrese http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/
Dále máme na výběr, v jaké podobě chceme získat výstup. Zaměříme se na XML, protože API v tomto případě umožňuje použití filtrování.
Pojďme si zkusit získat seznam všech spuštění úkolu. Zajímá nás pouze název sestavení (displayName) a jeho výsledek (result):
http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]Podařilo se to?
Teď si filtrujeme pouze ta spuštění, která mají výsledek SUCCESS. Použijeme argument &exclude a jako parametr mu předáme cestu k hodnotě, která se nerovná SUCCESS. Ano, ano. Dvojité negace znamená afirmaci. Vylučujeme všechno, co nás nezajímá:
http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]&exclude=freeStyleProject/allBuild[result!='SUCCESS'] Snímek obrazovky seznamu úspěšných

A abychom se jen tak pobavili, ujistíme se, že nás filtr nezklamal (filtry přece nikdy nelžou!) a zobrazíme seznam "neúspěšných":
http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]&exclude=freeStyleProject/allBuild[result='SUCCESS'] Snímek obrazovky seznamu neúspěšných

Seznam verzí z adresáře na vzdáleném serveru
Existuje i druhý způsob, jak získat seznam verzí. Ten se mi dokonce líbí víc než dotazování na API Jenkins. No, protože pokud se aplikace úspěšně zkompilovala, byla zabalena a umístěna do repozitáře ve správném adresáři. V podstatě, repozitář je ve výchozím nastavení úložiště pracovních verzí aplikací. Tak se zeptejte, jaké verze jsou uloženy. Vzdálený adresář budeme stahovat pomocí curl, grep a awk. Pokud by někoho zajímal one-liner, tak je pod spoilerem.
Příkaz jedním řádkem
Všimněte si dvou věcí: předávám v hlavičce údaje pro připojení a nepotřebuji vůbec všechny verze z adresáře, vybírám pouze ty, které byly vytvořeny během měsíce. Upravte příkaz podle svých potřeb:
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[^/]+ )Nastavení úkolů a konfigurační soubor úkolu v Jenkins
Se seznamem verzí jsme se vyřešili. Teď získaný seznam začneme s úkolem. Pro mě bylo jasným řešením přidat krok do úkolu při sestavování aplikace. Krok, který by se vykonal v případě výsledku „úspěch“.
Otevřete nastavení úkolu pro sestavení a scrollujte dolů. Klikněte na tlačítka: Přidat krok sestavení -> Podmínkový krok (jednotlivě). V nastavení kroku vybíráme podmínku Aktuální stav sestavení, nastavujeme hodnotu SUCCESS, vykonávaná akce v případě úspěchu Spustit shell příkaz.
A teď to nejzajímavější. Konfigurace úloh Jenkins se ukládají do souborů. Ve formátu XML. Na cestě http://cesta-k-úloze/config.xml Odpovídajícím způsobem lze stáhnout soubor s konfigurací, upravit ho podle potřeby a vrátit zpět na místo, odkud byl stažen.
Pamatujte, že jsme se výše dohodli, že pro seznam verzí vytvoříme parametr BUILD_VERSION?
Stáhněme soubor konfigurace a podívejme se dovnitř. Jen abychom se ujistili, že parametr je na místě a opravdu v požadovaném formátu.
Snímek obrazovky pod spoilerem.
U vás by měl uvedený fragment config.xml vypadat stejně. S tím rozdílem, že obsah prvku choices zatím chybí

Jste si jisti? Dobře, napišme skript, který se bude provádět v případě úspěšného sestavení.
Skript bude získávat seznam verzí, stahovat soubor konfigurace, zapisovat do něj potřebný seznam verzí a pak ho vrátí zpět. Ano. Je to správně. Zapisovat seznam verzí do XML na místo, kde již seznam verzí existuje (to se stane v budoucnu, po prvním spuštění skriptu). Vím, že na světě stále žijí zavilí milovníci regulárních výrazů. Já mezi ně nepatřím. Prosím, nainstalujte na stroj, kde bude konfigurační soubor upravován. Myslím, že to není tak velká cena za to, abychom se vyhnuli úpravě XML pomocí sed.
Pod spoilerem uvádím kód, který provádí výše popsanou sekvenci celé.
Zapisujeme do konfigurace seznam verzí z adresáře na vzdáleném serveru
#!/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.xmlPokud se vám více líbí varianta s získáváním verzí z Jenkinsu a jste také tak leniví jako já, pak pod spoilerem je stejný kód, ale seznam z Jenkinsu:
Zapisujeme do konfigurace seznam verzí z Jenkinsu
Ale vezměte v úvahu, že moje jméno sestavení se skládá z pořadového čísla a čísla verze, oddělených dvojtečkou. Odpovídajícím způsobem, awk ořezává nepotřebnou část. Tuto řádku si přizpůsobte podle svých potřeb.
#!/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.xmlIdeálně, pokud jste otestovali kód, napsaný na základě výše uvedených příkladů, měl by ve vaší nasazovací úloze již být k dispozici rozbalovací seznam s verzemi. Přibližně jak na snímku obrazovky pod spoilerem.
Správně vyplněný seznam verzí

Pokud vše fungovalo, tak zkopírujte skript do Spustit shell příkaz a uložte změny.
Připojení k Cloud shell
Sběrači máme v kontejnerech. Jako nástroj pro dodávku aplikací a správce konfigurací používáme Ansible. Když tedy přijde na stavbu kontejnerů, nápady přicházejí tři: nainstalovat Docker v Dockeru, nainstalovat Docker na stroj s Ansible, nebo sestavit kontejnery v cloudové konzoli. O pluginech pro Jenkins jsme se dohodli v této poznámce mlčet. Pamatujete?
Rozhodl jsem se: pokud lze kontejnery «out of the box» stavět v cloudové konzoli, proč komplikovat situaci? Keep it clean, že? Chci stavět kontejnery Jenkins v cloudové konzoli a pak je odtud posílat do Kubernetes. Tím spíše, že uvnitř infrastruktury Googlu jsou velmi silné kanály, což pozitivně ovlivní rychlost nasazení.
K připojení k cloudové konzoli jsou potřeba dvě věci: gcloud a přístupová práva k Google Cloud API pro ten výchozí VM, ze kterého bude toto připojení realizováno.
Pro ty, kteří plánují připojit se vůbec ne z Googlovského cloudu
Google umožňuje možnost vypnout interaktivní autorizaci ve svých službách. To umožní připojit se k konzoli i z kávovaru, pokud je na *nixech a má také konzoli.
Pokud máte potřebu, abych tento problém osvětlil podrobněji v rámci této poznámky - pište do komentářů. Když se nasbírá dostatečný počet hlasů - napíšu aktualizaci na toto téma.
Nejjednodušší způsob, jak udělit práva - přes webové rozhraní.
- Zastavte VM, ze kterého se později bude připojovat k cloudové konzoli.
- Otevřete informace o instanci a klikněte na Změnit.
- Na samém spodku stránky vyberte oblast platnosti přístupu instance Úplný přístup ke všem Cloud API.
Snímek obrazovky

- Uložte změny a spusťte instanci.
Po skončení spuštění VM se připojte k ní přes SSH a ujistěte se, že připojení probíhá bez chyby. Použijte příkaz:
gcloud alpha cloud-shell ssh Úspěšné připojení vypadá zhruba takto

Nasazení v GKE
Jelikož se snažíme plně přejít na IaC (Infrastucture as a Code), naše Dockerfile jsou uloženy v Gitu. To je z jedné strany. A nasazení v Kubernetes je popsáno YAML souborem, který se používá pouze pro tuto úlohu, která sama o sobě je také jakoby kód. To je z druhé strany. Zkrátka, mám na mysli, že plán je následující:
- Bereme hodnoty proměnných BUILD_VERSION a volitelně hodnoty proměnných, které budou předány prostřednictvím ENV.
- Stahujeme Dockerfile z Gitu.
- Generujeme YAML pro nasazení.
- Oba tyto soubory nahráváme pomocí SCP do cloudové konzole.
- Tam buildíme kontejner a posíláme ho do Container registry
- Používáme soubor nasazení zatížení v Kubernetes.
Buďme konkrétnější. Když už jsme o tom mluvili, ENVpředpokládejme, že budeme potřebovat předat hodnoty dvou parametrů: PARAM1 a PARAM2. Přidáváme jejich zadání na nasazení, typ — String Parameter.
Snímek obrazovky

YAML generujeme jednoduchým přesměrováním echo do souboru. Předpokládá se, že v Dockerfile máte přítomny PARAM1 a PARAM2, že jméno zátěže bude awesomeapp, a zkompilovaný kontejner s aplikací dané verze leží v Container registry na cestě gcr.io/awesomeapp/awesomeapp-$BUILD_VERSION, kde $BUILD_VERSION bylo vybráno z rozbalovacího seznamu.
Seznam příkazů
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.yamlAgenta Jenkinsu po připojení, gcloud alpha cloud-shell ssh interaktivní režim není k dispozici, proto předáváme příkazy do cloudové konzole pomocí parametru --command.
Čistíme domácí složku v cloudové konzoli od starého Dockerfile:
gcloud alpha cloud-shell ssh --command="rm -f Dockerfile"Ukládáme nově stažený Dockerfile do domácí složky cloudové konzole pomocí SCP:
gcloud alpha cloud-shell scp localhost:./Dockerfile cloudshell:~Buildíme, tagujeme a posíláme kontejner do 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"
Postupujeme obdobným způsobem s nasazovacím souborem. Všimněte si, že v níže uvedených příkazech jsou použita fiktivní jména clusteru, kam dochází k nasazení (awsm-cluster) a jméno projektu (awesome-project), kde se nachází cluster.
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"Spouštíme úlohu, otevíráme výstup konzole a doufáme, že uvidíme úspěšné sestavení kontejneru.
Snímek obrazovky

A pak také úspěšné nasazení sestaveného kontejneru
Snímek obrazovky

Úmyslně jsem se vyhnul konfiguraci Ingress. Z jednoho prostého důvodu: jakmile jej nastavíte na workload s daným názvem, zůstane funkční, kolik nasazení s tímto názvem byste neprovedli. A vůbec, to je trochu mimo téma.
Nam místo výstupů
Všechny výše uvedené kroky by pravděpodobně nebylo nutné dělat, stačilo by nainstalovat nějaký plugin pro Jenkins, jejich je milion. Ale já z nějakého důvodu nemám rád pluginy. No, přesněji, sahám po nich jen v bezvýchodné situaci.
A také se mi prostě líbí prozkoumávat nějaké nové téma. Text výše je také způsob, jakým se chci podělit o zjištění, která jsem udělal při řešení úkolu popsaného na začátku. Podělit se s těmi, kteří nejsou zas až tak zkušení v devops. Pokud mé zjištění pomůže alespoň někomu — budu spokojen.
Zdroj: habr.com

