本文的主要记录了如何利用手头空余的 vps 服务器建立一个 k8s 集群以供实验学习。目前我是有三台空余的 vps 服务器,刚好可以搭建一个小的 k8s 集群。集群采用 1*master + 2*work 的方式。
因为 k8s 集群搭建本身并不复杂,所以本篇文章也主要是记录下来每一步操作。在开始阅读此篇文章之前强烈建议先阅读我之前写的跨云k8s组网方案讨论这篇文章,里面完整记录了关于我这个小环境组网方案的新路历程。

1. 前言

  • 目标: 搭建一个安全、稳定、符合官方标准的K8s学习与实验环境。
  • 操作系统版本: Debian12
  • k8s 版本: 1.33.4
  • CRI 容器运行时: containerd v1.6.20
  • CNI 网络插件: Calico v3.30.3
  • 服务暴露: Cloudflare Tunnel

2. 基础配置

此部分所有 master、work 节点都需要操作。

2.1 基本工具安装

更新系统并安装必要工具:

1
2
3
apt update
apt upgrade -y
apt install -y apt-transport-https ca-certificates curl gpg bash-completion

2.2 关闭 swap

k8s 建议关闭 swap分区:

1
2
swapoff -a
vim /etc/fstab # 编辑fstab文件,注释掉 swap 那一行

2.3 安装 WireGuard

1
apt install wireguard -y

2.4 生成密钥对

1
2
3
4
cd /etc/wireguard
wg genkey | tee privatekey | wg pubkey > publickey
chmod 600 privatekey
chmod 600 publickey

2.5 配置 WireGuard

关于 WireGUard 更详细的内容解析,可以查看我之前写的这篇文章 WireGuard原理解析与生产实践
编辑 /etc/wireguard/wg0.conf,写入以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
[Interface]
# 本机的私钥
PrivateKey = xxxxxxxxxxxx
# 本机在私有组网中的IP地址
Address = 10.66.66.1/24
# 默认监听端口(UDP协议)
ListenPort = 51820
# 统一设置 MTU
MTU = 1420

# 一号工作节点的 Peer
[Peer]
# 一号工作节点的公钥
PublicKey = xxxxxxxxxxxx
# 一号工作节点的公网IP和端口
Endpoint = x.x.x.x:51820
# 允许使用 WireGuard 接口通信的网段。这里注意下除了自定义的私网网段,还需要加上POD的网段。因为我的组网方案里POD是用Calico BGP模式通信的。
AllowedIPs = 10.66.66.0/24,10.244.0.0/16
# 保持连接,25秒发一次心跳,最佳建议值
PersistentKeepalive = 25

# 二号工作节点的 Peer
[Peer]
# 二号工作节点的公钥
PublicKey = xxxxxxxxxxxx
# 二号工作节点的公网IP和端口
Endpoint = x.x.x.x:51820
# 允许使用 WireGuard 接口通信的网段。这里注意下除了自定义的私网网段,还需要加上POD的网段。因为我的组网方案里POD是用Calico BGP模式通信的。
AllowedIPs = 10.66.66.0/24,10.244.0.0/16
# 保持连接,25秒发一次心跳,最佳建议值
PersistentKeepalive = 25

上面这个配置文件是以 master 节点为例编写的,其他两个节点也使用同样的格式配置,只不过变换下 Peer 部分 为其余两个主机、本机部分按照本节点配置。

2.6 启动与验证

1
2
3
4
5
6
7
8
9
10
11
# 启动并设置开机自启
wg-quick up wg0
systemctl enable wg-quick@wg0

# 查看状态
wg show

# 验证网络
ping 10.66.66.1
ping 10.66.66.2
ping 10.66.66.3

2.7 参数与模块配置

k8s需要特定的内核模块和系统参数来支持容器和网络,同时我希望 kube-proxy 使用 ipvs 模式所以需要做如下操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 安装 ipvs
apt-get install -y ipset ipvsadm

# 配置启动自加载 ipvs 所需模块
cat <<EOF | tee /etc/modules-load.d/k8s-ipvs.conf
ip_vs
ip_vs_rr
ip_vs_wrr
ip_vs_sh
nf_conntrack
EOF

