Mallux - 宁静致远

04 - Docker 私有仓库

Docker registry

2015 年四月份,随着 Docker registry 2.0 把版本的发布,之前 Python 版本实现的 registry 正式被标记为 ‘deprecated’。V2 版本用 go 实现,在安全性和性能上做了诸多优化,并重新设计了镜像的存储的格式,带来了翻天覆地的变化。

Registry v2 在镜像存储方面不仅支持本地盘,还支持诸多主流第三方存储方案。通过分布式存储系统你还可以实现一个分布式 Docker Registry 服务。

背景

Registry 作为 Docker 的核心组件之一负责镜像内容的存储与分发,客户端的 docker pull 以及 push 命令都将直接与 registry 进行交互。最初版本的 registry 由 Python实现。由于设计初期在安全性,性能以及 API 的设计上有着诸多的缺陷,该版本在 0.9 之后停止了开发,新的项目 distribution 来重新设计并开发下一代 registry。新的项目由 go 语言开发,所有的 API,底层存储方式,系统架构都进行了全面的重新设计已解决上一代 registry 中存在的问题。四月份 rgistry 2.0 正式发布,docker 1.6 版本开始支持 registry 2.0, 八月份随着 docker 1.8 发布,docker hub 正式启用 2.1 版本 registry 全面替代之前版本 registry。

新版 registry 对镜像存储格式进行了重新设计并和旧版不兼容,docker 1.5 和之前的版本无法读取 2.0 的镜像。同时旧版本的 registry 中已有大量的镜像需要迁移到新版本 registry 中。

Registry V2 的变化

镜像 id 改进

Docker build 镜像时会为每个 layer 生成一串 layer id,这个 layer id 是一个客户端随机生成的字符串,和镜像内容无关。我们可以通过一个简单的例子来查看。

这里选择官方的 nginx dockerfile 来 build 镜像

1
# docker build -t nginx:dockerfile .

接下来重新 build 该镜像,加上 –no-cache 强制重新 build

1
# docker build --no-cache=true -t nginx:dockerfile .

可以看到除了基础镜像 debian 的两层 layer id 一致,其余 layer 的 id 都发生了变化。这种随机 layer id 以及 layer id 与内容无关的设计会带来很多的问题。

首先, registry v1 通过 id 来判断镜像是否存在,客户需不需要重新 push,而由于镜像内容和 id 无关,再重新 build 后 layer 在内容不变的情况下很可能 id 发生变化,造成无法利用 registry 中已有 layer 反复 push 相同内容。服务器端也会有重复存储造成空间浪费。

其次,尽管 id 由 32 字节组成但是依然存在 id 碰撞的可能,在存在相同 id 的情况下,后一个 layer 由于 id 和仓库中已有 layer 相同无法被 push 到 registry 中,导致数据的丢失。用户也可以通过这个方法来探测某个 id 是否存在。

最后,同样是由于这个原因如果程序恶意伪造大量 layer push 到 registry 中占位会导致新的 layer 无法被 push 到 registry 中。
Docker 官方重新设计新版 registry 的一个主要原因也就是为了解决该问题。

新版的 registry 吸取了旧版的教训,在服务器端会对镜像内容进行哈希,通过内容的哈希值来判断 layer 在 registry 中是否存在,是否需要重新传输。这个新版本中的哈希值被称为 digest 是一个和镜像内容相关的字符串,相同的内容会生成相同的 digest。

由于 digest 和内容相关,因此只要重新 build 的内容相同理论上讲无需重新 push,但是由于安全性的考量在特定情况下 layer 依然要重新传输。由于 layer 是按 digest 进行存储,相对 v1 按照随机 id 存储可以大幅减小磁盘空间占用。registry 服务端会对冲突 digest 进一步进行处理,同时由于 digest 是由 registry 服务端生成,用户无法伪造 digest 也很大程度上保证了 registry 内容的安全性。

安全性改进

除了对 image 内容进行唯一性哈希外,新版 registry 还在鉴权方式以及 layer 权限上上进行了大幅度调整。

鉴权方式

旧版本的服务鉴权模型如下图所示:

该模型每次 client 端和 registry 的交互都要多次和 index 打交道,新版本的鉴权模型去除了上图中的第四第五步,如下图所示:

新版本的鉴权模型需要 registry 和 authorization service 在部署时分别配置好彼此的信息,并将对方信息作为生成 token 的字符串,已减少后续的交互操作。新模型客户端只需要和 authorization service 进行一次交互获得对应 token 即可和 registry 进行交互,减少了复杂的流程。同时 registry 和 authorization service 一一对应的方式也降低了被攻击的可能。

registry v2 push/pull 流程

  1. docker client 尝试到 registry 中进行 push/pull 操作;
  2. registry 会返回 401 未认证信息给 client(未认证的前提下),同时返回的信息中还包含了到哪里去认证的信息;
  3. client 发送认证请求到认证服务器(authorization service);
  4. 认证服务器(authorization service)返回 token;
  5. client 携带这附有 token 的请求,尝试请求 registry;
  6. registry 接受了认证的 token 并且使得 client 继续操作。

权限控制

旧版的 registry 中对 layer 没有任何权限控制,所有的权限相关内容都由 index 完成。在新版 registry 中加入了对 layer 的权限控制,每个 layer 都有一个 manifest 来标识该 layer 由哪些 repository 共享,将权限做到 repository 级别。

Pull 性能改进

旧版 registry 中镜像的每个 layer 都包含一个 ancestry 的 json 文件包含了父亲 layer 的信息,因此当我们 pull 镜像时需要串行下载,下载完一个 layer 后才知道下一个 layer 的 id 是多少再去下载。如下图所示:

新版 registry 在 image 的 manifest 中包含了所有 layer 的信息,客户端可以并行下载所有的 layer 如下图所示:

其他改进

  • 全新的 API
  • push 和 pull 支持断点
  • 后端存储的插件化
  • notification 机制

Docker 私有仓库的实现

http://tonybai.com/2016/02/26/deploy-a-private-docker-registry/

docker-registry(基于 Nginx + 私有 CA)

基于最早的 v2 实现,不做上传。除了基于 docker registry 实现的私库以外,还有比较流行的开源实现方式有:

  • Harbor:VMWare 企业级 Docker 私库实现方式,支持多节点复制,应用较多。
  • Nexus3:支持命令行搜索镜像仓库,可实现自托管和 Proxy 两种方式。常见于应用于 maven 等 JAVA 私库的实现。

Harbor

[Official] https://vmware.github.io/harbor/index_cn.html

Harbor 是一个用于存储和分发 Docker 镜像的企业级 Registry 服务器,基于官方 Registry V2 实现,通过添加一些企业必需的功能特性,例如安全、标识和管理等,扩展了开源 Docker Distribution。作为一个企业级私有 Registry 服务器,Harbor 提供了更好的性能和安全。提升用户使用 Registry 构建和运行环境传输镜像的效率。Harbor 支持安装在多个 Registry 节点的镜像资源复制,镜像全部保存在私有 Registry 中, 确保数据和知识产权在公司内部网络中管控。另外,Harbor 也提供了高级的安全特性,诸如用户管理,访问控制和活动审计等。

  • 基于角色的访问控制 - 用户与 Docker 镜像仓库通过“项目”进行组织管理,一个用户可以对多个镜像仓库在同一命名空间(project)里有不同的权限。

  • 镜像复制 - 镜像可以在多个 Registry 实例中复制(同步)。尤其适合于负载均衡,高可用,混合云和多云的场景。

  • 图形化用户界面 - 用户可以通过浏览器来浏览,检索当前 Docker 镜像仓库,管理项目和命名空间。

  • AD/LDAP 支持 - Harbor 可以集成企业内部已有的 AD/LDAP,用于鉴权认证管理。

  • 审计管理 - 所有针对镜像仓库的操作都可以被记录追溯,用于审计管理。

  • 国际化 - 已拥有英文、中文、德文、日文和俄文的本地化版本。更多的语言会添加进来。

  • RESTful API - RESTful API 提供给管理员对于Harbor更多的操控, 使得与其它管理软件集成变得更容易。

  • 部署简单 - docker-compose和离线安装。

Harbor 架构

  • Proxy
  • Registry
  • Core services
  • Database
  • Job Services
  • Log collector

这几个 Contianer 通过 Docker link的形式连接在一起,在容器之间通过容器名字互相访问。对终端用户而言,只需要暴露 proxy(即 Nginx)的服务端口

