☰
Kubespray离线部署实践:内网环境K8s集群搭建全攻略
2026/10/15 6:00:59 网站建设 项目流程

Kubespray离线部署这个标题,我第一次看到的时候,心里其实挺嘀咕的——毕竟Kubespray本身设计上就是面向在线环境,Ansible从GitHub拉代码、从网上抓镜像,真要把它搬进一个完全断网的内网里,感觉到处都是坑。但我这几年在几个不同项目的隔离环境里(某政务系统的生产区、某制造企业的工业网段)反复折腾过,踩了不少坑,也把流程跑通了。这篇文章就把这套离线部署的完整方案、每一步为什么这么做、以及那些官方文档里根本不会写的细节都聊透,给准备在无外网环境里装K8s的运维朋友一个可抄作业的参考。

Kubespray是什么一句话说清楚:一套基于Ansible的Kubernetes集群一键部署工具,用Playbook把ETCD、控制平面、工作节点、负载均衡全部串起来。离线部署指的是在没有任何出口访问权限的隔离网络里,把这个过程完整复现。适合谁?适合企业内网、生产隔离区、以及那些对“软件供应链完整性”有硬性要求的环境,也适合不想手动敲kubeadm命令、希望整个集群配置可 Git 化、可回滚的团队。

1. 为什么偏偏选Kubespray做离线部署

1.1 离线部署的典型场景

先说清楚什么时候会遇见这种需求。很多生产环境是逻辑隔离甚至是物理隔离的,管理网段和数据网段之间不放行任何公网流量。我在某制造企业的项目实施时,目标集群整个网段连DNS都只能解析内网域名,更别提出网下载东西。这种情况下,Kubernetes集群本身又依赖一堆镜像(pause、etcd、coredns、kube-proxy等),没有提前准备,集群根本起不来。

还有一种场景,是等保或者行业规范要求软件物料清单(SBOM)可控,所有组件版本必须经过审核。你不能让一个生产节点在部署时自己跑出去拉最新镜像,万一拉到带安全问题的版本怎么办。所以需要把全链路都锁死在内网。分布式离线部署的核心矛盾,从“怎么装”变成了“怎么把外网的东西完整搬进来”。

1.2 为什么是Kubespray而不是裸kubeadm或国产发行版

我经常被人问:离线部署为什么不直接写kubeadm脚本?这个要看规模。三五台机器,你手动敲kubeadm确实没问题,但集群一上到几十个节点、要分master、node、etcd不同角色、还要考虑高可用,纯手写脚本的维护成本就上来了。

Kubespray的优势在于,它把所有组件版本、证书参数、网络插件配置、系统调优参数全部用清晰的变量抽离出来,节点多了也只是改Inventory文件的事。更关键的是,它支持“离线模式”的变量设计——你把镜像传到内网仓库、把包管理器指向本地源之后,剩下的逻辑跟线上部署几乎一致。这意味着你的部署流程可以在有网环境先测试通过,再用同一套代码走到离线环境,两边差异尽量缩小。

我这么说吧:Kubespray不是性能最好的部署工具,但它是“可预测性”最好的。所有步骤都是Ansible任务,任务执行有输出、有重试、有状态,出了问题你能直接定位到是哪个Task失败,这在内网排障时值千金。

2. 离线部署的物料准备和仓库规划

2.1 你需要搬运什么

一套K8s集群跑起来,需要的资源分三大类,我习惯叫“三件套”:

第一是二进制文件,包括etcd、kubelet、kubeadm、kubectl、containerd(或Docker)以及CNI插件二进制,这些在Kubespray里多数打包成tar.gz,存在downloads/目录里,也可以从宿主机本地目录继承。第二是容器镜像,从pause、etcd到coredns、coredns、Local Path Provisioner、Ingress Controller等,全部要push到内网镜像仓库。第三是系统级依赖包,比如yum源里的libselinux-python、iproute、conntrack-tools等,需要通过本地Yum/Apt仓库提供。

这三部分是离线化工作的核心。很多人搞完镜像就觉得完事了,结果部署到中间爆出某个系统包没装,又去挠头。我的经验是:动手之前,先把“想要部署的K8s版本”对应的“Kubespray版本”确定下来,再对应锁一批依赖清单,版本之间不要随意混用。

