利用kubernetes exec接口實(shí)現(xiàn)任意容器的web-terminal
一、?? Kubectl exec命令登錄指定容器
如果你用過k8s,那么kubectl exec 命令一定不要錯(cuò)過。簡(jiǎn)單的敲上:
kubectl exec -it pod名 -- /bin/sh
就可以登錄到任意節(jié)點(diǎn)的指定的容器里面,效果和使用ssh登錄到一臺(tái)機(jī)器進(jìn)行操作一模一樣,非常的方便。
那有沒有想過:
這個(gè)功能是怎么實(shí)現(xiàn)的呢?
能不能在Web網(wǎng)頁(yè)上面,直接擁有這個(gè)功能呢?
接下來,我們一一解讀。
二、?? Kubectl exec實(shí)現(xiàn)
1.???? 最底層實(shí)現(xiàn):Docker容器的exec命令
K8s實(shí)現(xiàn)的“進(jìn)入某個(gè)容器”的功能,底層本質(zhì)是Docker容器通過exec進(jìn)入容器的擴(kuò)展。即從本機(jī)容器,擴(kuò)展為任意節(jié)點(diǎn)的容器。
所以咱們先看看Docker怎么通過exec進(jìn)入容器的呢?
docker exec -it 容器id /bin/sh
通過上面的命令,就可以進(jìn)入到容內(nèi)部。 本質(zhì)是新建了一個(gè)“與目標(biāo)容器,共享namespace的”新的shell進(jìn)程。所以該shell進(jìn)程,看到的世界,就是容器內(nèi)的世界了。
那么K8s要做的就是,跨節(jié)點(diǎn)利用Docker的這個(gè)功能。
2.???? Kubectl到容器的超長(zhǎng)路徑
從kubectl命令行工具,到容器內(nèi)部,這里經(jīng)過的網(wǎng)絡(luò)路徑其實(shí)是很長(zhǎng)的。如下:
因?yàn)閑xec命令行,是實(shí)時(shí)交互的。即輸入和輸出,實(shí)時(shí)發(fā)生。
所以K8s選擇了使用 類似Websocket 這種雙向?qū)崟r(shí)通信的協(xié)議,來傳遞輸入/輸出內(nèi)容。
Kubectl <---(雙向?qū)崟r(shí)協(xié)議)---> Kube-Api-Server <---(雙向?qū)崟r(shí)協(xié)議)--->節(jié)點(diǎn)kubelet
3.???? Kubectl實(shí)現(xiàn)exec代碼簡(jiǎn)析
通過簡(jiǎn)單查詢 kubectl 的源碼:
import "k8s.io/client-go/tools/remotecommand"
//這里初始化了一個(gè) remote-cmd 的對(duì)象
exec, err := remotecommand.NewSPDYExecutor(config, method, url)
if err != nil {
return err
}
//這里開始,將輸入輸出,進(jìn)行實(shí)時(shí)傳遞(Stream)
return exec.Stream(remotecommand.StreamOptions{
Stdin:???????????? stdin,
Stdout:??????????? stdout,
Stderr:??????????? stderr,
Tty:?? ????????????tty,
TerminalSizeQueue: terminalSizeQueue,
})
這里可以看到,kubectl使用了一個(gè)叫 SPDY 的協(xié)議去連K8s的API-server。 這里的SPDY協(xié)議是Google公司搞的,基本類似Websocket可以進(jìn)行雙向?qū)崟r(shí)傳輸,但是這個(gè)協(xié)議已經(jīng)被淘汰了,被HTTP2所替代。見k8s的issue:SPDY is deprecated. Switch to HTTP/2。
(https://github.com/Kubernetes/Kubernetes/issues/7452)
好在K8s的API-server除了支持 SPDY協(xié)議,也支持Websocket協(xié)議。
(https://github.com/kubernetes/kubernetes/issues/89163)
三、?? 網(wǎng)頁(yè)web-terminal直連容器
有了前面的背景知識(shí),那么如果想在web網(wǎng)頁(yè)中,實(shí)現(xiàn)exec實(shí)時(shí)登錄到容器里面。那么可以有以下思路:
首先,SPDY是個(gè)淘汰的協(xié)議,所以前端JS代碼可供參考的很少。而前端對(duì)Websocket的支持則很廣泛。所以咱們Web側(cè)選擇使用Websocket。
于是,實(shí)現(xiàn)的方案有如下幾種:
1.???? web網(wǎng)頁(yè)使用Websocket直連K8s
這種場(chǎng)景,雖然看著最直接,但是適用場(chǎng)景反而有限。 因?yàn)闄?quán)限隔離問題,一般情況你不可能讓前端獲得最大的k8s權(quán)限,允許進(jìn)入任意容器中。
2.???? Web網(wǎng)頁(yè)經(jīng)過一個(gè)后端,中轉(zhuǎn)至K8s
在前端和K8s的中間,增加一個(gè)自研的Server,可以很好的控制權(quán)限隔離,封裝K8s到業(yè)務(wù)的轉(zhuǎn)換。
那么對(duì)于自研Server來說,它就是一個(gè)類似Proxy的程序。其中,[Web<---->自研Server]這一段肯定是使用Websocket協(xié)議。但是[自研Server<---->K8s]這一段,則有2種實(shí)現(xiàn):1種是直接使用Websocket協(xié)議。第2種是使用SPDY協(xié)議(即利用 remotecommand代碼實(shí)現(xiàn))。
下面分2種場(chǎng)景分析。
3.???? 中轉(zhuǎn)Server通過SPDY與K8s相連
因?yàn)閗ubectl代碼中有exec的實(shí)現(xiàn)(通過SPDY),所以中轉(zhuǎn)Server直接借鑒,也是很方便。
這種實(shí)現(xiàn)方案,可以參考:https://github.com/jcops/k8-web-terminal
整體Server使用GO語(yǔ)言的beego框架,簡(jiǎn)單好用。前段連接使用Websocket,后段連接使用了SPDY協(xié)議。
不過經(jīng)過代碼分析,感覺后段連接的實(shí)現(xiàn)不如純Websocket轉(zhuǎn)發(fā)簡(jiǎn)潔。所以這里更推薦下一種實(shí)現(xiàn)方式。
4.???? 中轉(zhuǎn)Server通過Websocket與K8S相連
因?yàn)镾PDY協(xié)議已經(jīng)被淘汰了,所以直接使用Websocket實(shí)現(xiàn),顯得更高大上,并且代碼也更簡(jiǎn)潔。
這里沒有找到參考實(shí)現(xiàn)的倉(cāng)庫(kù),直接貼一點(diǎn)我們自己的代碼實(shí)現(xiàn)。使用的包是:
import " github.com/gorilla/websocket"
后段連接主要代碼邏輯:
// Server去連接K8s,得到websocket的連接
ws, _, err := websocket.DefaultDialer.Dial(addr, h)
// 與前端的websocket,進(jìn)行proxy橋接
go k8stoweb(connFrontEnd, connBackEnd, errFrontEnd) go webtok8s(connBackEnd, connFrontEnd, errBackEnd)
// 其中,橋接函數(shù)如下
func replicateWebSocket (dst, src *websocket.Conn, errc chan error) { for { msgType, msg, err1 := src.ReadMessage() if err1 != nil { m := websocket.FormatCloseMessage(websocket.CloseNormalClosure, fmt.Sprintf("%v", err1)) if e, ok := err1.(*websocket.CloseError); ok { if e.Code != websocket.CloseNoStatusReceived { m = websocket.FormatCloseMessage(e.Code, e.Text) } } errc <- err1 _ = dst.WriteMessage(websocket.CloseMessage, m) break } err1 = dst.WriteMessage(msgType, msg) if err1 != nil { errc <- err1 break } } }
這種實(shí)現(xiàn),后段轉(zhuǎn)發(fā)比SPDY那個(gè)參考倉(cāng)庫(kù)更簡(jiǎn)潔,我們自己選用了此方式。
四、?? K8s的Websocket協(xié)議,是有擴(kuò)展的!
中轉(zhuǎn)Proxy說完,我們要好好說道一下這個(gè)前端。因?yàn)榍岸耸鞘褂肳ebsocket,經(jīng)過proxy中轉(zhuǎn),直接到達(dá)K8s。所以相當(dāng)于直接與K8s的Websocket協(xié)議互連。
所以這里就要引出實(shí)現(xiàn)中,遇到的最大的坑。即:K8s的exec在使用Websocket協(xié)議時(shí),是有擴(kuò)展的,并且擴(kuò)展規(guī)則是K8s自己設(shè)置的規(guī)則。 我們以 用戶敲下“l(fā)s”命令到容器,然后容器list文件列表為例來說明。
如果你直接發(fā)送”ls”內(nèi)容,那么肯定是不通的。因?yàn)镵8s根本不認(rèn)這種“輸入”。
K8s認(rèn)為websocket的報(bào)文內(nèi)容,有“頻道”的。不然一條cmd命令行執(zhí)行后,用戶無(wú)法判斷響應(yīng)的內(nèi)容是 stdout,還是 stderr。所以k8s這么約定:
1.???? websocket報(bào)文內(nèi)容的第一個(gè)字節(jié),用來表示“頻道”:
第一字節(jié)值
其余內(nèi)容含義
0
標(biāo)準(zhǔn)輸入
1
標(biāo)準(zhǔn)輸出
2
標(biāo)準(zhǔn)錯(cuò)誤
3
服務(wù)端異常信息
4
terminal窗口大小調(diào)整resize
參考:https://www.cnblogs.com/a00ium/p/10905279.html
不過文章中,調(diào)整窗口的行為,沒有提。最終是在
https://github.com/kubernetes-ui/container-terminal/blob/ba560d4f715f405beb0a64bab8fb29a21aac2671/container-terminal.js#L152
看到的。前端調(diào)整窗口時(shí),需要這么發(fā)送信息給K8s。
2.???? 頻道的內(nèi)容,需要使用Base64進(jìn)行編碼!
即要發(fā)送“l(fā)s”命令,需要向K8s發(fā)送的內(nèi)容為:
sendMsg := "0" + base64.Encoding("ls")
這樣發(fā)送才行,K8s才認(rèn)為是收到”ls”命令。
收到響應(yīng),要先去掉第一個(gè)字節(jié),然后再進(jìn)行base64解碼。
Ps:這里推薦一個(gè)websocket調(diào)試網(wǎng)站:http://coolaf.com/tool/chattest
用來連自己的中轉(zhuǎn)Proxy,比較方便。
3.???? 前端JS的實(shí)現(xiàn)
因?yàn)橹虚g的“自研Server”主要是進(jìn)行中轉(zhuǎn)Proxy,所以剛才提到的K8s接口內(nèi)容中,首字節(jié)頻道,以及響應(yīng)的編碼,其實(shí)都是交由前端來處理的。
這里可以直接參考k8s的web-ui的實(shí)現(xiàn):https://github.com/kubernetes-ui/container-terminal
其中的container-terminal.js文件中,主要實(shí)現(xiàn)如下:
//發(fā)送內(nèi)容
ws.send("0" + utf8_to_b64(data));
//接收內(nèi)容
switch(ev.data[0]) {
case '1':
case '2':
case '3':
term.write(b64_to_utf8(data));
break;
}
五、?? 總結(jié)
到此,如果你想自己實(shí)現(xiàn)K8s的web-terminal,并且增加各種權(quán)限控制之類的業(yè)務(wù)邏輯,應(yīng)該是沒有障礙了。還有哪里需要補(bǔ)充的也歡迎交流。
Docker Kubernetes web前端
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。