Harbor 結(jié)合 Traefik 的 HA 安裝配置
Harbor 是一個 CNCF 基金會托管的開源的可信的云原生 docker registry 項目,可以用于存儲、簽名、掃描鏡像內(nèi)容,Harbor 通過添加一些常用的功能如安全性、身份權(quán)限管理等來擴(kuò)展 docker registry 項目,此外還支持在 registry 之間復(fù)制鏡像,還提供更加高級的安全功能,如用戶管理、訪問控制和活動審計等,在新版本中還添加了 Helm 倉庫托管的支持。
Harbor 最核心的功能就是給 docker registry 添加上一層權(quán)限保護(hù)的功能,要實現(xiàn)這個功能,就需要我們在使用 docker login、pull、push 等命令的時候進(jìn)行攔截,先進(jìn)行一些權(quán)限相關(guān)的校驗,再進(jìn)行操作,其實這一系列的操作 docker registry v2 就已經(jīng)為我們提供了支持,v2 集成了一個安全認(rèn)證的功能,將安全認(rèn)證暴露給外部服務(wù),讓外部服務(wù)去實現(xiàn)。
Harbor 認(rèn)證原理
上面我們說了 docker registry v2 將安全認(rèn)證暴露給了外部服務(wù)使用,那么是怎樣暴露的呢?我們在命令行中輸入 docker login https://registry.qikqiak.com 為例來為大家說明下認(rèn)證流程:
- docker client 接收到用戶輸入的 docker login 命令,將命令轉(zhuǎn)化為調(diào)用 engine api 的 RegistryLogin 方法
- 在 RegistryLogin 方法中通過 http 調(diào)用 registry 服務(wù)中的 auth 方法
- 因為我們這里使用的是 v2 版本的服務(wù),所以會調(diào)用 loginV2 方法,在 loginV2 方法中會進(jìn)行 /v2/ 接口調(diào)用,該接口會對請求進(jìn)行認(rèn)證
- 此時的請求中并沒有包含 token 信息,認(rèn)證會失敗,返回 401 錯誤,同時會在 header 中返回去哪里請求認(rèn)證的服務(wù)器地址
- registry client 端收到上面的返回結(jié)果后,便會去返回的認(rèn)證服務(wù)器那里進(jìn)行認(rèn)證請求,向認(rèn)證服務(wù)器發(fā)送的請求的 header 中包含有加密的用戶名和密碼
- 認(rèn)證服務(wù)器從 header 中獲取到加密的用戶名和密碼,這個時候就可以結(jié)合實際的認(rèn)證系統(tǒng)進(jìn)行認(rèn)證了,比如從數(shù)據(jù)庫中查詢用戶認(rèn)證信息或者對接 ldap 服務(wù)進(jìn)行認(rèn)證校驗
- 認(rèn)證成功后,會返回一個 token 信息,client 端會拿著返回的 token 再次向 registry 服務(wù)發(fā)送請求,這次需要帶上得到的 token,請求驗證成功,返回狀態(tài)碼就是200了
- docker client 端接收到返回的200狀態(tài)碼,說明操作成功,在控制臺上打印 Login Succeeded 的信息 至此,整個登錄過程完成,整個過程可以用下面的流程圖來說明:
要完成上面的登錄認(rèn)證過程有兩個關(guān)鍵點需要注意:怎樣讓 registry 服務(wù)知道服務(wù)認(rèn)證地址?我們自己提供的認(rèn)證服務(wù)生成的 token 為什么 registry 就能夠識別?
對于第一個問題,比較好解決,registry 服務(wù)本身就提供了一個配置文件,可以在啟動 registry 服務(wù)的配置文件中指定上認(rèn)證服務(wù)地址即可,其中有如下這樣的一段配置信息:
- ......
- auth:
- token:
- realm: token-realm
- service: token-service
- issuer: registry-token-issuer
- rootcertbundle: /root/certs/bundle
- ......
其中 realm 就可以用來指定一個認(rèn)證服務(wù)的地址,下面我們可以看到 Harbor 中該配置的內(nèi)容。
關(guān)于 registry 的配置,可以參考官方文檔:https://docs.docker.com/registry/configuration/
第二個問題,就是 registry 怎么能夠識別我們返回的 token 文件?如果按照 registry 的要求生成一個 token,是不是 registry 就可以識別了?所以我們需要在我們的認(rèn)證服務(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)于上面我們講解的認(rèn)證服務(wù)部分的實現(xiàn)方法。
安裝
Harbor 涉及的組件比較多,我們可以使用 Helm 來安裝一個高可用版本的 Harbor,也符合生產(chǎn)環(huán)境的部署方式。在安裝高可用的版本之前,我們需要如下先決條件:
- Kubernetes 集群 1.10+ 版本
- Helm 2.8.0+ 版本
- 高可用的 Ingress 控制器
- 高可用的 PostgreSQL 9.6+(Harbor 不進(jìn)行數(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.6.2版本并解壓
- helm pull harbor/harbor --untar --version 1.6.2
在安裝 Harbor 的時候有很多可以配置的參數(shù),可以在 harbor-helm 項目上進(jìn)行查看,在安裝的時候我們可以通過 --set 指定參數(shù)或者 values.yaml 直接編輯 Values 文件即可:
Ingress 配置通過 expose.ingress.hosts.core 和 expose.ingress.hosts.notary
- 外部 URL 通過配置 externalURL
- 外部 PostgreSQL 通過配置 database.type 為 external,然后補(bǔ)充上 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è)置為
: , : , : 。還可以參考文檔https://community.pivotal.io/s/article/How-to-setup-HAProxy-and-Redis-Sentinel-for-automatic-failover-between-Redis-Master-and-Slave-servers 在 Redis 前面配置一個 HAProxy 來暴露單個入口點。 - 存儲,默認(rèn)情況下需要一個默認(rèn)的 StorageClass 在 K8S 集群中來自動生成 PV,用來存儲鏡像、Charts 和任務(wù)日志。如果你想指定 StorageClass,可以通過 persistence.persistentVolumeClaim.registry.storageClass、persistence.persistentVolumeClaim.chartmuseum.storageClass 以及 persistence.persistentVolumeClaim.jobservice.storageClass 進(jìn)行配置,另外還需要將 accessMode 設(shè)置為 ReadWriteMany,確保 PV 可以跨不同節(jié)點進(jìn)行共享存儲。此外我們還可以通過指定存在的 PVCs 來存儲數(shù)據(jù),可以通過 existingClaim 進(jìn)行配置。如果你沒有可以跨節(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 的 StorageClass 來提供存儲(生產(chǎn)環(huán)境不建議使用 NFS),又因為前面我們在安裝 GitLab 的時候就已經(jīng)單獨安裝了 postgresql 和 reids 兩個數(shù)據(jù)庫,所以我們也可以配置 Harbor 使用這兩個外置的數(shù)據(jù)庫,這樣可以降低資源的使用(我們可以認(rèn)為這兩個數(shù)據(jù)庫都是 HA 模式)。但是使用外置的數(shù)據(jù)庫我們需要提前手動創(chuàng)建數(shù)據(jù)庫,比如我們這里使用的 GitLab 提供的數(shù)據(jù)庫,則進(jìn)入該 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-566846fd86-9kps9 1/1 Running 1 2d
- $ kubectl exec -it postgresql-566846fd86-9kps9 /bin/bash -n kube-ops
- root@postgresql-566846fd86-9kps9:/var/lib/postgresql# sudo su - postgres
- postgres@postgresql-566846fd86-9kps9:~$ psql
- psql (12.3 (Ubuntu 12.3-1.pgdg18.04+1))
- Type "help" for help.
- postgres=# CREATE DATABASE harbor OWNER postgres; # 創(chuàng)建 harbor 數(shù)據(jù)庫
- CREATE DATABASE
- postgres=# GRANT ALL PRIVILEGES ON DATABASE harbor to postgres; # 授權(quán)給 postgres 用戶
- GRANT
- postgres=# GRANT ALL PRIVILEGES ON DATABASE harbor to gitlab; # 授權(quán)給 gitlab 用戶
- GRANT
- # Todo: 用同樣的方式創(chuàng)建其他兩個數(shù)據(jù)庫:notary_server、notary_signer
- ......
- postgres-# \q # 退出
數(shù)據(jù)庫準(zhǔn)備過后,就可以使用我們自己定制的 values 文件來進(jìn)行安裝了,完整的定制的 values 文件如下所示:
- # values-prod.yaml
- externalURL: https://harbor.k8s.local
- harborAdminPassword: Harbor12345
- logLevel: debug
- expose:
- type: ingress
- tls:
- enabled: true
- ingress:
- hosts:
- core: harbor.k8s.local
- notary: notary.k8s.local
- annotations:
- # 因為我們使用的 Traefik2.x 作為 Ingress 控制器
- kubernetes.io/ingress.class: traefik
- traefik.ingress.kubernetes.io/router.entrypoints: websecure
- traefik.ingress.kubernetes.io/router.tls: "true"
- persistence:
- enabled: true
- resourcePolicy: "keep"
- persistentVolumeClaim:
- registry:
- # 如果需要做高可用,多個副本的組件則需要使用支持 ReadWriteMany 的后端
- # 這里我們使用nfs,生產(chǎn)環(huán)境不建議使用nfs
- storageClass: "nfs-storage"
- # 如果是高可用的,多個副本組件需要使用 ReadWriteMany,默認(rèn)為 ReadWriteOnce
- accessMode: ReadWriteMany
- size: 5Gi
- chartmuseum:
- storageClass: "nfs-storage"
- accessMode: ReadWriteMany
- size: 5Gi
- jobservice:
- storageClass: "nfs-storage"
- accessMode: ReadWriteMany
- size: 1Gi
- trivy:
- storageClass: "nfs-storage"
- 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"
- # 默認(rèn)為一個副本,如果要做高可用,只需要設(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
由于我們這里使用的 Ingress 控制器是 traefik2.x 版本,在配置 Ingress 的時候,我們需要重新配置 annotations(如果你使用的是其他 Ingress 控制器,請參考具體的使用方式)。這些配置信息都是根據(jù) Harbor 的 Chart 包默認(rèn)的 values 值進(jìn)行覆蓋的,現(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: Sat May 29 16:22:13 2021
- 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 2021-05-29 16:22:13.495394 +0800 CST deployed harbor-1.6.2 2.2.2
- $ kubectl get pods -n kube-ops -l app=harbor
- NAME READY STATUS RESTARTS AGE
- harbor-harbor-chartmuseum-6996f677d4-dhkgd 1/1 Running 0 91s
- harbor-harbor-core-84b8db479-rpwml 1/1 Running 0 91s
- harbor-harbor-jobservice-5584dccd6-gpv79 1/1 Running 0 91s
- harbor-harbor-notary-server-7d79b7d46d-dlgt7 1/1 Running 0 91s
- harbor-harbor-notary-signer-69d8fdd476-7pt44 1/1 Running 0 91s
- harbor-harbor-portal-559c4d4bfd-8x2pp 1/1 Running 0 91s
- harbor-harbor-registry-758f67dbbb-nl729 2/2 Running 0 91s
- harbor-harbor-trivy-0 1/1 Running 0 50s
安裝完成后,我們就可以將域名 harbor.k8s.local 解析到 Ingress Controller 所在的節(jié)點,我們這里使用的仍然是 Traefik,由于我們開啟了 KubernetesIngress 支持的,所以我們只需要將域名解析到 Traefik 的 Pod 所在節(jié)點即可,然后就可以通過該域名在瀏覽器中訪問了:
- $ kubectl get ingress -n kube-ops
- NAME CLASS HOSTS ADDRESS PORTS AGE
- harbor-harbor-ingress <none> harbor.k8s.local 80, 443 115s
- harbor-harbor-ingress-notary <none> notary.k8s.local 80, 443 115s
用戶名使用默認(rèn)的 admin,密碼則是上面配置的默認(rèn) Harbor12345,需要注意的是要使用 https 進(jìn)行訪問,否則登錄可能提示用戶名或密碼錯誤:
但是這里也需要注意的是,由于我們這里使用的 traefik2.x 版本的 Ingress 控制器,所以對于 Ingress 資源的支持不是很友好,由于我們添加了 traefik.ingress.kubernetes.io/router.tls: "true" 這個注解,導(dǎo)致我們的 http 服務(wù)又失效了,為了解決這個問題,我們這里手動來創(chuàng)建一個 http 版本的 Ingress 對象:
- apiVersion: extensions/v1beta1
- kind: Ingress
- metadata:
- annotations:
- meta.helm.sh/release-name: harbor
- meta.helm.sh/release-namespace: kube-ops
- traefik.ingress.kubernetes.io/router.entrypoints: web
- traefik.ingress.kubernetes.io/router.middlewares: kube-system-redirect-https@kubernetescrd
- labels:
- app: harbor
- app.kubernetes.io/managed-by: Helm
- chart: harbor
- heritage: Helm
- release: harbor
- name: harbor-harbor-ingress-http
- namespace: kube-ops
- spec:
- rules:
- - host: harbor.k8s.local
- http:
- paths:
- - backend:
- serviceName: harbor-harbor-portal
- servicePort: 80
- path: /
- pathType: Prefix
- - backend:
- serviceName: harbor-harbor-core
- servicePort: 80
- path: /api
- pathType: Prefix
- - backend:
- serviceName: harbor-harbor-core
- servicePort: 80
- path: /service
- pathType: Prefix
- - backend:
- serviceName: harbor-harbor-core
- servicePort: 80
- path: /v2
- pathType: Prefix
- - backend:
- serviceName: harbor-harbor-core
- servicePort: 80
- path: /chartrepo
- pathType: Prefix
- - backend:
- serviceName: harbor-harbor-core
- servicePort: 80
- path: /c
- pathType: Prefix
- ---
- apiVersion: extensions/v1beta1
- kind: Ingress
- metadata:
- annotations:
- kubernetes.io/ingress.class: traefik
- meta.helm.sh/release-name: harbor
- meta.helm.sh/release-namespace: kube-ops
- traefik.ingress.kubernetes.io/router.entrypoints: web
- traefik.ingress.kubernetes.io/router.middlewares: kube-system-redirect-https@kubernetescrd
- labels:
- app: harbor
- app.kubernetes.io/managed-by: Helm
- chart: harbor
- heritage: Helm
- release: harbor
- name: harbor-harbor-ingress-notary-http
- namespace: kube-ops
- spec:
- rules:
- - host: notary.k8s.local
- http:
- paths:
- - backend:
- serviceName: harbor-harbor-notary-server
- servicePort: 4443
- path: /
- pathType: Prefix
為了讓能夠跳轉(zhuǎn)到 https,我們還需要創(chuàng)建如下所示的一個 Middleware(如果你使用的是其他 Ingress 控制器,請參考具體的使用方式):
- apiVersion: traefik.containo.us/v1alpha1
- kind: Middleware
- metadata:
- name: redirect-https
- namespace: kube-system
- spec:
- redirectScheme:
- scheme: https
需要注意的是在 Ingress 的 annotations 中配置中間件的格式為
登錄過后即可進(jìn)入 Harbor 的 Dashboard 頁面:
我們可以看到有很多功能,默認(rèn)情況下會有一個名叫 library 的項目,該項目默認(rèn)是公開訪問權(quán)限的,進(jìn)入項目可以看到里面還有 Helm Chart 包的管理,可以手動在這里上傳,也可以對該項目里面的鏡像進(jìn)行一些其他配置。
推送鏡像
現(xiàn)在我們來測試下使用 docker cli 來進(jìn)行 pull/push 鏡像,直接使用 docker login 命令登錄:
- $ docker login harbor.k8s.local
- Username: admin
- Password:
- Error response from daemon: Get https://harbor.k8s.local/v2/: x509: certificate signed by unknown authority
可以看到會登錄失敗,這是因為在使用 docker login 登錄的時候會使用 https 的服務(wù),而我們這里是自簽名的證書,所以就報錯了,我們可以將使用到的 CA 證書文件復(fù)制到 /etc/docker/certs.d/harbor.k8s.local 目錄下面來解決這個問題(如果該目錄不存在,則創(chuàng)建它)。ca.crt 這個證書文件我們可以通過 Ingress 中使用的 Secret 資源對象來提供:
- $ kubectl get secret harbor-harbor-ingress -n kube-ops -o yaml
- apiVersion: v1
- data:
- ca.crt: <ca.crt>
- tls.crt: <tls.crt>
- tls.key: <tls.key>
- kind: Secret
- metadata:
- ......
- name: harbor-harbor-ingress
- namespace: kube-ops
- resourceVersion: "450460"
- selfLink: /api/v1/namespaces/kube-ops/secrets/harbor-harbor-ingress
- uid: 0c44425c-8258-407a-a0a7-1c7e50d29404
- type: kubernetes.io/tls
其中 data 區(qū)域中 ca.crt 對應(yīng)的值就是我們需要證書,不過需要注意還需要做一個 base64 的解碼,這樣證書配置上以后就可以正常訪問了。
不過由于上面的方法較為繁瑣,所以一般情況下面我們在使用 docker cli 的時候是在 docker 啟動參數(shù)后面添加一個 --insecure-registry 參數(shù)來忽略證書的校驗的,在 docker 啟動配置文件 /usr/lib/systemd/system/docker.service 中修改ExecStart的啟動參數(shù):
- ExecStart=/usr/bin/dockerd --insecure-registry harbor.k8s.local
或者在 Docker Daemon 的配置文件中添加:
- $ cat /etc/docker/daemon.json
- {
- "insecure-registries" : [
- "harbor.k8s.local"
- ],
- "registry-mirrors" : [
- "https://ot2k4d59.mirror.aliyuncs.com/"
- ]
- }
然后保存重啟 docker,再使用 docker cli 就沒有任何問題了:
- $ docker login harbor.k8s.local
- Username: admin
- Password:
- 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
比如我們本地現(xiàn)在有一個名為 busybox:1.28.4 的鏡像,現(xiàn)在我們想要將該鏡像推送到我們的私有倉庫中去,應(yīng)該怎樣操作呢?首先我們需要給該鏡像重新打一個具有 harbor.k8s.local 前綴的鏡像,然后推送的時候就可以識別到推送到哪個鏡像倉庫:
- $ docker tag busybox:1.28.4 harbor.k8s.local/library/busybox:1.28.4
- $ docker push harbor.k8s.local/library/busybox:1.28.4
- The push refers to repository [harbor.k8s.local/library/busybox]
- 432b65032b94: Pushed
- 1.28.4: digest: sha256:74f634b1bc1bd74535d5209589734efbd44a25f4e2dc96d78784576a3eb5b335 size: 527
推送完成后,我們就可以在 Portal 頁面上看到這個鏡像的信息了:
鏡像 push 成功,同樣可以測試下 pull:
- $ docker rmi harbor.k8s.local/library/busybox:1.28.4
- Untagged: harbor.k8s.local/library/busybox:1.28.4
- Untagged: harbor.k8s.local/library/busybox@sha256:74f634b1bc1bd74535d5209589734efbd44a25f4e2dc96d78784576a3eb5b335
- $ docker rmi busybox:1.28.4
- Untagged: busybox:1.28.4
- Untagged: busybox@sha256:141c253bc4c3fd0a201d32dc1f493bcf3fff003b6df416dea4f41046e0f37d47
- Deleted: sha256:8c811b4aec35f259572d0f79207bc0678df4c736eeec50bc9fec37ed936a472a
- Deleted: sha256:432b65032b9466b4dadcc5c7b11701e71d21c18400aae946b101ad16be62333a
- $ docker pull harbor.k8s.local/library/busybox:1.28.4
- 1.28.4: Pulling from library/busybox
- 07a152489297: Pull complete
- Digest: sha256:74f634b1bc1bd74535d5209589734efbd44a25f4e2dc96d78784576a3eb5b335
- Status: Downloaded newer image for harbor.k8s.local/library/busybox:1.28.4
- harbor.k8s.local/library/busybox:1.28.4
- $ docker images |grep busybox
- harbor.k8s.local/library/busybox 1.28.4 8c811b4aec35 3 years ago 1.15MB
到這里證明上面我們的私有 docker 倉庫搭建成功了,大家可以嘗試去創(chuàng)建一個私有的項目,然后創(chuàng)建一個新的用戶,使用這個用戶來進(jìn)行 pull/push 鏡像,Harbor 還具有其他的一些功能,比如鏡像復(fù)制,Helm Chart 包托管等等,大家可以自行測試,感受下 Harbor 和官方自帶的 registry 倉庫的差別。