使用 GitHub Actions 自动化构建 yum/apt 离线源

离线部署

对于 PaaS toB 产品来讲,客户往往会要求产品的部署方案必须做到离线安装,即在部署时不能依赖任何在线的资源,比如安装一些 OS 软件包时依赖的 yum/apt 源;docker.io、k8s.gcr.io 、quay.io 上面的容器镜像;GitHub 上开源软件的二进制下载文件等。

作为平台部署工具的开发者,始终被离线部署这个难题困扰着。在线的容器镜像和二进制文件比较好解决,因为这些资源是与 OS 无关的,只要下载下来放到安装包里,部署的时候启动一个 HTTP 服务器和镜像仓库服务提供这些资源的下载即可。

但是对于 yum/apt 之类的软件来讲并不那么简单:

  • 首先由于各个包之间的依赖关系比较复杂,并不能将它们直接下载下来;
  • 其次即便下载下来之后也无法直接通过 yum/apt 的方式安装指定的软件包,虽然也可以使用 scp 的方式将这些包复制到部署节点,通过 rpm 或 dpkg 的方式来安装上,但这样并不是很优雅,而且通用性能也不是很好;
  • 最后需要适配的 Linux 发行版和包管理器种类也有多种,而且有些包的包名或者版本号在不同的包管理之间也相差甚大,无法做到统一管理。
  • 要同时适配 arm64 和 amd64 的源及其困难

综上,将平台部署依赖的在线 yum/apt 之类的软件包资源制作成离线安装包是一件很棘手的事情。个人就这个问题折腾了一段时间,终于找到了一个比较合适的解决方案:即通过一个 YAML 配置文件来管理包,然后使用 Dockerfile 来构建成离线的 tar 包或者容器镜像。如果有类似需求的小伙伴,可以参考一下本方案。

Docker build

传统制作离线源的方式是找一台相应的 Linux 机器,在上面通过包管理器下载这些软件包,然后再创建这些软件包的 repo 索引文件。

可以看出这种方式十分不灵活,假如我想要制作 Debian 9 的 apt 离线源,我就需要一台 Debian 9 的机器。如果要适配多个 Linux 发行版就需要多个相应的 OS 机器。要管理和使用这么多种类的 OS 不是一件容易的事儿,而如今已经十分普遍使用的容器技术恰恰能帮助我们解决这类问题。比如我想运行一个 Debian9 的操作系统,我只需要运行一个 Debian 9 镜像的容器即可,而且不需要额外的管理成本,使用起来也十分地轻量。

日常工作中我们常使用容器来构建一些 Golang 写的后端组件,那么构建离线源是不是也可以这样做?实践证明确实可以,我们只需要为不同的 OS 和包管理器写一个相应的 Dockerfile 即可。使用 docker build 多阶段构建的特性,可以将多个 Dockerfile 合并成一个,然后最后使用 COPY –from 的方式将这个构建的产物复制到同一个镜像中,比如提供 HTTP 的 nginx 容器,或者使用 BuildKit 的特性将这些构建产物导出为 tar包 或者为本地目录。

适配 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 文件来实现适配。

构建

构建的过程十分简单,使用一个 YAML 格式的配置文件来管理不同的包管理器或 Linux 发行版安装不同的包,并在一个 Dockerfile 里完成所有的构建操作。实现源码在 github.com/muzi502/scripts/build-packages-repo

1
2
3
4
5
6
build
├── Dockerfile
├── Dockerfile.centos
├── Dockerfile.debian
├── Dockerfile.ubuntu
└── packages.yaml

构建过程

使用 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。

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
common:
- vim
- curl
- wget
- tree
- lvm2

yum:
- nfs-utils
- yum-utils
- createrepo
- centos-release-gluster
- epel-release

apt:
- nfs-common
- apt-transport-https
- ca-certificates
- lsb-release
- software-properties-common
- aptitude
- dpkg-dev

centos:
- centos-release

debian:
- debian-builder

debian-buster:
- docker-ce=5:19.03.15~3-0~debian-buster

ubuntu:
- ubuntu-dev-tools

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

1
2
3
4
5
6
7
8
9
10
[root@centos:]# yum list docker-ce --showduplicates | grep 19.03.15
docker-ce.x86_64 3:19.03.15-3.el7 docker-ce-stable

