坑边闲话:容器是现代开发、运维都要学习的技术,开发人员要让应用的逻辑尽可能地做到可水平扩展、可监控状态,运维人员要尽可能地将服务以云原生的方式部署并实现高可用。在这个过程中,镜像管理非常重要,这涉及到软件的供应链安全,也关系到整个 DevOps 的流程顺滑性。

1. Harbor 简介·

在使用容器的时候,我们实际需要的是进程。进程是一个动态的概念,它需要某些二进制代码、可执行文件。后这就是我们说的 Application. 在传统环境中,我们需要到软件官网将程序下载下来,然后装入内存,得到一个动态的进程。使用容器部署任务与此类似,毕竟容器只是加了更强的隔离属性的进程。容器所需要的代码软件就是镜像。

这么说也不完全,毕竟容器在生成的时候可以指定目录映射,将本地的可执行程序映射到容器空间。但是传统的以容器为软件持久化存储的环境中,软件只来自于镜像,不来自于本地的文件映射。

因此,镜像与软件有相似的属性,也面临相似的问题。比如,容器也面临网络传播的体积限制,我们总是希望打包出来体积越小越好。此外,软件的供应链安全也是一个敏感话题,如何对镜像进行签名也是一个绕不开的问题。

如何才能像 Apple 的 App Store 一样将软件的上架、分发、更新等流程管理好呢?那当然是搭建一个属于自己的 Image Store 啊!

铺垫了这么多,笔者就是想说明搭建一套属于自己的镜像服务是多么的重要!

  • 如果有几十个 Node 都要拉取某个镜像,那么同时从 DockerHub 拉取就容易造成外线拥堵,甚至有可能触发 DockerHub 的 DDoS 防御。但如果使用 Harbor 先把副本拉下来,然后其他 Node 通过内网从 Harbor 获取镜像便可绕过该问题。因此,自建 Registry 的缓存功能在生产中非常重要。
  • 如果某个镜像非常大,而且性质有些敏感,不宜推送到公共镜像托管平台,则将之托管到内网里的 Harbor 私有 Registry 就非常合适。
    • 一来不用被外线带宽限制
    • 二来可以保护镜像的私密性

2. Harbor 部署·

2.1 容器部署的一般方式·

在 Windows 系统上执行程序,可以通过双击 exe 可执行文件的方式。其中的逻辑是

  • 首先找到程序在文件系统中的路径
  • Windows 的文件系统子系统将可执行文件从磁盘块设备中逐块读取到内存,并将 PE 文件头部解析出来装入进程结构体,最终交付给进程调度器。
  • CPU 开始轮转,从宏观上看就是操作系统启动了一个进程

在 Linux 上执行 SRv4 的 ELF 文件也具有类似的逻辑。前面我们提到,容器和镜像的关系,十分类似于进程和可执行程序。

  • exe 文件即容器技术中的镜像
  • 进程即容器技术中的容器实例

既然如此,容器也跟进程类似,他将拥有

  • 特权级
  • 是否是守护进程

等属性。反映到 Docker 命令中就是 --priviledge-d 两个 docker run 启动参数。

在云原生领域,我们认为服务以容器方式部署,因此这种服务都是要在后台常驻,而不是执行完一闪而过。在实际开发与测试中,我们只是希望借助容器的环境执行一些我们的代码,因此可以通过目录、文件映射的方式,让容器执行我们的代码,随后退出容器(即杀死进程)并删除容器。关于最后一点,有些新手或许比较好奇,为什么容器的退出和普通进程的退出不一样呢?普通进程退出就什么都没了,但是容器退出后还会在 docker ps -a 中留下一个表项。这一点很好理解,保存一个表项主要是为了方便我们后续快速重新启动这个容器。此外,容器具有部分虚拟机的性质,比如容器启动之后可能就是在空转,里面并没有什么代码在跑,只有容器中的某个具体可执行文件被运行的时候,容器才是真正开始执行。因此,可以把容器退出后留下一个表项理解为虚拟机关机。

因为容器的启动需要大量参数,在复杂的部署场景中这并不是个容易处理的工作。因此程序员发明了 compose,通过用 YAML 配置文件指定启动容器时的参数,可以快速启动容器。当然,在这个 Kubernetes 一统云原生的时代,K8S 配置脚本可能更受欢迎一些,只是在个人单机应用场景下,compose YAML 脚本更方便罢了。

2.2 总结·

3. Harbor 实战·

Harbor 的上手颇为繁琐,因为它是一个多容器组成的服务,配置文件比较复杂。

此外,Harbor 的 docker compose 配置文件需要根据 YAML 模板指导生成,因此先要理解 Harbor 的模板文件。

3.1 Harbor 的安装·

首先下载 Harbor 最新的安装包,我建议选择在线版本,其中不包含 docker images,因此可以在线拉取。

