Razvoj aplikacij in modro-zelena uvedba, ki temelji na metodologiji The Twelve-Factor App s primeri v php in dockerju

Razvoj aplikacij in modro-zelena uvedba, ki temelji na metodologiji The Twelve-Factor App s primeri v php in dockerju

Najprej malo teorije. Kaj se je zgodilo Aplikacija Twelve-Factor?

Preprosto povedano, ta dokument je zasnovan tako, da poenostavi razvoj aplikacij SaaS in pomaga z obveščanjem razvijalcev in inženirjev DevOps o težavah in praksah, s katerimi se najpogosteje srečujemo pri razvoju sodobnih aplikacij.

Dokument so ustvarili razvijalci platforme Heroku.

Aplikacijo Twelve-Factor lahko uporabite za aplikacije, napisane v katerem koli programskem jeziku in z uporabo katere koli kombinacije podpornih storitev (baze podatkov, čakalne vrste sporočil, predpomnilniki itd.).

Na kratko o dejavnikih, na katerih temelji ta metodologija:

  1. Codebase – Ena baza kode, sledena v nadzoru različic – več uvajanj
  2. Odvisnosti – Izrecno deklariraj in izoliraj odvisnosti
  3. Konfiguracija – Shranite konfiguracijo med izvajanjem
  4. Podporne storitve – Razmislite o podpornih storitvah kot virih vtičnikov
  5. Zgradite, sprostite, zaženite – Strogo ločite fazo montaže in izvedbe
  6. Procesi – Zaženite aplikacijo kot enega ali več procesov brez stanja
  7. Vezava vrat – Izvoz storitev prek povezovanja vrat
  8. Sočasnost – Povečajte svojo aplikacijo z uporabo procesov
  9. Enkratnost – Povečajte zanesljivost s hitrim zagonom in čistim izklopom
  10. Pariteta razvoj/delovanje aplikacij – Naj bodo vaša razvojna, uprizoritvena in produkcijska okolja čim bolj podobna
  11. Sečnja – Oglejte si dnevnik kot tok dogodkov
  12. Administrativna opravila – Izvajajte naloge administracije/upravljanja z ad hoc procesi

Več informacij o 12 dejavnikih lahko dobite v naslednjih virih:

Kaj je modro-zelena uvedba?

Modro-zelena uvedba je način dostave aplikacije v proizvodnja tako, da končni naročnik na svoji strani ne opazi sprememb. Z drugimi besedami, uvajanje aplikacije z ničlo odmore.

Klasična shema BG Deploy izgleda kot tista, prikazana na spodnji sliki.

Razvoj aplikacij in modro-zelena uvedba, ki temelji na metodologiji The Twelve-Factor App s primeri v php in dockerju

  • Na začetku sta 2 fizična strežnika z popolnoma isto kodo, aplikacijo, projektom in usmerjevalnik (balanser).
  • Usmerjevalnik na začetku vse zahteve usmeri na enega od strežnikov (zelena).
  • V trenutku, ko morate znova izdati, se celoten projekt posodobi na drugem strežniku (modra), ki trenutno ne obdeluje nobenih zahtev.
  • Ko je koda vklopljena modra strežnik popolnoma posodobljen, usmerjevalnik prejme ukaz za preklop zelena o modra strežnik.
  • Zdaj vsi odjemalci vidijo rezultat izvajanja kode modra strežnika.
  • Za nekaj časa, zelena strežnik služi kot rezervna kopija v primeru neuspešne uvedbe na modra strežnik in v primeru okvare in napak, usmerjevalnik preklopi uporabniški tok nazaj na zelena strežnik s staro stabilno različico, nova koda pa je poslana v revizijo in testiranje.
  • In na koncu postopka se posodobi na enak način zelena strežnik. In ko ga posodobi, usmerjevalnik preklopi tok zahteve nazaj na zelena strežnik.

Vse skupaj izgleda zelo dobro in na prvi pogled s tem ne bi smelo biti težav.
Ker pa živimo v sodobnem svetu, nam možnost s fizičnim preklopom, kot je prikazano v klasični shemi, ne ustreza. Za zdaj zabeležite podatke, k njim se bomo vrnili pozneje.

Slab in dober nasvet

Zavrnitev odgovornosti: Spodnji primeri prikazujejo pripomočke/metodologije, ki jih uporabljam, uporabite lahko popolnoma vse alternative s podobnimi funkcijami.