root@debian:/# apt-cache policy docker-ce
docker-ce:
Installed: (none)
Candidate: 5:19.03.15~3-0~debian-stretch
Version table:
5:19.03.15~3-0~debian-stretch 500
500 https://download.docker.com/linux/debian stretch/stable amd64 Packages

这个版本号的问题在 kubespray 的源码中也是同样做了特殊处理,目前确实没有太好的方案来解决,只能手动维护这个版本号。

  • roles/container-engine/docker/vars/redhat.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
---
# https://docs.docker.com/engine/installation/linux/centos/#install-from-a-package
# https://download.docker.com/linux/centos/<centos_version>>/x86_64/stable/Packages/
# or do 'yum --showduplicates list docker-engine'
docker_versioned_pkg:
'latest': docker-ce
'18.09': docker-ce-18.09.9-3.el7
'19.03': docker-ce-19.03.15-3.el{{ ansible_distribution_major_version }}
'20.10': docker-ce-20.10.5-3.el{{ ansible_distribution_major_version }}
'stable': docker-ce-19.03.15-3.el{{ ansible_distribution_major_version }}
'edge': docker-ce-19.03.15-3.el{{ ansible_distribution_major_version }}

docker_cli_versioned_pkg:
'latest': docker-ce-cli
'18.09': docker-ce-cli-18.09.9-3.el7
'19.03': docker-ce-cli-19.03.15-3.el{{ ansible_distribution_major_version }}
'20.10': docker-ce-cli-20.10.5-3.el{{ ansible_distribution_major_version }}

docker_package_info:
enablerepo: "docker-ce"
pkgs:
- "{{ containerd_versioned_pkg[containerd_version | string] }}"
- "{{ docker_cli_versioned_pkg[docker_cli_version | string] }}"
- "{{ docker_versioned_pkg[docker_version | string] }}"
  • roles/container-engine/docker/vars/ubuntu.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# https://download.docker.com/linux/ubuntu/
docker_versioned_pkg:
'latest': docker-ce
'18.09': docker-ce=5:18.09.9~3-0~ubuntu-{{ ansible_distribution_release|lower }}
'19.03': docker-ce=5:19.03.15~3-0~ubuntu-{{ ansible_distribution_release|lower }}
'20.10': docker-ce=5:20.10.5~3-0~ubuntu-{{ ansible_distribution_release|lower }}
'stable': docker-ce=5:19.03.15~3-0~ubuntu-{{ ansible_distribution_release|lower }}
'edge': docker-ce=5:19.03.15~3-0~ubuntu-{{ ansible_distribution_release|lower }}

docker_cli_versioned_pkg:
'latest': docker-ce-cli
'18.09': docker-ce-cli=5:18.09.9~3-0~ubuntu-{{ ansible_distribution_release|lower }}
'19.03': docker-ce-cli=5:19.03.15~3-0~ubuntu-{{ ansible_distribution_release|lower }}
'20.10': docker-ce-cli=5:20.10.5~3-0~ubuntu-{{ ansible_distribution_release|lower }}

docker_package_info:
pkgs:
- "{{ containerd_versioned_pkg[containerd_version | string] }}"
- "{{ docker_cli_versioned_pkg[docker_cli_version | string] }}"
- "{{ docker_versioned_pkg[docker_version | string] }}"

CentOS7

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

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
# 使用 centos 7.9 作为 base 构建镜像
FROM centos:7.9.2009 as builder

# 定义 centos 的版本和处理器体系架构
ARG OS_VERSION=7
ARG ARCH=x86_64

# 在这里定义一些构建时需要的软件包
ARG BUILD_TOOLS="yum-utils createrepo centos-release-gluster epel-release curl"

# 安装构建工具和配置一些软件源 repo
RUN yum install -q -y $BUILD_TOOLS \
&& yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo \
&& yum makecache && yum update -y -q

# 需要安装 yq 个工具来处理 packages.yaml 配置文件
RUN curl -sL -o /usr/local/bin/yq https://github.com/mikefarah/yq/releases/download/v4.9.3/yq_linux_amd64 \
&& chmod a+x /usr/local/bin/yq

# 解析 packages.yml 配置文件,生成所需要的 packages.list 文件
WORKDIR /centos/$OS_VERSION/os/$ARCH
COPY packages.yaml packages.yaml

