CentOS 9 部署 Kubernetes 1.30 集群实战:一份可复制、可验证的完整手册

适用版本:Kubernetes v1.30.0
适用环境:VMware NAT,CentOS Stream 9,1 Master + 2 Worker
适用场景:Kubernetes 实验、GitOps 练习、云原生部署学习

这个项目是什么

这是一份从零搭建 Kubernetes 集群的完整项目文档。它不是零散命令的堆砌,而是一个带有明确验收标准的部署流程。

项目最终会得到一套可继续做实验的 Kubernetes 环境:

  • 1 个控制平面节点和 2 个 Worker 节点
  • containerd 容器运行时
  • Flannel 网络插件
  • Metrics Server 资源监控
  • Local Path Provisioner 本地存储
  • Ingress-Nginx 流量入口
  • DNS、Service、Pod 网络、Ingress、PVC 等基础能力闭环验证

文章会按“环境规划、母版配置、克隆后网络配置、集群初始化、基础设施组件、最终验收、故障排查”的顺序展开。

适合谁阅读

这篇文章适合以下读者:

  • 已经掌握 Linux 基础命令,希望亲手搭建 Kubernetes 实验环境的人
  • 准备学习 GitOps、ArgoCD、Flux、CI/CD、微服务部署的人
  • 需要一套可复现的本地 Kubernetes 测试环境的人

阅读本文前,建议先了解以下概念:

  • Linux 系统管理、systemd、yum/dnf
  • 网络基础:IP、网关、DNS、网卡
  • Kubernetes 基本概念:Node、Pod、Service、Deployment、Ingress、PVC

读完这篇文章能得到什么

完成全部步骤后,你可以得到:

  1. 一台模板机,可快速克隆出多台 K8s 节点
  2. 一个 3 节点 Kubernetes 集群
  3. 一套带验证命令的部署流程
  4. 一套故障排查速查表
  5. 一个可以直接承载 GitOps 实验的基础环境

一、环境规划

1.1 集群规划

角色 IP 主机名 建议配置
Master 192.168.113.101 k8s-master 4 核 / 4 Gi+ RAM / 40 G 磁盘
Node01 192.168.113.111 k8s-node01 2 核 / 2 Gi+ RAM / 40 G 磁盘
Node02 192.168.113.112 k8s-node02 2 核 / 2 Gi+ RAM / 40 G 磁盘

1.2 软件版本

组件 版本
操作系统 CentOS Stream 9
Kubernetes v1.30.0
容器运行时 containerd.io
CNI Flannel v0.26.6
Ingress ingress-nginx controller-v1.11.5
Metrics Server 建议固定到 release 版本
StorageClass local-path v0.0.31

1.3 网络规划

  • VMware 网络:NAT
  • 网段:192.168.113.0/24
  • 网关:192.168.113.2
  • Pod 网段:10.244.0.0/16
  • Service 网段:10.96.0.0/12
  • DNS 域名:cluster.local

二、设计思路

这套部署方案采用“模板机 + 克隆”的方式:

  1. 先在一台虚拟机上完成系统初始化、containerd、kubeadm、kubelet、kubectl 的安装。
  2. 关机后克隆出 Master 和 Worker。
  3. 每台克隆机只修改主机名、IP、DNS。
  4. 最后在 Master 上执行 kubeadm init,在 Worker 上执行 kubeadm join

这样做的优点是:

  • 三台节点软件版本完全一致
  • 安装过程只需要做一次
  • 后续重建节点时更快

另一个关键设计是“每一步都有验证”。每个阶段都提供验证命令,避免到最后一刻才发现前面某一步失败。

三、第一阶段:模板机基础配置

执行位置:母版虚拟机,root 用户
目标:配置系统、内核参数、容器运行时和 Kubernetes 工具

3.1 系统基础设置

关闭 swap、SELinux 和 firewalld。Kubernetes 集群对这三项有比较严格的要求,实验环境建议直接关闭。

sudo swapoff -a
sudo sed -ri 's/.*swap.*/#&/' /etc/fstab
sudo setenforce 0
sudo sed -i 's/^SELINUX=enforcing$/SELINUX=disabled/' /etc/selinux/config
sudo systemctl disable --now firewalld

验证:

free -h | grep -i swap
getenforce
systemctl status firewalld | grep Active

预期结果:

  • swap 显示 0B
  • getenforce 返回 Permissive,重启后为 Disabled
  • firewalld 为 inactive (dead)

3.2 内核模块与网络参数

Kubernetes 需要 overlay 和 br_netfilter 内核模块,同时需要开启 IPv4 转发。

cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
sudo modprobe overlay
sudo modprobe br_netfilter

cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables  = 1
net.ipv4.ip_forward                 = 1
net.bridge.bridge-nf-call-ip6tables = 1
EOF
sudo sysctl --system

