Ustvarimo nalogo uvajanja v GKE brez vtičnikov, SMS-ov ali registracije. Pokukajmo pod Jenkinsovo jakno

Vse se je začelo, ko nas je vodja ene od naših razvojnih ekip prosil, da testiramo njihovo novo aplikacijo, ki je bila dan prej zaprta v kontejnerje. sem ga objavil. Po približno 20 minutah je prispela zahteva za posodobitev aplikacije, ker je bila tam dodana zelo potrebna stvar. obnovil sem. Po nadaljnjih nekaj urah... no, lahko ugibate, kaj se je potem začelo dogajati...

Moram priznati, da sem precej len (ali tega nisem prej priznal? Ne?) in glede na dejstvo, da imajo vodje ekip dostop do Jenkinsa, v katerem imamo vse CI/CD, sem si mislil: naj se namesti kot kolikor hoče! Spomnil sem se šale: daj človeku ribo, pa bo jedel en dan; pokličite osebo Fed in Fed bo vse življenje. In šel igrajte trike v službi, ki bi bil sposoben namestiti vsebnik, ki vsebuje aplikacijo katere koli uspešno vgrajene različice v Kuber in vanj prenesti vse vrednosti ENV (moj dedek, filolog, v preteklosti profesor angleščine, bi zdaj po tem stavku zavrtel s prstom na sencu in me zelo ekspresivno pogledal).

Torej, v tej opombi vam bom povedal, kako sem se naučil:

  1. Dinamično posodabljajte opravila v Jenkinsu iz samega opravila ali iz drugih opravil;
  2. Povežite se s konzolo v oblaku (Cloud shell) iz vozlišča z nameščenim agentom Jenkins;
  3. Razmestite delovno obremenitev v Google Kubernetes Engine.


Pravzaprav sem seveda nekoliko neiskren. Predpostavlja se, da imate vsaj del infrastrukture v Googlovem oblaku, torej ste njegov uporabnik in seveda imate GCP račun. Vendar ta opomba ne govori o tem.

To je moja naslednja goljufija. Takšne opombe želim napisati samo v enem primeru: soočil sem se s problemom, sprva nisem vedel, kako ga rešiti, rešitev ni bila googlana pripravljena, zato sem googlal po delih in na koncu rešil problem. In da mi v prihodnje, ko pozabim, kako mi je uspelo, ne bo treba vsega spet googlati po delih in skupaj sestavljati, si pišem takšne goljufije.

Disclaimer: 1. Opomba je bila napisana "zase", za vlogo najboljša praksa ne velja. Z veseljem preberem možnosti »bolje bi bilo, če bi to naredili na ta način« v komentarjih.
2. Če se naneseni del note šteje za sol, potem je, kot vse moje prejšnje note, tudi ta šibka raztopina soli.

Dinamično posodabljanje nastavitev opravil v Jenkinsu

Predvidevam vaše vprašanje: kaj ima s tem dinamično posodabljanje delovnih mest? Vnesite vrednost parametra niza ročno in že gre!

Odgovorim: res sem len, ne maram, ko se pritožujejo: Miša, uvajanje se sesuje, vse je izginilo! Začnete iskati in v vrednosti nekega parametra zagona opravila je tipkarska napaka. Zato raje vse naredim čim bolj učinkovito. Če je mogoče uporabniku preprečiti neposreden vnos podatkov tako, da namesto tega ponudi seznam vrednosti, med katerimi lahko izbira, potem organiziram izbor.

Načrt je naslednji: ustvarimo opravilo v Jenkinsu, v katerem lahko pred zagonom izberemo različico s seznama, določimo vrednosti za parametre, posredovane vsebniku prek ENV, nato zbere vsebnik in ga potisne v register vsebnikov. Nato se od tam posoda spusti v kubero kot obremenitev s parametri, navedenimi v delovnem mestu.

Ne bomo obravnavali postopka ustvarjanja in vzpostavitve delovnega mesta v Jenkinsu, to ni tema. Predvidevamo, da je naloga pripravljena. Za implementacijo posodobljenega seznama z različicami potrebujemo dve stvari: obstoječi izvorni seznam z a priori veljavnimi številkami različic in spremenljivko, kot je Izbirni parameter v nalogi. V našem primeru naj bo spremenljivka poimenovana BUILD_VERSION, se na njem ne bomo podrobneje zadrževali. A poglejmo si seznam virov podrobneje.

