容器终端现在我们就是做这个功能用于连接容器但容器不像虚拟机那样经常登录进去维护容器是一个封装好的应用环境即开即用一个容器启动后基本不再对里面的环境做相关操作即使修改也是重新构建镜像我们做这个功能主要还是方便故障排查进入容器里做一些调试工作。要想实现类似SSH终端功能并非易事主要难点在于页面与连接的目标是实时交互的。说起实时交互相信大家都有接触过例如qq、微信、在线客服这些都是像一些网页版的在线聊天系统常用的实现方案就是用websocket。WebSocket协议与HTTP的主要区别HTTP是无状态协议由客户端发起请求客户端与服务器“一问一答”因此服务器端无法主动向客户端发送信息。而WebSocket是基于TCP长连接的协议客户端与服务器建立连接后服务器端随时能向客户端发送信息。WebSocket协议的主要价值在于其与HTTP的差异服务器端与客户端能够保持实时的双向通信使其在某些应用情景下比HTTP更能满足技术需求。Django Web框架实现WebSocket主要有两种方式channels和dwebsocket。Channels是针对Django项目的一个增强框架使得Django不仅支持HTTP协议还能支持WebSocket协议。为了更好的模拟shell终端还需要一个前端库xterm.js这是一个比较成熟的shell终端模拟库目前大部分公司实现的webssh都是用的这个。官网Xterm.js大概思路就是这样的1、写一个前端页面里面用xterm.js模拟shell终端再打开一个websocket监听终端里用户输入内容推送到django在浏览器端模拟 shell 终端, 监听用户输入通过 websocket 将用户输入的内容上传到 django2、django 接受到用户输入的内容, 将内容通过k8s stream与容器建立通道在容器里执行将返回的处理结果返回给django。3、django 将k8s stream返回的结果通过 websocket 返回到终端页面1、前端script typetext/html idbarDemoa classlayui-btn layui-btn-primary layui-btn-xs lay-eventyamlYAML/aa classlayui-btn layui-btn-xs lay-eventlog查看日志/aa classlayui-btn layui-btn-normal layui-btn-xs lay-eventterminal stylecolor: #FFF;background-color: #385985终端/aa classlayui-btn layui-btn-danger layui-btn-xs lay-eventdel重建/a/script} else if(obj.event terminal){// 逗号拼接容器名, 例如containersc1,c2cs data[containers];containers ;for(let c in cs) {if (c cs.length-1) {containers cs[c][c_name] ,} else {containers cs[c][c_name]}}layer.open({title: 容器终端,type: 2, // 加载层从另一个网址引用area : [ 50%, 60% ],content: {% url terminal %}?namespace data[namespace] pod_name data[name] containers containers,});}2、服务端re_path(^terminal/$, views.terminal, nameterminal),from django.views.decorators.clickjacking import xframe_options_exemptxframe_options_exemptk8s.self_login_requireddef terminal(request):namespace request.GET.get(namespace)pod_name request.GET.get(pod_name)containers request.GET.get(containers).split(,) # 返回 nginx1,nginx2转成一个列表方便前端处理auth_type request.session.get(auth_type) # 认证类型和token用于传递到websocketwebsocket根据sessionid获取token让websocket处理连接k8s认证用token request.session.get(token)connect {namespace: namespace, pod_name: pod_name, containers: containers, auth_type: auth_type, token: token}return render(request, workload/terminal.html, {connect: connect})容器终端页面workload/terminal.html!DOCTYPE htmlhtmlheadmeta charsetutf-8title容器终端/titlelink href/static/xterm/xterm.css relstylesheet typetext/css/stylebody {background-color: black}.terminal-window {background-color: #2f4050;width: 99%;color: white;line-height: 25px;margin-bottom: 10px;font-size: 18px;padding: 10px 0 10px 10px}.containers select,.containers option {width: 100px;height: 25px;font-size: 18px;color: #2F4056;text-overflow: ellipsis;outline: none;}/style/headbodydiv classterminal-windowdiv classcontainersPod名称{{ connect.pod_name }}容器select namecontainer_name idcontainerSelect{% for c in connect.containers %}option value{{ c }}{{ c }}/option{% endfor %}/select/div/divdiv idterminal stylewidth: 100px;/div/bodyscript src/static/xterm/xterm.js/scriptscriptvar term new Terminal({cursorBlink: true,rows:70});term.open(document.getElementById(terminal));var auth_type {{ connect.auth_type }};var token {{ connect.token }};var namespace {{ connect.namespace }};var pod_name {{ connect.pod_name }};var container document.getElementById(containerSelect).value;// 打开一个 websocketdjango也会把sessionid传过去var ws new WebSocket(ws:// window.location.host /workload/terminal/ namespace / pod_name / container /?auth_type auth_type token token);//打开websocket连接并打开终端ws.onopen function () {// 实时监控输入的字符串发送到后端term.on(data, function (data) {ws.send(data);});ws.onerror function (event) {console.log(error: e);};//读取服务器发送的数据并写入web终端ws.onmessage function (event) {term.write(event.data);};// 关闭websocketws.onclose function (event) {term.write(\n\r\x1B[1;3;31m连接关闭\x1B[0m);};};/script/html3、Django Channels3.1 安装模块pip install channelspip install channels_redischannels是Django的扩展模块用于处理WebSocket。channels_redis 使用redis作为存储维护不同消息传递。3.2 配置Channels在settings.py文件中注册channelsINSTALLED_APPS [django.contrib.admin,django.contrib.auth,django.contrib.contenttypes,django.contrib.sessions,django.contrib.messages,django.contrib.staticfiles,channels]ASGI_APPLICATION devops.routing.application3.3配置路由devops/routing.py # 路由文件from channels.auth import AuthMiddlewareStackfrom channels.routing import ProtocolTypeRouter, URLRouterfrom django.urls import re_pathfrom devops.consumers importStreamConsumerapplication ProtocolTypeRouter({websocket: AuthMiddlewareStack(URLRouter([re_path(r^workload/terminal/(?Pnamespace.*)/(?Ppod_name.*)/(?Pcontainer.*)/,StreamConsumer),])),})正则匹配所有以workload/terminal开头的websocket连接都交由名为StreamConsumer的处理。3.4WebSocket服务端消费devops/consumers.py#建立K8s Stream容器通道和处理前端与DjangoWebsocket通道3.5配置Redisdocker run --name redis -d -p 6379:6379 redis:3在settings.py配置文件添加redis配置内容如下CHANNEL_LAYERS {default: {BACKEND: channels_redis.core.RedisChannelLayer,CONFIG: {hosts: [(192.168.31.61, 6379)],},},}小结终端-弹出框从另一个加载/workload/terminal/?namespacedefaultpod_nameweb-7989784f96-wxbnmcontainersnginx-执行页面里websocket建立连接/workload/terminal/default/web-7989784f96-wxbnm/nginx/-routing.py路由大奥StreamConsumer处理K8s Stream与容器建立通道