SSH Server Containers For Development on Kubernetes

Gebouwd voor snelheid: ~10ms latentie, zelfs onder belasting
De razendsnelle manier om je modellen te bouwen, volgen en implementeren!
- Verwerkt 350+ RPS op slechts 1 vCPU — geen tuning nodig
- Productieklaar met volledige enterprise-ondersteuning
Vorig jaar bespraken we het draaien van Gehoste Jupyter Notebooks en VS Code (Code Server) op je Kubernetes Clusters. We vergeleken verschillende benaderingen en bestaande oplossingen, waaronder het huren en beheren van VM's. Vervolgens beschreven we onze aanpak om bruikbaarheidsproblemen aan te pakken, de ervaring te verbeteren en Kubernetes-details te abstraheren.
Sinds die tijd hebben we veel feedback ontvangen van onze klanten, voornamelijk over het gebrek aan een betere developmentervaring voor volwaardige applicaties. Hoewel Jupyter Lab geweldig is voor interactieve notebooks en lichte bewerkingen, vereist het omzetten naar volledige IDE-functionaliteiten vaak veel gedoe met Jupyter Extensions en blijft het mogelijk een mindere ervaring voor niet-Python-codebases. Om deze tekortkomingen aan te pakken, hebben we ondersteuning voor Code Server gelanceerd, wat min of meer VS Code in de browser is. Hoewel dit een oplossing is zonder installatie die veel Developer Experience-problemen van Jupyter Lab wegneemt, meldden gebruikers wrijving bij het werken met VS Code-extensies.
Een voorbeeld: Pylance, de extensie die uitstekende ondersteuning voor de Python-taal biedt in VS Code, kan niet worden geïnstalleerd op Code Server vanwege een merkgebonden licentie van Microsoft. In plaats daarvan moeten gebruikers vertrouwen op een combinatie van Jedi en Pyright, die nog steeds niet kunnen tippen aan Pylance. Een ander voorbeeld is Github CoPilot - hoewel het mogelijk is om dit op Code Server te installeren, vereist dit handmatig sleutelen aan het extensiebestand en het upgraden van de Code Server-versie.
Hoewel de bewerkingservaring van Code Server niet slecht is, is er soms een merkbare vertraging en vervormde tekst in de terminal, wat storend kan zijn. We wisten altijd al dat het verbinden van lokale VS Code met een remote VS Code Server via SSH of Tunnels een betere ervaring zou bieden.
Het doel is om gebruikers in staat te stellen een deployment te maken die een OpenSSH Server draait in een op Ubuntu gebaseerde container-image met dezelfde schijfpersistentie als Jupyter Lab, en gebruikers hier verbinding mee te laten maken.
Zo ziet dit eruit op het platform (documentatie):
In deze post leggen we uit hoe we het verbinden met containers via SSH hebben geïmplementeerd zonder directe toegang tot het cluster te verlenen en zonder verkeer buiten de VPC te sturen.
Istio en Routing
Wanneer we applicaties deployen, configureren we meestal een domeinnaam om die diensten te bereiken. Het kopen van een domein voor elke applicatie is echter onbetaalbaar. In plaats daarvan kopen we een enkel domein (bijv. acmecorp.com), configureren we subdomeinen (docs.acmecorp.com) en/of pad-prefixen (acmecorp.com/blog/) en gebruiken we vervolgens een router om regels te matchen en verkeer naar verschillende applicaties te sturen.
We gebruiken Istio voor al onze ingress routing. Istio biedt, naast vele andere functies, handige abstracties om de onderliggende Envoy-proxy te configureren die daadwerkelijk alle routing afhandelt.
Laten we kijken hoe een HTTP-verzoek wordt gerouteerd

