1. 为什么离线部署会让人头大:一次内网交付场景的复盘
前一阵子接了个交付任务,环境是一个完全隔离的内网,机器有了、K8s的规划也有了,但唯一的问题是没有外网。也就是说,Docker、Harbor、Rancher、Jenkins这些平时一条docker pull就能搞定的东西,在这里全都要靠离线包手工搬运。尤其是Rancher这种容器管理平台,它不光自己是一堆镜像,启动之后还要负责拉取K8s组件镜像、Agent镜像、各种系统Chart镜像,节点加入时还要反向找它要镜像,链条非常长,任何一环没接上,后面就全是ImagePullBackOff。
做完这一轮部署之后,我最大的感受是:离线部署最难的并不是某个组件的安装命令有多复杂,而是整个链路里每个镜像从哪来、先装谁、后装谁、证书信不信任、仓库地址能不能被每一台节点访问。这些问题如果不在动手之前排好顺序,很容易陷入“装好Harbor却推不进去镜像”“Rancher起来了但Agent一直连不上”的煎熬状态。
这篇文章就是想把我的完整部署思路拆开来讲:从离线物料准备,到Harbor仓库搭建,再到Rancher Server启动、节点纳管、Jenkins接入,最后附上我实际踩过的几个坑和排查命令。适合正在做内网交付、想用Rancher管K8s集群、同时又需要Jenkins做CI/CD流水线的同学参考。如果你之前只做过在线环境的docker run一条龙,这篇更值得看完,因为离线环境会逼你把每个环节的原理都弄明白。
2. 动手之前先把版本和物料清单锁死,否则后面寸步难行
离线部署最忌讳的就是“装到一半发现版本不匹配”。在线环境大不了重新拉一下镜像,内网环境改一个版本号,可能就要重新导入好几个G的镜像包,连带着Harbor里的镜像也要重新推一遍。所以我习惯的做法是,在刚开始接触这个项目时,先花半天时间把版本清单完整列出来,让团队所有人都照着一份清单准备物料。
2.1 我选的这套版本组合
| 组件 | 版本参考 | 选择理由 |
|---|---|---|
| Docker | 20.10.x | 兼容性好,Harbor和Rancher社区验证都比较充分,不追求新版 |
| Harbor | 2.8.x | 离线安装包体积可控,功能稳定,支持Trivy漏洞扫描和ChartMuseum |
| Rancher | 2.7.x | 对RKE1/RKE2都有完整支持,离线包体系成熟,导入Agent流程顺畅 |
| Jenkins | 2.4xx LTS | LTS版本插件兼容性更稳,JDK17版本配合新版插件体系 |
| 操作系统 | CentOS 7.9 / Kylin V10 类 | 内网交付最常见的系统,注意内核和glibc版本 |
为什么特别强调版本要“锁死”?因为Rancher在离线模式下有一个特点:它会通过一个叫rancher-images.txt的文件列出所有需要导入的镜像,你将镜像保存成离线包之后,后续的rancher-load-images.sh脚本会按照这个清单往本地Docker或私有仓库里导。如果你在另一台机器上用了不同小版本的Rancher,清单内容就变了,导入后缺三四个镜像,看起来没问题,实际上创建集群时节点Agent或者K8s组件镜像根本找不到。
2.2 离线物料到底要准备哪些东西
我的做法是把离线物料分成三类:安装包、镜像包、配置文件。
安装包指的是Harbor离线安装包、Docker RPM包或二进制包、docker-compose插件、helm工具包。这部分不要遗漏,因为离线环境没有yum源,底层依赖比如yum-utils、device-mapper-persistent-data、lvm2可能也要一并准备好。工作节点上如果连tar的某些扩展版本都没有,导入大镜像时还会遇到不常见的问题。
镜像包是重头戏。Rancher官方有rancher-save-images.sh脚本,在线环境跑一遍会把Rancher Server、Agent、系统Chart等镜像全部save成一个tar包。我要强调一个细节:很多人以为只要导入rancher/rancher这一个镜像就够了,实际上Rancher的镜像包里还包含了rancher/rancher-agent、rancher/mirrored-pause、rancher/mirrored-coredns-coredns这些基础组件镜像。如果导入不全,后续创建集群的时候节点加入就会卡住。
我还会额外准备这些镜像:Harbor运行需要的goharbor/redis、goharbor/registry、goharbor/nginx等,Jenkins运行需要的jenkins/jenkins,以及构建流水线可能用到的工具镜像,比如docker客户端镜像、kubectl镜像。把这些提前push到Harbor,后面Jenkins写Pipeline时才知道从哪拉镜像。
2.3 配置清单和网络规划
我一般在规划阶段就会把Harbor的访问地址定下来,例如registry.xxx.local,并且要求所有节点都能通过这个地址访问Harbor。这一步非常关键。曾经遇到过一种情况,Harbor部署好了,Rancher Server自己也能正常登录,但工作节点在加入K8s集群时镜像拉取失败,排查到最后才发现是工作节点访问Harbor走了另外一段网络,域名解析出来的IP不对。
所以建议在动手前就把Harbor地址、Rancher Server地址、Jenkins地址都写进一个网络规划表,然后每一台节点都先测试连通性。最朴素的验证办法就是在每个节点上执行:
curl -v http://registry.xxx.local/v2/如果返回了一个JSON响应,说明网络通路是正常的。如果超时或者证书报错,那就要先解决网络和证书问题,再继续往下装组件。
3. 第一块拼图:Harbor离线仓库的安装与自签证书处理
整个离线方案里,Harbor是绝对的中枢。后续Rancher的镜像、Jenkins的镜像、业务应用的镜像全都要经过它来分发。所以我习惯把Harbor当成“基础设施中的基础设施”来对待,先把它装牢,再往上盖。
3.1 Harbor离线安装流程
在离线环境里装Harbor,核心就是拿到harbor-offline-installer-v2.8.6.tgz这个安装包,传到目标机器上解压。解压后会看到一个harbor.yml和一个install.sh脚本。安装前我先做两件事:第一件,确认当前机器的Docker和Docker Compose是可用的,因为Harbor底层就是一堆容器;第二件,把harbor.yml里的关键配置改好,比如hostname改成规划好的域名地址。
我的harbor.yml典型配置长这样:
hostname: registry.xxx.local http: port: 80 https: port: 443 certificate: /data/cert/harbor.pem private_key: /data/cert/harbor.key harbor_admin_password: ChangeMe123 database: password: root123 data_volume: /data/harbor这里要提一个经常有人问的点:能不能只用HTTP不用HTTPS?如果是在纯内网环境,且所有节点都通过insecure-registries信任了Harbor地址,确实可以只用HTTP。但风险在于Docker默认会对HTTP仓库有安全限制,而Rancher在下发镜像拉取指令时,并不会自动帮你处理insecure-registries的配置。也就是说你需要在每一台节点的Docker配置里都写上"insecure-registries": ["registry.xxx.local"]。
这么做的维护成本很高,所以我更推荐一次性配置HTTPS。自签证书也没关系,只要把CA证书分发给每一台节点,并且让Docker信任这个CA,Rancher、Harbor、Jenkins之间的链路就会顺畅很多。
配置好harbor.yml后,直接执行:
./install.sh --with-trivy--with-trivy参数会额外启动漏洞扫描组件。离线环境下Trivy的漏洞数据库可能无法更新,但扫描功能仍然可以使用已有的数据库,如果不需要漏洞扫描,可以去掉这个参数,减少资源占用。
3.2 Harbor初始化:项目、用户、机器人账户
Harbor装起来之后,我第一件事不是立刻传镜像,而是先把项目目录建好。一般我会建两个项目:library和app。library用来放Rancher系统组件镜像、Jenkins基础镜像这类平台级镜像;app用来放业务应用镜像。这样在Harbor页面上看起来清爽,权限控制也好做。
然后我会创建一个专用的推送账号,而不是用管理员账号跑来跑去。原因很简单:管理员账号如果泄漏,整个镜像仓库就失控了。我通常会创建一个用户deploy并赋予app和library项目的推送权限,后续Jenkins登录Harbor推镜像时,就用这个账号。创建一个只读账号给Rancher节点拉取镜像用,也能避免有人误删镜像。
创建机器人账号有一个好处是Harbor会给你一个带极长密码的token,这个token可以直接写进流水线环境变量里,不用暴露人类用户的密码。我在Jenkins里配置Harbor凭据时就偏好用机器人账号。
3.3 向Harbor导入第一批镜像
拿到之前准备好的镜像tar包后,在能访问Harbor的机器上,先docker load导入离线包,再docker tag成Harbor地址的格式,最后docker push上去。以Rancher的镜像为例:
docker load -i rancher-images.tar.gz docker tag rancher/rancher:v2.7.5 registry.xxx.local/library/rancher:v2.7.5 docker login registry.xxx.local -u deploy docker push registry.xxx.local/library/rancher:v2.7.5这套动作看起来很简单,但镜像一多,手动执行就会出错。我的习惯是把所有镜像的tag映射关系写到一个脚本里,一次性批量处理。脚本大概是这样的逻辑:读取rancher-images.txt,把每一行镜像名前面的docker.io/等前缀替换成registry.xxx.local/,然后自动执行tag和push。
至此,离线环境的镜像源头已经就位了。但先别急着推进,还有一个容易被忽略的点:Harbor的数据卷备份。我会在Harbor运行稳定后进行第一次完整备份,备份内容包括/data/harbor/database和/data/harbor/registry目录。后面万一操作失误把镜像推错了或者数据库出问题,至少能回滚。
4. Rancher Server上手指南:导镜像、起容器、纳管节点
Harbor就绪后,Rancher Server的部署就有了稳定的镜像来源。但Rancher Server本身有两种部署方式,一种是在已有K8s集群上用Helm部署,另一种是用docker run直接启动单节点。在离线小环境里,我更推荐先用docker run把Rancher Server跑起来,等集群做好之后再考虑迁移到高可用部署。
4.1 启动Rancher Server时要顺手把默认仓库指到Harbor
这是离线部署中最容易翻车的一个地方。Rancher Server启动后不仅要运行自己,还要在各种系统组件需要镜像时去拉取。如果你不告诉它去哪个仓库拉,它默认会去公共镜像仓库。内网环境下这一拉就超时,所有依赖镜像都会卡住。
所以启动命令里一定要带上环境变量。以Rancher 2.7.x为例:
docker run -d \ --name rancher-server \ --restart=unless-stopped \ -p 8443:443 \ -p 8180:80 \ -e CATTLE_SYSTEM_DEFAULT_REGISTRY=registry.xxx.local \ -v /data/rancher:/var/lib/rancher \ registry.xxx.local/library/rancher:v2.7.5这里的CATTLE_SYSTEM_DEFAULT_REGISTRY就是告诉Rancher,后续所有的系统镜像都从registry.xxx.local拉。如果你的Harbor项目结构不是library,比如是rancher项目,对应的值就要改成registry.xxx.local/rancher。这里有一个关键点:镜像路径必须和Harbor里的项目路径完全对得上,差一个层级都会拉不下来。
启动后,访问https://IP:8443,Rancher会引导你设置管理员密码。密码设置完成后不要急着创建集群,先到全局配置里确认默认仓库地址已经生效,并手动触发一次系统组件的部署,观察有没有镜像拉取失败。
4.2 创建自定义集群:节点的加入与Agent镜像分发
Rancher支持导入已有集群,也支持从零创建自定义集群。离线环境下,我习惯用“自定义集群”的方式来做,因为这种方式最贴合内网交付的实际情境。
操作路径大概是:进入Rancher UI,选择“创建集群”→“自定义集群”,填写集群名称,并选择K8s版本。这里要注意,Rancher列出的K8s版本不是所有都适用于你的离线环境,具体哪个版本能正常使用,得看你导入离线包时K8s组件镜像是否完整。通常我选用的都是离线包里自带的那几个默认版本,而不是Rancher UI上显示的最新版。
选择完成后,页面会给出一个加入节点的命令,大致长这样:
docker run -d --privileged \ --restart=unless-stopped \ --net=host \ -v /etc/kubernetes:/etc/kubernetes \ -v /var/run:/var/run \ registry.xxx.local/library/rancher-agent:v2.7.5 \ --server https://rancher.xxx.local:8443 \ --token xxxxxxxxx \ --etcd --controlplane --worker如果前面的CATTLE_SYSTEM_DEFAULT_REGISTRY没有配置,那这条命令里的rancher-agent镜像地址就会变成公共仓库地址,节点一执行就卡死。所以再次强调默认仓库一定是最先配好的。
节点加入时,我会把角色拆开执行。控制面节点先跑,等etcd和controlplane状态正常后,再添加worker节点。这样可以更清晰地判断问题出在哪个环节。如果worker节点出现rancher-agent容器反复重启,我的第一反应是看节点能否访问Rancher Server的8443端口,以及Harbor是否可达。
4.3 配置Kubeconfig和访问权限
集群创建成功之后,需要下载Kubeconfig文件。这个文件后续要给Jenkins用,也需要给开发人员用。我的做法是单独创建一个只读或指定命名空间的Kubeconfig,而不是直接把管理员Kubeconfig发给所有人。具体可以在Rancher UI的“用户”里建一个用户,然后通过“集群”页面的“成员”给这个用户分配权限,再用这个用户去生成Kubeconfig。
这一步看着不起眼,但经历过“Jenkins里的部署权限过大,意外删了整个命名空间”的教训之后,我每次都会坚持最小权限原则。
5. Jenkins落到离线K8s环境:动态Agent与Harbor联动
Rancher把K8s集群管起来之后,整个环境就有了比较好的基础能力。接下来就该把Jenkins接进来,形成“代码提交→构建镜像→推Harbor→更新K8s工作负载”的自动化闭环。
5.1 在Rancher的命名空间里部署Jenkins
我通常会在Rancher UI里单独创建一个cicd命名空间,然后通过自定义工作负载部署Jenkins。镜像路径使用Harbor地址:
registry.xxx.local/library/jenkins:2.440.3-lts-jdk17创建Deployment时,除了常规的端口配置,我特别关注两个东西:第一是持久化存储,第二是挂载Docker Socket。
持久化存储方面,如果离线环境里有现成的共享存储,直接使用PVC;如果没有,则使用节点上的hostPath目录,比如/data/jenkins_home。后者简单直接,但注意不要让Jenkins的Pod被调度到没有/data/jenkins_home目录的节点上。
挂载Docker Socket的目的是让Jenkins可以调用宿主机Docker来构建镜像。这是最常见的DooD模式,在Pod里挂载/var/run/docker.sock:
volumeMounts: - name: docker-sock mountPath: /var/run/docker.sock volumes: - name: docker-sock hostPath: path: /var/run/docker.sock这样Jenkins在构建时执行docker build,实际上跑在宿主机Docker上,构建出的镜像再push到Harbor。这种方式实现简单,内网环境非常实用。缺点是安全隔离较弱,如果Jenkins被攻破,宿主机Docker权限也就暴露了。如果对安全要求更高,可以考虑用Kaniko或BuildKit,但那需要额外准备离线镜像和调试构建方式,我一般默认先用DooD跑通。
5.2 插件与Update Center的离线处理
离线环境里,Jenkins插件是一个绕不开的大坑。在线安装插件时可以一键选择推荐插件,但离线环境没有这个便利。我通常会在准备阶段就根据实际需求把插件包下载好,常见的有:kubernetes、docker-workflow、git、pipeline-utility-steps、credentials-binding、harbor相关插件等。
插件包是.hpi或.jpi文件,放置路径是$JENKINS_HOME/plugins。我习惯把插件目录整包做好模板,在Jenkins第一次启动前就放到持久化目录里。这样Jenkins首次启动完成后,插件已经全部就绪。
有一个细节是Jenkins默认的更新中心会尝试访问外网。如果一直不配置,每次进入插件管理页面都会报错,而且启动时也会频繁重试。我的做法是在“插件管理→高级”里把Update Site地址改成http://localhost或者干脆清空,避免它一直做无效的联网尝试。如果后续需要补插件,就把插件包直接传到/data/jenkins_home/plugins目录,然后重启Jenkins进程。
5.3 配置Kubernetes Cloud动态Agent
接下来这一步能让流水线在K8s里动态创建Pod来执行任务。点击“Manage Jenkins → Clouds → New Cloud”,类型选择Kubernetes,然后把Rancher生成的Kubeconfig内容填进去。
填Kubeconfig时,需要把集群地址从Rancher的https://rancher.xxx.local:8443改写成K8s API Server的真实地址。一个常见误区是Jenkins Pod在集群内部时,用它访问Rancher的8443端口,中间经过了好几层转发,速度慢还容易超时。更稳妥的做法是在Kubeconfig中填写集群内部API地址,比如https://10.0.0.10:6443,这样动态Agent创建Pod的请求可以直接到达API Server。
配置好Cloud之后,在流水线里这样使用:
pipeline { agent { kubernetes { label 'jenkins-agent' yaml ''' apiVersion: v1 kind: Pod spec: containers: - name: agent-docker image: registry.xxx.local/library/jenkins-agent:latest command: ["/bin/sh", "-c", "tail -f /dev/null"] volumeMounts: - mountPath: /var/run/docker.sock name: docker-sock volumes: - name: docker-sock hostPath: path: /var/run/docker.sock ''' } } stages { stage('Build Image') { steps { container('agent-docker') { sh ''' docker login registry.xxx.local -u deploy -p $HARBOR_PASSWORD docker build -t registry.xxx.local/app/my-app:${BUILD_NUMBER} . docker push registry.xxx.local/app/my-app:${BUILD_NUMBER} ''' } } } } }5.4 凭据管理与流水线示例
我在流水线里不会把Harbor密码明文写在脚本中。Jenkins的“凭据”功能里添加一个Username with password类型的凭据,专门存Harbor机器人账号的密码。然后在流水线里通过withCredentials引用:
withCredentials([usernamePassword( credentialsId: 'harbor-deploy', usernameVariable: 'HARBOR_USER', passwordVariable: 'HARBOR_PASSWORD' )]) { sh 'docker login registry.xxx.local -u "$HARBOR_USER" -p "$HARBOR_PASSWORD"' }镜像构建完成后,还要部署到K8s。这一步可以用kubectl set image,也可以使用Helm。我在离线场景下更推荐kubectl set image,因为Helm需要在集群里预装Tiller或使用Helm v3的客户端,离线环境还得费一番工夫准备Helm镜像。用kubectl则只需在Jenkins Agent Pod里预置一个kubectl二进制,并把Rancher生成的Kubeconfig放进去。
6. 我经常碰到的几个翻车点:排查思路与修复命令
离线环境排障比在线环境痛苦得多,因为很多报错看起来是网络问题,实际却是证书、镜像路径、甚至磁盘空间的问题。下面这几个是我遇到频率比较高的,基本每一种都花了大半天才定位。
6.1 ImagePullBackOff,但明明镜像已经推送到Harbor了
有一次节点加入集群时,rancher-agent反复处于ImagePullBackOff。我第一反应是检查节点能否访问Harbor,用curl测通了,Docker也能登录。后来发现是tag不一致。Rancher从默认仓库拉取镜像时,拼接出来的路径是registry.xxx.local/library/rancher-agent:v2.7.5,但我当时的Harbor项目叫platform,路径是registry.xxx.local/platform/rancher-agent,所以永远拉不到。
这类问题排查时,先看Pod事件里的镜像完整路径,再和Harbor里的实际镜像列表比对,就会发现是不是项目层级或者版本号写错了。命令行快速查事件的方式是:
kubectl -n cattle-system describe pod rancher-agent-xxxxx kubectl -n cattle-system get events --sort-by='.lastTimestamp'6.2 x509证书错误的连环发作
Harbor配置成HTTPS并做了自签证书后,如果节点没有信任该CA,Docker拉镜像时会出现x509: certificate signed by unknown authority。这个问题不只是Docker层面,Rancher内部组件之间也可能因为证书信任问题而互相报错。
我的做法是准备一个证书分发脚本,在所有节点上执行:
mkdir -p /etc/docker/certs.d/registry.xxx.local cp harbor-ca.crt /etc/docker/certs.d/registry.xxx.local/ca.crt systemctl restart docker注意,还需要把CA证书追加到系统证书里:
cp harbor-ca.crt /etc/pki/ca-trust/source/anchors/ update-ca-trust extract这样Docker、containerd以及部分以系统方式调用CA证书的程序才能同时信任Harbor。别只看Docker重启成功就以为完事了,Rancher Agent、Kubelet内部调用镜像仓库时走的证书链也不一样,所以系统证书最好也一并进行更新。
6.3 Rancher Agent一直Disconnected
节点加入集群后,Rancher UI显示Agent状态为Disconnected。这个问题绝大多数时候是Agent不能反向连回Rancher Server的443端口。比如Rancher Server宿主机的防火墙只放行了8443,没放行443,或者节点访问Rancher Server走了不稳定的路由。
我一般会先检查Agent Pod日志:
kubectl -n cattle-system logs -f cattle-xxxxx如果日志里出现websocket: bad handshake,基本可以确定是节点访问Rancher Server的链路不稳定。处理方式就是检查防火墙策略,并确认节点的/etc/hosts里写的是Rancher Server的内网IP,而不是公网域名或不可达地址。
6.4 Harbor磁盘被镜像塞满
离线环境下,镜像包动辄几十个G,业务迭代一段时间后,Harbor的数据目录很可能被打满。现象是推送镜像时报错,但Harbor页面看起来还算正常。处理思路是先看磁盘剩余空间:
df -h /data然后到Harbor页面执行垃圾回收。但要注意,Harbor的垃圾回收并不是立刻释放磁盘空间,它只是删掉镜像文件系统中不再被引用的blob。执行GC后我还习惯执行一次:
cd /data/harbor/registry du -sh *确认空间真的释放了。如果磁盘还满,就需要考虑给/data目录扩容量,或者老镜像清理策略。
6.5 Jenkins Pipeline一直卡在创建Pod
Jenkins连接Kubernetes Cloud创建动态Agent时,如果一直卡在Pending,多半是Agent Pod需要的镜像在Harbor里没有,或者节点没有足够的资源调度。打开Jenkins的日志,会有类似Failed to schedule pod的信息。我的排查步骤是:先去查看cicd命名空间里有没有Jenkins生成的Pod,再describe看调度事件。
会遇到一种特殊情况,节点标签或污点导致Pod无法调度。我在Rancher创建自定义集群时,worker节点默认是没有特殊污点的,但如果你后续手动加了污点,Jenkins Pod又没有对应容忍,就会一直Pending。解决方式是在Jenkins的Pod模板里加上tolerations字段,允许调度到特定节点。
7. 一些踩坑之后沉淀下来的习惯
整套流程走完,我最大的感触是离线部署不是“照着在线文档抄一遍命令”这么简单。在线环境出了问题可以随时查文档、拉镜像试错,离线环境每一次试错成本都很高。所以我逐步养成了一些习惯,这些习惯后来帮我省了很多时间。
第一个习惯是物料清单里记录每个镜像包的大小和校验值。传输几十G的大文件时,丢包或截断并不罕见,如果导入时只报一个奇怪的解压错误,往往很难联想到是文件不完整。提前记录sha256sum,传输完先校验一遍,能节省大量排查时间。
第二个习惯是Harbor备份和Rancher备份并行做。Rancher本身有备份功能,可以在UI上配置定期备份到S3或者NFS存储。因为我这里用的是单节点docker run形式的Rancher Server,备份更是不可或缺。我会把Rancher Server的/data/rancher目录整体压缩备份,同时保留Harbor的数据目录快照。一旦升级或配置变更出现异常,直接回滚比重新折腾一遍快太多了。
第三个习惯是版本升级要克制。离线环境下,能不动版本就不动版本。哪怕上游发布了新版本,只要当前环境稳定,就先不升。真要升级,也要先在内网测试环境完全验证一次,再把离线包带到生产环境。Rancher升级尤其如此,它涉及CRD、系统Chart和Agent的联动升级,任何一步版本不匹配,整个集群的状态都可能变得不可预测。
最后还有一个实用技巧:把整个部署过程的关键命令写成一个部署手册,每一步执行完记录对应的验证命令。比如Harbor装完验证什么、Rancher启动完验证什么、集群创建完验证什么。这样过了一两个月再看这个环境,还能快速定位问题。离线部署不只是“装一次就结束”,后续的维护、扩容、新节点加入都需要这套文档作为支撑。
这条链路从Docker到Harbor,再到Rancher和Jenkins,看似是四个组件,实际上打通的是内网交付的一条完整通路。只要物料、版本、证书、网络这四个方面准备到位,整个流程跑下来是比较顺畅的。如果你最近也在做类似的离线部署,希望这篇文章能帮你少走一些弯路。