2.2 镜像仓库和软件源怎么搭

内网镜像仓库我用Harbor居多,主要原因是有镜像复制功能、有RBAC权限控制、在日志审计上更贴近企业需求。如果你内网环境很简单,Registry 2就够用,只要一个443端口就完事。软件源这块,CentOS/Rocky 8我用createrepo建yum源,Ubuntu则用apt-mirror或aptly。

搭建顺序有个讲究:先软件源,再镜像仓库,然后才是节点初始化。因为后面所有节点都要配置包管理器指向本地源,如果顺序反了,节点上缺基础依赖时就会卡住。软件源我建议覆盖这几类:base、extras、docker-ce、kubernetes镜像源(如果Kubespray从包管理器装kubelet的话)。

搭建软件源的具体操作,以Rocky 8为例,在有网机器上同步:

dnf reposync --repoid=baseos --repoid=appstream --download-metadata -p /data/yum dnf reposync --repoid=docker-ce-stable -p /data/yum createrepo /data/yum/baseos createrepo /data/yum/appstream createrepo /data/yum/docker-ce-stable

同步完后把这些目录拷贝到内网机器上,通过Nginx或HTTPD暴露,节点上把.repo文件的baseurl改成http://内网源IP/...。注意:不要直接在节点上测试外网连接,避免浪费时间。internal DNS解析一定要先行,否则内网源IP写在repo里,节点解析不了也是白搭。

2.3 镜像列表梳理和同步方法

镜像同步是最容易出错的地方。Kubespray的不同版本引用的镜像来源不同:有从docker.io直接拉的,有从registry.k8s.io(老的k8s.gcr.io)拉的,也有从quay.io拉的(比如Calico、Multus)。在国内容器环境里,即便有网,直接从这些官方仓库拉也可能较慢,所以你完全可以在有网机器上提前拉一遍,然后统一打tag打到内网Harbor。

我分享一个比较省力的办法:先去有网的环境中,用同样的Kubespray版本和变量跑一次部署,跑完后把目标节点上docker images(或者crictl images)列表导出来,这个列表就是最真实的离线镜像清单。然后写个循环脚本拉取并推送:

while read IMAGE; do LOCAL_NAME=$(echo $IMAGE | awk '{print $1}') LOCAL_TAG=$(echo $IMAGE | awk '{print $2}') docker pull ${LOCAL_NAME}:${LOCAL_TAG} docker tag ${LOCAL_NAME}:${LOCAL_TAG} harbor.internal.com/k8s-mirror/${LOCAL_TAG} docker push harbor.internal.com/k8s-mirror/${LOCAL_TAG} done < images.list

这里有个关键细节:Kubespray本身有变量可以让你重写镜像仓库的前缀。常用的是:

  • registry_override: 所有镜像的前缀替换,比如把docker.io/library/foo统一替换成harbor.internal.com/k8s-mirror/foo
  • kube_image_repo:用于控制平面组件的仓库地址
  • calico_image_repo、coredns_image_repo等插件级覆盖

我的经验是:尽量用registry_override做全局替换,再对个别不一致的插件用单独变量覆盖。全局替换的好处是,后续升级K8s小版本时你不用改一堆变量,插件默认名保持稳定就能直接换tag。

3. 完整实操:从Inhost文件到集群就绪

3.1 节点规划与Inventory文件写法

离线部署的第一步其实不是装软件,而是规划IP和角色。Kubespray要求至少一个Master节点,生产环境建议三Master、三ETCD、若干Worker,如果有VIP需求还要规划负载均衡节点。在离线环境下,我更推荐把ETCD和Master合设在同一节点,避免跨网段性能瓶颈。

Inventory文件在inventory/mycluster/hosts.yml里,大致结构:

all: hosts: master1: ip: 192.168.30.11 access_ip: 192.168.30.11 ansible_ssh_user: root ansible_ssh_pass: "内网弱密" worker1: ip: 192.168.30.12 access_ip: 192.168.30.12 ansible_ssh_user: root ansible_ssh_pass: "内网弱密" children: kube_control_plane: hosts: master1: etcd: hosts: master1: kube_node: hosts: worker1:

假设只有一台机器,也可以把控制平面和节点都挂上去,适合测试。这里踩过的坑是:如果集群节点有双网卡,一定要设access_ip,它用于Ansible SSH连接,不设的话可能在后续证书生成时报IP不匹配的错误。另外,SSH连接方式建议在内网场景直接用密码,不要依赖SSH agent,因为你在控制机上可能没有把公钥分发到每个节点——Ansible可以自动做这件事,但出于安全考虑,我更喜欢先用ssh-copy-id手工打通。

3.2 控制节点环境准备

控制节点(跑Ansible的机器)需要Python 3.8以上、pip、以及Kubespray依赖。在离线环境里这步最容易被忽略。你需要先在联网机器上准备好Kubespray的虚拟环境,再把整个目录传到内网。操作方式:

git checkout v2.20.0 python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt # 离线打包:把整个目录和.venv一起tar

传过去之后,内网机器上直接source .venv/bin/activate即可,不联网也不影响。但要注意,如果内网机器Python版本跟打包机器不一致,虚拟环境可能起不来。所以我强烈建议:用和节点相同操作系统的控制机来做环境打包,比如都是Rocky 8,都用Python 3.8。

另外,Ansible会在运行过程中生成缓存和执行文件,放在项目目录下,所以整个Kubespray目录必须在所有节点可访问的位置。局域网共享或NFS挂载都行,如果目录放在控制节点本地,则控制节点必须对所有目标节点免密可达。

3.3 预加载核心镜像与本地仓库配置

镜像仓库搭好后,接下来是修改inventory/mycluster/group_vars/k8s_cluster/k8s-cluster.yml和all.yml。核心变量如下:

container_manager: containerd kube_image_repo: harbor.internal.com/k8s-mirror kube_image_tag: v1.23.7 registry_override: docker.io: harbor.internal.com/k8s-mirror registry.k8s.io: harbor.internal.com/k8s-mirror quay.io: harbor.internal.com/k8s-mirror

如果你用的containerd,Kubespray会为每个节点生成containerd配置,让它默认走内网registry。此时你还需要配置containerd_registries_mirrors结构,让containerd在拉取时自动rewrite。参考写法:

containerd_registries_mirrors: - prefixes: - docker.io mirrors: - host: http://harbor.internal.com path: "k8s-mirror" skip_verify: false

这个配置比较新版本才支持,老版本建议直接在预加载阶段把所有镜像重打tag到内网仓库,然后让Kubespray不用拉镜像。执行完离线部署最怕什么?最怕节点在某个Task里突然发一个外网请求去拉镜像,由于没有外网,整个流程卡住。所以,预加载镜像这一步,我反复强调:把docker save出来的tar包直接在目标节点上ctr -n=k8s.io image import导入,这样至少核心镜像在内网节点本地是存在一份的,即便后续配置重写失败,也不会出现“基础组件完全起不来”的窘境。

3.4 关键系统参数和组件版本一致性

除了镜像,Kubespray还会在节点上安装一些系统级的包,比如conntrack-tools、ebtables、socat等。这些包如果走包管理器源,就直接依赖本地软件源。走软件源时,有一个必须确认的点是:源里面的包版本不能比Kubespray要求的低,否则某些模块编译或者依赖解析会失败。

我之前在一个内网环境里遇到过一个奇葩问题:节点系统自带yum源没有ipvsadm,而Kubespray默认开启IPVS模式的kube-proxy,这一步失败导致proxy起不来。后来我把内网软件源补全了ipvsadm,重新跑一遍原Task就好了。这种问题排查起来不复杂,但耗时,所以提前检查group_vars里每个扩展模块的依赖包,比事后补包省心得多。

还有kubelet、kubeadm、kubectl的二进制来源。如果离线环境完全没有外网,Kubespray默认从GitHub Releases下载这些二进制,这一步在国内一些网络环境里也会慢。解决方式:在有网机器上手动下载好这几个二进制文件,并在all.yml里配置:

kubelet_binary_url: "http://fileserver.internal.com/k8s-bin/kubelet" kubeadm_binary_url: "http://fileserver.internal.com/k8s-bin/kubeadm" kubectl_binary_url: "http://fileserver.internal.com/k8s-bin/kubectl"

这个文件服务器可以是你在内网起的一个Nginx目录,顺便也可以放etcd二进制。注意URL必须是HTTP可访问,不能是本地路径,因为Ansible会把节点上的临时文件下载到本地。如果是本地文件服务器,要保证每个节点都能访问到它。

