利用kubernetes exec接口实现任意容器的web-terminal

举报
tsjsdbd 发表于 2021/07/02 18:34:23 2021/07/02
【摘要】 利用kubernetes exec接口实现任意容器的web-terminal,介绍了K8s的Websocket接口的自定义规则,以及自己如何实现任意容器的Web-terminal功能的参考实现。

 

一、   Kubectl exec命令登录指定容器

如果你用过k8s,那么kubectl exec 命令一定不要错过。简单的敲上:

kubectl exec -it pod名 -- /bin/sh

就可以登录到任意节点的指定的容器里面,效果和使用ssh登录到一台机器进行操作一模一样,非常的方便。

那有没有想过:

  • 这个功能是怎么实现的呢?
  • 能不能在Web网页上面,直接拥有这个功能呢?


接下来,我们一一解读。

 

二、   Kubectl exec实现

1.     最底层实现:Docker容器的exec命令

K8s实现的“进入某个容器”的功能,底层本质是Docker容器通过exec进入容器的扩展。即从本机容器,扩展为任意节点的容器。

所以咱们先看看Docker怎么通过exec进入容器的呢?

docker exec -it 容器id /bin/sh

通过上面的命令,就可以进入到容内部。 本质是新建了一个“与目标容器,共享namespace的”新的shell进程。所以该shell进程,看到的世界,就是容器内的世界了。

    那么K8s要做的就是,跨节点利用Docker的这个功能。

 

2.     Kubectl到容器的超长路径

kubectl命令行工具,到容器内部,这里经过的网络路径其实是很长的。如下:


因为exec命令行,是实时交互的。即输入和输出,实时发生。

所以K8s选择了使用 类似Websocket 这种双向实时通信的协议,来传递输入/输出内容。

Kubectl <---(双向实时协议)---> Kube-Api-Server <---(双向实时协议)--->节点kubelet

 

3.     Kubectl实现exec代码简析

通过简单查询 kubectl 的源码:

import "k8s.io/client-go/tools/remotecommand"

 

//这里初始化了一个 remote-cmd 的对象

exec, err := remotecommand.NewSPDYExecutor(config, method, url)
if err != nil {
  
return err
}

 

//这里开始,将输入输出,进行实时传递(Stream
return exec.Stream(remotecommand.StreamOptions{
   Stdin:             stdin,
   Stdout:            stdout,
   Stderr:            stderr,
   Tty:  
            tty,
   TerminalSizeQueue: terminalSizeQueue,
})

这里可以看到,kubectl使用了一个叫 SPDY 的协议去连K8sAPI-server。 这里的SPDY协议是Google公司搞的,基本类似Websocket可以进行双向实时传输,但是这个协议已经被淘汰了,被HTTP2所替代。见k8sissueSPDY is deprecated. Switch to HTTP/2

https://github.com/kubernetes/kubernetes/issues/7452

 

好在K8sAPI-server除了支持 SPDY协议,也支持Websocket协议。

https://github.com/kubernetes/kubernetes/issues/89163

 

三、   网页web-terminal直连容器

有了前面的背景知识,那么如果想在web网页中,实现exec实时登录到容器里面。那么可以有以下思路:

首先,SPDY是个淘汰的协议,所以前端JS代码可供参考的很少。而前端对Websocket的支持则很广泛。所以咱们Web侧选择使用Websocket

于是,实现的方案有如下几种:

1.     web网页使用Websocket直连K8s

这种场景,虽然看着最直接,但是适用场景反而有限。 因为权限隔离问题,一般情况你不可能让前端获得最大的k8s权限,允许进入任意容器中。


 

2.     Web网页经过一个后端,中转至K8s

在前端和K8s的中间,增加一个自研的Server,可以很好的控制权限隔离,封装K8s到业务的转换。


那么对于自研Server来说,它就是一个类似Proxy的程序。其中,[Web<---->自研Server]这一段肯定是使用Websocket协议。但是[自研Server<---->K8s]这一段,则有2种实现:1种是直接使用Websocket协议。第2种是使用SPDY协议(即利用 remotecommand代码实现)。

下面分2种场景分析。

 

3.     中转Server通过SPDY与K8s相连

因为kubectl代码中有exec的实现(通过SPDY),所以中转Server直接借鉴,也是很方便。


