Kubernetes集群内部是一套单独的网络,与外部网络相隔离,Kubernetes提供了NodePort、LoadBalancer、Ingress等方式用于将集群内部的Service暴露到集群外部, 但这些方式都更适用于将边缘服务暴露出去。例如,一个产品由很多微服务组成,这些微服务大多数都是内部服务,只有少数作为边缘服务需要暴露到集群外部供用户使用。 而开发人员是存在从Kubernetes外部访问和调试这些内部服务的需求的,尤其是针对测试环境的Kubernetes集群,这个需求更加必要。针对这个需求,有以下两种实现方式:

  • 第一种方式:将这些内部服务以NodePort或Ingress暴露到集群外部,但是这种方式有很多缺点。对于NodePort的方式,这些内部服务会占用Kubernetes服务器节点的大量的NodePort端口,同时暴露的服务以NodeIP:NodePort的形式提供,明显不便于记录。对于以Ingress的形式,针对HTTP这类7层服务可以结合外部域名的形式暴露,但对于TCP这类4层服务,每个服务都会占用ingress-controller的一个端口,同样也不便于记录。
  • 第二种方式:在Kubernetes集群内部部署一个OpenVPN Pod,外部网络中的开发人员通过OpenVPN客户端连接,开发人员对集群内部服务的访问都由这个OpenVPN Pod转发。现在Helm的官方Chart Repo中已经提供了一个stable/openvpn的Chart,使用它可以在Kubernetes集群内部完成OpenVPN的部署。本文将详细介绍OpenVPN在Kubernetes集群内的部署并打通集群内网与本地开发网络的过程。

使用Helm在K8S上部署OpenVPN

创建持久化OpenVPN配置所需要的Local PV:

首先,在k8s集群上创建本地存储的StorageClass local-storage.yaml:

1apiVersion: storage.k8s.io/v1
2kind: StorageClass
3metadata:
4  name: local-storage
5provisioner: kubernetes.io/no-provisioner
6volumeBindingMode: WaitForFirstConsumer
7reclaimPolicy: Retain

OpenVPN的Local PV openvpn-local-pv.yaml:

 1apiVersion: v1
 2kind: PersistentVolume
 3metadata:
 4  name: data-openvpn
 5spec:
 6  capacity:
 7    storage: 10Mi 
 8  accessModes:
 9  - ReadWriteOnce
10  persistentVolumeReclaimPolicy: Retain
11  storageClassName: local-storage
12  local:
13    path: /home/openvpn/data
14  nodeAffinity:
15    required:
16      nodeSelectorTerms:
17      - matchExpressions:
18        - key: kubernetes.io/hostname
19          operator: In
20          values:
21          - node2

在node2上预先创建/home/openvpn/data这个目录。

使用helm官方Chart Repo中的stable/openvpn,编写下面的openvpn-values.yaml

 1replicaCount: 1
 2image:
 3  repository: jfelten/openvpn-docker
 4  tag: 1.1.0
 5service:
 6  type: ClusterIP
 7persistence:
 8  enabled: true
 9  storageClass: local-storage
10  accessMode: ReadWriteOnce
11  size: 10Mi
12openvpn:
13  OVPN_K8S_POD_NETWORK: 10.244.0.0
14  OVPN_K8S_POD_SUBNET: 255.255.0.0
15  OVPN_K8S_SVC_NETWORK: 10.96.0.0
16  OVPN_K8S_SVC_SUBNET: 255.240.0.0

注意上面的openvpn-values.yaml中:

  • OVPN_K8S_POD_NETWORKOVPN_K8S_POD_SUBNET要与Pod的网络cluster-cidr一致,我这里是16
  • OVPN_K8S_SVC_NETWORKOVPN_K8S_SVC_NETWORK要与kube-apiserver中指定的service-cluster-ip-range一致,我这里是10.96.0.0/12

使用helm在k8s上安装openvpn:

1helm install \
2  --name openvpn --namespace kube-system -f openvpn-values.yaml stable/openvpn 

安装完成后openvpn的证书会被保存在前面创建的Local PV的pki目录里,这里是/home/openvpn/data/pki/

可以看到openvpn在k8s中的Service如下:

1kubectl get svc -n kube-system -l app=openvpn
2NAME      TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
3openvpn   ClusterIP   10.101.165.239   <none>        443/TCP   8m18s

因为这里我们选择创建的是ClusterIP类型的Service,所以还需要使用Ingress将这个Service暴露到k8s集群外部,这样就可以再集群外部使用OpenVPN的客户端连接了。 注意这里暴露的是TCP服务,与暴露HTTP服务不同,不能通过创建Ingress资源暴露。而是需要在Ingress Controller的相同Namespace下的configmap/nginx-ingress-tcp添加nginx端口与需要暴露服务端口的对应关系。 因为这里的k8s Nginx Ingress是使用helm部署的,所以直接在Chart的value file ingress-nginx.yam中更新相关内容:

1......
2# TCP service key:value pairs
3tcp:
4  1194: "kube-system/openvpn:443"

这里相当于是通过k8s edge节点的1194端口暴露openvpn Service的443端口。 使用helm更新nginx-ingress release:

1helm upgrade nginx-ingress stable/nginx-ingress \
2-f ingress-nginx.yaml
3
4
5Release "nginx-ingress" has been upgraded.