Proxy

Harbor 的 registry、UI、token等服务,通过一个前置的反向代理统一接收浏览器、Docker 客户端的请求,并将请求转发给后端不同的服务。

运行容器 proxy: 由 Nginx 服务器构成的反向代理。

基础镜像: library/nginx:1.9

Registry

负责储存 Docker 镜像,并处理 docker push / pull 命令。由于我们要对用户进行访问控制,即不同用户对 Docker image 有不同的读写权限,Registry 会指向一个 token 服务,强制用户的每次 docker pull / push 请求都要携带一个合法的 token,Registry 会通过公钥对 token 进行解密验证。

运行容器 registry:由 Docker 官方的开源 registry 镜像构成的容器实例。

基础镜像: library/registry:2.5.0

Core services

这是 Harbor 的核心功能,主要提供以下服务:

  • UI:提供图形化界面,帮助用户管理 registry 上的镜像(image),并对用户进行授权。

  • webhook:为了及时获取 registry 上 image 状态变化的情况,在 Registry 上配置 webhook,把状态变化传递给 UI 模块。

  • token 服务:负责根据用户权限给每个 push / pull 请求签发 token。Docker 客户端向 Registry 服务发起的请求,如果不包含 token,会被重定向到这里,获得 token 后再重新向 Registry 进行请求。

运行容器 ui:即架构中的 core services,构成此容器的代码是 Harbor 项目的主体。

基础镜像: library/golang:1.6.2

Database

为 coreservices 提供数据库服务,负责储存用户权限、审计日志、Docker image 分组信息等数据。

运行容器 mysql:由官方 MySql 镜像构成的数据库容器。

基础镜像: library/mysql:5.6

Job Services

提供镜像远程复制功能,可以把本地镜像同步到其他 Harbor 实例中。

运行容器 job services:通过状态机机制实现远程镜像复制功能,包括镜像删除也可以同步到远端 Harbor 实例。

基础镜像: library/golang:1.6.2

Log collector

为了帮助监控 Harbor 运行,负责收集其他组件的 log,供日后进行分析。

运行容器 log:运行着 rsyslogd 的容器,通过 log-driver 的形式收集其他容器的日志。

基础镜像: library/ubuntu:14.04

部署 harbor

  1. 下载 harbor
  2. 配置 harbor.cfg
  3. 运行 installl.sh

下载 harbor 代码

[Release] https://github.com/vmware/harbor/releases

  • 方式一:在线安装(源码包)
1
2
3
4
5
6
[root@c7-Host ~]# git clone https://github.com/vmware/harbor /opt/harbor
Cloning into '/opt/harbor'...
remote: Counting objects: 13050, done.
remote: Total 13050 (delta 0), reused 0 (delta 0), pack-reused 13050
Receiving objects: 100% (13050/13050), 11.45 MiB | 186.00 KiB/s, done.
Resolving deltas: 100% (8281/8281), done.
  • 方式二:离线安装包
1
# wget https://github.com/vmware/harbor/releases/download/0.5.0/harbor-offline-installer-0.5.0.tgz