# 使用 yq 先将 YAML 文件转换成 json 格式的内容,再使用 jq 过滤出所需要的包,输出为一个列表
RUN yq eval '.common[],.yum[],.centos[]' packages.yaml | sort -u > packages.list \
&& rpm -qa >> packages.list

# 下载 packages.list 中的软件包,并生成 repo 索引文件
RUN cat packages.list | xargs yumdownloader --resolve \
&& createrepo -d .
# 将构建产物复制到一层空的镜像中,方便导出为 tar 包或目录的格式
FROM scratch
COPY --from=centos7 /centos /centos

在最后的一个 FROM 镜像中,这里指定的是 scratch,这是一个特殊的镜像名,它代表的是一个空的镜像 layer。

1
2
3
# 将构建产物复制到一层空的镜像中,方便导出为 tar 包或目录的格式
FROM scratch
COPY --from=centos7 /centos /centos

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

1
2
FROM nginx:1.19
COPY --from=centos7 /centos /usr/share/nginx/html
  • 如果要构建为 tar 包或者本地目录的方式,需要为 Docker 开启 DOCKER_BUILDKIT=1 这个特性
1
2
3
4
# 构建为本地目录
root@debian: ~ # DOCKER_BUILDKIT=1 docker build -o type=local,dest=$PWD -f Dockerfile.centos .
# 构建为 tar 包
root@debian: ~ # DOCKER_BUILDKIT=1 docker build -o type=tar,dest=$PWD/centos7.tar -f Dockerfile.centos .
  • 构建日志如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

[+] Building 30.9s (13/13) FINISHED
=> [internal] load .dockerignore 0.0s
=> => transferring context: 109B 0.0s
=> [internal] load build definition from Dockerfile.centos 0.0s
=> => transferring dockerfile: 979B 0.0s
=> [internal] load metadata for docker.io/library/centos:7.9.2009 2.6s
=> [centos7 1/7] FROM docker.io/library/centos:7.9.2009@sha256:0f4ec88e21daf75124b8a9e5ca03c37a5e937e0e108a255d890492430789b60e 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 818B 0.0s
=> CACHED [centos7 2/7] RUN yum install -q -y yum-utils createrepo centos-release-gluster epel-release curl && yum-config-manager --add-repo https://download.docker.c 0.0s
=> [centos7 3/7] WORKDIR /centos/7/os/x86_64 0.0s
=> [centos7 4/7] RUN curl -sL -o /usr/local/bin/yq https://github.com/mikefarah/yq/releases/download/v4.9.3/yq_linux_amd64 && chmod a+x /usr/local/bin/yq && curl 3.2s
=> [centos7 5/7] COPY packages.yaml packages.yaml 0.1s
=> [centos7 6/7] RUN yq eval packages.yaml -j | jq -r '.common[],.yum[],.centos[]' | sort -u > packages.list && rpm -qa >> packages.list 1.0s
=> [centos7 7/7] RUN cat packages.list | xargs yumdownloader --resolve && createrepo -d . 21.6s
=> [stage-1 1/1] COPY --from=centos7 /centos /centos 0.5s
=> exporting to client 0.7s
=> => copying files 301.37MB
  • 构建产物如下
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
root@debian:/build # tree centos
centos
└── 7
└── os
└── x86_64
├── acl-2.2.51-15.el7.x86_64.rpm
├── ansible-2.9.21-1.el7.noarch.rpm
├── at-3.1.13-24.el7.x86_64.rpm
├── attr-2.4.46-13.el7.x86_64.rpm
├── audit-libs-2.8.5-4.el7.x86_64.rpm
├── audit-libs-python-2.8.5-4.el7.x86_64.rpm
├── avahi-libs-0.6.31-20.el7.x86_64.rpm
├── basesystem-10.0-7.el7.centos.noarch.rpm
├── bash-4.2.46-34.el7.x86_64.rpm
……………………………………
├── redhat-lsb-submod-security-4.1-27.el7.centos.1.x86_64.rpm
├── repodata
│   ├── 28d2fe2d1dbd9b76d3e5385d42cf628ac9fc34d69e151edfe8d134fe6ac6a6d9-primary.xml.gz
│   ├── 5264ca1af13ec7c870f25b2a28edb3c2843556ca201d07ac681eb4af7a28b47c-primary.sqlite.bz2
│   ├── 591d9c2d5be714356e8db39f006d07073f0e1e024a4a811d5960d8e200a874fb-other.xml.gz
│   ├── c035d2112d55d23a72b6d006b9e86a2f67db78c0de45345e415884aa0782f40c-other.sqlite.bz2
│   ├── cd756169c3718d77201d08590c0613ebed80053f84a2db7acc719b5b9bca866f-filelists.xml.gz
│   ├── ed0c5a36b12cf1d4100f90b4825b93dac832e6e21f83b23ae9d9753842801cee-filelists.sqlite.bz2
│   └── repomd.xml
├── yum-utils-1.1.31-54.el7_8.noarch.rpm
└── zlib-1.2.7-19.el7_9.x86_64.rpm

4 directories, 368 files

Debian9

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

  • Dockerfile.debian
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
FROM debian:stretch-slim as stretch
ARG OS_VERSION=stretch
ARG ARCH=amd64

ARG DEP_PACKAGES="apt-transport-https ca-certificates curl gnupg aptitude dpkg-dev"
RUN apt update -y -q \
&& apt install -y --no-install-recommends $DEP_PACKAGES \
&& curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg \
&& echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian ${OS_VERSION} stable" \
| tee /etc/apt/sources.list.d/docker.list > /dev/null \
&& apt update -y -q

WORKDIR /debian/${OS_VERSION}

RUN curl -sL -o /usr/local/bin/yq https://github.com/mikefarah/yq/releases/download/v4.9.3/yq_linux_amd64 \
&& chmod a+x /usr/local/bin/yq

COPY packages.yaml packages.yaml

RUN yq eval '.common[],.apt[],.debian[]' packages.yaml | sort -u > packages.list \
&& dpkg --get-selections | grep -v deinstall | cut -f1 >> packages.list

RUN chown -R _apt /debian/$OS_VERSION \
&& cat packages.list | xargs -L1 -I {} apt-cache depends --recurse --no-recommends --no-suggests \
--no-conflicts --no-breaks --no-replaces --no-enhances {} | grep '^\w' | sort -u | xargs apt-get download

RUN cd ../ && dpkg-scanpackages $OS_VERSION | gzip -9c > $OS_VERSION/Packages.gz

FROM scratch
COPY --from=builder /debian /debian

Ubuntu

Ubuntu 离线源的制作步骤和 Debian 差不太多,只需要简单修改一下 Debian 的 Dockerfile 应该就 OK ,比如 's/debian/ubuntu/g' ,毕竟 Debian 是 Ubuntu 的爸爸嘛~~,所以 apt 使用的方式和包名几乎一模一样,这里就不再赘述了。

All-in-Oone

将上述几个 Linux 发行版的 Dockerfile 整合成一个,这样只需要一个 docker build 命令就能构建出所需要的所有 OS 的离线源了。

  • Dockerfile
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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# CentOS 7.9 2009
FROM centos:7.9.2009 as centos7
ARG OS_VERSION=7
ARG ARCH=x86_64
ARG BUILD_TOOLS="yum-utils createrepo centos-release-gluster epel-release curl"

RUN yum install -q -y $BUILD_TOOLS \
&& yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo \
&& yum makecache && yum update -y -q

RUN curl -sL -o /usr/local/bin/yq https://github.com/mikefarah/yq/releases/download/v4.9.3/yq_linux_amd64 \
&& chmod a+x /usr/local/bin/yq \
&& curl -sL -o /usr/local/bin/jq https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 \
&& chmod a+x /usr/local/bin/jq

WORKDIR /centos/$OS_VERSION/os/$ARCH
COPY packages.yaml packages.yaml
RUN yq eval packages.yaml -j | jq -r '.common[],.yum[],.centos[]' | sort -u > packages.list \
&& rpm -qa >> packages.list
RUN cat packages.list | xargs yumdownloader --resolve \
&& createrepo -d .

# Debian 9 stretch
FROM debian:stretch-slim as stretch
ARG OS_VERSION=stretch
ARG ARCH=amd64

ARG DEP_PACKAGES="apt-transport-https ca-certificates curl gnupg aptitude dpkg-dev"
RUN apt update -y -q \
&& apt install -y --no-install-recommends $DEP_PACKAGES \
&& curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg \
&& echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian ${OS_VERSION} stable" \
| tee /etc/apt/sources.list.d/docker.list > /dev/null \
&& apt update -y -q

