Harbor 結合 Traefik 的 HA 安裝配置
Harbor 是一個 CNCF 基金會托管的開源的可信的云原生 docker registry 項目,可以用于存儲、簽名、掃描鏡像內容,Harbor 通過添加一些常用的功能如安全性、身份權限管理等來擴展 docker registry 項目,此外還支持在 registry 之間復制鏡像,還提供更加高級的安全功能,如用戶管理、訪問控制和活動審計等,在新版本中還添加了 Helm 倉庫托管的支持。
Harbor 最核心的功能就是給 docker registry 添加上一層權限保護的功能,要實現這個功能,就需要我們在使用 docker login、pull、push 等命令的時候進行攔截,先進行一些權限相關的校驗,再進行操作,其實這一系列的操作 docker registry v2 就已經為我們提供了支持,v2 集成了一個安全認證的功能,將安全認證暴露給外部服務,讓外部服務去實現。
Harbor 認證原理
上面我們說了 docker registry v2 將安全認證暴露給了外部服務使用,那么是怎樣暴露的呢?我們在命令行中輸入 docker login https://registry.qikqiak.com 為例來為大家說明下認證流程:
- docker client 接收到用戶輸入的 docker login 命令,將命令轉化為調用 engine api 的 RegistryLogin 方法
- 在 RegistryLogin 方法中通過 http 調用 registry 服務中的 auth 方法
- 因為我們這里使用的是 v2 版本的服務,所以會調用 loginV2 方法,在 loginV2 方法中會進行 /v2/ 接口調用,該接口會對請求進行認證
- 此時的請求中并沒有包含 token 信息,認證會失敗,返回 401 錯誤,同時會在 header 中返回去哪里請求認證的服務器地址
- registry client 端收到上面的返回結果后,便會去返回的認證服務器那里進行認證請求,向認證服務器發送的請求的 header 中包含有加密的用戶名和密碼
- 認證服務器從 header 中獲取到加密的用戶名和密碼,這個時候就可以結合實際的認證系統進行認證了,比如從數據庫中查詢用戶認證信息或者對接 ldap 服務進行認證校驗
- 認證成功后,會返回一個 token 信息,client 端會拿著返回的 token 再次向 registry 服務發送請求,這次需要帶上得到的 token,請求驗證成功,返回狀態碼就是200了
- docker client 端接收到返回的200狀態碼,說明操作成功,在控制臺上打印 Login Succeeded 的信息 至此,整個登錄過程完成,整個過程可以用下面的流程圖來說明:
要完成上面的登錄認證過程有兩個關鍵點需要注意:怎樣讓 registry 服務知道服務認證地址?我們自己提供的認證服務生成的 token 為什么 registry 就能夠識別?
對于第一個問題,比較好解決,registry 服務本身就提供了一個配置文件,可以在啟動 registry 服務的配置文件中指定上認證服務地址即可,其中有如下這樣的一段配置信息:
- ......
- auth:
- token:
- realm: token-realm
- service: token-service
- issuer: registry-token-issuer
- rootcertbundle: /root/certs/bundle
- ......
其中 realm 就可以用來指定一個認證服務的地址,下面我們可以看到 Harbor 中該配置的內容。
關于 registry 的配置,可以參考官方文檔:https://docs.docker.com/registry/configuration/
第二個問題,就是 registry 怎么能夠識別我們返回的 token 文件?如果按照 registry 的要求生成一個 token,是不是 registry 就可以識別了?所以我們需要在我們的認證服務器中按照 registry 的要求生成 token,而不是隨便亂生成。那么要怎么生成呢?我們可以在 docker registry 的源碼中可以看到 token 是通過 JWT(JSON Web Token) 來實現的,所以我們按照要求生成一個 JWT 的 token 就可以了。
對 golang 熟悉的同學可以去 clone 下 Harbor 的代碼查看下,Harbor 采用 beego 這個 web 開發框架,源碼閱讀起來不是特別困難。我們可以很容易的看到 Harbor 中關于上面我們講解的認證服務部分的實現方法。
安裝
Harbor 涉及的組件比較多,我們可以使用 Helm 來安裝一個高可用版本的 Harbor,也符合生產環境的部署方式。在安裝高可用的版本之前,我們需要如下先決條件:
- Kubernetes 集群 1.10+ 版本
- Helm 2.8.0+ 版本
- 高可用的 Ingress 控制器
- 高可用的 PostgreSQL 9.6+(Harbor 不進行數據庫 HA 的部署)
- 高可用的 Redis 服務(Harbor 不處理)
- 可以跨節點或外部對象存儲共享的 PVC
Harbor 的大部分組件都是無狀態的,所以我們可以簡單增加 Pod 的副本,保證組件盡量分布到多個節點上即可,在存儲層,需要我們自行提供高可用的 PostgreSQL、Redis 集群來存儲應用數據,以及存儲鏡像和 Helm Chart 的 PVC 或對象存儲。
首先添加 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 的時候有很多可以配置的參數,可以在 harbor-helm 項目上進行查看,在安裝的時候我們可以通過 --set 指定參數或者 values.yaml 直接編輯 Values 文件即可:
Ingress 配置通過 expose.ingress.hosts.core 和 expose.ingress.hosts.notary
- 外部 URL 通過配置 externalURL
- 外部 PostgreSQL 通過配置 database.type 為 external,然后補充上 database.external 的信息。需要我們手動創建3個空的數據:Harbor core、Notary server 以及 Notary signer,Harbor 會在啟動時自動創建表結構
- 外部 Redis 通過配置 redis.type 為 external,并填充 redis.external 部分的信息。Harbor 在 2.1.0 版本中引入了 redis 的 Sentinel 模式,你可以通過配置 sentinel_master_set 來開啟,host 地址可以設置為
: , : , : 。還可以參考文檔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 和任務日志。如果你想指定 StorageClass,可以通過 persistence.persistentVolumeClaim.registry.storageClass、persistence.persistentVolumeClaim.chartmuseum.storageClass 以及 persistence.persistentVolumeClaim.jobservice.storageClass 進行配置,另外還需要將 accessMode 設置為 ReadWriteMany,確保 PV 可以跨不同節點進行共享存儲。此外我們還可以通過指定存在的 PVCs 來存儲數據,可以通過 existingClaim 進行配置。如果你沒有可以跨節點共享的 PVC,你可以使用外部存儲來存儲鏡像和 Chart(外部存儲支持:azure,gcs,s3 swift 和 oss),并將任務日志存儲在數據庫中。將設置為 persistence.imageChartStorage.type 為你要使用的值并填充相應部分并設置 jobservice.jobLogger 為 database
- 副本:通過設置 portal.replicas,core.replicas,jobservice.replicas,registry.replicas,chartmuseum.replicas,notary.server.replicas 和 notary.signer.replicas 為 n(n> = 2)
比如這里我們將主域名配置為 harbor.k8s.local,通過前面的 NFS 的 StorageClass 來提供存儲(生產環境不建議使用 NFS),又因為前面我們在安裝 GitLab 的時候就已經單獨安裝了 postgresql 和 reids 兩個數據庫,所以我們也可以配置 Harbor 使用這兩個外置的數據庫,這樣可以降低資源的使用(我們可以認為這兩個數據庫都是 HA 模式)。但是使用外置的數據庫我們需要提前手動創建數據庫,比如我們這里使用的 GitLab 提供的數據庫,則進入該 Pod 創建 harbor、notary_server、notary_signer 這3個數據庫:
- $ 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; # 創建 harbor 數據庫
- CREATE DATABASE
- postgres=# GRANT ALL PRIVILEGES ON DATABASE harbor to postgres; # 授權給 postgres 用戶
- GRANT
- postgres=# GRANT ALL PRIVILEGES ON DATABASE harbor to gitlab; # 授權給 gitlab 用戶
- GRANT
- # Todo: 用同樣的方式創建其他兩個數據庫:notary_server、notary_signer
- ......
- postgres-# \q # 退出
數據庫準備過后,就可以使用我們自己定制的 values 文件來進行安裝了,完整的定制的 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,生產環境不建議使用nfs
- storageClass: "nfs-storage"
- # 如果是高可用的,多個副本組件需要使用 ReadWriteMany,默認為 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"
- # 默認為一個副本,如果要做高可用,只需要設置為 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 控制器,請參考具體的使用方式)。這些配置信息都是根據 Harbor 的 Chart 包默認的 values 值進行覆蓋的,現在我們直接安裝即可:
- $ 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 所在的節點,我們這里使用的仍然是 Traefik,由于我們開啟了 KubernetesIngress 支持的,所以我們只需要將域名解析到 Traefik 的 Pod 所在節點即可,然后就可以通過該域名在瀏覽器中訪問了:
- $ 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
用戶名使用默認的 admin,密碼則是上面配置的默認 Harbor12345,需要注意的是要使用 https 進行訪問,否則登錄可能提示用戶名或密碼錯誤:
但是這里也需要注意的是,由于我們這里使用的 traefik2.x 版本的 Ingress 控制器,所以對于 Ingress 資源的支持不是很友好,由于我們添加了 traefik.ingress.kubernetes.io/router.tls: "true" 這個注解,導致我們的 http 服務又失效了,為了解決這個問題,我們這里手動來創建一個 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
為了讓能夠跳轉到 https,我們還需要創建如下所示的一個 Middleware(如果你使用的是其他 Ingress 控制器,請參考具體的使用方式):
- apiVersion: traefik.containo.us/v1alpha1
- kind: Middleware
- metadata:
- name: redirect-https
- namespace: kube-system
- spec:
- redirectScheme:
- scheme: https
需要注意的是在 Ingress 的 annotations 中配置中間件的格式為
登錄過后即可進入 Harbor 的 Dashboard 頁面:
我們可以看到有很多功能,默認情況下會有一個名叫 library 的項目,該項目默認是公開訪問權限的,進入項目可以看到里面還有 Helm Chart 包的管理,可以手動在這里上傳,也可以對該項目里面的鏡像進行一些其他配置。
推送鏡像
現在我們來測試下使用 docker cli 來進行 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 的服務,而我們這里是自簽名的證書,所以就報錯了,我們可以將使用到的 CA 證書文件復制到 /etc/docker/certs.d/harbor.k8s.local 目錄下面來解決這個問題(如果該目錄不存在,則創建它)。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 區域中 ca.crt 對應的值就是我們需要證書,不過需要注意還需要做一個 base64 的解碼,這樣證書配置上以后就可以正常訪問了。
不過由于上面的方法較為繁瑣,所以一般情況下面我們在使用 docker cli 的時候是在 docker 啟動參數后面添加一個 --insecure-registry 參數來忽略證書的校驗的,在 docker 啟動配置文件 /usr/lib/systemd/system/docker.service 中修改ExecStart的啟動參數:
- 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
比如我們本地現在有一個名為 busybox:1.28.4 的鏡像,現在我們想要將該鏡像推送到我們的私有倉庫中去,應該怎樣操作呢?首先我們需要給該鏡像重新打一個具有 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 倉庫搭建成功了,大家可以嘗試去創建一個私有的項目,然后創建一個新的用戶,使用這個用戶來進行 pull/push 鏡像,Harbor 還具有其他的一些功能,比如鏡像復制,Helm Chart 包托管等等,大家可以自行測試,感受下 Harbor 和官方自帶的 registry 倉庫的差別。
