# 手动加载 ipvs 相关模块
modprobe ip_vs
modprobe ip_vs_rr
modprobe ip_vs_wrr
modprobe ip_vs_sh
modprobe nf_conntrack

# 配置启动自加载容器运行时所需模块
cat <<EOF | tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF

# 手动加载容器运行时相关模块
modprobe overlay
modprobe br_netfilter

# 配置启动自加载内核参数
cat <<EOF | tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF

# 配置生效
sysctl --system

2.8 安装容器运行时

安装 containerd :

1
apt-get install -y containerd

生成默认配置文件并修改 cgroup 驱动为 systemd:

1
2
3
4
5
6
7
8
9
# 生成配置文件
mkdir -p /etc/containerd
containerd config default | tee /etc/containerd/config.toml

# 修改cgroup驱动为systemd,与kubelet保持一致
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml

# 重启 containerd
systemctl restart containerd

替换 containerd 为国内镜像源(可选,但位于国内的服务器必须要操作),配置方法可以按照这篇文章来 containerd配置国内镜像加速

2.9 安装 k8s 组件

添加 k8s 官方 GPG 密钥和 APT 仓库:

1
2
3
# Google的仓库在国内无法访问,这里建议使用阿里云的镜像
curl -fsSL https://mirrors.aliyun.com/kubernetes-new/core/stable/v1.33/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://mirrors.aliyun.com/kubernetes-new/core/stable/v1.33/deb/ /' | tee /etc/apt/sources.list.d/kubernetes.list

安装指定版本的 k8s 组件:

1
2
3
4
5
apt-get update
apt-get install -y kubelet=1.33.4-1.1 kubeadm=1.33.4-1.1 kubectl=1.33.4-1.1

# 锁定版本,防止意外升级
apt-mark hold kubelet kubeadm kubectl

k8s 集群创建的时候默认是通过路由来判断 internal ip 的,所以在使用 WireGuard 组建私有网络之后,k8s 并不会直接识别使用私网 IP,所以这里需要手动指定 kubelet 的 internal ip:

1
2
3
4
5
vim /usr/lib/systemd/system/kubelet.service.d/10-kubeadm.conf

# 在最后一行添加 --node-ip=x.x.x.x 这里的 x.x.x.x 替换成各个节点实际的 WireGuard 私网 IP
# 以我的 master 节点为例,最后一行修改为:
ExecStart=/usr/bin/kubelet $KUBELET_KUBECONFIG_ARGS $KUBELET_CONFIG_ARGS $KUBELET_KUBEADM_ARGS $KUBELET_EXTRA_ARGS --node-ip=10.66.66.1

重启 kubelet:

1
2
3
systemctl daemon-reload
systemctl enable kubelet
systemctl restart kubelet

3. master 配置

以下步骤仅在 master 节点上配置

3.1 创建 k8s 集群

执行 kubeadm init 命令创建集群:

1
2
3
4
5
6
7
8
9
# 我的 master 节点配置的 WireGuard IP 是 10.66.66.1、POD 网段是 10.244.0.0/16
kubeadm init \
--kubernetes-version=v1.33.4 \
--apiserver-advertise-address=10.66.66.1 \
--pod-network-cidr=10.244.0.0/16 \
--ignore-preflight-errors=Mem

# --apiserver-advertise-address: 这个参数非常重要它指明其他节点要通过 WireGuard 组建的私有网络来访问 API Server
# --ignore-preflight-errors=Mem: 添加这个参数是因为我的 master 节点内存很小只有 2GB ,不加会提示内存不足

初始化拉取镜像会花费一定时间,如果网络条件不好可能要等待5分钟左右,待命令执行完成后可以看到类似如下提示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Your Kubernetes control-plane has initialized successfully!

To start using your cluster, you need to run the following as a regular user:

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

...

Then you can join any number of worker nodes by running the following on each as root:

kubeadm join 10.66.66.1:6443 --token abcdef.1234567890abcdef \
--discovery-token-ca-cert-hash sha256:1234...

