A Keycloak futtatása HA módban a Kubernetesen

A Keycloak futtatása HA módban a Kubernetesen

TL, DR: lesz a Keycloak, egy nyílt forráskódú beléptető rendszer leírása, a belső struktúra elemzése, a konfiguráció részletei.

Bevezetés és kulcsötletek

Ebben a cikkben látni fogjuk azokat az alapvető ötleteket, amelyeket szem előtt kell tartani, amikor Keycloak-fürtöt telepít a Kubernetes tetejére.

Ha többet szeretne megtudni a Keycloak-ról, tekintse meg a cikk végén található hivatkozásokat. Annak érdekében, hogy jobban elmerüljön a gyakorlatban, tanulhat tárházunk egy modullal, amely megvalósítja a cikk főbb gondolatait (az indítási útmutató ott van, ez a cikk áttekintést nyújt az eszközről és a beállításokról, kb. fordító).

A Keycloak egy Java nyelven írt átfogó rendszer, amely egy alkalmazáskiszolgálóra épül Vadlégy. Röviden, ez egy engedélyezési keretrendszer, amely az alkalmazásfelhasználók összevonási és egyszeri bejelentkezési (SSO) képességeket biztosít.

Meghívjuk Önt, hogy olvassa el a hivatalos weboldal vagy Wikipédia a részletes megértéshez.

A Keycloak elindítása

A Keycloak futtatásához két állandó adatforrás szükséges:

  • Olyan adatbázis, amely a megállapított adatok, például a felhasználói információk tárolására szolgál
  • Datagrid gyorsítótár, amely adatok gyorsítótárazására szolgál az adatbázisból, valamint néhány rövid élettartamú és gyakran megváltozott metaadat, például felhasználói munkamenetek tárolására. Megvalósítva Infinispán, ami általában lényegesen gyorsabb, mint az adatbázis. De mindenesetre az Infinispanban mentett adatok átmenetiek - és a fürt újraindításakor nem kell sehova menteni.

A Keycloak négy különböző módban működik:

  • Normál - egy és egyetlen folyamat, fájlon keresztül konfigurálva standalone.xml
  • Rendszeres klaszter (magas rendelkezésre állás opció) - minden folyamatnak ugyanazt a konfigurációt kell használnia, amelyet manuálisan kell szinkronizálni. A beállítások egy fájlban tárolódnak standalone-ha.xml, ezen kívül meg kell osztani az adatbázishoz való hozzáférést és egy terheléselosztót.
  • Domain klaszter - a fürt normál módban történő indítása gyorsan rutin és unalmas feladattá válik, ahogy a fürt növekszik, mivel a konfiguráció minden egyes módosításakor minden változtatást el kell végezni a fürt minden csomópontján. A tartomány működési módja néhány megosztott tárhely beállításával és a konfiguráció közzétételével oldja meg ezt a problémát. Ezek a beállítások a fájlban tárolódnak domain.xml
  • Replikáció az adatközpontok között — ha a Keycloakot több adatközpontból álló fürtben szeretné futtatni, leggyakrabban különböző földrajzi helyeken. Ebben az opcióban minden adatközpontnak saját Keycloak-kiszolgálók fürtje lesz.

Ebben a cikkben részletesen megvizsgáljuk a második lehetőséget, azaz szabályos klaszter, és egy kicsit érintjük az adatközpontok közötti replikáció témáját is, mivel ezt a két lehetőséget érdemes Kubernetesben futtatni. Szerencsére a Kubernetesben nincs probléma több pod (Keycloak node) beállításainak szinkronizálásával, így domain klaszter Nem lesz túl nehéz megtenni.

Kérjük, vegye figyelembe azt is, hogy a szó fürt mert a cikk további része kizárólag a Keycloak csomópontok együtt dolgozó csoportjára vonatkozik, nincs szükség Kubernetes-fürtre hivatkozni.

Normál Keycloak klaszter