Ni toliko možnosti. Dve stvari sta mi takoj padli na misel:

  • Uporabite API za oddaljeni dostop, ki ga Jenkins ponuja svojim uporabnikom;
  • Zahtevajte vsebino mape oddaljenega repozitorija (v našem primeru je to JFrog Artifactory, kar ni pomembno).

API za oddaljeni dostop Jenkins

Po ustaljeni odlični tradiciji bi se raje izognil dolgim ​​razlagam.
Dovolil si bom le brezplačen prevod dela prvega odstavka prva stran dokumentacije API-ja:

Jenkins ponuja API za oddaljen strojno berljiv dostop do njegove funkcionalnosti. <…> Oddaljeni dostop je na voljo v slogu, podobnem REST. To pomeni, da ni enotne vstopne točke za vse funkcije, temveč URL, kot je ".../api/", Kje "...« pomeni objekt, za katerega so uporabljene zmožnosti API-ja.

Z drugimi besedami, če je naloga uvajanja, o kateri trenutno govorimo, na voljo na http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build, potem so piščalke API za to nalogo na voljo na http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/

Nato imamo izbiro, v kakšni obliki bomo prejeli izhod. Osredotočimo se na XML, saj API omogoča samo filtriranje v tem primeru.

Poskusimo dobiti seznam vseh potekov opravil. Zanima nas samo ime sklopa (prikazno ime) in njegov rezultat (povzroči):

http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]

Ali je uspelo?

Zdaj pa filtrirajmo samo tiste pogone, ki se končajo z rezultatom USPEH. Uporabimo argument &izključi in kot parameter mu bomo posredovali pot do vrednosti, ki ni enaka USPEH. Da Da. Dvojna negacija je izjava. Izloč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 zaslona seznama uspešnih
Ustvarimo nalogo uvajanja v GKE brez vtičnikov, SMS-ov ali registracije. Pokukajmo pod Jenkinsovo jakno

No, za šalo se prepričajmo, da nas filter ni zavedel (filtri nikoli ne lažejo!) in prikažemo seznam "neuspešnih":

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

Posnetek zaslona seznama neuspešnih
Ustvarimo nalogo uvajanja v GKE brez vtičnikov, SMS-ov ali registracije. Pokukajmo pod Jenkinsovo jakno

Seznam različic iz mape na oddaljenem strežniku

Obstaja še en način za pridobitev seznama različic. Všeč mi je celo bolj kot dostop do Jenkins API-ja. No, ker če je bila aplikacija uspešno zgrajena, to pomeni, da je bila zapakirana in postavljena v repozitorij v ustrezno mapo. Na primer, repozitorij je privzeta shramba delujočih različic aplikacij. Všeč mi je. No, vprašajmo ga, katere različice so v skladišču. Oddaljeno mapo bomo curl, grep in awk. Če koga zanima oneliner, potem je pod spojlerjem.

Enovrstični ukaz
Upoštevajte dve stvari: podrobnosti povezave posredujem v glavi in ​​ne potrebujem vseh različic iz mape ter izberem samo tiste, ki so bile ustvarjene v enem mesecu. Uredite ukaz, da bo ustrezal vašim razmeram in potrebam:

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 opravil in konfiguracijske datoteke opravil v Jenkinsu

Ugotovili smo vir seznama različic. Sedaj vključimo dobljeni seznam v nalogo. Zame je bila očitna rešitev dodati korak v nalogi gradnje aplikacije. Korak, ki bi bil izveden, če bi bil rezultat "uspeh".

Odprite nastavitve opravila sestavljanja in se pomaknite do samega dna. Kliknite na gumbe: Dodaj gradbeni korak -> Pogojni korak (enojni). V nastavitvah koraka izberite pogoj Trenutno stanje gradnje, nastavite vrednost USPEH, dejanje, ki se izvede, če je uspešno Zaženi ukaz lupine.

In zdaj zabavni del. Jenkins shrani konfiguracije opravil v datoteke. V formatu XML. Spotoma http://путь-до-задания/config.xml V skladu s tem lahko prenesete konfiguracijsko datoteko, jo po potrebi uredite in postavite nazaj, kjer ste jo dobili.

Ne pozabite, zgoraj smo se dogovorili, da bomo ustvarili parameter za seznam različic BUILD_VERSION?

Prenesimo konfiguracijsko datoteko in si oglejmo njeno notranjost. Samo zato, da se prepričam, da je parameter na mestu in želene vrste.

Posnetek zaslona pod spojlerjem.

