Containerd 對接私有鏡像倉庫 Harbor
Harbor 是一個 CNCF 基金會托管的開源的可信的云原生 docker registry 項目,可以用于存儲、簽名、掃描鏡像內(nèi)容,Harbor 通過添加一些常用的功能如安全性、身份權(quán)限管理等來擴展 docker registry 項目,此外還支持在 registry 之間復(fù)制鏡像,還提供更加高級的安全功能,如用戶管理、訪問控制和活動審計等,在新版本中還添加了 Helm 倉庫托管的支持。

Harbor 最核心的功能就是給 docker registry 添加上一層權(quán)限保護的功能,要實現(xiàn)這個功能,就需要我們在使用 docker login、pull、push 等命令的時候進行攔截,先進行一些權(quán)限相關(guān)的校驗,再進行操作,其實這一系列的操作 docker registry v2 就已經(jīng)為我們提供了支持,v2 集成了一個安全認證的功能,將安全認證暴露給外部服務(wù),讓外部服務(wù)去實現(xiàn)。
Harbor 認證原理
上面我們說了 docker registry v2 將安全認證暴露給了外部服務(wù)使用,那么是怎樣暴露的呢?我們在命令行中輸入 docker login https://registry.qikqiak.com 為例來為大家說明下認證流程:
docker client 接收到用戶輸入的 docker login 命令,將命令轉(zhuǎn)化為調(diào)用 engine api 的 RegistryLogin 方法 在 RegistryLogin 方法中通過 http 調(diào)用 registry 服務(wù)中的 auth 方法 因為我們這里使用的是 v2 版本的服務(wù),所以會調(diào)用 loginV2 方法,在 loginV2 方法中會進行 /v2/ 接口調(diào)用,該接口會對請求進行認證 此時的請求中并沒有包含 token 信息,認證會失敗,返回 401 錯誤,同時會在 header 中返回去哪里請求認證的服務(wù)器地址 registry client 端收到上面的返回結(jié)果后,便會去返回的認證服務(wù)器那里進行認證請求,向認證服務(wù)器發(fā)送的請求的 header 中包含有加密的用戶名和密碼 認證服務(wù)器從 header 中獲取到加密的用戶名和密碼,這個時候就可以結(jié)合實際的認證系統(tǒng)進行認證了,比如從數(shù)據(jù)庫中查詢用戶認證信息或者對接 ldap 服務(wù)進行認證校驗 認證成功后,會返回一個 token 信息,client 端會拿著返回的 token 再次向 registry 服務(wù)發(fā)送請求,這次需要帶上得到的 token,請求驗證成功,返回狀態(tài)碼就是200了 docker client 端接收到返回的200狀態(tài)碼,說明操作成功,在控制臺上打印 Login Succeeded 的信息 至此,整個登錄過程完成,整個過程可以用下面的流程圖來說明:

要完成上面的登錄認證過程有兩個關(guān)鍵點需要注意:怎樣讓 registry 服務(wù)知道服務(wù)認證地址?我們自己提供的認證服務(wù)生成的 token 為什么 registry 就能夠識別?
對于第一個問題,比較好解決,registry 服務(wù)本身就提供了一個配置文件,可以在啟動 registry 服務(wù)的配置文件中指定上認證服務(wù)地址即可,其中有如下這樣的一段配置信息:
......
auth:
token:
realm: token-realm
service: token-service
issuer: registry-token-issuer
rootcertbundle: /root/certs/bundle
......
其中 realm 就可以用來指定一個認證服務(wù)的地址,下面我們可以看到 Harbor 中該配置的內(nèi)容。
關(guān)于 registry 的配置,可以參考官方文檔:https://docs.docker.com/registry/configuration/
第二個問題,就是 registry 怎么能夠識別我們返回的 token 文件?如果按照 registry 的要求生成一個 token,是不是 registry 就可以識別了?所以我們需要在我們的認證服務(wù)器中按照 registry 的要求生成 token,而不是隨便亂生成。那么要怎么生成呢?我們可以在 docker registry 的源碼中可以看到 token 是通過 JWT(JSON Web Token) 來實現(xiàn)的,所以我們按照要求生成一個 JWT 的 token 就可以了。
對 golang 熟悉的同學(xué)可以去 clone 下 Harbor 的代碼查看下,Harbor 采用 beego 這個 web 開發(fā)框架,源碼閱讀起來不是特別困難。我們可以很容易看到 Harbor 中關(guān)于上面我們講解的認證服務(wù)部分的實現(xiàn)方法。

