Kubernetes 网络扩展


通过边界网关和边界 DNS 直接访问 k8s 中的服务

Kubernetes 网络扩展

通过边界网关和边界 DNS 直接访问 k8s 中的服务

Sun Feb 11, 2018

4000 Words|Read in about 8 Min
Tags: kubernetes   docker  

1. Kubernetes 中服务暴露的方式


k8s 的服务暴露分为以下几种情况:

  • hostNetwork
  • hostPort
  • NodePort
  • LoadBalancer
  • Ingress

说是暴露 Pod 其实跟暴露 Service 是一回事,因为 Pod 就是 Service 的 backend。

HostNetwork

这是一种直接定义 Pod 网络的方式。

如果在 Pod 中使用 hostNotwork:true 配置的话,在这种 pod 中运行的应用程序可以直接看到 pod 启动的主机的网络接口。在主机的所有网络接口上都可以访问到该应用程序。以下是使用主机网络的 pod 的示例定义:

apiVersion: v1
kind: Pod
metadata:
  name: influxdb
spec:
  hostNetwork: true
  containers:
    - name: influxdb
      image: influxdb

这种 Pod 的网络模式有一个用处就是可以将网络插件包装在 Pod 中然后部署在每个宿主机上,这样该 Pod 就可以控制该宿主机上的所有网络。

缺点:每次启动这个Pod的时候都可能被调度到不同的节点上,所有外部访问Pod的IP也是变化的,而且调度Pod的时候还需要考虑是否与宿主机上的端口冲突,因此一般情况下除非您知道需要某个特定应用占用特定宿主机上的特定端口时才使用 hostNetwork: true 的方式。

hostPort

这是一种直接定义 Pod 网络的方式。

hostPort 是直接将容器的端口与所调度的节点上的端口路由,这样用户就可以通过宿主机的 IP 加上来访问 Pod 了,如:

apiVersion: v1
kind: Pod
metadata:
  name: influxdb
spec:
  containers:
    - name: influxdb
      image: influxdb
      ports:
        - containerPort: 8086
          hostPort: 8086

缺点:因为 Pod 重新调度的时候该Pod被调度到的宿主机可能会变动,这样就变化了,用户必须自己维护一个 Pod 与所在宿主机的对应关系。

NodePort

NodePort 在 kubenretes 里是一个广泛应用的服务暴露方式。Kubernetes 中的 service 默认情况下都是使用的 ClusterIP 这种类型,这样的 service 会产生一个 ClusterIP,这个 IP 只能在集群内部访问,要想让外部能够直接访问 service,需要将 service type 修改为 nodePort

apiVersion: v1
kind: Pod
metadata:
  name: influxdb
  labels:
    name: influxdb
spec:
  containers:
    - name: influxdb
      image: influxdb
      ports:
        - containerPort: 8086

同时还可以给 service 指定一个 nodePort 值,范围是 30000-32767,这个值在 API server 的配置文件中,用– service-node-port-range 定义。

kind: Service
apiVersion: v1
metadata:
  name: influxdb
spec:
  type: NodePort
  ports:
    - port: 8086
      nodePort: 30000
  selector:
    name: influxdb

集群外就可以使用 kubernetes 任意一个节点的 IP 加上 30000 端口访问该服务了。kube-proxy 会自动将流量以 round-robin 的方式转发给该 service 的每一个 pod。

缺点:所有 node 上都会开启端口监听,且需要记住端口号。

LoadBalancer

LoadBalancer 只能在 service 上定义。这是公有云提供的负载均衡器,如 AWS、Azure、CloudStack、GCE 等。

kind: Service
apiVersion: v1
metadata:
  name: influxdb
spec:
  type: LoadBalancer
  ports:
    - port: 8086
  selector:
    name: influxdb

查看服务:

$ kubectl get svc influxdb

NAME       CLUSTER-IP     EXTERNAL-IP     PORT(S)          AGE
influxdb   10.97.121.42   10.13.242.236   8086:30051/TCP   39s

内部可以使用 ClusterIP 加端口来访问服务,如 19.97.121.42:8086。

外部可以用以下两种方式访问该服务:

  • 使用任一节点的 IP 加 30051 端口访问该服务
  • 使用 EXTERNAL-IP 来访问,这是一个 VIP,是云供应商提供的负载均衡器 IP,如 10.13.242.236:8086

缺点:需要云服务商支持。

Ingress