In het bovenstaande scenario, wanneer een gebruiker probeert https://myapp.acmecorp.com/api/v1 op te halen:
- Eerst wordt *.acmecorp.com via DNS opgelost naar het publieke IP-adres van de externe load balancer. De poort wordt afgeleid als 443 vanwege het https-protocol.
- Er wordt een TCP-verbinding tot stand gebracht met de load balancer en de HTTP-request-payload wordt verzonden
- De Load Balancer stuur de request-payload naar de Istio Ingress Pods
- Istio Ingress kijkt naar alle VirtualServices-configuraties (en Gateway-configuratie), matcht de hostnaam (belangrijker nog: het subdomein myapp) en pad-prefix en stuur het verkeer naar de bijbehorende Kubernetes Service
- Kubernetes stuur het verzoek naar een van de Endpoints (Pod) voor de Service
Dit wordt Layer 7 Routing genoemd, omdat we daadwerkelijke velden uit de HTTP-specificatie gebruiken om de routing uit te voeren.
Routing in de context van SSH
SSH gebruikt een aangepast protocol dat TCP gebruikt voor transport. Een eenvoudige SSH-verbinding ziet er ongeveer als volgt uit
ssh user@somemachine.acmecorp.com -p 22
Hier proberen we verbinding te maken met somemachine.acmecorp.com op poort 22. Hier moet somemachine.acmecorp.com:22 worden opgelost naar een unieke IP-adres- en poortcombinatie om de bestemming te bereiken. Maar herinner je dat in onze configuratie alle subdomeinen zijn ingesteld om naar dezelfde Load Balancer te wijzen - dat betekent dat abc.acmecorp.com, xyz.acmecorp.com en somemachine.acmecorp.com allemaal naar hetzelfde IP-adres verwijzen, en Istio/Envoy vervolgens naar het subdomein moet kijken om te beslissen waar het verkeer heen moet. Bij SSH is dit echter niet mogelijk, want na het oplossen van het IP-adres en het opzetten van een TCP-verbinding ziet Istio alleen het IP-adres en poortnummer van de Load Balancer. De werkelijke inhoud van de pakketten wordt namelijk versleuteld door SSH. Dus hoe kunnen we routen naar meerdere verschillende SSH-bestemmingen in het cluster?
Optie 1: Unieke poorten gebruiken op dezelfde LoadBalancer
Omdat we alleen unieke combinaties van IP-adressen en poorten moeten garanderen, kunnen we simpelweg verschillende poorten op de load balancer toewijzen aan unieke SSH-containers.
We kunnen vervolgens een TCP Route-poortmatch met Istio configureren

Hier zal al het TCP-verkeer dat binnenkomt op poort 22 van de LoadBalancer Service A bereiken, en al het TCP-verkeer op poort 23 zal Service B bereiken.
Hoewel dit goed werkt, zijn er enkele beperkingen
- Er kunnen maximaal 65.535 SSH-containers worden bereikt achter een enkele Load Balancer. Dit is geen groot probleem, omdat we realistisch gezien niet verwachten dat er zoveel SSH-containers tegelijkertijd zijn gedeployed.
- Het lastigere probleem is het nauwkeurig dynamisch openen en vrijmaken van poorten op de externe load balancer zonder ooit ander normaal verkeer te verstoren. Hoewel dit zeker mogelijk is, zou elke bug of race condition ernstige downtime kunnen veroorzaken voor andere applicaties. Om nog maar te zwijgen van het feit dat het openzetten van willekeurige poorten voor veel van onze klanten een groot beveiligingsrisico vormt.
Optie 2: Een nieuwe LoadBalancer gebruiken voor elke SSH-container

In dit geval hebben we abc.acmecorp.com en xyz.acmecorp.com expliciet laten wijzen naar twee verschillende externe Load Balancers in plaats van het wildcard *.acmecorp.com. Nu wijzen ze elk naar een uniek IP-adres en kunnen ze worden gerouteerd door twee verschillende Istio Gateways (één-op-één gekoppeld aan een externe load balancer). De duidelijke beperking hier is dat het provisonen van een nieuwe load balancer per SSH-container onbetaalbaar wordt.
Is er een manier om te profiteren van routing op HTTP-niveau en toch alleen met TCP-verkeer te werken? Maak kennis met HTTP CONNECT!
Proxying via HTTP CONNECT
De HTTP CONNECT-methode maakt het mogelijk om een "tunnel" op te zetten tussen twee bestemmingen via een Proxy. Denk aan de oude tijd van telefooncentrales - je wilt een nummer bellen maar hebt geen rechtstreekse lijn. In plaats daarvan vergemakkelijkt een telefonist tussendoor de verbinding namens jou en trekt zich vervolgens terug zodat de twee partijen kunnen communiceren.

We raden aan om de volgende video te bekijken voor een goede uitleg: https://www.youtube.com/watch?v=PAJ5kK50qp8
Gelukkig gebruiken we in ons geval al een proxy die CONNECT kan gebruiken: Envoy Proxy. Laten we kijken hoe dit werkt in ons scenario:

- De client opent een verbinding met acmecorp.com:80 - de externe load balancer die verkeer naar Envoy stuurt.
- De client stuurt een HTTP CONNECT-verzoek
CONNECT svc-a.ns.cluster.svc.local:80 HTTP/1.1
Host: svc-a.ns.cluster.svc.local
waarmee Envoy de opdracht krijgt om namens hen een TCP-verbinding op te zetten met svc-a.ns.cluster.svc.local:80
- Zodra een verbinding tot stand is gebracht, wordt er een 200 OK teruggegeven aan de client.
- Vanaf dit punt maakt het Envoy niet meer uit wat de inhoud van het verkeer is en fungeert het als een "tunnel" waarmee het verkeer tussen de client en de pod kan stromen. Dit kan alles zijn wat op TCP werkt, inclusief maar niet beperkt tot SSH.