配置 harbor.cfg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
## Configuration file of Harbor
#The IP address or hostname to access admin UI and registry service.
#DO NOT use localhost or 127.0.0.1, because Harbor needs to be accessed by external clients.
hostname = reg.mydomain.com
#The protocol for accessing the UI and token/notification service, by default it is http.
#It can be set to https if ssl is enabled on nginx.
### 指定以 http 协议进行认证,需要更改 docker 启动参数,指定 --insecure-registry
ui_url_protocol = http
#Email account settings for sending out password resetting emails.
email_server = smtp.mydomain.com
email_server_port = 25
email_username = sample_admin@mydomain.com
email_password = abc
email_from = admin <sample_admin@mydomain.com>
email_ssl = false
##The initial password of Harbor admin, only works for the first time when Harbor starts.
#It has no effect after the first launch of Harbor.
#Change the admin password from UI after launching Harbor.
harbor_admin_password = Harbor12345
##By default the auth mode is db_auth, i.e. the credentials are stored in a local database.
#Set it to ldap_auth if you want to verify a user's credentials against an LDAP server.
auth_mode = db_auth
#The url for an ldap endpoint.
ldap_url = ldaps://ldap.mydomain.com
#A user's DN who has the permission to search the LDAP/AD server.
#If your LDAP/AD server does not support anonymous search, you should configure this DN and ldap_search_pwd.
#ldap_searchdn = uid=searchuser,ou=people,dc=mydomain,dc=com
#the password of the ldap_searchdn
#ldap_search_pwd = password
#The base DN from which to look up a user in LDAP/AD
ldap_basedn = ou=people,dc=mydomain,dc=com
#Search filter for LDAP/AD, make sure the syntax of the filter is correct.
#ldap_filter = (objectClass=person)
# The attribute used in a search to match a user, it could be uid, cn, email, sAMAccountName or other attributes depending on your LDAP/AD
ldap_uid = uid
#the scope to search for users, 1-LDAP_SCOPE_BASE, 2-LDAP_SCOPE_ONELEVEL, 3-LDAP_SCOPE_SUBTREE
ldap_scope = 3
#The password for the root user of mysql db, change this before any production use.
db_password = root123
#Turn on or off the self-registration feature
self_registration = on ### 是否允许用户注册,设置为 off 时,只能由管理员进行创建
#Determine whether the UI should use compressed js files.
#For production, set it to on. For development, set it to off.
use_compressed_js = on
#Maximum number of job workers in job service
max_job_workers = 3
#Secret key for encryption/decryption of password of remote registry, its length has to be 16 chars
#**NOTE** if this changes, previously encrypted password will not be decrypted!
#Change this key before any production use.
secret_key = secretkey1234567
#The expiration time (in minute) of token created by token service, default is 30 minutes
token_expiration = 30
#Determine whether the job service should verify the ssl cert when it connects to a remote registry.
#Set this flag to off when the remote registry uses a self-signed or untrusted certificate.
verify_remote_cert = on
#Determine whether or not to generate certificate for the registry's token.
#If the value is on, the prepare script creates new root cert and private key
#for generating token to access the registry. If the value is off, a key/certificate must
#be supplied for token generation.
customize_crt = on
#Information of your organization for certificate
crt_country = CN
crt_state = State
crt_location = CN
crt_organization = organization
crt_organizationalunit = organizational unit
crt_commonname = example.com
crt_email = example@example.com
#####

更新配置

1
[root@c7-Host Deploy]# ./prepare

运行 harbor 环境

  • 指定 registry 源

默认时,docker-compose.yml 中的 5 个组件的 image 是指向 docekr.hub,在contrib/prebuild-install/目录下有个update_compose.sh脚本,可以让更改指定的 registry 源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[root@iZ94lldbge5Z prebuild-install]# ./update_compose.sh
Please enter the registry service you want to pull the pre-built images from.
Enter 1 for Docker Hub.
Enter 2 for Daocloud.io (recommended for Chinese users).
or enter other registry URL such as https://my_registry/harbor/ .
The default is 1 (Docker Hub): 2
Succeeded!
Please follow the normal installation process to install Harbor.
[root@iZ94lldbge5Z prebuild-install]# grep image /opt/harbor/Deploy/docker-compose.yml
image: daocloud.io/harbor/deploy_log:0.3.0
image: library/registry:2.5.0
image: daocloud.io/harbor/deploy_mysql:0.3.0
image: daocloud.io/harbor/deploy_ui:0.3.0
image: daocloud.io/harbor/deploy_jobservice:0.3.0
image: library/nginx:1.9
  • docker-compose up -d

运行 docker-compose,有可能出现以下报错

1
pkg_resources.DistributionNotFound: backports.ssl-match-hostname>=3.5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[root@c7-Host Deploy]# pip install --upgrade backports.ssl_match_hostname
[root@c7-Host Deploy]# docker-compose up -d
Creating network "deploy_default" with the default driver
Building log
Step 1 : FROM library/ubuntu:14.04
14.04: Pulling from library/ubuntu
862a3e9af0ae: Pull complete
6498e51874bf: Pull complete
159ebdd1959b: Pull complete
0fdbedd3771a: Pull complete
7a1f7116d1e3: Pull complete
Digest: sha256:43d06a8f67d84647451fe7f6dcf97718d0cd8c243f8442737efb71c450a3ef93
Status: Downloaded newer image for ubuntu:14.04
...
Status: Downloaded newer image for nginx:1.9
Creating deploy_log_1
Creating deploy_mysql_1
Creating deploy_ui_1
Creating deploy_registry_1
Creating deploy_jobservice_1
Creating deploy_proxy_1

