上个月我在 VMware 里准备一套学习用的 Kubernetes 实验环境,顺手把镜像仓库也一起规划进去了。当时选型没怎么纠结,直接定成 openEuler 22.03 LTS SP4 作为宿主机系统,集群用 kubeadm 方式部署 Kubernetes,镜像仓库用 Harbor。整套环境跑下来,从系统安装到集群初始化、Harbor 上线、再到工作负载从私有仓库拉镜像验证,一共花了两天时间。这篇文章把整个部署过程按我实际操作的顺序整理出来,包括版本选型的理由、VMware 虚拟机配置、openEuler 系统初始化、containerd 的安装和 CRI 调用原理、kubeadm 初始化集群、Calico 网络插件、Harbor 的 Helm 部署,以及集群从 Harbor 拉取镜像的完整链路验证。适合手里有测试机想完整复现一遍的运维工程师,也适合准备考 CKA 但缺一套干净环境练手的人。
1. 部署前准备:版本选型、VMware 虚拟机配置与 openEuler 系统初始化
1.1 为什么选 openEuler 22.03 LTS SP4 + Kubernetes 1.28 + Harbor 2.8?
先说版本选型。openEuler 我选了 22.03 LTS SP4,这是一个长期支持版本,x86_64 的 dvd.iso 镜像在官方镜像站直接下就行,文件名类似 openEuler-22.03-LTS-SP4-x86_64-dvd.iso。LTS 版本意味着生命周期长,不会出现刚装完系统就发现源已经不维护的情况。SP4 是 22.03 这个大版本下的第四个补丁版本,内核相对成熟,对 Kubernetes 和容器运行时这种对内核参数敏感的系统兼容性比较好。
Kubernetes 版本我选的是 1.28.x。为什么不追最新版?因为学习测试环境最重要的是生态兼容稳定。Harbor 的 Helm Chart、Calico 网络插件、Dashboard 都有各自的版本适配节奏,选一个发布半年以上的 Kubernetes 小版本,所有周边组件版本都能找到明确的兼容组合。1.28 的 kubeadm 初始化流程非常成熟,网上踩坑资料也最多,遇到问题容易查。
Harbor 我选的 2.8 以上版本,配合 Helm Chart 的 1.12 或 1.13 版本。Harbor 从 2.x 开始默认就是 OCI 分发规范,和 containerd 的对接非常顺滑。学习环境不需要用到镜像复制、漏洞扫描这些重量级功能,但自带的项目隔离、机器人账户、Webhook 这些功能足够让你理解生产环境里镜像仓库的基本模型。
另外一点是这套系统硬件配置不高也扛得住。Master 节点 4C8G、Worker 节点 2C4G 就能跑完整套环境,磁盘 50G 起步,Harbor 默认要跑 Postgres、Redis、Registry、Core、Portal 等多个容器,内存吃紧的话会频繁触发 OOM,这一点后面排障部分我会细说。
1.2 VMware 虚拟机配置与系统安装流程
虚拟机我用的是 VMware Workstation,如果是国产化环境或者公司统一用 FusionCompute,套路是一样的,关键是虚拟机的配置参数。
新建虚拟机的配置我直接参考下面的参数:
- 操作系统类型:Linux,内核版本选 Red Hat Enterprise Linux 8 64 位(openEuler 和 RHEL 系兼容)
- CPU:4 核
- 内存:8GB(实测在 4GB 下能跑但很紧张,Harbor 一起部署的话建议 8GB)
- 磁盘:60GB(系统装完大概占 10GB,镜像仓库数据会慢慢涨)
- 网络:NAT 模式或桥接模式,NAT 模式更省事,但一定要在 VMware 的虚拟网络编辑器里固定网段,避免宿主机 DHCP 动态分配导致 IP 漂移
安装过程里有两个容易踩坑的地方。第一个是分区,openEuler 的图形安装器默认分区方案是 LVM,如果你后续要调整磁盘或者扩容目录,LVM 是必要的;但学习环境建议直接把 / var 和 / 放一块,不要单独分 /home,避免后续镜像数据把根分区占满后一头雾水。第二个是安装源验证,如果镜像下载不完整,安装器会在读取软件包时报错,建议下完镜像后校验一下 sha256sum。
系统安装完成后,用 root 登录,第一件事就是配置 yum 源。openEuler 官方源一般可以直接用,但有时候在公司网络环境下访问速度很难受,我习惯用国内镜像源替换。先备份原有的 repo 文件:
bash复制cp -a /etc/yum.repos.d /etc/yum.repos.d.bak
rm -f /etc/yum.repos.d/*.repo
然后创建 openEuler 的镜像源配置,以 22.03 LTS SP4 x86_64 为例,核心配置是这样:
ini复制[openEuler-everything]
name=openEuler 22.03 LTS SP4 everything
baseurl=https://mirrors.tuna.tsinghua.edu.cn/openeuler/openEuler-22.03-LTS-SP4/everything/x86_64/
enabled=1
gpgcheck=1
gpgkey=https://mirrors.tuna.tsinghua.edu.cn/openeuler/openEuler-22.03-LTS-SP4/everything/x86_64/RPM-GPG-KEY-openEuler
执行 yum makecache 缓存元数据,随后用 yum install -y vim wget tar lrzsz bash-completion 装一批基础工具。
1.3 系统初始化:关闭防火墙、禁用 swap、配置内核参数
这一步是整个部署的基础,很多集群起不来或者网络异常,追根溯源都是系统初始化没做干净。我按照 kubeadm 官方文档的要求逐项配置。
第一项,关闭防火墙。openEuler 默认安装并启动 firewalld,Kubernetes 的 kube-proxy 用的是 iptables 或 ipvs 模式,和 firewalld 的 nftables 后端有时会冲突,而且测试环境什么端口都要通,直接禁用最省心:
bash复制systemctl stop firewalld
systemctl disable firewalld
第二项,禁用 swap。Kubernetes 从 1.28 开始虽然可以在开启 swap 的状态下运行 kubelet,但那是实验特性,学习环境不要自找麻烦。直接用注释掉 fstab 里的 swap 行:
bash复制sed -i '/swap/s/^/#/' /etc/fstab
swapoff -a
第三项,配置内核参数。这里有两个必须打开的参数,一个是 net.ipv4.ip_forward,这是 kube-proxy 和容器网络转发的基础;另一个是 net.bridge.bridge-nf-call-iptables,保证经过 Linux 网桥的流量能被 iptables 规则处理。先加载 br_netfilter 模块:
bash复制modprobe br_netfilter
echo 'modprobe br_netfilter' >> /etc/rc.local
chmod +x /etc/rc.local
echo "br_netfilter" > /etc/modules-load.d/k8s.conf
然后写入 sysctl 配置:
bash复制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
如果你在初始化之后发现 Pod 网络跨节点不通,大概率是这两个内核参数没有生效。建议在这里先确认一下:
bash复制sysctl net.bridge.bridge-nf-call-iptables
sysctl net.ipv4.ip_forward
输出结果都应该是 1。
第四项,检查 SELinux。openEuler 默认安装的 SELinux 状态可能为开启,虽然 kubeadm 在 EL8 系的系统上会自动处理,但为了减少不确定性,我还是直接设置为 permissive 或 disabled。改 /etc/selinux/config,把 SELINUX=enforcing 改成 SELINUX=permissive,然后重启系统。生产环境你别这么干,但学习测试环境完全没有问题。
1.4 静态 IP、主机名与 hosts 解析规划
Kubernetes 集群的节点之间通信全靠主机名解析,这一步规划不好后面全是坑。
我在这套环境里规划了三台虚拟机:
- k8s-master:192.168.100.101,4C8G
- k8s-worker1:192.168.100.102,2C4G
- k8s-worker2:192.168.100.103,2C4G
三台机器全部安装 openEuler 22.03 LTS SP4。每台机器用 nmcli 配置静态 IP,以 master 为例:
bash复制nmcli connection modify ens160 ipv4.addresses 192.168.100.101/24
nmcli connection modify ens160 ipv4.gateway 192.168.100.2
nmcli connection modify ens160 ipv4.dns 192.168.100.2
nmcli connection modify ens160 ipv4.method manual
nmcli connection up ens160
每台机器的 /etc/hosts 都要加上三个节点的解析记录:
bash复制192.168.100.101 k8s-master
192.168.100.102 k8s-worker1
192.168.100.103 k8s-worker2
同时把每一台的主机名设置好,注意节点名不要用下划线,不要混合大小写,全部小写最靠谱:
bash复制hostnamectl set-hostname k8s-master
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 容器运行时层:containerd 的安装与 Kubernetes 调用链原理拆解
2.1 为什么选择 containerd 而不是 Docker?
很多从 Docker 时代过来的运维同事会习惯性地先装 Docker,再用 Docker 作为 Kubernetes 的运行时。这么做当然能跑,但现在的 Kubernetes 社区已经不怎么推荐了。kubeadm 从 1.24 开始默认就不再内置对 Docker 的直接支持,需要通过 cri-dockerd 这个适配层来把 Docker 包装成符合 CRI 接口的样子。
学习环境我建议直接用 containerd,理由很直接:它是 Kubernetes 生态里默认的 CRI 运行时,kubelet 通过 CRI 协议直接和 containerd 通信,中间没有多余的适配层。你排查问题的时候少一层,就少一个坑。
另外,你平时用 docker build 构建出来的镜像是标准 OCI 镜像,containerd 完全可以拉取和运行。对于镜像推送测试,我们可以让 Harbor 作为镜像终点,docker build 和 containerd 之间不存在格式冲突。
2.2 kubelet 调用 containerd 的 CRI 链路
很多运维同行能熟练执行 kubectl get pods,但当你问他一个 Pod 从创建到运行,kubelet 到底怎么把任务下发给容器运行时,他可能只能答个大概。借这套环境,我把这条链路的完整调用过程拆开说一下。
kubelet 是 Kubernetes 集群中运行在每个节点上的代理进程,它只负责和 API Server 打交道,监听分配给本节点的 Pod 变化。一旦 kubelet 发现有新 Pod 要创建,它会按照 PodSpec 里的容器配置,调用 CRI(Container Runtime Interface)插件接口。
这里有个关键点:CRI 不是直接调用 containerd 的原始接口,而是标准化的 gRPC 接口,定义了两个核心服务。RuntimeService 负责管理 Pod 沙箱和容器的整个生命周期,包括创建、启动、停止、删除,而 ImageService 负责镜像相关的操作,比如拉取、查看、删除镜像。
containerd 在启动后会监听一个 Unix Socket 文件 /run/containerd/containerd.sock,这个 Socket 就是 CRI 插件的服务入口。kubelet 通过 gRPC 协议连到这个 Socket,发一个 RunPodSandbox 请求,containerd 收到后会用 pause 镜像创建 Pod 沙箱容器,相当于先把网络和命名空间这些基础设施准备好。接着 kubelet 再发 CreateContainer 和 StartContainer 请求,containerd 才会真正去启动业务容器。
这里面还涉及两层调用:containerd 本身具备完整的容器生命周期管理能力,但它也通过自己的插件机制管理镜像、快照、网络、事件等。这个链路在运维排查中的直接意义是:如果 crictl 命令能正常工作,说明 containerd 的 CRI 插件没有挂;如果 kubelet 日志里报找不到 Socket,那问题往往出在 containerd 配置或服务启动顺序上。
crictl 是专门用来调试 CRI 接口的命令行工具,后面排查问题会高频用到。它的存在就是为了让你不依赖 docker 命令行也能查看和管理容器。
2.3 containerd 安装与 sandbox_image 镜像修改
openEuler 22.03 的官方软件源里已经有 containerd 包,安装非常简单:
bash复制yum install -y containerd
但直接装完的 containerd 默认配置是不能用于 Kubernetes 的,必须要改两个地方。先导出默认配置:
bash复制mkdir -p /etc/containerd
containerd config default > /etc/containerd/config.toml
然后在 config.toml 里找到 [plugins."io.containerd.grpc.v1.cri"] 这一段,把 sandbox_image 从默认的 registry.k8s.io/pause:3.6 改成国内可访问的镜像地址。否则后续初始化集群时,kubelet 创建 Pod 沙箱会一直卡在 ImagePullBackOff。
toml复制[plugins."io.containerd.grpc.v1.cri"]
sandbox_image = "registry.cn-hangzhou.aliyuncs.com/google_containers/pause:3.9"
另外一个经常出问题的配置是 systemd cgroup 驱动。kubelet 默认用 systemd 作为 cgroup 驱动,containerd 默认是 cgroupfs,两者不一致会导致 kubelet 报错。在 config.toml 中找到 SystemdCgroup,把它从 false 改成 true:
toml复制[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
SystemdCgroup = true
改完之后重启 containerd 并设置开机自启:
bash复制systemctl daemon-reload
systemctl restart containerd
systemctl enable containerd
这时候用 ctr version 和 crictl version 分别验证一下,前者看 containerd 本体的版本,后者验证 CRI 接口是否正常响应。
2.4 containerd 常用运维命令与状态检查
containerd 的命令行工具有两个:ctr 和 crictl。这两个命令的使用场景一定要分清楚。ctr 是 containerd 的原生命令,直接和 containerd 内部组件通信,走的是 containerd 自己的 API;crictl 是 Kubernetes 社区维护的 CRI 调试工具,走的是 CRI 插件接口。平时看 Pod 容器状态,一定要用 crictl,因为 kubelet 看到的视角就是 CRI 视角,而不是 containerd 原生视角。
对我这套学习环境来说,crictl 更常用。把 crictl 的配置文件写好:
bash复制cat > /etc/crictl.yaml <<EOF
runtime-endpoint: unix:///run/containerd/containerd.sock
image-endpoint: unix:///run/containerd/containerd.sock
timeout: 10
debug: false
EOF
日常我用的最多的命令就几个:
bash复制crictl ps -a
crictl images
crictl logs <container-id>
crictl rm -a -f
如果发现某个 Pod 一直 ContainerCreating,第一件事就是用 crictl ps -a 看容器事件,再用 crictl logs 拉容器日志,比直接看 kubectl describe 的输出更快定位到底层容器的问题。
3. Kubernetes 集群初始化:kubeadm 安装、CNI 网络插件与 Dashboard
3.1 kubelet / kubeadm / kubectl 版本一致性与安装过程
Kubernetes 三个核心包 kubelet、kubeadm、kubectl 必须保持一致版本,这是 kubeadm 官方明确要求的。我用的是 1.28.2,所有节点同样版本。
配置 Kubernetes 的 yum 源,这里使用阿里云的 EL7 源(openEuler 与 RHEL 兼容):
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
EOF
然后安装:
bash复制yum install -y kubeadm-1.28.2 kubelet-1.28.2 kubectl-1.28.2
systemctl enable kubelet
这里注意一个细节:kubelet 现在先不要启动,因为还没有初始化集群,没有相关配置文件,直接启动会不断报错退出。把 enabled 设置好就行,等 kubeadm init 成功之后它自然会起来。
3.2 kubeadm init 参数详解与完整执行过程
在 master 节点上初始化集群。核心命令如下:
bash复制kubeadm init \
--kubernetes-version=1.28.2 \
--apiserver-advertise-address=192.168.100.101 \
--pod-network-cidr=10.244.0.0/16 \
--cri-socket=unix:///run/containerd/containerd.sock \
--image-repository=registry.cn-hangzhou.aliyuncs.com/google_containers
这几个参数每一个都有讲究。--apiserver-advertise-address 是指 API Server 对外广播的地址,必须填 master 节点的 IP,如果网卡多,你不指定它就可能选错。--pod-network-cidr 是 Pod 网段,这里我用的是 10.244.0.0/16,这个网段必须和后面安装的 Calico 配置一致,否则网络插件初始化会失败。--image-repository 相当于全局镜像仓库前缀,kubeadm 初始化时需要从公共仓库拉取 apiserver、controller-manager、scheduler、etcd、pause 等一堆镜像,设置成国内可访问的仓库能大幅减少失败概率。
初始化跑完,终端会输出三个重要信息:kubectl 的配置方法、节点加入集群的命令、以及一个带 token 的 kubeadm join 命令。我把 join 命令完整保存下来,后面 worker 节点加入就靠它。
配置 kubectl:
bash复制mkdir -p $HOME/.kube
cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
chown $(id -u):$(id -g) $HOME/.kube/config
执行 kubectl get nodes,你会发现 master 节点的状态是 NotReady。这是正常的,因为 CNI 网络插件还没安装。这在初始化过程中是非常常见的一幕,不用慌。
3.3 网络插件 Calico 安装与 Pod 网段验证
CNI 插件我选了 Calico。Flannel 简单,但 Calico 的 networkPolicy 功能你后面学习起来会更有价值。Calico 安装非常简单,官方 manifest 直接 apply 就行:
bash复制kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/calico.yaml
默认配置文件里会自动探测 --pod-network-cidr 的配置。不过有个细节要确认:如果 Calico manifest 默认网段不是 10.244.0.0/16,你需要先下载文件,把 Deployment 里 CALICO_IPV4POOL_CIDR 环境变量的值改成 10.244.0.0/16,再 apply。否则 Pod 和 Calico 的网段对不上,Pod 拿到 IP 也通不了。
等 Calico 的 Pod 全部 Running 后再看节点状态:
bash复制kubectl get nodes
如果所有节点都是 Ready,说明基础网络链路已经通了。这时候可以快速验证一下 Pod 网段的连通性,起一个 busybox,从它去 ping 其他 Pod 的 IP,能通就说明 Overlay 网络正常。
Worker 节点加入集群的命令,直接在 worker 机器上执行:
bash复制kubeadm join 192.168.100.101:6443 --token <token> --discovery-token-ca-cert-hash sha256:<hash>
3.4 Kubernetes Dashboard 部署与访问(NodePort 方式)
学习环境没有生产环境那么严格,直接用 NodePort 方式暴露 Dashboard 最方便。安装官方 Dashboard:
bash复制kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.7.0/aio/deploy/recommended.yaml
默认的 Service 是 ClusterIP,把它改成 NodePort:
bash复制kubectl -n kubernetes-dashboard edit svc kubernetes-dashboard
把 type 改成 NodePort,然后看端口:
bash复制kubectl -n kubernetes-dashboard get svc
访问 https://
登录 Dashboard 需要 token。创建一个管理员用户和绑定:
bash复制cat <<EOF | kubectl apply -f -
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
EOF
获取 token:
bash复制kubectl -n kubernetes-dashboard create token admin-user
这个 token 是带过期时间的。如果你打算长期用,可以考虑用 kubectl get secret 拿到永久 token,但学习环境每次临时创建也够了。
4. Harbor 镜像仓库部署:Helm 安装、自签证书与持久化配置
4.1 Helm 安装与 Harbor Chart 仓库添加
Harbor 我用 Helm 部署,原因很简单:官方 Helm Chart 把 Postgres、Redis、Registry、Core、Portal、Nginx 这些组件全都编排好了,改 values.yaml 就能完成定制,比手动 docker-compose 方式好维护。
先在 master 节点上安装 Helm 3。直接用脚本或者下载二进制包都行,我习惯用官方脚本:
bash复制curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod 700 get_helm.sh
./get_helm.sh
然后添加 Harbor 的 Chart 仓库:
bash复制helm repo add harbor https://helm.goharbor.io
helm repo update
4.2 Harbor 自签证书生成与 Docker/containerd 信任配置
Harbor 默认走 HTTPS,你需要一套自己的证书。生产环境可以用 Let's Encrypt 或者企业内部的 CA,学习环境直接自签。
先生成 CA 私钥和证书:
bash复制mkdir -p /opt/harbor-certs && cd /opt/harbor-certs
openssl genrsa -out ca.key 4096
openssl req -x509 -new -nodes -sha256 -days 3650 \
-subj "/CN=Harbor CA" \
-key ca.key \
-out ca.crt
再生成 Harbor 服务端证书。这里注意,Common Name 或者 Subject Alternative Name 里必须包含你访问 Harbor 用的域名或 IP。我在这套环境里用 IP 地址 192.168.100.101 访问,所以生成证书时要指定 IP 类型的 SAN:
bash复制openssl genrsa -out harbor.key 2048
cat > harbor.csr.cnf <<EOF
[req]
distinguished_name = dn
req_extensions = v3_req
prompt = no
[dn]
CN = 192.168.100.101
[v3_req]
subjectAltName = @alt_names
[alt_names]
IP.1 = 192.168.100.101
EOF
openssl req -new -key harbor.key -out harbor.csr -config harbor.csr.cnf
cat > harbor.ext <<EOF
subjectAltName = @alt_names
[alt_names]
IP.1 = 192.168.100.101
EOF
openssl x509 -req -in harbor.csr \
-CA ca.crt -CAkey ca.key -CAcreateserial \
-out harbor.crt -days 3650 -extfile harbor.ext
生成的 harbor.crt 和 harbor.key 就是后面 Helm values 里要用的。
另外,如果之后要用 docker login 推送镜像到 Harbor,还需要把 ca.crt 加到 Docker 的信任目录:
bash复制mkdir -p /etc/docker/certs.d/192.168.100.101
cp ca.crt /etc/docker/certs.d/192.168.100.101/ca.crt
如果你不想折腾证书,也可以在 Docker daemon.json 里把 Harbor 地址配到 insecure-registries。但 containerd 侧的信任配置方式不一样,后面我会同时给出两条路径。
4.3 values.yaml 核心参数配置
把 Harbor Chart 拉下来,基于默认 values 生成自己的配置:
bash复制helm show values harbor/harbor > harbor-values.yaml
需要修改的关键参数有这几个。
一个是 externalURL,改成你的 Harbor 访问地址:
yaml复制externalURL: https://192.168.100.101:30003
Harbor 默认的 Service 是 ClusterIP,学习环境我改成 NodePort 方式访问,端口映射为:
yaml复制service:
type: NodePort
nodePorts:
http: 30002
https: 30003
这样访问 https://192.168.100.101:30003 就能打开 Harbor Web 界面。
接下来配证书。把之前生成的 harbor.crt 和 harbor.key 做成 Kubernetes Secret 让 Helm 注入:
yaml复制expose:
tls:
enabled: true
secretName: harbor-tls
我们可以先把证书放到命令行参数里,也可以用预先创建的 Secret。用 kubectl create secret tls harbor-tls --cert=harbor.crt --key=harbor.key -n harbor 创建。
然后是默认密码和持久化:
yaml复制harborAdminPassword: "Harbor12345"
persistence:
enabled: true
persistentVolumeClaim:
registry:
size: 20Gi
database:
size: 5Gi
redis:
size: 2Gi
如果是单节点测试环境,没有独立的存储类,但 openEuler 上安装一个 storageClass 比较麻烦。为了省事,我在测试环境把持久化关掉,或者直接用 hostPath。生产环境千万别这么干。测试环境关掉持久化的写法:
yaml复制persistence:
enabled: false
但这样 Harbor 重启后数据可能丢失。如果只是想跑通流程验证功能,这个方案可以接受。不过我还是建议尝试配置一个简单的 local-path StorageClass,这样 Harbor 数据能保留,也贴近生产形态。
4.4 Harbor 部署完成后的服务检查与 Web 登录验证
创建命名空间并开始部署:
bash复制kubectl create ns harbor
helm install harbor harbor/harbor -f harbor-values.yaml -n harbor
部署过程会拉取很多镜像,第一个是 PostgreSQL、Redis,然后是 Harbor 核心组件。镜像都比较大,耐心等几分钟。观察 Pod 状态:
bash复制kubectl -n harbor get pods
如果一切正常,你最终会看到 9 个左右的 Pod 全部 Running。这时候用浏览器访问 https://192.168.100.101:30003,输入 admin / Harbor12345 登录。
如果界面打开后一直转圈或者报 502,首先看 nginx Pod 是否正常,然后看 core Pod 是否启动成功。Harbor 的 core 依赖数据库和 Redis,任何一个没起来都会直接导致 Web 界面 502,这个排查思路后面会专门展开。
5. 集群拉取 Harbor 镜像全流程验证
5.1 在 Harbor 创建项目和测试用户
Harbor 里的项目是镜像隔离的最小粒度。登录 Web 界面后,先创建一个项目,名称填 library,访问级别选公开。学习环境图省事可以选公开,这样 containerd 拉镜像的时候不需要每次都带认证信息。但如果想理解私有仓库的认证流程,建议建一个私有的项目,然后在机器人账户或者项目成员里创建一个专门用于拉取的账户。
我实际操作时创建了一个测试项目 library,又在项目成员里加了一个专用账号 devuser,密码设置为 Dev@12345。这个账号后面用来在集群节点上配置镜像拉取凭证。
5.2 使用 docker 推送镜像到 Harbor
先把 Harbor 官方构建好的测试镜像拉下来,再推送。以 nginx 为例,先登录,然后打 tag,再 push:
bash复制docker pull nginx:1.24
docker login 192.168.100.101:30003 -u devuser -p Dev@12345
docker tag nginx:1.24 192.168.100.101:30003/library/nginx:1.24
docker push 192.168.100.101:30003/library/nginx:1.24
在这套环境里,集群节点上只装了 containerd,没有 docker。推送时我是在一台装有 docker 的独立机器上操作的,或者临时在 master 上装个 docker-cli 也行。docker login 的时候需要注意,如果之前没有把 ca.crt 放到 Docker 的信任目录,登录会报 x509 证书校验错误。学习环境为了省事,可以在 /etc/docker/daemon.json 中配置:
json复制{
"insecure-registries": ["192.168.100.101:30003"]
}
然后重启 docker,免掉证书校验。docker 在这里只是作为推送工具,不影响集群运行时。如果你不想装 docker,也可以用 podman 或 skopeo 推,方式类似。
5.3 配置 containerd 访问 Harbor 私有仓库
这一步是很多教程里讲得不清不楚的地方。containerd 不是 docker,它不读 /etc/docker/daemon.json。访问 Harbor 的认证和 TLS 配置都要写在 /etc/containerd/config.toml 里。
我直接修改配置,在 CRI 插件的 registry 部分加上 Harbor 的认证信息和跳过证书校验:
toml复制[plugins."io.containerd.grpc.v1.cri".registry]
[plugins."io.containerd.grpc.v1.cri".registry.configs]
[plugins."io.containerd.grpc.v1.cri".registry.configs."192.168.100.101:30003"]
[plugins."io.containerd.grpc.v1.cri".registry.configs."192.168.100.101:30003".auth]
username = "devuser"
password = "Dev@12345"
[plugins."io.containerd.grpc.v1.cri".registry.configs."192.168.100.101:30003".tls]
insecure_skip_verify = true
这个配置的作用是在 CRI 层面显式声明访问特定仓库的认证凭证和 TLS 策略。如果你前面已经通过 Helm 给 Harbor 配了自签证书,这里也可以指定 ca.crt 的路径来代替 insecure_skip_verify,但测试环境直接跳过校验更省事。
修改完配置后重启 containerd:
bash复制systemctl restart containerd
重启不影响已运行的容器,但正在运行的 Pod 会短时间内无法操作,属于正常现象。
5.4 部署 Nginx 工作负载验证镜像拉取
现在写一个真正从 Harbor 拉镜像的 Deployment,验证整条链路。直接写个最简单的:
yaml复制apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-test
spec:
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: 192.168.100.101:30003/library/nginx:1.24
ports:
- containerPort: 80
apply 这个文件:
bash复制kubectl apply -f nginx-test.yaml
然后观察事件:
bash复制kubectl get pods -l app=nginx -w
kubectl describe pod nginx-test-xxxx
如果镜像拉取成功,Pod 会进入 Running 状态。如果一直 ImagePullBackOff,用 describe 或者 crictl pull 192.168.100.101:30003/library/nginx:1.24 手动拉一次,错误信息会直接暴露问题,最常见的还是认证失败或者证书校验失败。
到这里,整条链路已经打通了:开发机 docker push 镜像到 Harbor,Kubernetes 集群里的 kubelet 通过 containerd 从 Harbor 拉取镜像并运行容器。
6. 学习测试环境常见问题与排障实录
6.1 kubeadm init 失败的常见原因
kubeadm init 是整套环境最容易被卡住的环节。我遇到的几个典型问题:
第一个是镜像拉取超时。虽然配置了 --image-repository,但如果你没有提前 kubeadm config images pull 验证,初始化过程中可能因为网络波动导致某些镜像拉取失败。建议先将镜像列表拉出来,单独执行一遍 pull:
bash复制kubeadm config images list --image-repository=registry.cn-hangzhou.aliyuncs.com/google_containers
kubeadm config images pull --image-repository=registry.cn-hangzhou.aliyuncs.com/google_containers
第二个是 /etc/kubernetes/manifests 下的静态 Pod 没有启动。比如 kube-apiserver 反复 CrashLoopBackOff,这部分通过 crictl logs 看容器日志就能定位。曾经遇到过 etcd 容器起不来,原因是磁盘空间不足,etcd 对 IO 和空间都是硬需求,用 df -h 和 iostat 检查一下。
第三个是 swap 没有完全关闭。kubeadm 会直接拒绝初始化,报错信息里有明显的 running with swap on is not supported 字样。这个错误最直观,但也最容易被忽略,因为 swapoff 之后如果没改 fstab,重启机器以后问题复现。
第四个是端口冲突。6443、2379、2380 这些端口被占用,kubeadm 会在 preflight 阶段直接报错。用 ss -lntp | grep 6443 查一下即可。
6.2 containerd 拉取镜像失败与 sandbox_image 问题
最常见的问题是 Pod 一直处于 ContainerCreating,describe 看到的事件提示 sandbox image 拉取失败。
这个问题的根源几乎都在 sandbox_image 配置。如果你没有修改 /etc/containerd/config.toml,默认值是 registry.k8s.io/pause:3.6,这个地址在不少网络环境下访问不稳定。我已经在前面把它改成了阿里云的镜像地址。
但这里有个坑:crictl pull 手动拉取成功不代表 CRI 会自动用这个镜像。如果你修改配置之后 containerd 没有重启,或者 kubelet 还缓存着旧的 sandbox 状态,Pod 依然会失败。建议在修改完配置后,重启 containerd 并删除所有已存在的 sandbox 容器,再重新调度 Pod。
另外要注意配置路径的准确性。containerd 的配置层级嵌套很多,一个斜杠写错就会导致整个 registry 配置失效,而且它会静默忽略错误,不报出来,你在 describe 里只会看到 401 或 certificate 相关的错误。遇到认证问题,先检查 auth 配置是否真的在最终生效的 config.toml 里,不要改完就以为生效了。
6.3 Harbor 服务启动后登录 502/503 的排查
Harbor 部署起来以后,访问 Web 页面报 502,这是最常见的问题。表面上是 Nginx 反代失败,实际上是后端 core 服务没有就绪。
排查链路是先看所有 Pod 的状态:
bash复制kubectl -n harbor get pods
如果 database 或 redis 还在 CrashLoopBackOff,说明依赖的数据库没有起来。Harbor 的 database 容器启动比较慢,第一次初始化数据库可能要两分钟左右,耐心等一会再观察。
如果 core Pod 已经 Running,但日志里有连接数据库失败的错误,通常是数据库还没初始化完成,或者数据库用户名密码不对。测试环境我建议先把 persistence.enabled 关掉,这样数据库和 Redis 用空数据启动,数据库初始化时间会短很多,故障范围也小。
如果 nginx Pod 正常、core 正常、浏览器还是 502,检查一下 externalURL 和实际访问地址是否一致。Helm 部署时 externalURL 写的是 IP:端口,但实际 Nginx 配置会根据 host header 来决定转发到哪个后端。不一致就会导致路由错误,表现就是 502 或 404。
6.4 集群资源不足导致 Pod 调度失败
学习环境最容易被忽略的问题是资源不足。Harbor 默认部署会占掉 master 节点一大半内存,我 8G 内存的 master 节点跑完 Harbor 所有 Pod 后,再用它调度 Nginx 测试镜像,经常遇到 0/1 nodes are available 或者 Insufficient memory 的调度失败。
遇到这种问题,我的处理方法是给 master 节点打上允许调度的标签,并调整 Harbor 的资源需求:
bash复制kubectl taint nodes k8s-master node-role.kubernetes.io/control-plane:NoSchedule-
默认情况下 master 节点有 NoSchedule 污点,工作负载不会调度上去。学习环境无所谓,去掉这个污点就能让测试用的 Deployment 调度到 master 上。
同时,在 Harbor 的 values.yaml 里可以把资源 requests 调小:
yaml复制resources:
requests:
cpu: 100m
memory: 256Mi
把 registry、database、redis 这些组件的 requests 都调低,Harbor 占用的总内存能控制到 3G 左右。
另外一个容易被忽略的资源是 inotify 限制。当集群里有大量容器时,systemd 报 Failed to watch file: No space left on device 实际上不是磁盘满,而是 inotify watch 数量用完了。调一下:
bash复制sysctl fs.inotify.max_user_watches=524288
sysctl fs.inotify.max_user_instances=512
6.5 快速重置与重新部署技巧
学习环境和生产环境最大的区别就是容错率高,你可以在同一个环境里反复折腾。如果集群被搞坏了,与其费劲修,不如直接重置重来。
master 节点重置:
bash复制kubeadm reset -f
rm -rf /etc/cni/net.d
rm -rf $HOME/.kube
worker 节点只需要执行 kubeadm reset -f,然后重新 join 即可。
重置后重新部署的时候,有个小技巧是提前把所有镜像拉好。在正式 kubeadm init 之前,先用 kubeadm config images pull 把镜像都拉到本地,这样初始化过程几乎是秒过,不会因为网络波动导致失败。
Harbor 重置更方便,直接删除命名空间即可:
bash复制kubectl delete ns harbor
helm uninstall harbor -n harbor
但要注意,如果之前开了持久化存储,PVC 不会因为删除命名空间自动清理,需要手动删除 PVC。测试环境如果不关心数据,直接把 PVC 一起删掉:
bash复制kubectl -n harbor delete pvc --all
最后再分享一个小经验。整套环境跑通之后,建议把每台机器上执行过的命令整理成 Shell 脚本或者 Ansible Playbook,尤其是系统初始化部分和 containerd 配置部分。原因很简单,这套环境你大概率会用到不止一次,过两个月再回来搭第二套,翻文档远不如直接跑脚本高效。我在实际操作中遇到过配置 containerd 时少写一个嵌套层级导致镜像拉取 401 的问题,也遇到过 Harbor 数据库初始化和 core 组件启动顺序不对导致 502 的情况。这些坑如果你提前把脚本固化下来,后续重装环境基本可以做到半小时内复现整套集群。
