Openyurt Yurt-Tunnel DNS模式实践

2,761 阅读4分钟

在Openyurt中,Yurt-Tunnel是用于构建云边隧道的重要组件。在Yurt-Tunnel 详解|如何解决 K8s 在云边协同下的运维监控挑战一文中,详细的介绍了Yurt-Tunnel的基本原理。在本文中,将进一步介绍Yurt-Tunnel的DNS模式以及具体的配置方法。

DNAT方式的缺陷

Yurt-Tunnel支持DNAT和DNS两种导流模式。云端yurt-tunnel-server通过为节点配置DNAT规则,默认将发往kubelet两个端口(10250,10255)的请求转发到yurt-tunnel-server上。也就是说,在DNAT模式下,当执行kubectl logs/exec时,apiserver对边缘节点kubelet的请求会被导流到yurt-tunnel-server,实现云边隧道通信。 DNAT方式实现了无缝导流,不需要云边组件做额外配置,开箱即用。但DNAT模式也存在一些弊端:

  1. 在边缘计算场景下,边缘节点的NodeIP有可能是重复的,仅通过NodeIP:Port,yurt-tunnel-server无法唯一标识一个边缘节点。

  2. DNAT规则只有在yurt-tunnel-server所在的节点才会被设置,当需要访问边缘的云端组件(如apiserver,promethues)与yurt-tunnel-server不在同一个节点时,由于没有设置DNAT规则,云端组件将不能通过隧道访问边缘组件。例如下图的场景:

image.png

要解决以上两个问题,需要使用Yurt-Tunnel的DNS模式。

DNS模式原理

Yurt-Tunnel 详解|如何解决 K8s 在云边协同下的运维监控挑战一文介绍了DNS模式的原理:

# dns域名解析原理:
1. yurt-tunnel-server向kube-apiserver创建或更新yurt-tunnel-nodes configmap, 
其中tunnel-nodes字段格式为: {x-tunnel-server-internal-svc clusterIP}  {nodeName},
确保记录了所有nodeName和yurt-tunnel-server的service的映射关系
2. coredns pod中挂载yurt-tunnel-nodes configmap,同时使用host插件使用configmap的dns records
3. 同时在x-tunnel-server-internal-svc中配置端口映射,10250映射到1026310255映射到102644. 
通过上述的配置,可以实现http://{nodeName}:{port}/{path}请求无缝转发到yurt-tunnel-servers

其流程大致如下:

image.png

当apiserver接收到exec/logs请求时,apiserver使用节点hostname来访问kubelet。节点hostname会被集群内CoreDNS解析到yurt-tunnel-server的service地址,达到导流的目的。

要把这套流程打通,需要解决以下几个问题:

  1. 要配置apiserver以及其他云端组件(例prometheus)使用节点hostname作为域名来访问kubelet和其他边缘组件。
  2. 要配置apiserver以及其他云端组件(例promethues)使用CoreDNS作为DNS服务器。
  3. 要解决问题2,又衍生出一个新问题:云端组件要通过service IP访问CoreDNS,为了不让云端访问到边缘的CoreDNS地址,我们需要为云端开启流量闭环功能———即云端也要部署Yurthub,并配置kube-proxy通过Yurthub访问apiserver,通过流量闭环功能,在Yurthub中把边缘的CoreDNS地址过滤掉。

示例

接下来我们通过示例,来演示如何配置Yurt-Tunnel DNS模式,使得K8S原生接口(kubectl logs/exec)和Promethues可以正常工作。本示例可以在本地虚机环境运行,环境清单如下:

  1. master和worker节点各一个;
  2. K8S集群版本为v1.18.9,使用kubeadm默认配置部署原生集群;
  3. 使用yurt convert或者手工部署的方式转换成Openyurt集群,Openyurt镜像使用当前最新的master分支(commit id: 33e3445)

依赖一些Openyurt的最新特性和优化,使用旧版本的Openyurt镜像可能导致示例无法正常工作。

# master-1为云端节点
# worker-1为边缘节点
$ kubectl get nodes -owide
NAME       STATUS   ROLES    AGE     VERSION   INTERNAL-IP      EXTERNAL-IP   OS-IMAGE                KERNEL-VERSION           CONTAINER-RUNTIME
master-1   Ready    master   2d18h   v1.18.9   192.168.33.220   <none>        CentOS Linux 7 (Core)   3.10.0-1127.el7.x86_64   docker://19.3.15
worker-1   Ready    <none>   2d18h   v1.18.9   192.168.33.221   <none>        CentOS Linux 7 (Core)   3.10.0-1127.el7.x86_64   docker://19.3.15

流量闭环以及CoreDNS配置

云端部署Yurthub

要解决问题3,需要在云端部署Yurthub。从worker-1节点拷贝一份Yurthub manifest。稍作修改,添加--disabled-resource-filters=discardcloudservice flag,保存到master-1的manifest目录下:

