Totul a început atunci când liderul uneia dintre echipele noastre de dezvoltare a cerut să expună în mod temporar aplicația lor nouă, care fusese recent containerizată. Am făcut asta. Aproximativ după 20 de minute am primit o solicitare de actualizare a aplicației, pentru că s-a adăugat o funcționalitate foarte necesară. Am actualizat. După câteva ore… ei bine, vă imaginați ce s-a întâmplat mai departe…
Recunosc că sunt destul de leneș (m-am mai mărturisit asta? nu?), și având în vedere că liderii de echipă au acces la Jenkins, unde avem tot CI/CD, m-am gândit: de ce să nu se ocupe singur de deploy, cât îi place! Mi-a venit în minte o glumă: dă-i unei persoane pește și va fi sătul o zi; numește-l Sătul și va fi sătul toată viața. Și am plecat să configurez un job, care să poată desfășura un container Kubernetes cu aplicația orice versiune de construit cu succes și să transmită valori în el ENV (bunicul meu, filolog, fost profesor de engleză, ar întoarce acum degetul la tâmplă și s-ar uita foarte expresiv la mine, citind această propoziție).
Așadar, în acest articol voi povesti despre cum am învățat:
- Să actualizez dinamic sarcinile în Jenkins din sarcina în sine sau din alte sarcini;
- Să mă conectez la consola cloud (Cloud shell) dintr-un nod cu agentul Jenkins instalat;
- Să desfășurăm o sarcină (workload) în Google Kubernetes Engine.
De fapt, desigur, sunt puțin cinic. Se presupune că o parte din infrastructură este în cloud-ul Google, deci sunteți utilizator și, desigur, aveți un cont GCP. Dar articolul nu este despre asta.
Aceasta este o altă notă de tip referință. Îmi place să scriu astfel de note doar într-un singur caz: am avut o sarcină, la început nu știam cum să o rezolv, soluția nu am găsit-o pe Google în formă completă, așa că am căutat-o în părți și, în final, am rezolvat sarcina. Și pentru a nu fi nevoit să o caut din nou în viitor, scriu astfel de referințe.
Avertisment: 1. Nota a fost scrisă „pentru mine”, nu aspiră la rolul best practice Cu plăcere aș citi alternativele de „ar fi fost mai bine să faci așa” în comentarii.
2. Dacă partea aplicativă a notei este considerată sarea, atunci, ca toate notele mele anterioare, aceasta este o soluție slab sărată.
Actualizarea dinamică a setărilor sarcinilor în Jenkins
Anticipez întrebarea ta: ce legătură are actualizarea dinamică a jobului? Am introdus manual o valoare a parametrului de tip șir și gata!
Răspund: într-adevăr sunt leneș, nu îmi place când se plâng: Misha, implementarea se blochează, totul e pierdut! Încep să cercetez, iar acolo e o greșeală de tipar în valoarea vreunui parametru de lansare a jobului. De aceea prefer să fac totul cât mai infailibil. Dacă există posibilitatea de a lipsi utilizatorul de a introduce date direct, oferindu-i în schimb o listă de valori din care să aleagă, atunci organizez selecția.
Planul este acesta: creăm un job în Jenkins în care, înainte de lansare, putem alege o versiune dintr-o listă, specificând valorile pentru parametrii transmiși în container prin ENV, ulterior acesta construiește containerul și îl trimite în Container Registry. Apoi, de acolo, containerul este lansat în Kubernetes ca workload cu parametrii specificați în job.
Nu vom discuta despre procesul de creare și configurare a jobului în Jenkins, acesta este offtopic. Vom presupune că jobul este pregătit. Pentru a implementa o listă actualizabilă cu versiunile, avem nevoie de două lucruri: o listă sursă existentă cu numere de versiune validabile a priori și o variabilă de tip Choice parameter în job. În exemplul nostru, să numim variabila BUILD_VERSION, nu ne vom opri pe aceasta în detaliu. Dar hai să ne oprim mai în detaliu asupra listei sursă.
Nu sunt atâtea opțiuni. Îmi vin imediat în minte două:
- Folosirea API-ului de acces la distanță, pe care Jenkins îl oferă utilizatorilor săi;
- Solicitarea conținutului unei foldere la distanță dintr-un depozit (în cazul nostru JFrog Artifactory, ceea ce nu este esențial).
API-ul de acces la distanță Jenkins
În tradiția noastră excelentă, prefer să evit explicațiile elaborate.
Îmi permit să fac doar o traducere liberă a unei părți din primul paragraf :
Jenkins oferă un API pentru accesul la distanță, într-un mod care poate fi înțeles de mașini către funcționalitățile sale. Accesul la distanță este oferit într-un stil similar REST. Asta înseamnă că nu există un punct unic de acces către toate funcționalitățile, ci în schimb se folosește un URL de forma "…/api/", unde "…" reprezintă obiectul la care se aplică funcționalitățile API.
Cu alte cuvinte, dacă jobul de implementare despre care vorbim în acest moment este disponibil la adresa http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build, API-urile pentru această sarcină sunt disponibile la adresa http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/
Apoi avem opțiunea de a primi output-ul într-un anumit format. Ne vom concentra pe XML, deoarece API-ul permite filtrarea doar în acest caz.
Să încercăm să obținem o listă cu toate execuțiile sarcinii. Ne interesează doar numele build-ului (displayName) și rezultatul acestuia (rezultat):
http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]A reușit?
Acum vom filtra doar execuțiile care au rezultatul final SUCCESS. Folosim argumentul &exclude și ca parametru îi vom transmite calea către un valoare diferită de SUCCESS. Da-da. Două negații = o afirmație. Excludem tot ce nu ne interesează:
http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]&exclude=freeStyleProject/allBuild[result!='SUCCESS'] Captură de ecran a listei de succes