这种实现方案,可以参考:https://github.com/jcops/k8-web-terminal

整体Server使用GO语言的beego框架,简单好用。前段连接使用Websocket,后段连接使用了SPDY协议。

不过经过代码分析,感觉后段连接的实现不如纯Websocket转发简洁。所以这里更推荐下一种实现方式。

 

4.     中转Server通过Websocket与K8S相连

因为SPDY协议已经被淘汰了,所以直接使用Websocket实现,显得更高大上,并且代码也更简洁。


 这里没有找到参考实现的仓库,直接贴一点我们自己的代码实现。使用的包是:

import " github.com/gorilla/websocket"

后段连接主要代码逻辑:

// Server去连接K8s,得到websocket的连接
ws, _, err := websocket.DefaultDialer.Dial(addr, h)
 
// 与前端的websocket,进行proxy桥接
go k8stoweb(connFrontEnd, connBackEnd, errFrontEnd)

go webtok8s(connBackEnd, connFrontEnd, errBackEnd)
 
// 其中,桥接函数如下
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

      }

   }

}
 

这种实现,后段转发比SPDY那个参考仓库更简洁,我们自己选用了此方式。

 

四、   K8s的Websocket协议,是有扩展的!

中转Proxy说完,我们要好好说道一下这个前端。因为前端是使用Websocket,经过proxy中转,直接到达K8s。所以相当于直接与K8sWebsocket协议互连。

 

所以这里就要引出实现中,遇到的最大的坑。即:K8sexec在使用Websocket协议时,是有扩展的,并且扩展规则是K8s自己设置的规则。 我们以 用户敲下“ls”命令到容器,然后容器list文件列表为例来说明。


如果你直接发送”ls”内容,那么肯定是不通的。因为K8s根本不认这种“输入”。

 

K8s认为websocket的报文内容,有“频道”的。不然一条cmd命令行执行后,用户无法判断响应的内容是 stdout,还是 stderr。所以k8s这么约定:

 

1.     websocket报文内容的第一个字节,用来表示“频道”:

第一字节值

其余内容含义

0

标准输入

1

标准输出

2

标准错误

3

服务端异常信息

4

terminal窗口大小调整resize

 

参考:https://www.cnblogs.com/a00ium/p/10905279.html

不过文章中,调整窗口的行为,没有提。最终是在

https://github.com/kubernetes-ui/container-terminal/blob/ba560d4f715f405beb0a64bab8fb29a21aac2671/container-terminal.js#L152


看到的。前端调整窗口时,需要这么发送信息给K8s

 

2.     频道的内容,需要使用Base64进行编码!

即要发送“ls”命令,需要向K8s发送的内容为:

sendMsg := "0" + base64.Encoding("ls")

 

这样发送才行,K8s才认为是收到”ls”命令。


收到响应,要先去掉第一个字节,然后再进行base64解码。

 Ps:这里推荐一个websocket调试网站:http://coolaf.com/tool/chattest

用来连自己的中转Proxy,比较方便。

 

3.     前端JS的实现

因为中间的“自研Server”主要是进行中转Proxy,所以刚才提到的K8s接口内容中,首字节频道,以及响应的编码,其实都是交由前端来处理的。

 

这里可以直接参考k8sweb-ui的实现:https://github.com/kubernetes-ui/container-terminal

其中的container-terminal.js文件中,主要实现如下:

 

//发送内容
ws.send("0" + utf8_to_b64(data));
 
//接收内容
switch(ev.data[0]) {
case '1':
case '2':
case '3':
term.write(b64_to_utf8(data));
break;
}

  

4.     避免bash泄漏

每次连接后就容易在pod中残留一个bash的进程。

这里需要前端在任意中断连接前,发送了一个特殊的message 0ZXhpdA0K


其中“0”表示标准输入,前面提过。

ZXhpdA0K”经过base64反编码后,其实就是 “exit”命令。

在点击页面close时,或者任何关闭web-terminal 行为,前端都记得发送该信息,避免bash进程泄漏。

五、   总结

到此,如果你想自己实现K8sweb-terminal,并且增加各种权限控制之类的业务逻辑,应该是没有障碍了。还有哪里需要补充的也欢迎交流。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。