安裝
Harbor 涉及的組件比較多,我們可以使用 Helm 來安裝一個高可用版本的 Harbor,也符合生產(chǎn)環(huán)境的部署方式。在安裝高可用的版本之前,我們需要如下先決條件:
Kubernetes 集群 1.10+ 版本 Helm 2.8.0+ 版本 高可用的 Ingress 控制器 高可用的 PostgreSQL 9.6+(Harbor 不進行數(shù)據(jù)庫 HA 的部署) 高可用的 Redis 服務(wù)(Harbor 不處理) 可以跨節(jié)點或外部對象存儲共享的 PVC
Harbor 的大部分組件都是無狀態(tài)的,所以我們可以簡單增加 Pod 的副本,保證組件盡量分布到多個節(jié)點上即可,在存儲層,需要我們自行提供高可用的 PostgreSQL、Redis 集群來存儲應(yīng)用數(shù)據(jù),以及存儲鏡像和 Helm Chart 的 PVC 或?qū)ο蟠鎯Α?/p>
首先添加 Chart 倉庫地址:
# 添加 Chart 倉庫
$ helm repo add harbor https://helm.goharbor.io
# 更新
$ helm repo update
# 拉取1.9.2版本并解壓
$ helm pull harbor/harbor --untar --version 1.9.2
在安裝 Harbor 的時候有很多可以配置的參數(shù),可以在 harbor-helm 項目上進行查看,在安裝的時候我們可以通過 --set 指定參數(shù)或者 values.yaml 直接編輯 Values 文件即可:
Ingress 配置通過 expose.ingress.hosts.core和expose.ingress.hosts.notary外部 URL 通過配置 externalURL外部 PostgreSQL 通過配置 database.type為external,然后補充上database.external的信息。需要我們手動創(chuàng)建3個空的數(shù)據(jù):Harbor core、Notary server以及Notary signer,Harbor 會在啟動時自動創(chuàng)建表結(jié)構(gòu)外部 Redis 通過配置 redis.type為external,并填充redis.external部分的信息。Harbor 在 2.1.0 版本中引入了 redis 的Sentinel模式,你可以通過配置sentinel_master_set來開啟,host 地址可以設(shè)置為<host_sentinel1>:<port_sentinel1>,<host_sentinel2>:<port_sentinel2>,<host_sentinel3>:<port_sentinel3>。還可以參考文檔https://community.pivotal.io/s/article/How-to-setup-HAProxy-and-Redis-Sentinel-for-automatic-failover-between-Redis-Master-and-Slave-servers 在 Redis 前面配置一個 HAProxy 來暴露單個入口點。存儲,默認情況下需要一個默認的 StorageClass在 K8S 集群中來自動生成 PV,用來存儲鏡像、Charts 和任務(wù)日志。如果你想指定StorageClass,可以通過persistence.persistentVolumeClaim.registry.storageClass、persistence.persistentVolumeClaim.chartmuseum.storageClass以及persistence.persistentVolumeClaim.jobservice.storageClass進行配置,另外還需要將 accessMode 設(shè)置為ReadWriteMany,確保 PV 可以跨不同節(jié)點進行共享存儲。此外我們還可以通過指定存在的 PVCs 來存儲數(shù)據(jù),可以通過existingClaim進行配置。如果你沒有可以跨節(jié)點共享的 PVC,你可以使用外部存儲來存儲鏡像和 Chart(外部存儲支持:azure,gcs,s3 swift 和 oss),并將任務(wù)日志存儲在數(shù)據(jù)庫中。將設(shè)置為persistence.imageChartStorage.type為你要使用的值并填充相應(yīng)部分并設(shè)置jobservice.jobLogger為database副本:通過設(shè)置 portal.replicas,core.replicas,jobservice.replicas,registry.replicas,chartmuseum.replicas,notary.server.replicas和notary.signer.replicas為 n(n> = 2)
比如這里我們將主域名配置為 harbor.k8s.local,通過一個 nfs-client 的 StorageClass 來提供存儲,又因為前面我們在安裝 GitLab 的時候就已經(jīng)單獨安裝了 postgresql 和 reids 兩個數(shù)據(jù)庫,所以我們也可以配置 Harbor 使用這兩個外置的數(shù)據(jù)庫,這樣可以降低資源的使用(我們可以認為這兩個數(shù)據(jù)庫都是 HA 模式)。但是使用外置的數(shù)據(jù)庫我們需要提前手動創(chuàng)建數(shù)據(jù)庫,比如我們這里使用的 GitLab 提供的數(shù)據(jù)庫,則進入該 Pod 創(chuàng)建 harbor、notary_server、notary_signer 這3個數(shù)據(jù)庫:
$ kubectl get pods -n kube-ops -l name=postgresql
NAME READY STATUS RESTARTS AGE
postgresql-75b8447fb5-th6bw 1/1 Running 1 2d
$ kubectl exec -it postgresql-75b8447fb5-th6bw /bin/bash -n kube-ops
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
root@postgresql-75b8447fb5-th6bw:/var/lib/postgresql# sudo su - postgres
postgres@postgresql-75b8447fb5-th6bw:~$ psql
psql (12.3 (Ubuntu 12.3-1.pgdg18.04+1))
Type "help" for help.
postgres=# CREATE DATABASE harbor OWNER postgres;
CREATE DATABASE
postgres=# GRANT ALL PRIVILEGES ON DATABASE harbor to postgres;
GRANT
postgres=# GRANT ALL PRIVILEGES ON DATABASE harbor to gitlab;
GRANT
# Todo: 用同樣的方式創(chuàng)建其他兩個數(shù)據(jù)庫:notary_server、notary_signer
......
postgres-# \q # 退出
數(shù)據(jù)庫準備過后,就可以使用我們自己定制的 values 文件來進行安裝了,完整的定制的 values 文件如下所示:
# values-prod.yaml
externalURL: https://harbor.k8s.local
harborAdminPassword: Harbor12345
logLevel: debug
expose:
type: ingress
tls:
enabled: true
ingress:
className: nginx # 指定 ingress class
hosts:
core: harbor.k8s.local
notary: notary.k8s.local
persistence:
enabled: true
resourcePolicy: "keep"
persistentVolumeClaim:
registry:
# 如果需要做高可用,多個副本的組件則需要使用支持 ReadWriteMany 的后端
# 這里我們使用nfs,生產(chǎn)環(huán)境不建議使用nfs
storageClass: "nfs-client"
# 如果是高可用的,多個副本組件需要使用 ReadWriteMany,默認為 ReadWriteOnce
accessMode: ReadWriteMany
size: 5Gi
chartmuseum:
storageClass: "nfs-client"
accessMode: ReadWriteMany
size: 5Gi
jobservice:
storageClass: "nfs-client"
accessMode: ReadWriteMany
size: 1Gi
trivy:
storageClass: "nfs-client"
accessMode: ReadWriteMany
size: 2Gi
database:
type: external
external:
host: "postgresql.kube-ops.svc.cluster.local"
port: "5432"
username: "gitlab"
password: "passw0rd"
coreDatabase: "harbor"
notaryServerDatabase: "notary_server"
notarySignerDatabase: "notary_signer"
redis:
type: external
external:
addr: "redis.kube-ops.svc.cluster.local:6379"
# 默認為一個副本,如果要做高可用,只需要設(shè)置為 replicas >= 2 即可
portal:
replicas: 1
core:
replicas: 1
jobservice:
replicas: 1
registry:
replicas: 1
chartmuseum:
replicas: 1
trivy:
replicas: 1
notary:
server:
replicas: 1
signer:
replicas: 1
這些配置信息都是根據(jù) Harbor 的 Chart 包默認的 values 值進行覆蓋的,現(xiàn)在我們直接安裝即可:
$ cd harbor
$ helm upgrade --install harbor . -f values-prod.yaml -n kube-ops
Release "harbor" does not exist. Installing it now.
NAME: harbor
LAST DEPLOYED: Thu Jul 7 17:31:43 2022
NAMESPACE: kube-ops
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
Please wait for several minutes for Harbor deployment to complete.
Then you should be able to visit the Harbor portal at https://harbor.k8s.local
For more details, please visit https://github.com/goharbor/harbor
正常情況下隔一會兒就可以安裝成功了:
$ helm ls -n kube-ops
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
harbor kube-ops 1 2022-07-07 17:31:43.083547 +0800 CST deployed harbor-1.9.2 2.5.2
$ kubectl get pods -n kube-ops -l app=harbor
NAME READY STATUS RESTARTS AGE
harbor-chartmuseum-544ddbcb64-nvk7w 1/1 Running 0 30m
harbor-core-7fd9964685-lqqw2 1/1 Running 0 30m
harbor-jobservice-6dbd89c59-vvzx5 1/1 Running 0 30m
harbor-notary-server-764b8859bf-82f5q 1/1 Running 0 30m
harbor-notary-signer-869d9bf585-kbwwg 1/1 Running 0 30m
harbor-portal-74db6bb688-2w79p 1/1 Running 0 35m
harbor-registry-695db89bfd-v9wwt 2/2 Running 0 30m
harbor-trivy-0 1/1 Running 0 35m
安裝完成后,我們就可以將域名 harbor.k8s.local 解析到 Ingress Controller 流量入口點,然后就可以通過該域名在瀏覽器中訪問了:
$ kubectl get ingress -n kube-ops
NAME CLASS HOSTS ADDRESS PORTS AGE
harbor-ingress nginx harbor.k8s.local 80, 443 12s
harbor-ingress-notary nginx notary.k8s.local 80, 443 12s
用戶名使用默認的 admin,密碼則是上面配置的默認 Harbor12345,需要注意的是要使用 https 進行訪問(默認也會跳轉(zhuǎn)到 https),否則登錄可能提示用戶名或密碼錯誤:

登錄過后即可進入 Harbor 的 Dashboard 頁面:

我們可以看到有很多功能,默認情況下會有一個名叫 library 的項目,該項目默認是公開訪問權(quán)限的,進入項目可以看到里面還有 Helm Chart 包的管理,可以手動在這里上傳,也可以對該項目里面的鏡像進行一些其他配置。
推送鏡像
接下來我們來測試下如何在 containerd 中使用 Harbor 鏡像倉庫。
首先我們需要將私有鏡像倉庫配置到 containerd 中去,修改 containerd 的配置文件 /etc/containerd/config.toml:
[plugins."io.containerd.grpc.v1.cri".registry]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"]
endpoint = ["https://bqr1dr1n.mirror.aliyuncs.com"]
[plugins."io.containerd.grpc.v1.cri".registry.configs]
[plugins."io.containerd.grpc.v1.cri".registry.configs."harbor.k8s.local".tls]
insecure_skip_verify = true
[plugins."io.containerd.grpc.v1.cri".registry.configs."harbor.k8s.local".auth]
username = "admin"
password = "Harbor12345"在 plugins."io.containerd.grpc.v1.cri".registry.configs 下面添加對應(yīng) harbor.k8s.local 的配置信息,insecure_skip_verify = true 表示跳過安全校驗,然后通過 plugins."io.containerd.grpc.v1.cri".registry.configs."harbor.k8s.local".auth 配置 Harbor 鏡像倉庫的用戶名和密碼。
配置完成后重啟 containerd:
$ systemctl restart containerd
現(xiàn)在我們使用 nerdctl 來進行登錄:
$ nerdctl login -u admin harbor.k8s.local
Enter Password:
ERRO[0004] failed to call tryLoginWithRegHost error="failed to call rh.Client.Do: Get \"https://harbor.k8s.local/v2/\": x509: certificate signed by unknown authority" i=0
FATA[0004] failed to call rh.Client.Do: Get "https://harbor.k8s.local/v2/": x509: certificate signed by unknown authority
[root@master1 ~]#
可以看到還是會報證書相關(guān)的錯誤,只需要添加一個 --insecure-registry 參數(shù)即可解決該問題:
$ nerdctl login -u admin --insecure-registry harbor.k8s.local
Enter Password:
WARN[0004] skipping verifying HTTPS certs for "harbor.k8s.local"
WARNING: Your password will be stored unencrypted in /root/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
Login Succeeded
然后我們先隨便拉一個鏡像:
$ nerdctl pull busybox:1.35.0
docker.io/library/busybox:1.35.0: resolved |++++++++++++++++++++++++++++++++++++++|
index-sha256:8c40df61d40166f5791f44b3d90b77b4c7f59ed39a992fd9046886d3126ffa68: done |++++++++++++++++++++++++++++++++++++++|
manifest-sha256:8cde9b8065696b65d7b7ffaefbab0262d47a5a9852bfd849799559d296d2e0cd: done |++++++++++++++++++++++++++++++++++++++|
config-sha256:d8c0f97fc6a6ac400e43342e67d06222b27cecdb076cbf8a87f3a2a25effe81c: done |++++++++++++++++++++++++++++++++++++++|
layer-sha256:fc0cda0e09ab32c72c61d272bb409da4e2f73165c7bf584226880c9b85438e63: done |++++++++++++++++++++++++++++++++++++++|
elapsed: 83.7s
然后將該鏡像重新 tag 成 Harbor 上的鏡像地址:
$ nerdctl tag busybox:1.35.0 harbor.k8s.local/library/busybox:1.35.0
再執(zhí)行 push 命令即可將鏡像推送到 Harbor 上:
$ nerdctl push --insecure-registry harbor.k8s.local/library/busybox:1.35.0
INFO[0000] pushing as a reduced-platform image (application/vnd.docker.distribution.manifest.list.v2+json, sha256:29fe0126b13c3ea2641ca42c450fa69583d212dbd9b7b623814977b5b0945726)
WARN[0000] skipping verifying HTTPS certs for "harbor.k8s.local"
index-sha256:29fe0126b13c3ea2641ca42c450fa69583d212dbd9b7b623814977b5b0945726: done |++++++++++++++++++++++++++++++++++++++|
manifest-sha256:8cde9b8065696b65d7b7ffaefbab0262d47a5a9852bfd849799559d296d2e0cd: done |++++++++++++++++++++++++++++++++++++++|
config-sha256:d8c0f97fc6a6ac400e43342e67d06222b27cecdb076cbf8a87f3a2a25effe81c: done |++++++++++++++++++++++++++++++++++++++|
elapsed: 6.9 s total: 2.2 Ki (333.0 B/s)
推送完成后,我們就可以在 Portal 頁面上看到這個鏡像的信息了:

