SSH-Server-Container für die Entwicklung auf Kubernetes

Auf Geschwindigkeit ausgelegt: ~ 10 ms Latenz, auch unter Last
Unglaublich schnelle Methode zum Erstellen, Verfolgen und Bereitstellen Ihrer Modelle!
- Verarbeitet mehr als 350 RPS auf nur 1 vCPU — kein Tuning erforderlich
- Produktionsbereit mit vollem Unternehmenssupport
Letztes Jahr haben wir über die Ausführung von Hosted Jupyter Notebooks und VS Code (Code Server) auf Ihren Kubernetes-Clustern gesprochen. Wir haben verschiedene Ansätze und bestehende Lösungen verglichen, einschließlich des Mietens und Verwaltens von VMs. Anschließend haben wir unsere Ansätze beschrieben, um Benutzerfreundlichkeitsprobleme zu beheben, die Nutzung zu verbessern und Kubernetes-Details zu abstrahieren.
Seitdem haben wir viel Feedback von unseren Kunden erhalten, vor allem das Fehlen einer besseren Entwicklungs-Experience für vollwertige Anwendungen. Während Jupyter Lab hervorragend für interaktive Notebooks und leichtes Editieren geeignet ist, erfordert die Erweiterung auf vollständige IDE-Funktionen möglicherweise viel Bastelei mit Jupyter-Erweiterungen und ist für Nicht-Python-Codebasen möglicherweise immer noch keine optimale Erfahrung. Um solche Unzulänglichkeiten zu beheben, haben wir die Unterstützung für Code Server eingeführt, was mehr oder weniger VS Code im Browser ist. Obwohl es sich um eine Lösung ohne Einrichtungsaufwand handelt und viele Developer Experience-Probleme mit Jupyter Lab lindert, berichteten Benutzer von Hürden bei der Arbeit mit VS Code-Erweiterungen.
Beispielsweise kann Pylance, die Erweiterung, die eine hervorragende Python-Sprachunterstützung in VS Code bietet, aufgrund einer proprietären Microsoft-Lizenz nicht auf dem Code Server installiert werden. Stattdessen müssen sich Benutzer auf eine Kombination aus Jedi und Pyright verlassen, die mit Pylance immer noch nicht mithalten können. Ein weiteres Beispiel ist Github CoPilot – obwohl die Installation auf dem Code Server möglich ist, erfordert sie manuelle Anpassungen an der Erweiterungsdatei und ein Upgrade der Code Server-Version.
Obwohl die Editier-Experience auf dem Code Server nicht schlecht ist, gibt es manchmal eine spürbare Verzögerung und durcheinandergebrachten Text im Terminal, was störend sein kann. Wir wussten schon immer, dass die Verbindung von lokalem VS Code mit dem Remote-VS Code Server über SSH oder Tunnels eine bessere Erfahrung bieten würde.
Das Ziel besteht darin, den Benutzern das Erstellen eines Deployments zu ermöglichen, auf dem der OpenSSH Server in einem Ubuntu-basierten Container-Image mit derselben Festplattenpersistenz wie bei Jupyter Lab läuft, und den Benutzern die Verbindung dazu zu ermöglichen.
So sieht es auf der Plattform aus (docs):
In diesem Beitrag erläutern wir, wie wir die Verbindung zu Containern über SSH implementiert haben, ohne direkten Zugriff auf den Cluster zu gewähren und ohne Traffic außerhalb der VPC zu senden.
Istio und Routing
Wenn wir Anwendungen deployen, konfigurieren wir normalerweise einen Domainnamen, um diese Dienste zu erreichen, aber der Kauf einer Domain für jede Anwendung ist unerschwinglich teuer. Stattdessen kaufen wir eine einzelne Domain (z. B. acmecorp.com), konfigurieren Subdomains (docs.acmecorp.com) und/oder Pfad-Präfixe (acmecorp.com/blog/) und verwenden dann einen Router, um Regeln abzugleichen und Traffic an verschiedene Anwendungen zu leiten.
Wir verwenden Istio für unser gesamtes Ingress-Routing. Istio bietet neben vielen Funktionen komfortable Abstraktionen zur Konfiguration des zugrunde liegenden Envoy-Proxys, der tatsächlich das gesamte Routing verwaltet.
Verstehen wir, wie eine HTTP-Anfrage geroutet wird