Ingress 是自 kubernetes1.1 版本后引入的资源类型。必须要部署 Ingress controller 才能创建 Ingress 资源,Ingress controller 是以一种插件的形式提供。Ingress controller 是部署在 Kubernetes 之上的 Docker 容器。它的 Docker 镜像包含一个像 nginxHAProxy 的负载均衡器和一个控制器守护进程。控制器守护程序从 Kubernetes 接收所需的 Ingress 配置。它会生成一个 nginx 或 HAProxy 配置文件,并重新启动负载平衡器进程以使更改生效。换句话说,Ingress controller 是由 Kubernetes 管理的负载均衡器。

Kubernetes Ingress 提供了负载平衡器的典型特性:HTTP 路由,粘性会话,SSL 终止,SSL 直通,TCP 和 UDP 负载平衡等。目前并不是所有的 Ingress controller 都实现了这些功能,需要查看具体的 Ingress controller 文档。

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: influxdb
spec:
  rules:
    - host: influxdb.kube.example.com
      http:
        paths:
          - backend:
              serviceName: influxdb
              servicePort: 8086

外部访问 URL http://influxdb.kube.example.com/ping 访问该服务,入口就是 80 端口,然后 Ingress controller 直接将流量转发给后端 Pod,不需再经过 kube-proxy 的转发,比 LoadBalancer 方式更高效。

缺点:80 端口暴露 必需通过域名引入,而且一次只能一条规则,很麻烦。

但是在正常的虚拟机环境下,我们只需要一个 IP 地址+端口 即可访问服务。

为什么我们不能做到像访问虚拟机一样直接访问 k8s 集群服务呢?当然可以,以下架构可以实现:

  • 打通 k8s 网络和物理网络直通
  • 物理网络的 dns 域名服务直接调用 k8s-dns 域名服务直接互访

2. 集群环境


架构环境

  • k8s 集群网络:172.28.0.0/16
  • k8s-service 网络:10.96.0.0/12
  • 物理机网络:192.168.0.0/16

k8s 集群节点

$ kubectl get cs

NAME                 STATUS    MESSAGE              ERROR
scheduler            Healthy   ok                   
controller-manager   Healthy   ok                   
etcd-0               Healthy   {"health": "true"}

$ kubectl get nodes -owide

NAME      STATUS    AGE       VERSION   EXTERNAL-IP   OS-IMAGE                KERNEL-VERSION
node1     Ready     13d       v1.7.11   <none>        CentOS Linux 7 (Core)   3.10.0-514.el7.x86_64
node2     Ready     13d       v1.7.11   <none>        CentOS Linux 7 (Core)   3.10.0-514.el7.x86_64
node3     Ready     13d       v1.7.11   <none>        CentOS Linux 7 (Core)   3.10.0-514.el7.x86_64

角色定义:

角色名称 IP 地址 主机名
边界网关路由器 192.168.2.173 calico-gateway
边界 dns 代理服务器 192.168.1.62 node3

假设我们要访问 k8s 中的 dao-2048 服务:

$ kubectl get svc|egrep 'NAME|2048'

NAME                              CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
dao-2048                          10.98.217.155    <none>        80/TCP           13m

该方案的架构原理如下:

          +-----------------+
          |                 |
          |  192.168.0.0/16 |          # 物理网络以域名或tcp方式发起访问k8s service以及端口
          |                 |
          +-----------------+
                   |
                   |
+------------------------------------+
| dao-2048.default.svc.cluster.local |  # 请求k8s服务所在空间的服务名,完整域名
+------------------------------------+
                   |
                   |
          +-----------------+
          |                 |           # dns代理服务以ingress-udp pod的模式运行在此节点udp53号端口上,
          |   192.168.1.62  |           # 为物理网络提供仿问k8s-dns的桥梁解析dns
          |                 |           # 此节点应固定做为一个节点布署,所有外部机器设置dns为此 192.168.1.62
          +-----------------+
                   |
                   |
          +-----------------+
          |                 |
          |  10.98.217.155  |           # 获取 svc 的实际 clusterip
          |                 |
          +-----------------+
                   |
                   |
          +-----------------+           # 边界网关,用于物理网络连接k8s集群,需要开启内核转发:net.ipv4.ip_forward=1
          |                 |           # 所有外部物理机加一条静态路由:访问 k8s 网络 10.96.0.0/12 网段必需经过网关 192.168.2.173
          |  192.168.2.173  |           # ip route add 10.96.0.0/12 via 192.168.2.173
          |                 |           # 边界网关运行 kube-proxy 用于防火墙规则同步实现 svc 分流,此节点不运行 kubele 服务,不受 k8s 管控
          +-----------------+
                   |
                   |
         +-------------------+
         |                   |
         |  calico-Iface接口  |
         |                   |
         +-------------------+
                   |
                   |
          +-----------------+
          |   k8s 集群网络   |            # 流量最终到达 k8s 集群
          +-----------------+

