
TL;DR: wird die Beschreibung von Keycloak, einem Open-Source-Zugriffskontrollsystem, die interne Struktur und Details zur Konfiguration umfassen.
Einführung und grundlegende Ideen
In diesem Artikel werden wir die grundlegenden Ideen sehen, die man beim Bereitstellen eines Keycloak-Clusters auf Kubernetes beachten sollte.
Wenn Sie mehr über Keycloak erfahren möchten, werfen Sie einen Blick auf die Links am Ende des Artikels. Um tiefer in die Praxis einzutauchen, können Sie unser mit dem Modul studieren, das die grundlegenden Ideen dieses Artikels umsetzt (eine Anleitung zur Implementierung finden Sie auch dort; in diesem Artikel wird eine Übersicht über die Struktur und Konfigurationen gegeben, Anm. des Übersetzers).
Keycloak ist ein umfassendes System, das in Java geschrieben und auf dem Anwendungsserver aufgebaut ist. Kurz gesagt, es ist ein Authorisierungs-Framework, das den Benutzern von Anwendungen Föderierung und die Möglichkeit von SSO (Single Sign-On) bietet.
Wir laden Sie ein, die offizielle oder für eine detaillierte Verständnis zu besuchen.
Start von Keycloak
Für Keycloak sind zwei ständig gespeicherte Datenquellen für den Betrieb erforderlich:
- Eine Datenbank, die zur Speicherung fester Daten verwendet wird, z.B. Informationen über Benutzer
- Datagrid-Cache, der zum Caching von Daten aus der Datenbank verwendet wird, sowie zum Speichern bestimmter kurzlebiger und häufig geänderter Metadaten, wie z.B. Benutzersitzungen. Dies wird durch , das in der Regel wesentlich schneller ist als die Datenbank, realisiert. Aber in jedem Fall sind die in Infinispan gespeicherten Daten ephemeral — und sie müssen beim Neustart des Clusters nicht irgendwo gespeichert werden.
Keycloak arbeitet in vier verschiedenen Modi:
- Normal — ein und nur ein Prozess, konfiguriert über die Datei standalone.xml
- Ein normaler Cluster (hochsichere Option) — alle Prozesse müssen dieselbe Konfiguration verwenden, die manuell synchronisiert werden muss. Die Einstellungen werden in der Datei standalone-ha.xml, zusätzlich muss ein gemeinsamer Datenbankzugang und ein Lastenausgleich eingerichtet werden.
- Domänencluster — das Starten eines Clusters im normalen Modus wird schnell zu einer routinemäßigen und langweiligen Aufgabe, wenn das Cluster wächst, da bei jeder Änderung der Konfiguration alle Änderungen an jedem Knoten des Clusters vorgenommen werden müssen. Der Betriebsmodus der Domäne löst dieses Problem, indem er einen gemeinsamen Speicherort einrichtet und die Konfiguration veröffentlicht. Diese Einstellungen werden in der Datei domain.xml
- gespeichert. Replication zwischen Rechenzentren — im Falle, dass Sie Keycloak in einem Cluster aus mehreren Rechenzentren starten möchten, meistens an geografisch unterschiedlichen Orten. In diesem Arbeitsmodus wird jedes Rechenzentrum seinen eigenen Keycloak-Servercluster haben.
In diesem Artikel werden wir die zweite Variante im Detail betrachten, nämlich den regulären Cluster, und außerdem werden wir das Thema der Replikation zwischen den Rechenzentren kurz anschneiden, da es sinnvoll ist, diese beiden Varianten in Kubernetes zu starten. Glücklicherweise gibt es in Kubernetes kein Problem mit der Synchronisierung der Einstellungen mehrerer Pods (Keycloak-Knoten), sodass ein Domänencluster nicht besonders schwierig umzusetzen sein wird.
Bitte beachten Sie auch, dass das Wort Cluster im Verlauf des Artikels ausschließlich auf eine Gruppe von zusammenarbeitenden Keycloak-Knoten angewendet wird; es ist nicht erforderlich, auf das Kubernetes-Cluster zu verweisen.
Regulärer Keycloak-Cluster
Um Keycloak in diesem Modus zu starten, sind folgende Schritte erforderlich:
- eine externe gemeinsame Datenbank einzurichten
- einen Lastenausgleichsmechanismus zu installieren
- ein internes Netzwerk mit Unterstützung für IP-Multicast zu haben
Die Einrichtung der externen Datenbank werden wir nicht behandeln, da dies nicht das Ziel dieses Artikels ist. Lassen Sie uns annehmen, dass irgendwo eine funktionierende Datenbank vorhanden ist — und wir einen Verbindungspunkt zu ihr haben. Wir fügen einfach diese Daten in die Umgebungsvariablen ein.
Für ein besseres Verständnis, wie Keycloak in einem Hochverfügbarkeits-Cluster (HA) funktioniert, ist es wichtig zu wissen, wie sehr alles von den Clusterfähigkeiten von Wildfly abhängt.
Wildfly verwendet mehrere Teilsysteme, von denen einige als Lastenausgleichsmechanismus und andere für die Hochverfügbarkeit genutzt werden. Der Lastenausgleich garantiert die Verfügbarkeit der Anwendung bei Überlastung eines Knoten im Cluster, während die Hochverfügbarkeit die Verfügbarkeit der Anwendung auch im Falle eines Ausfalls eines Teils der Knoten im Cluster sicherstellt. Einige dieser Teilsysteme sind:
mod_cluster: arbeitet zusammen mit Apache als HTTP-Lastenausgleichsmechanismus und ist standardmäßig von TCP-Multicast abhängig, um Knoten zu finden. Kann durch einen externen Lastenausgleich ersetzt werden.infinispan: ein verteilter Cache, der JGroups-Kanäle als Transportschicht verwendet. Kann zusätzlich das HotRod-Protokoll für die Kommunikation mit einem externen Infinispan-Cluster zur Synchronisation des Cache-Inhalts anwenden.jgroups: bietet Unterstützung für Gruppenkommunikation für hochverfügbare Dienste auf Basis von JGroups-Kanälen. Benannte Kanäle ermöglichen es Anwendungsinstanzen im Cluster, sich in Gruppen zu verbinden, sodass die Kommunikation Eigenschaften wie Zuverlässigkeit, Reihenfolge und Fehlertoleranz aufweist.
Lastenausgleich
Beim Einrichten des Lastenausgleichs als Ingress-Controller im Kubernetes-Cluster sind die folgenden Punkte zu beachten:
Der Betrieb von Keycloak setzt voraus, dass die Remote-IP-Adresse des Clients, der sich per HTTP mit dem Authentifizierungsserver verbindet, die tatsächliche IP-Adresse des Client-Computers ist. Die Einstellungen des Lastenausgleichs und des Ingress sollten die HTTP-Header korrekt festlegen X-Forwarded-For und X-Forwarded-Proto, sowie den ursprünglichen Header beibehalten HOST. Die neueste Version ingress-nginx (> 0.22.0)
Aktivieren Sie das Flag proxy-address-forwarding indem Sie die Umgebungsvariable PROXY_ADDRESS_FORWARDING in true setzen, um Keycloak zu signalisieren, dass es hinter einem Proxy arbeitet.
Zusätzlich muss Sticky Sessions im Ingress aktiviert werden. Keycloak verwendet den verteilten Infinispan-Cache, um Daten zu speichern, die mit der aktuellen Authentifizierungssitzung und der Benutzersitzung verbunden sind. Caches arbeiten standardmäßig mit einem Besitzer, das bedeutet, diese spezielle Sitzung wird auf einem bestimmten Clusterknoten gespeichert, während andere Knoten sie remote abfragen müssen, wenn sie Zugriff auf diese Sitzung benötigen.
Konkret hat bei uns entgegen der Dokumentation die Sitzung mit dem Cookie-Namen
AUTH_SESSION_IDnicht funktioniert. Keycloak hat eine Schleife in der Umleitung erzeugt, daher empfehlen wir, einen anderen Cookie-Namen für die klebende Sitzung zu wählen.
Außerdem fügt Keycloak den Namen des Knotens hinzu, der zuerst geantwortet hat, zu AUTH_SESSION_ID, und da jeder Knoten in der hochverfügbaren Variante dieselbe Datenbank verwendet, muss jeder von ihnen separate und einzigartige Knoten-ID haben, um Transaktionen zu verwalten. Es wird empfohlen, in JAVA_OPTS die Parameter jboss.node.name und jboss.tx.node.id einzigartig für jeden Knoten festzulegen – zum Beispiel den Pod-Namen zu verwenden. Wenn Sie den Pod-Namen verwenden, denken Sie bitte an die 23-Zeichen-Beschränkung für jboss-Variablen, daher ist es besser, StatefulSet anstelle von Deployment zu verwenden.
Ein weiteres Problem – wenn der Pod gelöscht oder neu gestartet wird, geht sein Cache verloren. Daher sollte die Anzahl der Cache-Besitzer für alle Caches mindestens zwei betragen, damit eine Kopie des Caches erhalten bleibt. Die Lösung ist, zu starten beim Starten des Pods, indem Sie es in das Verzeichnis legen /opt/jboss/startup-scripts im Container:
Inhalt des Skripts
embed-server --server-config=standalone-ha.xml --std-out=echo
batch
echo * Einstellung von CACHE_OWNERS auf "${env.CACHE_OWNERS}" in allen Cache-Containern
/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-servernachdem Sie den Wert der Umgebungsvariable CACHE_OWNERS auf den erforderlichen Wert gesetzt haben.
Privates Netzwerk mit Unterstützung für IP-Multicast
Wenn Sie Weavenet als CNI verwenden, funktioniert Multicast sofort — und Ihre Keycloak-Knoten werden sich gegenseitig sehen, sobald sie gestartet sind.
Wenn Sie keine Unterstützung für IP-Multicast im Kubernetes-Cluster haben, können Sie JGroups so konfigurieren, dass es mit anderen Protokollen zur Knotensuche arbeitet.
Die erste Option ist die Verwendung von KUBE_DNS, das service ohne Kopf für die Knotensuche verwendet. Sie übergeben einfach JGroups den Namen des Dienstes, der zur Knotensuche verwendet wird.
Eine weitere Möglichkeit ist die Anwendung der Methode KUBE_PING, die mit der API zur Knotensuche arbeitet (Sie müssen serviceAccount mit den Rechten die Umwandlung von numerischen und textuellen Daten in die Formate und get, danach die Pods so konfigurieren, dass sie mit diesem serviceAccount).
Methode zur Knotensuche für JGroups wird konfiguriert, indem Umgebungsvariablen gesetzt werden JGROUPS_DISCOVERY_PROTOCOL und JGROUPS_DISCOVERY_PROPERTIES. Für KUBE_PING Sie müssen die Pods auswählen, indem Sie Namespace und labels.
️ Wenn Sie Multicast verwenden und zwei oder mehr Keycloak-Cluster in einem Kubernetes-Cluster (zum Beispiel eines im Namespace
Produktion, das zweite —staging) ausführen — Knoten eines Keycloak-Clusters können sich einem anderen Cluster anschließen. Stellen Sie sicher, dass für jedes Cluster eine einzigartige Multicast-Adresse festgelegt wird, indem Sie die Variablenjboss.default.multicast.addressundjboss.modcluster.multicast.addressinJAVA_OPTS.
gespeichert. Replication zwischen Rechenzentren