Im obigen Beispiel, wenn ein Benutzer versucht, https://myapp.acmecorp.com/api/v1 aufzurufen:
- Zuerst wird *.acmecorp.com per DNS auf die öffentliche IP des externen Load Balancers aufgelöst. Der Port wird aufgrund des HTTPS-Protokolls als 443 abgeleitet.
- Eine TCP-Verbindung zum Load Balancer wird hergestellt und die HTTP-Request-Payload wird gesendet.
- Der Load Balancer leitet die Request-Payload an die Istio Ingress Pods weiter.
- Istio Ingress prüft alle VirtualServices-Konfigurationen (und die Gateway-Konfiguration), gleicht den Hostnamen (vor allem die Subdomain myapp) und das Pfad-Präfix ab und leitet an den entsprechenden Kubernetes Service weiter.
- Kubernetes leitet die Anfrage an einen der Endpunkte (Pod) für den Service weiter.
Dies wird als Layer 7 Routing bezeichnet, da wir tatsächliche Felder aus der HTTP-Spezifikation für das Routing verwenden.
Routing im Kontext von SSH
SSH verwendet ein benutzerdefiniertes Protokoll, das TCP für den Transport nutzt. Eine einfache SSH-Verbindung sieht etwa wie folgt aus
ssh user@somemachine.acmecorp.com -p 22
Hier versuchen wir, eine Verbindung zu somemachine.acmecorp.com auf Port 22 herzustellen. Hier muss somemachine.acmecorp.com:22 auf eine eindeutige Kombination aus IP-Adresse und Port aufgelöst werden, um das Ziel zu erreichen. Erinnern wir uns jedoch daran, dass in unserem Setup alle Subdomains so konfiguriert sind, dass sie auf denselben Load Balancer zeigen – das bedeutet, dass abc.acmecorp.com, xyz.acmecorp.com und somemachine.acmecorp.com alle auf dieselbe IP-Adresse aufgelöst werden und Istio/Envoy dann auf die Subdomain schauen und entscheiden soll, wohin geroutet wird. Im Fall von SSH ist dies jedoch nicht möglich, da Istio nach dem Auflösen der IP-Adresse und dem Herstellen einer TCP-Verbindung nur die Load Balancer-IP und die Portnummer sieht; die tatsächlichen Inhalte der Pakete werden von SSH verschlüsselt. Wie können wir also zu mehreren verschiedenen SSH-Zielen im Cluster routen?
Option 1: Eindeutige Ports auf demselben LoadBalancer verwenden
Da wir nur eindeutige Kombinationen aus IP-Adresse und Ports sicherstellen müssen, können wir den eindeutigen SSH-Containern einfach verschiedene Ports auf dem Load Balancer zuweisen.
Wir können dann ein TCP Route Port Match mit Istio konfigurieren.

Hier erreicht der gesamte TCP-Traffic, der an Port 22 des LoadBalancers eingeht, Service A, und der gesamte TCP-Traffic auf Port 23 erreicht Service B.
Obwohl dies gut funktioniert, gibt es einige Einschränkungen:
- Maximal 65.535 SSH-Container können hinter einem einzigen Load Balancer erreicht werden. Das ist kein großes Problem, da wir realistischerweise nicht erwarten, dass so viele SSH-Container gleichzeitig deployed sind.
- Das schwierigere Problem besteht darin, Ports auf dem externen Load Balancer präzise und dynamisch zu öffnen und freizugeben, ohne jemals den normalen Traffic zu unterbrechen. Obwohl dies durchaus möglich ist, könnte jeder Bug oder jede Race Condition zu schwerwiegenden Ausfallzeiten für andere Anwendungen führen. Ganz zu schweigen davon, dass das Öffnen beliebiger Ports für viele unserer Kunden ein erhebliches Sicherheitsrisiko darstellt.
Option 2: Einen neuen LoadBalancer für jeden SSH-Container verwenden

In diesem Fall haben wir abc.acmecorp.com und xyz.acmecorp.com explizit auf zwei verschiedene externe Load Balancer verwiesen, anstatt das Wildcard *.acmecorp.com zu verwenden. Nun zeigen sie jeweils auf eine eindeutige IP-Adresse und können von zwei verschiedenen Istio Gateways geroutet werden (eins-zu-eins mit einem externen Load Balancer verknüpft). Die offensichtliche Einschränkung hierbei ist, dass die Bereitstellung eines neuen Load Balancers pro SSH-Container unerschwinglich teuer wird.
Gibt es eine Möglichkeit, die Vorteile des Routings auf HTTP-Ebene zu nutzen und dennoch nur mit TCP-Traffic zu arbeiten? Hier kommt HTTP CONNECT ins Spiel!
Proxying mit HTTP CONNECT
Die HTTP CONNECT-Methode ermöglicht das Einrichten eines „Tunnels“ zwischen zwei Zielen über einen Proxy. Stellen Sie sich die alten Zeiten der Telefonvermittlungen vor – Sie möchten eine Nummer anrufen, haben aber keine direkte Leitung dorthin; stattdessen stellt ein Vermittler dazwischen die Verbindung in Ihrem Namen her und zieht sich dann zurück, damit die beiden Parteien kommunizieren können.