验证:

lsmod | grep -E "overlay|br_netfilter"
sudo sysctl net.bridge.bridge-nf-call-iptables
sudo sysctl net.ipv4.ip_forward

预期结果:

  • overlay 和 br_netfilter 都有输出
  • 两个 sysctl 参数都为 1

3.3 安装基础工具

sudo dnf install -y wget curl net-tools vim bash-completion

3.4 安装 containerd 并配置镜像加速

这里使用阿里云 Docker CE 镜像源,并把 pause 镜像换成阿里云地址,避免国内网络拉取失败。

sudo curl -o /etc/yum.repos.d/docker-ce.repo \
  https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
sudo sed -i 's+download.docker.com+mirrors.aliyun.com/docker-ce+' \
  /etc/yum.repos.d/docker-ce.repo
sudo dnf makecache
sudo dnf install -y containerd.io

生成 containerd 默认配置,并修改两个关键参数:

sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml > /dev/null
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/g' /etc/containerd/config.toml

PAUSE_IMAGE="registry.aliyuncs.com/google_containers/pause:3.10.2"
sudo sed -i "s|sandbox = '.*'|sandbox = '${PAUSE_IMAGE}'|" /etc/containerd/config.toml

sudo systemctl daemon-reload
sudo systemctl enable --now containerd

验证:

sudo systemctl is-active containerd
grep SystemdCgroup /etc/containerd/config.toml
grep "sandbox = " /etc/containerd/config.toml

预期结果:

  • containerd 为 active
  • SystemdCgroup = true
  • sandbox 指向阿里云 pause 镜像

3.5 安装 kubeadm、kubelet、kubectl

cat <<EOF | sudo tee /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://mirrors.aliyun.com/kubernetes-new/core/stable/v1.30/rpm/
enabled=1
gpgcheck=1
gpgkey=https://mirrors.aliyun.com/kubernetes-new/core/stable/v1.30/rpm/repodata/repomd.xml.key
EOF

sudo dnf install -y kubelet-1.30.0 kubeadm-1.30.0 kubectl-1.30.0
sudo systemctl enable --now kubelet

验证:

kubeadm version -o short
kubelet --version
kubectl version --client -o yaml | grep gitVersion

预期结果:

  • kubeadm 为 v1.30.0
  • kubelet 为 Kubernetes v1.30.0
  • kubectl client 为 v1.30.0

提示:此时 kubelet 还没有集群配置,服务可能不会真正 Ready,这是正常现象,等 kubeadm init 后才会正常。

完成后关机,基于这台母版克隆三台虚拟机。

四、第二阶段:克隆后网络配置

执行位置:三台克隆机分别执行,root 用户

4.1 设置主机名和 hosts

在每台机器上把 <HOSTNAME><IP> 替换成实际值:

HOSTNAME="<HOSTNAME>"
IP="<IP>"
sudo hostnamectl set-hostname "$HOSTNAME"

cat <<EOF | sudo tee /etc/hosts
127.0.0.1   localhost localhost.localdomain
192.168.113.101 k8s-master
192.168.113.111 k8s-node01
192.168.113.112 k8s-node02
EOF

4.2 修改 IP、网关和 DNS

ETH_NAME=$(ip route get 8.8.8.8 | awk '{print $5; exit}')
sudo nmcli connection modify "$ETH_NAME" \
  ipv4.addresses "$IP/24" \
  ipv4.gateway 192.168.113.2 \
  ipv4.dns "223.5.5.5 114.114.114.114" \
  ipv4.method manual
sudo nmcli connection up "$ETH_NAME"

验证:

hostname
ping -c 2 192.168.113.2
ping -c 2 k8s-master

预期结果:

  • 三台主机名分别为 k8s-masterk8s-node01k8s-node02
  • 网关和主机名 ping 均为 0% packet loss

五、第三阶段:初始化集群

执行位置:k8s-master,root 用户

5.1 使用 YAML 配置 kubeadm init

相比直接在命令行写一堆参数,YAML 配置更容易阅读、复用和保存。

cat <<'EOF' | sudo tee kubeadm-init.yaml
apiVersion: kubeadm.k8s.io/v1beta3
kind: InitConfiguration
localAPIEndpoint:
  advertiseAddress: "192.168.113.101"
  bindPort: 6443
nodeRegistration:
  criSocket: "unix:///var/run/containerd/containerd.sock"
  name: "k8s-master"
---
apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
kubernetesVersion: "v1.30.0"
imageRepository: "registry.aliyuncs.com/google_containers"
controlPlaneEndpoint: "k8s-master:6443"
networking:
  podSubnet: "10.244.0.0/16"
  serviceSubnet: "10.96.0.0/12"
  dnsDomain: "cluster.local"
