Rulu Keycloak en HA-reĝimo sur Kubernetes

Rulu Keycloak en HA-reĝimo sur Kubernetes

TL; DR: estos priskribo de Keycloak, malfermfonta alirkontrolsistemo, analizo de la interna aparato, agordaj detaloj.

Enkonduko kaj ĉefaj ideoj

En ĉi tiu artikolo, ni vidos la ĉefajn ideojn por konservi en menso kiam disfaldi Keycloak-grupon sur Kubernetes.

Se vi volas scii pli pri Keycloak, bonvolu raporti al la ligiloj ĉe la fino de la artikolo. Por pliprofundiĝi en la praktiko, vi povas studi nia deponejo kun modulo, kiu efektivigas la ĉefajn ideojn de ĉi tiu artikolo (la lanĉa gvidilo estas tie, en ĉi tiu artikolo estos superrigardo de la aparato kaj agordoj, ĉ. tradukisto).

Keycloak estas kompleksa sistemo skribita en Java kaj konstruita sur aplikaĵoservilo. Sovaĝa muŝo. Mallonge, ĝi estas rajtigokadro, kiu donas al aplikaĵuzantoj federacion kaj SSO (ununura ensaluto) kapablon.

Ni invitas vin legi la oficialan retejoVikipedio por detala kompreno.

Komencu Keycloak

Keycloak bezonas du konstantajn datumfontojn por funkcii:

  • Datumaro uzata por konservi konstantajn datumojn, kiel informojn pri uzantoj
  • Datagrid-kaŝmemoro, kiu kutimas konservi datumojn de la datumbazo, same kiel por stoki kelkajn mallongdaŭrajn kaj ofte ŝanĝitajn metadatenojn, kiel ekzemple uzantsesioj. Liberigita Infinispan, kiu estas kutime signife pli rapida ol la datumbazo. Sed ĉiukaze, la datumoj konservitaj en Infinispan estas efemeraj - kaj ĝi ne bezonas esti konservita ie kiam la areto estas rekomencita.

Keycloak funkcias en kvar malsamaj reĝimoj:

  • ordinaraj - unu kaj nur unu procezo, agordita per dosiero standalone.xml
  • regula areto (tre disponebla opcio) - Ĉiuj procezoj devas uzi la saman agordon, kiu devas esti sinkronigita permane. Agordoj estas konservitaj en dosiero standalone-ha.xml, krome, vi devas fari komunan aliron al la datumbazo kaj ŝarĝbalancilon.
  • Domajna areto - komenci la areton en normala reĝimo rapide fariĝas rutina kaj enuiga tasko dum la areto kreskas, ĉar ĉiufoje kiam vi ŝanĝas la agordon, vi devas fari ĉiujn ŝanĝojn sur ĉiu nodo de la areto. La reĝimo de funkciado solvas ĉi tiun problemon agordante iom da komuna stokado kaj publikigante la agordon. Ĉi tiuj agordoj estas konservitaj en dosiero domajno.xml
  • Reproduktado inter datumcentroj - se vi volas ruli Keycloak en aro de pluraj datumcentroj, plej ofte en malsamaj geografiaj lokoj. En ĉi tiu opcio, ĉiu datumcentro havos sian propran areton de Keycloak-serviloj.

En ĉi tiu artikolo, ni rigardos pli detale la duan opcion, t.e. regula areto, kaj ankaŭ iom da tuŝo pri la temo de reproduktado inter datumcentroj, ĉar havas sencon ruli ĉi tiujn du opciojn en Kubernetes. Feliĉe Kubernetes ne havas problemon kun sinkronigado de la agordoj de multoblaj podoj (Keycloak-nodoj), do domajna areto ne estos tro malfacile fari.

Ankaŭ bonvolu noti ke la vorto areto ĝis la fino de la artikolo nur validos por grupo de Keycloak-nodoj laborantaj kune, ne necesas raporti al Kubernetes-grupo.

Regula Keycloak Cluster

Por ruli Keycloak en ĉi tiu reĝimo, vi bezonas:

  • starigi eksteran komunan datumbazon
  • instali ŝarĝbalancilon
  • havas internan reton kun ip-multielsenda subteno

Ni ne analizos la agordon de la ekstera datumbazo, ĉar ĝi ne estas la celo de ĉi tiu artikolo. Ni supozu, ke ie estas funkcianta datumbazo - kaj ni havas konekton al ĝi. Ni simple aldonos ĉi tiujn datumojn al la mediaj variabloj.