Wir empfehlen, sich das folgende Video für eine gute Erklärung anzusehen: https://www.youtube.com/watch?v=PAJ5kK50qp8
Glücklicherweise verwenden wir in unserem Fall bereits einen Proxy, der CONNECT nutzen kann: Envoy Proxy. Schauen wir uns an, wie das in unserem Anwendungsfall funktionieren würde:

- Der Client öffnet eine Verbindung zu acmecorp.com:80 – dem externen Load Balancer, der den Traffic an Envoy leitet.
- Der Client sendet eine HTTP CONNECT-Anfrage
CONNECT svc-a.ns.cluster.svc.local:80 HTTP/1.1
Host: svc-a.ns.cluster.svc.local
welche Envoy anweist, in ihrem Namen eine TCP-Verbindung zu svc-a.ns.cluster.svc.local:80 herzustellen
- Sobald eine Verbindung hergestellt ist, wird ein 200 OK an den Client zurückgegeben.
- Ab diesem Zeitpunkt kümmert sich Envoy nicht mehr um den Inhalt des Traffics und fungiert als „Tunnel“, der den Traffic zwischen dem Client und dem Pod fließen lässt. Dies kann alles sein, was auf TCP basiert, einschließlich, aber nicht beschränkt auf SSH.

Beachten Sie, dass svc-a.ns.cluster.svc.local:80 ein Kubernetes Service ist und nicht auf eine öffentliche IP-Adresse zeigt, sondern nur innerhalb des Kubernetes-Clusters aufgelöst werden kann. Da sich Envoy innerhalb des Clusters befindet, können wir ihn so konfigurieren, dass er die Pods dahinter erreicht.
Es bleibt nur noch, Envoy so zu konfigurieren, dass er ein solches Routing durchführt. Leider verfügt Istio nicht über hochrangige Abstraktionen, um dies einfach zu konfigurieren; stattdessen müssen wir Patches auf die Envoy-Konfiguration anwenden, indem wir Envoy Filters verwenden.
Envoy Filters
Das Verständnis der Envoy-Funktionen und Envoy Filters den Rahmen dieses Blogbeitrags sprengen würde, betrachten Sie es einfach als eine bequeme Möglichkeit, Istio-Routing-Regeln mit kleinen Patches zu ändern. Um CONNECT-basiertes Routing zu aktivieren, müssen wir:
- Einen öffentlich zugänglichen Port auf dem LoadBalancer haben, um TCP-Traffic zu akzeptieren (z. B. 2222), und das entsprechende Istio Gateway so konfigurieren, dass es HTTP-Traffic akzeptiert. Wir haben uns entschieden, bei Port 80 zu bleiben, da wir ihn bereits für normalen HTTP-Traffic verwenden und der SSH-Traffic ohnehin verschlüsselt wird.
- Den öffentlich zugänglichen Port am Gateway so konfigurieren, dass er Anfragen vom Typ CONNECT akzeptiert. Wir haben festgestellt, dass dies für Anfragen an Port 80 bereits aktiviert ist. Für jeden anderen Port können Sie einen Envoy Filter wie folgt anwenden:
E.g. Enable CONNECT on Port 2222apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
spec:
configPatches:
- applyTo: NETWORK_FILTER
match:
context: GATEWAY
listener:
filterChain:
filter:
name: envoy.filters.network.http_connection_manager
portNumber: 2222
patch:
operation: MERGE
value:
typed_config:
'@type': >-
type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
http2_protocol_options:
allow_connect: true
upgrade_configs:
- upgrade_type: CONNECT
workloadSelector:
labels:
app: tfy-istio-ingress
kind: EnvoyFilter
spec:
configPatches:
- applyTo: NETWORK_FILTER
match:
context: GATEWAY
listener:
filterChain:
filter:
name: envoy.filters.network.http_connection_manager
portNumber: 2222
patch:
operation: MERGE
value:
typed_config:
'@type': >-
type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
http2_protocol_options:
allow_connect: true
upgrade_configs:
- upgrade_type: CONNECT
workloadSelector:
labels:
app: tfy-istio-ingress
- Für jeden SSH-Container CONNECT-basiertes Routing konfigurieren:
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: svc-a-ns-ssh-envoy-filter
namespace: istio-system
spec:
configPatches:
- applyTo: NETWORK_FILTER
match:
context: GATEWAY
listener:
filterChain:
filter:
name: envoy.filters.network.http_connection_manager
portNumber: 80
patch:
operation: MERGE
value:
typed_config:
'@type': >-
type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
route_config:
name: local_route
virtual_hosts:
- domains:
- svc-a.ns.svc.cluster.local:80
name: svc-a-ns-ssh-vh
routes:
- match:
connect_matcher: {}
route:
cluster: >-
outbound|80||svc-a.ns.svc.cluster.local
upgrade_configs:
- connect_config: {}
enabled: true
upgrade_type: CONNECT
workloadSelector:
labels:
istio: tfy-istio-ingress
Das ist ein sehr furchterregend aussehendes YAML, aber alles, was wir tun, ist das Ändern des Listeners auf Port 80 am Gateway, um nach CONNECT-Anfragen an svc-a.ns.svc.cluster.local:80 zu suchen und sie an outbound|80||svc-a.ns.svc.cluster.local zu leiten, d. h. an den Port 80 des Kubernetes Service svc-a.ns.svc.cluster.local, wo unser OpenSSH-Server im Container auf SSH-Verbindungen wartet.
Puffern von CONNECT auf der SSH-Client-Seite
Von sich aus weiß der SSH-Client nichts von HTTP CONNECT. Stattdessen bietet er eine ProxyCommand-Option, die es anderen Programmen ermöglicht, die SSH-Verbindung zu erleichtern. Hier verwenden wir das ProxyTunnel-Projekt, das dies vereinfacht. Die Konfiguration in ~/.ssh/config sieht wie folgt aus
Host svc-a-ns
User jovyan
HostName svc-a.ns.svc.cluster.local
Port 80
ServerAliveInterval 100
IdentityFile ~/.ssh/my-private-key
ProxyCommand proxytunnel -v -p ssh.acmecorp.com:80 -o %h -d %h:%p
Nachdem all dies erledigt ist, können sich Benutzer jetzt einfach verbinden und ihren bevorzugten Entwicklungs-Workflow einrichten – sei es Neovim, VS Code, JetBrains IDE usw.