Večina primerov se bo na tak ali drugačen način križala s spletnim razvojem (to je presenečenje), s PHP in Dockerjem.

Spodnji odstavki nudijo preprost praktičen opis uporabe faktorjev z uporabo posebnih primerov; če želite izvedeti več teorije o tej temi, sledite zgornjim povezavam do izvirnega vira.

1. Kodna baza

Uporabite FTP in FileZilla za nalaganje datotek na strežnike eno za drugo, kodo ne shranjujte nikjer drugje kot na produkcijskem strežniku.

Projekt mora imeti vedno eno kodno osnovo, kar pomeni, da vsa koda izvira iz enega git repozitorij. Strežniki (production, staging, test1, test2...) uporabljajo kodo iz vej enega skupnega repozitorija. Na ta način dosežemo konsistentnost kode.

2. Odvisnosti

Prenesite vse knjižnice v mapah neposredno v koren projekta. Posodobite preprosto tako, da novo kodo prenesete v mapo s trenutno različico knjižnice. Namestite vse potrebne pripomočke neposredno na gostiteljski strežnik, kjer se izvaja še 20 dodatnih storitev.

Projekt mora vedno imeti jasno razumljiv seznam odvisnosti (z odvisnostmi mislim tudi okolje). Vse odvisnosti morajo biti izrecno definirane in izolirane.
Vzemimo za primer skladatelj и Lučki delavec.

skladatelj — upravitelj paketov, ki vam omogoča namestitev knjižnic v PHP. Skladatelj vam omogoča, da določite različice strogo ali ohlapno in jih eksplicitno definirate. Na strežniku je lahko 20 različnih projektov in vsak bo imel osebni seznam paketov in knjižnic, neodvisen od drugega.

Lučki delavec — pripomoček, ki omogoča definiranje in izolacijo okolja, v katerem se bo izvajala aplikacija. Skladno s tem lahko tako kot pri skladatelju, vendar bolj temeljito, ugotovimo, s čim aplikacija deluje. Izberite določeno različico PHP, namestite samo pakete, ki so potrebni za delovanje projekta, brez dodajanja česar koli dodatnega. In kar je najpomembnejše, brez poseganja v pakete in okolje gostiteljskega stroja in drugih projektov. To pomeni, da lahko vsi projekti na strežniku, ki se izvajajo prek Dockerja, uporabljajo absolutno kateri koli nabor paketov in popolnoma drugačno okolje.

3. Konfiguracija

Shranite konfiguracije kot konstante neposredno v kodo. Ločene konstante za testni strežnik, ločeno za proizvodnjo. Delovanje aplikacije v odvisnosti od okolja povežite neposredno s poslovno logiko projekta z uporabo konstrukcij if else.

Konfiguracije - to je edini način, da se morajo projektne uvedbe razlikovati. V idealnem primeru bi bilo treba konfiguracije posredovati prek spremenljivk okolja (env vars).

Se pravi, tudi če shranite več konfiguracijskih datotek .config.prod .config.local in jih ob uvajanju preimenujete v .config (glavna konfiguracija, iz katere aplikacija bere podatke) - to ne bo pravi pristop, saj v tem primeru bodo informacije iz konfiguracij javno dostopne vsem razvijalcem aplikacij, podatki iz produkcijskega strežnika pa bodo ogroženi. Vse konfiguracije morajo biti shranjene neposredno v sistemu za uvajanje (CI/CD) in generirane za različna okolja z različnimi vrednostmi, potrebnimi za določeno okolje v času uvajanja.

4. Storitve tretjih oseb

Bodite striktno vezani na okolje, uporabljajte različne povezave za iste storitve v določenih okoljih.

Dejansko se ta točka močno prekriva s točko o konfiguracijah, saj brez te točke ni mogoče ustvariti običajnih konfiguracijskih podatkov in na splošno bo zmožnost konfiguracije padla na nič.

Vse povezave z zunanjimi storitvami, kot so strežniki čakalnih vrst, baze podatkov, storitve predpomnjenja, morajo biti enake tako za lokalno okolje kot za tretje/produkcijsko okolje. Z drugimi besedami, kadar koli lahko s spremembo povezovalnega niza zamenjam klice na bazo #1 z bazo #2, ne da bi spremenil kodo aplikacije. Ali če pogledamo naprej, vam na primer pri skaliranju storitve ne bo treba posebej določiti povezave za dodatni predpomnilniški strežnik.