# vi /etc/kubernetes/manifests/yurt-hub.yaml
...
    command:
    - yurthub
    - --v=2
    - --server-addr=https://192.168.33.220:6443
    - --node-name=$(NODE_NAME)
    - --access-server-through-hub=true
    - --disabled-resource-filters=discardcloudservice # 添加--disabled-resource-filters=discardcloudservice
...

Yurthub数据过滤框架默认开启discardcloudservice功能,会过滤掉一些云端专用的service,例如x-tunnel-server-internal-svc。但在云端,我们需要通过x-tunnel-server-internal-svc service来访问yurt-tunnel-server,因此我们需要禁用这个filter。 待Yurthub正常启动后,需要修改kubelet配置,让kubelet通过Yurthub访问apiserver。kubelet的配置方式可参考文档:github.com/openyurtio/…

为Yurthub添加RBAC

当前版本Yurthub遗漏了一些rbac规则,在修复前,我们先手动补上:

$ cat <<EOF | kubectl apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: yurt-hub
rules:
  - apiGroups:
      - ""
    resources:
      - events
    verbs:
      - get
  - apiGroups:
      - apps.openyurt.io
    resources:
      - nodepools
    verbs:
      - list
      - watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: yurt-hub
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: yurt-hub
subjects:
  - apiGroup: rbac.authorization.k8s.io
    kind: Group
    name: system:nodes
EOF

设置节点池

流量闭环功能依赖节点池,我们为云边各自创建一个节点池:

$ cat <<EOF | kubectl apply -f -
apiVersion: apps.openyurt.io/v1alpha1
kind: NodePool
metadata:
  name: master
spec:
  type: Cloud
---
apiVersion: apps.openyurt.io/v1alpha1
kind: NodePool
metadata:
  name: worker
spec:
  type: Edge
EOF

master-1worker-1节点分别加入masterworker节电池。

$ kubectl label node master-1 apps.openyurt.io/desired-nodepool=master
node/master-1 labeled
$ kubectl label node worker-1  apps.openyurt.io/desired-nodepool=worker
node/worker-1 labeled

修改CoreDNS相关配置

kubeadm默认用Deployment的方式部署CoreDNS,在云边场景下,每个节点池都需要CoreDNS的能力。这里我们以Daemonset方式重新部署CoreDNS,修改的过程中,同时为该Daemonset增加以下Volume:

        volumeMounts:
        - mountPath: /etc/edge
          name: hosts
          readOnly: true
...

      volumes:
      - configMap:
          defaultMode: 420
          name: yurt-tunnel-nodes
        name: hosts

上述新增的yurt-tunnel-nodes ConfigMap volume保存着各节点的DNS记录,由yurt-tunnel-server负责维护:

$ kubectl get cm -n kube-system yurt-tunnel-nodes -oyaml
apiVersion: v1
data:
  tunnel-nodes: "10.106.217.16\tworker-1\n192.168.33.220\tmaster-1"
kind: ConfigMap
....

该配置将worker-1解析到10.106.217.16,也就是x-tunnel-server-internal-svc service地址。将master-1解析到192.168.33.220,也就是master-1自身的IP地址。显然的,master-1访问自己不需要通过yurt-tunnel-server

接下来修改coredns ConfigMap,添加hosts插件:

$ kubectl edit cm -n kube-system coredns -oyaml
apiVersion: v1
data:
  Corefile: |
    .:53 {
        errors
        health {
           lameduck 5s
        }
        ready
        hosts /etc/edge/tunnel-nodes { # 增加hosts插件
            reload 300ms
            fallthrough
        }
        kubernetes cluster.local in-addr.arpa ip6.arpa {
           pods insecure
           fallthrough in-addr.arpa ip6.arpa
           ttl 30
        }
        prometheus :9153
        forward . /etc/resolv.conf
        cache 30
        loop
        reload
        loadbalance
    }
...

修改后,重启CoreDNS Pod使之生效。

修改kube-dns service

要使CoreDNS的kube-dns service支持流量闭环,需要增加openyurt.io/topologyKeys: openyurt.io/nodepoolannotation:

$ kubectl edit svc kube-dns -n kube-system
apiVersion: v1
kind: Service
metadata:
  annotations:
    openyurt.io/topologyKeys: openyurt.io/nodepool # 增加annotation
...

配置kube-proxy

要支持流量闭环,Kubernetes版本需要>=1.18(依赖EndpointSlices功能),并且需要开启kube-proxy的EndpointSlices功能。

开启EndpointSlices后,kube-proxy监听EndpointSlices(而不是原本的Endpoints资源)来配置后端服务的代理转发。Yurthub通过过滤EndpointSlices,只返回节点池内的endpoint地址,从实现流量闭环的功能。

