Ubuntu 22.04 上搭建一主一从 K8s 集群:从零到 Dashboard 的完整指南
1. 为什么要在 Ubuntu 22.04 上折腾一套一主一从的 K8s
先把话说在前头:这套一主一从的集群不是给生产环境用的,它的定位非常明确——学习、验证、开发联调、CI 前置环境。我在实际工作中带过不少刚接触容器编排的同学,发现一个普遍现象:文档看了一堆,概念背得滚瓜烂熟,但真让他从零把集群拉起来,卡在镜像拉取、网络插件、证书、kubelet 起不来这些地方能耗掉一整天。所以我一直建议,入门阶段别贪多,先用两台机器把一主一从跑通,把 dashboard 挂上去,能登录、能看到节点、能部署一个带 Service 的示例应用,这一圈走完,你对 K8s 的整体骨架就有了肌肉记忆。
Ubuntu 22.04 是目前长期支持版本里比较稳的一个选择,内核 5.15 起步,对 cgroups v2、overlayfs、iptables-nft 这些容器运行时依赖的东西支持得比较完整,社区资料也多。一主一从的意思是:一台机器当控制平面(control plane),跑 API Server、调度器、控制器管理器、etcd;另一台当工作节点(worker),跑 kubelet、容器运行时和实际业务 Pod。这个规模小到不能再小,但 K8s 的核心组件一个不少,学习价值反而最高——因为你能清楚看到每个组件在哪台机器上、干什么活。
这套东西能解决什么问题?最直接的是给你一个可反复推倒重来的实验环境。云厂商托管的 K8s 服务确实省事,但你看不到底层,出了问题只能提工单;而自建一主一从,从系统内核参数到 CNI 插件到 dashboard 的 token 认证,每一层你都能摸到。适合谁来参考?我把它分成三类:一是刚学 K8s、想搞懂组件关系的开发;二是需要本地联调微服务、又不想依赖外部集群的团队;三是准备考相关认证、需要动手环境的人。下面我按实际操作的顺序,把每一步的意图、参数和踩过的坑都摊开讲。
2. 整体设计与方案选型:为什么这么搭
2.1 组件选型背后的取舍逻辑
一套能跑起来的 K8s 集群,绕不开几个关键决策:容器运行时选什么、CNI 网络插件选什么、安装方式用 kubeadm 还是二进制、dashboard 怎么暴露。我先把结论摆出来,再解释为什么。
| 决策项 | 我的选择 | 备选方案 | 选择理由 |
|---|---|---|---|
| 容器运行时 | containerd | Docker Engine + cri-dockerd | containerd 是 CRI 原生实现,链路短、依赖少,Docker 那套中间层在新版本里反而增加故障点 |
| 安装工具 | kubeadm | 二进制手工部署 / 第三方脚本 | kubeadm 是官方维护的引导工具,证书、静态 Pod、引导令牌全帮你管好,学习成本可控 |
| CNI 插件 | Calico | Flannel / Cilium | Calico 同时提供网络与网络策略,排查工具成熟,一主一从规模下资源占用可接受 |
| Dashboard 暴露方式 | NodePort | Ingress / kubectl proxy | 实验环境用 NodePort 最直接,少一层依赖,浏览器直接访问 |
| 系统 | Ubuntu 22.04 LTS | 20.04 / 24.04 | 22.04 生态成熟,cgroups v2 默认开启,社区踩坑记录最全 |
这里重点说两个容易被忽略的取舍。第一是容器运行时。很多人习惯性装 Docker,然后发现 K8s 1.24 之后不再内置 dockershim,还得额外装 cri-dockerd 做适配,多一层就多一个出错点。containerd 本身就是 CRI 标准实现,kubelet 直接跟它对话,配置里SystemdCgroup = true一开,和 systemd 的 cgroup 管理对齐,稳定性明显更好。第二是安装方式。二进制手工部署确实能让你彻底理解每个组件,但对新手来说,光是证书签发和 systemd 单元文件就能劝退。kubeadm 把这些脏活包了,你只需要理解它做了什么,而不是亲手做一遍——入门阶段这个平衡点很关键。
2.2 一主一从的拓扑与网络规划
拓扑很简单,但网络规划必须提前定死,否则后面改起来很痛苦。我习惯用下面这套规划,你可以直接抄:
- 主节点:内网地址假设为
192.168.56.10,主机名k8s-master - 从节点:内网地址假设为
192.168.56.11,主机名k8s-node1 - Pod 网段:
10.244.0.0/16(Calico 默认用这个,保持一致省事) - Service 网段:
10.96.0.0/12(kubeadm 默认值,一般不动) - API Server 端口:
6443
为什么 Pod 网段和 Service 网段不能重叠?因为 kube-proxy 会在节点上写大量 iptables 规则做 Service 到 Pod 的转发,两个网段一旦交叉,路由就会打架,表现为"Pod 能起但 Service 访问不通",这种问题排查起来极其费劲。所以规划阶段就把10.244.0.0/16和10.96.0.0/12分开,后面 Calico 的CALICO_IPV4POOL_CIDR也要跟pod-network-cidr严格一致,这是硬约束。
另外主机名必须唯一且能互相解析。我见过有人两台机器都叫ubuntu,结果 kubelet 注册节点时直接冲突。做法很简单,hostnamectl set-hostname改好,然后在两台机器的/etc/hosts里互相加上对方的 IP 和主机名,别依赖外部 DNS,实验环境自己闭环最稳。
2.3 资源与前置条件的现实考量
一主一从对资源的要求其实不高,但有几个下限得守住。主节点我建议至少 2 核 4G,从节点 2 核 2G 起步。为什么主节点要 4G?因为控制平面要跑 etcd、API Server、调度器、控制器管理器,再加上 Calico 的若干 Pod,2G 内存在启动阶段就容易触发 OOM,表现为kube-apiserver反复重启,日志里全是内存分配失败。磁盘至少 20G,容器镜像和 etcd 数据都吃空间。
还有一个前置条件是关闭 swap。K8s 的调度器假设节点内存是可预测的,swap 会让内存压力下的行为变得不可控,所以 kubelet 默认拒绝在开启 swap 的节点上启动。Ubuntu 22.04 默认可能开了 swap 文件,得手动关掉并注释掉/etc/fstab里的相关行,否则重启后又回来了。这个细节后面实操部分会具体写。
3. 系统层准备:那些不起眼但致命的细节
3.1 主机名、hosts 与时间同步
系统层准备看着琐碎,但十有八九的"集群起不来"都栽在这一步。我按顺序来。先在两台机器上分别设置主机名:
# 主节点执行 hostnamectl set-hostname k8s-master # 从节点执行 hostnamectl set-hostname k8s-node1然后编辑两台机器的/etc/hosts,把对方的解析加进去:
192.168.56.10 k8s-master 192.168.56.11 k8s-node1为什么要手动写 hosts 而不是靠 DNS?因为 kubeadm 初始化时会用主机名去解析 API Server 地址,实验环境往往没有内网 DNS,靠 hosts 是最可靠的闭环方式。写完用ping k8s-node1从主节点验证一下,能通再往下走。
时间同步也别跳过。K8s 的证书有效期、token 过期判断、etcd 的租约机制都依赖系统时间一致。两台机器时间差太大,会出现"token 明明没过期却认证失败"的诡异现象。Ubuntu 22.04 自带systemd-timesyncd,一般默认就开着,用timedatectl看一眼System clock synchronized: yes就行。如果没同步,timedatectl set-ntp true打开即可。
3.2 关闭 swap 与内核模块加载
关闭 swap 是硬性要求,操作分两步,缺一不可:
# 临时关闭 swapoff -a # 永久关闭:注释掉 /etc/fstab 里 swap 那一行 sed -ri 's/.*swap.*/#&/' /etc/fstab只做swapoff -a的话,机器一重启 swap 又回来了,kubelet 照样起不来。所以fstab那一步必须做。验证方式:free -h看 Swap 那一行是不是全 0,swapon --show输出为空就对了。
接着加载内核模块。K8s 的网络转发和流量过滤依赖overlay和br_netfilter两个模块:
cat <<EOF | tee /etc/modules-load.d/k8s.conf overlay br_netfilter EOF modprobe overlay modprobe br_netfilteroverlay是容器分层文件系统的基础,br_netfilter让 iptables 能处理桥接流量——没有它,Pod 之间的跨节点通信和 Service 转发都会出问题。写进/etc/modules-load.d/是为了开机自动加载,别只modprobe一次就完事。
3.3 内核参数调优与验证
内核参数这块,核心是让 iptables 能"看见"桥接的流量,并开启 IP 转发:
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逐条解释一下意图。bridge-nf-call-iptables=1让经过网桥的流量也走一遍 iptables 规则链,这是 kube-proxy 实现 Service 负载均衡的前提;ip_forward=1开启 IPv4 转发,节点要当路由器给 Pod 转发包,不开这个跨节点通信直接断。改完用sysctl net.ipv4.ip_forward确认输出是 1。
注意:
bridge-nf-call-iptables这个参数依赖br_netfilter模块已加载,如果sysctl --system报 "cannot stat /proc/sys/net/bridge/..." 之类的错,说明模块没加载成功,回头检查上一步。
到这里系统层就准备完了。我习惯在两台机器上都做一遍,别偷懒只做主节点——从节点同样需要这些配置,漏了的话加入集群时会卡在 kubelet 启动阶段。
4. 容器运行时与 kubeadm 安装实操
4.1 安装 containerd 并配置 SystemdCgroup
先装依赖和 containerd。Ubuntu 22.04 的官方源里就有 containerd,但版本可能偏旧,我一般用 Docker 官方源里的containerd.io,版本新且稳定:
apt-get update apt-get install -y ca-certificates curl gnupg lsb-release install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \ gpg --dearmor -o /etc/apt/keyrings/docker.gpg chmod a+r /etc/apt/keyrings/docker.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | \ tee /etc/apt/sources.list.d/docker.list > /dev/null apt-get update apt-get install -y containerd.io装完 containerd 后,必须生成默认配置并改一处关键参数:
mkdir -p /etc/containerd containerd config default | tee /etc/containerd/config.toml # 把 SystemdCgroup 从 false 改成 true sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml systemctl restart containerd systemctl enable containerd为什么SystemdCgroup一定要改成true?因为 Ubuntu 22.04 默认用 systemd 管理 cgroup,如果 containerd 还用 cgroupfs 驱动,两套 cgroup 管理器会打架,表现为节点资源统计不准、Pod 被莫名杀掉。这个参数是 containerd 和 kubelet 协同工作的关键,改完重启 containerd 生效。验证一下:containerd config dump | grep SystemdCgroup输出true就对了。
4.2 配置 kubelet 的 cgroup 驱动对齐
containerd 改好了,kubelet 这边也得对齐。kubeadm 的配置里要显式指定cgroupDriver: systemd,否则 kubelet 默认可能用 cgroupfs,又回到刚才那个打架的问题。这个配置在初始化时通过配置文件传入,后面 4.4 节会给出完整配置。
另外还要处理一个 Ubuntu 22.04 特有的坑:/etc/default/kubelet里如果残留旧配置可能干扰启动,一般全新安装不会有,但如果你之前装过又卸了,建议apt-get purge kubelet kubeadm kubectl清干净再装。
4.3 安装 kubeadm、kubelet、kubectl
这三个组件版本必须一致,我一般锁定一个稳定的小版本,比如 1.28.x。先加源:
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.28/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://pkgs.k8s.io/core:/stable:/v1.28/deb/ /' | \ tee /etc/apt/sources.list.d/kubernetes.list apt-get update apt-get install -y kubelet kubeadm kubectl apt-mark hold kubelet kubeadm kubectlapt-mark hold这一步别省。不加 hold 的话,某次apt upgrade可能把 kubelet 升到新版本,而 kubeadm 还是旧的,版本错配会导致节点注册失败。锁住版本,升级时手动来,可控得多。
装完用kubeadm version、kubelet --version、kubectl version --client三个命令确认版本一致。这一步两台机器都要做,从节点同样需要这三个组件。
4.4 主节点初始化:配置文件比命令行参数更靠谱
kubeadm 初始化有两种方式:一长串命令行参数,或者一个 YAML 配置文件。我强烈推荐后者,因为参数可读、可版本管理、出错好回看。配置文件kubeadm-config.yaml内容如下:
apiVersion: kubeadm.k8s.io/v1beta3 kind: InitConfiguration localAPIEndpoint: advertiseAddress: 192.168.56.10 bindPort: 6443 nodeRegistration: criSocket: unix:///run/containerd/containerd.sock name: k8s-master --- apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: v1.28.0 controlPlaneEndpoint: "192.168.56.10:6443" networking: podSubnet: "10.244.0.0/16" serviceSubnet: "10.96.0.0/12" --- apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration cgroupDriver: systemd几个关键点解释一下。advertiseAddress填主节点内网 IP,API Server 会在这个地址上监听;criSocket指向 containerd 的 socket,告诉 kubelet 用哪个运行时;podSubnet必须和后面 Calico 的网段一致;最后那段KubeletConfiguration里的cgroupDriver: systemd就是前面说的对齐。三份配置用---分隔,kubeadm 会分别解析。
执行初始化:
kubeadm init --config kubeadm-config.yaml --upload-certs--upload-certs是把证书加密上传到集群的 Secret 里,方便后续加节点时自动下发。初始化过程会拉取控制平面镜像,网络慢的话会卡一会儿,耐心等。成功后会输出一段kubeadm join命令,务必复制保存,从节点加入就靠它。
初始化完成后,配置 kubectl 的凭据:
mkdir -p $HOME/.kube cp -i /etc/kubernetes/admin.conf $HOME/.kube/config chown $(id -u):$(id -g) $HOME/.kube/config然后kubectl get nodes应该能看到主节点,状态是NotReady——这是正常的,因为还没装 CNI 插件。
5. 网络插件、从节点加入与 Dashboard 部署
5.1 安装 Calico 并验证网络就绪
主节点NotReady的根因就是缺 CNI。Calico 的安装用官方 manifest 最省事,但要注意版本和网段匹配:
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.26.1/manifests/calico.yaml如果前面podSubnet用的不是10.244.0.0/16,得先下载 manifest 改CALICO_IPV4POOL_CIDR再 apply。用默认网段的话直接 apply 即可。装完等一会儿,用下面命令观察:
kubectl get pods -n kube-system -w看到calico-node-xxxx和calico-kube-controllers-xxxx都Running后,再kubectl get nodes,主节点应该变成Ready。如果一直NotReady,八成是 Calico Pod 起不来,用kubectl describe pod看事件,常见原因是镜像拉取失败或网段配置冲突。
5.2 从节点加入集群与常见卡点
从节点上执行主节点初始化时输出的 join 命令,形如:
kubeadm join 192.168.56.10:6443 --token abcdef.0123456789abcdef \ --discovery-token-ca-cert-hash sha256:xxxxtoken 默认 24 小时过期,过期了在主节点用kubeadm token create --print-join-command重新生成一条。加入过程如果卡在[preflight] Running pre-flight checks,多半是前面系统层准备没做全——swap 没关、内核模块没加载、hosts 解析不通,逐个排查。
加入成功后回主节点kubectl get nodes,两台都Ready就说明集群骨架成了。这时候可以跑个验证:kubectl run nginx --image=nginx起个 Pod,kubectl get pods -o wide看它调度到哪台、拿到什么 IP,再kubectl exec进去curl一下另一个 Pod,跨节点通信通了,网络层就算过关。
5.3 Dashboard 部署与 token 登录
Dashboard 的 manifest 官方维护在 kubernetes/dashboard 仓库,我一般用 v2.7 这个稳定版本:
kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.7.0/aio/deploy/recommended.yaml默认它创建的是 ClusterIP 类型的 Service,集群外访问不了。实验环境最直接的做法是改成 NodePort:
kubectl -n kubernetes-dashboard patch svc kubernetes-dashboard \ -p '{"spec":{"type":"NodePort"}}' kubectl -n kubernetes-dashboard get svc kubernetes-dashboard拿到映射的 NodePort(比如 30000 多),浏览器访问https://192.168.56.10:<NodePort>就能打开登录页。注意是 https,证书是自签的,浏览器会警告,手动信任继续即可。
登录需要 token。先创建一个有集群管理员权限的 ServiceAccount:
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-dashboardapply 之后生成 token:
kubectl -n kubernetes-dashboard create token admin-user把输出的长串 token 粘到登录页,就能进 dashboard 了。这里有个安全提醒:cluster-admin权限极大,实验环境图省事可以这么干,但别把这套配置原样搬到任何对外暴露的环境,token 泄露等于集群裸奔。
6. 常见问题排查与实操避坑清单
6.1 高频故障速查表
这套流程我走过很多遍,下面这些坑几乎每次带新人都会遇到,整理成表方便对照:
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 主节点一直 NotReady | CNI 未安装或 Calico Pod 异常 | kubectl get pods -n kube-system看 calico 状态,describe看事件 |
| kubelet 启动失败 | swap 未关 / cgroup 驱动不一致 | journalctl -u kubelet -n 50看日志,确认 swap 和 SystemdCgroup |
| 从节点 join 卡住 | hosts 解析不通 / 防火墙拦截 6443 | 主节点ping从节点,检查安全组或 ufw 规则 |
| Pod 起不来,镜像拉取失败 | 网络问题或镜像仓库不可达 | kubectl describe pod看 Events,必要时配置镜像加速 |
| Service 访问不通 | Pod 网段与 Service 网段重叠 | 检查 kubeadm 配置和 Calico 的 CIDR 是否一致 |
| Dashboard 登录 401 | token 过期或权限不足 | 重新create token,确认 ClusterRoleBinding 生效 |
| 节点时间漂移导致认证失败 | 时间未同步 | timedatectl确认 NTP 同步状态 |
6.2 几个文档里不会写的实操心得
第一个心得:初始化失败后别急着kubeadm reset重来。先看/var/log/和journalctl -u kubelet,很多问题是一次性的,比如镜像拉取超时,重试就好。真要 reset,记得kubeadm reset -f之后手动清理$HOME/.kube、/etc/cni/net.d、/var/lib/etcd,否则残留配置会干扰下一次初始化。
第二个心得:Calico 的镜像拉取是最大的不确定性。manifest 里引用了好几个镜像,网络不好时容易卡在ImagePullBackOff。我的做法是提前kubectl describe看它要哪些镜像,用crictl pull在节点上先拉一遍,拉通了再 apply,能省不少等待。
第三个心得:dashboard 的 NodePort 别用默认随机分配。默认可能分到 30000 以上的随机端口,每次重建都变,记不住。可以在 patch 时指定nodePort,比如固定 30443,方便书签。但要注意端口范围 30000-32767,别填超了。
第四个心得:一主一从的 etcd 是单点。主节点挂了整个集群就不可用,这不是 bug 是架构决定的。所以这套环境只适合学习,别在上面跑任何你丢不起的数据。想练高可用,至少三台控制平面,那是另一个话题了。
6.3 环境验证的完整闭环
集群搭完别急着庆祝,跑一遍完整验证才算数。我习惯按这个顺序:先kubectl get nodes确认两台 Ready;再kubectl get pods -A确认所有系统 Pod 都 Running;然后部署一个带 Deployment 和 Service 的示例,用kubectl expose暴露成 NodePort,从浏览器访问验证 Service 转发链路;最后进 dashboard 看一眼节点和 Pod 列表是否正常展示。这一圈走完,从网络到认证到 UI 全链路都验证过了,后面拿它做实验心里才有底。
我个人在实际操作中的体会是,这套一主一从的价值不在于"搭起来"那一刻,而在于你把它拆了重装三五遍之后,对每个组件的依赖关系形成的那种直觉。第一次可能要折腾大半天,第三次基本半小时能搞定,到那时候你再看 K8s 的官方文档,很多之前看不懂的段落会突然变得清晰——因为你亲手摸过每一层。