5. Zgradite, sprostite, izvedite

Na strežniku imejte samo končno različico kode, brez možnosti za vrnitev izdaje. Ni vam treba zapolniti prostora na disku. Kdor misli, da lahko pusti kodo v produkcijo z napako, je slab programer!

Vse stopnje uvajanja morajo biti med seboj ločene.

Imejte priložnost, da se vrnete nazaj. Naredite izdaje s starimi kopijami aplikacije (že sestavljenimi in pripravljenimi za boj), shranjenimi v hitrem dostopu, tako da lahko v primeru napak obnovite staro različico. To pomeni, da pogojno obstaja mapa za javnost in mapo tok, po uspešni uvedbi in sestavljanju pa mapo tok povezana s simbolično povezavo do nove izdaje, ki je v njej za javnost z običajnim imenom številke izdaje.

Tu se spomnimo modro-zelene uvedbe, ki vam omogoča ne samo preklapljanje med kodo, ampak tudi preklapljanje med vsemi viri in celo okolji z možnostjo povrnitve vsega.

6. Procesi

Shranite podatke o stanju aplikacije neposredno v sami aplikaciji. Uporabite seje v RAM-u same aplikacije. Uporabite čim več deljenja med storitvami tretjih oseb. Zanesite se na dejstvo, da ima lahko aplikacija samo en proces in ne dovolite skaliranja.

Kar zadeva seje, shranjujte podatke le v predpomnilnik, ki ga nadzorujejo storitve tretjih oseb (memcached, redis), tako da tudi če imate v teku 20 aplikacijskih procesov, bo kateri koli od njih, ko je dostopal do predpomnilnika, lahko nadaljeval delo z odjemalcem v isto stanje, v katerem je uporabnik delal z aplikacijo v drugem procesu. S tem pristopom se izkaže, da ne glede na to, koliko kopij storitev tretjih oseb uporabljate, bo vse delovalo normalno in brez težav z dostopom do podatkov.

7. Vezava vrat

Samo spletni strežnik bi moral vedeti, kako delati s storitvami tretjih oseb. Ali še bolje, namestite storitve tretjih oseb neposredno v spletni strežnik. Na primer kot modul PHP v Apache.
Vse vaše storitve morajo biti med seboj dostopne prek dostopa do nekega naslova in vrat (localgost:5432, localhost:3000, nginx:80, php-fpm:9000), kar pomeni, da lahko iz nginxa dostopam do php-fpm in do postgres in od php-fpm do postgres in nginx in dejansko iz vsake storitve lahko dostopam do druge storitve. Na ta način sposobnost preživetja storitve ni vezana na sposobnost preživetja druge storitve.

8. Vzporednost

Delajte z enim procesom, sicer se več procesov med seboj ne bo moglo ujemati!

Pustite prostor za skaliranje. Docker swarm je odličen za to.
Docker Swarm je orodje za ustvarjanje in upravljanje grozdov vsebnikov med različnimi stroji in množico vsebnikov na istem stroju.

S pomočjo roja lahko določim, koliko sredstev bom dodelil posameznemu procesu in koliko procesov iste storitve bom zagnal, notranji uravnoteževalec, ki prejme podatke na danih vratih, pa jih samodejno posreduje procesom. Tako lahko glede na povečano obremenitev strežnika dodam več procesov in s tem zmanjšam obremenitev določenih procesov.

9. Enkratnost

Ne uporabljajte čakalnih vrst za delo s procesi in podatki. Ukinitev enega procesa bi morala vplivati ​​na celotno aplikacijo. Če upade ena služba, upade vse.

Vsak proces in storitev lahko kadarkoli izklopite in to ne sme vplivati ​​na druge storitve (seveda to ne pomeni, da storitev ne bo na voljo za drugo storitev, ampak da se druga storitev ne bo izklopila za to). Vse procese je treba končati elegantno, tako da ob njihovi prekinitvi ne pride do poškodb nobenih podatkov in da bo sistem deloval pravilno, ko ga naslednjič vklopite. To pomeni, da tudi v primeru nujne prekinitve podatki ne bi smeli biti poškodovani (tu je primeren transakcijski mehanizem, poizvedbe v bazi delujejo samo v skupinah in če vsaj ena poizvedba iz skupine ne uspe ali se izvede z napaka, potem nobena druga poizvedba iz skupine na koncu dejansko ne uspe).