以下为该方案的实施步骤。

3. 部署边界 dns 代理服务器


布署 dns 代理服务节点为外部提供 dns 服务,以 hostNetwork: true 为非 k8s 集群网络物理机节点提供 dns 服务

$ cd ~/dns-udp; ll ./

total 20K
-rw-r--r--. 1 root root 1.2K Feb 12 05:13 default-backend.yaml
-rw-r--r--. 1 root root  140 Feb 12 05:14 nginx-udp-ingress-configmap.yaml
-rw-r--r--. 1 root root 1.8K Feb 12 05:35 nginx-udp-ingress-controller.yaml
-rw-r--r--. 1 root root 2.4K Feb 12 05:15 rbac.yaml

$ cat default-backend.yaml

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: default-http-backend
  labels:
    app: default-http-backend
  namespace: kube-system
spec:
  replicas: 1
  selector:
    matchLabels:
      app: default-http-backend
  template:
    metadata:
      labels:
        app: default-http-backend
    spec:
      terminationGracePeriodSeconds: 60
      containers:
      - name: default-http-backend
        # Any image is permissible as long as:
        # 1. It serves a 404 page at /
        # 2. It serves 200 on a /healthz endpoint
        image: gcr.io/google_containers/defaultbackend:1.4
        livenessProbe:
          httpGet:
            path: /healthz
            port: 8080
            scheme: HTTP
          initialDelaySeconds: 30
          timeoutSeconds: 5
        ports:
        - containerPort: 8080
        resources:
          limits:
            cpu: 10m
            memory: 20Mi
          requests:
            cpu: 10m
            memory: 20Mi
---

apiVersion: v1
kind: Service
metadata:
  name: default-http-backend
  namespace: kube-system
  labels:
    app: default-http-backend
spec:
  ports:
  - port: 80
    targetPort: 8080
  selector:
    app: default-http-backend
    
$ cat nginx-udp-ingress-configmap.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-udp-ingress-configmap
  namespace: kube-system
data:
  53: "kube-system/kube-dns:53"
  
$ cat nginx-udp-ingress-controller.yaml

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: nginx-udp-ingress-controller
  labels:
    k8s-app: nginx-udp-ingress-lb
  namespace: kube-system
spec:
  replicas: 1
  selector:
    matchLabels:
      k8s-app: nginx-udp-ingress-lb
  template:
    metadata:
      labels:
        k8s-app: nginx-udp-ingress-lb
        name: nginx-udp-ingress-lb
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: kubernetes.io/hostname
                operator: In
                values:
                - node3
      hostNetwork: true
      serviceAccountName: nginx-ingress-serviceaccount
      terminationGracePeriodSeconds: 60
      containers:
      - image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.10.2
        name: nginx-udp-ingress-lb
        readinessProbe:
          httpGet:
            path: /healthz
            port: 10254
            scheme: HTTP
        livenessProbe:
          httpGet:
            path: /healthz
            port: 10254
            scheme: HTTP
          initialDelaySeconds: 10
          timeoutSeconds: 1
        env:
          - name: POD_NAME
            valueFrom:
              fieldRef:
                fieldPath: metadata.name
          - name: POD_NAMESPACE
            valueFrom:
              fieldRef:
                fieldPath: metadata.namespace
        ports:
        - containerPort: 80
          hostPort: 80
        - containerPort: 443
          hostPort: 443
        - containerPort: 53
          hostPort: 53
        args:
        - /nginx-ingress-controller
        - --default-backend-service=$(POD_NAMESPACE)/default-http-backend
        - --udp-services-configmap=$(POD_NAMESPACE)/nginx-udp-ingress-configmap

$ cat rbac.yaml

apiVersion: v1
kind: ServiceAccount
metadata:
  name: nginx-ingress-serviceaccount
  namespace: kube-system

---

apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  name: nginx-ingress-clusterrole
rules:
  - apiGroups:
      - ""
    resources:
      - configmaps
      - endpoints
      - nodes
      - pods
      - secrets
    verbs:
      - list
      - watch
  - apiGroups:
      - ""
    resources:
      - nodes
    verbs:
      - get
  - apiGroups:
      - ""
    resources:
      - services
    verbs:
      - get
      - list
      - watch
  - apiGroups:
      - "extensions"
    resources:
      - ingresses
    verbs:
      - get
      - list
      - watch
  - apiGroups:
      - ""
    resources:
        - events
    verbs:
        - create
        - patch
  - apiGroups:
      - "extensions"
    resources:
      - ingresses/status
    verbs:
      - update

---

apiVersion: rbac.authorization.k8s.io/v1beta1
kind: Role
metadata:
  name: nginx-ingress-role
  namespace: kube-system
rules:
  - apiGroups:
      - ""
    resources:
      - configmaps
      - pods
      - secrets
      - namespaces
    verbs:
      - get
  - apiGroups:
      - ""
    resources:
      - configmaps
    resourceNames:
      # Defaults to "<election-id>-<ingress-class>"
      # Here: "<ingress-controller-leader>-<nginx>"
      # This has to be adapted if you change either parameter
      # when launching the nginx-ingress-controller.
      - "ingress-controller-leader-nginx"
    verbs:
      - get
      - update
  - apiGroups:
      - ""
    resources:
      - configmaps
    verbs:
      - create
  - apiGroups:
      - ""
    resources:
      - endpoints
    verbs:
      - get

---

apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
  name: nginx-ingress-role-nisa-binding
  namespace: kube-system
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: nginx-ingress-role
subjects:
  - kind: ServiceAccount
    name: nginx-ingress-serviceaccount
    namespace: kube-system

---

apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: nginx-ingress-clusterrole-nisa-binding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: nginx-ingress-clusterrole
subjects:
  - kind: ServiceAccount
    name: nginx-ingress-serviceaccount
    namespace: kube-system
    
$ kubectl create -f ./

deployment "default-http-backend" created
service "default-http-backend" created
configmap "nginx-udp-ingress-configmap" created
deployment "nginx-udp-ingress-controller" created

通过 nginx 反向代理 kube-dns 服务,同时以 hostNetwork: true 向集群外部暴露 53 端口,为非 k8s 集群网络物理机节点提供 dns 服务。

4. 部署 gateway 边界网关节点


此节点只运行 calicokube-proxy

首先开启内核转发

$ echo 'net.ipv4.ip_forward=1' >>/etc/sysctl.conf
$ sysctl -p

运行 calico

$ docker run --net=host --privileged --name=calico-node -d --restart=always \
  -v /etc/etcd/ssl:/etc/kubernetes/ssl \
  -e ETCD_ENDPOINTS=https://192.168.1.60:12379 \
  -e ETCD_KEY_FILE=/etc/kubernetes/ssl/peer-key.pem \
  -e ETCD_CERT_FILE=/etc/kubernetes/ssl/peer-cert.pem \
  -e ETCD_CA_CERT_FILE=/etc/kubernetes/ssl/ca.pem \
  -e NODENAME=${HOSTNAME} \
  -e IP= \
  -e CALICO_IPV4POOL_CIDR=172.28.0.0/16 \
  -e NO_DEFAULT_POOLS= \
  -e AS= \
  -e CALICO_LIBNETWORK_ENABLED=true \
  -e IP6= \
  -e CALICO_NETWORKING_BACKEND=bird \
  -e FELIX_DEFAULTENDPOINTTOHOSTACTION=ACCEPT \
  -v /var/run/calico:/var/run/calico \
  -v /lib/modules:/lib/modules \
  -v /run/docker/plugins:/run/docker/plugins \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /var/log/calico:/var/log/calico \
  calico/node:v2.6.7

需要提前将相关证书拷贝到 /etc/kubernetes/ssl/ 目录下。

注意:此处的 -e CALICO_IPV4POOL_CIDR=172.28.0.0/16 要与 k8s 集群网络的网段一致

创建边界路由器

以下命令在 k8s 的 master 节点上进行操作

$ cat bgpPeer.yaml

apiVersion: v1
kind: bgpPeer
metadata:
  peerIP: 192.168.2.173
  scope: global
spec:
  asNumber: 64512
  
$ calicoctl  create -f bgpPeer.yaml

查看 node 情况

$ calicoctl node status

Calico process is running.

