开源容器镜像仓库,提供镜像签名、扫描与多租户复制能力
Harbor 是 CNCF 毕业项目,在开源的 registry 之上补齐了企业真正需要的那一层:多租户项目、基于 RBAC 的权限、机器人账号、镜像漏洞扫描、镜像签名与跨实例复制。单纯的 registry 只有「存取」,而 Harbor 是「可运营的镜像平台」。
Harbor 与 Kubernetes 的关系可以概括成两句话:集群里的所有镜像几乎都从它拉取;它自身的所有组件又都跑在 Kubernetes 上。
kubelet 按 Pod 里的 image 字段向 Harbor 发起拉取,凭据来自 imagePullSecrets
Harbor 校验凭据后返回镜像 manifest 与各层 blob
containerd / CRI-O 接收并落盘,交给 kubelet 启动容器
Harbor 的核心组件:
harbor-core API 服务,权限、项目、Webhook 都在这
harbor-portal Web 界面
harbor-registry 真正的镜像存储,内部就是 distribution
harbor-jobservice 异步任务:复制、扫描、GC
harbor-db PostgreSQL,存元数据
harbor-redis 缓存与任务队列
trivy 漏洞扫描器(2.x 默认内置)
本页聚焦 Harbor 本体的安装与运维。底层 registry 的原理与手工搭建请看 registry 页。
生产环境一律用 Helm 安装:
helm repo add harbor https://helm.goharbor.io
helm repo update
helm install harbor harbor/harbor \
--namespace harbor --create-namespace \
--set externalURL=https://harbor.example.com \
--set expose.type=ingress \
--set expose.ingress.hosts.core=harbor.example.com \
--set expose.tls.enabled=true \
--set expose.tls.certSource=secret \
--set expose.tls.secret.secretName=harbor-tls
先落一份 values 文件再安装,后续升级会轻松很多:
helm show values harbor/harbor > harbor-values.yaml
# harbor-values.yaml(节选)
expose:
type: ingress
tls:
enabled: true
certSource: secret
secret:
secretName: harbor-tls
ingress:
className: nginx
hosts:
core: harbor.example.com
externalURL: https://harbor.example.com
persistence:
persistentVolumeClaim:
registry:
storageClass: ceph-rbd
size: 200Gi
helm install harbor harbor/harbor -n harbor --create-namespace -f harbor-values.yaml
externalURL 与 expose.tls 是 Harbor 安装里最容易出事、也最不能省的两项。
externalURL 格式必须是 protocol://domain[:port],不能带路径
Harbor 用它生成界面上展示的 docker push 命令
更重要的是,它是返回给 docker 客户端的 token 服务地址
如果 externalURL 写错(最常见的错误是写成了 Service 名 harbor-core.harbor.svc.cluster.local),你在集群外执行 docker push 时就会看到类似这样的报错:
Error response from daemon: Get "http://harbor-core.harbor.svc.cluster.local/v2/":
dial tcp: lookup harbor-core.harbor.svc.cluster.local: no such host
TLS 则是另一种经典故障。docker / containerd 默认只走 HTTPS,如果 Harbor 实际以 HTTP 暴露,你会得到:
Error response from daemon: Get "https://harbor.example.com/v2/":
http: server gave HTTP response to HTTPS client
这句话的字面意思是「服务器对 HTTPS 请求返回了 HTTP 响应」,根因是客户端在说 HTTPS,而服务端只会说 HTTP。正确的做法是给 Harbor 配上证书(expose.tls.enabled=true),而不是去改客户端的 insecure 配置。只有内网测试环境才考虑放行不安全仓库。
用自签证书时,集群的每个节点都必须信任 Harbor 的 CA,否则 kubelet 拉镜像会报 x509: certificate signed by unknown authority。
# containerd:集群节点走的是这条
sudo mkdir -p /etc/containerd/certs.d/harbor.example.com
sudo cp ca.crt /etc/containerd/certs.d/harbor.example.com/ca.crt
# docker 客户端(节点上跑 docker 时)
sudo mkdir -p /etc/docker/certs.d/harbor.example.com
sudo cp ca.crt /etc/docker/certs.d/harbor.example.com/ca.crt
# /etc/containerd/certs.d/harbor.example.com/hosts.toml
server = "https://harbor.example.com"
[host."https://harbor.example.com"]
capabilities = ["pull", "resolve"]
ca = "/etc/containerd/certs.d/harbor.example.com/ca.crt"
并确认 containerd 的 config_path 指向了这个目录:
[plugins.'io.containerd.cri.v1.images'.registry]
config_path = "/etc/containerd/certs.d"
# 方式一:创建 imagePullSecret 并在 Pod 中引用
kubectl create secret docker-registry harbor-cred \
--docker-server=harbor.example.com \
--docker-username='robot$ci+puller' \
--docker-password='******' \
-n default
# 方式二:挂到 ServiceAccount 上,该命名空间的 Pod 自动生效
kubectl patch serviceaccount default \
-p '{"imagePullSecrets":[{"name":"harbor-cred"}]}'
Pod 里的镜像地址要写完整路径,包含项目名:
image: harbor.example.com/library/nginx:1.27
仓库主机名必须与证书 CN/SAN 完全一致,而且不能用 IP 代替域名,否则证书校验一定失败。
CI 流水线不要用个人账号,用项目级的机器人账号:
名称形式 robot$<项目名>+<机器人名>(系统级机器人为 robot$<机器人名>)
权限 只勾 pull 或 push,不勾 delete
有效期 设置到期时间,配合轮换
docker login harbor.example.com -u 'robot$ci+builder' -p '******'
docker push harbor.example.com/library/app:v1
Harbor 2.x 内置 Trivy,默认在推送时自动扫描:
# 手工触发某个镜像的扫描
curl -u admin:****** -X POST \
"https://harbor.example.com/api/v2.0/projects/library/repositories/nginx/artifacts/1.27/scan"
# 查看扫描结果概览
curl -u admin:****** \
"https://harbor.example.com/api/v2.0/projects/library/repositories/nginx/artifacts/1.27"
配合项目的「部署安全策略」,可以做到存在高危漏洞的镜像直接拒绝拉取,这对集群是一道很实在的防线 —— 拒绝发生在 kubelet 拉取阶段,Pod 会停在 ImagePullBackOff。
删除镜像标签不会释放磁盘,必须跑 GC:
# 界面上:系统管理 → 垃圾清理 → 立即清理
# 或走 API 触发一次 GC
curl -u admin:****** -X POST \
-H "Content-Type: application/json" \
"https://harbor.example.com/api/v2.0/system/gc/schedule" \
-d '{"schedule":{"type":"Manual"},"parameters":{"delete_untagged":true,"dry_run":false}}'
GC 的本质是标记-清除:delete_untagged 决定是否连同「没有标签指向的 manifest」一起清理,开启它才能真正清掉那些被 docker push 覆盖后遗留的旧版本。
多集群、多机房场景下用 Harbor 的复制功能做镜像分发:
方向 推模式(本端推送到远端)/ 拉模式(从远端拉取)
触发 手动 / 定时 / 推送到本端时自动触发
过滤 按项目、按标签正则、按资源类型
异地多活时通常是「主 Harbor 推模式分发到各机房 Harbor」,这样每个机房的 kubelet 只拉本地仓库,避免跨专线拉大镜像。
单实例 Harbor 的数据库与 Redis 都在集群内,挂掉就整体不可用,生产环境建议改成外部依赖:
database.type=external 使用外部 PostgreSQL(主从)
redis.type=external 使用外部 Redis(哨兵或集群)
persistence.imageChartStorage.type=s3 镜像存储改用对象存储
core.replicas=2 / jobservice.replicas=2
镜像存储改成 S3 类对象存储后,registry 组件变成无状态,扩容与容灾都简单得多。
helm repo update
helm upgrade harbor harbor/harbor -n harbor -f harbor-values.yaml
# 确认 Pod 全部就绪,并观察数据库迁移 Job 的日志
kubectl -n harbor get pods -w
kubectl -n harbor logs job/harbor-db-migrate
Harbor 只支持从最近的两个小版本升级。也就是说想从 2.12 升到 2.15,必须依次经过 2.13、2.14,不能跳级。
externalURL 必须配成集群外可解析的地址。它是返回给 docker 客户端的 token 服务地址,写成集群内部 Service 名会导致「在集群内能拉、在集群外 docker push 报 DNS 解析失败」。格式必须是 protocol://domain[:port],不支持带路径。http: server gave HTTP response to HTTPS client。正确解法是配证书,而不是在客户端放行不安全仓库。docker login 成功不代表 kubelet 能拉 —— containerd 走的是 /etc/containerd/certs.d/ 下的证书目录,不是系统信任库。只执行 update-ca-certificates 通常不够。expose.type=ingress 需要集群里已有 Ingress Controller。没装 controller 时,Ingress 资源会创建成功但永远不会被处理,Harbor 表现为「装好了却访问不了」。expose.tls.certSource 有三个取值,选错会静默出问题。auto 由 chart 自动签发一张自签证书(浏览器一定报不信任);secret 引用你自己的证书(生产用这个);none 表示不配证书、由 Ingress Controller 提供默认证书。用 auto 后又忘了把 CA 分发到节点,就是 x509 报错的最常见来源。delete_untagged,否则被覆盖的旧 tag 指向的 manifest 会一直占着空间。Harbor 2.x 的 GC 已是非阻塞实现,不再强制把仓库切成只读(1.11 之前才会),但它仍是重 IO 操作,请安排在低峰期,并确保存储后端的快照、复制任务不与它重叠。harborAdminPassword 只在首次安装时生效。helm upgrade 时再改这个值不会修改已存在的管理员密码,得走界面或 API 改。imagePullSecret 时 Pod 会一直 ImagePullBackOff,而事件里往往只写 unauthorized,信息量很少。$ 在 shell 里要转义。docker login -u robot$ci+builder 会被 shell 当成变量展开,必须写成单引号 'robot$ci+builder'。persistence 相关的 storageClass 必须真实存在。用 helm install 时写了一个不存在的 StorageClass,PVC 会一直 Pending,而后端组件因为等不到卷会 CrashLoop,Harbor 表现为「装了一半」。trivy 的漏洞库要能定期更新。离线环境里 Trivy 拉不到漏洞库时扫描会静默失败,界面上的「未发现漏洞」并不等于真的安全。harbor.example.com/library/nginx),与 Docker Hub 的两段式写法不同。少写一段会在推送时报 denied: requested access to the resource is denied。registry — Harbor 底层的镜像存储实现skopeo — 跨仓库搬运镜像,常用于配合 Harbor 做离线同步containerd — 节点侧真正执行拉取的运行时kubelet — 通过 CRI 触发镜像拉取secret — 存放 imagePullSecret