在k8s的edge节点上查看1194端口已经被nginx-ingress-controller监听:

1netstat -nltp | grep 1194
2tcp        0      0 0.0.0.0:1194            0.0.0.0:*               LISTEN      16291/nginx: master 
3tcp6       0      0 :::1194                 :::*                    LISTEN      16291/nginx: master

使用客户端连接OpenVPN

这里是Mac OS X系统,使用的客户端软件是TunnelBlick

使用下面的脚本生成一个客户端key:

 1#!/bin/bash
 2
 3if [ $# -ne 3 ]
 4then
 5  echo "Usage: $0 <CLIENT_KEY_NAME> <NAMESPACE> <HELM_RELEASE>"
 6  exit
 7fi
 8
 9KEY_NAME=$1
10NAMESPACE=$2
11HELM_RELEASE=$3
12POD_NAME=$(kubectl get pods -n "$NAMESPACE" -l "app=openvpn,release=$HELM_RELEASE" -o jsonpath='{.items[0].metadata.name}')
13SERVICE_NAME=$(kubectl get svc -n "$NAMESPACE" -l "app=openvpn,release=$HELM_RELEASE" -o jsonpath='{.items[0].metadata.name}')
14SERVICE_IP=$(kubectl get svc -n "$NAMESPACE" "$SERVICE_NAME" -o go-template='{{range $k, $v := (index .status.loadBalancer.ingress 0)}}{{$v}}{{end}}')
15kubectl -n "$NAMESPACE" exec -it "$POD_NAME" /etc/openvpn/setup/newClientCert.sh "$KEY_NAME" "$SERVICE_IP"
16kubectl -n "$NAMESPACE" exec -it "$POD_NAME" cat "/etc/openvpn/certs/pki/$KEY_NAME.ovpn" > "$KEY_NAME.ovpn"
1./gen-client-key.sh client kube-system openvpn

上面的命令会生成client.ovpn配置文件,因为前面用Helm部署openvpn时,创建的Servie是ClusterIP类型的,生成的配置文件 中的VPN服务地址会有错误,需要修改这个配置文件,将remote配置修改如下地址为k8s边缘节点的ip,端口为前面暴露的1194端口,同时删除redirect-gateway def1这行内容。

1remote 192.168.99.11 1194 tcp

右键点击client.ovpn,选择使用TunnelBlick打开,导入后,连接即可。连接后,下面在本地直接测试访问k8s集群中的Service或PodIP,确认网络可达。 因为这里的k8s集群的kube-proxy使用的IPVS模式,所以直接ping ClusterIP也应该是可达的,在个人本地MacBook上连接OpenVPN后测试如下:

尝试解析kubernetes.default.svc.cluster.local,确认可正常解析,注意这里一定要给完整的Service域名:

1nslookup kubernetes.default.svc.cluster.local
2Server:		10.96.0.10
3Address:	10.96.0.10#53
4
5Name:	kubernetes.default.svc.cluster.local
6Address: 10.96.0.1

ping k8s集群中任何一个ClusterIP和PodIP确认可以ping通:

 1ping 10.96.0.1
 2PING 10.96.0.1 (10.96.0.1): 56 data bytes
 364 bytes from 10.96.0.1: icmp_seq=0 ttl=63 time=20.327 ms
 464 bytes from 10.96.0.1: icmp_seq=1 ttl=63 time=10.863 ms
 564 bytes from 10.96.0.1: icmp_seq=2 ttl=63 time=12.441 ms
 6
 7ping 10.244.0.1
 8PING 10.244.0.1 (10.244.0.1): 56 data bytes
 964 bytes from 10.244.0.1: icmp_seq=0 ttl=62 time=28.051 ms
1064 bytes from 10.244.0.1: icmp_seq=1 ttl=62 time=89.588 ms
1164 bytes from 10.244.0.1: icmp_seq=2 ttl=62 time=63.754 ms

经过上面的测试可以发现从集群外部连接上VPN后,已经可以正常解析集群内Service的域名,并访问集群内部的服务。

如果收回并取消生成的客户端key,可以执行下面的脚本:

 1#!/bin/bash
 2
 3if [ $# -ne 3 ]
 4then
 5  echo "Usage: $0 <CLIENT_KEY_NAME> <NAMESPACE> <HELM_RELEASE>"
 6  exit
 7fi
 8
 9KEY_NAME=$1
10NAMESPACE=$2
11HELM_RELEASE=$3
12POD_NAME=$(kubectl get pods -n "$NAMESPACE" -l "app=openvpn,release=$HELM_RELEASE" -o jsonpath='{.items[0].metadata.name}')
13kubectl -n "$NAMESPACE" exec -it "$POD_NAME" /etc/openvpn/setup/revokeClientCert.sh $KEY_NAME

总结

本篇通过介绍Helm chart for OpenVPN,将Kubernetes集群内网暴露到集群外部的开发网络。 实际正式使用还需要做进一步的完善和配置,例如对于每一个团队内部一般都会有一套基于LDAP的统一账号管理系统,这里的搭建的"Kubernetes VPN开发网络"也需要与这套LDAP相集成, 因此还需要做进一步的配置,关于OpenVPN如何集成LDAP可参考我之前编写的《团队环境:OpenVPN集成LDAP认证》,这个不再展开。

参考