10. Pariteta razvoj/delovanje aplikacije

Produkcija, uprizoritev in lokalna različica aplikacije se morajo razlikovati. V produkciji uporabljamo ogrodje Yii Lite, lokalno pa Yii, tako da deluje hitreje v produkciji!

V resnici bi morale biti vse uvedbe in delo s kodo v skoraj identičnem okolju (ne govorimo o fizični strojni opremi). Prav tako bi moral biti vsak zaposleni v razvoju sposoben namestiti kodo v produkcijo, če je to potrebno, in ne kakšen posebej usposobljen devops oddelek, ki samo zaradi posebne moči lahko dvigne aplikacijo v produkcijo.

Pri tem nam pomaga tudi Docker. Če upoštevate vse prejšnje točke, bo uporaba dockerja pripeljala postopek uvajanja okolja tako v produkcijo kot na lokalni računalnik do vnosa enega ali dveh ukazov.

11. Dnevniki

Pišemo dnevnike v datoteke in podatkovne baze! Datotek in baz podatkov ne čistimo iz dnevnikov. Kupimo samo trdi disk z 9000 Peta bajti in to je v redu.

Vse dnevnike je treba obravnavati kot tok dogodkov. Aplikacija sama ne bi smela sodelovati pri obdelavi dnevnikov. Dnevnike je treba izpisati v stdout ali poslati prek protokola, kot je udp, tako da delo z dnevniki ne povzroča težav aplikaciji. graylog je dober za to. Graylog, ki sprejema vse dnevnike preko udp (ta protokol ne zahteva čakanja na odgovor o uspešnem sprejemu paketa) v ničemer ne moti aplikacije in se ukvarja samo s strukturiranjem in obdelavo dnevnikov. Logika aplikacije se ne spremeni za delo s takšnimi pristopi.

12. Administrativna opravila

Če želite posodobiti podatke, baze podatkov itd., uporabite ločeno ustvarjeno končno točko v API-ju; če jo izvedete 2-krat zapored, se bo vse podvojilo. Ampak nisi neumen, ne boš dvakrat kliknil in ne potrebujemo migracije.

Vse skrbniške naloge je treba izvajati v istem okolju kot vsa koda na ravni izdaje. To pomeni, da če moramo spremeniti strukturo baze podatkov, potem tega ne bomo storili ročno s spreminjanjem imen stolpcev in dodajanjem novih prek nekaterih vizualnih orodij za upravljanje baze podatkov. Za take stvari izdelamo ločene skripte – migracije, ki se izvajajo povsod in v vseh okoljih na enak način s skupnim in razumljivim rezultatom. Za vsa druga opravila, kot je polnjenje projekta s podatki, je treba uporabiti podobne metodologije.

Primer implementacije v PHP, Laravel, Laradock, Docker-Compose

PS Vsi primeri so bili narejeni v sistemu MacOS. Večina jih je primernih tudi za Linux. Uporabniki sistema Windows, oprostite mi, vendar že dolgo nisem delal z operacijskim sistemom Windows.

Predstavljajmo si situacijo, ko na našem računalniku nimamo nameščene nobene različice PHP in prav nič.
Namestite najnovejše različice dockerja in docker-compose. (to se najde na internetu)

docker -v && 
docker-compose -v

Razvoj aplikacij in modro-zelena uvedba, ki temelji na metodologiji The Twelve-Factor App s primeri v php in dockerju

1. Postavili smo Laradock

git clone https://github.com/Laradock/laradock.git && 
ls

Razvoj aplikacij in modro-zelena uvedba, ki temelji na metodologiji The Twelve-Factor App s primeri v php in dockerju

Glede Laradocka bom rekel, da je zelo kul stvar, ki vsebuje veliko posod in pomožnih stvari. Vendar ne bi priporočal uporabe Laradocka kot takega brez sprememb v proizvodnji zaradi njegove redundance. Bolje je ustvariti lastne kontejnerje na podlagi primerov v Laradocku, to bo veliko bolj optimizirano, saj nihče ne potrebuje vsega, kar je tam hkrati.