EndpointSlices 参考文档:v1-18.docs.kubernetes.io/docs/concep…

在示例环境的K8S版本v1.18.9中,EndpointSlices功能是默认关闭的,我们需要为kube-proxy开启 EndpointSliceProxying feature gate

如何开启EndpointSlices: v1-18.docs.kubernetes.io/docs/tasks/… kubeadm的默认安装方式会为kube-proxy生成kubeconfig配置,为支持流量闭环,要让kube-proxy通过Yurthub来访问apiserver。得益于Yurthub的masterservice改写功能,以及bearer token透传功能,可以直接将kube-proxy的kubeconfig配置删除。

masterservice改写:删除kubeconfig后,kube-proxy会使用InClusterConfig配置来访问apiserver,而InClusterConfig配置(即环境变量KUBERNETES_SERVICE_HOSTKUBERNETES_SERVICE_PORT)的apiserver地址会被kubelet改写为Yurthub地址。

bearer token透传:通过Yurthub代理的请求,Yurthub会透传认证token(bearer token)。也就保留了kube-proxy ServiceAccount所具备的RBAC权限,使kube-proxy可以透明的使用Yurthub,无需额外配置rbac策略。

上述配置修改通过修改kube-proxy ConfigMap实现:

$ kubectl edit cm -n kube-system kube-proxy 
apiVersion: v1
data:
  config.conf: |-
    apiVersion: kubeproxy.config.k8s.io/v1alpha1
    bindAddress: 0.0.0.0
    featureGates: # ①开启EndpointSliceProxying feature gate.
      EndpointSliceProxying: true
    clientConnection:
      acceptContentTypes: ""
      burst: 0
      contentType: ""
      #kubeconfig: /var/lib/kube-proxy/kubeconfig.conf # ②注释或者删掉kubeconfig路径
      qps: 0
    clusterCIDR: 10.244.0.0/16
    configSyncPeriod: 0s
....

修改完后重启kube-proxy使之生效:

$ kubectl delete pods -l k8s-app=kube-proxy -n kube-system

查看重启后的kube-proxy日志,可以发现kube-proxy已经启用了endpointSlice controller:

$ kubectl logs -f kube-proxy-62b5k -n kube-system
I0911 17:37:08.422052       1 server.go:548] Neither kubeconfig file nor master URL was specified. Falling back to in-cluster config.
W0911 17:37:08.423936       1 server_others.go:559] Unknown proxy mode "", assuming iptables proxy
I0911 17:37:08.472651       1 node.go:136] Successfully retrieved node IP: 192.168.33.220
I0911 17:37:08.472685       1 server_others.go:186] Using iptables Proxier.
I0911 17:37:08.488403       1 server.go:583] Version: v1.18.9
I0911 17:37:08.491237       1 conntrack.go:52] Setting nf_conntrack_max to 131072
I0911 17:37:08.492341       1 config.go:315] Starting service config controller 
I0911 17:37:08.492357       1 shared_informer.go:223] Waiting for caches to sync for service config
I0911 17:37:08.492384       1 config.go:224] Starting endpoint slice config controller
I0911 17:37:08.492387       1 shared_informer.go:223] Waiting for caches to sync for endpoint slice config
I0911 17:37:08.593107       1 shared_informer.go:230] Caches are synced for endpoint slice config
I0911 17:37:08.593119       1 shared_informer.go:230] Caches are synced for service config

配置apiserver使用域名访问节点

apiserver使用--kubelet-preferred-address-types配置来决定通过什么方式访问kubelet,kubeadm默认配置会设置apiserver优先使用InternalIP来访问kubelet:

$ cat /etc/kubernetes/manifests/kube-apiserver.yaml | grep 'kubelet-preferred-address-types'
    - --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname 

显然这里需要配置apiserver优先使用Hostname来访问kubelet,将Hostname移到最前面: --kubelet-preferred-address-types=Hostname,InternalIP,ExternalIP

apiserver一般使用hostNetwork方式部署,在hostNetwork模式下,默认会使用主机上的DNS配置。为了让apiserver能在hostNetwork模式下使用CoreDNS,需要将apiserver Pod的dnsPolicy置为ClusterFirstWithHostNet

$ vi /etc/kubernetes/manifests/kube-apiserver.yaml
apiVersion: v1
kind: Pod
...
spec:
  dnsPolicy: ClusterFirstWithHostNet # ① dnsPolicy修改为ClusterFirstWithHostNet
  containers:
  - command:
    - kube-apiserver
...
    - --kubelet-preferred-address-types=Hostname,InternalIP,ExternalIP # ②把Hostname放在第一位
 ...

做完上述所有配置后,kubectl logs/exec就可以在Yurt-Tunnel的DNS模式下正常工作。

prometheus配置