Merk op dat svc-a.ns.cluster.svc.local:80 een Kubernetes Service is en niet naar een publiek IP-adres wijst; het kan alleen binnen het Kubernetes Cluster worden opgelost. Omdat Envoy zich in het cluster bevindt, kunnen we het configureren om de pods erachter te bereiken.
Het enige wat nog rest, is Envoy configureren om deze routing uit te voeren. Helaas heeft Istio geen abstracties op hoog niveau om dit eenvoudig te configureren; in plaats daarvan moeten we patches toepassen op de Envoy-configuratie met behulp van Envoy Filters
Envoy Filters
Het begrijpen van de mogelijkheden van Envoy en Envoy Filters valt buiten het bestek van deze blogpost, maar zie het als een handige manier om Istio-routingregels aan te passen met behulp van kleine patches. Om op CONNECT gebaseerde routing in te schakelen, moeten we:
- Een publiek toegankelijke poort op de LoadBalancer hebben om TCP-verkeer te accepteren (bijv. poort 2222) en de bijbehorende Istio Gateway configureren om HTTP-verkeer te accepteren. We hebben ervoor gekozen om bij poort 80 te blijven omdat we deze al gebruiken voor normaal HTTP-verkeer en SSH-verkeer toch versleuteld zal worden.
- De publiek toegankelijke poort op de Gateway configureren om verzoeken van het type CONNECT te accepteren. We merkten dat dit al is ingeschakeld voor verzoeken naar Poort 80. Voor elke andere poort kun je een Envoy Filter als volgt toepassen:
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
- Voor elke SSH-container op CONNECT gebaseerde routing configureren:
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
Dat is een erg ingewikkeld uitziende YAML, maar het enige wat we doen is de listener op poort 80 van de Gateway aanpassen om te matchen op CONNECT-verzoeken naar svc-a.ns.svc.cluster.local:80 en deze te routen naar outbound|80||svc-a.ns.svc.cluster.local, oftewel poort 80 van de Kubernetes service svc-a.ns.svc.cluster.local waar onze OpenSSH-server in de Container wacht op SSH-verbindingen.
CONNECT initialiseren aan de SSH Client-zijde
Van zichzelf weet de SSH-client niets van HTTP CONNECT. In plaats daarvan biedt het een ProxyCommand optie waarmee andere programma's de SSH-verbinding kunnen ondersteunen. Hier gebruiken we het ProxyTunnel-project dat dit eenvoudig maakt. De configuratie in ~/.ssh/config ziet er als volgt uit
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
Nu dit allemaal is ingesteld, kunnen gebruikers eenvoudig verbinding maken en hun favoriete dev-workflow opzetten - of dat nu Neovim, VS Code, JetBrains IDE, etc. is.

Beperkingen & Mogelijke Oplossingen
Hoewel deze functie de Developer Experience op het gebied van codebewerking en -uitvoering aanzienlijk verbetert, gelden er nog steeds enkele beperkingen omdat we ons nog steeds in een container bevinden.
- Docker werkt niet omdat we ons al in een container bevinden. Theoretisch is het mogelijk om een paar dingen werkend te krijgen met DIND, maar dat brengt zo zijn eigen uitdagingen met zich mee.
- Wijzigingen aan het root-bestandssysteem van de container
/blijven niet behouden wanneer de container opnieuw wordt opgestart. We bieden een manier om onze SSH Server-image uit te breiden en vanaf die aangepaste images te starten. - Kubernetes Pods zijn bedoeld om vluchtig (ephemeral) te zijn en kunnen worden verplaatst, maar dat is ongewenst voor een developmentomgeving. We configureren pod disruption budgets om te voorkomen dat de pod wordt verplaatst.
- Hoewel de proxying transparant is, stroomt het verkeer nog steeds door de load balancer en de Istio Envoy-pods. Dat betekent dat ongebruikelijke acties tijdens development, zoals het up- of downloaden van enorme bestanden, bandbreedte en resources kunnen opsnoepen en ander verkeer kunnen beïnvloeden. Het is het beste om een afzonderlijke set van LoadBalancer-, Gateway- en Envoy-pods te gebruiken voor het verbinden met SSH-containers.
SSH-servercontainers op Kubernetes bij TrueFoundry
TrueFoundry is een ML/LLM deployment PaaS op Kubernetes om ontwikkelaarsworkflows te versnellen, terwijl ze volledige flexibiliteit behouden bij het testen en deployen van modellen en het Infra-team volledige veiligheid en controle behoudt. Via ons platform stellen we teams in staat om modellen binnen 15 minuten te deployen en te monitoren met 100% betrouwbaarheid, schaalbaarheid en de mogelijkheid om binnen enkele seconden te roll-backen - waardoor ze kosten besparen en Modellen sneller naar productie brengen, wat leidt tot echte zakelijke waarderealisatie.
TrueFoundry AI Gateway levert een latentie van ~3–4 ms, verwerkt 350+ RPS op 1 vCPU, schaalt moeiteloos horizontaal en is klaar voor productie, terwijl LiteLLM last heeft van hoge latentie, moeite heeft boven een gemiddeld aantal RPS, geen ingebouwde schaling heeft en het meest geschikt is voor lichte workloads of prototypes.



















.png)

.webp)



.webp)








.webp)





