k3s笔记-02-在Oracle Cloud部署集群

文章目录

1. 前言

卖油翁说过:我亦无他,惟手熟尔。掌握工具的最好办法就是每天使用,笔者的小集群在Oracle免费虚拟机上已经运行超过100天了,前些天误操作删掉了master,恢复时顺手升级到最新的v1.22.7版本。

今天请了病假,在跑完几趟医院后,傍晚得空在家休息,闲着也是闲着,就记录了下如何使用Cloudflare和Oracle Cloud的免费资源搭建个人集群。

2. 可用的免费资源

2.1 Cloudflare

注册Cloudflare并将域名的NS设置完成后,我们可以获得以下资源:

1. 泛域名TLS证书

主域名及子域名都可以得到TLS加密,并获取到一份由cloudflare签发的泛域名源证书,证书部署在集群后,cloudflare与我们集群之间的通信也能得到TLS加密。

2. CDN加速

使用域名访问时,所有流量都会经过cloudflare的边缘节点转发,并支持以下协议:

  1. HTTP/1.1
  2. HTTP/2
  3. HTTP/3
  4. gRPC(基于HTTP/2)
  5. WebSocket(基于HTTP/1.1)

这意味着我们可以部署常见的HTTP应用,例如s3、DNS over HTTPS等,以及利用WebSocket/gRPC作为数据通道的代理。

如果是使用TCP或UDP协议的应用,还是需要访问宿主机端口,再转发到对应服务。

2.2 Oracle Cloud

关于Oracle Cloud免费资源,笔者在之前的博客中已经详细记录:Oracle Cloud笔记

笔者在这里使用了以下资源:

  1. 一台4核CPU,24G内存,50G系统盘的arm64虚拟机作为master,具备IPv4和IPv6公网地址
  2. 两台1核CPU,2G内存,50G系统盘的x86虚拟机作为worker,具备IPv4和IPv6公网地址
  3. 使用S3协议接入免费的对象存储备份etcd
  4. 一台AMD虚拟机挂载50G块存储供S3服务使用

服务暴露使用traefik和k3s自带的servicelb实现,不依赖Oracle Cloud的负载均衡器。

3. 集群架构

1➜  ~ kubectl get node
2NAME                     STATUS   ROLES                                  AGE     VERSION
3instance-20211118-1925   Ready    10.0.0.33,doh,goproxy,s3               2d22h   v1.22.7+k3s1
4instance-20211118-1935   Ready    10.0.0.220,proxy                       2d22h   v1.22.7+k3s1
5oracle                   Ready    10.0.0.184,control-plane,etcd,master   2d22h   v1.22.7+k3s1

如上,笔者的集群中有三台虚拟机,每台使用 node-role.kubernetes.io 附加了额外的角色用于标记宿主机。

graph LR; A[master,arm64] B[worker,x86] C[worker,x86] A--->B A--->C

集群是简单的一主二从结构,由于混合部署了arm64和x86,需要注意使用的镜像也支持不同架构,否则服务漂移后会因为架构不同导致启动失败。

graph LR; subgraph 集群内网 A[节点A
VPC地址:10.0.0.184/24
集群网段:10.253.0.0/24] B[节点B
VPC地址:10.0.0.33/24
集群网段:10.253.1.0/24] C[节点C
VPC地址:10.0.0.220/24
集群网段:10.253.2.0/24] A--->B B--->C C--->A end

所有节点处于同一个VPC,而集群网络基于VPC内网搭建,k3s默认使用flannel+vxlan作为集群网络方案,笔者在这里选择了 10.253.0.0/1610.254.0.0/16 作为cluster-cidr与service-cidr,避免与原有的VPC网段冲突。

graph LR; subgraph Pod访问Pod A1--->|集群内网通信|B1 subgraph 节点A A1[容器组A1
集群IP:10.253.0.2] end subgraph 节点B B1[容器组B1
集群IP:10.253.1.2] end end
graph LR; subgraph Pod访问Service A1--->SC SC[Service
服务IP:10.254.10.10] SC--->|iptables转发|C1 SC--->|iptables转发|C2 subgraph 节点A A1[容器组A1
集群IP:10.253.0.2] end subgraph 节点C C1[容器组C1
集群IP:10.253.2.2] C2[容器组C2
集群IP:10.253.2.4] end end
graph LR; subgraph Pod访问公网服务 A1--->B[snat转换
PodIP->
节点内网IP->
节点外网EIP] B--->C1 B--->C2 subgraph VPC subgraph 节点A A1[容器组A1
集群IP:10.253.0.2] end end subgraph 公网 C1[谷歌
域名:google.com] C2[苹果
域名:apple.com] end end