apiServer:
  extraArgs:
    authorization-mode: "Node,RBAC"
    enable-admission-plugins: "NodeRestriction"
controllerManager:
  extraArgs:
    bind-address: "0.0.0.0"
scheduler:
  extraArgs:
    bind-address: "0.0.0.0"
---
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: "systemd"
failSwapOn: true
EOF

sudo kubeadm init --config kubeadm-init.yaml --upload-certs

kubeadm init 完成后,会输出一条 join 命令。请保存下来:

kubeadm join k8s-master:6443 --token <TOKEN> --discovery-token-ca-cert-hash sha256:<HASH>

5.2 配置 kubectl

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
echo 'source <(kubectl completion bash)' >> ~/.bashrc
source ~/.bashrc

验证:

kubectl version -o yaml | grep gitVersion
kubectl get nodes

预期结果:

  • client 和 server 都是 v1.30.0
  • k8s-master 节点出现,STATUS 为 NotReady

此时 NotReady 是正常的,因为网络插件还没有安装。

5.3 安装 Flannel

kubectl apply -f https://raw.githubusercontent.com/flannel-io/flannel/v0.26.6/Documentation/kube-flannel.yml

等待约 30 秒后验证:

kubectl get pods -n kube-flannel
kubectl get pods -n kube-system | grep coredns

预期结果:

  • Flannel Pod 为 1/1 Running
  • CoreDNS 从 Pending 变为 Running

如果无法访问 GitHub raw 地址,需要为服务器配置代理,或使用可用的 Flannel 镜像源。

5.4 加入 Worker 节点

执行位置:k8s-node01 和 k8s-node02,root 用户

sudo kubeadm join k8s-master:6443 --token <TOKEN> \
    --discovery-token-ca-cert-hash sha256:<HASH>

如果 Token 过期,在 Master 上重新生成:

sudo kubeadm token create --print-join-command

在 Master 上验证:

kubectl get nodes
kubectl get pods -n kube-flannel -o wide
kubectl get pods -A | grep -v Running | grep -v Completed

预期结果:

  • 三行节点全部 Ready
  • 3 个 Flannel Pod 全部 1/1 Running
  • 除了 Running 和 Completed,没有异常 Pod

六、第四阶段:基础设施组件部署

执行位置:k8s-master,root 用户

6.1 Metrics Server

Metrics Server 提供 kubectl top 所需的 Metrics API。

这里使用 release 版本清单。建议根据实验环境固定版本,避免 latest 突然变化。

kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml

实验环境中,通常需要增加 --kubelet-insecure-tls 参数:

kubectl patch deployment metrics-server -n kube-system \
  --type='json' \
  -p='[{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value": "--kubelet-insecure-tls"}]'

kubectl wait --for=condition=ready pod \
  -l k8s-app=metrics-server -n kube-system --timeout=120s

验证:

kubectl top nodes

预期结果:

  • 三行节点 CPU 和内存数据
  • 没有 Metrics API not available 错误

注意:--kubelet-insecure-tls 只适合本地实验环境。生产环境应配置受信任证书,不要关闭 TLS 校验。

6.2 Local Path Provisioner

Local Path Provisioner 会提供一个本地磁盘 StorageClass,适合实验环境。

kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.31/deploy/local-path-storage.yaml

kubectl patch storageclass local-path \
  -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'

验证:

kubectl get sc

预期结果:

  • local-path 显示为 (default)

Local Path 的 PVC 是 WaitForFirstConsumer,必须由 Pod 消费后才会绑定。做一次读写测试:

kubectl apply -f - <<'EOF2'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: test-pvc
spec:
  accessModes: ["ReadWriteOnce"]
  resources:
    requests:
      storage: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
  name: test-pod
spec:
  volumes:
  - name: data
    persistentVolumeClaim:
      claimName: test-pvc
  containers:
  - name: app
    image: busybox:1.36
    command: ["sleep", "300"]
    volumeMounts:
    - name: data
      mountPath: /data
EOF2

kubectl wait --for=jsonpath='{.status.phase}'=Bound pvc/test-pvc --timeout=60s
kubectl get pvc test-pvc

测试完成后清理:

kubectl delete pod test-pod
kubectl delete pvc test-pvc

6.3 Ingress-Nginx Controller

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

等待约 60 秒后验证:

kubectl get pods -n ingress-nginx
kubectl get svc -n ingress-nginx ingress-nginx-controller

预期结果:

  • Ingress Controller Pod 为 1/1 Running
  • Service 显示 NodePort 端口

6.4 端到端 Ingress 测试

kubectl create deployment web-test --image=nginx:alpine --replicas=2
kubectl expose deployment web-test --port=80 --target-port=80

cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: test-ingress
spec:
  ingressClassName: nginx
  rules:
  - host: test.k8s.local
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: web-test
            port:
              number: 80
