Kubernetes离线部署实战:kubeadm+containerd+Harbor全流程指南
搞开发的人基本都遇到过这种场景公司网闸一拉服务器只能访问内网或者项目本身就在一个跟外网彻底隔离的隔离网里想装个Kubernetes集群结果连个镜像都拉不下来。上个月我正好在开发测试环境里把整套Kubernetes离线部署流程完整跑了一遍从物料准备到集群初始化再到后续组件补齐中间踩了不少坑。这篇文章就是这次实操的完整记录从制定方案到每一步的原理和命令都尽量写清楚给后面需要离线搭K8s的同学省点时间。如果你正打算在离线网络环境里搭建一套Kubernetes集群或者已经试过手动部署但卡在某个环节这篇指南应该能帮上忙。文章会覆盖离线部署的完整流程方案选型、离线物料的准备、containerd的配置、kubeadm初始化、工作节点接入、常用组件的离线安装以及整个过程中遇到的高频问题。我不打算讲那种纯理论的东西全部是实际操作里能直接用的办法。1. 离线部署的整体思路与方案选型1.1 三个常见方案的对比先说说目前离线部署Kubernetes的主流路线。如果你在网上搜“Kubernetes离线部署”基本会看到三类方案基于kubeadm加离线镜像包、基于sealos这类打包工具以及纯手工二进制部署。第一类是最传统的kubeadm路线在一台能访问外网的机器上把依赖的rpm包、镜像全部下载好然后搬到内网用kubeadm初始化集群。这种方式的好处是每一步都可以控制出了问题好排查坏处是准备工作量大镜像列表要自己整理容易漏。第二类是sealos之类的工具。sealos把整个集群的镜像、组件、配置全部打成一个大的OCI镜像一条命令就能拉起集群非常快。我在测试环境里试过sealos部署在线集群速度确实快几分钟就能看到node处于Ready状态。但离线场景下用sealos有个问题你需要提前找一台机器把sealos的集群镜像下载下来然后转成离线包这中间的依赖关系如果没理清楚反而比kubeadm更麻烦。另外sealos封装的K8s版本相对固定如果你需要某个特定版本就得自己重新做一个集群镜像定制成本不低。第三类纯二进制部署直接把kube-apiserver、kube-controller-manager、kube-scheduler、kubelet、kube-proxy这些二进制文件下载下来手动创建证书、配置文件、systemd服务单元。这种方式能让你对K8s内部机制有很深的理解适合学习但不适合开发测试环境。原因很简单手动做证书、配置高可用非常容易出错而且后续维护成本和排查成本都高。1.2 为什么我选择kubeadm containerd这条路线综合比较下来我的建议是开发测试环境的离线部署首选kubeadm containerd镜像通过内网Harbor仓库中转。这套组合有两个核心优势第一kubeadm本身就是官方推荐的部署工具版本兼容性有保障升级路径清晰第二containerd在1.24版本之后已经成为K8s默认的运行时配置和维护比Docker直连简单也避免了dockershim被移除后带来的兼容问题。用kubeadm还有一个实际的考虑这个工具在初始化的时候会自己检测集群的版本、健康状态如果有明显配置问题会在初始化前就报错而不是等集群起来了才出问题。这种“fail fast”的特性在离线环境里特别宝贵因为你没有外网可以查资料每节省一次拉锯战都相当于节省了大量时间。离线部署中需要重点解决的一个问题是“镜像从哪来”。在离线环境里各节点无法访问Docker Hub或quay.io等公共镜像仓库。我的做法是准备一台“中转机”这台机器能访问外网也有磁盘空间用它提前把Kubernetes组件镜像、网络插件镜像、存储类镜像全部拉下来推送到内网Harbor。然后内网各节点统一从Harbor拉取镜像。这样做的好处是镜像只下载一次后续所有节点复用Harbor内部可配置为项目公开节点拉取时不需要额外配置认证后续要加节点或升级组件只需从Harbor拉取不用再往机器上拷贝tar包。containerd上的镜像拉取也一样你需要在/etc/containerd/config.toml里配置好registry的endpoint让containerd把对registry.k8s.io的请求转发到内网Harbor这样kubeadm初始化时不需要修改镜像仓库地址参数所有组件镜像就能直接从内网拉取既省事又不容易出错。2. 开工前的资源准备一件都不能少2.1 主机规划与系统配置准备阶段最容易犯的错误是“拿到几台机器就直接开始装”。我在这次部署里一开始就吃了这个亏三台机器里有两台的hostname重复结果kubeadm初始化时etcd起不来排查了半天才发现是主机名冲突。建议先列一张主机清单规划好IP、主机名、角色、配置。我这次用的是三台机器主机名IP地址角色配置系统k8s-master01192.168.20.11控制面节点2C4GCentOS 7.9k8s-node01192.168.20.12工作节点4C8GCentOS 7.9k8s-node02192.168.20.13工作节点4C8GCentOS 7.9开发测试环境有一个主节点加两个工作节点完全够用如果你只是自己验证功能单节点模式也可以跑但不太推荐因为有些调度和网络特性在单节点上验证不出来。系统配置这一步不能跳。首先是设置主机名三台机器分别执行hostnamectl set-hostname k8s-master01 hostnamectl set-hostname k8s-node01 hostnamectl set-hostname k8s-node02然后编辑/etc/hosts把三台机器的IP和主机名映射写进去192.168.20.11 k8s-master01 192.168.20.12 k8s-node01 192.168.20.13 k8s-node02接下来是关闭防火墙和swap。开发测试环境这样处理没问题但如果是生产环境建议用安全组策略代替直接关闭防火墙。关闭swap是必须的因为kubelet默认要求swap处于关闭状态不关的话初始化时会直接报错。systemctl stop firewalld systemctl disable firewalld sed -i / swap / s/^/#/ /etc/fstab swapoff -a还要加载两个内核模块并调整系统参数modprobe overlay modprobe br_netfilter cat EOF /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注意net.bridge.bridge-nf-call-iptables这个参数如果不设置集群内部的ClusterIP访问会不通表现为Pod能启动但Service无法访问这个坑后面排查的时候很隐蔽。2.2 内网Harbor仓库搭建离线部署里Harbor是一个关键基础设施。我用的版本是harbor-offline-installer-v2.8.4.tgz大概2GB左右。你可以在中转机上下载Harbor离线安装包然后传到内网一台独立的服务器上安装。Harbor安装本身很简单解压后修改harbor.yml配置文件。比较重要的几项hostname改成你的Harbor服务器IP或域名比如harbor.internal.comport默认80如果80端口被占可以改成8080harbor_admin_password默认密码是Harbor12345第一次登录后立即修改data_volume镜像数据存储路径建议单独挂一块大磁盘。配置完成后执行./install.shHarbor启动以后在中转机上用docker登录docker login harbor.internal.com然后创建项目比如k8s、library。后面从外网拉镜像打好tag再push到Harbor。举例如果你拉取的是registry.k8s.io/pause:3.9把它重新打标签为harbor.internal.com/k8s/pause:3.9然后push。这样处理之后内网节点在使用registry.k8s.io/pause:3.9时才会自动转发到Harbor的k8s/pause:3.9。Harbor本身可以选registry:2代替但在有多个开发测试团队共用的情况下Harbor的镜像仓库管理、项目隔离、审核功能更全。如果团队很小只求简单用离线安装的单机registry也能撑住。2.3 离线物料清单镜像、软件包一网打尽软件包的准备是离线部署的重头戏。为了避免漏东西我列了一张清单每次部署前都按这个清单核对。基础软件包containerd安装包如containerd-1.7.13-linux-amd64.tar.gzrunc安装包部分containerd版本会自带建议单独准备一份Kubernetes组件kubeadm、kubelet、kubectl版本保持一致比如1.28.2crictl用于调试containerd中的Pod和容器非常有用calicoctl如果网络插件选Calico可选安装方便调试网络策略Kubernetes核心镜像这组镜像是kubeadm初始化时必需的。你可以手动从registry.k8s.io拉取也可以进入一个有外网的机器执行kubeadm config images list生成镜像清单然后脚本循环拉取。我把1.28.2版本的镜像清单列在这里registry.k8s.io/kube-apiserver:v1.28.2 registry.k8s.io/kube-controller-manager:v1.28.2 registry.k8s.io/kube-scheduler:v1.28.2 registry.k8s.io/kube-proxy:v1.28.2 registry.k8s.io/pause:3.9 registry.k8s.io/etcd:3.5.9-0 registry.k8s.io/coredns/coredns:v1.10.1网络与存储组件镜像Flannel或Calico镜像flannel:v0.24.0、flannel-cni-plugin、calico相关metrics-server镜像如果想用kubectl topIngress Controller镜像如ingress-nginx-controller存储相关镜像如local-path-provisioner或nfs-subdir-external-provisionerHelm可选如果你习惯用Helm管理应用提前准备好Helm二进制。离线环境里Helm本身只是一个二进制文件解压即可用不需要额外配置。所有二进制包和镜像文件准备好之后统一放到一个目录下打包比如k8s-offline-package.tar.gz。复制到内网环境时最好用校验和确认文件完整性避免传输过程中文件损坏导致装到一半报错。3. 一步步部署从系统初始化到集群就绪3.1 containerd的离线安装与配置containerd在这套方案里承担所有容器的生命周期管理。离线安装containerd时需要注意它的tar包解压的目标目录是/usr/local解压后containerd、ctr、containerd-shim-runc-v2这些二进制会到/usr/local/bin下。tar -C /usr/local -xzf containerd-1.7.13-linux-amd64.tar.gz mv containerd.service /etc/systemd/system/ systemctl daemon-reload systemctl enable --now containerd这里有个容易被忽略的地方默认的containerd配置是不存在的最好先执行一次containerd config default生成默认配置再在里面修改。mkdir -p /etc/containerd containerd config default | tee /etc/containerd/config.toml打开/etc/containerd/config.toml要做三处关键修改。第一处是SystemdCgroup把它从false改为true。为什么要改Kubernetes和containerd之间关于cgroup driver必须保持一致kubelet默认用systemd作为cgroup drivercontainerd如果用cgroupfs两者会对同一个Pod的资源管理竞争轻则报错重则节点状态不稳定。[plugins.io.containerd.grpc.v1.cri.containerd.runtimes.runc] [plugins.io.containerd.grpc.v1.cri.containerd.runtimes.runc.options] SystemdCgroup true第二处是sandbox_image。这个就是pause镜像地址默认是registry.k8s.io/pause:3.9。如果你的内网节点无法直接访问这个地址需要改为Harbor里的地址比如harbor.internal.com/k8s/pause:3.9。否则创建Pod时会发生Failed to pull image错误。第三处是镜像仓库的endpoint配置。在config.toml的registry部分增加如下内容让containerd访问公共仓库时自动转发到Harbor[plugins.io.containerd.grpc.v1.cri.registry] [plugins.io.containerd.grpc.v1.cri.registry.mirrors] [plugins.io.containerd.grpc.v1.cri.registry.mirrors.registry.k8s.io] endpoint [http://harbor.internal.com/k8s] [plugins.io.containerd.grpc.v1.cri.registry.mirrors.docker.io] endpoint [http://harbor.internal.com/dockerhub]这个mirror配置用起来真的很方便。kubeadm默认会从registry.k8s.io拉镜像Calico默认从quay.io拉镜像只要在mirrors里分别定义一个quay.io的转发规则这些组件就可以用默认地址正常拉取不用挨个修改yaml文件。修改完配置后重启containerdsystemctl restart containerd systemctl status containerd3.2 kubeadm、kubelet、kubectl的安装K3s这类轻量发行版我不太建议在脱机环境下做主力集群因为它的API Server、etcd、scheduler等组件都被封装在一个二进制里虽然部署简单但很多组件的独立配置和排障手段跟原生K8s有差异。开发测试环境跟生产环境保持同构以后有什么问题也能在测试环境里复现出来少走弯路。安装kubeadm这套需要下载三个rpm包或deb包。以CentOS为例可以从阿里云的K8s仓库下载# 在有外网的机器上把对应版本的kubeadm、kubelet、kubectl下载到本地 wget https://pkgs.k8s.io/core:/stable:/v1.28/rpm/kubeadm-1.28.2-0.x86_64.rpm wget https://pkgs.k8s.io/core:/stable:/v1.28/rpm/kubelet-1.28.2-0.x86_64.rpm wget https://pkgs.k8s.io/core:/stable:/v1.28/rpm/kubectl-1.28.2-0.x86_64.rpm把这些rpm包拷贝到每台内网节点上然后rpm -ivh kubeadm-1.28.2-0.x86_64.rpm kubelet-1.28.2-0.x86_64.rpm kubectl-1.28.2-0.x86_64.rpm注意如果节点已经装了老版本的K8s组件rpm -ivh会报冲突需要先卸载或者用rpm -Uvh升级安装。装完之后还要把kubelet服务设置成开机自启但先不要启动它因为还没有生成kubelet的配置文件启动会失败systemctl enable kubelet这条命令执行完以后kubelet会处于未激活状态这是正常的等kubeadm init之后它会自动被kubeadm生成的配置文件激活。3.3 用 kubeadm init 初始化控制面节点初始化之前先确认一下镜像是否已经能从Harbor正常拉取。用crictl配合测试crictl pull harbor.internal.com/k8s/pause:3.9如果这一步能成功那核心镜像基本也没什么问题。接下来在master节点上执行初始化。初始化命令有几个参数值得仔细说kubeadm init \ --kubernetes-versionv1.28.2 \ --control-plane-endpoint192.168.20.11:6443 \ --pod-network-cidr10.244.0.0/16 \ --service-cidr10.96.0.0/12 \ --apiserver-advertise-address192.168.20.11 \ --image-repositoryharbor.internal.com/k8s \ --upload-certs这几个参数的作用分别讲一下。--control-plane-endpoint用于指定控制面的访问地址如果后面要搭高可用可以填VIP地址或者域名当前面只有单master直接用master的IP加6443端口就好。--pod-network-cidr和--service-cidr要提前规划好因为网络插件特别是Calico会依据pod网段下发IP。如果你用Flannelpod-network-cidr建议设为10.244.0.0/16因为Flannel默认就装在这段网段如果用Calico可以设为172.16.0.0/16或10.42.0.0/16但要注意不要和你的物理网段冲突。--image-repository指向Harbor里的k8s项目。注意我们前面在config.toml里做了mirror转发这里还是显式指定了一下效果就是把registry.k8s.io/xxx替换为harbor.internal.com/k8s/xxx来拉取镜像。如果config.toml已经配好了mirror其实可以不指定这个参数让它默认从registry.k8s.io拉containerd会自动转发到Harbor但显式指定可以让日志和排障更清晰我习惯写上。--upload-certs只在需要多master高可用时才有用它会把证书加密后上传到etcd中其他master节点加入时可以直接拉取。单master环境下可以不加这个参数。执行初始化时kubeadm会先做一系列前置检查包括端口、swap、内核模块、CRI连通性等。如果前面配置有遗漏它会明确指出来。我这次第一次初始化时就是因为net.bridge.bridge-nf-call-iptables没设成1而报错可见这一步确实很容易被跳过。初始化成功后kubeadm会输出一段提示信息告诉你接下来要做什么。把它原样记录下来mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config还要记录下worker节点加入集群的命令类似这样kubeadm join 192.168.20.11:6443 --token token --discovery-token-ca-cert-hash sha256:hash这个token默认有效期是24小时如果你当时没有记下来后面可以执行kubeadm token list重新查看。如果token过期用kubeadm token create --print-join-command重新生成一条join命令。此时查看节点状态会看到master处于NotReady状态这是正常的因为还没有装网络插件。3.4 工作节点加入集群加入集群的方式很简单在node01和node02上执行刚才记录的kubeadm join命令即可。不过在此之前node节点上也必须安装好containerd和kubeadm、kubelet并完成系统初始化。这一步最容易犯的错是“只在master上装了软件包node上没装就直接join”结果node一直没法获取到kubelet配置。加入集群后在master上执行kubectl get nodes正常情况会看到三个节点但状态都是NotReady。不要慌这跟初始化时的master一样等你把网络插件装上去以后节点状态会从NotReady变到Ready。如果加入过程报错优先排查两个方向一是node节点的kubelet和containerd是否正常运行二是node节点能不能访问master的6443端口和Harbor仓库。用telnet测一下就知道。3.5 安装网络插件Flannel和Calico怎么选网络插件是整个集群能正常通信的关键。离线环境下Flannel和Calico是最常见的两个选择各有利弊。Flannel的优点是轻量、配置简单、资源占用低适合开发测试环境。它以VXLAN或者host-gw模式在节点间建立Overlay网络性能在测试场景下完全够用。缺点是它只解决Pod间通信和Pod到Service的通信不能做网络策略。Calico功能更强大支持网络策略NetworkPolicy、BGP路由、更细粒度的流量控制。离线安装时Calico的镜像会多一些部署起来也稍重但对调试和验证K8s的网络能力来说是个更好的选择。我在这个测试环境里选了Flannel原因是测试环境没那么多网络隔离需求而且Flannel的离线部署只有三四个镜像操作简单。如果你后面想验证NetworkPolicy那还是直接上Calico吧。Flannel的离线安装通常用yaml文件或Helm chart。我在中转机上下载kube-flannel.yml之后打开这个文件找到quay.io/coreos/flannel镜像把它替换为Harbor的对应地址确保离线环境下能拉取到。如果你在containerd的config.toml里配了quay.io的mirror转发这里其实不用改直接kubectl apply -f kube-flannel.yml就行。kubectl apply -f kube-flannel.yml等待一两分钟查看节点状态kubectl get nodes -o wide如果节点全部变成Ready说明集群容器网络已经正常工作了。3.6 从metrics-server到Dashboard的组合拳集群就绪之后开发测试环境通常会继续装一些配套组件让日常使用更方便。这里我按从常用到可选排了个序第一个是metrics-server。它收集节点和Pod的CPU、内存等指标装完之后kubectl top node和kubectl top pod才能用。没有metrics-serverHPA水平自动伸缩也跑不起来。离线安装metrics-server时要拉取registry.k8s.io/metrics-server/metrics-server镜像同样可以通过Harbor转发解决。第二个是Kubernetes Dashboard。它提供了Web界面开发同学可以在上面看Pod日志、执行命令、查看资源状态比命令行直观不少。离线安装时可以到GitHub下载kubernetes-dashboard.yaml修改其中的镜像地址为Harbor内地址然后kubectl apply即可。Dashboard的Service默认是ClusterIP测试环境可以通过kubectl port-forward代理访问也可以改成NodePort暴露出去。第三个是Ingress Controller。如果你要在集群里部署Web应用或者模拟生产环境的域名访问Ingress是绕不开的。ingress-nginx的离线安装需要拉取ingress-nginx/controller镜像同样走Harbor转发。安装完成之后它会以NodePort或LoadBalancer方式暴露80和443端口在yaml里配置一下即可。第四个是存储解决方方案。开发测试环境如果只是跑无状态应用不需要持久化存储但很多中间件比如MySQL、Redis都需要PVC。我在测试环境用的方案是local-path-provisioner它是Rancher团队开源的一个轻量本地存储方案安装简单部署之后自动为PVC创建本地目录作为存储。优点是不需要专门的存储服务器每台节点的本地磁盘都能用缺点是数据分散在节点本地不适用于生产环境的高可用要求。如果团队有NAS或者NFS服务器用nfs-subdir-external-provisioner也很方便。它把NFS上的一个目录作为动态存储供给池Pod申请PVC时自动在NFS上创建子目录。需要注意的是NFS的版本和服务端配置要做好否则Pod挂载时会报参数错误。4. 开发测试必须的扩展存储、Dashboard与监控4.1 本地存储方案local-path-provisioner的离线部署这个组件特别适合测试环境。部署方式非常简单只需要一个yaml文件加上一个镜像。把local-path-provisioner的yaml下载下来修改其中的镜像地址为Harbor对应地址然后apply。本地存储方案的处理流程是这样的它会创建一个默认的StorageClass名字叫local-path。当有PVC创建时provisioner就会在节点上的指定目录默认是/opt/local-path-provisioner下创建一块目录然后通过hostPath方式挂载给Pod。对开发测试来说用这种方式部署一个单实例的数据库来做验证非常方便。有一点要提前提醒如果你把某个有状态应用的Pod调度到了node01当Pod被删掉并重建时新Pod可能会被调度到node02而node02上没有原来那份数据。所以local-path这种方案只适合测试不适合需要数据稳定的场景。要么给数据库类应用绑定固定节点要么直接上NFS。4.2 让开发更方便Dashboard与K9sDashboard装好之后默认账号有kubernetes-dashboard这个ServiceAccount需要给它绑定一个cluster-admin的ClusterRole才能看到集群内容。可以创建一个admin-user并生成token然后用token方式登录Dashboard。除了Dashboard我还建议在本地电脑上装一个k9s。它是一个终端版的K8s管理界面支持键盘快捷键快速切换namespace、查看Pod日志、进入容器、编辑资源定义。开发测试环境里我大多数时候是kubectl搭k9s一起用K9s用于日常巡检和查看状态kubectl用于写脚本和批量操作。4.3 监控告警先上metrics-server再加kube-prometheus开发测试环境的监控需求不用一开始就铺很大。先用metrics-server把资源使用情况可视化出来让kubectl top能工作大部分情况下就够用了。如果后面想进一步做告警和可视化大盘再考虑kube-prometheus-stack它把Prometheus、Grafana、Alertmanager打包在一起还带了一组开箱即用的告警规则。它的缺点是镜像数量非常多离线部署时要把几十个镜像全部拉到Harbor比较麻烦。我的建议是先把metrics-server用起来确认有明确需求后再上全套监控别一上来就把自己卷进镜像搬运的大坑里。4.4 用Helm简化应用部署在开发测试环境里Helm的价值更多体现在“用Chart快速部署一套有依赖的应用”比如WordPress加MySQL或者Prometheus全家桶。离线环境下使用Helm只要提前把需要的Chart打包成tgz文件传到内网之后用helm install本地文件安装即可。Helm二进制本身就是一个可执行文件没有外部依赖下载后放到/usr/local/bin就能用。Chart包可以从Artifact Hub下载注意下载时带上依赖避免安装时提示缺依赖项。5. 高频排错实录与避坑心得5.1 我实际遇到的几个典型问题这次离线部署从头到尾大概花了两个工作日中间遇到不少问题。挑几个有代表性的记录下来。问题1kubeadm init报“Initial timeout of 40s passed”这个错误一般出现在初始化时kubelet起不起来原因通常是系统配置未完全到位。我用journalctl -u kubelet查看日志发现里面有很多关于cgroup的报错。排查思路是确认SystemdCgroup是否已改为true确认没有其他容器运行时残留在节点上确认kubelet的cgroup driver与containerd一致。这里有个快速验证命令kubeadm init前先执行crictl info查看containerd的runtime信息是否符合预期。问题2节点一直NotReadyFlannel Pod反复重启Flannel Pod重启多半是网络插件与Pod CIDR冲突或者Flannel的配置无法找到子网。我的排查方法是kubectl logs -n kube-flannel pod-name查看Flannel日志确认初始化时是否加了--pod-network-cidr10.244.0.0/16如果使用的是Calico还要查看IP池配置是否与Pod CIDR一致。还有一次是Flannel的镜像tag拉取错误导致Pod镜像拉取失败。这种问题通常在日志里表现为ImagePullBackOff检查一下镜像地址是不是指向Harbor的正确项目就行。问题3Service ClusterIP ping不通但DNS正常这个现象最迷惑人。后来发现是net.bridge.bridge-nf-call-iptables没开启导致宿主机iptables的规则没有作用于桥接流量所以Service的访问被绕过表现为Pod之间可以通信但Service无法转发。解决方法是开启内核参数重启节点或者执行sysctl --system即可。问题4token过期工作节点无法加入开发测试环境经常要隔几天再加节点此时你会发现之前记录的kubeadm join命令已经失效token过期了。重新创建一个kubeadm token create --print-join-command它会生成一条新的join命令直接执行即可。注意新token也默认是24小时有效期。5.2 关于离线包的版本管理离线部署最怕的就是版本不一致尤其是kubeadm、kubelet、kubectl这三个组件必须严格保持相同版本否则集群会报version skew问题。我的习惯是每次做的离线包在包内附带一个manifest.txt把里面每个镜像、每个二进制文件的版本和来源写清楚比如containerd: 1.7.13 kubeadm/kubelet/kubectl: 1.28.2 flannel: v0.24.0 pause: 3.9 etcd: 3.5.9-0这样下次再建环境时直接照着清单准备不用重新查版本兼容性。5.3 其他值得注意的实操经验中转机建议用一台有图形界面的机器后面如果要调试Harbor、查看镜像列表会方便很多containerd默认没有docker命令但ctr和crictl都能操作容器习惯用docker的人可能会一开始觉得别扭多试几次就好了开发测试环境尽量保持跟生产环境大版本一致比如你生产上准备用1.30测试环境就别贪新用1.31版本差异带来的配置差异有时候非常隐晦在离线环境里官方文档不好直接访问建议提前把K8s和containerd的离线文档、yaml模板下载到本地有备无患所有节点的时间必须保持同步否则etcd会因时间偏差导致leader选举异常。离线环境一般没有外网NTP建议内网搭一个chrony服务器指向一些或多个可用的上游时间源。这套离线部署方案我后来又帮团队在其他两套环境里复现过整体稳定。如果你按照这个流程走应该能避开大部分我踩过的坑。部署过程中如果遇到其他问题建议先把kubelet日志和containerd日志拉出来看大部分问题都会自己暴露身份。离线部署的魅力就在于此一切都在掌控之中只要把你需要的那份镜像和二进制准备齐了剩下的就是按步骤执行。希望这份实操记录能帮你顺利把Kubernetes环境在离线环境下跑起来。