更改 Docker 启动参数,指定 –insecury-registry

默认时,client 与 Registry 的交互是通过 https 通信的。在 install Registry 时,若未配置任何tls 相关的 key 和 crt 文件,https 访问必然失败。使用 "--insecure-registry <harbor IP>"可以指定 client 与 Registry 以 http 的方式进行通信。

1
2
[root@c7-QHost ~]# grep 'insecure-registry' /usr/lib/systemd/system/docker.service
ExecStart=/usr/bin/dockerd -g /data/docker --registry-mirror=https://hub.c.163.com --insecure-registry=172.16.0.11

重载、重启 docker 服务,验证配置是否生效

1
2
3
4
5
6
7
8
[root@c7-QHost ~]# systemctl daemon-reload
[root@c7-QHost ~]# systemctl restart docker
[root@c7-QHost ~]# docker info
...
Insecure Registries:
172.16.0.11
127.0.0.0/8

harbor 访问

测试

1
2
3
4
[root@c7-Host Deploy]# docker login 172.16.0.11
Username: mallux
Password:
Login Succeeded

Documents

docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
version: '2'
services:
log:
image: vmware/harbor-log:0.5.0
container_name: harbor-log
restart: always
volumes:
- /var/log/harbor/:/var/log/docker/
ports:
- 1514:514
registry:
image: library/registry:2.5.0
container_name: registry
restart: always
volumes:
- /data/harbor/registry:/storage
- ./common/config/registry/:/etc/registry/
environment:
- GODEBUG=netdns=cgo
command:
["serve", "/etc/registry/config.yml"]
depends_on:
- log
logging:
driver: "syslog"
options:
syslog-address: "tcp://127.0.0.1:1514"
tag: "registry"
mysql:
image: vmware/harbor-db:0.5.0
container_name: harbor-db
restart: always
volumes:
- /data/database/mysql:/var/lib/mysql
env_file:
- ./common/config/db/env
depends_on:
- log
logging:
driver: "syslog"
options:
syslog-address: "tcp://127.0.0.1:1514"
tag: "mysql"
ui:
image: vmware/harbor-ui:0.5.0
container_name: harbor-ui
env_file:
- ./common/config/ui/env
restart: always
volumes:
- ./common/config/ui/app.conf:/etc/ui/app.conf
- ./common/config/ui/private_key.pem:/etc/ui/private_key.pem
- /data/harbor/:/harbor_storage
depends_on:
- log
logging:
driver: "syslog"
options:
syslog-address: "tcp://127.0.0.1:1514"
tag: "ui"
jobservice:
image: vmware/harbor-jobservice:0.5.0
container_name: harbor-jobservice
env_file:
- ./common/config/jobservice/env
restart: always
volumes:
- /data/harbor/job_logs:/var/log/jobs
- ./common/config/jobservice/app.conf:/etc/jobservice/app.conf
depends_on:
- ui
logging:
driver: "syslog"
options:
syslog-address: "tcp://127.0.0.1:1514"
tag: "jobservice"
proxy:
image: nginx:1.11.5
container_name: nginx
restart: always
volumes:
- ./common/config/nginx:/etc/nginx
ports:
- 80:80
- 5000:443
depends_on:
- mysql
- registry
- ui
- log
logging:
driver: "syslog"
options:
syslog-address: "tcp://127.0.0.1:1514"
tag: "proxy"

Harbor 镜像复制

Harbor 的复制实现

Harbor 可实现基于策略的 Docker 镜像复制功能,可在不同的数据中心、不同的运行环境之间同步镜像,并提供友好的管理界面,大大简化了实际运维中的镜像管理工作,已经有用户部署了远程镜像双向复制的案例。

功能简介

在功能设计方面,Harbor 仍然以“项目”为中心, 通过对项目配置“复制策略”,标明需要复制的项目以及镜像。管理员在复制策略中指明目标实例,即复制的“目的地”,并对它的地址和连接时使用的用户名密码进行设置。当复制策略被激活时,源项目下的所有镜像,都会被复制到目标实例;此外,当源项目下的镜像被添加或删除(push或delete), 只要策略还在激活状态,镜像的变化都会同步到目标实例上去, 如下图所示:

在较大的容器集群中,往往需要多个 Registry 服务器做负载均衡,可以采用主从发布模式,镜像只需要发布一次,就可以推送到多个 Registry 实例中。同时还支持双主复制和层次型的多级镜像发布,如下图所示:

设计与实现

在不同的 Registry 实例之间复制镜像是十分普遍的需求,过去常见的做法是通过拷贝镜像数据,比如定期通过 rsync 同步文件系统中镜像的数据,或者,对于部署在 IaaS 服务上的情况,通过对 IaaS 存储服务一层进行配置实现对象复制,这些方法往往是根据 registry 使用的存储而采用不同工具。然而对于 Harbor 来说,我们希望降低这种依赖,并提高灵活性, 比如用户可能有一个开发用的 registry 使用文件系统作为存储,并希望把镜像同步到基于 S3 存储的远端发布用的 registry 上。考虑到这种情况,我们选择通过调用 registry 本身的 API 下载并传输镜像,从而做到了与下层存储无关。

在控制方面,Harbor 内置的Job Service,用来对镜像复制任务进行管理。当以项目为单位进行复制时,会以镜像为单位生成一系列任务(job)由 Job Service 调度管理,Job Service 在执行任务的过程中将每个任务的状态更新到数据库中, 以便用户通过 UI 查看。大体结构如下图所示:

Job Service 的实现,从外部看它也是通过REST API接收请求调度并执行任务,面临的问题主要有两点,首先,接收到大量复制请求时需要进行限流以免消耗过多 IO 资源;其次,复制策略有可能在任务执行过程中改变,比如失效,这就需要一种机制能从外界对运行中的任务进行干预。

通过任务队列,分发器(dispatcher)和 worker pool 实现了生产者消费者模型,利用 Go 语言内置的 channel,每个任务会通过 scheduler 放到 channel 里,dispatcher 通过 channel 获得任务,同时,worker 在工作结束后会被放入另一个 channel,dispatcher 通过这个 channel 与 worker 配对,于是,空闲的 worker 通过 dispatcher 获得任务 id 并执行任务,这样可以很方便地通过 worker pool 中 worker 数量来控制并发数:

对于另一个问题,每一个 worker 内部是一个抽象的状态机(state machine),通过给不同状态注册处理器(handler)完成具体工作,同时,状态机可以受到干预,可以中途取消(cancel)任务,或在任务执行发生异常时将任务置为错误(error)状态丢弃或交给调度器(scheduler)重试。 另外由于状态机的状态是可定制的,这样就很方便扩展和调整。对于一个抽象的任务来说,它的状态转移如下图所示:

而对于具体远程同步镜像的任务来说,Running 状态会被进一步细分成多个子状态,如下图所示:

首先, 从源 Harbor 实例下载相应 tag 的 manifest,分析其所包含的 blob,针对每一个 blob,检查其在目标实例中是否已经存在,如果不存在,则同步此 blob。最后,检查 manifest 在目标实例中是否已存在,如果不存在,则上传 manifest。检查 blob 的存在性,可以有效减少不必要的网络流量;而由于 manifest 的上传有可能会触发镜像的同步,所以对 manifest 存在性的检查,则可以避免当同步的多个 Harbor 形成环路时进入不断同步的死循环状态。对同一个镜像中的每一个 tag 重复以上过程,就可以完成整个镜像的同步工作。

Harobr 的 CI 与高可用

CI 应用

高可用方案

  • LB:http 反向代理
  • 存储:Ceph

2.3.3 部署 Harbor 复制

Harbor 的复制配置很简单,因其通过 http 的 REST API 请求来实现,因此只需配置一个管理员账号,可对复制目的地的 Harbor 项目拥有读写权限即可。

注意事项

若 Harbor 没用启用 https 的认证方式(Docker 指定--insecure-registry),那么在配置复制目标时,“终端 URL” 必须跟 “–insecure-registry” 指定的地址保持一致,否则 Harbor 将始终于 https 的方式来进行认证,同步失败。