每个节点上的容器访问Pod和Service时使用集群IP,访问外网时使用宿主机的公网IP,如果是代理类的应用,需要注意绑定宿主机避免源IP漂移。

4. 应用部署与服务暴露

集群的服务暴露一直都是个难题,而k3s选择自带附加组件servicelb与traefik充当了LoadBalancer与Ingress Controller的角色。

集群部署完成后,会启动traefik并创建一个关联的service,所有宿主机的IP会被加入该service的loadBalancer的ingress的IP列表中,访问宿主机80和443端口的流量首先转发到service,接着进一步转发到后端的traefik容器组。

 1➜  ~ kubectl -n kube-system describe svc traefik
 2Name:                     traefik
 3Namespace:                kube-system
 4Labels:                   app.kubernetes.io/instance=traefik
 5                          app.kubernetes.io/managed-by=Helm
 6                          app.kubernetes.io/name=traefik
 7                          helm.sh/chart=traefik-10.14.100
 8Annotations:              meta.helm.sh/release-name: traefik
 9                          meta.helm.sh/release-namespace: kube-system
10Selector:                 app.kubernetes.io/instance=traefik,app.kubernetes.io/name=traefik
11Type:                     LoadBalancer
12IP Family Policy:         SingleStack
13IP Families:              IPv4
14IP:                       10.254.142.204
15IPs:                      10.254.142.204
16LoadBalancer Ingress:     10.0.0.184, 10.0.0.220, 10.0.0.33
17Port:                     web  80/TCP
18TargetPort:               web/TCP
19NodePort:                 web  31348/TCP
20Endpoints:                10.253.0.2:8000
21Port:                     websecure  443/TCP
22TargetPort:               websecure/TCP
23NodePort:                 websecure  32684/TCP
24Endpoints:                10.253.0.2:8443
25Session Affinity:         None
26External Traffic Policy:  Cluster
27Events:                   <none>

在请求进入traefik后,就可以使用内部域名访问集群中的服务了。以之前部署的S3服务为例,我们可以画出以下的请求流程:

graph LR; subgraph 公网访问域名 User[内网用户访问:
s3.wbuntu.com]--->CF CF--->NAT subgraph 公网 CF[Cloudflare CDN] end subgraph VPC NAT[dnat转换
节点外网EIP->
节点内网IP] NAT--->|iptables转发|A1 subgraph 节点A A1[容器组traefik
集群IP:10.253.0.2] end end end
graph LR; subgraph traefik根据域名转发请求 A1--->S{域名路由} S--->|doh.wbuntu.com|SB S--->|s3.wbuntu.com|SC subgraph 节点A A1[容器组traefik
集群IP:10.253.0.2] end SB[Service doh] SC[Service minio] SB--->B1 SC--->C1 SC--->C2 subgraph 节点B B1[容器组doh
集群IP:10.253.1.2] end subgraph 节点C C1[容器组minio
集群IP:10.253.2.2] C2[容器组minio
集群IP:10.253.2.4] end end

如上所示,要暴露服务到公网时,只需创建路由规则即可,可以把多个路由规则合并到一个Ingressroute中,就像使用nginx一样按域名和路径转发请求。

下面是将minio部署在集群中,并暴露服务到公网的相关yaml配置。

 1apiVersion: apps/v1
 2kind: Deployment
 3metadata:
 4  name: minio
 5spec:
 6  replicas: 1
 7  selector:
 8    matchLabels:
 9      app: minio
10  template:
11    metadata:
12      labels:
13        app: minio
14    spec:
15      nodeSelector:
16        node-role.kubernetes.io/s3: "true"
17      containers:
18        - name: minio
19          image: minio/minio
20          args: ["server", "/data", "--address", ":9000", "--console-address", ":9001"]
21          env:
22            - name: MINIO_ROOT_USER
23              value: "xxxxxxxxxxxxxxxxx"
24            - name: MINIO_ROOT_PASSWORD
25              value: "xxxxxxxxxxxxxxxxx"
26            - name: MINIO_SERVER_URL
27              value: "https://s3.wbuntu.com"
28            - name: MINIO_BROWSER_REDIRECT_URL
29              value: "https://s3-console.wbuntu.com"
30          imagePullPolicy: IfNotPresent
31          ports:
32            - containerPort: 9000
33            - containerPort: 9001
34          volumeMounts:
35            - name: minio
36              mountPath: /data
37      volumes:
38        - name: minio
39          hostPath:
40            path: /data/minio
41            type: DirectoryOrCreate