3.5 执行部署和验证

物料全部备齐,就可以跑Ansible了。进入Kubespray目录,执行:

ansible-playbook -i inventory/mycluster/hosts.yml --become --become-user=root cluster.yml -vvv

整个Playbook执行时间根据节点数量和带宽差异很大,我见过最快20分钟(测试环境三节点),也见过一个多小时(生产环境十节点)。如果某个Task失败,Ansible默认继续执行完再报错,也可以加--step手动交互。离线环境下我更推荐一次跑完,然后把日志保存下来,因为重跑时有些Task是幂等的,重头来一次其实不太必要。

部署完以后,验证命令建议按下面顺序:

  • 控制节点上执行kubectl get nodes,看node是否Ready,如果NotReady则检查CNI镜像是否work。
  • 查看kube-system命名空间下的Pod,先看Pause容器是否都Running,然后看coredns是否Running,最后看网络插件Pod。
  • 如果集群状态正常,用kubectl run起一个busybox测试DNS和服务发现。

在线环境部署完一切顺利很正常,离线环境能一次跑通真的是烧高香。多数时候是某个镜像tag不一致导致Pod循环重启。遇到这种问题,先上节点执行crictl logs <container-id>,不要盲猜。

4. 常见问题与排查技巧实录

4.1 镜像拉取永远失败:究竟是仓库配置还是镜像没推全

离线部署最经典的问题就是Pod一直ImagePullBackOff。我在两台机器上遇到过一模一样的情况,一台是containerd根本连不上内网仓库,因为Kubespray配置文件里漏了containerd_registries_mirrors,另一台则非常简单——我推镜像时少推了一个multus的镜像。

排查顺序很重要,先把重点放在“到底是仓库地址对不对”还是“仓库里有没有这个镜像”。进入故障节点:

crictl pull harbor.internal.com/k8s-mirror/coredns:v1.9.3

如果这条命令在节点上能拉成功,说明仓库通信正常,那问题更多出在节点的kubelet配置可能被写过,或者某个namespace里的pod被其他镜像引用。如果这条命令拉取失败,直接看containerd的debug日志,路径在/var/log/containerd/,重点看报错里的HTTP状态码和DNS解析记录。如果harbor.internal.com解析不到,那就是DNS问题,优先修内网DNS。

另外一个隐藏点:仓库的HTTPS证书。生产环境很多是自签证书,你需要把证书放到每台节点上,要么配置skip_verify,要么统一注入到系统的 truststore。Kubespray有变量docker_registry_insecure老版本支持,新版本建议直接维护一份公用的证书分发任务。

4.2 Inventory配置错误引起的不明连接失败

Ansible连接Target时,最担心的是控制节点和目标节点的Python版本不一致。尤其内网环境如果控制节点是CentOS 7目标节点是Rocky 8,两个系统默认Python版本不同,Ansible走raw模块时可能出现异常。我的经验是:控制节点和目标节点尽量统一OS发行版,至少Python版本要统一。

还有个常见粗心点:多个节点共用一个IP,或者access_ip写错。Ansible在跑任务时会先发出一个ping检测,如果你看到某个Task卡在 “unreachable” 和 “success” 之间反复横跳,八成是IP写乱了。建议在跑正式集群前,先跑一个临时Playbook来验证所有节点连通性,里面有:

- hosts: all gather_facts: false tasks: - name: ping ping:

4.3 版本一致性:一个不小心就全乱

Kubespray对版本一致性要求很高。比如Kubelet版本和Kubeadm版本必须与K8s控制平面版本一致,否则在加入节点时会出现configuration errors。离线环境下,很多人为了方便,把所有镜像tag都设为latest,这是大忌。因为镜像仓库里的latest可变,而Kubespray生成的manifest写死的是一个具体版本,最后节点上拉下来的镜像和实际期望不符,直接导致集群组件不断重启。

在离线环境里,不要用latest,全部固定到具体版本号。另外注意etcd_version、coredns_version等内嵌变量,默认值随Kubespray版本变化,你在内网仓库推镜像时,最好把版本号从这些变量里读出来,再跟着推。

4.4 网络插件和内核参数的坑

