Kreiramo zadatak implementacije u GKE bez dodataka, SMS-a ili registracije. Hajde da zavirimo ispod Dženkinsove jakne

Sve je počelo kada nas je vođa tima jednog od naših razvojnih timova zamolila da testiramo njihovu novu aplikaciju, koja je dan ranije bila u kontejnerima. Ja sam to objavio. Nakon 20-ak minuta stigao je zahtjev za ažuriranje aplikacije, jer je tu dodata vrlo neophodna stvar. obnovio sam. Nakon još par sati... pa, možete pretpostaviti šta je dalje počelo...

Moram priznati da sam dosta lijen (zar to nisam ranije priznao? Ne?), a s obzirom na to da čelnici tima imaju pristup Jenkinsu, u kojem imamo sve CI/CD, pomislio sam: neka se rasporedi kao koliko god želi! Sjetio sam se vica: daj čovjeku ribu i on će jesti jedan dan; nazovite osobu Fed i ona će biti hranjena cijeli život. I otišao izigravati trikove na poslu, koji bi mogao da postavi kontejner koji sadrži aplikaciju bilo koje uspešno ugrađene verzije u Kuber i prenese sve vrednosti u njega ENV (moj deda, filolog, nekada profesor engleskog jezika, sada bi okretao prstom na slepoočnici i pogledao me veoma izražajno nakon što je pročitao ovu rečenicu).

Dakle, u ovoj napomeni ću vam reći kako sam naučio:

  1. Dinamički ažurirajte poslove u Jenkinsu iz samog posla ili iz drugih poslova;
  2. Povežite se na Cloud konzolu (Cloud shell) iz čvora s instaliranim Jenkins agentom;
  3. Postavite radno opterećenje na Google Kubernetes Engine.


U stvari, ja sam, naravno, pomalo neiskren. Pretpostavlja se da imate barem dio infrastrukture u Google oblaku, pa ste stoga njegov korisnik i, naravno, imate GCP nalog. Ali ova beleška nije o tome.

Ovo je moja sljedeća varalica. Samo u jednom slučaju želim da napišem takve napomene: suočio sam se sa problemom, u početku nisam znao kako da ga rešim, rešenje nije guglano gotovo, pa sam ga proguglao u delovima i na kraju rešio problem. I da ubuduće, kada zaboravim kako sam to uradio, ne moram ponovo guglati sve dio po dio i sastavljati zajedno, pišem sebi takve varalice.

odricanje od odgovornosti: 1. Poruka je napisana “za sebe”, za ulogu najbolja praksa ne primjenjuje. Drago mi je da u komentarima pročitam opcije „bilo bi bolje da to uradimo na ovaj način“.
2. Ako se primijenjeni dio note smatra solju, onda je, kao i sve moje prethodne note, i ova slaba otopina soli.

Dinamičko ažuriranje postavki posla u Jenkinsu

Predviđam vaše pitanje: kakve veze ima dinamičko ažuriranje posla s tim? Unesite vrijednost parametra niza ručno i možete!

Odgovaram: baš sam lijen, ne volim kad se žale: Miša, ruši raspored, sve je nestalo! Počnete da tražite, a postoji greška u kucanju u vrednosti nekog parametra pokretanja zadatka. Stoga više volim da sve radim što efikasnije. Ako je moguće spriječiti korisnika da direktno unese podatke dajući umjesto toga listu vrijednosti za izbor, tada organiziram odabir.

Plan je sljedeći: kreiramo posao u Jenkinsu, u kojem bismo, prije pokretanja, mogli odabrati verziju sa liste, specificirati vrijednosti za parametre proslijeđene kontejneru putem ENV, zatim prikuplja kontejner i gura ga u registar kontejnera. Zatim se odatle kontejner pokreće u cuber as opterećenje sa parametrima navedenim u poslu.

Nećemo razmatrati proces kreiranja i postavljanja posla u Jenkinsu, ovo nije tema. Pretpostavljamo da je zadatak spreman. Za implementaciju ažurirane liste s verzijama, potrebne su nam dvije stvari: postojeća lista izvora s a priori važećim brojevima verzija i varijabla kao što je Parametar izbora u zadatku. U našem primjeru neka varijabla bude imenovana BUILD_VERSION, nećemo se detaljnije zadržavati na tome. Ali hajde da pobliže pogledamo listu izvora.

Nema toliko opcija. Dvije stvari su mi odmah pale na pamet:

  • Koristite API za daljinski pristup koji Jenkins nudi svojim korisnicima;
  • Zatražite sadržaj foldera udaljenog spremišta (u našem slučaju ovo je JFrog Artifactory, što nije važno).