A Keycloak futtatásához ebben a módban a következőkre lesz szüksége:

  • külső megosztott adatbázis konfigurálása
  • telepítse a terheléselosztót
  • van egy belső hálózata IP multicast támogatással

Nem foglalkozunk külső adatbázis létrehozásával, mivel ennek a cikknek nem ez a célja. Tegyük fel, hogy van valahol egy működő adatbázis – és van hozzá kapcsolódási pontunk. Ezeket az adatokat egyszerűen hozzáadjuk a környezeti változókhoz.

Ahhoz, hogy jobban megértsük, hogyan működik a Keycloak egy feladatátvételi (HA) fürtben, fontos tudni, hogy mindez mennyire függ a Wildfly fürtözési képességeitől.

A Wildfly több alrendszert használ, ezek egy része terheléselosztóként, más részük hibatűrésként szolgál. A terheléselosztó biztosítja az alkalmazások rendelkezésre állását, ha egy fürtcsomópont túlterhelt, a hibatűrés pedig akkor is biztosítja az alkalmazás elérhetőségét, ha egyes fürtcsomópontok meghibásodnak. Néhány ilyen alrendszer:

  • mod_cluster: Az Apache-val együtt működik HTTP terheléselosztóként, a TCP csoportos küldéstől függ, hogy alapértelmezés szerint megtalálja a gazdagépeket. Külső egyensúlyozóra cserélhető.

  • infinispan: Elosztott gyorsítótár, amely JGroups csatornákat használ szállítási rétegként. Ezenkívül a HotRod protokoll segítségével kommunikálhat egy külső Infinispan-fürttel a gyorsítótár tartalmának szinkronizálása érdekében.

  • jgroups: Csoportos kommunikációs támogatást biztosít a JGroups csatornákon alapuló, magasan elérhető szolgáltatásokhoz. Az elnevezett csövek lehetővé teszik a fürtben lévő alkalmazáspéldányok csoportokba való kapcsolását, így a kommunikáció olyan tulajdonságokkal rendelkezik, mint a megbízhatóság, a rendezettség és a hibákra való érzékenység.

Terhelés elosztó

Amikor egy kiegyensúlyozót bemeneti vezérlőként telepít egy Kubernetes-fürtbe, fontos szem előtt tartani a következőket:

A Keycloak feltételezi, hogy a HTTP-n keresztül a hitelesítési kiszolgálóhoz csatlakozó ügyfél távoli címe az ügyfélszámítógép valós IP-címe. A kiegyenlítő és bemeneti beállításoknak megfelelően be kell állítaniuk a HTTP-fejléceket X-Forwarded-For и X-Forwarded-Proto, és mentse el az eredeti címet is HOST. Legújabb verzió ingress-nginx (>0.22.0) ezt alapértelmezés szerint letiltja

A zászló aktiválása proxy-address-forwarding környezeti változó beállításával PROXY_ADDRESS_FORWARDING в true megérti a Keycloak-et, hogy proxy mögött dolgozik.

Engedélyeznie is kell ragadós ülések behatoláskor. A Keycloak egy elosztott Infinispan gyorsítótárat használ az aktuális hitelesítési munkamenethez és felhasználói munkamenethez kapcsolódó adatok tárolására. A gyorsítótárak alapértelmezés szerint egyetlen tulajdonossal működnek, vagyis az adott munkamenetet a fürt valamely csomópontja tárolja, és a többi csomópontnak távolról kell lekérdeznie, ha hozzáférésre van szükségük az adott munkamenethez.

Konkrétan a dokumentációval ellentétben nálunk nem működött a cookie névvel ellátott munkamenet csatolása AUTH_SESSION_ID. A Keycloak hurkolta az átirányítást, ezért javasoljuk, hogy válasszon más cookie-nevet a ragadós munkamenethez.