因此,在 “–insecure-registry” 模式下,http 方式的、基于 NAT 映射的跨网络复制是无法实现的。这种模式下,你需要基于 VPN 网络来实现,比较简单的 VPN 实现方式,如 Tinc。

部署实现

  1. 跨数据中心,Docker 以 “–insecure-registry” 模式运行时,打通内部网络;
  2. 配置 Harbor 复制(Web 管理平台)
    • “管理员选项” -> “系统管理” -> “新建目标”
    • “项目” -> 选择某一个需要复制的项目 -> “复制” -> “新增策略”

Harbor 离线安装

[Download] https://github.com/vmware/harbor/releases/

Harbor v0.5.0

下载离线安装包:

1
2
3
# cd /opt/
# wget https://github.com/vmware/harbor/releases/download/0.5.0/harbor-offline-installer-0.5.0.tgz
# tar -xvf harbor-offline-installer-0.5.0.tgz

启用 HTTPS 认证(自签,若不启用 https,忽略)

openssl 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# mkdir -p /data/harbor/certs
# cd /data/harbor/certs
# cp -af /etc/pki/tls/openssl.cnf openssl.cnf
### 追加、修改配置部分:
---
[ req ]
distinguished_name = req_distinguished_name
req_extensions = v3_req
[ req_distinguished_name ]
countryName_default = CN
stateOrProvinceName_default = GuangDong
localityName_default = ShenZhen
0.organizationName_default = mallux.org
organizationalUnitName_default = OpsDev
emailAddress = admin@mallux.org
[ v3_req ]
subjectAltName = @alt_names
[ alt_names ]
IP.1 = 192.168.112.5
DNS.1 = registry.mallux.org

生成私钥:

1
# ( umask 077 ; openssl genrsa -out registry.key 2048 )

