为容器镜像定制安装Linux工具

在调试的场景下, 容器在使用中通常需要进入容器并执行一些工具命令, 比如 wget, htop, lsof, rz, sz 等. 本篇文章就讨论如何为镜像定制安装这些工具

传统的工具安装方式

传统的运维中, 安装 Linux 工具直接使用各自发行版的包管理器就可以了, 方便, 快捷, 省心

  • RedHat 系列: yum install lsof
  • Debian 系列: apt update && apt install lsof

当使用物理机或虚拟机时, 使用上面的安装方法没有任何问题, 但是到了容器镜像领域, 按照上面的安装方式就不行了. 试过的童鞋会发现, 一个 centos 的基础镜像大约 200MB 左右, 用以上yum的方式安装完lsof工具之后, 镜像的体积增大到了 300MB 仅仅一个小工具的安装, 就会让容器镜像的体积快速膨胀, 虽然成功的完成了安装, 单这显然不是我们希望看到的结果

1
2
3
docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
centos latest 75835a67d134 9 days ago 200MB

镜像中的工具安装方式

在镜像中安装基础工具, 就不能像在传统虚拟机里那样粗犷操作了, 镜像的体积是个非常敏感的数字, 牵动着打包者的神经, 以下乃安装工具之正道

  • RedHat 系列: yum install lsof && yum clean all
  • Debian 系列: apt update && apt install lsof && apt clean && apt autoclean && rm -fr /var/lib/apt/lists/*

经过善后处理的安装方式, 仍以 centos 安装 lsof 工具为例, 安装完成后, 体积为 223MB 相比之前 300MB 的体积, 可以说已经得到了很大的优化

1
2
3
docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
mycentos 0.1 80bb7d931f74 1 minutes ago 223MB

Debian 系列比较特殊, apt clean 操作是为了删除掉 /var/cache/apt/archives/ 下的软件包, 后面还指定删除了 /var/lib/apt/lists/* 此路径下的所有文件, 是为了删除 apt update 操作后更新到此目录的远程服务器提供的软件列表信息

为容器定制安装工具包

经过上面的实践, 在 centos 镜像中安装 lsof 工具的体积成本, 已经成功从 300MB 降到了 223 MB, 是否还有更极致的安装方法呢? 当然有的, 就是直接拷贝二进制文件到镜像中

找到命令文件位置及依赖

找命令位置

启动临时容器, 传统安装 lsof

1
docker container run -it centos:latest /bin/bash

进入到容器后, 正常安装工具包

1
yum -y install lsof

查看该工具包安装的所有文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[root@7b532cd952dc /]# rpm -qa | grep lsof
lsof-4.87-5.el7.x86_64
[root@7b532cd952dc /]# rpm -ql lsof-4.87-5.el7.x86_64
/usr/sbin/lsof
/usr/share/doc/lsof-4.87
/usr/share/doc/lsof-4.87/00.README.FIRST
/usr/share/doc/lsof-4.87/00.README.FIRST_4.87
/usr/share/doc/lsof-4.87/00CREDITS
/usr/share/doc/lsof-4.87/00DCACHE
/usr/share/doc/lsof-4.87/00DIALECTS
/usr/share/doc/lsof-4.87/00DIST
/usr/share/doc/lsof-4.87/00FAQ
/usr/share/doc/lsof-4.87/00LSOF-L
/usr/share/doc/lsof-4.87/00MANIFEST
/usr/share/doc/lsof-4.87/00PORTING
/usr/share/doc/lsof-4.87/00QUICKSTART
/usr/share/doc/lsof-4.87/00README
/usr/share/doc/lsof-4.87/00TEST
/usr/share/doc/lsof-4.87/00XCONFIG
/usr/share/doc/lsof-4.87/README.lsof_4.87
/usr/share/man/man8/lsof.8.gz

以上文件中, 除了第一个主程序外, 其他的都是文档文件, 由于 centos 基础镜像中并不包含 man 命令, 所以, 默认这些文件都没有实际存在在磁盘中, 我们要拷贝的目标只有第一个主程序

安装完成后, 找到软件包的安装位置(由于没有 which 命令, 我们可以使用 find 命令)

1
2
[root@7b532cd952dc /]# find / -name "lsof"
/usr/sbin/lsof

找依赖

使用 ldd 命令, 找到 lsof 命令依赖的库文件

1
2
3
4
5
6
7
8
[root@7b532cd952dc /]# ldd /usr/sbin/lsof
linux-vdso.so.1 => (0x00007ffca8dd6000)
libselinux.so.1 => /lib64/libselinux.so.1 (0x00007f09b6b7e000)
libc.so.6 => /lib64/libc.so.6 (0x00007f09b67b1000)
libpcre.so.1 => /lib64/libpcre.so.1 (0x00007f09b654f000)
libdl.so.2 => /lib64/libdl.so.2 (0x00007f09b634b000)
/lib64/ld-linux-x86-64.so.2 (0x00007f09b6da5000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f09b612f000)
  • 第一列:程序需要依赖什么库
  • 第二列: 系统提供的与程序需要的库所对应的库
  • 第三列:库加载的开始地址

通过上面的信息,我们可以得到以下几个信息:

  • 通过对比第一列和第二列,我们可以分析程序需要依赖的库和系统实际提供的,是否相匹配
  • 通过观察第三列,我们可以知道在当前的库中的符号在对应的进程的地址空间中的开始位置

在本文的场景中, 我们需要关心的就是第一列和第二列, 我们需要记下第一列的所有库文件, 然后到初始的 centos 镜像中去挨个儿查找

哪些库文件需要查找呢, 着重找那些 第一列 => 第二列 的库文件在新镜像中是否存在, 因为出现了=>标志, 就意味着当前容器提供了这个文件给程序使用

拿上面的例子来说, 着重查找以下文件:

  • libselinux.so.1 => /lib64/libselinux.so.1
  • libc.so.6 => /lib64/libc.so.6
  • libpcre.so.1 => /lib64/libpcre.so.1
  • libdl.so.2 => /lib64/libdl.so.2
  • /lib64/ld-linux-x86-64.so.2
  • libpthread.so.0 => /lib64/libpthread.so.0

如果你嫌多的话, 还可以执行以下命令, 查找该程序没有使用到的直接依赖文件

1
2
3
[root@7b532cd952dc /]# ldd -u /usr/sbin/lsof
Unused direct dependencies:
/lib64/libselinux.so.1

也就是说, 在上面的列表中, 可以去掉检查 /lib64/libselinux.so.1 该库文件

检查依赖

启动一个新的原始镜像进行上面检查列表的查找

1
2
3
4
5
6
docker container run -it centos:latest /bin/bash                                                                          
[root@099f82deeb3d /]#
[root@099f82deeb3d /]# find / -name "libc.so.6"
/usr/lib64/libc.so.6

# ...... 按照上面的列表文件, 挨个儿查找

确保原始镜像中的对应列表文件都已经存在, 如果有部分库文件不存在, 需要在后面的操作中, 和 lsof 主程序文件一同拷贝新的镜像中

拷贝程序及依赖库

我们将第一启动的容器, 使用 yum 安装了 lsof 命令的容器称之为 A 容器; 将第二次启动的原始容器, 检查了依赖文件的容器称之为 B 容器

将 A 容器的主程序文件以及在 B 容器中缺失的库文件一同拷贝出来(操作对象: A 容器)

1
2
3
4
5
6
# 找到 A 容器的 containerID: 7b532cd952dc
# 我们假设在 B 容器中检查文件列表时, 同时缺失了 `libc.so.6` 库文件 (注意: 是假设!!!)

# 将主文件和缺失的库文件从容器中拷贝到本地目录
docker container cp 7b532cd952dc:/usr/sbin/lsof ./
docker container cp 7b532cd952dc:/usr/lib64/libc.so.6 ./

然后制作新的镜像, 以支持使用 lsof 命令

新的 dockerfile 内容如下:

1
2
3
4
FROM centos:latest

COPY lsof /usr/sbin/
COPY libc.so.6 /usr/lib64/

注意: 由于上面👆我是假设的缺失了libc.so.6文件, 所以在 dockerfile 里加入了这个文件的拷贝, 实际上这个文件在 centos 基础镜像是存在的, 而且是以软连接的形式存在的, 当我们真的COPY libc.so.6 /usr/lib64/这么操作时, 反而会报错. 一定注意: 此处只是演示缺失库文件的处理情况, 库文件已经存在的话, 最好不要动原配!!!

然后我们执行编译新镜像的命令

1
docker build -t mycentos:0.2 ./

进入到新的容器验证 lsof 是否可以正常使用

1
2
3
4
5
6
7
8
9
10
docker container run -it mycentos:0.2 /bin/bash                                                                           
[root@032d75e40f56 /]# lsof
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
bash 1 root cwd DIR 0,133 4096 47026 /
bash 1 root rtd DIR 0,133 4096 47026 /
bash 1 root txt REG 8,1 964544 917638 /usr/bin/bash
bash 1 root mem REG 8,1 62184 919647 /usr/lib64/libnss_files-2.17.so
bash 1 root mem REG 8,1 2173512 919510 /usr/lib64/libc-2.17.so
bash 1 root mem REG 8,1 19776 919537 /usr/lib64/libdl-2.17.so
......

支持, 关于为容器镜像定制的Linux工具命令的工作就已经全部完成了, 我们再看下定制版的镜像体积大小

1
2
3
docker image ls                                                                                                           
REPOSITORY TAG IMAGE ID CREATED SIZE
mycentos 0.2 805d98b565c8 7 minutes ago 201MB

精简到了只有 201MB 与原始的镜像相比, 只增加了 1MB


参考文档: