Skip to main content
版本:v1.2

YurtTunnel

1. 背景简介

在应用的部署和运维过程中,用户常常需要获取应用的日志,或直接登录到应用的运行环境中进行调试。在 Kubernetes 环境中,我们通常使用 kubectl log,kubectl exec 等指令来实现这些需求。如下图所示,在 kubectl 请求链路上, kubelet 将扮演服务器端,负责处理由 kube-apiserver(KAS) 转发来的请求,这就要求 KAS 和 kubelet 之间需要存在一条网络通路,允许 KAS 主动访问 kubelet。

img

云与边一般位于不同网络平面,同时边缘节点普遍位于防火墙内部,采用云(中心)边协同架构,将导致原生 K8s 系统的运维监控能力面临如下挑战:

  • K8s 原生运维能力缺失(如 kubectl logs/exec 等无法执行)
  • 社区主流监控运维组件无法工作(如 Prometheus/metrics-server )

为了支持通过云端节点对边缘端应用进行运维操作,我们必须在云、边之间建立反向运维通道。

2. 反向通道

在 OpenYurt 中,我们引入了专门的组件 YurtTunnel 负责解决云边通信问题。反向通道是解决跨网络通信的一种常见方式,而 YurtTunnel 的本质也是一个反向通道。 它是一个典型的C/S结构的组件,由部署于云端的 Yurt—Tunnel-Server 和部署于边缘节点上的 Yurt-Tunnel—Agent组成。YurtTunnel的部署结构如下图所示, 反向通道整体的工作流程包括以下几个步骤:

  • 在管控组件所在网络平面内,部署 Yurt-Tunnel-Server。
  • Yurt-Tunnel-Server 对外开放一个公网可以访问的 IP。
  • 在每个边缘节点部署一个 Yurt-Tunnel-Agent,并且通过 Server 的公网 IP 与 Server 建立长连接。
  • 管控组件对边缘节点的访问请求都将转发到 Yurt-Tunnel-Server。
  • Yurt-Tunnel-Server 再将请求通过对应的长连接发往目标节点。

img

3. 实现方式

在 K8s 云边一体化架构中构建一个安全、非侵入、可扩展的反向通道解决方案,方案中至少需要包括以下三部分能力。

  • 隧道构建
  • 隧道两端证书的自管理
  • 云端组件请求被无缝导流到隧道

YurtTunnel 的架构模块如下图:

img

  1. 隧道构建
  • 当边缘的 Yurt-Tunnel-Agent 启动时,会根据访问地址与 Yurt-Tunnel-Server 建立连接并注册,并周期性检测连接的健康状态以及重建连接等。

Yurt-Tunnel-Agent注册的身份信息如下:

"agentID": {NodeName}
"agentIdentifiers": ipv4={nodeIP}&host={NodeName}"
  • 当 Yurt-Tunnel-Server 收到云端组件的请求时,需要把请求转发给对应的 Yurt-Tunnel-Agent 。因为除了转发初始请求之外,该请求 session 后续还有数据返回或者数据的持续转发(如 kubectl exec )。因此需要双向转发数据。同时需要支持并发转发云端组件的请求,意味需要为每个请求生命周期建立一个独立的标识。所以设计上一般会有两种方案。

方案 1: 初始云边连接仅通知转发请求,Yurt-Tunnel-Agent 会和云端建立新连接来处理这个请求。通过新连接可以很好的解决请求独立标识的问题,同时并发也可以很好的解决。但是为每个请求都需要建立一个连接,将消耗大量的资源。

方案 2: 仅利用初始云边连接来转发请求,大量请求为了复用同一条连接,所以需要为每个请求进行封装,并增加独立标识,从而解决并发转发的诉求。同时由于需要复用一条连接,所以需要解耦连接管理和请求生命周期管理,即需要对请求转发的状态迁移进行独立管理。该方案涉及到封包解包,请求处理状态机等,方案会复杂一些。

  • OpenYurt 选择的 ANP 组件,采用的是上述方案2,这个和我们的设计初衷也是一致的。
  • 请求转发链路构建封装在 Packet_DialRequest 和 Packet_DialResponse 中,其中 Packet_DialResponse.ConnectID 用于标识 request ,相当于 tunnel 中的 requestID。请求以及关联数据封装在 Packet_Data 中。Packet_CloseRequest 和 Packet_CloseResponse 用于转发链路资源回收。具体可以参照下列时序图:

img

  • RequestInterceptor 模块的作用 从上述分析可以看出,Yurt-Tunnel-Server 转发请求之前,需要请求端先发起一个 Http Connect 请求来构建转发链路。但是为 Prometheus、metrics-server 等开源组件增加相应处理会比较困难,因此在 Yurt-Tunnel-Server 中增加请求劫持模块 Interceptor ,用来发起 Http Connect 请求。
  1. 证书管理

为了保证云边通道的长期安全通信,同时也为了支持 https 请求转发,YurtTunnel 需要自行生成证书并且保持证书的自动轮替。具体细节如下:

# 1. Yurt-Tunnel-Server证书:
# https://github.com/openyurtio/openyurt/blob/master/cmd/yurt-tunnel-server/app/start.go#L120-L139
- 证书存储位置: /var/lib/yurt-tunnel-server/pki/yurt-tunnel-server-xxx.pem
- SignerName: "kubernetes.io/kubelet-serving"
- CommonName: "system:node:tunnel-server"
- Organization: {"system:nodes"}
- Subject Alternate Name values: {x-tunnel-server-svc, x-tunnel-server-internal-svc的ips和dns names}
- KeyUsage: ["key encipherment", "digital signature", "server auth"]

# 2. tunnel proxy client证书: yurt-tunnel-server中使用,用于和边缘节点的组件(如kubelet)建立tls连接,从而实现请求转发。
# https://github.com/openyurtio/openyurt/blob/master/cmd/yurt-tunnel-server/app/start.go#L146-L152
- 证书存储位置: /var/lib/yurt-tunnel-server/pki/yurt-tunnel-server-proxy-client-xxx.pem
- SignerName: "kubernetes.io/kube-apiserver-client"
- CommonName: "tunnel-proxy-client"
- Organization: {"openyurt:yurttunnel"}
- KeyUsage: ["key encipherment", "digital signature", "client auth"]

# 3. Yurt-Tunnel-Agent证书:
# https://github.com/openyurtio/openyurt/blob/master/cmd/yurt-tunnel-agent/app/start.go#L99-L107
- 证书存储位置: /var/lib/yurt-tunnel-agent/pki/yurt-tunnel-agent-xxx.pem
- SignerName: "kubernetes.io/kube-apiserver-client"
- CommonName: "tunnel-agent-client"
- Organization: {"openyurt:yurttunnel"}
- KeyUsage: ["key encipherment", "digital signature", "client auth"]

# 4. yurt-tunnel证书申请(CSR)均由Yurt-Controller-Manager来approve
# https://github.com/openyurtio/openyurt/blob/master/pkg/controller/certificates/csrapprover.go

# 5. 证书自动轮替处理
# https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/client-go/util/certificate/certificate_manager.go#L224
  1. 无缝导流云端组件请求到隧道

因为需要无缝把云端组件的请求转发到 Yurt-Tunnel-Server ,也意味不需要对云端组件进行任何修改。因此需要对云端组件的请求进行分析,目前组件的运维请求主要有以下两种类型:

  • 类型1: 直接使用 IP 地址访问,如: http://{nodeIP}:{port}/{path}
  • 类型2: 使用域名访问, 如: http://{NodeName}:{port}/{path}

针对不同类型请求的导流,需要采用不同方案。

方案1: 使用 iptables dnat rules 来保证类型1的请求无缝转发到 Yurt-Tunnel-Server

# 相关iptables rules维护代码: https://github.com/openyurtio/openyurt/blob/master/pkg/yurttunnel/iptables/iptables.go
# Yurt-Tunnel-Server维护的iptables dnat rules如下:
[root@xxx /]# iptables -nv -t nat -L OUTPUT
TUNNEL-PORT tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* edge tunnel server port */