上述yaml中,使用deployment部署minio,挂载宿主机目录存储数据。

 1apiVersion: v1
 2kind: Service
 3metadata:
 4  name: minio
 5spec:
 6  type: ClusterIP
 7  selector:
 8    app: minio
 9  ports:
10    - name: s3
11      port: 9000
12      targetPort: 9000
13    - name: s3-console
14      port: 9001
15      targetPort: 9001

上述yaml中,使用service将minio暴露给traefik。

 1apiVersion: traefik.containo.us/v1alpha1
 2kind: IngressRoute
 3metadata:
 4  name: ingress
 5spec:
 6  entryPoints:
 7    - websecure
 8  routes:
 9  - match: Host(`s3.wbuntu.com`)
10    kind: Rule
11    services:
12    - name: minio
13      port: 9000
14  - match: Host(`s3-console.wbuntu.com`)
15    kind: Rule
16    services:
17    - name: minio
18      port: 9001
19  tls:
20    secretName: tls-cert

上述yaml中,创建了一个ingressroute,将Host匹配 s3.wbuntu.com 的请求转发到minio服务的9000端口,Host匹配 s3-console.wbuntu.com 的请求转发到minio的9001端口。

这里引用了一个密钥 tls-cert,包含前面提到由Cloudflare签发的泛域名证书,我们可以继续在这个ingress中添加更多的路由规则,将其他的集群服务暴露到公网,这里就不做详细展开了。

5. 获取用户IP

以代理应用为例,收到请求时会打印来源用户IP,默认配置下我们只能看到经过转发的内网IP,如下:

12022/04/01 10:24:59 10.253.0.1:0 accepted tcp:www.google.com:443 [direct] email: [email protected]

执行 kubectl -n kube-system edit svc traefik 编辑traefik的svc,设置为 externalTrafficPolicyLocal,让外部流量直接进入traefik,如下:

1spec:
2  ...
3  externalTrafficPolicy: Local
4  internalTrafficPolicy: Cluster
5  ...

因为此时我们的请求是经过Cloudflare的CDN转发的,再次检查代理应用日志,可以发现用户IP变成了Cloudclare的IP,如下:

12022/04/01 10:40:00 172.70.93.35:0 accepted tcp:www.google.com:443 [direct] email: [email protected]

因为Cloudflare转发请求时,已经通过X-Forwarded-For头部设置了真实用户IP,我们可以修改traefik容器的启动参数,信赖来自Cloudflare的IP地址段,将请求中的用户IP转发到后端。

执行 kubectl -n kube-system edit deploy traefik 编辑traefike的deployment,为两个entrypoint分别添加forwardedHeaders.trustedIPs,对应的value为Cloudflare的IP段,如下:

1    spec:
2      containers:
3      - args:
4        ...
5        - --entrypoints.web.address=:8000/tcp
6        - --entrypoints.web.forwardedHeaders.trustedIPs=103.21.244.0/22,103.22.200.0/22,103.31.4.0/22,104.16.0.0/13,104.24.0.0/14,108.162.192.0/18,131.0.72.0/22,141.101.64.0/18,162.158.0.0/15,172.64.0.0/13,173.245.48.0/20,188.114.96.0/20,190.93.240.0/20,197.234.240.0/22,198.41.128.0/17
7        - --entrypoints.websecure.address=:8443/tcp
8        - --entrypoints.websecure.forwardedHeaders.trustedIPs=103.21.244.0/22,103.22.200.0/22,103.31.4.0/22,104.16.0.0/13,104.24.0.0/14,108.162.192.0/18,131.0.72.0/22,141.101.64.0/18,162.158.0.0/15,172.64.0.0/13,173.245.48.0/20,188.114.96.0/20,190.93.240.0/20,197.234.240.0/22,198.41.128.0/17
9        ...

Cloudflare的IPv4地址段可以通过这个命令获取:

1curl -s https://www.cloudflare.com/ips-v4 | sort -h

重启traefik容器组后,再次检查代理应用日志,这时可以看到真实的用户IP了,如下:

12022/04/01 10:57:20 106.52.172.214:0 accepted tcp:content-autofill.googleapis.com:443 [direct] email: [email protected]

需要注意的是,这种场景下必须将相关域名解析到traefik所在宿主机的公网IP,因为 externalTrafficPolicyLocal 时,外部流量通过NodePort进入集群时只能发给本机的Pod,无法转发到其他节点上的Pod。