鏡像 push 成功,同樣可以測試下 pull:
$ nerdctl rmi harbor.k8s.local/library/busybox:1.35.0
Untagged: harbor.k8s.local/library/busybox:1.35.0@sha256:8c40df61d40166f5791f44b3d90b77b4c7f59ed39a992fd9046886d3126ffa68
Deleted: sha256:cf4ac4fc01444f1324571ceb0d4f175604a8341119d9bb42bc4b2cb431a7f3a5
$ nerdctl rmi busybox:1.35.0
Untagged: docker.io/library/busybox:1.35.0@sha256:8c40df61d40166f5791f44b3d90b77b4c7f59ed39a992fd9046886d3126ffa68
Deleted: sha256:cf4ac4fc01444f1324571ceb0d4f175604a8341119d9bb42bc4b2cb431a7f3a5
$ nerdctl pull --insecure-registry harbor.k8s.local/library/busybox:1.35.0
WARN[0000] skipping verifying HTTPS certs for "harbor.k8s.local"
harbor.k8s.local/library/busybox:1.35.0: resolved |++++++++++++++++++++++++++++++++++++++|
index-sha256:29fe0126b13c3ea2641ca42c450fa69583d212dbd9b7b623814977b5b0945726: done |++++++++++++++++++++++++++++++++++++++|
manifest-sha256:8cde9b8065696b65d7b7ffaefbab0262d47a5a9852bfd849799559d296d2e0cd: done |++++++++++++++++++++++++++++++++++++++|
config-sha256:d8c0f97fc6a6ac400e43342e67d06222b27cecdb076cbf8a87f3a2a25effe81c: done |++++++++++++++++++++++++++++++++++++++|
layer-sha256:fc0cda0e09ab32c72c61d272bb409da4e2f73165c7bf584226880c9b85438e63: done |++++++++++++++++++++++++++++++++++++++|
elapsed: 0.7 s total: 2.2 Ki (3.2 KiB/s)
$ nerdctl images
REPOSITORY TAG IMAGE ID CREATED PLATFORM SIZE BLOB SIZE
harbor.k8s.local/library/busybox 1.35.0 29fe0126b13c 17 seconds ago linux/amd64 1.2 MiB 757.7 KiB
但是上面我們也可以看到單獨使用 containerd 比如通過 nerdctl 或者 ctr 命令訪問 Harbor 鏡像倉庫的時候即使跳過證書校驗或者配置上 CA 證書也是會出現(xiàn)證書錯誤的,這個時候我們需要去跳過證書校驗或者指定證書路徑才行。
# 解決辦法1.指定 -k 參數(shù)跳過證書校驗。
$ ctr images pull --user admin:Harbor12345 -k harbor.k8s.local/library/busybox:1.35.0
# 解決辦法2.指定CA證書、Harbor 相關(guān)證書文件路徑。
$ ctr images pull --user admin:Harbor12345 --tlscacert ca.crt harbor.k8s.local/library/busybox:1.35.0
但是如果直接使用 ctrctl 則是有效的:
$ crictl pull harbor.k8s.local/library/busybox@sha256:29fe0126b13c3ea2641ca42c450fa69583d212dbd9b7b623814977b5b0945726
Image is up to date for sha256:d8c0f97fc6a6ac400e43342e67d06222b27cecdb076cbf8a87f3a2a25effe81c
如果想要在 Kubernetes 中去使用那么就需要將 Harbor 的認證信息以 Secret 的形式添加到集群中去:
$ kubectl create secret docker-registry harbor-auth --docker-server=https://harbor.k8s.local --docker-username=admin --docker-password=Harbor12345 [email protected] -n default
然后我們使用上面的私有鏡像倉庫來創(chuàng)建一個 Pod:
# test-harbor.yaml
apiVersion: v1
kind: Pod
metadata:
name: harbor-registry-test
spec:
containers:
- name: test
image: harbor.k8s.local/library/busybox:1.35.0
args:
- sleep
- "3600"
imagePullSecrets:
- name: harbor-auth
創(chuàng)建后可以查看該 Pod 是否能正常獲取鏡像:
$ kubectl describe pod harbor-registry-test
Name: harbor-registry-test
Namespace: default
Priority: 0
Node: node1/192.168.0.107
Start Time: Thu, 07 Jul 2022 18:52:39 +0800
# ......
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 10s default-scheduler Successfully assigned default/harbor-registry-test to node1
Normal Pulling 10s kubelet Pulling image "harbor.k8s.local/library/busybox:1.35.0"
Normal Pulled 5s kubelet Successfully pulled image "harbor.k8s.local/library/busybox:1.35.0" in 4.670528883s
Normal Created 5s kubelet Created container test
Normal Started 5s kubelet Started container test
到這里證明上面我們的私有鏡像倉庫搭建成功了,大家可以嘗試去創(chuàng)建一個私有的項目,然后創(chuàng)建一個新的用戶,使用這個用戶來進行 pull/push 鏡像,Harbor 還具有其他的一些功能,比如鏡像復(fù)制,Helm Chart 包托管等等,大家可以自行測試,感受下 Harbor 和官方自帶的 registry 倉庫的差別。
