来由
容器化应用在构建时经常因为下载包而花费很多时间(有时遇到网络抽风,一个版本要多次构建才能成功),而大多依赖包几乎又都是不变更的。于是我有这样一个构建镜像的优化思路:
用一个最小化的容器,将所需的包提前下载到本地,再将这些包构建成一个个的本地仓库。在需要构建的容器中,把软件源替换成本地仓库,就可以节省构建容器的时间(数量级的)。
这里之所以将其记下来,写成博客,还有一个原因:虽然这里用到的全都是现成的工具和软件,但是除了自身的手册和 --help,其他有帮助的文档实在过于分散,而且据我搜索一圈下来,这些现成的工具总会碰到一些文档中未提及,甚至 Stack Overflow 之类 网站都很少碰到的小“坑”,而解决这些“坑”才是最耗时的。
TL;DR
我之后会在 github 上将其部分开源出来,放在这里(TODO)
目前经过测试兼容的发行版有:
- centos 6 / 7 / 8
- fedora 31 / 32 / 33
- amazonlinux 1 / 2
- ubuntu trusty (14.04) / xenial (16.04) / bionic (18.04) / focal (20.04)
- debian jessie (8) / stretch (9) / buster (10)
- opensuse leap 15
流程
- 基于该发行版最小化的容器,添加一些需要用到的软件源。
- 针对不同的发行版,使用对应的包管理工具,下载所需软件包列表的所有软件包以及其依赖包
- 将这些包按发行版放置在对应的目录下,使用容器中创建软件仓库的命令来构建本地仓库
- 使用一个简单的静态 web server,监听一个本地端口。这样一个本地的 http 软件仓库就搭起来了
- 将本地的软件源添加到需要经常更新构建的容器
Dockerfile中。这里需要注意的是,本地的软件仓库一般没有做签名校验或者 https 之类,需要手动添加信任。
这里只对较为繁琐的步骤进行说明
0x02. 软件包下载
yum / dnf
$ cd /path/to/dir \
&& yumdownloader --resolve pkg-1 pkg-2 ...
- 这里首选
yumdownloader,前一个方案试过dnf install --downloadonly,发现这里的未知的坑不少,其中一个是下载完成后,已经下载到本地的包偶尔会被删掉,感觉是dnf / yum本身有一些存储优化策略。 --resolve选项是为了指定让yumdownloader下载指定软件包的依赖包--installroot不推荐使用这个选项来指定下载路径,使用该选项后,软件源配置文件中的宏(变量)都不自动解析了。比如常见的$releasever变量,需要额外手动指定。yumdownloader会直接将包下载到工作目录,直接用 cd 提前切换工作目录即可
apt-get
$ cd /path/to/dir \
&& apt-get download \
$(apt-cache depends --recurse --no-recommends --no-suggests \
--no-conflicts --no-breaks --no-replaces --no-enhances \
pkg-1 pkg-2 ... | grep "^\w")
- 如果使用
apt-get install --donwload-only --reinstall来下载包,那么依赖包如果是当前容器中已经存在的包就不会再下载了。
如 downloader-container (用于下载的容器) 中已存在 ca-certificates 和 openssl 两个软件包,此时再执行接下来的命令,结果就是:由于 --reinstall 选项 ca-certificates 会被下载,但是 openssl 作为 ca-certificates 的依赖包,就会被忽略了。
$ apt-get download \
$(apt-cache depends --recurse --no-recommends --no-suggests \
--no-conflicts --no-breaks --no-replaces --no-enhances \
ca-certificates | grep "^\w")
- 这里使用的是
apt-get download而不是apt-get --install --donwload-only,主要原因是在子命令apt-cache depends中,查询到的依赖包,会有首选和次选(替代),而这两者往往是冲突的,就算apt-get install使用了--donwload-only也会导致包下载失败,因为无法解决冲突。
下面列举一个 apt-cache depends 的结果,其中 pinentry-curses 是 <pinentry:i386> 的更优先选择。
详细说明可以参见 https://www.thecodeship.com/gnu-linux/understanding-apt-cache-depends-output/
$ apt-cache depends --recurse --no-recommends \
--no-suggests --no-conflicts --no-breaks \
--no-replaces --no-enhances --no-pre-depends \
gnupg2 | grep -E '^gnupg-agent:i386' -A10
gnupg-agent:i386
|Depends: pinentry-curses:i386
Depends: <pinentry:i386>
mew-beta-bin:i386
mew-bin:i386
pinentry-curses:i386
pinentry-gnome3:i386
pinentry-gtk2:i386
pinentry-qt:i386
pinentry-tty:i386
Depends: libassuan0:i386
apt-get download也是直接将软件包下载到当前目录的,所以提前用cd命令切换工作目录即可
zypper
$ zypper --no-gpg-checks --non-interactive \
--pkg-cache-dir /path/to/dir \
install -y -f --download-only \
pkg-1 pkg-2 ...
--non-interactive主要用于脚本中,防止zypper等待用户输入直到超时--pkg-cache-dir用来指定下载目录-f用来强制下载已经安装的包。这里其实会遇到和apt-get install --download-only中一样的问题,就是依赖包如果已经安装,则不会下载。目前我暂时这样写,有缺少的基础包就手动加上了。- 对于
zypper要区分 global arguments 和 subcommand arguments,具体到这条命令就是 install 前面为 global arguments,而后面是 subcommand arguments
0x03. 目录结构
yum
yum 仓库的目录结构如下:
base/
├── amazonlinux-1
│ └── x86_64
| ├── audit-libs-2.6.5-3.28.amzn2.i686.rpm
| ├── ...
│ └── repodata
...
说明:yum 仓库的结构比较简单,在发行版子目录 -> CPU架构目录下,存放下载的 rpm 包,然后在同目录下创建本地仓库索引。
创建 yum 仓库索引的命令如下:
cd /path/to/dir \
&& createrepo --update ./
其中,createrepo 还有一个 c 版本的 createrepo_c,速度会更快,使用方法相同。推荐较新的发行版直接使用,比如 centos 8 / fedora 31+ / amazonlinux
cd /path/to/dir \
&& createrepo_c --update ./
较新的发行版某些包是用 modularity 1的方式构建的,如果想针对这些包构建本地仓库需要额外的命令:
文档详见:https://docs.fedoraproject.org/en-US/modularity/hosting-modules/
cd /path/to/dir \
&& createrepo_c --update ./ \
&& repo2module -s stable -n REPO_NAME -d ./ ./repodata/modules \
&& modifyrepo_c --mdtype=modules ./repodata/modules.yaml ./repodata
其中 REPO_NAME 是本地仓库的名字
这里一个值得注意的命令是 repo2module(来自 https://github.com/rpm-software-management/modulemd-tools),因为在上述文档中并未提及如何生成 modules.yaml 文件。
fedora 或者 centos 8 (需要额外添加 epel 仓库) 可以通过 dnf install -y python3-gobject modulemd-tools 来安装 repo2module 命令
apt
apt 仓库的目录结构如下:
ubuntu/
├── dists
│ ├── bionic
│ │ └── base
│ │ └── main
│ │ └── binary-amd64
| ...
└── pool
├── bionic
│ └── base
│ └── main
│ └── binary-amd64
...
说明: apt 仓库分 dists/ 和 pool/ 两个子目录,dists/ 子目录下存放索引,pool/ 子目录下存放软件包。
创建 apt 仓库索引的命令如下:
这里本地仓库就不再使用 gpg 签名 Release 了,完整命令详见:https://medium.com/sqooba/create-your-own-custom-and-authenticated-apt-repository-1e4a4cf0b864#35dd
cd /path/to/dir
apt-ftparchive --arch amd64 packages \
pool/bionic/base/main/binary-amd64 \
> dists/base/main/binary-amd64/Packages
gzip -k -c \
-f dists/base/main/binary-amd64/Packages \
> dists/base/main/binary-amd64/Packages.gz
apt-ftparchive release dists/bionic/base > dists/bionic/Release
其中 base 是自定义的仓库子目录,这里方便之后扩展。
apt-ftparchive 命令可以通过 apt-get install -y dpkg-dev 安装。
0x05. 添加本地仓库
下面的 host.docker.internal 是通过 docker build 的 --add-host 添加的域名,4891 为本地 openresty 监听的端口
yum
printf "[local-base]\n\
name=Local Base Repo\n\
baseurl=http://host.docker.internal:4891/base/centos-7/x86_64/\n\
skip_if_unavailable=True\n\
gpgcheck=0\n\
repo_gpgcheck=0\n\
enabled=1\n\
enabled_metadata=1" > /etc/yum.repos.d/local-base.repo
zypper
printf "[local-base]\n\
name=Local Base Repo\n\
baseurl=http://host.docker.internal:4891/base/sles-12/x86_64/\n\
skip_if_unavailable=True\n\
gpgcheck=0\n\
repo_gpgcheck=0\n\
enabled=1\n\
enabled_metadata=1" > /root/local-base.repo \
&& zypper -n ar --check --refresh -G file:///root/local-base.repo \
&& zypper -n mr --gpgcheck-allow-unsigned-repo local-base \
&& zypper -n mr --gpgcheck-allow-unsigned-package local-base \
&& rm -f /root/local-base.repo
apt
echo "deb [trusted=yes] http://host.docker.internal:4891/ubuntu bionic/base main" > /etc/apt/sources.list