
如果你也在用Kubernetes八成会遇到一个绕不开的问题镜像从哪来我自己动手搭第一套kubeadm集群v1.26.0的时候最耽误时间的不是YAML写得不对、不是PV权限没挂上而是私有本地仓库的落地。集群里的每个节点都要从同一个仓库拉镜像光靠Docker Hub根本扛不住更别说业务镜像里还带着内网配置、密钥这些不能外传的东西。于是我把Harbor本地仓库从头到尾搭了一遍从CentOS 7上的证书准备、install.sh部署到K8s用imagePullSecret拉取镜像的完整链路中间踩过的坑、踩完之后想明白的原理今天一篇讲清楚。这篇文章对刚入门Kubernetes的人是个完整落地参考对已经跑通基础集群、正要补镜像分发这一环的人也适用。1. 为什么私有镜像仓库成了我搭Kubernetes最先做的事1.1 镜像从哪来Kubernetes运行时的第一性问题Kubernetes里有一个容易忽略的基本事实kubelet本身不会构建镜像也不会把镜像从一个节点复制到另一个节点。它只负责拿着Pod定义里的image字段去对应的镜像仓库拉取然后交给容器运行时containerd或docker创建容器。这意味着只要集群里有多个Worker节点同一个镜像就必须在每一个节点上都拉一遍。如果你只有一台机器能build镜像剩下的节点怎么办临时方案我也用过就是把镜像导出成tar包scp到每台机器上再用docker load或者ctr -nk8s.io images import导进去。单机三五个镜像的时候没事可一旦要滚动更新、水平扩容这套操作立刻变成噩梦你得记住每一台机器导到哪个版本了哪些节点漏了哪个节点还在跑旧镜像。这不是敏捷这是手工运维的灾难。所以私有仓库解决的不只是“网络能不能拉通”的问题它解决的是整个Kubernetes集群镜像分发的“第一性问题”让所有节点从一个可信、可达、权限可控的地址获取镜像。Kubernetes集群建好之后第一件要补的基础设施就是这个仓库。1.2 内网团队为什么必须有自己的分发中心可能有人会说我直接把镜像推到Docker Hub节点从Docker Hub拉不是更省事吗我一开始也这么干过然后很快就碰到了三个现实问题。第一是稳定性和限流。Docker Hub对匿名拉取有速率限制节点一多、拉取一频繁就时不时报pull rate limit exceeded。业务高峰期镜像拉不下来那感觉真是急到跺脚。第二是网络延迟和内网约束。很多公司的Kubernetes集群都在内网环境节点出公网本身就走受限通道每拉一个镜像都要绕一大圈慢不说还占带宽。第三是安全性。业务镜像里往往包含了服务配置、密钥、项目代号这类敏感信息推到公共仓库之后等于把这些信息交到了外部服务器手里这个分险绝对不能接受。私有镜像仓库给每个团队提供的是项目级隔离、访问控制和审计能力。不是所有人都能看全所有镜像不是所有节点都能推镜像哪些人在什么时间推送了什么镜像仓库层都能记下来。对多团队共享一个集群的场景这些能力不是锦上添花是底线。1.3 它还是CI/CD流程的“最后一段管道”镜像仓库在Kubernetes体系里的位置其实比很多人以为的更靠前。代码仓库是源头CI流水线负责构建产出物就是镜像镜像必须有一个确定地址可以推送。这个地址稳定了CD流程才能把新镜像通过Deployment、Helm或GitOps工具发布到集群里。回滚时候也简单把镜像tag切回上一版重新apply一次就行。Harbor天然支持Webhook、漏洞扫描和Tag保留策略这些能力都卡在“镜像进集群之前”的位置上相当于给交付链路上加了一道闸。所以我把搭建Harbor看成是Kubernetes基础设施的一部分而不是一个孤立的存储服务。2. 为什么选了Harbor而不是裸Registry或Nexus2.1 同场对比Registry、Harbor、Nexus各自适合什么在决定用Harbor之前我把市面上几个方案都试了一圈。结论是没有最好的只有最合适当前场景的。下面这个对比可以帮你快速定位。方案部署成本核心能力短板适合场景Docker Registry最低一个容器基础的push/pull无Web UI、无权限模型、无扫描复制个人单机测试、临时中转Harbor中等偏高组件多项目隔离、RBAC、机器人账号、漏洞扫描、跨域复制、Webhook、OIDC对接需要Docker和Compose占用资源偏高正式内网/生产环境、多团队共享集群Nexus 3中等支持Docker等几乎所有制品类型Docker镜像专属功能弱配置复杂公司已有统一制品库且不想再维护一套我之前在内网搭过一个裸Registry用是能用但一碰到“谁能拉、谁能推、谁误删了”这种问题就完全抓瞎。因为没有Web页面连查看镜像列表都得用API团队里其他人只能伸手找我要地址。Harbor一装上这些问题基本都消失了。它把一个镜像仓库变成了一个可以登录、可见、可管理的平台这才是内网协作需要的东西。2.2 Harbor组件架构拆解它为什么不是一个“大容器”Harbor给我的第一印象是“重”装完以后十几个容器在跑。但理解了它的组件结构之后就会发现每个容器都有明确的职责这一点在排错时尤其重要。Harbor的核心组件包括nginx入口网关负责HTTPS终结和流量分发、核心服务harbor-core处理API请求、认证鉴权、项目管理、镜像存储harbor-registry底层就是Docker开源的distribution、数据库harbor-db新版本使用PostgreSQL存储项目、用户、镜像元数据规则等、Redis缓存和Job队列、harbor-jobservice负责复制、GC、扫描这些后台任务、harbor-registryctl管理registry的配置和权限控制还有日志组件harbor-log和端口门户harbor-portal。加装Trivy组件之后还能做漏洞扫描。它们之间不是平行关系而是有调用链的客户端请求先到nginx再到corecore再根据请求类型去操作数据库或者调度registry。所以如果某个功能异常第一件事不是乱重启而是想清楚这条链断在哪一环。这也是我后面排查故障的主要思路。2.3 版本选择为什么要固定用离线安装包Harbor发布比较快我最终选的是v2.8.x系列的离线安装包。原因很简单离线包把所有依赖镜像都打包在一个tgz里内网环境不需要访问公网镜像源也能完整安装。在线安装包虽然体积小但安装过程中需要从Docker Hub拉镜像一旦网络抖动就是各种失败反反复复很折磨人。另外要注意版本兼容。Harbor v2.8要求Docker 20.10以上、Docker Compose V2.2.2以上如果你的Kubernetes集群用的是kubeadm v1.26.0节点上的容器运行时默认就是containerdHarbor本身并不关心底层是containerd还是docker它只负责对外提供registry服务。Harbor从v2.4开始数据库从MariaDB换成了PostgreSQL版本升级不能跳版本也就是说不要从v2.3直接升到v2.8中间大版本必须逐个过。这一点后面遇到升级需求时要特别小心。3. CentOS 7上完整部署Harbor从自签CA到install.sh跑通3.1 环境准备先把Docker和Compose调教好我用的机器是CentOS 7它自带的docker版本你可能想象不到还是1.13。这个版本太老了直接跑Harbor会有各种兼容问题而且Kubernetes v1.26早就把dockershim挪出去了。所以第一步肯定是升级到Docker CE同时安装docker-compose插件或二进制。yum remove -y docker docker-common docker-selinux docker-engine yum install -y yum-utils yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo yum install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin systemctl enable --now docker docker --version docker compose version如果你习惯用docker-compose这个单独命令也可以再装一个二进制文件放到/usr/local/bin/docker-compose并加执行权限。Harbor的install.sh在检测Compose时会兼容两种方式只要保证命令可用就行。装完之后有个细节容易被忽略CentOS 7默认只有Python 2.7而Harbor新版installer里的prepare脚本是Python 3写的直接执行./install.sh可能报prepare相关错误。我的建议是提前yum install -y python3再把默认的python命令软链到python3或者至少保证python3在PATH里。这一步不做后面够你排查半天的。3.2 生成一套自签CA和Harbor证书生产内网环境我强烈建议走HTTPS不要图省事直接开HTTP。因为你后面接Kubernetes时会发现所有节点都需要信任你这个仓库如果仓库本身只是HTTP那每个节点都要额外配置跳过TLS验证的规则节点一多这个配置散落各处早晚出问题。我自己用的是自签CA方案大致命令如下mkdir -p /data/certs cd /data/certs # 1. 生成根CA私钥并自签 openssl genrsa -out ca.key 4096 openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 \ -subj /CNLab Root CA -out ca.crt # 2. 生成Harbor服务器私钥和证书签发请求 openssl genrsa -out harbor.key 2048 openssl req -new -key harbor.key -subj /CNharbor.local -out harbor.csr # 3. 用SAN声明域名和IP让证书同时支持域名和IP访问 cat harbor.ext EOF subjectAltName DNS.1:harbor.local, IP.1:192.168.10.10 EOF openssl x509 -req -in harbor.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ -out harbor.crt -days 825 -sha256 -extfile harbor.ext这里两个细节值得说清楚。第一SAN扩展必须加如果只写CN而现在主流客户端都要求SAN里有匹配的域名或IP否则会报证书不匹配。第二CA证书生命周期可以给10年服务端证书我按行业习惯给了825天跟主流CA策略对齐。生成完证书之后把ca.crt放到系统信任链里这样本机所有程序都能信任这个仓库cp /data/certs/ca.crt /etc/pki/ca-trust/source/anchors/harbor-ca.crt update-ca-trust extract后面给其他节点分发CA时也要执行同样的操作。不过Kubernetes节点的容器运行时docker或containerd还有自己独立的证书目录光配系统信任链不一定够这个我放到第5章专门讲。3.3 修改harbor.yml这篇配置里的每个坑都值得看从Harbor官网Release页面下载离线安装包后把它解压到固定目录比如/opt/harbor。里面最重要的文件就是harbor.yml。如果环境里没有这个文件可以复制harbor.yml.tmpl改名为harbor.yml再改。cd /opt/harbor tar zxvf harbor-offline-installer-v2.8.5.tgz cd harbor cp harbor.yml.tmpl harbor.yml vim harbor.yml关键配置项如下hostname: harbor.local # 对外唯一访问入口不能写localhost http: port: 80 # HTTP端口如果不用可以注释 https: port: 443 # HTTPS端口 certificate: /data/certs/harbor.crt private_key: /data/certs/harbor.key harbor_admin_password: Harbor12345 data_volume: /data/harbor # 镜像和数据库的实际存储路径务必放到大分区 database: password: root1232024这里最坑的是hostname。这个字段不能写成127.0.0.1否则nginx入口不会监听你期望的域名。我统一用的是harbor.local这样一个内网假域名配合各节点的/etc/hosts解析到Harbor所在机器IP。这样做的好处是以后Harbor迁移IP只需要改hosts不用改所有image的tag。数据库密码和admin密码一定改成强密码尤其是harbor_admin_password因为安装完成后你是要用admin 这个密码登录Web界面的默认密码以Harbor12345为主线上必须改。3.4 执行install.sh并验证服务配置改好后直接执行安装脚本cd /opt/harbor/harbor ./install.sh这个脚本会做三件事生成所需的nginx和各类服务配置、把离线包内的镜像导入本地Docker、然后用Compose把全部服务编排起来。整个过程一般在几分钟左右。安装完成后先看容器状态docker ps正常你会看到一堆harbor-开头的容器nginx、core、jobservice、registry、registryctl、db、redis、portal、log再加上trivy的如果选了。接下来验证端口是否监听curl -v https://harbor.local/v2/然后打开浏览器访问https://harbor.local用admin账号登录一次。第一次登录后立刻把密码改掉然后就可以在Web界面创建项目了。此时本机docker login也应该能成功因为系统信任链和Docker证书目录都已经布置好docker login harbor.local -u admin输入密码看到Login Succeeded说明Harbor部署这一环已经通了。要是卡在这一步多半是证书目录没放对或者防火墙还没放行80/443这个在第6章有完整排查链路。4. 把Harbor接进KubernetesDNS、证书分发、imagePullSecret4.1 在Harbor中创建项目并推送第一个镜像Harbor里组织镜像靠项目Project。登录Web界面之后在“项目”页新建一个项目比如叫demo。项目创建后镜像在仓库里的完整名称就是harbor.local/demo/镜像名:tag。这一点很多人第一次用会懵以为镜像路径就是harbor.local/镜像名少了一层项目路径会导致推送失败或者权限报错。接下来先在Harbor服务器上准备好一个测试镜像并推送docker tag nginx:latest harbor.local/demo/nginx:v1.0.0 docker push harbor.local/demo/nginx:v1.0.0因为Docker客户端已经信任了harbor.local的证书并且已经执行过docker login所以推送能顺利通过。推送成功后在Harbor的Web界面就能看到这个镜像和它的tag点进去还能看到manifest信息。4.2 集群侧的第一次握手hosts解析与CA分发现在轮到Kubernetes集群了。不管你有几个Worker节点一律要执行两件基础操作。第一是让节点能解析harbor.local在每台节点的/etc/hosts里加一行192.168.10.10 harbor.local第二是让节点的容器运行时代理信任Harbor的CA。系统级信任链要加cp /data/certs/ca.crt /etc/pki/ca-trust/source/anchors/harbor-ca.crt update-ca-trust extract但这里有个关键点kubelet本身不直接跟Harbor通信负责跟Harbor通信的是容器运行时。你系统信任链配得再好运行时自己的证书目录没配一样报x509错误。所以下一步要看这个节点用的是docker还是containerd。用docker就在/etc/docker/certs.d/harbor.local:443/下放ca.crt用containerd就在/etc/containerd/certs.d/harbor.local:443/下放ca.crt放完分别重启docker或containerd。这部分细节我放到第5章展开因为你大概率会发现kubeadm v1.26默认用的其实是containerd而不是你以为的docker。4.3 用imagePullSecret完成Pod部署配置好节点侧的信任之后还要解决认证问题。Kubernetes的kubelet在拉取私有仓库镜像时并不知道你本机上执行过docker login它需要Pod显式引用一组镜像仓库凭据这就是imagePullSecret。创建凭据很简单kubectl create secret docker-registry harbor-registry \ --docker-serverharbor.local \ --docker-usernameadmin \ --docker-password你的密码 \ --namespacedefault这里有个极容易出错的地方--docker-server必须写harbor.local不能带https://也不能带/v2/这些路径。一旦带上协议前缀Pod拉镜像时去匹配的registry地址就不对了会一路报401。然后在Deployment的Pod模板里引用这个secretapiVersion: apps/v1 kind: Deployment metadata: name: demo-nginx namespace: default spec: replicas: 1 selector: matchLabels: app: demo-nginx template: metadata: labels: app: demo-nginx spec: imagePullSecrets: - name: harbor-registry containers: - name: demo-nginx image: harbor.local/demo/nginx:v1.0.0 ports: - containerPort: 80apply之后用kubectl get pods -w观察正常情况下Pod很快就会进入Running状态。如果卡在ImagePullBackOff一定要用kubectl describe pod xxx去看事件里的具体错误。是401还是x509是no basic auth credentials还是manifest unknown每一类错误对应的处理方法完全不同。后面第6章我会逐个拆。5. 容器运行时对接Harbor的两种姿势docker与containerd5.1 为什么先确认运行时再说配置接Kubernetes集群最搞笑的事就是你辛辛苦苦把docker的证书、daemon.json都配好了重启也重启了结果Pod还是拉不下来镜像。为什么因为从Kubernetes 1.24开始kubelet默认根本不再通过docker的socket拉镜像了dockershim已经被彻底移除。kubeadm在v1.26.0这个版本的预检阶段就会明确告诉你默认使用的容器运行时是containerd。如果你还在用docker这个命令的视角来思考K8s拉镜像就会陷入“我明明都配好了啊”的困惑。所以第一步永远是确认运行时crictl info看到runtimeType是io.containerd.runc.v2就说明节点走的是containerd。只有当你非常确定节点上装的是Docker并且kubelet还在通过docker的CRI适配器工作才需要考虑docker那一套配置。绝大多数2023年之后的集群你面对的基本都是containerd。5.2 docker运行时certs.d目录是最稳的方案如果节点确实走docker在我的实践里最稳的做法不是给daemon.json加insecure-registries而是在docker自己的证书目录里放CA文件。因为insecure-registries意味着跳过TLS校验等于把安全边界整个拆掉而放CA证书是真正建立了信任关系两者性质完全不同。目录结构是这样/etc/docker/certs.d/harbor.local:443/ca.crt注意目录名必须和镜像地址里的host:port完全一致。如果Harbor监听的是443目录就叫harbor.local:443如果监听的是8443目录名就得是harbor.local:8443。写错目录名Docker根本就不会加载这个CA然后报证书错误。放好之后重启dockersystemctl restart docker然后建议在节点上先手动验证一次docker login harbor.local以及crictl不能用于docker但docker pull harbor.local/demo/nginx:v1.0.0可以直接在节点上试。这条验证能过就说明docker这一层的通道没有问题了。5.3 containerd运行时/etc/containerd/certs.d是你的主战场对kubeadm v1.26.0的默认配置来说主战场是/etc/containerd/certs.d/。containerd从v1.6开始支持这套目录配置比老的config.toml里改mirror模式要直观得多。我推荐的最简方案是mkdir -p /etc/containerd/certs.d/harbor.local:443 cp /data/certs/ca.crt /etc/containerd/certs.d/harbor.local:443/ca.crt systemctl restart containerdcontainerd会自动扫描certs.d目录下每个registry子目录如果目录里有ca.crt就会用这个CA去验证对应的registry服务。这一招够用其他地方不用动。很多教程会顺手让你改config.toml里的[plugins.io.containerd.grpc.v1.cri.registry.mirrors]但如果你只有一个私有仓库根本没有必要。改得多反而容易出问题。改完之后在节点上用crictl直接验证拉取crictl pull harbor.local/demo/nginx:v1.0.0这条命令如果返回成功说明containerd到Harbor的TLS链路已经通了。后续再遇到Pod拉镜像失败就可以把问题范围缩小到secret、namespace或者yaml层面而不是反复折腾运行时了。6. 实战中必踩的四个坑HTTP、x509、401、磁盘GC6.1 报错http: server gave HTTP response to HTTPS client这个报错在第一次搭Harbor时简直人手一个。现象就是Docker客户端在push或pull时报错看起来像是服务端给了一个HTTP响应但客户端在用HTTPS请求。原理很简单Docker默认把所有registry都当成HTTPS来访问如果你的Harbor实际上只开了HTTPDocker发过去的HTTPS请求就会被HTTP服务拒绝或原样返回然后报出这么一句莫名其妙的话。解决方式有两种。第一种是去harbor.yml把https开起来这是我在第3章推荐的正路。第二种是如果你只想快点验证就在所有要用到这个仓库的节点的docker配置里加上insecure-registries{ insecure-registries: [harbor.local] }然后systemctl restart docker。这会同时解决两个问题让Docker允许对harbor.local使用HTTP也让Docker不再校验harbor.local的TLS证书。但记住这是给测试环境省事用的多节点生产环境千万别这么搞否则等于裸奔。而且配置了insecure-registries后docker如果再走HTTPS链接触发证书校验失败也会因为insecure配置而跳过验证表面上可能是“能用”但排查问题时反而更难定位。6.2 报错certificate signed by unknown authority这个错误说明TLS链路建立到了但客户端不信任Harbor的证书签发者。如果你的CA证书没在正确位置生效就会出现这个结果。我的排查思路是分层的先在节点上直接用curl访问Harbor看系统信任链是否生效。curl -v https://harbor.local/v2/ 21 | grep SSL certificate verify如果curl都说不认识那就是系统级CA没配好回去重新执行update-ca-trust extract。如果curl已经通了但crictl或docker还是报x509那问题就锁定在容器运行时自己的证书目录。docker看/etc/docker/certs.d/containerd看/etc/containerd/certs.d/。还有一种很低级的错误你复制CA的命令是对的但目录权限不对或文件叫ca.crt.bak运行时压根没读。检查文件名必须精确叫ca.crt不要画蛇添足加后缀。6.3 Pod事件里的401 UnauthorizedimagePullBackOff的报错里如果出现401或者unauthorized基本可以肯定是认证环节出了问题而不是网络或TLS。常见原因有三个secret不在Pod所在namespacesecret里的registry地址跟image里的地址对不上Harbor侧账号密码被改或该账号被禁。先说namespace。Kubernetes的secret是namespace级别的你得确认Pod所在namespace里有这个secret还确认Deployment的imagePullSecrets引用名正确。然后检查registry地址用下面这条命令可以把secret里的内容透出来看kubectl get secret harbor-registry -n default -o jsonpath{.data.\.dockerconfigjson} | base64 -d看到的内容是一个docker config JSON里面有registry地址和base64后的凭据。如果地址里多了https://跟image里的harbor.local不一致Pod拉镜像时肯定匹配不上。另外一个容易被忽视的点是如果Harbor侧创建了机器人账号Robot Account用于CI/CD机器人账号的权限集合里必须包含对应项目的“拉取”权限否则也会报401。机器人账号的好处是权限可裁剪、可单独撤销不会因为你离职或者密码改了导致整个集群拉不到镜像。6.4 磁盘GC与镜像保留策略这个坑不是第一次搭建就遇到而是跑一段时间后必然遇到的。现象是Harbor里删除了几个旧tagWeb界面看着已经没有了但磁盘占用却不下降甚至系统盘直接飘红。原因在于Docker Registry的存储结构删除tag只删除了manifest的引用镜像层数据实际还存放在磁盘上必须通过GC才能释放。Harbor提供了“垃圾回收”功能但如果你直接在Web界面点“立即清理”在线GC有时会保守处理磁盘释放不明显尤其是distribution使用filesystem存储时。我的做法是在低峰期停掉registry再清cd /opt/harbor/harbor docker compose stop registry # 在Harbor Web界面执行 垃圾回收 - 立即清理 docker compose start registry这套流程能比较彻底地释放磁盘空间。但更重要的不是事后清理而是事前控制。Harbor项目支持Tag保留规则可以设置保留最近多少天或最近多少个tag按项目自动清理旧版本。如果你的发布频率很高这个规则一定要配否则再大的数据盘都会被历史镜像塞满。还有一个经验是Harbor的数据目录不要放在系统盘至少搞一块独立数据盘挂到/data/harbor否则镜像一多系统盘满盘会导致整个节点的kubelet和containerd跟着遭殃。另外升级之前提醒一句Harbor的大版本升级不能跳级比如从v2.3升到v2.8中间要经过v2.4、v2.5直到v2.8每步都有数据库迁移逻辑在里面。升级前备份Harbor的PG数据库和注册表数据目录这个备份救过我一次就是因为没备份升级中途失败之后回滚才发现旧数据已经被迁移脚本改过了。最后补一条我自己的实操体会Harbor上线后尽量给每类应用固定一套命名规范比如项目名/应用名/环境名:版本并且在Deployment里直接把tag写成具体版本号而不是latest。用latest虽然方便但会掩盖“镜像没变但Pod重启了”的假象也会让回滚变得不可控。镜像tag一旦确定配合Harbor的Tag保留策略和Kubernetes的imagePullSecrets整套镜像流转就非常稳了。我搭完这套之后后续所有新环境都是先装Harbor再建集群顺序从来不变。