Por pli bone kompreni kiel Keycloak funkcias en malsukcesa (HA) areto, gravas scii kiom ĉio dependas de la amasigaj kapabloj de Wildfly.

Wildfly uzas plurajn subsistemojn, kelkaj el ili estas uzataj kiel ŝarĝbalancilo, kelkaj estas uzataj por malfunkciigo. La ŝarĝbalancilo certigas la haveblecon de la aplikaĵo kiam la aretnodo estas troŝarĝita, kaj malsukceso certigas la haveblecon de la aplikaĵo eĉ se kelkaj el la aretnodoj malsukcesas. Kelkaj el tiuj subsistemoj estas:

  • mod_cluster: funkcias kune kun Apache kiel HTTP-ŝarĝbalancilo, dependas de TCP-multirolantaro por defaŭlta gastiga malkovro. Povas esti anstataŭigita per ekstera balancilo.

  • infinispan: distribuita kaŝmemoro uzante JGroups-kanalojn kiel transporta tavolo. Laŭvole, ĝi povas uzi la HotRod-protokolon por komuniki kun ekstera Infinispan-grupo por sinkronigi la enhavon de la kaŝmemoro.

  • jgroups: Provizas subtenon por grupa asocio por tre disponeblaj servoj bazitaj sur JGroups-kanaloj. Nomitaj pipoj permesas al aplikaĵkazoj en areto esti ligitaj en grupojn tiel ke la ligo havas trajtojn kiel ekzemple fidindeco, bonordeco kaj fiaskosentemo.

ŝarĝobalancilo

Kiam vi instalas ekvilibron kiel enirregilon en Kubernetes-areo, gravas memori la jenajn aferojn:

La laboro de Keycloak implicas ke la fora adreso de la kliento konektanta per HTTP al la aŭtentikigservilo estas la vera IP-adreso de la klienta komputilo. Balancilo kaj enir-agordoj devus ĝuste agordi HTTP-titolojn X-Forwarded-For и X-Forwarded-Proto, kaj konservu la originalan titolon HOST. lasta versio ingress-nginx (> 0.22.0) malŝaltas ĝin defaŭlte

Aktivigo de flago proxy-address-forwarding per agordo de mediovariablo PROXY_ADDRESS_FORWARDING в true donas al Keycloak la komprenon, ke ĝi funkcias malantaŭ prokurilo.

Vi ankaŭ devas ebligi gluiĝemaj sesioj en eniro. Keycloak uzas la distribuitan kaŝmemoron de Infinispan por stoki datumojn asociitajn kun la nuna aŭtentikiga sesio kaj uzantsesio. Kaŝmemoroj estas ununura posedanto defaŭlte, alivorte tiu speciala sesio estas stokita sur iu aretnodo kaj aliaj nodoj devas peti ĝin malproksime se ili bezonas aliron al tiu sesio.

Specife, male al la dokumentado, kunsido kun la kuketonomo ne funkciis por ni AUTH_SESSION_ID. Keycloak cirkuli la alidirektilon, do ni rekomendas elekti alian kuketan nomon por la glueca sesio.

Keycloak ankaŭ aldonas la nomon de la gastiganto, kiu unue respondis AUTH_SESSION_ID, kaj ĉar ĉiu nodo en la tre disponebla versio uzas la saman datumbazon, ĉiu el ili devas havi aparta kaj unika noda ID por administri transakciojn. Oni rekomendas enmeti JAVA_OPTS parametroj jboss.node.name и jboss.tx.node.id unika por ĉiu nodo - ekzemple, vi povas agordi la nomon de la podo. Se vi metas la nomon de la pod - ne forgesu pri la 23 signolimo por jboss-variabloj, do estas pli bone uzi StatefulSet, ne Deployment.

Unu plia rastilo - se baldo estas forigita aŭ rekomencita, ĝia kaŝmemoro estas perdita. Konsiderante ĉi tion, indas agordi la nombron da kaŝmemorposedantoj por ĉiuj kaŝmemoroj al almenaŭ du, do estos kopio de la kaŝmemoro. La solvo estas kuri manuskripto por Wildfly kiam oni komencas la pod, metante ĝin en la dosierujon /opt/jboss/startup-scripts en ujo:

Skripto enhavo

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

tiam starigu la valoron de la mediovariablo CACHE_OWNERS al la bezonata.

Privata reto kun ip multirolantaro subteno

Se vi uzas Weavenet kiel vian CNI, multielsendo funkcios tuj - kaj viaj Keycloak-nodoj vidos unu la alian tuj kiam ili ekfunkcios.