CNI如果选Calico,而节点又开启了防火墙,经常出现BGP邻居起不来、跨节点网络不通。离线环境下防火墙策略可能比较严格,记得在目标节点上放行容器网段和Calico所需的端口。我之前在一个项目里,控制层面怎么都正常,但Pod与Pod之间间歇性超时,排查一圈下来,是节点上iptables的FORWARD链默认策略是DROP,导致跨主机网络被丢掉。改内核参数net.ipv4.ip_forward和net.bridge.bridge-nf-call-iptables=1后,立刻正常。

Kernel模块也很重要。Kubespray虽然会自动加载一些模块,但离线环境有些节点重启后没有自动加载,建议在/etc/modules-load.d/里固化加速。例如:

overlay br_netfilter

4.5 重跑任务:什么时候安全什么时候危险

Ansible任务大多是幂等的,不代表你可以无脑重跑。比如证书生成和节点初始化这些阶段,部分Task在重复执行时可能有副作用。我的经验是:第一次部署失败后,先分析失败的Task,如果是配置类问题(比如仓库地址写错),可以直接改完变量重跑整个Playbook;如果是服务起不来的问题,建议先手动清理掉状态不完整的服务,再用--limit限定到出问的节点重跑。

举个例子,kubelet一直启动失败,你直接重跑cluster.yml可能看到 “become” 权限检查失败等一堆噪音。我通常的做法是:

ansible-playbook -i inventory/mycluster/hosts.yml --become --limit worker1 cluster.yml

只跑worker1,避免其他节点被重复配置搞出额外问题。

4.6 摸索出的几个加速优化策略

离线部署的瓶颈通常在包分发和镜像导入上。如果你网络环境只有千兆,几十个节点拉镜像可能耗时较长。我后来采用的方案是:先在每台节点上把基础OS包装好(用无人值守脚本),然后把镜像tar包放在内网文件服务器上,节点开机后自动执行导入任务,这样并行速率远高于逐个执行Ansible任务。

另外,如果Kubespray版本支持,你可以配置download_localhost这个变量,让控制节点把所有资源下载好后统一分发,而不是每个节点去仓库拉取,效率提升也很明显。不过这功能依赖控制节点与所有节点之间的高速通道,视网络条件取舍。

还有一个关于部署日志的技巧:我会在跑ansible-playbook前先设置环境变量:

export ANSIBLE_LOG_PATH=~/ks-deploy-$(date +%Y%m%d-%H%M%S).log export ANSIBLE_DISPLAY_SKIPPED_HOSTS=false

这样即时空跑特别长时间,也能在事后快速找到唯一失败的Task,而不是刷屏的skipped信息。

5. 离线部署后的运维补充建议

集群起来以后,离线环境还有一些收尾工作,容易被忽视。

镜像仓库里的镜像不会自动更新,所以后续发版或升级时,你要保持一个“导出—传输—导入”的标准流程。我习惯把这套流程写成一个shell脚本,内网文档里每次都强调,防止负责同步的同学把镜像漏推。如果你有CICD,那么构建机的镜像推送目标直接指向内网Harbor,流程就顺了。

另一点是Kubeconfig的管理和分权。离线环境通常监管更严格,建议将所有kubeconfig通过内网文件服务器集中管理,并设置权限。Kubespray默认生成admin kubeconfig,权限过大,在生产环境最好把不同namespace的权限分拆给不同人员,避免一个kubeconfig文件泄露导致整个集群被控制。

日志和监控组件(比如prometheus、fluentd)的镜像也要在搭建时一并推入。很多人把核心集群部署完了以为就结束了,结果过几天装监控组件时又发现没镜像、没法拉取,非常被动。所以我在“物料准备”环节就把监控、日志采集、ingress等常用组件的镜像清单并进去。

我个人在实际操作中的体会是:Kubespray离线部署没什么高深的技术,本质上就是“把外网的东西提前搬到内网,再让Ansible透明地走内网源”。只要物料清单锁得死、版本不漂移,整个部署过程就是一套稳定的重复劳动。但真正值钱的往往是那些细节——比如镜像前缀的全局覆盖、DNS解析的前置验证、防火墙对容器网段放行。上面这些坑,我都实打实踩过,也是因此才想把这套方案完整写下来,希望后来的人能少花几个晚上在内网里对着Ansible报错发愁。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询