Și pentru distracție, să ne asigurăm că filtrul nu ne-a păcălit (filtrele nu mint niciodată!) și să afișăm lista celor „ne-succes”:
http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]&exclude=freeStyleProject/allBuild[result='SUCCESS'] Captură de ecran a listei ne-succes

Lista versiunilor din folderul de pe serverul remote
Există și o a doua opțiune de a obține lista versiunilor. Mi-a plăcut chiar mai mult decât să chem API-ul Jenkins. Deoarece dacă aplicația s-a compilat cu succes, asta înseamnă că a fost ambalată și a fost plasată în repo-ul corespunzător. Gen, repo-ul este implicit un depozit al versiunilor de lucru ale aplicațiilor. În fine. Așa că să întrebăm ce versiuni sunt la păstrare. Vom face curl, grep și awk pe folderul remote. Dacă pe cineva îl interesează un one-liner, acesta se află sub spoiler.
Comanda pe o singură linie
Rețineți două lucruri: transmit acreditivele pentru conectare în antet și nu am nevoie de toate versiunile din folder, ci doar de cele create în ultimul lunar. Editați comanda în funcție de realitățile și nevoile dumneavoastră:
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[^/]+Configurarea sarcinilor și fișierul de configurație a sarcinii în Jenkins
Ne-am lămurit cu sursa listei de versiuni. Să integrăm acum lista obținută în sarcină. Pentru mine, soluția evidentă a fost să adaug un pas în sarcină pentru a construi aplicația. Un pas care să fie executat în cazul rezultatelor „succes”.
Deschidem setările sarcinii de construcție și derulăm în jos. Apăsăm butoanele: Adauga pas de construcție -> Pas condițional (unic). În setările pasului alegem condiția Statusul curent al construcției, stabilim valoarea SUCCESS, acțiunea care se execută în cazul succesului Execută comanda shell.
Și acum vine partea cea mai interesantă. Configurațiile sarcinilor Jenkins sunt stocate în fișiere. În format XML. Pe calea http://calea-catre-sarcina/config.xml Prin urmare, se poate descărca fișierul cu configurația, îl putem edita așa cum avem nevoie și apoi îl putem pune înapoi de unde l-am luat.
Amintiți-vă, mai sus am convenit că pentru lista versiunilor vom crea un parametru BUILD_VERSION?
Să descărcăm fișierul de configurație și să ne uităm în interiorul lui. Pur și simplu pentru a ne asigura că parametrul este la locul lui și că arată exact cum trebuie.
Captura de ecran se află sub spoiler.
Fragmentul config.xml pe care îl aveți ar trebui să arate la fel. Cu excepția faptului că conținutul elementului choices între timp lipsește