记录上面屏幕提示中 kubeadm join 这段命令,后续工作节点将通过此条命令加入新建的 k8s 集群中

按照提示配置:

1
2
3
mkdir -p $HOME/.kube
cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
chown $(id -u):$(id -g) $HOME/.kube/config

配置命令补全:

1
2
echo 'source <(kubectl completion bash)' >> ~/.bashrc
source ~/.bashrc

修改 kube-proxy 为 ipvs 模式:

1
kubectl edit configmap kube-proxy -n kube-system

mode: "" 修改为 mode: "ipvs",操作完成之后再执行命令删除老的 POD 让它重新生成:

1
kubectl delete pod -l k8s-app=kube-proxy -n kube-system

3.2 配置 CNI 网络插件

在前面跨云 k8s 组网方案中已经说明了使用到的 CNI 插件是 Calico,网络模式配置为 BGP。

安装 Calico:

1
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.30.3/manifests/calico.yaml

等待几分钟,让 Calico 的 Pod 启动即可

修改 calico 网络模式:

1
2
3
4
kubectl edit ippool default-ipv4-ippool
# 将 ipipMode: Always 修改为 ipipMode: Never
# 手动重启 calico-node
kubectl rollout restart daemonset calico-node -n kube-system

下载 calicoctl 工具:

1
2
wget https://github.com/projectcalico/calico/releases/download/v3.30.3/calicoctl-linux-amd64 -O calicoctl
chmod +x calicoctl

4. 防火墙配置

如果你的 vps 云厂商默认启用了安全组限制,需要打开 WireGuard 的监听端口的访问权限。

注意: 这里不需要再为 apiserver、etcd、kubelet 端口打开访问权限,因为前面已经做了 wg 组网。

因此,所有节点安全组上都仅需要却保对集群内其他节点的公网地址放开 51820/UDP

5. 加入工作节点

此部分内容仅在两个工作节点上进行

在两个工作节点上使用 root 权限执行 3.1 章节中记录下的 kubeadm join 命令即可加入集群:

1
2
kubeadm join 10.66.66.1:6443 --token abcdef.1234567890abcdef \
--discovery-token-ca-cert-hash sha256:1234...

执行成功,屏幕会打印 This node has joined the cluster

在 master 节点上执行以下命令,检查节点状态:

1
kubectl get nodes -o wide

正常情况下所有节点的状态都会是 Ready 并且 INTERNAL-IP 应该显示的是 WireGuard 组网时配置的私有 IP。
在第四步工作节点加入集群的时候工作节点会拉取 kube-proxy 和 calico-node 的镜像并部署,这一步也有可能会因为网络不好导致进度缓慢,所以如果节点是 NotReady 可以先等下再看。

检查 Pod 状态:

1
kubectl get pods -A -o wide

确保 calico-node, coredns, kube-proxy 等所有 Pod 都处于 Running 状态。

使用 calicoctl 工具检查 BGP 是否建立成功:

1
./calicoctl node status

输出结果里面每一个节点都应该是 Established 才正常。

6. 暴露服务

6.1 部署 ingress

在 master 上执行以下命令部署 ingress:

1
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.13.2/deploy/static/provider/baremetal/deploy.yaml

6.2 创建 tunnel

登录 cloudflare 仪表盘,点击左侧面板进入 ZeroTrust 页面,随后依次点击 “网络”-“tunnels”-“创建隧道”-“选择 cloudflared”-“自定义名称”-“保存”-“环境选择 Docker”-“点击复制命令”
随后记录下–token 后面的一长串字符,例如我的是 eyJhxxxxxxxxxxxxxxxxxxxxVdyJ9

6.3 创建 secret

在 k8s 中通常使用 secret 安全保存各类 token、密码等内容:
新建 cloudflare-tunnel-secret.yaml,写入以下内容:

1
2
3
4
5
6
7
apiVersion: v1
kind: Secret
metadata:
name: tunnel-token-secret # 自定义的名字,后面要用到
namespace: default
stringData:
token: "<尖括号内的这部分文字替换成上面复制出来的Token>"

