CentOS 7 上装 Kubernetes,这活儿我这些年至少干了二十遍。都说 k8s 不好装,其实真正折腾人的不是 k8s 本身,而是版本兼容、网络插件和镜像来源这三件事。这篇内容就是一套我在 CentOS 7 上反复验证过的部署流程,从系统初始化开始,到 kubeadm init、Flannel 网络、工作节点加入,再到用 Nginx 把集群跑通。适合只有一两台机器练手的人,也适合给生产版本选型做预演。整篇按 1.23.17 + Docker 20.10 这套组合来写,因为它在 CentOS 7 上最稳,如果你打算用更新的版本,我会在文里专门讲差异。
1. 动手之前,先把手上的版本调性搞清楚
1.1 CentOS 7 到底适不适合跑 K8s
很多人一听到 CentOS 7 就觉得老,但现实是大量服务器现在还在跑 CentOS 7,而且内核版本虽然只有 3.10,K8s 官方和容器运行时都一直在兼容它。给 CentOS 7 装 K8s,真正要盯住的不是系统本身,而是几个配套组件有没有选择到合适的版本:容器运行时、kubeadm、kubelet、kubectl、CNI 插件,这五样只要有一个版本不匹配,集群就会出现各种“玄学问题”。
我的建议是,只要不是必须上新功能,CentOS 7 上就不要追 K8s 最新版。生产环境里 K8s 1.23 到 1.28 这几个版本都很常见,但 CentOS 7 上最省事的组合往往不是最新版,而是稳定迭代了很久的版本。我这几年在 CentOS 7 上跑 1.23 系列的体验最好,坑基本都被社区踩平了,教程也多,随便搜都能找到对应的解决方案。
1.2 版本组合怎么选:老派 Docker 还是新派 containerd
K8s 和容器运行时之间的关系,我简单类比一下:K8s 是管“集装箱”的调度中心,Docker 或者 containerd 是真正干活的那个“起重机”。K8s 1.24 之前,Docker 是默认的容器运行时,kubelet 会通过 dockershim 直接调用 Docker;从 1.24 开始,dockershim 被移除,K8s 不再直接支持 Docker,底层必须用 containerd、CRI-O 这类符合 CRI 标准的运行时。
所以这里会分出两条路线。路线一:K8s 1.23.17 + Docker 20.10,安装最简单,网上大部分旧教程都能直接用;路线二:K8s 1.28+ + containerd,适合要新特性的人,但配置 containerd 时要注意 systemd cgroup 和 sandbox 镜像地址,细节比第一种更多。这篇完整流程按路线一来讲,末尾我会单独说明 containerd 方案的差异点。千万别拿着 1.23 的文档去装 1.29 的集群,很多参数已经变了。
1.3 节点和网络规划,两台机器怎么分
最少两台机器就能搭一个标准集群:一台作为控制节点,跑控制面组件;一台作为工作节点,跑业务负载。当然,单台也可以,后面我会讲怎么把控制节点的工作负载调度限制去掉。资源方面,控制节点我建议至少 2 核 4G,工作节点至少 2 核 4G,磁盘空余 20G 以上。如果只有 2G 内存,kubeadm init 时经常因为 etcd 或 API Server 内存不足而失败,即使勉强起来,集群也会很脆弱。
网络规划上,要提前定好两个 CIDR:一个是集群内 Pod 的网段,一个是 Service 的网段。这篇文章用 Flannel 做 CNI,所以 Pod 网段用 Flannel 默认的 10.244.0.0/16;Service 网段用 kubeadm 默认的 10.96.0.0/12。这两个网段必须保证和你的物理机、虚拟机局域网网段不冲突,否则路由会出现奇怪的问题。我见过有人把 Pod 网段设成 192.168.0.0/16,结果和家里路由器网段撞车,节点状态反复 Ready 又 NotReady,折腾了一整天才发现是网段打架。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 系统层面先收拾干净
2.1 关防火墙、swap 和 SELinux
装 K8s 之前,系统初始化这一步我从来不会跳过。首先是防火墙,CentOS 7 默认的 firewalld 在装 K8s 时会挡住很多内部通信,测试环境直接停掉最省事。执行以下命令:
bash复制systemctl stop firewalld
systemctl disable firewalld
然后是 swap,kubelet 默认不允许节点使用 swap,因为 K8s 的内存模型里,内存配额是硬约束,如果 pod 被 swap 悄悄拿去换页,内存 limit 就会失真。因此要把 swap 永久关闭:
bash复制swapoff -a
sed -i '/ swap / s/^/#/' /etc/fstab
注意 swapoff -a 只是临时生效,重启后会重新挂载 swap,所以必须修改 /etc/fstab,把 swap 那行注释掉。我遇到过不少同学只执行 swapoff,重启后 kubeadm init 直接报错,查半天才发现 swap 又回来了。
最后是 SELinux,这个机制本身是好事,但在 K8s 环境里会让容器挂载目录、访问设备时出现 permission denied,排查起来特别费劲。测试和学习环境我会直接把 SELinux 关掉:
bash复制setenforce 0
sed -i 's/^SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config
setenforce 0 临时关闭,改配置文件是为了重启后依然关闭。这三步做完,系统的兼容性坑就少了大半。
2.2 内核参数与模块
K8s 的 Pod 网络通信依赖 iptables 处理桥接流量,而 CentOS 7 默认可能没有开启 bridge-nf 相关参数。如果你不提前配置,Flannel 装上后节点之间互相 ping 不通,Pod 通信会时断时续。我习惯准备一个专门的 sysctl 配置:
bash复制cat > /etc/sysctl.d/k8s.conf <<EOF
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
vm.swappiness = 0
EOF
sysctl --system
这里有两个点要解释。第一,net.bridge.bridge-nf-call-iptables 必须为 1,这样经过 Linux 网桥的流量才会被 iptables 规则处理,K8s 的 Service 转发和网络策略才能生效。第二,vm.swappiness = 0 是进一步告诉内核尽量别用 swap,和前面关 swap 是同一套思路。
同时还要加载一个模块:
bash复制modprobe br_netfilter
有些最小化安装的系统连 bridge 模块都没有,可以检查一下 lsmod | grep br_netfilter,没有输出就执行上面的命令。如果不做这步,即使 sysctl 配置写好了,也可能因为内核模块缺失而出现参数不生效的情况。
2.3 主机名与 hosts 解析
集群内的节点之间需要通过主机名互相访问,至少控制节点的 API Server 证书里会用到主机名,所以主机名必须稳定,而且所有节点都要能通过 /etc/hosts 解析到其他节点。我用的是三台机器做演示,规划如下:
| 节点角色 | 主机名 | IP |
|---|---|---|
| 控制节点 | master01 | 192.168.1.10 |
| 工作节点 | node01 | 192.168.1.11 |
| 工作节点 | node02 | 192.168.1.12 |
每台机器执行:
bash复制hostnamectl set-hostname master01 # 对应节点改成自己的名字
然后编辑 /etc/hosts,把下面几行加进去:
code复制192.168.1.10 master01
192.168.1.11 node01
192.168.1.12 node02
为什么要用主机名而不是直接写 IP?因为控制平面在生成证书时会把 --control-plane-endpoint 写进证书的 SAN(Subject Alternative Name),如果你后续要做高可用或者负载均衡,主机名比 IP 灵活得多。另外要注意,云服务器如果用的是 DHCP 或者云平台自带的网络配置,重启后 IP 可能变化,这种情况建议在服务器控制台绑定一个固定内网 IP 或者弹性 IP,不然 hosts 解析很快就会失效。
3. 容器运行时:Docker 安装与配置
3.1 安装 Docker CE
这里不打算用 CentOS 7 自带的 docker 包,那东西太旧了。直接用阿里云的 docker-ce 源安装,速度稳定,版本也新:
bash复制yum install -y yum-utils
yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
yum install -y docker-ce docker-ce-cli containerd.io
安装完之后,立刻把 Docker 设为开机自启并启动:
bash复制systemctl enable --now docker
containerd.io 会被当作依赖一起装上,这是正常的,Docker 底层就是靠它来管理容器生命周期。就算后续你想切到 containerd 路线,也不需要额外再装一遍。
3.2 cgroupdriver 与日志配置
Docker 默认的 cgroup 驱动是 cgroupfs,而 kubelet 在大多数场景下用的是 systemd 驱动,如果两者不一致,kubelet 启动时会报错,节点状态也一直是 NotReady。所以安装完 Docker 后,第一件事就是改 daemon.json,把 cgroupdriver 统一成 systemd:
bash复制mkdir -p /etc/docker
cat > /etc/docker/daemon.json <<EOF
{
"exec-opts": ["native.cgroupdriver=systemd"],
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "3"
},
"registry-mirrors": ["https://换成你自己的加速器地址.mirror.aliyuncs.com"]
}
EOF
max-size 和 max-file 这两项是很多人忽略的。默认情况下 Docker 会把每个容器的完整 stdout 日志写到宿主机,如果某个容器疯狂打日志,磁盘很快就会被写满,集群莫名其妙就挂掉。限制单文件 100M、保留 3 个文件,是我在生产环境里固定使用的方案。
镜像加速器这里,不同云厂商的地址不一样,阿里云会在容器镜像服务控制台给每个账号生成专属加速地址。如果你用的是其他云,也可以填对应的镜像加速地址。这一步不做,后面拉 k8s 组件镜像时容易卡住。配置完成后重启 Docker:
bash复制systemctl daemon-reload
systemctl restart docker
3.3 快速验证 Docker 是否合格
启动完成后,用 docker info 检查两处关键信息:一是 Cgroup Driver 是否显示 systemd,二是 Registry Mirrors 是否已经生效。如果 Cgroup Driver 不是 systemd,后面 kubeadm init 时 kubelet 的状态会非常折磨人,一定要在这里就发现并解决。
再顺手跑一个测试容器:
bash复制docker run hello-world
这一步主要是确认 Docker 能正常拉取和运行容器。如果 hello-world 都跑不起来,后面 K8s 的镜像肯定也拉不动,先把网络和仓库配置问题解决再继续。
4. 安装 kubeadm、kubelet、kubectl
4.1 添加 Kubernetes 的 yum 源
K8s 官方源部署在 Google 的服务器上,国内访问不稳定,我每次都是直接换成阿里云的镜像源。新建 /etc/yum.repos.d/kubernetes.repo,写入以下内容:
bash复制cat > /etc/yum.repos.d/kubernetes.repo <<EOF
[kubernetes]
name=Kubernetes
baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64/
enabled=1
gpgcheck=0
repo_gpgcheck=0
EOF
这里我直接设置了 gpgcheck=0,因为 CentOS 7 下开启 GPG 检查后,偶尔会碰到密钥导入不完整导致 yum 拒绝安装的情况。如果你是严格安全环境,可以保留 gpgcheck=1 并手动导入官方密钥,但我个人认为,测试环境里 gpgcheck=0 能省掉大量无意义的排障时间。
4.2 安装并锁定版本
kubeadm、kubelet、kubectl 这三个组件的版本必须保持一致,否则会出现 API 版本不匹配,初始化时各种报错。安装指定版本:
bash复制yum install -y kubelet-1.23.17 kubeadm-1.23.17 kubectl-1.23.17
安装完成后,立刻把 kubelet 设为开机自启,但是先不要启动它。kubelet 要等 kubeadm init 完成了才知道自己该干什么,提前启动会在系统里留下一个没配置好的状态,偶尔会干扰后续初始化:
bash复制systemctl enable kubelet
这时候可以用 kubeadm version 和 kubectl version --client 确认版本。如果你打算用更新的 K8s 版本,比如 1.28 或 1.29,那就要注意:1.24 之后 kubelet 默认对接 containerd 而不是 Docker,需要在节点上额外配置 containerd 的 systemd cgroup 和 sandbox 镜像地址,安装命令本身差别不大,但容器运行时的配置步骤不能照抄 Docker 方案。
4.3 先解决镜像来源问题
kubeadm init 的时候要拉一堆控制面镜像:kube-apiserver、kube-controller-manager、kube-scheduler、kube-proxy、pause、etcd、coredns。这些镜像的默认仓库是 k8s.gcr.io,国内直接拉非常吃力。解决办法是用 kubeadm 的 --image-repository 参数,把镜像仓库统一替换为阿里云的 registry.aliyuncs.com/google_containers。
系统初始化之前,可以先预览一下需要哪些镜像:
bash复制kubeadm config images list --kubernetes-version=v1.23.17
然后手动拉取一遍确认网络通不通:
bash复制kubeadm config images pull --image-repository=registry.aliyuncs.com/google_containers --kubernetes-version=v1.23.17
这个操作很有价值。如果这一步能顺利拉完,说明你的节点到镜像仓库的网络没问题,后续 init 会非常顺滑。如果这里报错,优先排查 Docker 的 registry-mirrors 是否配置正确、防火墙是否真的关了。
5. 用 kubeadm 初始化集群
5.1 init 参数怎么填
在控制节点 master01 上执行初始化命令。这里有几个参数需要按你的实际环境调整:
bash复制kubeadm init \
--kubernetes-version=v1.23.17 \
--control-plane-endpoint=master01 \
--apiserver-advertise-address=192.168.1.10 \
--pod-network-cidr=10.244.0.0/16 \
--image-repository=registry.aliyuncs.com/google_containers
逐个说下含义。--kubernetes-version 要和你安装的 kubeadm 版本一致。--control-plane-endpoint 填写控制节点的主机名,它会写进 API Server 证书的 SAN,后面工作节点 join 时也用这个地址连控制平面,我这里填了 master01,因为 /etc/hosts 里已经做了解析。--apiserver-advertise-address 是 API Server 对外广播的 IP,必须填控制节点的内网 IP。--pod-network-cidr 要和 CNI 插件匹配,Flannel 就是 10.244.0.0/16。--image-repository 指定替代 k8s.gcr.io 的仓库。
初始化过程一般持续 3 到 5 分钟,看到类似 Your Kubernetes control-plane has initialized successfully 的输出就说明成功了。这个过程中千万别去重启 Docker 或 kubelet,让它安安静静跑完。
5.2 初始化后的目录与 kubectl 配置
init 成功后会生成一个 /etc/kubernetes 目录,里面最重要的文件是 admin.conf,这就是集群的管理员 kubeconfig。普通用户想用 kubectl 操作集群,需要把这个配置复制到自己的家目录:
bash复制mkdir -p $HOME/.kube
cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
chown $(id -u):$(id -g) $HOME/.kube/config
如果不用 root 用户操作,这步必须做,否则 kubectl 找不到集群配置,提示 The connection to the server localhost:8080 was refused。配置完以后验证一下:
bash复制kubectl get nodes
此刻节点状态应该是 NotReady,因为网络插件还没有安装。控制面组件是否正常,可以用 kubectl get pods -n kube-system 观察,除了网络插件相关的 Pod,其他组件应该都处于 Running 或 Completed 状态。
5.3 网络插件 Flannel 安装
到了这里,集群的骨架已经起来了,但节点之间还不能正常通信,因为缺少 CNI 网络插件。我选 Flannel,是因为它在 CentOS 7 这种环境中足够简单,部署一条命令就能搞定:
bash复制kubectl apply -f https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml
如果你所在的网络访问 GitHub 不稳定,可以先在自己电脑上下载这个 yaml 文件,再通过 scp 传到服务器上,最后用 kubectl apply -f kube-flannel.yml 本地应用。Flannel 的 Pod 会运行在 kube-system 命名空间下,等待镜像拉取和启动完成后,再查看节点状态:
bash复制kubectl get nodes
kubectl get pods -n kube-system
当节点状态变成 Ready,并且 kube-system 下所有 Pod 都处于 Running,说明网络已经通了。这里我想提醒一句:Flannel 的 Pod 网段必须和 kubeadm init 时指定的 --pod-network-cidr 一致,如果 yaml 里默认用的是其他网段,节点状态会一直卡在 NotReady,这个我后面排障部分再展开。
5.4 单节点怎么继续玩
如果你只有一台机器,控制节点默认是不参与业务负载调度的,因为它带了一个污点(Taint),普通 Pod 不会调度上去。为了让单机也跑业务,可以把这个污点去掉:
bash复制kubectl taint nodes --all node-role.kubernetes.io/master-
注意命令末尾有一个短横线,表示移除该污点。在 1.24 及以后的版本里,污点键名变成了 node-role.kubernetes.io/control-plane,所以命令也要对应调整。如果你只想让某个特定 Pod 跑到控制节点,也可以不删污点,而是在 Pod 的 yaml 里加容忍(tolerations),但测试环境直接删掉最方便。
6. 工作节点加入集群
6.1 join 前需要做好的事
工作节点不需要执行 kubeadm init,但系统初始化、Docker 安装和配置、kubelet 安装这几步一个都不能少。换句话说,第 2 节、第 3 节、第 4.1 和 4.2 的步骤要在每个工作节点上重新执行一遍。不要图省事只装 kubeadm 就去 join,kubelet 没有装好、Docker cgroupdriver 配置不一致,join 成功率会大打折扣。
工作节点上可以不配 kubectl,因为 kubectl 只是管理客户端,工作节点的日常运行不需要它。当然,装上也不影响,纯看个人习惯。我自己一般只在控制节点上配 kubectl,工作节点保持最小化。
6.2 执行 join 并验证
kubeadm init 成功后的输出里,会有一段 join 命令,格式大概是:
bash复制kubeadm join 192.168.1.10:6443 --token xxxxxx.xxxxxxxxxxx \
--discovery-token-ca-cert-hash sha256:xxxxxxxxxxxxxxxxxxxx
把这段命令复制到工作节点上执行,输出显示 This node has joined the cluster 就表示加入成功。如果当时忘了保存输出,也可以在控制节点上通过以下命令重新生成 join 信息:
bash复制kubeadm token create --print-join-command
回到控制节点执行:
bash复制kubectl get nodes
这时候应该能看到两个或三个节点,状态会从 NotReady 慢慢变成 Ready。第一次 join 后节点状态变成 Ready 的速度取决于 CNI 镜像拉取速度,一般一到两分钟正常。如果超过五分钟还是 NotReady,就要按后面的排障思路去查了。
7. 部署测试应用,验证集群真的能用
7.1 用 Nginx 跑通对外访问
节点全部 Ready 不代表集群没问题,我的习惯是立刻跑一个测试应用,把网络、DNS、调度全链路验证一遍。用命令行创建 Nginx 部署:
bash复制kubectl create deployment nginx --image=nginx
kubectl expose deployment nginx --port=80 --type=NodePort
kubectl get pods -o wide
kubectl get svc nginx
kubectl get svc 会显示 NodePort 端口,比如 80:30245/TCP,这个 30245 就是对外暴露的端口。然后从任意节点 IP 加上这个端口访问:
bash复制curl http://192.168.1.10:30245
如果返回 Nginx 的欢迎页面,说明整个链路已经通了:Pod 调度正常、Flannel 网络正常、Service 转发正常、NodePort 映射正常。这比只看节点状态更让人放心。如果卡在这一步,优先用 kubectl logs 和 kubectl describe pod 看具体原因,大部分情况是镜像拉取慢或 DNS 解析出问题。
7.2 装个 Dashboard 当管理面板
K8s 命令行虽然很强大,但对新手来说,有个可视化界面会直观得多。Dashboard 的安装文件在 GitHub 上,我常用 2.7.0 版本:
bash复制kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.7.0/aio/deploy/recommended.yaml
等 Pod 跑起来后,需要创建一个管理员账号并绑定集群管理员权限。简单做法是创建 admin-user ServiceAccount,再通过 ClusterRoleBinding 绑定 cluster-admin:
yaml复制apiVersion: v1
kind: ServiceAccount
metadata:
name: admin-user
namespace: kubernetes-dashboard
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: admin-user
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: admin-user
namespace: kubernetes-dashboard
保存为 dashboard-admin.yaml,执行 kubectl apply -f dashboard-admin.yaml。获取登录 token:
bash复制kubectl -n kubernetes-dashboard create token admin-user
用 kubectl -n kubernetes-dashboard get svc 找到 Dashboard 的 Service,默认是 ClusterIP,想从外部访问可以用 kubectl port-forward -n kubernetes-dashboard service/kubernetes-dashboard 8080:443,然后浏览器打开 https://localhost:8080,用 token 登录。注意 Dashboard 用的自签名证书,浏览器会提示不安全,直接选择继续访问即可。
8. 安装过程中最容易翻车的几个点
8.1 kubelet 一直没就绪
这个问题出现频率最高,典型症状是 kubeadm init 卡在 [wait-control-plane],或者执行 kubectl get nodes 时节点一直是 NotReady。第一件事是看 kubelet 日志:
bash复制journalctl -u kubelet -f
常见原因无非三种。第一,swap 没关干净,重启后 /etc/fstab 里的 swap 又生效了。第二,Docker 的 cgroupdriver 不是 systemd,kubelet 与运行时驱动不一致。第三,镜像拉取失败,控制面 Pod 没起来。这三种情况在日志里都会留下明确线索,比如 Failed to start ContainerManager 或 failed to pull image。按日志里提示的方向去处理,通常比盲目重启快得多。
8.2 CoreDNS 起来又崩
CoreDNS 崩溃或 CrashLoopBackOff,大概率是网络插件没有正常工作,而不是 CoreDNS 本身的问题。这时候先看 coredns 的日志:
bash复制kubectl logs -n kube-system -l k8s-app=kube-dns
如果日志里频繁出现超时、连接失败,就回到 Flannel 的方向去排查。确认一下 Flannel 的 Pod 是否 Running,以及 kube-proxy 是否正常。还有一个容易忽略的点:kubeadm init 指定的 --pod-network-cidr 和 Flannel 配置里的网段是否一致,不一致的话 Flannel 无法正确规划路由,CoreDNS 也起不来。
8.3 镜像拉不下来
ErrImagePull 或 ImagePullBackOff 是部署应用时最常见的错误。先手动 docker pull 一下对应的镜像,看看到底是因为网络慢、镜像地址不存在,还是私有仓库没有认证。如果是 Docker Hub 镜像拉取慢,就确保 daemon.json 里的 registry-mirrors 配置生效了;如果是 k8s.gcr.io 相关镜像,就检查 kubeadm init 时有没有用 --image-repository 指定替代仓库。如果你后来换成 containerd 方案,不能再用 docker pull,得用 crictl pull 验证镜像,命令路径不一样,很容易踩坑。
8.4 Node 反复 NotReady 或重启失效
节点偶尔 Ready、过一会儿又 NotReady,这个问题最磨人。排查顺序我一般固定这样走:先看 kubectl describe node <name> 里的 Conditions 和 Events,是 KubeletReady 变了还是 NetworkUnavailable 一直存在;再看 Flannel 和 kube-proxy 的 Pod 状态;最后看 kubelet 日志。如果是重启后集群异常,重点排查两件事:Docker 和 kubelet 有没有设置开机自启,/etc/fstab 里的 swap 有没有永久注释掉。我就踩过这种坑,Docker 开了自启但 kubelet 没开,重启后控制面组件全在,但节点加入不回来。
整个流程走下来,你会发现 CentOS 7 装 K8s 并没有想象中那么神秘,每一步都是环环相扣的。我自己的固定习惯是:先花十分钟把系统初始化干净,再按版本组合去装运行时和 kubelet,最后才做 init。很多人的问题就出在急着 init,前面任何一步有残留,后面就是一连串连锁反应。如果你也是资源有限、又想快速体验完整集群,我特别建议先在虚拟机里把这套流程完整走一遍,记下自己的错误再重来,印象会深很多。