Ne-am asigurat? Ei bine, scriem scriptul care va fi executat în cazul unei compilări reușite.
Scriptul va obține lista versiunilor, va descărca fișierul de configurație, va scrie în el în locul dorit lista versiunilor și apoi îl va pune înapoi. Da. Totul este corect. Scriem lista versiunilor în XML la locul unde există deja lista versiunilor (va fi în viitor, după prima rulare a scriptului). Știu, în lume mai există fani fervenți ai expresiilor regulate. Nu fac parte din aceștia. Vă rog să instalați pe mașina unde se va edita config-ul. Mi se pare că nu este o plată prea mare pentru a evita editarea XML cu sed.
Sub spoiler vă ofer codul care execută întreaga secvență descrisă mai sus.
Scriem în config lista versiunilor din folderul de pe serverul extern
#!/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.xmlDacă v-a plăcut mai mult varianta cu obținerea versiunilor din Jenkins și sunteți la fel de leneși ca mine, sub spoiler este același cod, dar lista este din Jenkins:
Scriem în config lista versiunilor din Jenkins
Numai să țineți cont de un aspect: numele compilării mele constă dintr-un număr secvențial și un număr de versiune, separate prin două puncte. Prin urmare, awk va tăia partea inutilă. Modificați această linie după nevoile dumneavoastră.
#!/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.xmlTeoretic, dacă ați testat codul scris pe baza exemplelor de mai sus, atunci în sarcina de desfășurare ar trebui să aveți deja un meniu derulant cu versiuni. Iată cam cum arată în captura de ecran de sub spoiler.
Lista versiunilor completată corect

Dacă totul a funcționat, copiați scriptul în Execută comanda shell și salvați modificările.
Conectare la Cloud shell
Constructorii sunt în containerele noastre. Ca mijloc de livrare a aplicațiilor și manager de configurație, folosim Ansible. Prin urmare, când vine vorba de construirea containerelor, trei opțiuni vin în minte: să instalăm Docker în Docker, să instalăm Docker pe mașina cu Ansible sau să construim containerele în consola cloud. Am convenit să nu discutăm despre pluginurile pentru Jenkins în această notă. Țineți minte?
Am decis: bine, dacă containerele pot fi construite din cutie în consola cloud, de ce să complicăm lucrurile? Keep it clean, nu-i așa? Vreau să construiesc containere Jenkins în consola cloud și apoi să le trimit direct în Kubernetes. Mai mult, canalele interne din infrastructura Google sunt foarte rapide, ceea ce va îmbunătăți viteza de deploy.
Pentru a vă conecta la consola cloud, sunt necesare două lucruri: gcloud și permisiuni pentru Google Cloud API pentru instanța VM de la care se va realiza conexiunea.
Pentru cei care intenționează să se conecteze de fapt din afara cloud-ului Google
Google permite dezactivarea autentificării interactive în serviciile sale. Aceasta va permite conectarea la consolă chiar și de la o maşină de cafea, atâta timp cât aceasta rulează *nix și are propria console.
Dacă aveți nevoie să detaliez această întrebare mai amplu în cadrul acestei note, lăsați un comentariu. Dacă se adună un număr suficient de voturi, voi scrie un update pe această temă.
Cel mai simplu mod de a oferi permisiuni este prin intermediul interfeței web.
- Opriți instanța VM de la care va fi efectuată ulterior conexiunea la consola cloud.
- Deschideți Detaliile instanței și faceți clic pe Schimbați.
- În partea de jos a paginii, selectați domeniul de acces al instanței Acces complet la toate API-urile Cloud.
Captură de ecran