Vaš fragment config.xml bi moral izgledati enako. Le da vsebina elementa izbir še manjka
Ustvarimo nalogo uvajanja v GKE brez vtičnikov, SMS-ov ali registracije. Pokukajmo pod Jenkinsovo jakno

Ali si prepričan? To je to, napišimo skript, ki se bo izvedel, če bo gradnja uspešna.
Skript bo prejel seznam različic, prenesel konfiguracijsko datoteko, vanjo zapisal seznam različic na mesto, ki ga potrebujemo, in ga nato postavil nazaj. ja Tako je. Seznam različic v XML napišite na mesto, kjer že obstaja seznam različic (bo v prihodnosti, po prvem zagonu skripte). Vem, da so na svetu še vedno hudi oboževalci regularnih izrazov. Jaz ne spadam mednje. Prosim namestite xmlstarler na stroj, kjer bo urejena konfiguracija. Zdi se mi, da to ni tako visoka cena, da bi se izognili urejanju XML z uporabo sed.

Pod spojlerjem predstavljam kodo, ki v celoti izvaja zgornje zaporedje.

Zapišite seznam različic iz mape na oddaljenem strežniku v 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.xml

Če imate raje možnost pridobivanja različic od Jenkinsa in ste tako leni kot jaz, potem je pod spojlerjem ista koda, vendar seznam od Jenkinsa:

Napišite seznam različic iz Jenkinsa v konfiguracijo
Upoštevajte le to: ime mojega sklopa je sestavljeno iz zaporedne številke in številke različice, ločenih z dvopičjem. V skladu s tem awk odreže nepotreben del. Zase spremenite to vrstico, da bo ustrezala vašim potrebam.

#!/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

Teoretično, če ste preizkusili kodo, napisano na podlagi zgornjih primerov, bi morali v nalogi uvajanja že imeti spustni seznam z različicami. To je kot na sliki pod spojlerjem.

Pravilno izpolnjen seznam različic
Ustvarimo nalogo uvajanja v GKE brez vtičnikov, SMS-ov ali registracije. Pokukajmo pod Jenkinsovo jakno

Če je vse delovalo, kopirajte in prilepite skript v Zaženi ukaz lupine in shranite spremembe.

Povezovanje z lupino v oblaku

Imamo zbiralnike v kontejnerjih. Ansible uporabljamo kot orodje za dostavo aplikacij in upravitelja konfiguracije. V skladu s tem, ko gre za gradnjo vsebnikov, pridejo na misel tri možnosti: namestitev Dockerja v Docker, namestitev Dockerja na stroj, ki izvaja Ansible, ali izdelava vsebnikov v konzoli v oblaku. Dogovorili smo se, da bomo v tem članku molčali o vtičnikih za Jenkins. Se spomniš?

Odločil sem se: no, saj je vsebnike "izven škatle" mogoče zbirati v konzoli v oblaku, zakaj bi se potem trudil? Naj bo čisto, kajne? Želim zbrati vsebnike Jenkins v konzoli v oblaku in jih nato od tam zagnati v kuber. Poleg tega ima Google zelo bogate kanale znotraj svoje infrastrukture, kar bo ugodno vplivalo na hitrost uvajanja.

Za povezavo s konzolo v oblaku potrebujete dve stvari: gcloud in pravice dostopa do Google Cloud API za primerek VM, s katerim bo vzpostavljena ista povezava.

Za tiste, ki se sploh ne nameravate povezati iz Googlovega oblaka
Google dopušča možnost onemogočanja interaktivne avtorizacije v svojih storitvah. To vam bo omogočilo povezavo s konzolo tudi iz aparata za kavo, če poganja *nix in ima samo konzolo.

Če je potrebno, da to vprašanje podrobneje obravnavam v okviru tega zapisa, napišite v komentarje. Če dobimo dovolj glasov, bom napisal posodobitev na to temo.

Pravice najlažje podelite prek spletnega vmesnika.

  1. Ustavite primerek VM, iz katerega se boste pozneje povezali s konzolo v oblaku.
  2. Odprite podrobnosti primerka in kliknite spremeniti.
  3. Na samem dnu strani izberite obseg dostopa do primerka Popoln dostop do vseh API-jev v oblaku.

    Posnetek zaslona
    Ustvarimo nalogo uvajanja v GKE brez vtičnikov, SMS-ov ali registracije. Pokukajmo pod Jenkinsovo jakno

  4. Shranite spremembe in zaženite primerek.

Ko se VM konča z nalaganjem, se povežite z njim prek SSH in se prepričajte, da se povezava vzpostavi brez napake. Uporabite ukaz:

gcloud alpha cloud-shell ssh

Uspešna povezava izgleda nekako takole
Ustvarimo nalogo uvajanja v GKE brez vtičnikov, SMS-ov ali registracije. Pokukajmo pod Jenkinsovo jakno

Razmesti v GKE

Ker si na vse možne načine prizadevamo za popoln prehod na IaC (Infrastucture as a Code), so naše docker datoteke shranjene v Gitu. To je po eni strani. In uvajanje v kubernetes je opisano z datoteko yaml, ki jo uporablja samo ta naloga, ki je tudi sama kot koda. To je z druge strani. Na splošno je načrt takšen:

  1. Vzamemo vrednosti spremenljivk BUILD_VERSION in po želji vrednosti spremenljivk, ki bodo posredovane ENV.
  2. Prenesite datoteko docker iz Gita.
  3. Ustvarite yaml za uvajanje.
  4. Obe datoteki prek scp naložimo v konzolo v oblaku.
  5. Tam zgradimo vsebnik in ga potisnemo v register vsebnikov
  6. Na kuber uporabimo datoteko za uvedbo obremenitve.

Bodimo bolj natančni. Ko smo se začeli pogovarjati o ENV, potem predpostavimo, da moramo posredovati vrednosti dveh parametrov: PARAM1 и PARAM2. Dodamo njihovo nalogo za uvajanje, vnesite - Parameter niza.

Posnetek zaslona
Ustvarimo nalogo uvajanja v GKE brez vtičnikov, SMS-ov ali registracije. Pokukajmo pod Jenkinsovo jakno

Yaml bomo ustvarili s preprosto preusmeritvijo echo vložiti. Seveda se predpostavlja, da imate v svoji datoteki docker PARAM1 и PARAM2da bo ime tovora awesomeapp, sestavljena posoda z aplikacijo navedene različice pa leži v Register zabojnikov na poti gcr.io/awesomeapp/awesomeapp-$BUILD_VERSIONČe $BUILD_VERSION je bil pravkar izbran s spustnega seznama.

Seznam ekip

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 agent po povezovanju z uporabo gcloud alfa lupina oblaka ssh interaktivni način ni na voljo, zato pošljemo ukaze v konzolo v oblaku s parametrom --ukaz.

Domačo mapo v konzoli v oblaku očistimo iz stare dockerfile:

gcloud alpha cloud-shell ssh --command="rm -f Dockerfile"

Postavite sveže preneseno datoteko docker v domačo mapo konzole v oblaku s pomočjo scp:

gcloud alpha cloud-shell scp localhost:./Dockerfile cloudshell:~

Posodo zberemo, označimo in potisnemo v register zabojnikov:

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"

Enako naredimo z razmestitveno datoteko. Upoštevajte, da spodnji ukazi uporabljajo izmišljena imena gruče, kjer pride do uvedbe (awsm-gruča) in ime projekta (super-projekt), kjer se nahaja grozd.

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"

Zaženemo nalogo, odpremo izhod konzole in upamo, da bomo videli uspešno sestavljanje vsebnika.

Posnetek zaslona
Ustvarimo nalogo uvajanja v GKE brez vtičnikov, SMS-ov ali registracije. Pokukajmo pod Jenkinsovo jakno

In nato uspešna namestitev sestavljenega kontejnerja

Posnetek zaslona
Ustvarimo nalogo uvajanja v GKE brez vtičnikov, SMS-ov ali registracije. Pokukajmo pod Jenkinsovo jakno

Nastavitev sem namerno prezrl Vstop. Iz enega preprostega razloga: ko ga nastavite obremenitev z danim imenom bo ostal delujoč, ne glede na to, koliko razmestitev s tem imenom izvedete. No, na splošno je to malo zunaj okvira zgodovine.

Namesto zaključkov

Vseh zgornjih korakov verjetno ne bi bilo mogoče narediti, ampak preprosto namestiti vtičnik za Jenkins, njihov muuulion. Toda iz neznanega razloga ne maram vtičnikov. No, natančneje, k njim se zatečem le iz obupa.

In prav rad naberem kakšno novo temo zame. Zgornje besedilo je tudi način, da delim ugotovitve, do katerih sem prišel med reševanjem problema, opisanega na samem začetku. Delite s tistimi, ki tako kot on sploh niso hud volk v devopsu. Če bodo moje ugotovitve pomagale vsaj komu, bom vesel.

Vir: www.habr.com

Dodaj komentar