EOF

验证:

NODE_PORT=$(kubectl get svc -n ingress-nginx ingress-nginx-controller \
  -o jsonpath='{.spec.ports[?(@.port==80)].nodePort}')
NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}')
curl -s -H "Host: test.k8s.local" http://$NODE_IP:$NODE_PORT | head -5

预期结果:

  • 返回 nginx 欢迎页 HTML
  • 包含 <title>Welcome to nginx!</title>

测试完成后清理:

kubectl delete ingress test-ingress
kubectl delete service web-test
kubectl delete deployment web-test

七、最终验收清单

复制执行下面这些检查,确认环境已经闭环。

7.1 节点全部 Ready

kubectl get nodes -o wide

预期:三行节点,STATUS 全部 Ready

7.2 系统 Pod 全部正常

kubectl get pods -A | grep -v Running | grep -v Completed

预期:无输出。

7.3 DNS 可用

kubectl run dns-test --rm -i --restart=Never --image=busybox:1.36 \
  -- nslookup kubernetes.default.svc.cluster.local

预期:返回 10.96.0.1

busybox 的 nslookup 不会自动追加 search 域,所以必须使用完整 FQDN。

7.4 跨节点 Pod 网络互通

cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: net-a
spec:
  nodeName: k8s-node01
  containers:
  - name: app
    image: busybox:1.36
    command: ["sleep", "300"]
---
apiVersion: v1
kind: Pod
metadata:
  name: net-b
spec:
  nodeName: k8s-node02
  containers:
  - name: app
    image: busybox:1.36
    command: ["sleep", "300"]
EOF

sleep 5
A_IP=$(kubectl get pod net-a -o jsonpath='{.status.podIP}')
kubectl exec net-b -- ping -c 2 "$A_IP"

预期:0% packet loss,回包为 10.244.x.x 的 Pod IP。

测试完成后清理:

kubectl delete pod net-a net-b

7.5 Metrics API 可用

kubectl top nodes

预期:显示 CPU% 和 MEMORY%。

7.6 StorageClass 就绪

kubectl get sc

预期:local-path (default)

7.7 Ingress 就绪

kubectl get pods -n ingress-nginx

预期:Running 1/1

八、故障排查速查表

现象 第一反应 深入排查
节点 NotReady kubectl describe node <node> 看 Conditions 和 Events
Pod Pending kubectl describe pod <pod> 资源不足或网络未就绪
ImagePullBackOff kubectl describe pod <pod> 镜像 tag 或网络问题
CrashLoopBackOff kubectl logs <pod> --previous 看上次崩溃日志
Flannel 0/1 Not Ready kubectl logs -n kube-flannel <pod> 使用固定版本 v0.26.6
DNS 不通 kubectl logs -n kube-system -l k8s-app=kube-dns 检查 CoreDNS 和 CNI
PVC Pending kubectl describe pvc <pvc> 检查 StorageClass 和调度节点
kubectl top 不可用 kubectl get pods -n kube-system | grep metrics 检查 Metrics Server

九、常见坑与注意事项

  1. 不要在未关闭 swap 的机器上直接 init。 kubelet 配置了 failSwapOn: true,swap 开启会导致节点异常。
  2. Flannel 版本要固定。 不同 Flannel 版本可能存在兼容性差异,教程中固定为 v0.26.6。
  3. CoreDNS Pending 不一定是 DNS 问题。 先检查 Flannel 是否 Running,再排查 CoreDNS。
  4. busybox nslookup 必须写完整域名。 例如 kubernetes.default.svc.cluster.local,不要只写 kubernetes
  5. Local Path 是单节点本地盘。 数据删除后不可恢复,只适合实验环境。
  6. Metrics Server 的 --kubelet-insecure-tls 只适合实验。 生产环境应使用受信任证书。
  7. 克隆机之间要确保主机名、IP、UUID 都不同。 网络配置错误是克隆后最常见的问题。
  8. GitHub raw 地址在某些网络下访问不稳定。 如果拉取失败,先解决代理或镜像源,再继续下一步。

十、总结

到这里,你已经完成了一套可验证的 Kubernetes 实验环境:

  • 3 节点集群全部 Ready
  • containerd 作为容器运行时
  • Flannel 提供 Pod 网络
  • Metrics Server 提供资源监控
  • Local Path 提供本地存储
  • Ingress-Nginx 提供 HTTP/HTTPS 入口

接下来可以继续做这些实验:

  • 安装 ArgoCD 或 Flux,实现 GitOps 部署
  • 安装 cert-manager,管理 Ingress 证书
  • 部署 Prometheus 和 Grafana,做集群监控
  • 部署微服务应用,验证 Service、Ingress、PVC 组合使用
  • 练习 etcd 备份和集群升级

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