Se vi ne havas ip-multirolan subtenon en via Kubernetes-areo, vi povas agordi JGroups por labori kun aliaj protokoloj por trovi nodojn.

La unua opcio estas uzi KUBE_DNSkiu uzas headless service por trovi Keycloak-nodojn, vi simple transdonas al JGroups la nomon de la servo, kiu estos uzata por trovi la nodojn.

Alia eblo estas uzi la metodon KUBE_PING, kiu funkcias kun la API por trovi nodojn (vi devas agordi serviceAccount kun rajtoj list и get, kaj poste agordu la podojn por labori kun ĉi tio serviceAccount).

Kiel nodoj estas serĉataj por JGroups estas agordita fiksante mediovariablojn JGROUPS_DISCOVERY_PROTOCOL и JGROUPS_DISCOVERY_PROPERTIES. por KUBE_PING vi devas elekti podojn per demandado namespace и labels.

️ Se vi uzas multielsendon kaj rulas du aŭ pli da Keycloak-grupoj en la sama Kubernetes-areto (ni diru unu en nomspaco production, la dua - staging) - nodoj de unu Keycloak-grupo povas aliĝi al alia areto. Nepre uzu unikan multrolantadreson por ĉiu areto agordante variablojnjboss.default.multicast.address и jboss.modcluster.multicast.address в JAVA_OPTS.

Reproduktado inter datumcentroj

Rulu Keycloak en HA-reĝimo sur Kubernetes

Konektebleco

Keycloak uzas plurajn apartajn Infinispan Cache Clusters por ĉiu datumcentro gastiganta Keycloack-grupojn konsistantajn el Keycloak-nodoj. Sed samtempe, ne ekzistas diferenco inter Keycloak-nodoj en malsamaj datumcentroj.

Keycloak-nodoj uzas eksteran Java Data Grid (Infinispan-serviloj) por komuniki inter datumcentroj. Komunikado funkcias laŭ la protokolo Infinispan HotRod.

Infinispan kaŝmemoroj devas esti agordita kun la atributo remoteStore, tiel ke la datumoj povas esti stokitaj en fora (en alia datumcentro, ĉ. tradukisto) kaŝmemoroj. Estas apartaj infinispan aretoj inter la JDG-serviloj, do datumoj stokitaj sur JDG1 surloke site1 estos reproduktita al JDG2 surloke site2.

Fine, la ricevanta JDG-servilo sciigas la servilojn Keycloak de sia areto per klientkonektoj, kio estas trajto de la HotRod-protokolo. Keycloak nodoj sur site2 ĝisdatigi iliajn Infinispan-kaŝmemorojn kaj la aparta uzantsesio fariĝas havebla sur la Keycloak-nodoj site2.

Ankaŭ eblas ke iuj kaŝmemoroj ne estu sekurkopiitaj kaj tute rifuzi skribi datumojn per la Infinispan-servilo. Por fari tion, vi devas forigi la agordon remote-store specifa Infinispan kaŝmemoro (en dosiero standalone-ha.xml), post kio iuj specifaj replicated-cache ankaŭ ne plu estos bezonata flanke de la Infinispan-servilo.

Agordo de kaŝmemoroj

Estas du specoj de kaŝmemoroj en Keycloak:

  • Loka. Ĝi situas apud la bazo, servas por redukti la ŝarĝon sur la datumbazo, kaj ankaŭ por redukti respondan latentecon. Ĉi tiu speco de kaŝmemoro konservas la regnon, klientojn, rolojn kaj uzantmetadatenojn. Ĉi tiu speco de kaŝmemoro ne estas reproduktita eĉ se ĉi tiu kaŝmemoro estas parto de Keycloak-grupo. Se iu eniro en la kaŝmemoro ŝanĝiĝas, ŝanĝmesaĝo estas sendita al la resto de la serviloj en la areto, post kio la eniro estas ekskludita de la kaŝmemoro. vidu priskribon work sube por pli detala priskribo de la proceduro.

  • Replikebla. Prilaboras uzantsesiojn, eksterretajn ĵetonojn kaj kontrolas ensalutmalsukcesojn por detekti pasvortajn phishing provojn kaj aliajn atakojn. La datumoj stokitaj en ĉi tiuj kaŝmemoroj estas provizoraj, stokitaj nur en RAM, sed povas esti reproduktitaj tra la areto.

Infinispan Kaŝmemoroj