A Keycloak annak a csomópontnak a nevét is csatolja, amelyikre először válaszolt AUTH_SESSION_ID, és mivel a magasan elérhető verzió minden csomópontja ugyanazt az adatbázist használja, mindegyik kellett volna egy különálló és egyedi csomópontazonosító a tranzakciók kezelésére. Ajánlott betenni JAVA_OPTS paraméterek jboss.node.name и jboss.tx.node.id minden csomópont egyedi – megadhatja például a pod nevét. Ha pod nevet ad meg, ne feledkezzen meg a jboss változók 23 karakteres korlátjáról, ezért jobb, ha StatefulSet-et használ, nem pedig Deployment-et.

Újabb rake - ha a pod törlődik vagy újraindul, a gyorsítótár elvész. Ezt szem előtt tartva érdemes az összes cache-hez legalább kettőre állítani a cache tulajdonosok számát, így lesz a cache másolata. A megoldás a futás a Wildfly forgatókönyve a pod indításakor a könyvtárba helyezve /opt/jboss/startup-scripts a tartályban:

Szkript tartalma

embed-server --server-config=standalone-ha.xml --std-out=echo
batch

echo * Setting CACHE_OWNERS to "${env.CACHE_OWNERS}" in all cache-containers

/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:write-attribute(name=owners, value=${env.CACHE_OWNERS:1})
/subsystem=infinispan/cache-container=keycloak/distributed-cache=authenticationSessions:write-attribute(name=owners, value=${env.CACHE_OWNERS:1})
/subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens:write-attribute(name=owners, value=${env.CACHE_OWNERS:1})
/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions:write-attribute(name=owners, value=${env.CACHE_OWNERS:1})
/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions:write-attribute(name=owners, value=${env.CACHE_OWNERS:1})
/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions:write-attribute(name=owners, value=${env.CACHE_OWNERS:1})
/subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures:write-attribute(name=owners, value=${env.CACHE_OWNERS:1})

run-batch
stop-embedded-server

majd állítsa be a környezeti változó értékét CACHE_OWNERS a szükségeshez.

Magánhálózat IP multicast támogatással

Ha a Weavenet CNI-ként használja, a multicast azonnal működni fog – és a Keycloak csomópontjai azonnal látni fogják egymást, amint elindulnak.

Ha nem rendelkezik IP multicast támogatással a Kubernetes-fürtben, beállíthatja a JGroups-ot, hogy más protokollokkal működjön együtt a csomópontok megtalálása érdekében.

Az első lehetőség a használata KUBE_DNSamely felhasználja headless service a Keycloak csomópontok megtalálásához egyszerűen át kell adnia a JGroupsnak a csomópontok megtalálásához használt szolgáltatás nevét.

Egy másik lehetőség a módszer használata KUBE_PING, amely az API-val működik a csomópontok kereséséhez (konfigurálnia kell serviceAccount jogokkal list и get, majd konfigurálja a podokat, hogy ezzel működjenek serviceAccount).

A JGroups csomópontok keresésének módja a környezeti változók beállításával állítható be JGROUPS_DISCOVERY_PROTOCOL и JGROUPS_DISCOVERY_PROPERTIES. For KUBE_PING kérdezéssel kell kiválasztani a hüvelyeket namespace и labels.

️ Ha multicastot használ, és két vagy több Keycloak-fürtöt futtat egy Kubernetes-fürtben (tegyük fel, hogy egyet a névtérben production, a második - staging) - az egyik Keycloak-fürt csomópontjai csatlakozhatnak egy másik fürthöz. Változók beállításával ügyeljen arra, hogy minden fürthöz egyedi csoportos küldési címet használjonjboss.default.multicast.address и jboss.modcluster.multicast.address в JAVA_OPTS.

Replikáció az adatközpontok között

A Keycloak futtatása HA módban a Kubernetesen

Связь

A Keycloak több különálló Infinispan gyorsítótár-fürtöt használ minden egyes adatközponthoz, ahol a Keycloak csomópontokból álló Keycloack-fürtök találhatók. De nincs különbség a Keycloak csomópontok között a különböző adatközpontokban.

A Keycloak csomópontok külső Java Data Grid-et (Infinispan szerverek) használnak az adatközpontok közötti kommunikációhoz. A kommunikáció a protokoll szerint működik Infinispan HotRod.