[root@xxx /]# iptables -nv -t nat -L TUNNEL-PORT
TUNNEL-PORT-10255 tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:10255 /* jump to port 10255 */
TUNNEL-PORT-10250 tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:10250 /* jump to port 10250 */

[root@xxx /]# iptables -nv -t nat -L TUNNEL-PORT-10255
RETURN tcp -- * * 0.0.0.0/0 127.0.0.1 /* return request to access node directly */ tcp dpt:10255
RETURN tcp -- * * 0.0.0.0/0 172.16.6.156 /* return request to access node directly */ tcp dpt:10255
DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* dnat to tunnel for access node */ tcp dpt:10255 to:172.16.6.156:10264

方案2: 使用 dns 域名解析 NodeName 为 Yurt-Tunnel-Server 的访问地址,从而使类型 2 请求无缝转发到 yurt-tunnel。其工作原理如图所示:

img

  • Yurt-Tunnel-Server会维护两个Service地址:
    • x-tunnel-server-svc: 主要expose 10262/10263端口,用于从公网访问Yurt-Tunnel-Server。如Yurt-Tunnel-Agent
    • x-tunnel-server-internal-svc: 主要用于云端组件从内部网络访问,如prometheus,metrics-server等
  • Yurt-Tunnel-Server 内置一个 DNS Controller,动态配置 Core DNS Host 记录,维持 NodeName 与 IP 的映射关系(Cloud Node 根据 IP 直接可达,即直接映射为 Node IP;Edge Node 需要通过 Tunnel 隧道代理通信,即映射到 Yurt-Tunnel-Server Internal Service 的 cluster ip)
  • 当云端组件通过 NodeName 访问 Edge 节点时,默认会通过 CoreDNS 做域名解析, 对 Edge Node 的请求会被定向到 Yurt-Tunnel-Server 的 internal service 的 ClusterIP,进而借助 kube-proxy 的转发能力,将请求负载均衡到健康的 Yurt-Tunnel-Server Pod 内
  • Yurt-Tunnel-Server 会检查请求 Host 格式,当 Host 格式 NodeName 时,则通过节点名找到正确的 Agent 后端,转发数据
  1. 端口扩展

如果用户需要访问边缘的其他端口(10250 和 10255 之外),那么需要在 iptables 中增加相应的 dnat rules 或者 x-tunnel-server-internal-svc 中增加相应的端口映射,如下所示:


# 例如需要访问边缘的9051端口
# 新增iptables dnat rule:
[root@xxx /]# iptables -nv -t nat -L TUNNEL-PORT
TUNNEL-PORT-9051 tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:9051 /* jump to port 9051 */

[root@xxx /]# iptables -nv -t nat -L TUNNEL-PORT-9051
RETURN tcp -- * * 0.0.0.0/0 127.0.0.1 /* return request to access node directly */ tcp dpt:9051
RETURN tcp -- * * 0.0.0.0/0 172.16.6.156 /* return request to access node directly */ tcp dpt:9051
DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* dnat to tunnel for access node */ tcp dpt:9051 to:172.16.6.156:10264

# x-tunnel-server-internal-svc中新增端口映射
spec:
ports:
- name: https
port: 10250
protocol: TCP
targetPort: 10263
- name: http
port: 10255
protocol: TCP
targetPort: 10264
- name: dnat-9051 # 新增映射
port: 9051
protocol: TCP
targetPort: 10264

当然上述的 iptables dnat rules 和 service 端口映射,都是由 Yurt-Tunnel-Server 自动更新。用户只需要在 kube-system 下的 yurt-tunnel-server-cfg configmap 中增加端口配置即可。具体如下:

apiVersion: v1
data:
http-proxy-ports: "9051" # 新增HTTP端口
https-proxy-ports: "" # 新增HTTPs端口
kind: ConfigMap
metadata:
name: yurt-tunnel-server-cfg
namespace: kube-system