如何通过Web终端的方式访问容器?


【编者的话】在微服务大行其道的今天,容器恰巧又是微服务的主要载体,所以我们操作的对象也由最开始的「物理机」到「虚拟机」再到今天的「容器」。由于这些载体的变更,我们的使用方式也需要随之发生一些改变。比如一个最常用的登入操作,「虚拟机」下我们可能通过 SSH 的方式 ,但如果是容器呢?SSH 的方式就需要在每个容器中都运行一个 SSHd 进程,这种做法可行但略显繁琐,也不太符合一个容器只运行一个进程的思想。

那么有没有一个即方便快捷又安全的登入方式呢?

有,通过 Web Terminal 的方式,通过 Web 的方式即可以避免对客户端的依赖又能够实现用户权限控制。目前,有很多开源的 Web Terminal 的项目,基本上都是通过 SSH 代理的方式调用并返回一个 shell 的虚拟终端(pty)。
1.jpg

实现容器的 Web Terminal

架构图

2.jpg

前端 Web Termianl 页面

Linux 终端返回的内容会带很多特殊的字符,比如我输入一个 ls 指令,终端返回的结果如下:
'l'
's'
'\r\n'
'\x1b[0;0mRUNNING_PID\x1b[0m  \x1b[1;34mbin\x1b[0m          
\x1b[1;34mconf\x1b[0m         \x1b[1;34mlib\x1b[0m\r\nbash-4.3# '

这样我们就需要自己做穷举处理了,这里推荐使用一款的模拟 Terminal 的 JavaScript 库 xterm.js。这个库已经帮我们做了这些复杂操作。
<script>
var term = new Terminal();
term.open(document.getElementById('terminal'));
term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ')
</script>

可以看到它已经将 \x1B[1;3;31mxterm.js\x1B[0m 这些特殊字符变成了红色:
3.png

调用 Docker Daemon API 返回 Shell 虚拟终端

在平常的命令行操作下,我们经常会使用 docker exec -i -t <container_id> /bin/sh 来模拟一个 Shell 的伪终端。在 Web Terminal 实现里,我们需要通过 API 调用的方式来实现同样的操作。当然,我们首先要确保 Docker Daemon 的远程调用是开启的。

先调用 execCreate 来创建一个 Exec。在调用时,需要指定Tty,AttachStdin、AttachStdout和 AttachStderr 参数均为 true,Cmd 参数为 bash,这样才能获得 bash 进程的标准输入输出和错误;

request:
POST /v1.24/containers/e90e34656806/exec HTTP/1.1
Content-Type: application/json

{
"AttachStdin": true,
"AttachStdout": true,
"AttachStderr": true,
"Cmd": ["sh"],
"DetachKeys": "ctrl-p,ctrl-q",
"Tty": true,
...


如果调用 execCreate 成功,调用请求会返回该 Exec 的 ID,根据这个 ID 继续调用execStart 接口。在调用时,需要指定 Detach 为 False,Tty 为 True,这样才会返回一个 HTTP 的 stream:

request:
POST /v1.24/exec/e90e34656806/start HTTP/1.1
Content-Type: application/json

{
"Detach": false,
"Tty": true


response:
HTTP/1.1 200 OK
Content-Type: application/vnd.docker.raw-stream

{{ STREAM }} 

d-terminal

d-terminal 是这个系统的核心,它分成两个部分:
  1. 一部分用于处理用户端的输入和输出,以及存储和展示后端 Docker Dameon 主机的 IP 和 container_id。因为像 top 这样的监控命令需要服务端定时推送数据给客户端,所以使用了 WebSocket 协议以支持服务端推送。
  2. 另一部分用于调用 Docker Daemon 返回虚拟终端。对于终端来说,通常是你输入一个字符就会立马返回,直到你输入一个 "归位键" 终端才会把你输入的字符拼接成一个字符串并发送给 shell 解释器,并将 shell 解释器的结果返回。为了提升使用流畅性,新启用了一个线程去调用 Docker Daemon API,当然也可以使用像 epoll 这样的多路复用技术来实现。


4.jpg

d-terminal 是使用 Python 实现的 Web 应用,核心代码如下:
@sockets.route('/echo')
def echo_socket(ws):
...
# 调用 Docker API 返回一个虚拟终端
docker_daemon_sock = get_tty()._sock
# 启动一个与 Docker Daemon 通讯的子线
docker_daemon_sock_thd = DockerDaemonSock(ws, docker_daemon_sock)
docker_daemon_sock_thd.start()

while not ws.closed:
    message = ws.receive() # 接收 terminal 的输入
    # 将用户的输入发送那个 docker daemon
    docker_daemon_sock.send(bytes(message, encoding='utf-8'))

子线程 DockerDaemonSock 类

class DockerDaemonSock(threading.Thread):
def __init__(self, ws, docker_daemon_sock):
    super(DockerDaemonSock, self).__init__()
    self.ws = ws
    self.docker_daemon_sock = docker_daemon_sock

def run(self):
    while not self.ws.closed:
        try:
            # 接收 docker daemon 的返回
            resp = self.docker_daemon_sock.recv(2048)
            if resp:
                # 将 docker daemon 的返回发送给前端 terminal
                self.ws.send(str(resp, encoding='utf-8'))
            else:
                print("docker socket closed.")
                self.ws.close()
        except Exception as e:
            print("docker termial socket error: {}".format(e))
            self.ws.close()

总结

上述仅仅是描述了一个最基本的实现,完全是为了抛砖引玉,后续可以通过在中间层添加一些扩展,比如,用户权限的分配,与自己环境中的容器编排引擎集成等,最终作为 PaaS 平台的一个基础的组成部分。

最后,上述的 demo 可去 GitHub 具体查看。效果如下:
5.gif


参考:《一种新的进入容器的方式: WebSocket + Docker Remote API

原文链接:https://mp.weixin.qq.com/s/zlHJTxDeHgjn9A9XuYp9fQ

0 个评论

要回复文章请先登录注册