Az Infinispan gyorsítótárakat az attribútummal kell konfigurálni remoteStore, hogy az adatok távolról is tárolhatók legyenek (egy másik adatközpontban, kb. fordító) gyorsítótárak. Külön infinispan fürtök vannak a JDG szerverek között, így a JDG1-en a helyszínen tárolt adatok site1 JDG2-re replikálják a helyszínen site2.

Végül pedig a fogadó JDG szerver klienskapcsolatokon keresztül értesíti a Keycloak szervereket a fürtjéről, ami a HotRod protokoll egyik jellemzője. Keycloak csomópontok bekapcsolva site2 frissítik az Infinispan gyorsítótárukat, és az adott felhasználói munkamenet elérhetővé válik a Keycloak csomópontokon is site2.

Egyes gyorsítótárak esetében lehetőség van arra is, hogy ne készítsünk biztonsági másolatot, és elkerüljük az adatok írását az Infinispan szerveren keresztül. Ehhez el kell távolítania a beállítást remote-store adott Infinispan gyorsítótár (a fájlban standalone-ha.xml), ami után néhány konkrét replicated-cache szintén nem lesz szükség az Infinispan szerver oldalon.

Gyorsítótárak beállítása

A Keycloakban kétféle gyorsítótár található:

  • Helyi. Az alap mellett található, csökkenti az adatbázis terhelését, valamint csökkenti a válaszadási késleltetést. Az ilyen típusú gyorsítótár a tartományt, az ügyfeleket, a szerepköröket és a felhasználói metaadatokat tárolja. Az ilyen típusú gyorsítótár nem replikálódik, még akkor sem, ha a gyorsítótár egy Keycloak-fürt része. Ha a gyorsítótár egyes bejegyzései módosulnak, a rendszer változásüzenetet küld a fürt többi kiszolgálójának, majd a bejegyzést kizárja a gyorsítótárból. lásd a leírást work Az eljárás részletesebb leírását lásd alább.

  • Replikált. Feldolgozza a felhasználói munkameneteket, az offline tokeneket, és figyeli a bejelentkezési hibákat is, hogy észlelje a jelszavas adathalász kísérleteket és egyéb támadásokat. Az ezekben a gyorsítótárakban tárolt adatok ideiglenesek, csak a RAM-ban tárolódnak, de replikálhatók a fürtben.

Infinispan gyorsítótárak

Munkamenetek - egy koncepció a Keycloak-ben, külön gyorsítótárak hívják authenticationSessions, meghatározott felhasználók adatainak tárolására szolgálnak. Az ezekből a gyorsítótárakból származó kérésekre általában a böngészőnek és a Keycloak-kiszolgálóknak van szükségük, nem pedig az alkalmazásoknak. Itt jön képbe a ragadós munkamenetektől való függés, és magukat az ilyen gyorsítótárakat nem kell replikálni, még Active-Active mód esetén sem.

Akciójelzők. Egy másik fogalom, amelyet általában különféle forgatókönyvekre használnak, amikor például a felhasználónak valamit aszinkron módon kell megtennie levélben. Például az eljárás során forget password cache actionTokens a társított tokenek metaadatainak nyomon követésére szolgál – például egy tokent már használtak, és nem lehet újra aktiválni. Az ilyen típusú gyorsítótárat általában az adatközpontok között kell replikálni.

A tárolt adatok gyorsítótárazása és öregítése úgy működik, hogy tehermentesítse az adatbázist. Ez a fajta gyorsítótárazás javítja a teljesítményt, de nyilvánvaló problémát okoz. Ha az egyik Keycloak-kiszolgáló frissíti az adatokat, a többi szervert értesíteni kell, hogy frissíteni tudják a gyorsítótárukban lévő adatokat. A Keycloak helyi gyorsítótárakat használ realms, users и authorization adatok gyorsítótárazásához az adatbázisból.