Einschränkungen & Potenzielle Lösungen
Obwohl diese Funktion die Developer Experience in Bezug auf Code-Bearbeitung und -Ausführung erheblich verbessert, gelten weiterhin einige Einschränkungen, da wir immer noch innerhalb eines Containers laufen.
- Docker funktioniert nicht, weil wir uns bereits in einem Container befinden. Theoretisch ist es möglich, einige Dinge mit DIND zum Laufen zu bringen, aber das bringt eigene Herausforderungen mit sich.
- Änderungen am Root-Dateisystem des Containers
/sind über Container-Neustarts hinweg nicht persistent. Wir bieten eine Möglichkeit, unser SSH-Server-Image zu erweitern und von diesen benutzerdefinierten Images aus zu starten. - Kubernetes Pods sind als kurzlebig gedacht und können verschoben werden, was für eine Entwicklungsumgebung jedoch unerwünscht ist. Wir konfigurieren Pod Disruption Budgets, um zu verhindern, dass der Pod verschoben wird.
- Obwohl das Proxying transparent ist, fließt der Traffic weiterhin durch den Load Balancer und die Istio Envoy Pods. Das bedeutet, dass ungewöhnliche Aktionen in der Entwicklung, wie das Hoch- oder Herunterladen riesiger Dateien, Bandbreite und Ressourcen verbrauchen und anderen Traffic beeinträchtigen können. Es ist am besten, ein separates Set von LoadBalancer-, Gateway- und Envoy-Pods für die Verbindung zu SSH-Containern zu verwenden.
SSH-Server-Container auf Kubernetes bei TrueFoundry
TrueFoundry ist eine ML/LLM-Deployment-PaaS über Kubernetes, um Entwickler-Workflows zu beschleunigen und ihnen volle Flexibilität beim Testen und Deployen von Modellen zu bieten, während gleichzeitig volle Sicherheit und Kontrolle für das Infra-Team gewährleistet werden. Über unsere Plattform ermöglichen wir es Teams, Modelle in 15 Minuten mit 100 % Zuverlässigkeit, Skalierbarkeit und der Möglichkeit zum Rollback in Sekunden zu deployen und zu überwachen – so können Kosten gespart und Modelle schneller in die Produktion gebracht werden, was echten geschäftlichen Mehrwert schafft.
TrueFoundry AI Gateway bietet eine Latenz von ~3—4 ms, verarbeitet mehr als 350 RPS auf einer vCPU, skaliert problemlos horizontal und ist produktionsbereit, während LiteLM unter einer hohen Latenz leidet, mit moderaten RPS zu kämpfen hat, keine integrierte Skalierung hat und sich am besten für leichte Workloads oder Prototyp-Workloads eignet.


















%20(28).webp)



.webp)
.webp)
.png)

.png)
.png)
.png)
.png)
.png)
.webp)
.webp)






