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:

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

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

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

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

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

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

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

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

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

1
2
3
kubectl get svc -n kube-system -l app=openvpn
NAME      TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
openvpn   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
3
4
......
# TCP service key:value pairs
tcp:
  1194: "kube-system/openvpn:443"

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

1
2
3
4
5
helm upgrade nginx-ingress stable/nginx-ingress \
-f ingress-nginx.yaml


Release "nginx-ingress" has been upgraded.

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

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

使用客户端连接OpenVPN

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#!/bin/bash

if [ $# -ne 3 ]
then
  echo "Usage: $0 <CLIENT_KEY_NAME> <NAMESPACE> <HELM_RELEASE>"
  exit
fi

KEY_NAME=$1
NAMESPACE=$2
HELM_RELEASE=$3
POD_NAME=$(kubectl get pods -n "$NAMESPACE" -l "app=openvpn,release=$HELM_RELEASE" -o jsonpath='{.items[0].metadata.name}')
SERVICE_NAME=$(kubectl get svc -n "$NAMESPACE" -l "app=openvpn,release=$HELM_RELEASE" -o jsonpath='{.items[0].metadata.name}')
SERVICE_IP=$(kubectl get svc -n "$NAMESPACE" "$SERVICE_NAME" -o go-template='{{range $k, $v := (index .status.loadBalancer.ingress 0)}}{{$v}}{{end}}')
kubectl -n "$NAMESPACE" exec -it "$POD_NAME" /etc/openvpn/setup/newClientCert.sh "$KEY_NAME" "$SERVICE_IP"
kubectl -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这行内容。

1
remote 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域名:

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

Name:	kubernetes.default.svc.cluster.local
Address: 10.96.0.1

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
ping 10.96.0.1
PING 10.96.0.1 (10.96.0.1): 56 data bytes
64 bytes from 10.96.0.1: icmp_seq=0 ttl=63 time=20.327 ms
64 bytes from 10.96.0.1: icmp_seq=1 ttl=63 time=10.863 ms
64 bytes from 10.96.0.1: icmp_seq=2 ttl=63 time=12.441 ms

ping 10.244.0.1
PING 10.244.0.1 (10.244.0.1): 56 data bytes
64 bytes from 10.244.0.1: icmp_seq=0 ttl=62 time=28.051 ms
64 bytes from 10.244.0.1: icmp_seq=1 ttl=62 time=89.588 ms
64 bytes from 10.244.0.1: icmp_seq=2 ttl=62 time=63.754 ms

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#!/bin/bash

if [ $# -ne 3 ]
then
  echo "Usage: $0 <CLIENT_KEY_NAME> <NAMESPACE> <HELM_RELEASE>"
  exit
fi

KEY_NAME=$1
NAMESPACE=$2
HELM_RELEASE=$3
POD_NAME=$(kubectl get pods -n "$NAMESPACE" -l "app=openvpn,release=$HELM_RELEASE" -o jsonpath='{.items[0].metadata.name}')
kubectl -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认证》,这个不再展开。

参考