使用 Docker 构建 yum/apt 离线源

k8s技术圈

共 4626字,需浏览 10分钟

 ·

2021-09-10 12:19

对于 PaaS toB 产品来讲,客户往往会要求产品的部署方案必须做到离线安装,即在部署时不能依赖任何在线的资源,比如安装一些 OS 软件包时依赖的 yum/apt 源;docker.io、k8s.gcr.io 、quay.io 上面的容器镜像;GitHub 上开源软件的二进制下载文件等。
作为平台部署工具的开发者,始终被离线部署这个难题困扰着。在线的容器镜像和二进制文件比较好解决,因为这些资源是与 OS 无关的,只要下载下来放到安装包里,部署的时候启动一个 HTTP 服务器和镜像仓库服务提供这些资源的下载即可。
但是对于 yum/apt 之类的软件来讲并不那么简单:
  • 首先由于各个包之间的依赖关系比较复杂,并不能将它们直接下载下来;
  • 其次即便下载下来之后也无法直接通过 yum/apt 的方式安装指定的软件包,虽然也可以使用 scp 的方式将这些包复制到部署节点,通过 rpm 或 dpkg 的方式来安装上,但这样并不是很优雅,而且通用性能也不是很好;
  • 最后需要适配的 Linux 发行版和包管理器种类也有多种,而且有些包的包名或者版本号在不同的包管理之间也相差甚大,无法做到统一管理。
综上,将平台部署依赖的在线 yum/apt 之类的软件包资源制作成离线安装包是一件很棘手的事情。个人就这个问题折腾了一段时间,终于找到了一个比较合适的解决方案:即通过一个 YAML 配置文件来管理包,然后使用 Dockerfile 来构建成离线的 tar 包或者容器镜像。如果有类似需求的小伙伴,可以参考一下本方案。
K8sMeetup
Docker build
传统制作离线源的方式是找一台相应的 Linux 机器,在上面通过包管理器下载这些软件包,然后再创建这些软件包的 repo 索引文件。
可以看出这种方式十分不灵活,假如我想要制作 Debian 9 的 apt 离线源,我就需要一台 Debian 9 的机器。如果要适配多个 Linux 发行版就需要多个相应的 OS 机器。要管理和使用这么多种类的 OS 不是一件容易的事儿,而如今使用已经相当普遍的容器技术恰恰能帮助我们解决这类问题。比如我想运行一个 Debian 9 的操作系统,我只需要运行一个 Debian 9 镜像的容器即可,而且不需要额外的管理成本,使用起来也十分轻量。
日常工作中我们常使用容器来构建一些 Golang 写的后端组件,那么构建离线源是不是也可以这样做?实践证明确实可以,我们只需要为不同的 OS 和包管理器写一个相应的 Dockerfile 即可。使用 Docker build 多阶段构建的特性,可以将多个 Dockerfile 合并成一个,然后最后使用 COPY --from 的方式将这个构建的产物复制到同一个镜像中,比如提供 HTTP 的 nginx 容器,或者使用 BuildKit 的特性将这些构建产物导出为 tar 包或者本地目录。
K8sMeetup
适配 OS
根据自己的 PaaS toB 从业经验可知,目前国内的私有云客户在生产环境中使用的 OS,CentOS 应该是最多的,其次是 Ubuntu 和 Debian。至于 RedHat 则需要付费订阅才能使用,DockerHub 更是没有免费可使用的镜像,因此本方案无法确保适用于 RedHat。产品方面 CentOS 需要的版本只有 7.9;Ubuntu 需要支持 18.04 和 20.04;Debian 需要支持 9 和 10。因为时间和精力有限,本方案支持的 Linux 发行版和相应的版本只有 CentOS 7,Debian 9/10,Ubuntu 18.04/20.04 这五个。如果要支持其他 OS 的离线源比如 OpenSUSE,也可以参考本方案编写一个 Dockerfile 文件来实现适配。
K8sMeetup
构建
构建的过程十分简单,使用一个 YAML 格式的配置文件来管理不同的包管理器或 Linux 发行版安装不同的包,并在一个 Dockerfile 里完成所有的构建操作。

构建过程
使用 Docker Build 的方式构建离线源大致可以分为如下几个步骤:
  • 在构建容器内配置 yum/apt 源,安装构建时需要工具;
  • 生成系统内的 rpm/deb 包的列表和需要下载的包列表,解决一些软件包依赖的问题;
  • 根据生成的包列表使用相应的包管理器工具下载需要的软件包;
  • 使用相应的包管理器生成这些包的 index 文件,如 repodata 或 Packages.gz 文件;
  • 将上述的构建产物 COPY 到同一个容器镜像里,比如 nginx ;也可以导出为 tar 包或目录;
