Az Ansible alapjai, amelyek nélkül a játékkönyveid egy ragacsos tészta gombóc lesz.

Sokat írok mások Ansible kódjáról és sokat írok én magam is. A hibák elemzése során (akár másoké, akár a sajátjaim), valamint néhány interjú alatt rájöttem, hogy a felhasználók legnagyobb hibája az, hogy bonyolult dolgokba vágnak bele anélkül, hogy elsajátítanák az alapokat.

A világegyetem ezen igazságtalanságának orvoslására úgy döntöttem, hogy írok egy bevezetőt az Ansible-hez azok számára, akik már ismerik. Figyelmeztetem, hogy ez nem egy kézikönyv összefoglalás, hanem egy hosszú olvasmány, amely tele van betűkkel, de képek nincsenek benne.

A várható olvasói szint — már írtak néhány ezer sort YAML fájlt, már van valami a termelésben, de "valahogy minden görbe".

Címek

Az Ansible felhasználók legnagyobb hibája az, hogy nem tudják, mi micsoda. Ha nem tudod a neveket, nem tudod érteni, ami a dokumentációban van. Élő példa: egy interjún egy ember, aki állítása szerint sokat írt Ansible-ben, nem tudott válaszolni arra a kérdésre, hogy "miből áll egy playbook?". Amikor pedig azt mondtam, hogy "a válasz várhatóan az, hogy a playbook play-ekből áll", akkor egy lesújtó megjegyzést kaptam: "mi ezt nem használjuk". Az emberek pénzért írnak Ansible-re, és nem használják a play-t. Valójában használják, de nem tudják, mi az.

Tehát kezdjük a legegyszerűbbtől: hogyan nevezik azt, ami van. Lehet, hogy tudod, de lehet, hogy nem, mert nem figyeltél oda, amikor a dokumentációt olvastad.

Az ansible-playbook végrehajtja a playbook-ot. A playbook egy yml/yaml kiterjesztésű fájl, amelyben valami ilyesmi van:

---
- hosts: group1
  roles:
    - role1

- hosts: group2,group3
  tasks:
    - debug:

Már megértettük, hogy ez az egész fájl — a playbook. Meg tudjuk mutatni, hol vannak a szerepek (roles), hol a feladatok (tasks). De hol van itt a play? És miben különbözik a play a szereptől vagy a playbook-tól?

Ez mind benne van a dokumentációban. De ezt kihagyják. A kezdők — mert túl sok van, és nem lehet mindent egyszerre megjegyezni. A tapasztaltak — mert "triviális dolgok". Ha tapasztalt vagy — olvasd át ezeket az oldalakat legalább fél évente, és a kódod sokkal jobb lesz.

Tehát jegyezd meg: A playbook egy lista, amely play-ből és import_playbook.
Ez itt egy play:

- hosts: group1
  roles:
    - role1

És ez is itt egy másik play:

- hosts: group2,group3
  tasks:
    - debug:

Mi is az a play? Mire való?

A play a playbook kulcsfontosságú eleme, mert a play, és csak a play köti össze a szerepek és/vagy feladatok listáját azokkal a hosztokkal, amelyeken végre kell hajtani őket. A dokumentáció mélyebb rétegeiben találni lehet említést a delegate_to, helyi lookup bővítmények, hálózati cli-specifikus beállítások, jump-hostok stb. Ezek lehetővé teszik a feladatok végrehajtásának helyének enyhe megváltoztatását. De felejtsd el ezt. Mindegyik okos opció nagyon speciális alkalmazásokkal rendelkezik, és biztosan nem univerzálisak. Mi az alapvető dolgokról beszélünk, amelyeket mindenki tudnia kell és használnia kell.

Ha "valamit" szeretnél végrehajtani "valahol" — írsz egy play-t. Nem szerepet. Nem szerepet modulokkal és delegáltakkal. Csak írsz egy play-t. Amelyben a hosts mezőben felsorolod, hol kell végrehajtani, a roles/tasks mezőben pedig, mit kell végrehajtani.

Egyszerű, igaz? Hát hogyan lenne másként?

Az egyik tipikus pillanat, amikor az emberek hajlamosak ezt nem play-en keresztül megcsinálni, az a "szerep, amely mindent beállít". Szeretnénk egy szerepet, amely beállítja és a szerver az első típusú, valamint a második típusú szervereket.