Jenkins API za daljinski pristup

Prema ustaljenoj odličnoj tradiciji, radije bih izbjegao duga objašnjenja.
Dozvoliću sebi samo slobodan prevod dela prvog pasusa prva stranica API dokumentacije:

Jenkins obezbeđuje API za daljinski mašinski čitljiv pristup njegovoj funkcionalnosti. <…> Daljinski pristup se nudi u stilu REST-a. To znači da ne postoji jedinstvena ulazna tačka za sve funkcije, već URL kao što je ".../api/", gdje"..." označava objekat na koji se primjenjuju mogućnosti API-ja.

Drugim riječima, ako je zadatak raspoređivanja o kojem trenutno govorimo dostupan na http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build, tada su API zvižduci za ovaj zadatak dostupni na http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/

Zatim imamo izbor u kojem obliku ćemo primiti izlaz. Fokusirajmo se na XML, pošto API samo u ovom slučaju dozvoljava filtriranje.

Hajde samo da pokušamo da dobijemo listu svih izvođenja poslova. Zanima nas samo naziv sklopa (displayName) i njegov rezultat (rezultat):

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

Ispostavilo se?

Sada filtrirajmo samo one runde koje završe s rezultatom USPEH. Hajde da iskoristimo argument &isključi i kao parametar ćemo mu proslijediti putanju do vrijednosti koja nije jednaka USPEH. Da da. Dvostruki negativ je izjava. Isključujemo sve što nas ne zanima:

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

Snimak ekrana liste uspješnih
Kreiramo zadatak implementacije u GKE bez dodataka, SMS-a ili registracije. Hajde da zavirimo ispod Dženkinsove jakne

Pa, iz zabave, uvjerimo se da nas filter nije prevario (filteri nikad ne lažu!) i prikažimo listu "neuspješnih":

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

Snimak ekrana liste neuspješnih
Kreiramo zadatak implementacije u GKE bez dodataka, SMS-a ili registracije. Hajde da zavirimo ispod Dženkinsove jakne

Lista verzija iz foldera na udaljenom serveru

Postoji drugi način da dobijete listu verzija. Sviđa mi se čak više od pristupa Jenkins API-ju. Pa, jer ako je aplikacija uspješno napravljena, to znači da je upakovana i stavljena u spremište u odgovarajuću mapu. Na primjer, spremište je zadana pohrana radnih verzija aplikacija. Sviđa mi se. Pa, hajde da ga pitamo koje su verzije u skladištu. Mi ćemo savijati, grep i awk udaljeni folder. Ako nekoga zanima oneliner, onda je ispod spojlera.

Komanda u jednoj liniji
Imajte na umu dvije stvari: ja prosljeđujem detalje veze u zaglavlju i ne trebaju mi ​​sve verzije iz foldera, a biram samo one koje su kreirane u roku od mjesec dana. Uredite naredbu tako da odgovara vašim realnostima i potrebama:

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[^/]+' )

Postavljanje poslova i konfiguracijske datoteke u Jenkinsu

Otkrili smo izvor liste verzija. Hajde da sada ugradimo rezultirajuću listu u zadatak. Za mene je očigledno rješenje bilo dodavanje koraka u zadatak izgradnje aplikacije. Korak koji bi se izvršio ako bi rezultat bio "uspješan".

Otvorite postavke zadatka montaže i pomaknite se do samog dna. Kliknite na dugmad: Dodaj korak izgradnje -> Uslovni korak (jedan). U postavkama koraka odaberite uvjet Trenutni status izrade, postavite vrijednost USPEH, radnju koja će se izvršiti ako je uspješna Pokreni shell komandu.

A sada zabavni dio. Jenkins pohranjuje konfiguracije poslova u datoteke. U XML formatu. Usput http://путь-до-задания/config.xml U skladu s tim, možete preuzeti konfiguracijsku datoteku, urediti je po potrebi i vratiti tamo gdje ste je dobili.

Zapamtite, gore smo se dogovorili da ćemo kreirati parametar za listu verzija BUILD_VERSION?

Hajde da preuzmemo konfiguracioni fajl i zavirimo u njega. Samo da biste bili sigurni da je parametar na mjestu i željenog tipa.

Snimak ekrana ispod spojlera.

Vaš config.xml fragment bi trebao izgledati isto. Osim što još nedostaje sadržaj elementa izbora
Kreiramo zadatak implementacije u GKE bez dodataka, SMS-a ili registracije. Hajde da zavirimo ispod Dženkinsove jakne