创建 secret:

1
kubectl apply -f cloudflare-tunnel-secret.yaml

6.4 创建 cloudflare deployment

上一步的 secret 创建好之后即可进一步新建 deployment,新建 cloudflare-deployment.yaml 文件并写入以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
apiVersion: apps/v1
kind: Deployment
metadata:
name: cloudflared
namespace: default
labels:
app: cloudflared
spec:
replicas: 2 # 2个副本高可用
selector:
matchLabels:
app: cloudflared
template:
metadata:
labels:
app: cloudflared
spec:
containers:
- name: cloudflared
image: cloudflare/cloudflared:latest
args:
- tunnel
- --no-autoupdate
- run
- --token
- $(TUNNEL_TOKEN)
env:
- name: TUNNEL_TOKEN
valueFrom:
secretKeyRef:
# 下面这行 name 的值必须和 5.3 章节中创建的 name 值保持一致
name: tunnel-token-secret
key: token

创建 deployment:

1
kubectl apply -f cloudflare-deployment.yaml

创建完成之后检查 cloudflare 的 pod 状态运行正常即可。

6.5 创建测试用 web 页面

新建一个 hello-deployment.yaml 文件,并写入以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello-app
namespace: default
spec:
replicas: 2 # 2个副本高可用
selector:
matchLabels:
app: hello-app
template:
metadata:
labels:
app: hello-app
spec:
containers:
- name: hello-app
image: nginxdemos/hello # 惯用的用于演示的 ng 镜像
ports:
- containerPort: 80

创建 deployment:

1
kubectl apply -f hello-deployment.yaml

新建一个 hello-service.yaml 文件,并写入以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: Service
metadata:
name: hello-app-service
namespace: default
spec:
type: ClusterIP # 只在集群内部暴露服务
selector:
app: hello-app
ports:
- protocol: TCP
port: 80 # service 监听端口
targetPort: 80 # 流量转发到容器的端口

创建 service:

1
kubectl apply -f hello-service.yaml

检查 deployment、pod、service 是否正常:

1
kubectl get deployment,service,pods -n default

新建 hello-app-ingress.yaml 文件,并写入以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: hello-app-ingress
namespace: default
spec:
ingressClassName: "nginx"
rules:
- host: "hello.com" # 替换为自己的域名,例如 blog.xxx.com、test.xxx.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: hello-app-service # 流量转发到 hello-app-service
port:
number: 80

创建 ingress:

1
kubectl apply -f hello-app-ingress.yaml

6.6 配置域名和路由

这里以 blog.hello.com 域名为例。
回到之前打开的 cloudflare tunnel 页面,选择“下一步”,子域栏填 hello,域栏选择托管在 cloudflare 上的 hello.com,路径栏留空,服务选择 HTTP,URL 栏填 http://ingress-nginx-controller.ingress-nginx,最后点击保存完成配置。

7. 验证

浏览器访问 https://blog.hello.com 并勾选 Auto Refresh,应该可以看到每秒自动刷新的 nginx 图标页面,并且会显示 pod 的 ip

高可用测试:
轮流关闭工作节点主机,可以看到页面仍然能够正常访问,且 IP 会对应的变成剩余工作节点上的那个 pod ip。

后续新上线一个服务,就要创建 deployment-service-ingress-web 上跳转 tunnel。
因为我是有个域名完全拿来在这个环境做测试的,所以我的 tunnel 上面配置的是 * 通配符跳转,tunnel 相当于仅作为连接器。而 ingress 负责实现实验学习所需要的复杂的流量路由,同时这样后面也就不用在网页上调整 cloudflare 了。
如果你不需要用 ingress 来做这种复杂的流量路由实验,则完全可以去除本文中 ingress 相关的章节。用 tunnel 既当连接器又当路由分发器。比如直接跳过创建 ingress 部分,同样按照文中创建完成 hello-app 的 deployment 和 service 之后。在 tunnel 页面依然选择 HTTP 类型,URL 改成你 service 对应的 cluster ip 即可。