A klasszikus példa a monitoring. Szeretnénk egy monitoring szerepet, amely beállítja a monitoringot. A monitoring szerepet a monitoring hosztokra rendeljük (a megfelelő play-ben). De kiderül, hogy a monitoringhoz csomagokat kell telepítenünk a monitorozott hosztokra. Miért ne használnánk delegáltat? És meg kellene állítanunk az iptables-t. delegált? És szükség van egy konfigurációs fájl írására/módosítására az adatbázis-kezelő számára, hogy a monitoringot engedélyezze. delegált! És ha kreatív ötleted támad, akkor egy beágyazott delegációt is létrehozhatsz include_role a csoportok listáján végzett okos szűrővel végzett beágyazott ciklusban, belül include_role még lehet csinálni delegate_to megint. És folytatódik…

A jó szándék — hogy legyen egyetlen monitoring szerep, amely "mindent csinál" — a pokolba vezet, ahonnan a leggyakrabban egyetlen kiút van: mindent újraírni a nulláról.

Hol történt a hiba? Abban a pillanatban, amikor rájöttél, hogy az "x" feladat végrehajtásához az X hoszton el kell menned a Y hosztra és ott "y"-t kell csinálnod, egyszerű gyakorlatot kellett volna végrehajtanod: elmenni és megírni egy play-t, amely a Y hoszton végzi a y-t. Nem kiegészíteni az "x"-t, hanem újraírni a nulláról. Még ha hardkódolt változókkal is.

Úgy tűnik, az előző bekezdésekben minden helyesen van megfogalmazva. De ez nem a te eseted! Mert te egy újrahasználható kódot szeretnél írni, amely DRY és könyvtár-szerű, és meg kell keresni a módját, hogyan tedd ezt.

Itt rejtőzik egy újabb súlyos hiba. Ez a hiba számos projektet változtatott át tűrhetően megírtból (lehetne jobb, de működik, és könnyen bővíthető) egy teljes rémálommá, amelyben még a szerző sem tud eligibilis régi venni. Működik, de óvva intem, ha változtatni szeretnél rajta.

Ez a hiba így hangzik: a szerep — könyvtári funkció. Ez az analógia annyi jó kezdeményezést tönkretett, hogy szinte szomorú nézni. A szerep nem könyvtári funkció. Nem végezhet számításokat, és nem hozhat play szintű döntéseket. Emlékeztessetek, milyen döntéseket hoz a play?

Köszönöm, igazad van. A play döntést hoz (pontosabban, információt tartalmaz arról), hogy mely feladatokat és szerepeket hajtsanak végre, és mely hosztokon.

Ha ezt a döntést a szerepre bízzuk, ráadásul még számításokkal is, akkor egy nyomorult életre kárhoztatod magadat (és azt, aki a kódodat próbálja megfejteni). A szerep nem határozza meg, hogy hol hajtódik végre. Ezt a döntést a play hozza meg. A szerep azt teszi, amit mondtak neki, ott, ahol mondták.

Arról beszélünk, hogy miért veszélyes az Ansible programozás, és miben jobb a COBOL az Ansible-nál az változók és jinja fejezetben. Jelenleg annyit mondhatunk — minden számításod megmarad elmoshatatlan nyomot hagyva a globális változók megváltoztatásából, és ezzel semmit sem tudsz tenni. Amint két "nyom" keresztezi egymást — minden elveszett.

Megjegyzés a figyelmeseknek: a szerep kétségtelenül befolyásolhatja a control flow-t. Van delegate_to és vannak ésszerű alkalmazásai. Van meta: end host/play. De! Ne feledjük, az alapokat tanuljuk? Elfeledkeztünk a delegate_to. A legegyszerűbb és legszebb Ansible kódról beszélünk. Amit könnyű olvasni, könnyű írni, könnyű hibakeresni, könnyű tesztelni és könnyű bővíteni. Tehát, még egyszer:

a play, és csak a play eldönti, hogy mely hosztokon mi hajtódik végre.

Ebben a szakaszban tisztáztuk a play és a szerep közötti konfliktust. Most beszéljünk a tasks és a role közötti kapcsolatról.

Feladatok és Szerepek

Nézzük meg a play-t:

- hosts: somegroup
  pre_tasks:
    - some_tasks1:
  roles:
     - role1
     - role2
  post_tasks:
     - some_task2:
     - some_task3:

Tegyük fel, hogy foo-t kell készítened. Így néz ki foo: name=foobar state=present. Hol kell ezt írni? a pre-ben? a post-ban? Készítsünk szerepet?

… És hová tűntek a feladatok?

Újra kezdjük az alapokkal — a play szerkezetével. Ha nem úszol ebben a kérdésben, nem tudod a play-t alapul venni minden máshoz, és az eredményed “ingatag” lesz.

A play eszköz: hosts direktíva, magának a play beállításai és a pre_tasks, tasks, roles, post_tasks szekciók. A többi paraméter nem fontos most a play számára.

A szekciók sorrendje feladatokkal és szerepekkel: pre_tasks, roles, tasks, post_tasks. Mivel a végrehajtás sorrendje között tasks és roles nem világos, a legjobb gyakorlat szerint szekciót adunk hozzá, taskscsak akkor, ha nincs roles, akkor megtiltjuk (403). roles, akkor minden kapcsolódó feladat a szekciókba kerül. pre_tasks/post_tasks.

Csak az marad, ami szemantikailag tiszta: először pre_tasks, aztán roles, aztán post_tasks.

De még mindig nem válaszoltunk a kérdésre: hova kellene írni a modulhívást? foo Meg kell-e írni minden modulhoz egy teljes szerepet? Vagy jobb, ha van egy vastag szerep mindent lefedve? És ha nem szerep, akkor hova írjunk — pre-be vagy post-ba?

Ha ezekre a kérdésekre nincs megalapozott válasz, akkor az intuíció hiányának jele, azaz a "törékeny alapok". Nézzük meg. Először egy ellenőrző kérdés: Ha a play rendelkezik pre_tasks és post_tasks (és nincs sem task, sem role), akkor történhet-e valami hiba, ha az első feladatot a post_tasks végére mozgatom? pre_tasks?

Természetesen a kérdés megfogalmazása arra utal, hogy valami elromlik. De mi pontosan?

… Handler-ek. Az alapok olvasása fontos tényt tár fel: minden handler automatikusan flush-olódik minden szekció után. Tehát végrehajtódik minden feladat a pre_tasks, majd az összes handler, ami notify volt. Ezt követően végrehajtódnak minden szerep és az összes handler, ami notify volt a szerepekben. Utána post_tasks és azok handler-jei.

Így ha egy feladatot áthelyez a post_tasks be. pre_tasksszekcióból, potenciálisan azt végrehajthatja a handler végrehajtása előtt. Például, ha a pre_tasks telepítést és konfigurálást végez, webszerver, míg a post_tasks valami küldésével, akkor ennek a feladatnak a mozgása a pre_tasks szekcióba azt eredményezi, hogy a "küldés" pillanatában a szerver még nem lesz elindítva és minden elromlik.

Most nézzük meg még egyszer, hogy miért van szükségünk a pre_tasks és post_tasks? Например, для того, чтобы выполнить всё нужное (включая хэндлеры) до выполнения роли. А post_tasks segíteni fog nekünk a szerepek teljesítésének eredményeivel (beleértve a handler-eket) való munkában.

Egy szőrszálhasogató Ansible szakértő azt fogja mondani, hogy van meta: flush_handlers, de miért van szükségünk a flush_handlers-re, ha bízhatunk a szekciók végrehajtási sorrendjében a play-ben? Sőt, a meta: flush_handlers használata váratlan problémákat okozhat ismétlődő handler-ekkel, furcsa figyelmeztetéseket okozhat a when sa-logic-subsets-canary-vs.yaml block használatakor stb. Minél jobban ismered az Ansible-t, annál több árnyalatot tudsz említeni a "trükkös" megoldásról. Egy egyszerű megoldás — a természetes elválasztás használata a pre/roles/post között — nem okoz árnyalatokat.

És most térjünk vissza a 'foo'-hoz. Hová helyezzük be? A pre-be, post-ba vagy a roles-ba? Nyilvánvaló, hogy ez attól függ, szükségünk van-e a foo handlerének működésének eredményeire. Ha nincs, akkor a foo-t nem kell a pre-be vagy a post-ba tenni — ezek a szakaszok speciális jelentéssel bírnak — a feladatok végrehajtása a fő kódblokk előtt és után.

