Co si myslíte o společném WC na chodbě v hotelu? A doma v rámci rodiny? Budujete svou kontejnerovou strategii v cloudu a váháte kolik mít clusterů a jak s multi-tenancy? Tohle téma je velké a docela složité. Pojďme si v tom dnes zkusit udělat nějaký myšlenkový pořádek a v následujících dílech se zaměřit na technologie umožňující jak multi-tenanci v AKS tak efektivní správu velkého množství single-tenant AKS (a samozřejmě i kombinaci obojího - což je to, kam většinou doiterujete).
Máte, tak jako já, firemní kanceláře ve stylu otevřeného prostoru bez jmenovek na židlích? Sednete si kde je zrovna volno, pijete kávu z hrnečku, z kterého ráno pil někdo jiný a na toaletě si nezpíváte, protože o dveře vedle je možná kolega. Oproti tomu pokud třeba máte domácí kancelář v rodinném domku, tak stejně jako váše pubertální děcko máte na dveřích cedulku nerušit a všechno je jen vaše, zatímco jiné části domu sdílíte jen s blízkou rodinou. No a pak je spousta variant mezi tím - bytový dům, kde sdílíte střechu a výtah, sociální bydlení, kde máte společnou sprchu nebo třeba ubytovna, kde sdílíte skoro všechny prostory, jen postel máte svojí.
To všechno je spektrum mezi single a multi tenant řešením a druhá část analogie je, co bude ten tenant. Pokud jsou to vaši rodinní příslušníci, je určitě snazší s nimi sdílet koupelnu, než když jste na dovolené v hotelu a koupelna na chodbě je využívána i úplně cizími lidmi. Sedět u jedné pracovní desky s ostatními zaměstnanci firmy Coca-cola je určitě menší problém, než mezi sebou střídat i lidi z Pepsi.
Co všechno je sdílené, jak moc je to “multi-tenant”, to jeden aspekt diskuse. Je rozdíl mít pro sebe celý cluster, kde mám na všechno dedikovanou kapacitu a sám si všechno řídím (něco jako domek) vs. mít izolované prostředky v rámci sdíleného clusteru (něco jako vlastní byt, ale sdílené vytápění, střecha či vchodové dveře) nebo se v tělocvičně na karimatce snažit uhájit nějaký prostor alespoň na spánek (něco jako Pod mezi ostatními na stejném nodu) a doufat, že vám někdo v noci neukradne doklady (ono se to tam hlídá, ale znáte to - všichni na jednom prostoru, nějaké riziko to je).
S kým jste ochotni něco sdílet je ten druhý rozměr. V rámci vašeho týmu co provozuje nějakou aplikaci a její komponenty nebude problém se dohodnout (rodina v jednom domě), s nějakými opatřeními zvládnete možná i jiné podobné týmy (snacha, strýc nebo babička ve vašem domku), ale budete asi chtít větší oddělenost pokud jde o zcela odlišné týmy patřící do stejné organizační složky či bezpečnostní skupiny (třeba ostatní neutrální účastníci kulečníkového turnaje) a ještě větší pokud to může být kdokoli (lidé, které vyloženě nemáte rádi a mohou vám škodit nebo se jim nedá věřit).
Zásadní výhodou multi-tenance jsou výnosy z rozsahu. Nějaká forma výnosu je ten důvod, proč přenecháváte výrobu chleba zemědělcům a pekařům, nestavíte si své vlastní auto a IT infrastrukturu a platformní služby využijete u cloudových poskytovatelů. Typicky půjde o snížení nákladů díky sdílení prostředků a jejich větší výtěžnosti, sdílení správy a s tím spojené nižší operační náklady a méně lidských odborníků na jednotku výkonu. Proti tomu jdou ale otázky bezpečnostní (jsou od sebe aplikace dostatečně oddělené), organizační (některé vlastnosti vyžadují kordinovaný postup - například verze Kubernetu, instalace CRD, OIDC provider, verze nadstavbových komponent pokud se je rozhodnete sdílet, typ síťařiny a důsledky jeho výběru), provozní (výkonnostní ovlivnění aplikací mezi sebou, efekt hlučného souseda) a z pohledu FinOps (jak spravedlivě měřit náklady, rozpočítávat sdílené komponenty a nevyužité zdroje v kontextu fragmentačních a škálovacích problémů nebo neférového jednání uživatelů podstřelujících requesty za účelem snížení ceny hrající na to, že v clusteru bude dostatek burstovatelné nabídky).
Data plane je o samotných zdrojích, jejich řízení a míře izolace. Obecně vzato půjde primárně o compute, storage a networking.
Pody v Kubernetu využívají v základu sdílený kernel a jejich oddělenost je tak řešena softwarovými prostředky.
Cgroups dokáží dobře řešit rozdělení zdrojů CPU a RAM, ale nativně neumí hospodařit s některými zdroji v kernelu a ty mohou být dost zásadní. Pokud tyto zdroje některý z Podů vyčerpá, nebudou dostupné pro ostatní - dojde tedy k omezení jejich provozu, ovlivnění - porušení principu izolace z pohledu dostupnosti prostředků. Kubernetes limits ve verzi 1.26 například do stable přidalo podporu omezení hugepages nebo, byť ne nastavitelné per Pod ale jen na Node, omezit množství PID v každém Podu (podMaxPids). Ostatní limity jsou ale nastavitelné jen per-node, tedy jsou principiálně sdílené, protože sdílíte kernel. Například fswatchers (ulimit) a další souborové limity jako je aio-nr nebo nr_open, worker thread limity, nf conntrack limity, socket buffery, swappiness a další vlastnosti virtuální paměti.
Co izolace z pohledu bezpečnosti? Už samotný Kubernetes použije prostředky pro to, aby se kontejnery neviděly a nemohly se zmocnit nodu a k tomu používá technologie jako chroot nebo network namespace (kontejner má virtuální síťovku, která je do hostu napojena přes veth pair) a některé další. Druhým krokem je zpřísnění seznamu povolených systémových volání technologií seccomp, kde ve výchozím stavu je Docker o něco přísnější, než Kubernetes. To znamená že pro zvýšení izolace je vhodné používat explicitně seccomp profily a vynutit jejich přítomnost přes nějakou politiku (například AKS má Azure Policy s OPA pod kapotou). Nově v Kubernetes 1.25 přišla do beta podpora SeccompDefault, takže jste schopni to už řídit lépe. Pokračováním této cesty pak můžou být hrátky s AppArmor nebo SELinux nejlépe odladěné pro každou aplikaci na least privilege - a tady upřímně řečeno myslím, že to neškáluje a je to neúměrně složité. Jedna věc je nějaká default restrikce (třeba OpenShift mívá default přísnější, než AKS, ale to si dnes můžete nastavit), ale vytváření profilů per aplikace a jejich udržení v čase je myslím pro většinu situací nereálné. Samozřejmě bychom měli zmínit, že kromě restrikcí taky můžete spustit kontejner v privilegovaném režimu nebo mu explicitně přidat nějaké Linux capabilities a s tím je potřeba hodně opatrnosti. Kontejner tak defacto může vlastnit node a pokud ho útočník zneužije, izolace je pryč. Nezapomeňme, že útočník nemusí nutně prolamovat kontejner, když se mu podaří třeba kompromitovat CI/CD pipeline a nasadit se privilegovaně nebo trikem přimět administrátora k chybě (proto je důležité mít i preventivní politiky a detekci - ale to je na jiný článek).
Podle míry potřebné izolace jak z pohledu přidělování prostředků tak bezpečnosti vám sdílený kernel pro určité třídy aplikací prostě nemusí stačit nebo je použití zpřísněných nastavení nevhodné z pohledu správy a vývoje. Pak uvažujme o variantě, kdy kernel není sdílený:
Prostředky kernelu (od chroot až po AppArmor/SELinux profily) představují různé formy izolace bytových jednotek od papírových stěn v japonském stylu až po bezpečnostní dveře, tlusté zdi a mříže. Separátní nodepool je jiný dům pro různé třídy aplikací a Kata containers jsou pořád ve stejném domě se sdíleným topením a jinými energiemi, ale každý byt má vchod přímo z ulice, takže se sousedy se nepotkáváte. Možná nepřesné analogie, ale tak zhruba pro představu.
Sdílení storage považuji za velmi problematické, protože Kubernetes nemá defacto žádné schopnosti to řídit. Velmi často pak vidím, jak někdo použije emptyDir pro všechny aplikace (což směřuje na jediný disk a tím je by default disk nodu) a jedna aplikace vyžere všechno (třeba veškeré IO) a je to velký problém. Na druhou stranu - na rozdíl od compute nejsou nody něco co Kubernetes z pohledu data plane zajišťuje (není žadný “Kubelet”, který by běžel ve vaší storage), takže problém řezání storage můžete přenechat něčemu venku - třeba Azure a jeho diskům, sharům a blobům, které na rozdíl třeba od nějakého monstrózního NFS prostředky krásně oddělují. Izolace je tedy na úrovni založení různých storageClass s různými parametry a v tom, že storage z pohledu Kubernetu nikdy nesdílíte. Nicméně i přesto jsou určité limity na úrovni nodu, které musíme brát v úvahu - celkové storage IO směrem k diskům, počet připojených disků a v případě share a blob jste omezeni celkovou propustností síťovky nodu. Něco z toho se dá trochu řídit kvótou (počty PVC ve třídě viz dále), ale takové storage IO per node víceméně nevyřešíte.
Takže jako v předchozích případech mluvíme o škále. Pokud používáte emptyDir, je to tělocvična, ve které se snažíte obhájit alespoň postel. Pokud využijete storage class a storage v cloudu, pak má každý svůj vlastní byt. Vchodové dveře ale společné, pokud se ucpou, do bytu se nedostanete. S kým jste ochotni sdílet spaní v tělocvičně? S kým jste schopni žít v bytovém domě? Pokud si Pepsi s Coca-cola nevěří, možná bude lepší, ať mají oddělené bytové domy a nepotkávají se u vchodu (separátní nodepooly).
Pokud neřeknete jinak, všechny Pody clusteru na sebe vidí - a to i napříč namespace. Typicky tedy nasadíte Network Policy pro omezení komunikace a izolaci na síťové úrovni. Další možností je aplikovat L7 pravidla na úrovni service mesh (Open Service Mesh v případě AKS) nebo aplikační platformy (DAPR), nicméně za mě tohle jsou cesty s větším dopadem na provoz a náklady, takže filtrování v síti by nemělo být jejich hlavním důvodem k nasazení (jděte po tom, co je jednoduché - tedy network policy - pokud nemáte echt důvody to udělat jinak). Problematika QoS v síti je řekněme neřešena co se cloudu týče - nody mají typicky pásma víc než dost, cloudová síť stejně žádné QoS nectí a Kubernetes nemá nějaký maturovaný model pro traffic shaping (bandwidth CNI plugin existuje, ale reálně jsem ho neviděl v žádném cloudu nasazen). Samozřejmě něco jiného to je u L7 nadstaveb, takže můžete dělat různé rate-limit policy třeba na API managementu nebo nějakém chytrém ingressu.
Častá otázka jsou odchozí IP a za mě pokud můžete, ať to prosím nehraje roli - v cloud-native světě by neměla být IP adresa nikde důležitá. Nicméně pokud to musíte řešit, tak na úrovni sdíleného prostředí je to velmi komplikované (použití třeba různých egress gateway na různých nodepoolech, ale je to dost obskurní), takže bych zvážil spíš separátní nodepooly v odlišných subnetech, čehož se pak pak můžete chytnout na firewallu a podle toho NATovat.
Co se týče in fly šifrování, tak to je typicky na bedrech aplikace, ingressu, API managementu, nebo nějaké multi-kulti service mesh či service discovery (třeba Consul Connect). Izolace tady bude daná především certifikáty a klíči a to lze řešit separátně pro každý typ nájemce.
Problematika at rest musí vycházet z toho, že jednotlivým nájemcům vytvoříte jejich vlastní storageClass namapovanou na jejich vlastní diskEncryptionSet, čímž bude každý šifrovat svým vlastním klíčem. Nicméně node samotný je, pokud chcete, také šifrován a tam samozřejmě jen klíčem jediným.
Tím se dostáváme k nejnovější kategorii šifrování in use, tedy Confidential Computing. Jde o šifrování dat v paměti, takže ani pokud by administrátor serveru udělal dump, žádná data tam nenajde, protože k jejich rozšifrování dochází až při zpracování v bezpečné enklávě uvnitř procesoru (Trusted Execution Environment). Pokud něco takového chcete, lze to jednoduše získat použitím Azure Confidential VM v Azure Kubernetes Service a pak je obsah celé paměti nodu šifrován. Tím se bráníte před cloudovým poskytovatelem (Microsoft nejen že má provozní opatření zabraňující koukat do vašich dat, ale s tímto režimem toho ani není technicky schopen i kdyby si vykopíroval obsah paměti nodu), ale ne před admin přístupem do nodu (pokud vám někdo vyskočí z kontejneru nebo se spustí privilegovaně, může získat obsah paměti ostatních kontejnerů). Pokud byste chtěli jít o krok dál, můžete použít Azure Confidential Containers, kdy každý Pod běží jako samostatný process v bezpečné enklávě. Dokážete tak běžet supertajnou aplikaci nejen v cloudu, kterému nevěříte (na to vám stačí Confidential VM), ale i v situaci, kdy nevěříte vlastním administrátorům. Ani root access do nodu neumožňuje provést dump paměti aplikace v rozšifrovaném stavu. Nicméně Confidential Containers znamenají do značné míry předělat aplikace nebo minimálně jejich build.
V rámci celého clusteru jsou některá rozhodnutí a některé prostředky sdílené a vyžadují tak koordinaci uživatelů. Tady je pár příkladů:
Určitě bychom toho našli víc. Zkrátka jsou některé vlastnosti, které jsou per-cluster. Jiné ale řešit lze - přístupy uživatelů, rozdělení control plane do škatulek (namespace a jeho potenciální nástupci), řízení kvót a politiky.
Co je v Kubernetu zpracováno velmi dobře je RBAC model a skutečně není problém přesně řídit k čemu má mít jaký uživatel přístup. Díky integraci AKS s Azure Active Directory získáváte věci jako je single-sign-on nebo vícefaktorové ověření. Pokud budete RBAC řídit přes Azure RBAC tak dokonce i Privileged Access Management (možnost eskalačních procedur na oprávnění, takže skutečně vývojář má pouze čtecí přístup do clusteru, nicméně je schopen si zažádat o zvýšené oprávnění v případě krize a to je zaprotokolováno případně schváleno bezpečností). Přiřazování základních RBAC rolí k namespace (viz dále) považuji za dost jasné a doporučené.
Trochu horší je to s vytvářením vlastních granulárních rolí, kde se myslím dá velmi rychle zamotat a spíš věci uškodit, než jí pomoci. Tak například můžeme si určitě hrát s laděním toho, že ten kdo nasazuje aplikaci nevidí Secret. Nojo, jenže jeho kontejner ano, takže do něj skočí a vypíše si env nebo obsah souboru? Tak mu to zakážeme, fajn (a znesnadníme troubleshooting). Co když ale upraví aplikaci tak, že secret zapíše do logu? Zakážeme přístup do logu? V rámci kubectl ok, ale asi logujete ještě někam jinam třeba do Azure Monitor. Asi už tušíte kam s tím směřuji - granulární ladění je složité a má dost postranních efektů a za mě bych co nejvíc potřeb namířil do oblastí admin clusteru, admin v namespace a reader v namespace a spíš si dobře vyladil AAD, MFA, PIM, používal co nejméně hesla (password-less autentizace uživatelů nebo MFA, workload identity federation pro přístup ke cloudovým prostředkům typu storage nebo databáze) a měl audit monitoring.
Základním konstruktem řezání control plane je namespace a ten jednak umožňuje vyřešit problém jmen prostředků (což je z názvu jasné), ale funguje i jako schránka pro přiřazení RBAC, kvót nebo různých dalších omezení jako jsou síťové politiky (např. network policy umožňující komunikaci aplikačních namespace pouze se sdíleným namespace API managementu, přes který se otáčí komunikace mezi aplikacemi i komunikace zvenku) nebo přiřazení na nodepooly (můžete asociovat namespace s nodepool a efektivně tak propojit izolaci na compute úrovni s izolací v control plane).
Často dává ale smysl, aby jeden tým využíval vícero namespace pro zjednodušení práce, přehlednost a tak podobně. Protože nejsou cluster admin nemůžou něco takového udělat. Osobně preferuji tohle řešit kvalitním Infrastructure as Code, kdy na požádání týmu jim automaticky přidáte další namespace, umožníte komunikaci všech jejich namespace mezi sebou (network policy), dáte přístupy, nastavíte ke stejnému nodepoolu apod. Druhá cesta může být použití hierarchických namespace, což není nativní vlastnost Kubernetu, ale nadstavba, kterou si můžete doinstalovat. Ta v zásadě zajišťuje řekněme rozkopírovávání práv a dalších objektů a vzniká tak dojem, že tým dostane svůj namespace a v rámci něj si mohou sami vytvářet další.
Nicméně ani to neřeší třeba sdílená CRD a tak existuje ještě extrémnější projekt vClusterů. Myšlenka je taková, že do Kubernetes clusteru nasadíte svůj vlastní dedikovaný API server (tedy defacto jiný Kubernetes cluster), který ale nespravuje vlastní nody, ale spuští Pody v hostitelském Kubernetu (je to tedy taková kombinace Virtual Kubelet, kdy se simuluje Kubernetes node a k tomu kompletní control plane API serveru). To vypadá docela zajímavě, ale z provozního hlediska mě to dost děsí. Je to přidaná složitost a místo managed API serveru od poskytovatele (třeba Azure Kubernetes Service) jste tak vlastně v režimu self-managed Kubernetu a to mi nepřipadá dobrý nápad.
Když jsme mluvili o data plane, řešili jsme nastavení některých limitů třeba na CPU nebo paměť na Podech. Na úrovni namespace, tedy v rámci control plane, jste schopni omezit kolik zdrojů si takhle jeho uživatelé mohou dohromady vzít.
Kromě namespace lze v kvotách rozlišovat scope jako je například priority class. To umožňuje ještě jemnější členění.
Pokud se cluster dostane na hranici své kapacity, budete pravděpodobně mít nastavený cluster autoscaler, který přitočí. Nicméně to pár minut potrvá a tak může dávat smysl některé Pody prioritizovat na úkor jiných (nechávám teď stranou problematiku vysoké dostupnosti kdy při havárii celé availability zone nemáte garanci, že stejnou kapacitu prostředků dostanete v jiných zónách, protože pokud je AZ dole, všichni ostatní taky vytáčí - takže prioritizace využití kapacity je důležitá i z toho pohledu). Třeba ty zajišťující systémové úkoly, ingress nebo bezpečnost ať jsou nesestřelitelné. Aplikační mohou být rozdělené do těch, co jsou synchronní a v reálném čase odpovídají uživateli, ale pak jsou tu takové, které pracují asynchronně (odesílají nějaké notifikace, dávkově zpracovávají data apod.) a často bude dávat smysl je sestřelit, pokud je potřeba víc Podů pro ty synchronní.
Za mě tohle je další rozměr sdílení prostředků v clusteru. Můžete nastavit nižší request než limit a umožnit tak bursting konkrétnímu Podu (udělat overcommit). Nicméně další možnost je overcommit použít pouze mírný nebo žádný a soustředit se na scale-out - přidávat kontejnery dle potřeby třeba s HPA nebo KEDA. Díky prioritám Podů pak budete schopni řídit co ty zdroje dostane přednostně a co si musí počkat, než se cluster přifoukne. Za mě je tento druhý model víc “cloud-native”, ale samozřejmě nic není černobíle a nějaká kombinace tak bude nejčastější. Dělejte requesty poctivě tak, aby to skutečně bylo minimum co aplikace potřebuje k rozumnému běhu (ne méně). Bursting berte jako vhodný bonus pro překonání krátké špičky zejména pokud jde o nějakou jednotlivou součástku, ale cokoli většího by mělo systém rozhýbat - KEDA nebo HPA přidat Pody, dle priorit krátkodobě na úkor nedůležitých věcí nafouknout kritické komponenty zatímco se na pozadí přifukuje cluster, aby bylo prostředků víc.
Někdy můžete mít potřebu řídit věci v clusteru způsobem, který není zabudovaný do chování Kubernetu jako takového. Ten ale dovoluje integrace přes admission kontrolery jako je Open Policy Agent, které vám pak umožní vynutit další specifická chování v multi-tenant clusteru. Kontrolovat nebo přidávat atributy Podů (třeba povinné labely), zakazovat některé konfigurace (privilegované kontejnery, použití hostPath apod.), omezovat doménová jména v ingressu per namespace nebo implementovat nějakou další logiku.
No jo, ptáte se, ale co Ingress, CoreDNS, Cert-manager, ExternalDNS, KEDA, DAPR, OpenServiceMesh, ArgoCD a hromada dalších komponent co mám nad tím? Pro ty vlastně platí totéž a každá se tak defacto musí posoudit ve stejných intencích samostatně. Je problém mít jeden sdílený Ingress? Jsou tam nějaké potíže s verzováním a upgrady (nové funkce přidané anotacemi například nebo rozpady session při upgradu)? Mohou aplikace sdílet dataplane z bezpečnostního hlediska, když se mi v tom kontejneru třeba sejde jejich traffic nebo jejich certifikáty (pokud se útočník dostane do těchto kontejnerů jak moc toho uvidí a ovlivní)? Mám pokryté provozní riziko, aby zátěž na jednu aplikaci neshodila ty ostatní (chráním se před DoS/DDoS, autoškáluju dostatečně rychle)? Mohu tolerovat stejnou IP a TLS SNI pro všechny provozované aplikace nebo potřebuji nějaké bezpečnostní zóny, různé IP apod.? Podle toho musím rozhodnout pro koho bude tato komponenta sdílená a kde bude separovaná.
Řízení nákladů je samostatnou kapitolou a podstatné je, že Azure náklady Kubernetu budou primárně drivované spotřebou v data plane. Pokud mám samostatné clustery nebo samostatné nodepooly pro svoje finanční potřeby, tak se to dá udělat snadno přes tagy a je to velmi spravedlivé - sdílených “drahých” položek moc nebude (Standard SKU aka SLA pro cluster nebo možná u velkého clusteru dedikovananý system-pool na věci typu CoreDNS a Ingress a ani jedno nebude nějaký velký peněžní objem) a s těmi přímo Azure Cost Managed dokáže při správném tagování automaticky nakládat (rozpočítávat). Jakmile chci ale účtovat za věci sdílené v rámci jednoho nodu, situace se značně (jakože hodně značně) komplikuje - mrkněte na článek, kde jsem to popsal: Azure, Kubernetes, FinOps a strategie účtování nákladů.
Tolik tedy k takovému přehledu témat pro multi-tenant clustery. Zní to složitě? Ono docela je a se single-tenant clustery většinu z toho řešit nemusíte, ale zas vás čeká přemýšlení o efektivní správě takového řešení a potenciálně nižší výtěžnost “železa”. Na všechno se v dalších článcích podíváme - na správu velkého množství clusterů, na jednotlivé metody izolace v data-plane i nastíněné vychytávky v control-plane.