IPv4 BGP status
+---------------+-------------------+-------+------------+-------------+
| PEER ADDRESS  |     PEER TYPE     | STATE |   SINCE    |    INFO     |
+---------------+-------------------+-------+------------+-------------+
| 192.168.1.61  | node-to-node mesh | up    | 2018-01-29 | Established |
| 192.168.1.62  | node-to-node mesh | up    | 2018-02-09 | Established |
| 192.168.2.173 | node-to-node mesh | up    | 2018-02-09 | Established |
| 192.168.2.173 | global            | start | 2018-02-09 | Idle        |
+---------------+-------------------+-------+------------+-------------+

IPv6 BGP status
No IPv6 peers found.

查看全局对等体节点

$ calicoctl get bgpPeer --scope=global

SCOPE    PEERIP          NODE   ASN     
global   192.168.2.173          64512

部署 kube-proxy

安装 conntrack

$ yum install -y conntrack-tools

创建 kube-proxy 的 service 配置文件

文件路径 /usr/lib/systemd/system/kube-proxy.service

[Unit]
Description=Kubernetes Kube-Proxy Server
Documentation=https://github.com/GoogleCloudPlatform/kubernetes
After=network.target

[Service]
EnvironmentFile=-/etc/kubernetes/config
EnvironmentFile=-/etc/kubernetes/proxy
ExecStart=/usr/local/bin/kube-proxy \
        --logtostderr=true \
        --v=2 \
        --bind-address=192.168.2.173 \
        --hostname-override=192.168.2.173 \
        --kubeconfig=/etc/kubernetes/kube-proxy.kubeconfig \
        --proxy-mode=iptables \
        --cluster-cidr=172.28.0.0/16
Restart=on-failure
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target

需要提前将 kube-proxy.kubeconfig 文件拷贝到 /etc/kubernetes/ 目录下。

启动 kube-proxy

$ systemctl daemon-reload
$ systemctl enable kube-proxy
$ systemctl start kube-proxy

5. 测试网关和 dns 解析以及服务访问情况


找台集群外的机器来验证,这台机器只有一个网卡,没有安装 calico

  • 添加路由
$ ip route add 10.96.0.0/12 via 192.168.2.173 dev ens160
  • 修改 dns 为 192.168.1.62
$ cat /etc/resolv.conf

nameserver 192.168.1.62
search default.svc.cluster.local svc.cluster.local cluster.local
nameserver 223.5.5.5
  • 解析 dao-2048 服务的域名
$ dig dao-2048.default.svc.cluster.local

; <<>> DiG 9.9.4-RedHat-9.9.4-51.el7_4.2 <<>> dao-2048.default.svc.cluster.local
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 57053
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;dao-2048.default.svc.cluster.local. IN A

;; ANSWER SECTION:
dao-2048.default.svc.cluster.local. 5 IN A      10.98.217.155

;; Query time: 1 msec
;; SERVER: 192.168.1.62#53(192.168.1.62)
;; WHEN: Mon Feb 12 06:46:38 EST 2018
;; MSG SIZE  rcvd: 79
  • 访问 dao-2048 服务
$ curl dao-2048.default.svc.cluster.local

* About to connect() to dao-2048.default.svc.cluster.local port 80 (#0)
*   Trying 10.98.217.155...
* Connected to dao-2048.default.svc.cluster.local (10.98.217.155) port 80 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.29.0
> Host: dao-2048.default.svc.cluster.local
> Accept: */*
> 
< HTTP/1.1 200 OK
< Server: nginx/1.10.1
< Date: Mon, 12 Feb 2018 11:47:58 GMT
< Content-Type: text/html
< Content-Length: 4085
< Last-Modified: Sun, 11 Feb 2018 11:31:27 GMT
< Connection: keep-alive
< ETag: "5a80298f-ff5"
< Accept-Ranges: bytes
< 
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>2048</title>
.........

成功访问!

如果是 windows 用户,添加路由可以用管理员打开 cmd 命令运行:

$ route ADD -p 172.28.0.0 MASK 255.255.0.0 192.168.2.173

PS:如果你不想一台台机器加路由和 dns,你可以把路由信息加入物理路由器上,这样就不用每台机都加路由和 dns 了,直接打通所有链路。

参考

k8s-dns-gateway 网关网络扩展实战

「真诚赞赏,手留余香」

杨传胜

真诚赞赏,手留余香

使用微信扫描二维码完成支付

See Also