Most a "szerep vagy feladat" kérdése arra redukálódik, ami már benne van a play-ben — ha ott vannak feladatok, akkor azokat a feladatokhoz kell hozzáadni. Ha vannak szerepek — szerepet kell csinálni (még ha csak egy feladatból is). Emlékeztetlek, a feladatok és szerepek egyszerre nem használhatóak.

Az Ansible alapjainak megértése megalapozott válaszokat ad, látszólag ízlésbeli kérdésekre.

Feladatok és szerepek (második rész)

Most beszéljük meg azt a helyzetet, amikor éppen csak elkezded írni a playbookot. Azon dolgozol, hogy foo, bar és baz legyen. Ez három feladat, egy szerep vagy három szerep? Összefoglalva a kérdést: mikor kezdjük el szerepeket írni? Mi a célja szerepek írásának, amikor feladatokat is írhatunk?… De mi is az a szerep?

Az egyik legnagyobb hiba (már beszéltem erről) — azt feltételezni, hogy a szerep olyan, mint egy függvény egy program könyvtárában. Hogyan néz ki egy összefoglaló függvény leírás? Bemeneti argumentumokat fogad, interakcióba lép az oldalsó okokkal, mellékhatásokat generál, és visszatér egy értékkel.

Most figyelem. Melyik részeket lehet szerepben megvalósítani? Mellékhatásokat generálni — mindig lehet, ez az Ansible alapja — mellékhatásokat létrehozni. Oldalirányú okokkal bírni? Egyszerű. De ami a "értéket átadni és visszatérni" — itt nem. Először is, nem tudsz értéket átadni egy szerepnek. Globális változót állíthatsz be a szerep vars szakaszában, amely élettartamával a play élettartamára korlátozódik. Beállíthatsz globális változót a play élettartama alatt a szerep belsejében. Vagy akár a playbook élettartamáig is.set_fact/register). De nem lehet "helyi változóid". Nem tudod "fogadni az értéket" és "visszatérni".

Ebből következik a legfontosabb: az Ansible-ben nem lehet olyat írni, ami mellékhatások nélkül működik. A globális változók megváltoztatása mindig mellékhatás egy függvény számára. Például Rustban a globális változó megváltoztatása — unsafe. Az Ansible-ben pedig az egyetlen módja annak, hogy hatással legyünk a szerepre vonatkozó értékekre. Figyelj a használt szavakra: nem "értéket átadni szerepnek", hanem "megváltoztatni az értékeket, amelyeket a szerep használ". Nincs elszigeteltség a szerepek között. Nincs elszigeteltség a feladatok és szerepek között.

Összesen: a szerep nem függvény.

Mit is jó a szerepkörben? Először is, a szerepkörnek vannak alapértelmezett értékei (/default/main.yaml), másodszor pedig a szerepkörnek vannak további könyvtárai a fájlok tárolására.

Miért jók az alapértelmezett értékek? Azért, mert Maslow piramisának meglehetősen torz prioritási táblázatában az Ansible esetében a szerepkör alapértelmezett beállításai a legkevésbé prioritásosak (kivéve az Ansible parancssori paramétereit). Ez azt jelenti, hogy ha alapértelmezett értékeket szeretnél megadni anélkül, hogy aggódnod kellene, hogy felülírják az értékeket az inventory vagy csoportváltozók, akkor a szerepkör alapértelmezettjei jelentik az egyetlen helyes megoldást számodra. (Kicsit hazudok – van még |d(your_default_here), de ha statikus helyekről beszélünk – akkor csak a szerepkörök alapértelmezettjei számítanak).