- Salvați modificările și porniți instanța.
După ce VM a terminat încărcarea, conectați-vă prin SSH și asigurați-vă că conexiunea se realizează fără eroare. Folosiți comanda:
gcloud alpha cloud-shell ssh O conexiune reușită arată cam așa

Deploy în GKE
Deoarece ne străduim să migram complet la IaC (Infrastructure as Code), fișierele Docker sunt păstrate în git. Asta e o parte. Iar deploy-ul în Kubernetes este descris printr-un fișier yaml, care este folosit doar de această sarcină, care, la rândul ei, este tot un fel de cod. În general, ideea este că:
- Luăm valorile variabilelor BUILD_VERSION și, opțional, valorile variabilelor care vor fi transmise prin ENV.
- Descărcăm din git fișierul Docker.
- Generăm yaml pentru desfășurare.
- Încărcăm amândouă aceste fișiere prin scp în consola cloud.
- Construim acolo containerul și îl împingem în Container registry
- Aplicăm fișierul de desfășurare a sarcinii în kubernetes.
Să fim mai specifici. Din moment ce vorbim despre ENV, să presupunem că va trebui să transmitem valorile a două parametrii: PARAM1 și PARAM2. Adăugăm setarea lor pentru desfășurare, tip — String Parameter.
Captură de ecran

Vom genera yaml printr-o simplă redirecționare echo în fișier. Se presupune, desigur, că în fișierul Docker aveți PARAM1 și PARAM2, că numele sarcinii va fi awesomeapp, iar containerul construit cu versiunea specificată a aplicației se află în Container registry pe calea gcr.io/awesomeapp/awesomeapp-$BUILD_VERSION, unde $BUILD_VERSION exact și a fost ales din lista derulantă.
Listarea comenzilor
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.yamlAgentului Jenkins, după conectare folosind gcloud alpha cloud-shell ssh modul interactiv nu este disponibil, așa că trimitem comenzile în consola cloud folosind parametrul —command.
Curățăm folderul home din consola cloud de fișierul Docker vechi:
gcloud alpha cloud-shell ssh --command="rm -f Dockerfile"Puntăm fișierul Docker proaspăt descărcat în folderul home al consolei cloud folosind scp:
gcloud alpha cloud-shell scp localhost:./Dockerfile cloudshell:~Construim, etichetăm și împingem containerul în 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"
Procedăm în mod similar cu fișierul de desfășurare. Rețineți că în comenzile de mai jos sunt folosite nume fictive ale clusterului, unde se desfășoară desfășurarea (awsm-cluster) și numele proiectului (awesome-project), unde se află clusterul.
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"Pornim sarcina, deschidem ieșirea consolei și sperăm să vedem o construcție reușită a containerului.
Captură de ecran

Iar apoi, o desfășurare reușită a containerului construit.
Captură de ecran

Am evitat intenționat să discut despre setarea Ingress. Dintr-un motiv simplu: odată ce l-ai configurat cu un anumit nume, acesta va rămâne funcțional, nu contează câte desfășurări cu acest nume vor avea loc. Și, de fapt, este puțin în afara subiectului. workload Toți pașii de mai sus probabil că puteau fi evitați, iar un plugin pentru Jenkins ar fi putut fi instalat, sunt o mulțime. Dar eu, dintr-un motiv oarecare, nu iubesc pluginurile. Mai degrabă, recurg la ele doar din disperare.
În loc de concluzii
Și, de asemenea, îmi place să explorez un subiect nou pentru mine. Textul de mai sus este, de asemenea, o modalitate de a împărtăși descoperirile pe care le-am făcut în rezolvarea sarcinii descrise la început. Împărtășind cu cei care, la fel ca mine, nu sunt deloc lupi fioroși în devops. Dacă descoperirile mele ajută măcar pe cineva, voi fi mulțumit.
Facem o sarcină de desfășurare în GKE fără pluginuri, SMS-uri și înregistrare. Ne aruncăm o privire sub haină la Jenkins.
Sursa: habr.com