生成证书签署请求(--config):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# openssl req -new -key registry.key -out registry.csr -config openssl.cnf
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [CN]:
State or Province Name (full name) [GuangDong]:
Locality Name (eg, city) [ShenZhen]:
Organization Name (eg, company) [mallux.org]:
Organizational Unit Name (eg, section) [OpsDev]:
Common Name (eg, your name or your server's hostname) []:registry.mallux.org
admin@mallux.org []:
Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

CA 签署证书(--config and --extensions):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# openssl ca -in registry.csr -out registry.crt -days 1000 -config openssl.cnf -extensions v3_req
Using configuration from openssl.cnf
Check that the request matches the signature
Signature ok
Certificate Details:
Serial Number: 8 (0x8)
Validity
Not Before: Apr 9 11:18:39 2017 GMT
Not After : Jan 4 11:18:39 2020 GMT
Subject:
countryName = CN
stateOrProvinceName = GuangDong
organizationName = mallux.org
organizationalUnitName = OpsDev
commonName = registry.mallux.org
X509v3 extensions:
X509v3 Basic Constraints:
CA:FALSE
X509v3 Key Usage:
Digital Signature, Non Repudiation, Key Encipherment
X509v3 Subject Alternative Name:
IP Address:192.168.112.5, DNS:registry.mallux.org
Certificate is to be certified until Jan 4 11:18:39 2020 GMT (1000 days)
Sign the certificate? [y/n]:y
1 out of 1 certificate requests certified, commit? [y/n]y
Write out database with 1 new entries
Data Base Updated

  • X509v3 Subject Alternative Name:SANs 多域名(*.mallux.org)和 IP 支持特性

配置 Harbor

harbor.cfg 配置文件

  • 设置邮件
  • 开启 HTTPs、LDAP 认证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
hostname = registry.mallux.org:5000
ui_url_protocol = https
email_server = smtp.exmail.mallux.org
email_server_port = 465
email_username = admin@mallux.org
email_password = password
email_from = docker <reg.docker@mallux.org>
email_ssl = true
harbor_admin_password = hbradmin
auth_mode = ldap_auth
ldap_url = ldaps://192.168.112.5
ldap_basedn = dc=mallux,dc=org
ldap_filter = (&(objectClass=inetOrgPerson)(|(gidNumber=3000)(gidNumber=8000)))
ldap_uid = uid
ldap_scope = 3
db_password = sqlpass
crt_country = CN
crt_state = GuangDong
crt_location = ShenZhen
crt_organization = mallux.org
crt_organizationalunit = OpsDev
crt_commonname = mallux.org
crt_email = admin@mallux.org
ssl_cert = /data/harbor/certs/registry.crt
ssl_cert_key = /data/harbor/certs/registry.key
  • auth_modeldap_auth,启用 LDAP 认证。开启后,原 db_auth 的认证,除了 Harbor 默认的 admin 管理员用户以外,其它的用户全部无法登陆。
  • ldap_basedn:LDAP 默认搜索域
  • ldap_filter:过滤 LDAP 搜索域结果,这里限制只有 devops(gidNumber 为 3000)和 integration(集成组,gitNumber 为 8000)的用户可以登陆 Harbor。

更改 docker-compose.yml

  • registry 容器
    • 数据卷:/data/harbor/registry:/storage
  • mysql 容器
    • 数据卷:/data/database/mysql:/var/lib/mysql
  • ui 容器
    • 数据卷:/data/harbor/:/harbor_storage
  • jobservice 容器
    • 数据卷:/data/harbor/job_logs:/var/log/jobs
  • proxy 容器
    • 端口:5000:443

注意

  • 当开启 HTTPs 认证时,若修改容器外部的访问端口,例如这里改成 5000。那么还有一个地方需要修改:
    • harbor.cfghostname 更改成 registry.mallux.org:5000,harbor 是根据这个 hostname 生成 pull 镜像的 url

若不改,docker login 时会报如下错误,原因地址不正确:

1
Error response from daemon: Get https://registry.mallux.org:5000/v2/: Get https://192.168.112.5/service/token?account=hj.mallux&client_id=docker&offline_token=true&service=token-service: x509: cannot validate certificate for 192.168.112.5 because it doesn't contain any IP SANs

初始化、启动 Harbor

1
# ./install.sh

注意:每次更改 harbor.cfg 配置以后,重新执行一下 install.sh 即可。

LDAP 新增 Harbor 用户 roUser(只能 pull 镜像)

1
roUser / roPassword
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# cat << EOF | ldapadd -x -D cn=Superadmin,dc=mallux,dc=org -W
dn: uid=roUser,ou=Integration,dc=mallux,dc=org
objectClass: pwdPolicy
objectClass: inetOrgPerson
objectClass: shadowAccount
objectClass: posixAccount
objectClass: top
uid: roUser
uidNumber: 8000
gidNumber: 8000
sn: Harbor
givenName: Registry
cn: Harbor Registry
homeDirectory: /data/harbor
pwdAttribute: userPassword
loginShell: /bin/false
mail: reg.docker@mallux.org
pwdMaxFailure: 5
shadowMax: 99999
shadowMin: 0
shadowWarning: 7
telephoneNumber: 13688888888
userPassword: {SSHA}ZWMZc5khn3H+kCZsEd1QGK8KY/LDcLtN
EOF

客户端 HTTPs login

默认时,启用 HTTPs 认证后 docker 会对证书的签名进行合法性验证,对于自签的证书,还需要进行以下几个设置:

  • 若客户端 docker 启动选项中有 --insecury-registry,需要去掉该设置,不需要设置 tlsverify。
  • 将私有 CA 的 cacert.pem 证书 和 Harbor registry.crt 证书合并,放到本地 CA 信任证书的位置,并更新 CA 配置。

否则在 docker login 登陆时,会一直报 certificate signed by unknown authority

Step 1:合并 cacert.pem 和 registry.cry 证书

1
2
# cat cacert.pem >> registry.crt
# mv registry.crt registry_cacert.crt

Step 2:在 OS 级别信任证书

  • ubuntu
1
2
# cp registry_cacert.crt /usr/local/share/ca-certificates/
# update-ca-certificates
  • CentOS
1
2
# cp registry_cacert.crt /etc/pki/ca-trust/source/anchors/
# update-ca-trust

Step 3:docker login 验证

1
2
3
# docker login -u hj.mallux https://registry.mallux.org:5000
Password:
Login Succeeded
1
2
3
4
5
6
7
8
9
10
11
# cat /root/.docker/config.json
{
"auths": {
"192.168.112.5:5000": {
"auth": "aGoubWFsbHV4OmVkY29rbSMz"
},
"https://registry.mallux.org:5000": {
"auth": "aGoubWFsbHV4OmVkY29rbSMz"
}
}
}

END