Van egy külön gyorsítótár is work, amely az összes adatközpontban replikálódik. Maga nem tárol semmilyen adatot az adatbázisból, hanem arra szolgál, hogy az adatközpontok közötti fürtcsomópontokhoz üzeneteket küldjön az adatok öregedésével kapcsolatban. Más szóval, amint az adatok frissülnek, a Keycloak csomópont üzenetet küld az adatközpontjában lévő többi csomópontnak, valamint más adatközpontok csomópontjainak. Egy ilyen üzenet fogadása után minden csomópont törli a megfelelő adatokat a helyi gyorsítótáraiból.

Felhasználói munkamenetek. Gyorsítótárak nevekkel sessions, clientSessions, offlineSessions и offlineClientSessions, általában adatközpontok között replikálódnak, és adatok tárolására szolgálnak azokról a felhasználói munkamenetekről, amelyek aktívak, amíg a felhasználó aktív a böngészőben. Ezek a gyorsítótárak a végfelhasználók HTTP-kéréseit kezelő alkalmazással működnek, ezért ragadós munkamenetekhez vannak társítva, és replikálni kell őket az adatközpontok között.

Brute force védelem. Gyorsítótár loginFailures A bejelentkezési hibaadatok nyomon követésére szolgál, például, hogy a felhasználó hányszor adott be helytelen jelszót. A gyorsítótár replikálása az adminisztrátor felelőssége. De a pontos számításhoz érdemes aktiválni az adatközpontok közötti replikációt. Másrészről azonban, ha nem replikálja ezeket az adatokat, javítja a teljesítményt, és ha ez a probléma felmerül, előfordulhat, hogy a replikáció nem aktiválódik.

Az Infinispan-fürt bevezetésekor gyorsítótár-definíciókat kell hozzáadnia a beállításfájlhoz:

<replicated-cache-configuration name="keycloak-sessions" mode="ASYNC" start="EAGER" batching="false">
</replicated-cache-configuration>

<replicated-cache name="work" configuration="keycloak-sessions" />
<replicated-cache name="sessions" configuration="keycloak-sessions" />
<replicated-cache name="offlineSessions" configuration="keycloak-sessions" />
<replicated-cache name="actionTokens" configuration="keycloak-sessions" />
<replicated-cache name="loginFailures" configuration="keycloak-sessions" />
<replicated-cache name="clientSessions" configuration="keycloak-sessions" />
<replicated-cache name="offlineClientSessions" configuration="keycloak-sessions" />

A Keycloak-fürt elindítása előtt konfigurálnia és el kell indítania az Infinispan-fürtöt

Ezután konfigurálni kell remoteStore Keycloak gyorsítótárak számára. Ehhez elég egy script, ami az előzőhöz hasonlóan történik, ami a változó beállítására szolgál. CACHE_OWNERS, el kell mentenie egy fájlba, és el kell helyeznie egy könyvtárba /opt/jboss/startup-scripts:

Szkript tartalma

embed-server --server-config=standalone-ha.xml --std-out=echo
batch

echo *** Update infinispan subsystem ***
/subsystem=infinispan/cache-container=keycloak:write-attribute(name=module, value=org.keycloak.keycloak-model-infinispan)

echo ** Add remote socket binding to infinispan server **
/socket-binding-group=standard-sockets/remote-destination-outbound-socket-binding=remote-cache:add(host=${remote.cache.host:localhost}, port=${remote.cache.port:11222})

echo ** Update replicated-cache work element **
/subsystem=infinispan/cache-container=keycloak/replicated-cache=work/store=remote:add( 
    passivation=false, 
    fetch-state=false, 
    purge=false, 
    preload=false, 
    shared=true, 
    remote-servers=["remote-cache"], 
    cache=work, 
    properties={ 
        rawValues=true, 
        marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, 
        protocolVersion=${keycloak.connectionsInfinispan.hotrodProtocolVersion} 
    } 
)

/subsystem=infinispan/cache-container=keycloak/replicated-cache=work:write-attribute(name=statistics-enabled,value=true)