配置完K8S原生接口,接下来我们将配置让prometheus也通过Yurt-Tunnel来访问边缘的expoter地址。示例中采用社区流行的prometheus-operator监控方案,并使用kube-prometheus(v0.5.0)部署。

prometheus默认使用节点IP来访问kubelet和node-exporter的metric地址,我们可以通过prometheus提供的relabel功能将节点IP改写为节点hostname。promethues-operator使用ServiceMonitor CRD来定义抓取配置,relabel规则也配置在对应的ServiceMonitor中。

kubelet监控配置

修改kubelet的ServiceMonitor,增加relabel规则,用__meta_kubernetes_endpoint_address_target_name替换掉节点IP:

$ kubectl edit serviceMonitor kubelet -n monitoring
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  ....
spec:
  endpoints:
  ...
    relabelings:
    - sourceLabels:
      - __metrics_path__
      targetLabel: metrics_path
    - action: replace  # ① 增加relabel规则
      regex: (.*);.*:(.*)
      replacement: $1:$2
      sourceLabels:
      - __meta_kubernetes_endpoint_address_target_name
      - __address__
      targetLabel: __address__
    ...
  - bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
    ...
    relabelings:
    - sourceLabels:
      - __metrics_path__
      targetLabel: metrics_path
    - action: replace # ② 增加relabel规则
      regex: (.*);.*:(.*)
      replacement: $1:$2
      sourceLabels:
      - __meta_kubernetes_endpoint_address_target_name
      - __address__
      targetLabel: __address__
    ...

kubelet暴露了两个metric地址,/metrics/metrics/cadvisor 。通过①②两项relabel规则配置,将默认抓取地址__address__中的IP+端口改写为域名+端口。

relabel配置方法参考promethues文档 prometheus.io/docs/promet…

node-exporter配置

类似的,我们修改node-exporter的ServiceMonitor,用__meta_kubernetes_pod_node_name替换掉节点IP。

$ kubectl edit ServiceMonitor -n monitoring node-exporter
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
 ...
spec:
  endpoints:
  ...
    relabelings:
    - action: replace
      regex: (.*)
      replacement: $1
      sourceLabels:
      - __meta_kubernetes_pod_node_name
      targetLabel: instance
    - action: replace # ① 增加relabel规则
      regex: (.*);.*:(.*)
      replacement: $1:$2
      sourceLabels:
      - __meta_kubernetes_pod_node_name
      - __address__
      targetLabel: __address__
    ...

要注意kubelet配置中替换节点IP的是:__meta_kubernetes_endpoint_address_target_name
而node-exporter中替换节点IP的是:__meta_kubernetes_pod_node_name
这二者的具体差异可参考文档:prometheus.io/docs/promet…

node-exporter的端口是9100,yurt-tunnel-server默认只转发10250和10255两个kubelet相关端口,对于其他端口的映射,可以修改配置来添加。修改yurt-tunnel-server-cfg ConfigMap,把9100端口添加到https-proxy-ports中:(类似的,如果是添加http端口,则修改http-proxy-ports配置)

$ kubectl edit cm -n kube-system yurt-tunnel-server-cfg
apiVersion: v1
data:
  dnat-ports-pair: ""
  http-proxy-ports: ""
  https-proxy-ports: "9100" # 添加9100端口映射,多个端口之间逗号分隔
  localhost-proxy-ports: 10266, 10267
kind: ConfigMap

yurt-tunnel-server中的DNSController监听到配置发生变化后,会修改x-tunnel-server-internal-svcservice,添加9100到10263的映射。

$ kubectl describe svc x-tunnel-server-internal-svc -n kube-system
Name:              x-tunnel-server-internal-svc
Namespace:         kube-system
Labels:            name=yurt-tunnel-server
Annotations:       <none>
Selector:          k8s-app=yurt-tunnel-server
Type:              ClusterIP
IP:                10.106.217.16
Port:              http  10255/TCP
TargetPort:        10264/TCP
Endpoints:         192.168.33.220:10264
Port:              https  10250/TCP
TargetPort:        10263/TCP
Endpoints:         192.168.33.220:10263
Port:              dnat-9100  9100/TCP  # 自动添加9100端口到10263的映射
TargetPort:        10263/TCP
Endpoints:         192.168.33.220:10263
Session Affinity:  None
Events:            <none>

配置完成后,从promethues控制台可以看到kubelet和node-exporter都使用了节点的hostname作为域名访问,且可以正常拉取数据。

image.png

End

至此,所有的配置都已完成。可以发现,目前手工配置Yurt-Tunnel DNS模式的过程还比较复杂。需要手动创建节点池,部署云端Yurthub,配置apiserver,kube-proxy等。期待Openyurt社区后续的持续优化,为大家带来更丝滑的部署体验。

参考

深度解读Openyurt系列
Yurthub数据过滤框架Proposal