Sesioj - koncepto en Keycloak, apartaj kaŝmemoroj, kiuj estas nomitaj authenticationSessions, estas uzataj por konservi la datumojn de specifaj uzantoj. Petoj de ĉi tiuj kaŝmemoroj kutime bezonas la retumilo kaj Keycloak-serviloj, ne aplikaĵoj. Ĉi tie manifestiĝas la dependeco de gluiĝemaj sesioj, kaj tiaj kaŝmemoroj mem ne bezonas esti reproduktitaj, eĉ en la kazo de Aktiva-Aktiva reĝimo.

Ago-ĵetonoj. Alia koncepto, kutime uzata por diversaj scenaroj, kiam, ekzemple, la uzanto bezonas fari ion nesinkrone per poŝto. Ekzemple, dum la proceduro forget password kaŝaĵo actionTokens uzata por spuri la metadatenojn de rilataj ĵetonoj - ekzemple, la ĵetono jam estis uzata kaj ne povas esti reaktivigita. Ĉi tiu speco de kaŝmemoro devus tipe esti reproduktita inter datencentroj.

Kaŝmemoro kaj eksvalidiĝo de konservitaj datumoj funkcias por forigi la ŝarĝon de la datumbazo. Ĉi tiu kaŝmemoro plibonigas rendimenton sed aldonas evidentan problemon. Se unu Keycloak-servilo ĝisdatigas la datumojn, la ceteraj serviloj devas esti sciigitaj por ke ili povu ĝisdatigi siajn kaŝmemorojn. Keycloak uzas lokajn kaŝmemorojn realms, users и authorization por konservado de datumoj de la datumbazo.

Estas ankaŭ aparta kaŝmemoro work, kiu estas reproduktita tra ĉiuj datencentroj. Ĝi mem ne konservas iujn ajn datumojn de la datumbazo, sed servas por sendi datummaljuniĝajn mesaĝojn al grupaj nodoj inter datencentroj. Alivorte, tuj kiam la datumoj estas ĝisdatigitaj, la nodo Keycloak sendas mesaĝon al aliaj nodoj en sia datumcentro, same kiel nodoj en aliaj datumcentroj. Post ricevo de tia mesaĝo, ĉiu nodo purigas la respondajn datumojn en siaj lokaj kaŝmemoroj.

Uzantaj kunsidoj. Kaŝmemoroj kun nomoj sessions, clientSessions, offlineSessions и offlineClientSessions, estas kutime reproduktitaj inter datencentroj kaj servas por stoki datenojn pri uzantsesioj kiuj estas aktivaj dum la uzanto estas aktiva en la retumilo. Ĉi tiuj kaŝmemoroj funkcias kun la aplikaĵo, kiu pritraktas HTTP-petojn de finaj uzantoj, do ili estas asociitaj kun gluiĝemaj sesioj kaj devas esti reproduktitaj inter datencentroj.

krudforta protekto. Kaŝaĵo loginFailures uzata por spuri datumojn pri ensalutaj eraroj, kiel la nombro da fojoj kiam uzanto enigis malĝustan pasvorton. Reproduktado de ĉi tiu kaŝmemoro dependas de la administranto. Sed por preciza kalkulo, indas aktivigi reproduktadon inter datumcentroj. Sed aliflanke, se vi ne reproduktas ĉi tiujn datumojn, vi povos plibonigi rendimenton, kaj se ĉi tiu demando aperos, reproduktado eble ne estas aktivigita.

Dum lanĉado de Infinispan-grupo, vi devas aldoni kaŝmemordifinojn al la agorda dosiero:

<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" />

Vi devas agordi kaj komenci la Infinispan-grupon antaŭ ol ruli la Keycloak-grupon

Tiam vi devas agordi remoteStore por Keycloak kaŝmemoroj. Por tio sufiĉas skripto, kiu estas farita simile al la antaŭa, kiu estas uzata por agordi la variablon CACHE_OWNERS, vi devas konservi ĝin en dosieron kaj meti ĝin en dosierujon /opt/jboss/startup-scripts:

Skripto enhavo

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 forgesu instali JAVA_OPTS por ke Keycloak-nodoj funkciu HotRod: remote.cache.host, remote.cache.port kaj servonomo jboss.site.name.

Ligiloj kaj plia dokumentaro

La artikolon tradukis kaj preparis por Habr dungitoj Slurm trejncentro - intensaj, videokursoj kaj kompania trejnado de praktikistoj (Kubernetes, DevOps, Docker, Ansible, Ceph, SRE)

fonto: www.habr.com

Aldoni komenton