RUN curl -sL -o /usr/local/bin/yq https://github.com/mikefarah/yq/releases/download/v4.9.3/yq_linux_amd64 \
&& chmod a+x /usr/local/bin/yq \
&& curl -sL -o /usr/local/bin/jq https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 \
&& chmod a+x /usr/local/bin/jq

WORKDIR /debian/${OS_VERSION}
COPY packages.yaml packages.yaml
RUN yq eval packages.yaml -j | jq -r '.common[],.apt[],.debian[]' | sort -u > packages.list \
&& dpkg --get-selections | grep -v deinstall | cut -f1 >> packages.list

RUN chown -R _apt /debian/$OS_VERSION \
&& cat packages.list | xargs -L1 -I {} apt-cache depends --recurse --no-recommends --no-suggests \
--no-conflicts --no-breaks --no-replaces --no-enhances {} | grep '^\w' | sort -u | xargs apt-get download

RUN cd ../ && dpkg-scanpackages $OS_VERSION | gzip -9c > $OS_VERSION/Packages.gz

# Debian 10 buster
FROM debian:buster-slim as buster
ARG OS_VERSION=buster
ARG ARCH=amd64

ARG DEP_PACKAGES="apt-transport-https ca-certificates curl gnupg aptitude dpkg-dev"
RUN apt update -y -q \
&& apt install -y --no-install-recommends $DEP_PACKAGES \
&& curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg \
&& echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian ${OS_VERSION} stable" \
| tee /etc/apt/sources.list.d/docker.list > /dev/null \
&& apt update -y -q

RUN curl -sL -o /usr/local/bin/yq https://github.com/mikefarah/yq/releases/download/v4.9.3/yq_linux_amd64 \
&& chmod a+x /usr/local/bin/yq \
&& curl -sL -o /usr/local/bin/jq https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 \
&& chmod a+x /usr/local/bin/jq

WORKDIR /debian/${OS_VERSION}
COPY packages.yaml packages.yaml
RUN yq eval packages.yaml -j | jq -r '.common[],.apt[],.debian[]' | sort -u > packages.list \
&& dpkg --get-selections | grep -v deinstall | cut -f1 >> packages.list

RUN chown -R _apt /debian/$OS_VERSION \
&& cat packages.list | xargs -L1 -I {} apt-cache depends --recurse --no-recommends --no-suggests \
--no-conflicts --no-breaks --no-replaces --no-enhances {} | grep '^\w' | sort -u | xargs apt-get download

RUN cd ../ && dpkg-scanpackages $OS_VERSION | gzip -9c > $OS_VERSION/Packages.gz

# Ubuntu 18.04 bionic
FROM ubuntu:bionic as bionic
ARG OS_VERSION=bionic
ARG ARCH=amd64

ARG DEP_PACKAGES="apt-transport-https ca-certificates curl gnupg aptitude dpkg-dev"
RUN apt update -y -q \
&& apt install -y --no-install-recommends $DEP_PACKAGES \
&& curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg \
&& echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu ${OS_VERSION} stable" \
| tee /etc/apt/sources.list.d/docker.list > /dev/null \
&& apt update -y -q

RUN curl -sL -o /usr/local/bin/yq https://github.com/mikefarah/yq/releases/download/v4.9.3/yq_linux_amd64 \
&& chmod a+x /usr/local/bin/yq \
&& curl -sL -o /usr/local/bin/jq https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 \
&& chmod a+x /usr/local/bin/jq

WORKDIR /ubuntu/${OS_VERSION}
COPY packages.yaml packages.yaml
RUN yq eval packages.yaml -j | jq -r '.common[],.apt[],.ubuntu[]' | sort -u > packages.list \
&& dpkg --get-selections | grep -v deinstall | cut -f1 >> packages.list

RUN chown -R _apt /ubuntu/$OS_VERSION \
&& cat packages.list | xargs -L1 -I {} apt-cache depends --recurse --no-recommends --no-suggests \
--no-conflicts --no-breaks --no-replaces --no-enhances {} | grep '^\w' | sort -u | xargs apt-get download

RUN cd ../ && dpkg-scanpackages $OS_VERSION | gzip -9c > $OS_VERSION/Packages.gz

# Ubuntu 20.04 focal
FROM ubuntu:focal as focal
ARG OS_VERSION=focal
ARG ARCH=amd64