Jesi li siguran? To je to, hajde da napišemo skriptu koja će se izvršiti ako gradnja bude uspešna.
Skripta će dobiti listu verzija, preuzeti konfiguracionu datoteku, upisati listu verzija u nju na mjesto koje nam je potrebno, a zatim je vratiti. Da. Tako je. Napišite listu verzija u XML-u na mjestu gdje već postoji lista verzija (biće u budućnosti, nakon prvog pokretanja skripte). Znam da u svijetu još uvijek postoje žestoki ljubitelji regularnih izraza. Ja ne pripadam njima. Molimo instalirajte xmlstarler na mašinu na kojoj će se konfiguracija uređivati. Čini mi se da ovo nije tako velika cijena da se izbjegne uređivanje XML-a koristeći sed.

Ispod spojlera predstavljam kod koji u cijelosti izvodi gornji niz.

Napišite listu verzija iz foldera na udaljenom serveru u konfiguraciju

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

Ako više volite opciju da dobijete verzije od Jenkinsa i lijeni ste kao ja, onda ispod spojlera je isti kod, ali spisak od Jenkinsa:

Napišite listu verzija od Jenkinsa u konfiguraciju
Samo imajte ovo na umu: moje ime sklopa sastoji se od rednog broja i broja verzije, odvojenih dvotočkom. U skladu s tim, awk odsijeca nepotreban dio. Za sebe promijenite ovu liniju kako bi odgovarala vašim potrebama.

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

U teoriji, ako ste testirali kod napisan na osnovu gornjih primjera, tada bi u zadatku implementacije već trebali imati padajuću listu s verzijama. Kao na snimku ispod spojlera.

Ispravno popunjena lista verzija
Kreiramo zadatak implementacije u GKE bez dodataka, SMS-a ili registracije. Hajde da zavirimo ispod Dženkinsove jakne

Ako je sve uspjelo, kopirajte i zalijepite skriptu Pokreni shell komandu i sačuvajte promjene.

Povezivanje na Cloud shell

Imamo sakupljače u kontejnerima. Koristimo Ansible kao naš alat za isporuku aplikacija i konfiguracijski menadžer. Shodno tome, kada je u pitanju pravljenje kontejnera, tri opcije padaju na pamet: instalirati Docker u Docker, instalirati Docker na mašini koja radi na Ansibleu ili izgraditi kontejnere u konzoli u oblaku. Dogovorili smo se da šutimo o dodacima za Jenkins u ovom članku. Sjećaš se?

Odlučio sam: pa, pošto se kontejneri „iz kutije“ mogu sakupljati u konzoli u oblaku, zašto se onda mučiti? Neka bude čisto, zar ne? Želim prikupiti Jenkinsove kontejnere u konzoli oblaka, a zatim ih odatle pokrenuti u cuber. Štaviše, Google ima veoma bogate kanale unutar svoje infrastrukture, što će povoljno uticati na brzinu implementacije.

Da biste se povezali na cloud konzolu, potrebne su vam dvije stvari: gcloud i prava pristupa Google Cloud API za VM instancu iz koje će se ova ista veza napraviti.

Za one koji se planiraju povezati ne iz Google oblaka uopće
Google dozvoljava mogućnost onemogućavanja interaktivne autorizacije u svojim servisima. Ovo će vam omogućiti da se povežete na konzolu čak i sa aparata za kafu, ako radi *nix i ima samu konzolu.

Ako postoji potreba da ovo pitanje detaljnije pokrijem u okviru ove napomene, pišite u komentarima. Ako dobijemo dovoljno glasova, napisat ću ažuriranje na ovu temu.

Najlakši način za dodjelu prava je putem web sučelja.

  1. Zaustavite VM instancu sa koje ćete se naknadno povezati na konzolu u oblaku.
  2. Otvorite Detalji o instanci i kliknite izmjene i dopune.
  3. Na samom dnu stranice odaberite opseg pristupa instanci Potpuni pristup svim Cloud API-jima.

    Snimak ekrana
    Kreiramo zadatak implementacije u GKE bez dodataka, SMS-a ili registracije. Hajde da zavirimo ispod Dženkinsove jakne

  4. Sačuvajte promjene i pokrenite instancu.

Kada VM završi učitavanje, povežite se s njim putem SSH-a i uvjerite se da se veza odvija bez greške. Koristite naredbu:

gcloud alpha cloud-shell ssh