2. Konfigurirajte Laradock za zagon naše aplikacije.

cd laradock && 
cp env-example .env

Razvoj aplikacij in modro-zelena uvedba, ki temelji na metodologiji The Twelve-Factor App s primeri v php in dockerju

2.1. Odprite imenik habr (nadrejena mapa, v katero je laradock kloniran) v nekem urejevalniku. (V mojem primeru PHPStorm)

Na tej stopnji projektu le damo ime.

Razvoj aplikacij in modro-zelena uvedba, ki temelji na metodologiji The Twelve-Factor App s primeri v php in dockerju

2.2. Zaženite sliko delovnega prostora. (V vašem primeru bo izdelava slik trajala nekaj časa)
Delovni prostor je posebej pripravljena slika za delo z ogrodjem v imenu razvijalca.

Z uporabo gremo v posodo

docker-compose up -d workspace && 
docker-compose exec workspace bash

Razvoj aplikacij in modro-zelena uvedba, ki temelji na metodologiji The Twelve-Factor App s primeri v php in dockerju

2.3. Namestitev Laravela

composer create-project --prefer-dist laravel/laravel application

Razvoj aplikacij in modro-zelena uvedba, ki temelji na metodologiji The Twelve-Factor App s primeri v php in dockerju

2.4. Po namestitvi preverimo, ali je imenik s projektom ustvarjen in prekinemo sestavljanje.

ls
exit
docker-compose down

Razvoj aplikacij in modro-zelena uvedba, ki temelji na metodologiji The Twelve-Factor App s primeri v php in dockerju

2.5. Vrnimo se k PHPStorm in nastavimo pravilno pot do naše aplikacije laravel v datoteki .env.

Razvoj aplikacij in modro-zelena uvedba, ki temelji na metodologiji The Twelve-Factor App s primeri v php in dockerju

3. Dodajte vso kodo v Git.

Da bi to naredili, bomo ustvarili repozitorij na Githubu (ali kjer koli drugje). Pojdimo v imenik habr v terminalu in izvedimo naslednjo kodo.

echo "# habr-12factor" >> README.md
git init
git add README.md
git commit -m "first commit"
git remote add origin [email protected]:nzulfigarov/habr-12factor.git # здесь будет ссылка на ваш репо
git push -u origin master
git status

Preverimo, če je vse v redu.

Razvoj aplikacij in modro-zelena uvedba, ki temelji na metodologiji The Twelve-Factor App s primeri v php in dockerju

Za udobje priporočam uporabo vizualnega vmesnika za Git, v mojem primeru je to GitKraken. (tu je referenčna povezava)

4. Začnimo!

Preden začnete, se prepričajte, da na vratih 80 in 443 nič ne visi.

docker-compose up -d nginx php-fpm

Razvoj aplikacij in modro-zelena uvedba, ki temelji na metodologiji The Twelve-Factor App s primeri v php in dockerju

Tako je naš projekt sestavljen iz 3 ločenih storitev:

  • nginx - spletni strežnik
  • php-fpm - php za sprejemanje zahtev s spletnega strežnika
  • delovni prostor - php za razvijalce

Trenutno smo dosegli, da smo ustvarili aplikacijo, ki izpolnjuje 4 točke od 12, in sicer:

1. Codebase — vsa koda je v enem repozitoriju (majhna opomba: morda bi bilo pravilno dodati docker znotraj projekta laravel, vendar to ni pomembno).

2. Odvisnosti - Vse naše odvisnosti so izrecno zapisane v application/composer.json in v vsaki Dockerfile vsakega vsebnika.

3. Podporne storitve — Vsaka od storitev (php-fom, nigx, workspace) živi svoje življenje in je povezana od zunaj in pri delu z eno storitvijo druga ne bo prizadeta.

4. Procesi — vsaka storitev je en proces. Vsaka od storitev ne vzdržuje notranjega stanja.

5. Vezava vrat

docker ps

Razvoj aplikacij in modro-zelena uvedba, ki temelji na metodologiji The Twelve-Factor App s primeri v php in dockerju

Kot lahko vidimo, vsaka storitev deluje na svojih vratih in je dostopna vsem ostalim storitvam.

6. Sočasnost

Docker nam omogoča ustvarjanje več procesov istih storitev s samodejnim uravnavanjem obremenitve med njimi.