ARG DEP_PACKAGES="apt-transport-https ca-certificates curl gnupg aptitude dpkg-dev"
RUN apt update -y -q \
&& apt install -y --no-install-recommends $DEP_PACKAGES \
&& curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg \
&& echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu ${OS_VERSION} stable" \
| tee /etc/apt/sources.list.d/docker.list > /dev/null \
&& apt update -y -q

RUN curl -sL -o /usr/local/bin/yq https://github.com/mikefarah/yq/releases/download/v4.9.3/yq_linux_amd64 \
&& chmod a+x /usr/local/bin/yq \
&& curl -sL -o /usr/local/bin/jq https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 \
&& chmod a+x /usr/local/bin/jq

WORKDIR /ubuntu/${OS_VERSION}
COPY packages.yaml packages.yaml
RUN yq eval packages.yaml -j | jq -r '.common[],.apt[],.ubuntu[]' | sort -u > packages.list \
&& dpkg --get-selections | grep -v deinstall | cut -f1 >> packages.list

RUN chown -R _apt /ubuntu/$OS_VERSION \
&& cat packages.list | xargs -L1 -I {} apt-cache depends --recurse --no-recommends --no-suggests \
--no-conflicts --no-breaks --no-replaces --no-enhances {} | grep '^\w' | sort -u | xargs apt-get download

RUN cd ../ && dpkg-scanpackages $OS_VERSION | gzip -9c > $OS_VERSION/Packages.gz

FROM scratch
COPY --from=centos7 /centos /centos
COPY --from=stretch /debian /debian
COPY --from=buster /debian /debian
COPY --from=bionic /ubuntu /ubuntu
COPY --from=focal /ubuntu /ubuntu

使用

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

  • CentOS 7
1
2
3
4
5
[Inra-Mirror]
name=Infra Mirror Repository
baseurl=http://172.20.0.10/centos/7/
enabled=1
gpgcheck=1
  • Debian 9 stretch
1
deb [trusted=yes] http://172.20.0.10:8080/debian stretch/
  • Debian 10 buster
1
deb [trusted=yes] http://172.20.0.10:8080/debian buster/
  • Ubuntu 18.04 bionic
1
deb [trusted=yes] http://172.20.0.10:8080/ubuntu bionic/
  • Ubuntu 20.04 focal
1
deb [trusted=yes] http://172.20.0.10:8080/debian focal/

GitHub Action 自动构建

准备好上面这些 Dockerfile 之后,接下来就要考虑构建的问题了。对于一个 PaaS 或者 IaaS 产品需要适配主流的 Linux 发行版,有时还需要适配 arm64 架构的机器。如果本地手动 docker build 来构建的话,效率很低。因此我们需要使用 GitHub actions 自动构建这些 rpm/deb 包的离线源,具体实现代码可参考 k8sli/os-packages

代码结构

在 build 目录里存放各种发行版的 Dockerfile。由于不同的发行版以及每个发行版的版本构建方法千差万别,因此每个发行版 OS 在一个单独的 Dockerfile 里构建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
os-packages/
├── LICENSE
├── Makefile
├── README.md
├── build
│   ├── Dockerfile.os.centos7
│   ├── Dockerfile.os.centos8
│   ├── Dockerfile.os.debian10
│   ├── Dockerfile.os.debian9
│   ├── Dockerfile.os.fedora33
│   ├── Dockerfile.os.fedora34
│   ├── Dockerfile.os.ubuntu1804
│   └── Dockerfile.os.ubuntu2004
├── packages.yaml
└── repos
├── CentOS-All-in-One.repo
├── Debian-buster-All-in-One.list
├── Fedora-All-in-One.repo
└── Ubuntu-focal-All-in-One.list

Workflow

  • 触发方式
1
2
3
4
5
6
7
8
---
name: Build os-packages image
on:
push:
tag:
- 'v*'
branch: [main, release-*, master]
workflow_dispatch:
  • 全局变量