echo ** Update distributed-cache sessions element **
/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions/store=remote:add( 
    passivation=false, 
    fetch-state=false, 
    purge=false, 
    preload=false, 
    shared=true, 
    remote-servers=["remote-cache"], 
    cache=sessions, 
    properties={ 
        rawValues=true, 
        marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, 
        protocolVersion=${keycloak.connectionsInfinispan.hotrodProtocolVersion} 
    } 
)
/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:write-attribute(name=statistics-enabled,value=true)

echo ** Update distributed-cache offlineSessions element **
/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions/store=remote:add( 
    passivation=false, 
    fetch-state=false, 
    purge=false, 
    preload=false, 
    shared=true, 
    remote-servers=["remote-cache"], 
    cache=offlineSessions, 
    properties={ 
        rawValues=true, 
        marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, 
        protocolVersion=${keycloak.connectionsInfinispan.hotrodProtocolVersion} 
    } 
)
/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions:write-attribute(name=statistics-enabled,value=true)

echo ** Update distributed-cache clientSessions element **
/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions/store=remote:add( 
    passivation=false, 
    fetch-state=false, 
    purge=false, 
    preload=false, 
    shared=true, 
    remote-servers=["remote-cache"], 
    cache=clientSessions, 
    properties={ 
        rawValues=true, 
        marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, 
        protocolVersion=${keycloak.connectionsInfinispan.hotrodProtocolVersion} 
    } 
)
/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions:write-attribute(name=statistics-enabled,value=true)

echo ** Update distributed-cache offlineClientSessions element **
/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions/store=remote:add( 
    passivation=false, 
    fetch-state=false, 
    purge=false, 
    preload=false, 
    shared=true, 
    remote-servers=["remote-cache"], 
    cache=offlineClientSessions, 
    properties={ 
        rawValues=true, 
        marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, 
        protocolVersion=${keycloak.connectionsInfinispan.hotrodProtocolVersion} 
    } 
)
/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions:write-attribute(name=statistics-enabled,value=true)

echo ** Update distributed-cache loginFailures element **
/subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures/store=remote:add( 
    passivation=false, 
    fetch-state=false, 
    purge=false, 
    preload=false, 
    shared=true, 
    remote-servers=["remote-cache"], 
    cache=loginFailures, 
    properties={ 
        rawValues=true, 
        marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, 
        protocolVersion=${keycloak.connectionsInfinispan.hotrodProtocolVersion} 
    } 
)
/subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures:write-attribute(name=statistics-enabled,value=true)

echo ** Update distributed-cache actionTokens element **
/subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens/store=remote:add( 
    passivation=false, 
    fetch-state=false, 
    purge=false, 
    preload=false, 
    shared=true, 
    cache=actionTokens, 
    remote-servers=["remote-cache"], 
    properties={ 
        rawValues=true, 
        marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, 
        protocolVersion=${keycloak.connectionsInfinispan.hotrodProtocolVersion} 
    } 
)
/subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens:write-attribute(name=statistics-enabled,value=true)

echo ** Update distributed-cache authenticationSessions element **
/subsystem=infinispan/cache-container=keycloak/distributed-cache=authenticationSessions:write-attribute(name=statistics-enabled,value=true)

echo *** Update undertow subsystem ***
/subsystem=undertow/server=default-server/http-listener=default:write-attribute(name=proxy-address-forwarding,value=true)

run-batch
stop-embedded-server

Ne felejtse el telepíteni JAVA_OPTS a Keycloak csomópontokhoz a HotRod futtatásához: remote.cache.host, remote.cache.port és a szolgáltatás neve jboss.site.name.

Linkek és kiegészítő dokumentáció

A cikket az alkalmazottak fordították le és készítették el a Habr számára Slurm edzőközpont - intenzív tanfolyamok, videó tanfolyamok és vállalati képzés gyakorló szakemberektől (Kubernetes, DevOps, Docker, Ansible, Ceph, SRE)

Forrás: will.com

Hozzászólás