packages.yaml
这个文件用来管理不同的包管理器或者 Linux 发行版需要安装的软件包。根据不同的包管理器和发行版我们可以将这些包大致划分为以下 4 类:
  • common:适用于一些所有包管理器中包名相同或者对版本无要求的包,比如 vim 、curl、wget 这类工具。一般情况下使用这些工具我们并不关心它的版本,并且这类包的包名在所有的包管理器中都是相同的,所以这类可以划分为公共包。
  • yum/apt/dnf:适用于不同的发行版使用相同的包管理器。比如 nfs 的包,在 yum 中包名为 nfs-utils 但在 apt 中为 nfs-common,这类软件包可以划分为一类。
  • OS:适用于一些该 OS 独有的包,比如安装一个 Ubuntu 中有但 Debian 中没有的包(比如 debian-builder 或 ubuntu-dev-tools)。
  • OS-发行版代号:这类包的版本和发行版代号绑定在一起,比如 docker-ce=5:19.03.15~3-0~debian-stretch

在这里需要额外注意一下,在不同的包管理器之间指定包版本的方式也各不相同,比如在 yum 中如果要安装 19.03.15 版本的 docker-ce 包名为 docker-ce-19.03.15,而在 debian 中包名则为 docker-ce=5:19.03.15~3-0~debian-stretch。可以使用包管理器查看相同的一个包(如 docker-ce)在不同的包管理器之间的差异,如下:

这个版本号的问题在 Kubespray 的源码中也是同样做了特殊处理,目前确实没有太好的方案来解决,只能手动维护这个版本号。
  • roles/container-engine/docker/vars/redhat.yml

  • roles/container-engine/docker/vars/ubuntu.yml

CentOS 7
介绍完上述的包配置文件之后,接下来我们就根据这个 packages.yml 配置文件使用 Dockerfile 构建这些包的离线源。以下是构建 CentOS 7 离线源的 Dockerfile。

在最后的一个 FROM 镜像中,我指定的是 scratch,这是一个特殊的镜像名,它代表的是一个空的镜像 layer。这样方便将它导出为 tar 包和目录格式。

也可以直接将构建出来的产物放到 nginx 容器中,这样直接运行 nginx 容器就能提供 yum/apt 源的服务。

  • 如果要构建为 tar 包或者本地目录的方式,需要为 Docker 开启 DOCKER_BUILDKIT=1 这个特性。

  • 构建日志如下:

  • 构建产物如下:

Debian 9
下面是 Debian 9 构建 Dockerfile,流程上和 CentOS 相差不多,只是包管理器的使用方式不太相同,这里就不再做详细的源码介绍。
  • Dockerfile.debian

Ubuntu
Ubuntu 离线源的制作步骤和 Debian 差不多,只需要简单修改一下 Debian 的 Dockerfile 应该就 OK ,比如 's/debian/ubuntu/g' ,毕竟 Debian 是 Ubuntu 的爸爸嘛~~,所以 apt 使用的方式和包名几乎一模一样,这里就不再赘述了。
All-in-One
将上述几个 Linux 发行版的 Dockerfile 整合成一个,这样只需要一个 Docker Build 命令就能构建出所需要的所有 OS 的离线源了
  • Dockerfile


K8sMeetup
使用
构建好了离线源之后,在部署的机器上运行一个 Nginx 服务,用于提供 HTTP 方式下载这些软件包,同时需要配置一下机器的包管理器 repo 配置文件。
  • CentOS 7

  • Debian 9 stretch

  • Debian 10 buster

  • Ubuntu 18.04 bionic

  • Ubuntu 20.04 focal
K8sMeetup
优化
Dockerfile
可以考虑将 Dockerfile 中的构建过程合并成一个 shell 脚本,然后在 Dockerfile 中调用这个脚本即可。这样可优化 Dockerfile 代码的可维护性,同时后续适配多种 OS 的时候也可以复用部分相同的代码,但这种做法可能会导致 Docker Build 缓存的失效问题
Package version
对于一些版本中包含 Linux 发行版本代号的包来讲,手动维护这个代号不太方便,可以考虑将它魔改成占位变量的方式,在构建容器内生成 package.list 文件后统一使用 sed 把这些占位的变量给替换一下,如下:

使用 sed 处理一下 packages.list 中的这些占位符变量:

虽然这样做很不美观,但这种方式确实可行 😂,最终能够得到正确的版本号。总之我们尽量地少维护一些包的版本,比如使用这种方式就可以将某个版本的 docker-ce 包放在配置文件的 apt 中,而不是 debian/ubuntu 中,通过一些环境变量或者 shell 脚本自动添加上这些特殊项,这样能减少一些维护成本。
参考
  • aptly.info:https://www.aptly.info/tutorial/mirror/
  • jq 常用操作:https://mozillazg.com/2018/01/jq-use-examples-cookbook.html
  • yq 之读写篇:https://lyyao09.github.io/2019/08/02/tools/The-usage-of-yq-read-write/
  • Build images with BuildKit:https://docs.docker.com/develop/develop-images/build_enhancements/
  • kubernetes-sigs/kubespray/pull/6766:https://github.com/kubernetes-sigs/kubespray/pull/6766
    万字长文:彻底搞懂容器镜像构建
    为 CentOS 与 Ubuntu 制作离线本地源:https://www.xiaocoder.com/2017/09/12/offline-local-source/
浏览 144
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报