Mi még a jó a szerepkörökben? Az, hogy saját könyvtáraik vannak. Ezek a könyvtárak változókhoz készültek, akár állandók (azaz a szerepkörhöz számítottak), akár dinamikusak (van egy ilyen mintázat, vagy anti-mintázat – include_vars együtt a {{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml.). Ezek könyvtárak a files/, templates/. Ezenkívül lehetővé teszik, hogy a szerepkörök saját modulokat és bővítményeket rendelkezzenek (library/). De a playbook feladatokhoz képest (amelyek szintén tartalmazhatják ezeket) itt csak annyiban hasznos, hogy a fájlok nincsenek egy kupacban, hanem több különálló kupacban.

Egy másik részlet: lehet próbálkozni olyan szerepkörök létrehozásával, amelyek újrahasználhatóak lesznek (galaxy-n keresztül). Miután megjelentek a gyűjtemények, a szerepkörök terjedését majdnem elfelejthetjük.

Ezáltal a szerepkörök két fontos tulajdonsággal rendelkeznek: van alapértelmezettjük (ami egyedi jellemző) és lehetővé teszik a kód struktúrázását.

Visszatérve a kezdeti kérdéshez: mikor készítsünk feladatokat és mikor szerepköröket? A playbookban a feladatokat általában vagy "ragasztóként" használják a szerepkörök előtt/ után, vagy önálló építőelemként (akkor a kódban nem legyenek szerepkörök). A normális feladatok halmozása szerepkörökkel keverve egyértelműen ápolatlanság. Követni kell egy adott stílust – vagy feladatok, vagy szerepkörök. A szerepkörök elkülönítik a dolgokat és biztosítják az alapértelmezetteket, a feladatok pedig lehetővé teszik a kód gyorsabb olvasását. Általában a szerepkörökbe kerül a "stabilabb" (fontos és bonyolult) kód, míg a feladatstílusban segédprogramokat írnak.

Lehetőség van az import_role használatára, mint feladatra, de ha ilyet írsz, akkor készülj fel egy magyarázatra a saját esztétikai érzéked számára, hogy miért akarod ezt tenni.

Egy aprólékos olvasó azt mondhatja, hogy a szerepek importálhatják egymást, a szerepek között lehet függőség a galaxy.yml-en keresztül, és még ott van a félelmetes és szörnyű include_role — emlékeztetem, hogy az alap Ansible tudást fejlesztjük, nem pedig a ritmikus gimnasztikát.

Handler-ek és feladatok

Beszéljünk meg még egy nyilvánvaló dolgot: a handler-ek. Az ő helyes használatuk szinte művészet. Mi a különbség a handler és a feladat között?

Mivel a alapokat bontogatjuk, itt van egy példa:

- hosts: group1
  tasks:
    - foo:
      notify: handler1
  handlers:
     - name: handler1
       bar:

A szerep handler-ei a rolename/handlers/main.yaml fájlban találhatók. A handler-ek megoszthatók minden résztvevő között a play-en belül: a pre/post_tasks hívhatják a szerep handler-eit, míg a szerep hívhatja a play handler-eit. Azonban a "kereszt-szerepes" handler hívások sokkal nagyobb WTF érzést keltenek, mint a triviális handler ismétlése. (Egy másik jó gyakorlat — próbálj meg ne ismételni a handler nevek használatát).

A fő különbség abban rejlik, hogy a feladat mindig végrehajtásra kerül (idempotens módon), azaz (plusz/mínusz címkék és when), míg a handler az állapotváltozáskor (notify csak akkor aktiválódik, ha volt változás). Miért is komoly ez? Például, ha újra futtatjuk, és nem volt változás, akkor a handler sem fog végrehajtódni. De miért fordulhat elő, hogy a handler-t kell végrehajtani, amikor a generáló feladatnál nem volt változás? Például azért, mert valami elromlott, és volt változás, de a végrehajtás nem jutott el a handler-hez. Például, mert a hálózat átmenetileg leállt. A konfiguráció megváltozott, a szolgáltatás nem indult újra. A következő futtatásnál a konfiguráció már nem változik, és a szolgáltatás a régi konfigurációs verzióval marad.

A konfigurációval kapcsolatos helyzet nem megoldható (pontosabban lehet létrehozni egy speciális újraindítási protokollt fájlflag-ekkel stb., de az már nem az 'alap ansible' semmilyen értelemben). Viszont van egy másik gyakori történet: telepítettük az alkalmazást, felvettük a .service-fájlt, és most szeretnénk a daemon_reload és state=started. És a megfelelő hely erre, úgy tűnik, egy handler. De ha nem handlerként használjuk, hanem feladatként a feladatlapon vagy szerepként, akkor minden alkalommal idempotens módon fog végrehajtódni. Még akkor is, ha a playbook felmondta a szolgálatot a közepén. Ez egyáltalán nem oldja meg a restarted problémát (nem szabad feladatot kérni restarted attribútummal, mivel ez elveszíti az idempotenciát), de mindenképpen érdemes state=started-re állítani, a playbookok általános stabilitása nő, mivel csökken a kapcsolatok és a dinamikus állapotok száma.

Egy másik pozitív tulajdonsága a handlernek, hogy nem terheli le a kimenetet. Ha nem voltak változások, akkor nincsenek felesleges skipped vagy ok a kimenetben — könnyebb olvasni. Ugyanakkor ez egy negatív tulajdonság is – ha egy elírást találsz a lineárisan végrehajtott taskban az első futás során, akkor a handler csak changed esetén fog végrehajtódni, tehát bizonyos feltételek mellett — nagyon ritkán. Például, életemben először öt év után. És persze, ott lesz az elírás a névben, és minden összedől. Másodjára már nem tudják futtatni, hiszen nincs changed.

Külön érdemes beszélni a változók elérhetőségéről. Például, ha értesítesz egy ciklikusan végrehajtott feladatot, akkor mi lesz a változókban? Elemző úton kitalálható, de nem mindig triviális, különösen, ha a változók különböző helyekről érkeznek.

… Így a handlerek sokkal kevésbé hasznosak és sokkal problémásabbak, mint amilyennek tűnnek. Ha lehet valamit szépen (bonyodalmak nélkül) megírni handlerek nélkül, akkor érdemes azt tenni. Ha nem sikerül szépen — akkor inkább használj handlerek.

A figyelmes olvasó joggal megjegyzi, hogy nem beszéltünk listen, hogy a handler értesítheti egy másik handler-t, hogy a handler import_tasks-ot is tartalmazhat (amely tartalmazhat include_role-t with_items-el), hogy a handlerek rendszere Ansible-ben Turing-komplett, hogy az include_role-ból származó handlerek érdekesen keresztezik a playbeli handerekkel stb. — mindez egyértelműen nem "alapok".

Bár van egy bizonyos WTF, ami valójában funkció, és amire emlékezni kell. Ha a feladatot végrehajtjuk delegate_to és van értesítése, akkor a megfelelő handler a delegate_to, tehát azon a hoszton, amelyre a play ki van jelölve, fog végrehajtódni. (Bár a handlernek természetesen lehet delegate_to is).

Külön szeretnék pár szót szólni az újrahasználható szerepekről. A gyűjtemények megjelenése előtt az volt az elképzelés, hogy univerzális szerepeket készíthetünk, amiket lehet ansible-galaxy install és elment. Minden operációs rendszeren működik, minden változatban, minden helyzetben. Nos, a véleményem: ez nem működik. Bármilyen szerep, amely tömeg támogatással bír include_vars, 100500 esetben a corner case hibák mélységeire van ítélve. Ezeket lehet masszív teszteléssekkel eltüntetni, de mint bármilyen tesztelésnél, vagy a bemeneti értékek Descartes-szorzatával és totális funkcióval dolgozol, vagy pedig "tudod, hogy lefedted a különböző forgatókönyveket". A véleményem — sokkal jobb, ha a szerep lineáris (ciklomatikus bonyolultság 1).

Minél kevesebb if-fel (akár nyílt, akár deklaratív — változók formájában) when vagy formában include_vars a változók halmazát tekintve), annál jobb a szerep. Néha el kell ágazni, de hangsúlyozom, minél kevesebb, annál jobb. Ezért úgy tűnik, hogy egy jó szerep a galaxy (működik!) sok when kevésbé preferálható lehet, mint a "saját" szerep öt feladattal. Az a pillanat, amikor a galaxy szerep jobb — amikor elkezdesz valamit írni. Az a pillanat, amikor rosszabbá válik — amikor valami elromlik, és gyanakszol, hogy ez a "galaxy szerep" miatt van. Megnyitod, és ott öt include, nyolc feladatlista és egy csomó when‘... Ezen kellene végigmenni. Ahelyett, hogy 5 feladat lineáris listaként, amiben elvileg nincs is mit elrontani.

A következő részekben

  • Kicsit az inventoryról, csoportváltozókról, a host_group_vars pluginnal, a hostvars-ról. Hogyan kössük össze a spagettit Gordiuszi csomóvá. A változók köre és precedenciája, az Ansible memória modellje. "De hol is tároljuk a felhasználónevet az adatbázishoz?"
  • jinja: {{ jinja }} - nosql notype nosense puha gyurma. Mindenhol ott van, még ott is, ahol nem számítasz rá. Kicsit a !!unsafe és ízletes yaml-ról.

Forrás: habr.com

Megbízható hosting vásárlása weboldalak számára, DDoS védelemmel, VPS VDS szerverek. 🔥 Megbízható webtárhely vásárlása DDoS védelemmel, VPS VDS szerverek | ProHoster