Ustavimo zabojnike in jih spustimo skozi zastavo --lestvica

docker-compose down && 
docker-compose up -d --scale php-fpm=3 nginx php-fpm

Razvoj aplikacij in modro-zelena uvedba, ki temelji na metodologiji The Twelve-Factor App s primeri v php in dockerju

Kot lahko vidimo, so bile ustvarjene kopije vsebnika php-fpm. Pri delu s tem vsebnikom nam ni treba ničesar spremeniti. Prav tako še naprej dostopamo do njega na vratih 9000, Docker pa nam uravnava obremenitev med kontejnerji.

7. Enkratnost - vsako posodo je mogoče ubiti, ne da bi pri tem poškodovali drugo. Zaustavitev ali ponovni zagon vsebnika ne bo vplival na delovanje aplikacije med nadaljnjimi zagoni. Posamezen zabojnik se lahko kadarkoli tudi dvigne.

8. Pariteta razvoj/delovanje aplikacij - vsa naša okolja so enaka. Če sistem izvajate na strežniku v produkciji, vam ne bo treba spremeniti ničesar v vaših ukazih. Vse bo temeljilo na Dockerju na enak način.

9. Sečnja — vsi dnevniki v teh vsebnikih gredo v tok in so vidni v konzoli Docker. (v tem primeru pravzaprav pri drugih doma narejenih posodah morda ne bo tako, če zanjo ne poskrbite)

 docker-compose logs -f

Razvoj aplikacij in modro-zelena uvedba, ki temelji na metodologiji The Twelve-Factor App s primeri v php in dockerju

Vendar obstaja ulov v tem, da privzete vrednosti v PHP in Nginx prav tako pišejo dnevnike v datoteko. Za izpolnjevanje 12 dejavnikov je potrebno prekinite povezavo pisanje dnevnikov v datoteko v konfiguracijah vsakega vsebnika posebej.

Docker ponuja tudi možnost pošiljanja dnevnikov ne le v stdout, temveč tudi v stvari, kot je graylog, ki sem ga omenil zgoraj. Znotraj grayloga pa lahko upravljamo z dnevniki, kot želimo, in naša aplikacija tega nikakor ne bo opazila.

10. Administrativna opravila — vse skrbniške naloge rešuje laravel zahvaljujoč obrtniškemu orodju točno tako, kot si želijo ustvarjalci 12-faktorske aplikacije.

Kot primer bom pokazal, kako se izvajajo nekateri ukazi.
Gremo v posodo.

 
docker-compose exec workspace bash
php artisan list

Razvoj aplikacij in modro-zelena uvedba, ki temelji na metodologiji The Twelve-Factor App s primeri v php in dockerju

Zdaj lahko uporabimo poljuben ukaz. (upoštevajte, da baze podatkov in predpomnilnika nismo konfigurirali, zato se polovica ukazov ne bo pravilno izvedla, ker so zasnovani za delo s predpomnilnikom in bazo podatkov).

Razvoj aplikacij in modro-zelena uvedba, ki temelji na metodologiji The Twelve-Factor App s primeri v php in dockerju

11. Konfiguracije in 12. Zgradite, sprostite, zaženite

Ta del sem želel posvetiti modro-zeleni uvedbi, a se je izkazalo, da je preobsežen za ta članek. O tem bom napisal ločen članek.

Na kratko, koncept temelji na sistemih CI/CD, kot je Jenkins и Gitlab CI. V obeh lahko nastavite spremenljivke okolja, povezane z določenim okoljem. Skladno s tem bo v tem primeru točka c izpolnjena Konfiguracije.

In bistvo o Zgradite, sprostite, zaženite rešujejo vgrajene funkcije z imenom Pipeline.

Pipeline vam omogoča, da razdelite postopek uvajanja na več stopenj, s poudarkom na stopnjah sestavljanja, izdaje in izvajanja. Tudi v Pipelineu lahko ustvarite varnostne kopije in kar koli. To je orodje z neomejenim potencialom.

Koda aplikacije je na GitHub.
Pri kloniranju tega repozitorija ne pozabite inicializirati podmodula.

PS: Vse te pristope je mogoče uporabiti s katerim koli drugim pripomočkom in programskim jezikom. Glavna stvar je, da se bistvo ne razlikuje.

Vir: www.habr.com

Dodaj komentar