1
2
3
4
5
6
7
8
9
env:
# 镜像仓库域名
IMAGE_REGISTRY: "ghcr.io"
# 镜像仓库用户名
REGISTRY_USER: "${{ github.repository_owner }}"
# 镜像仓库登录凭据
REGISTRY_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
# 镜像仓库推送 repo
IMAGE_REPO: "ghcr.io/${{ github.repository_owner }}"
  • 构建矩阵,这些 job 会各自运行一个 runner 来进行并行构建
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
jobs:
build:
runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
include:
- name: ubuntu-bionic
image_name: os-packages-ubuntu1804
dockerfile: build/Dockerfile.os.ubuntu1804
- name: ubuntu-focal
image_name: os-packages-ubuntu2004
dockerfile: build/Dockerfile.os.ubuntu2004
- name: centos-7
image_name: os-packages-centos7
dockerfile: build/Dockerfile.os.centos7
- name: centos-8
image_name: os-packages-centos8
dockerfile: build/Dockerfile.os.centos8
- name: debian-buster
image_name: os-packages-debian10
dockerfile: build/Dockerfile.os.debian10
- name: debian-stretch
image_name: os-packages-debian9
dockerfile: build/Dockerfile.os.debian9
  • checkout 代码,配置 buildx 构建环境
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
steps:
- name: Checkout
uses: actions/checkout@v2
with:
# fetch all git repo tag for define image tag
fetch-depth: 0

- name: Set up QEMU
uses: docker/setup-qemu-action@v1

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1

- name: Log in to GitHub Docker Registry
uses: docker/login-action@v1
with:
registry: ${{ env.IMAGE_REGISTRY }}
username: ${{ env.REGISTRY_USER }}
password: ${{ env.REGISTRY_TOKEN }}
  • 通过 git describe --tags 方式生成一个唯一的镜像 tag
1
2
3
4
- name: Prepare for build images
shell: bash
run: |
git describe --tags --always | sed 's/^/IMAGE_TAG=/' >> $GITHUB_ENV
  • 构建镜像并 push 到镜像仓库,后面打包一个 All-in-one 的包时候会用到
1
2
3
4
5
6
7
8
9
- name: Build and push os-package images
uses: docker/build-push-action@v2
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
file: ${{ matrix.dockerfile }}
platforms: linux/amd64,linux/arm64
tags: |
${{ env.IMAGE_REPO }}/${{ matrix.image_name }}:${{ env.IMAGE_TAG }}
  • 生成新的 Dockerfile,导出镜像到本地目录
1
2
3
4
5
6
7
8
9
10
11
12
- name: Gen new Dockerfile
shell: bash
run: |
echo -e "FROM scratch\nCOPY --from=${{ env.IMAGE_REPO }}/${{ matrix.image_name }}:${{ env.IMAGE_TAG }} / /" > Dockerfile

- name: Build kubeplay image to local
uses: docker/build-push-action@v2
with:
context: .
file: Dockerfile
platforms: linux/amd64,linux/arm64
outputs: type=local,dest=./
  • 将最终构建产物打包上传到 GitHub release
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- name: Prepare for upload package
shell: bash
run: |
mv linux_amd64/resources resources
tar -I pigz -cf resources-${{ matrix.image_name }}-${IMAGE_TAG}-amd64.tar.gz resources --remove-files
mv linux_arm64/resources resources
tar -I pigz -cf resources-${{ matrix.image_name }}-${IMAGE_TAG}-arm64.tar.gz resources --remove-files
sha256sum resources-${{ matrix.image_name }}-${IMAGE_TAG}-{amd64,arm64}.tar.gz > resources-${{ matrix.image_name }}-${IMAGE_TAG}.sha256sum.txt

- name: Release and upload packages
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
files: |
resources-${{ matrix.image_name }}-${{ env.IMAGE_TAG }}.sha256sum.txt
resources-${{ matrix.image_name }}-${{ env.IMAGE_TAG }}-amd64.tar.gz
resources-${{ matrix.image_name }}-${{ env.IMAGE_TAG }}-arm64.tar.gz
  • All-in-one 合并所有构建的镜像
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
upload:
needs: [build]
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v2
with:
# fetch all git repo tag for define image tag
fetch-depth: 0

- name: Set up QEMU
uses: docker/setup-qemu-action@v1

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1

- name: Log in to GitHub Docker Registry
uses: docker/login-action@v1
with:
registry: ${{ env.IMAGE_REGISTRY }}
username: ${{ env.REGISTRY_USER }}
password: ${{ env.REGISTRY_TOKEN }}