Kommunikation
Keycloak verwendet mehrere separate Infinispan-Cache-Cluster für jedes Rechenzentrum, in dem die Keycloak-Cluster bestehen, die aus Keycloak-Knoten bestehen. Es gibt jedoch keinen Unterschied zwischen Keycloak-Knoten in verschiedenen Rechenzentren.
Keycloak-Knoten verwenden das externe Java Data Grid (Infinispan-Server) zur Kommunikation zwischen den Rechenzentren. Die Verbindung funktioniert über das Protokoll .
Infinispan-Caches müssen mit dem Attribut remoteStore, konfiguriert werden, damit die Daten in den entfernten (in einem anderen Rechenzentrum, Anm. des Übersetzers) Caches gespeichert werden können. Es gibt separate Infinispan-Clusters unter JDG-Servern, sodass die Daten, die auf JDG1 am Standort site1 gespeichert werden, auf JDG2 am Standort site2.
schließlich informiert der empfangende JDG-Server die Keycloak-Server seines Clusters über Client-Verbindungen, was ein besonderes Merkmal des HotRod-Protokolls ist. Die Keycloak-Knoten auf site2 aktualisieren ihre Infinispan-Caches, und die spezifische Benutzersitzung wird auch auf den Keycloak-Knoten auf site2.
für einige Caches ist es auch möglich, keine Backups zu erstellen und vollständig auf das Schreiben von Daten über den Infinispan-Server zu verzichten. Dazu muss die Einstellung remote-store vom spezifischen Infinispan-Cache (in der Datei standalone-ha.xml) entfernt werden, woraufhin ein bestimmter replicated-cache auch auf der Seite des Infinispan-Servers nicht mehr benötigt wird.
Cache-Einstellungen
Es gibt zwei Arten von Caches in Keycloak:
Lokal. Er befindet sich in der Nähe der Datenbank, dient zur Reduzierung der Belastung der Datenbank sowie zur Verringerung der Antwortverzögerung. In diesem Cache werden Realm, Clients, Rollen und Benutzermetadaten gespeichert. Dieser Cache wird nicht repliziert, selbst wenn dieser Cache Teil eines Keycloak-Clusters ist. Wenn ein Datensatz im Cache geändert wird, wird den anderen Servern im Cluster eine Änderungsbenachrichtigung gesendet, nach der der Datensatz aus dem Cache entfernt wird. Siehe Beschreibung
workweiter, für eine detaillierte Beschreibung des Verfahrens.Replizierbar. Verarbeitet Benutzersitzungen, Offline-Token und überwacht Anmeldefehler zur Feststellung von Phishing-Versuchen und anderen Angriffen. Die in diesen Caches gespeicherten Daten sind temporär, werden nur im Arbeitsspeicher gehalten, können jedoch im Cluster repliziert werden.
Infinispan-Caches
Sitzungen – ein Konzept in Keycloak, separate Caches, die als authenticationSessions, verwendet werden, um Daten spezifischer Benutzer zu speichern. Anfragen von diesen Caches sind normalerweise für den Browser und die Keycloak-Server erforderlich, nicht für die Anwendungen. Hier zeigt sich die Abhängigkeit von Sticky Sessions, und solche Caches müssen nicht repliziert werden, selbst nicht im Active-Active-Modus.
Aktionstoken. Ein weiteres Konzept, das normalerweise für verschiedene Szenarien angewendet wird, wenn der Benutzer beispielsweise etwas asynchron per E-Mail erledigen muss. Zum Beispiel während des Verfahrens Passwort vergessen Cache actionTokens wird verwendet, um Metadaten verwandter Tokens zu verfolgen – beispielsweise wurde der Token bereits verwendet und kann nicht erneut aktiviert werden. Dieser Cache-Typ sollte normalerweise zwischen Rechenzentren repliziert werden.
Caching und Veraltung gespeicherter Daten arbeitet, um die Last von der Datenbank zu nehmen. Solches Caching verbessert die Leistung, bringt aber offensichtliche Probleme mit sich. Wenn ein Keycloak-Server Daten aktualisiert, müssen die anderen Server darüber informiert werden, damit sie die Daten in ihren Caches aktualisieren können. Keycloak verwendet lokale Caches Realm, users und Berechtigung zum Cachen von Daten aus der Datenbank.
Es gibt auch einen separaten Cache work, der über alle Rechenzentren repliziert wird. Er speichert keine Daten aus der Datenbank, sondern dient dazu, Nachrichten über das Veralten von Daten an die Clusterknoten zwischen den Rechenzentren zu senden. Mit anderen Worten, sobald die Daten aktualisiert werden, sendet der Keycloak-Knoten eine Nachricht an andere Knoten in seinem Rechenzentrum sowie an Knoten in anderen Rechenzentren. Nach Erhalt einer solchen Nachricht führt jeder Knoten eine Bereinigung der entsprechenden Daten in seinen lokalen Caches durch.
Benutzersitzungen. Caches mit Namen sessions, clientSessions, offlineSessions und offlineClientSessions, werden normalerweise zwischen Rechenzentren repliziert und dienen zur Speicherung von Daten über Benutzersitzungen, die während der Aktivität des Benutzers im Browser aktiv sind. Diese Caches arbeiten mit der Anwendung, die HTTP-Anfragen von Endbenutzern bearbeitet, sodass sie mit Sticky Sessions verknüpft sind und zwischen den Rechenzentren repliziert werden müssen.
Schutz vor Brute-Force-Angriffen. Der Cache loginFailures dient zur Verfolgung von Fehlermeldungen beim Login, beispielsweise wie oft ein Benutzer ein falsches Passwort eingegeben hat. Die Replikation dieses Caches liegt in der Verantwortung des Administrators. Für eine genaue Zählung sollte jedoch die Replikation zwischen den Rechenzentren aktiviert werden. Andernfalls kann die Leistung verbessert werden, und wenn diese Frage aufkommt, kann die Replikation auch deaktiviert werden.
Bei der Bereitstellung des Infinispan-Clusters müssen Cache-Definitionen in die Konfigurationsdatei aufgenommen werden:
Es ist notwendig, den Infinispan-Cluster einzurichten und zu starten, bevor der Keycloak-Cluster gestartet wird.
Dann muss man einstellen remoteStore für die Keycloak-Caches. Dazu genügt ein Skript, das analog zum vorherigen erstellt wird, das zur Einrichtung der Variable verwendet werden soll. CACHE_OWNERS, man muss es in einer Datei speichern und im Verzeichnis ablegen. /opt/jboss/startup-scripts:
Inhalt des Skripts
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 ** Fügen Sie eine Remote-Socket-Bindung zum Infinispan-Server hinzu **
/socket-binding-group=standard-sockets/remote-destination-outbound-socket-binding=remote-cache:add(host=${remote.cache.host:localhost}, port=${remote.cache.port:11222})
echo ** Aktualisieren Sie das replizierte Cache-Arbeitelement **
/subsystem=infinispan/cache-container=keycloak/reduplicated-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/reduplicated-cache=work:write-attribute(name=statistics-enabled,value=true)
echo ** Aktualisieren Sie das verteilte Cache-Sitzungs-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 ** Aktualisieren Sie das verteilte Cache-Element offlineSessions **
/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 ** Aktualisieren Sie das verteilte Cache-Element clientSessions **
/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 ** Aktualisieren Sie das verteilte Cache-Element offlineClientSessions **
/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 ** Aktualisieren Sie das verteilte Cache-Element loginFailures **
/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 ** Aktualisieren Sie das verteilte Cache-Element actionTokens **
/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 ** Aktualisieren Sie das verteilte Cache-Element authenticationSessions **
/subsystem=infinispan/cache-container=keycloak/distributed-cache=authenticationSessions:write-attribute(name=statistics-enabled,value=true)
echo *** Aktualisieren Sie das undertow Subsystem ***
/subsystem=undertow/server=default-server/http-listener=default:write-attribute(name=proxy-address-forwarding,value=true)
run-batch
stop-embedded-serverVergessen Sie nicht, einzurichten JAVA_OPTS für Keycloak-Knoten zur Verwendung von HotRod: remote.cache.host, remote.cache.port und den Servicenamen jboss.site.name.
Links und zusätzliche Dokumentation
Der Artikel wurde für Habr von Mitarbeitern — Intensivkurse, Videokurse und betriebliche Schulungen von erfahrenen Fachleuten (Kubernetes, DevOps, Docker, Ansible, Ceph, SRE)
Quelle: habr.com
