本文记录一次真实的三节点 K8s 集群搭建全过程:3 台阿里云 ECS(4C8G)、Alibaba Cloud Linux 4、kubeadm v1.31.14、containerd 运行时、Calico 网络插件,最后部署 Prometheus + Grafana 监控栈。所有命令和输出均为实战实录,踩坑过程一并保留,可直接复现。
1. 环境规划
1.1 节点信息
| 主机名 | 内网 IP | 公网 IP | 角色 | 配置 | 操作系统 |
|---|---|---|---|---|---|
| k8s-master | 192.168.0.201 | 131.42.218.249 | 控制平面 | 4核8G | Alibaba Cloud Linux 4 LTS |
| k8s-node1 | 192.168.0.202 | 47.96.118.121 | 工作节点 | 4核8G | Alibaba Cloud Linux 4 LTS |
| k8s-node2 | 192.168.0.203 | 118.158.121.148 | 工作节点 | 4核8G | Alibaba Cloud Linux 4 LTS |
架构方案:
- Kubernetes v1.31.14(阿里云新源最高支持版本)
- 容器运行时:containerd 1.7.29(K8s 1.24+ 官方标准,不使用 Docker)
- 网络插件:Calico v3.29.1
- 监控:Prometheus + Grafana(kube-prometheus-stack)
1.2 阿里云安全组配置【前置必做】
在阿里云控制台 → ECS 安全组,为这三台机器所在安全组添加入方向规则(源地址 192.168.0.0/16):
| 端口范围 | 协议 | 作用 |
|---|---|---|
| 6443 | TCP | K8s apiserver |
| 2379-2380 | TCP | etcd 集群通信 |
| 10250 | TCP | kubelet 通信 |
| 10259 | TCP | kube-scheduler |
| 10257 | TCP | kube-controller-manager |
| 179 | TCP | Calico BGP 路由 |
| 30000-32767 | TCP/UDP | NodePort 服务端口 |
⚠️ 漏配安全组是最常见的坑:集群内网不通、Calico Pod 起不来,90% 都是这里的问题。
2. 三台节点统一初始化
以下操作三台机器全部执行,逐一验证。
2.1 设置主机名
# Master(131.42.218.249)
hostnamectl set-hostname k8s-master
# Node1(47.96.118.121)
hostnamectl set-hostname k8s-node1
# Node2(118.158.121.148)
hostnamectl set-hostname k8s-node2
执行结果(Master):
$ hostnamectl set-hostname k8s-master && hostname
k8s-master
三台分别确认:
# node1
k8s-node1
# node2
k8s-node2
2.2 配置 hosts 解析(三台执行)
cat >> /etc/hosts <<EOF
192.168.0.201 k8s-master
192.168.0.202 k8s-node1
192.168.0.203 k8s-node2
EOF
验证:
$ tail -3 /etc/hosts
192.168.0.201 k8s-master
192.168.0.202 k8s-node1
192.168.0.203 k8s-node2
2.3 关闭 SELinux 和防火墙
setenforce 0
sed -i 's/^SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config
systemctl stop firewalld
systemctl disable firewalld
执行结果:
$ getenforce
Disabled
2.4 永久关闭 Swap
swapoff -a
sed -i '/swap/s/^/#/' /etc/fstab
grep swap /etc/fstab || echo "(no swap entries found - OK)"
执行结果(三台一致):
Swap closed, checking fstab:
(no swap entries found - OK)
2.5 加载内核模块 + 开启网络转发
modprobe overlay
modprobe br_netfilter
cat > /etc/sysctl.d/k8s.conf <<EOF
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF
sysctl --system
验证(三台均输出):
net.ipv4.ip_forward = 1
2.6 添加阿里云 K8s YUM 源【踩坑记录】
坑 1:文档里的 el7 旧源不支持 Alibaba Cloud Linux 4
最初按老教程写的源:
# ❌ 失败:el7 源最高只有 1.28,没有 1.31
baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64/
yum makecache 直接报 GPG 签名错误:
Error: Failed to download metadata for repo 'kubernetes':
repomd.xml GPG signature verification error: Bad GPG signature
即使关闭 GPG 检查,也只能装到 1.28:
$ yum list --showduplicates kubeadm
kubeadm.x86_64 1.25.5-0 kubernetes
...
kubeadm.x86_64 1.28.2-0 kubernetes # 最高只到 1.28
正确做法:使用阿里云 kubernetes-new 源
cat > /etc/yum.repos.d/kubernetes.repo <<EOF
[kubernetes]
name=Kubernetes
baseurl=https://mirrors.aliyun.com/kubernetes-new/core/stable/v1.31/rpm/
enabled=1
gpgcheck=0
repo_gpgcheck=0
EOF
yum clean all && yum makecache
验证:
$ yum list kubeadm --showduplicates
kubeadm.x86_64 1.31.11-150500.1.1 kubernetes
kubeadm.x86_64 1.31.12-150500.1.1 kubernetes
kubeadm.x86_64 1.31.13-150500.1.1 kubernetes
kubeadm.x86_64 1.31.14-150500.1.1 kubernetes # 选最新
3. 安装 containerd 运行时(三台执行)
3.1 安装并生成默认配置
yum install -y containerd
containerd config default > /etc/containerd/config.toml
3.2 修改关键配置
# 开启 systemd cgroup 驱动(必须和 kubelet 保持一致)
sed -i 's#SystemdCgroup = false#SystemdCgroup = true#g' /etc/containerd/config.toml
# 替换 pause 镜像为阿里云镜像(防止拉取超时)
sed -i 's#registry.k8s.io/pause#registry.aliyuncs.com/google_containers/pause#g' /etc/containerd/config.toml
sed -i 's#registry.k8s.io#registry.aliyuncs.com/google_containers#g' /etc/containerd/config.toml
3.3 启动并设置开机自启
systemctl daemon-reload
systemctl start containerd
systemctl enable containerd
systemctl status containerd
执行结果:
● containerd.service - containerd container runtime
Loaded: loaded (/usr/lib/systemd/system/containerd.service; enabled)
Active: active (running) since Mon 2026-08-03 11:41:51 CST
Main PID: 4386 (containerd)
...
"containerd successfully booted in 0.048508s"
三台均安装成功:containerd 1.7.29 + runc 1.2.8。
4. 安装 K8s 核心组件(三台执行)
yum install -y kubeadm-1.31.14 kubelet-1.31.14 kubectl-1.31.14
systemctl enable kubelet
验证版本(三台一致):
$ kubeadm version
kubeadm version: &version.Info{Major:"1", Minor:"31", GitVersion:"v1.31.14", ...}
$ kubectl version --client
Client Version: v1.31.14
Kustomize Version: v5.4.2
$ kubelet --version
Kubernetes v1.31.14
5. Master 节点初始化(仅在 192.168.0.201 执行)
5.1 初始化控制平面
kubeadm init \
--apiserver-advertise-address=192.168.0.201 \
--image-repository registry.aliyuncs.com/google_containers \
--kubernetes-version v1.31.14 \
--service-cidr=10.96.0.0/12 \
--pod-network-cidr=10.244.0.0/16
关键输出(完整流程):
[init] Using Kubernetes version: v1.31.14
[preflight] Running pre-flight checks
[preflight] Pulling images required for setting up a Kubernetes cluster
[certs] Generating "ca" certificate and key
[certs] Generating "apiserver" certificate and key
[certs] apiserver serving cert is signed for DNS names [k8s-master kubernetes ...]
and IPs [10.96.0.1 192.168.0.201]
...
[wait-control-plane] Waiting for the kubelet to boot up the control plane
[kubelet-check] The kubelet is healthy after 1.000841782s
[api-check] The API server is healthy after 5.501143988s
[addons] Applied essential addon: CoreDNS
[addons] Applied essential addon: kube-proxy
Your Kubernetes control-plane has initialized successfully!
重点:保存好 join 命令! 输出末尾的这段,Node 节点加入要用:
kubeadm join 192.168.0.201:6443 --token 5lxqj1.v7rnhfd00wbwmlw6 \
--discovery-token-ca-cert-hash sha256:312cf079265eb18b8578abf6a1ce1e087899460b332a945b9e1837f6a1fb46dc
Token 有效期 24 小时,过期后可用
kubeadm token create --print-join-command重新生成。
5.2 配置 kubectl
mkdir -p /root/.kube
cp -i /etc/kubernetes/admin.conf /root/.kube/config
chown $(id -u):$(id -g) /root/.kube/config
此时查看节点状态(NotReady 是正常现象,因为还没装 CNI 插件):
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
k8s-master NotReady control-plane 34s v1.31.14
5.3 部署 Calico 网络插件【踩坑记录】
坑 2:官方文档路径 404
# ❌ 失败
kubectl apply -f https://docs.projectcalico.org/v3.28/manifests/calico.yaml
# error: server reported 404 Not Found
正确做法(使用 GitHub raw 地址):
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.29.1/manifests/calico.yaml
输出:
poddisruptionbudget.policy/calico-kube-controllers created
serviceaccount/calico-kube-controllers created
serviceaccount/calico-node created
configmap/calico-config created
customresourcedefinition.apiextensions.k8s.io/bgpconfigurations.crd.projectcalico.org created
...(20+ 个 CRD)
daemonset.apps/calico-node created
deployment.apps/calico-kube-controllers created
6. Node 节点加入集群
在 k8s-node1 和 k8s-node2 分别执行:
kubeadm join 192.168.0.201:6443 --token 5lxqj1.v7rnhfd00wbwmlw6 \
--discovery-token-ca-cert-hash sha256:312cf079265eb18b8578abf6a1ce1e087899460b332a945b9e1837f6a1fb46dc
执行结果(Node1):
[preflight] Running pre-flight checks
[preflight] Reading configuration from the cluster...
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[kubelet-start] Starting the kubelet
[kubelet-check] The kubelet is healthy after 1.001627868s
This node has joined the cluster:
* Certificate signing request was sent to apiserver and a response was received.
* The Kubelet was informed of the new secure connection details.
Node2 同样加入成功。
踩坑 3:Calico 镜像拉取失败(国内网络问题)
等待后查看,所有节点 NotReady,Calico Pod 全是 ImagePullBackOff:
$ kubectl get pods -n kube-system | grep calico
calico-node-sdbkn 0/1 Init:ErrImagePull 0 2m23s
calico-node-cl845 0/1 Init:ImagePullBackOff 0 81s
calico-node-zdg9w 0/1 Init:ImagePullBackOff 0 83s
查看原因:
Failed to pull image "docker.io/calico/cni:v3.29.1":
dial tcp 104.244.43.6:443: i/o timeout # Docker Hub 国内无法直连
解决:配置 containerd 镜像加速(三台都执行)
mkdir -p /etc/containerd/certs.d/docker.io
cat > /etc/containerd/certs.d/docker.io/hosts.toml <<EOF
server = "https://docker.1ms.run"
[host."https://docker.1ms.run"]
capabilities = ["pull", "resolve"]
skip_verify = true
EOF
# 启用 config_path
sed -i 's#config_path = ""#config_path = "/etc/containerd/certs.d"#g' /etc/containerd/config.toml
systemctl restart containerd
⚠️ 注意:目录名必须是
docker.io,不是_default。这是 containerd hosts.toml 的匹配规则——按 registry 域名匹配目录。
删除失败的 Pod 让其重建:
kubectl delete pods -n kube-system -l k8s-app=calico-node --force --grace-period=0
kubectl delete pods -n kube-system -l k8s-app=kube-dns --force --grace-period=0
7. 集群验证
7.1 节点全部 Ready
$ kubectl get nodes -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP CONTAINER-RUNTIME
k8s-master Ready control-plane 16m v1.31.14 192.168.0.201 containerd://1.7.29
k8s-node1 Ready <none> 14m v1.31.14 192.168.0.202 containerd://1.7.29
k8s-node2 Ready <none> 14m v1.31.14 192.168.0.203 containerd://1.7.29
7.2 系统组件全部 Running
$ kubectl get pods -n kube-system
NAME READY STATUS RESTARTS AGE
calico-kube-controllers-5d7d9cdfd8-fnlrc 1/1 Running 0 33m
calico-node-9gjrw 1/1 Running 0 16m
calico-node-jcvzf 1/1 Running 0 33m
calico-node-tj7w7 1/1 Running 0 33m
coredns-855c4dd65d-59whc 1/1 Running 0 33m
coredns-855c4dd65d-drqlv 1/1 Running 0 16m
etcd-k8s-master 1/1 Running 0 44m
kube-apiserver-k8s-master 1/1 Running 0 44m
kube-controller-manager-k8s-master 1/1 Running 0 44m
kube-proxy-nf7s6 1/1 Running 0 42m
kube-proxy-wcrps 1/1 Running 0 44m
kube-proxy-xwxsv 1/1 Running 0 42m
kube-scheduler-k8s-master 1/1 Running 0 44m
7.3 部署测试应用
kubectl create deployment nginx-demo --image=nginx:alpine
kubectl expose deployment nginx-demo --type NodePort --port 80
$ kubectl get pods -l app=nginx-demo -o wide
NAME READY STATUS NODE IP
nginx-demo-db78b9d74-sw48p 1/1 Running k8s-node2 10.244.169.130
$ kubectl get svc nginx-demo
NAME TYPE CLUSTER-IP PORT(S) AGE
nginx-demo NodePort 10.97.237.99 80:31347/TCP 10s
内网访问验证:
$ curl -s http://192.168.0.201:31347 | head -5
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
集群搭建完成 🎉
8. 部署 Prometheus + Grafana 监控栈
8.1 安装 Helm 3(Master 执行)
GitHub 直连慢,改用华为云镜像:
curl -fsSL -o helm-v3.16.4-linux-amd64.tar.gz \
https://mirrors.huaweicloud.com/helm/v3.16.4/helm-v3.16.4-linux-amd64.tar.gz
tar -zxvf helm-v3.16.4-linux-amd64.tar.gz
mv linux-amd64/helm /usr/local/bin/helm
helm version
version.BuildInfo{Version:"v3.16.4", GitCommit:"7877b45b..."}
8.2 获取 kube-prometheus-stack chart【踩坑记录】
坑 4:Helm 仓库全部不可达
# ❌ 官方仓库:connection reset
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
# ❌ 华为云镜像:404
https://mirrors.huaweicloud.com/repository/helm/prometheus-community/
解决:本地下载 chart 包后上传
# 本地(能翻墙的机器)下载
curl -fsSL -o kube-prometheus-stack-66.3.0.tgz \
"https://github.com/prometheus-community/helm-charts/releases/download/kube-prometheus-stack-66.3.0/kube-prometheus-stack-66.3.0.tgz"
# 上传到 Master
scp kube-prometheus-stack-66.3.0.tgz root@131.42.218.249:/root/
8.3 安装监控栈
kubectl create namespace monitoring
helm install prometheus /root/kube-prometheus-stack-66.3.0.tgz \
--namespace monitoring \
--set grafana.adminPassword='YourPassword' \
--set grafana.persistence.enabled=true \
--set grafana.persistence.size=5Gi \
--set grafana.service.type=NodePort \
--set prometheus.service.type=NodePort \
--set prometheus.prometheusSpec.resources.limits.cpu='1' \
--set prometheus.prometheusSpec.resources.limits.memory='2Gi' \
--set alertmanager.enabled=true
8.4 连环踩坑解决
坑 5:admission webhook 镜像不存在
Failed to pull image "registry.k8s.io/ingress-nginx/kube-webhook-certgen:...": not found
阿里云镜像仓库里没有这个特定版本。解决:禁用 admission webhook(可选组件):
helm uninstall prometheus -n monitoring
kubectl delete namespace monitoring
kubectl create namespace monitoring
# 重装时加上禁用参数
helm install prometheus /root/kube-prometheus-stack-66.3.0.tgz \
--namespace monitoring \
... \
--set prometheusOperator.admissionWebhooks.enabled=false \
--set prometheusOperator.admissionWebhooks.patch.enabled=false
坑 6:quay.io 镜像拉取超时
operator 等组件镜像在 quay.io,同样需要加速(三台执行):
mkdir -p /etc/containerd/certs.d/quay.io
cat > /etc/containerd/certs.d/quay.io/hosts.toml <<EOF
server = "https://quay.io"
[host."https://quay.m.daocloud.io"]
capabilities = ["pull", "resolve"]
skip_verify = true
EOF
systemctl restart containerd
坑 7:Grafana PVC Pending(无 StorageClass)
Warning FailedScheduling pod has unbound immediate PersistentVolumeClaims
$ kubectl get sc
No resources found # 集群没有默认存储类
解决:安装 local-path-provisioner 本地存储:
kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.30/deploy/local-path-storage.yaml
# 设为默认 StorageClass
kubectl patch storageclass local-path -p \
'{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
PVC 成功绑定:
$ kubectl get pvc -n monitoring
NAME STATUS VOLUME CAPACITY STORAGECLASS
prometheus-grafana Bound pvc-663ee392-2d30-44de-b039-6a8593308cbf 5Gi local-path
坑 8:operator CrashLoopBackOff(TLS 证书缺失)
failed to create web server err="failed to load serving certificate and key:
open /cert/cert: no such file or directory"
因为禁用了 admission webhook 但 operator 仍要求 TLS 证书。解决:
# 创建空 secret + helm upgrade 禁用 TLS
kubectl create secret generic prometheus-kube-prometheus-admission -n monitoring
helm upgrade prometheus /root/kube-prometheus-stack-66.3.0.tgz \
--namespace monitoring \
... \
--set prometheusOperator.tls.enabled=false
坑 9:registry.k8s.io 镜像超时
kube-state-metrics 在 registry.k8s.io,配置 daocloud 加速:
mkdir -p /etc/containerd/certs.d/registry.k8s.io
cat > /etc/containerd/certs.d/registry.k8s.io/hosts.toml <<EOF
server = "https://registry.k8s.io"
[host."https://k8s.m.daocloud.io"]
capabilities = ["pull", "resolve"]
skip_verify = true
EOF
systemctl restart containerd
8.5 最终状态:全部 Running
$ kubectl get pods -n monitoring -o wide
NAME READY STATUS NODE
alertmanager-prometheus-kube-prometheus-alertmanager-0 2/2 Running k8s-node1
prometheus-grafana-7445c99b47-5gxvt 3/3 Running k8s-node2
prometheus-kube-prometheus-operator-85c55f769c-bgctl 1/1 Running k8s-node1
prometheus-kube-state-metrics-6489887dc-n97z7 1/1 Running k8s-node2
prometheus-prometheus-kube-prometheus-prometheus-0 2/2 Running k8s-node2
prometheus-prometheus-node-exporter-dwtl4 1/1 Running k8s-node2
prometheus-prometheus-node-exporter-n8mqk 1/1 Running k8s-master
prometheus-prometheus-node-exporter-twxb5 1/1 Running k8s-node1
8.6 访问入口
$ kubectl get svc -n monitoring | grep NodePort
prometheus-grafana NodePort 80:31964/TCP
prometheus-kube-prometheus-prometheus NodePort 9090:30090/TCP
| 组件 | 访问地址 | 凭证 |
|---|---|---|
| Grafana | http://131.42.218.249:31964 | admin / YourPassword |
| Prometheus | http://131.42.218.249:30090 | 无需登录 |
安全组需放行 31964 和 30090 端口(30000-32767 已在第一步放行 ✅)。
Grafana 登录页:
登录后首页(预置 20+ K8s 监控仪表盘):
内置 Kubernetes 仪表盘列表:
Kubernetes / Compute Resources / Cluster 集群资源总览:
Prometheus 查询界面:
9. 踩坑总结
| # | 问题 | 根因 | 解决方案 |
|---|---|---|---|
| 1 | yum 源 GPG 签名错误 | el7 旧源不兼容 Alinux4 | 换 kubernetes-new/core/stable/v1.31/rpm/ 源 |
| 2 | kubeadm-1.31.4 找不到 | 源里最高 1.31.14 | 用 1.31.14,同 minor 版本完全兼容 |
| 3 | Calico 404 | 官方 docs 路径变更 | 用 GitHub raw:raw.githubusercontent.com/projectcalico/calico/v3.29.1/... |
| 4 | docker.io 镜像超时 | 国内无法直连 Docker Hub | containerd certs.d 配置镜像加速(目录名必须叫 docker.io) |
| 5 | Helm 仓库不可达 | GitHub Pages 被墙 | 本地下载 chart tgz 后 scp 上传,用 helm install <tgz> |
| 6 | webhook-certgen not found | 阿里云仓库无此镜像 | 禁用 admissionWebhooks |
| 7 | PVC Pending | 集群无 StorageClass | 装 local-path-provisioner 并设为默认 |
| 8 | operator 要 /cert/cert | 禁用 webhook 但 TLS 仍开启 | --set prometheusOperator.tls.enabled=false |
| 9 | registry.k8s.io 超时 | Google 镜像仓库被墙 | k8s.m.daocloud.io 加速 |
10. 常用运维命令速查
# 节点与 Pod
kubectl get nodes -o wide
kubectl get pods -A -o wide
kubectl describe pod <pod> -n <ns> # 排错第一步:看 Events
# 日志
kubectl logs <pod> -n <ns> --tail=50
journalctl -u kubelet -f # kubelet 日志
# 集群事件(排错神器)
kubectl get events -A --sort-by=.lastTimestamp
# 重新生成 join 命令(token 过期后)
kubeadm token create --print-join-command
# 集群重置(推倒重来)
kubeadm reset -f
# containerd 手动拉镜像(排查镜像问题)
ctr -n k8s.io images pull --hosts-dir /etc/containerd/certs.d <image>
附录:最终集群全貌
集群:
- Kubernetes v1.31.14 / containerd 1.7.29 / Calico v3.29.1
- 1 Master + 2 Worker,全部 Ready
监控栈(kube-prometheus-stack 66.3.0):
- Prometheus v2.55.1(NodePort 30090)
- Grafana 11.3.1(NodePort 31964,持久化 5Gi)
- Alertmanager + node-exporter(3 节点全覆盖)+ kube-state-metrics
镜像加速配置(三台统一):
/etc/containerd/certs.d/
├── docker.io/hosts.toml → docker.1ms.run
├── quay.io/hosts.toml → quay.m.daocloud.io
└── registry.k8s.io/hosts.toml → k8s.m.daocloud.io