- name: Prepare for build images
shell: bash
run: |
git describe --tags --always | sed 's/^/IMAGE_TAG=/' >> $GITHUB_ENV
source $GITHUB_ENV
echo "FROM scratch" > Dockerfile
echo "COPY --from=${{ env.IMAGE_REPO }}/os-packages-ubuntu1804:${IMAGE_TAG} / /" >> Dockerfile
echo "COPY --from=${{ env.IMAGE_REPO }}/os-packages-ubuntu2004:${IMAGE_TAG} / /" >> Dockerfile
echo "COPY --from=${{ env.IMAGE_REPO }}/os-packages-centos7:${IMAGE_TAG} / /" >> Dockerfile
echo "COPY --from=${{ env.IMAGE_REPO }}/os-packages-centos8:${IMAGE_TAG} / /" >> Dockerfile
echo "COPY --from=${{ env.IMAGE_REPO }}/os-packages-debian9:${IMAGE_TAG} / /" >> Dockerfile
echo "COPY --from=${{ env.IMAGE_REPO }}/os-packages-debian10:${IMAGE_TAG} / /" >> Dockerfile

- name: Build os-packages images to local
uses: docker/build-push-action@v2
with:
context: .
file: Dockerfile
platforms: linux/amd64,linux/arm64
outputs: type=local,dest=./

- name: Prepare for upload package
shell: bash
run: |
mv linux_amd64/resources resources
tar -I pigz -cf resources-os-packages-all-${IMAGE_TAG}-amd64.tar.gz resources --remove-files
mv linux_arm64/resources resources
tar -I pigz -cf resources-os-packages-all-${IMAGE_TAG}-arm64.tar.gz resources --remove-files
sha256sum resources-os-packages-all-${IMAGE_TAG}-{amd64,arm64}.tar.gz > resources-os-packages-all-${IMAGE_TAG}.sha256sum.txt

- name: Release and upload packages
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
files: |
resources-os-packages-all-${{ env.IMAGE_TAG }}.sha256sum.txt
resources-os-packages-all-${{ env.IMAGE_TAG }}-amd64.tar.gz
resources-os-packages-all-${{ env.IMAGE_TAG }}-arm64.tar.gz

优化

Dockerfile

可以考虑将 Dockerfile 中的构建过程合并成一个 shell 脚本,然后在 Dockerfile 中调用这个脚本即可,这样可优化 Dockerfile 代码的可维护性,同时后续适配多种 OS 的时候也可以复用部分相同的代码,但这样可能会导致 docker build 缓存的失效问题。

当然也可以使用脚本将多个 Dockerfile 合并成一个,如下:

1
2
3
4
# Merge all Dockerfile.xx to an all-in-one file
ls Dockerfile.* | xargs -L1 grep -Ev 'FROM scratch|COPY --from=' > Dockerfile
echo "FROM scratch" >> Dockerfile
ls Dockerfile.* | xargs -L1 grep 'COPY --from=' >> Dockerfile

其实如果使用 GitHub actions 来构建的话,就不需要进行合并了,使用 actions 矩阵构建的特性可并行构建。

Package version

对于一些版本中包含 Linux 发行版本代号的包来讲,手动维护这个代号不太方便,可以考虑将它魔改成占位变量的方式,在构建容器内生成 package.list 文件后统一使用 sed 把这些占位的变量给替换一下,如下:

1
2
apt:
- docker-ce=5:19.03.15~3-0~__ID__-__VERSION_CODENAME__

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

1
sed -i "s|__ID__|$(sed -n 's|^ID=||p' /etc/os-release)|;s|__VERSION_CODENAME__|$(sed -n 's|^VERSION_CODENAME=||p' /etc/os-release)|" packages.list

虽然这样做很不美观,但这种方式确实可行 😂,最终能够的到正确的版本号。总之我们尽量地少维护一些包的版本,比如使用这种方式就可以将某个版本的 docker-ce 包放在配置文件的 apt 中,而不是 debian/ubuntu 中,通过一些环境变量或者 shell 脚本自动添加上这些特殊项,这样能减少一些维护成本。

踩坑

  • Fedora 指定包的版本时,也需要加上 Fedora 的版本
  • CentOS 7 和 CentOS 8 有些包的包名不同,需要单独处理一下
  • CentOS 7 和 CentOS 8 构建方式不同,最后生成 repodata 的时候 CentOS 8 需要单独处理一下
  • Fedora 33 和 Fedora34 使用 GitHub action 构建的时候 arm64 架构的会一直卡住,是由于 buildx 的 bug 所致,因此只给出了 Dockerfile,并未放在 GitHub actions 构建流水线中。

参考