解压之后,会得到目录 harbor,结构如下:

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
├── common
│   └── config
│   ├── core
│   │   ├── app.conf
│   │   ├── certificates
│   │   └── env
│   ├── db
│   │   └── env
│   ├── jobservice
│   │   ├── config.yml
│   │   └── env
│   ├── log
│   │   ├── logrotate.conf
│   │   └── rsyslog_docker.conf
│   ├── nginx
│   │   ├── conf.d
│   │   └── nginx.conf
│   ├── portal
│   │   └── nginx.conf
│   ├── registry
│   │   ├── config.yml
│   │   └── passwd
│   ├── registryctl
│   │   ├── config.yml
│   │   └── env
│   └── shared
│   └── trust-certificates
├── common.sh
├── harbor.yml # 复制模板文件,去除 .tmpl 后缀并编辑所得,原本并没有。
├── harbor.yml.tmpl # 模板文件
├── install.sh
├── LICENSE
└── prepare

随后,执行:

1
./prepare

即可得到一个 docker-compose.yml 文件,以此启动服务即可。

下面将对模板文件的主要内容做讲解。harbor.yml 大概有三百多行,其中大部分是注释。

  • data_volume, Harbor 数据存储的目录
  • external_url, Harbor 服务使用的 URL

3.2 启用 HTTPS·

最佳实践

HTTPS 一般是通过反向代理实现的。由于 Harbor 一般是内网服务,但是包括 K8S 在内的许多服务在访问 Registry 的时候,需要以 HTTPS 的方式发起访问。

对外服务的环境我们非常熟悉,通过 Nginx Ingress 调度器可以很轻松地实现服务上线。然而,内网中的机器访问内网的服务时,再通过 Nginx 就不合适了,毕竟服务本身就触手可及,何必再让 Nginx 转发一遍呢?

然而,不让 Nginx 转发,就没有办法实现 HTTPS 包装。有了包装,也就有了转发负载。这个问题恐怕我们得妥协,因为所有外域流量都走一遍 Nginx 是不可绕过的。

但是情况可能还会更加糟糕。如果我们的 Nginx 在内网里,只是通过路由器做 80443 端口的映射,则内网机器解析 Nginx 时会解析到公网 IP,然后走默认路由到路由器,路由器发现目的 IP 就是路由器自己,端口还需要把转发,因此就绕回内网。此时可以发现,流量白白走了一遍路由器。 这可如何是好?

答案就是在内往里将 Nginx 服务做 DNS 重写,如此一来,访问 Nginx 就可以绕过路由器,直达内网地址。

3.3 客户端如何使用 Harbor 服务·

4. 使用 Cosign 对镜像签名·

Harbor 的存在就是为了更好地对容器镜像进行管理。所谓良好的管理,不仅仅要实现完备的功能,还需要实现较高的安全性和可用性。古往今来,软件的供应链安全问题经久不衰,镜像作为传统软件的扩展,也面临同样的问题。然而,镜像最初只是为了满足可用性,并没有在格式定义上对完整性校验、数字签名等功能做特殊处理。如今镜像的使用已经非常普及,再造一个标准难以为市场接纳,扩展现有标准在技术上困难重重。所以我们该如何抉择?

问题确实存在,问题必须解决!

使用 Cosign 工具对容器进行签名,随后将签名单独存储于 Registry 或许是个可行的方案。CosignSigStore 组织的核心工具之一,如今主流的 Linux 发行版已经在官方库里提供了 Cosign. 我们可以借助 Cosign 轻松实现对 OCI 容器镜像的数字签名。

通过参考官方文档可以快速安装 Cosign,以 Debian 12 为例:

1
sudo apt install cosign

4.1 什么是数字签名·

数字签名是公钥密码体系里的一个概念,在现实生活中也应用极广。在介绍数字签名之前,先要介绍公钥密码的特性。公钥密码与对称密码不同,它有两个密钥,分别是公钥(Public Key)和私钥(Private Key),其中,私钥一般用 key 来指代。如果有人问你要 Key,他一般是要你的私钥,要坚决保密!

  • 私钥保持在你私人手里,绝对不能告诉其他任何人;
  • 公钥可以通过直接分发的方式散播出去,或者以证书的方式散播;

在加解密特性上要满足:

  • 公钥加密的内容,只有私钥能解密
  • 私钥可以对内容做签名,用公钥可以验签

其中的数学原理比较复杂,涉及到有限域运算的一些定理,此处不展开介绍。

4.2 生成 Cosign Key·

通过以下命令可以生产 Cosign 公私密钥对:

1
cosign generate-key-pair

因为 Cosign 要求对私钥加密,因此这个过程会要求你输入一个解密的口令。留空即可创建无加密的密钥对。

随后,你将得到一对公私钥:

1
2
cosign.key
cosign.pub

4.3 使用 Cosign 对本地的镜像做签名·

1
cosign sign --key /path/to/cosign.key <image_name>:<tag>

Cosign 签名是需要联网的,因为上述命令会完成:

  1. 生成签名
  2. 上传签名到 Registry

登录到 Harbor,即可查看 Cosign 的结果。

4.3 验证 Cosign 签名·

在下载了一个镜像之后,我们可以像镜像的提供者索要他的 Cosign 公钥,并以此做验证:

1
cosign verify --key /path/to/cosign.pub <image_name>:<tag>  | jq

通过上述命令,即可验证签名的有效性。由于结果是 Json 格式,所以用 jq 命令可查看结果。