Uspješna veza izgleda otprilike ovako
Kreiramo zadatak implementacije u GKE bez dodataka, SMS-a ili registracije. Hajde da zavirimo ispod Dženkinsove jakne

Postavite na GKE

Budući da na sve moguće načine nastojimo da u potpunosti pređemo na IaC (Infrastucture as a Code), naši docker fajlovi su pohranjeni u Gitu. Ovo je s jedne strane. A implementacija u kubernetes-u je opisana yaml datotekom, koju koristi samo ovaj zadatak, koji je sam po sebi isto kao kod. Ovo je sa druge strane. Generalno, mislim, plan je ovakav:

  1. Uzimamo vrijednosti varijabli BUILD_VERSION i, opciono, vrijednosti varijabli koje će biti proslijeđene ENV.
  2. Preuzmite dockerfile sa Gita.
  3. Generirajte yaml za implementaciju.
  4. Oba ova fajla prenosimo putem scp-a na konzolu u oblaku.
  5. Tamo pravimo kontejner i guramo ga u registar kontejnera
  6. Primjenjujemo datoteku za postavljanje učitavanja na cuber.

Budimo konkretniji. Jednom smo počeli da pričamo ENV, onda pretpostavimo da trebamo proslijediti vrijednosti dva parametra: PARAM1 и PARAM2. Dodamo njihov zadatak za implementaciju, ukucajte - String parametar.

Snimak ekrana
Kreiramo zadatak implementacije u GKE bez dodataka, SMS-a ili registracije. Hajde da zavirimo ispod Dženkinsove jakne

Mi ćemo generirati yaml jednostavnim preusmjeravanjem odjek da fajl. Pretpostavlja se, naravno, da imate u svom docker fajlu PARAM1 и PARAM2da će naziv opterećenja biti awesomeapp, a u njemu se nalazi sastavljeni kontejner s aplikacijom navedene verzije Registar kontejnera na putu gcr.io/awesomeapp/awesomeapp-$BUILD_VERSIONgde $BUILD_VERSION je upravo odabran sa padajuće liste.

Spisak tima

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 nakon povezivanja koristeći gcloud alpha cloud-shell ssh interaktivni način rada nije dostupan, pa šaljemo komande na cloud konzolu pomoću parametra --komanda.

Očistimo početnu mapu u konzoli oblaka od starog dockerfile-a:

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

Postavite svježe preuzeti dockerfile u početnu mapu cloud konzole koristeći scp:

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

Prikupljamo, označavamo i guramo kontejner u registar kontejnera:

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"

Isto radimo sa datotekom za implementaciju. Imajte na umu da naredbe u nastavku koriste izmišljena imena klastera u kojem se odvija implementacija (awsm-cluster) i naziv projekta (super-projekat), gdje se nalazi klaster.

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"

Pokrećemo zadatak, otvaramo izlaz konzole i nadamo se da ćemo vidjeti uspješno sklapanje kontejnera.

Snimak ekrana
Kreiramo zadatak implementacije u GKE bez dodataka, SMS-a ili registracije. Hajde da zavirimo ispod Dženkinsove jakne

I onda uspješno postavljanje sastavljenog kontejnera

Snimak ekrana
Kreiramo zadatak implementacije u GKE bez dodataka, SMS-a ili registracije. Hajde da zavirimo ispod Dženkinsove jakne

Namerno sam ignorisao postavku Ulaz. Iz jednog jednostavnog razloga: nakon što ga postavite opterećenje sa datim imenom, ostat će operativan, bez obzira koliko implementacija s ovim imenom izvršite. Pa, generalno, ovo je malo izvan okvira istorije.

Umesto zaključaka

Svi gore navedeni koraci vjerovatno nisu mogli biti urađeni, već jednostavno instaliran neki dodatak za Jenkins, njihov muuulion. Ali iz nekog razloga ne volim dodatke. Pa, tačnije, pribjegavam im samo iz očaja.

I jednostavno volim da otvorim neku novu temu za sebe. Gornji tekst je i način da podijelim nalaze do kojih sam došao rješavajući problem opisan na samom početku. Podijelite sa onima koji, poput njega, uopće nisu strašni vuk u devopsu. Ako moja saznanja barem nekome pomognu, bit ću zadovoljan.

izvor: www.habr.com

Kupite pouzdan hosting za sajtove sa DDoS zaštitom, VPS VDS servere 🔥 Kupite pouzdan web hosting sa DDoS zaštitom, VPS VDS servere | ProHoster