所谓的“最小” tomcat 镜像是相对的,它的大小取决于如下几点:
基础镜像是否使用 glibc,也就是是否使用 alpine 作为基础镜像; 使用 jdk 还是只使用 jre 作为 tomcat 运行环境; 使用 openjdk 还是 oracle jdk。
上述的条件决定了 tomcat 镜像的大小。
总所周知,alpine 算是基础镜像中最小的了,它还自带包管理器,但是它的缺点也同样明显,就是它没有使用 glibc,因此会带来兼容性的问题。
本文会使用基于 glibc 的 distroless(基于 debian)作为基础镜像,然后使用 oracle jdk8 + tomcat8,最终镜像大小为 181M(可以更小点,但是会不实用)。这个大小是有一定浮动空间的,因为即使是使用 oracle jdk8,不同的小版本之间大小也会相差很大。
以下是和官方镜像的一个对比:
| 镜像名 | java | 大小 | 镜像层 | glibc |
|---|---|---|---|---|
| tomcat:8 | openjdk jre | 463MB | 31 | glibc |
| tomcat:8-alpine | openjdk jre | 106MB | 25 | muslc |
| tomcat:8-slim | openjdk jre | 223MB | 29 | glibc |
| 本文编译使用 | oracle jdk | 181M | 3 | glibc |
可以看出 alpine 的优势非常大,它足够小,但是它使用的是 jre,且不是 glibc。如果你程序编译是针对 glibc,那么运行起来会有问题。我也不确定我公司的开发是否对 glibc 有强依赖,但是不敢冒险,而且 alpine 对我来说并没有什么优势。
你可以看到本文编译使用的 tomcat 的镜像层只有 3 层,你可能会吃惊于它的层数,等你看完你就会明白了,因为这个镜像并不是通过 dockerfile 构建出来的。
因为我已经将编译好的镜像上传到了 dockerhub,你可以直接使用 docker run maxadd/tomcat:8-jdk8-distroless
运行查看,或者使用 dive[1] 查看其构成。
缘由
当你选择 tomcat 镜像时,其实要考虑很多东西:
是否存在必须的命令; java 是否能够满足需要; 是否足够灵活定制; 是否足够安全(命令和库文件足够少); 是否足够小。
真当你需要用的时候,发现官方镜像使用起来或多或少都有些不顺手,总不是那么令人满意。对我而言,最重要的是官方没有 oracle jdk 的镜像提供,因为 oracle 要对 oracle jdk 收费。虽然也有人自己提供了基于 oracle jdk 的版本,但是镜像实在太大。
总之基于这样或那样的原因,我准备手动创建一个自己的 tomcat 镜像,这让我将目光移向了 distroless[2] 镜像。因为 distroless 镜像是所有基于 glibc 中最小的,只有 19M,里面只包含一个二进制程序应有的最基础的运行环境,没有一个命令提供,包括 shell。
由于 distroless 镜像被墙,因此我已经将其上传到 dockerhub,名称为
maxadd/distroless_base-debian10
。这种镜像基本没有动手脚的可能,你可以直接拿来使用。
因为一开始我并不知道 dockerfile 中的 ADD 命令可以直接解压 tar 包,因此我还特地学习了构建 distroless 的 bazel 工具,因此本文会提到基于 bazel 的实现。你不会 bazel 也没有关系,使用 ADD 就好。
使用 bazel 构建镜像和基于 dockerfile 会有些一些的差别,使用 bazel 只会产生一个镜像层,而不管你做了多少操作;而使用 dockerfile,你每执行一个 dockerfile 命令都会产生一个镜像层,包括 ENV
这类的命令。并且在 dive[3] 命令的视角下,二者构建的镜像也会有所差别。
其实主要是二者的理念不同,bazel 是直接一次性将镜像构建完成,提供容器运行所需的所有文件;而 dockerfile 使用的是镜像层的概念,每执行一个指令就会在其上增加一层。如果为了这点差异去学 bazel 并不划算,建议直接使用 dockefile。不过两种方式下面都会提到。
有了这样的前提之后,我开始规划我的镜像:
要安装 bash,要通过它来设置 jvm 参数; 要安装 jdk,因为需要用到 jdk 中的一些命令; 通过 busybox 提供 300+ 基础的命令; 将镜像时区设置为 Asia/Shanghai; 让镜像支持中文; 自己写脚本启动 tomcat,catalina.sh 脚本中涉及到的命令太多,没必要使用; 使用 jmx_prometheus_javaagent 监控 tomcat jvm。
下面一步步实现上面的需求。
安装 bash
我的做法是建立一个目录,作为根目录,里面存放需要复制到 distroless 镜像中的所有文件。文件需要放在哪,那就在该目录下创建对应的目录。最终将这个“根目录”打包,并在 distroless 的根目录解压,就能将所有文件一步到位。这也是镜像层只有 3 层的原因,其中 2 层是 distroless 自带的。
第一步是安装 bash,安装 bash 的目的是为了执行脚本,同样也是为了能够登录上去执行 jmap、jstack 之类的命令。前面已经提到了,我这里使用 distroless 作为基础镜像,且因为 distroless 使用的是 debian 的库文件,因此我们可以将 debian 中的命令直接复制下来使用。
Linux 中命令的运行不仅需要命令本身,还需要它依赖的库文件,库文件通过 ldd
命令查看。因此我们不仅需要复制命令本身,还得复制它所需的库文件。
由于本次的 distroless 镜像版本为 gcr.io/distroless/base-debian10
,所以我们首先需要启动一个 debian 容器。虽然镜像名称中显示使用的是 debian10,但是我发现将 debian10 的 bash 移植到 distroless 存在问题,而使用 debian9 的就没有问题,因此这里会使用 debian9 的 bash。
# docker run -it --name debian debian:9 bin/bash
查看 bash 所依赖的库文件:
root@45104fade344:/# ldd bin/bashlinux-vdso.so.1 (0x00007ffda05b1000) # 这一行无需理会libtinfo.so.5 => lib/x86_64-linux-gnu/libtinfo.so.5 (0x00007fdf4532f000)libdl.so.2 => lib/x86_64-linux-gnu/libdl.so.2 (0x00007fdf4512b000)libc.so.6 => lib/x86_64-linux-gnu/libc.so.6 (0x00007fdf44d8c000)lib64/ld-linux-x86-64.so.2 (0x0000564175f7f000)
上面总共有四个库文件,但是需要的只是 /lib/x86_64-linux-gnu/libtinfo.so.5
,其他库文件 distroless 中已经存在了。
我们先在宿主机上创建所谓的“根目录”,这里取名为 tomcat_jdk8。然后在这个目录下创建 lib/x86_64-linux-gnu
、bin
、etc
、usr/local
、usr/lib/locale
这几个目录。
mkdir -p tomcat_jdk8/{lib/x86_64-linux-gnu,bin,etc,usr/{lib/locale,local}}
接着使用 docker cp 命令将 debian 容器中的命令和库文件复制到我们创建的对应的目录下。
docker cp debian:/bin/bash tomcat_jdk8/bin/# -L 表示复制链接文件指向的实际文件docker cp -L debian:/lib/x86_64-linux-gnu/libtinfo.so.5 tomcat_jdk8/lib/x86_64-linux-gnu/
通过这种方式可以将你想要使用的其他命令都拷贝下来,这里就不一一演示了。
准备 jdk
因为这里我打算使用 oracle jdk 而非 openjdk,所以先从 oracle 官网[4]上将 jdk8 下载下来。我这里下载的是 162 版本,没有别的原因,只是因为公司使用的是这个版本。
注意下载 tar 包并解压,我这里将 java 解压到了 usr/local 目录下:
# ls tomcat_jdk8/usr/localjdk1.8.0_162
完整的 jdk 总共有 371M,里面有很多我们用不到的东西,先将其都删除掉:
# cd tomcat_jdk8/usr/local/jdk1.8.0_162# rm -rf *src.zip \lib/missioncontrol \lib/visualvm \lib/*javafx* \jre/lib/plugin.jar \jre/lib/ext/jfxrt.jar \jre/bin/javaws \jre/lib/javaws.jar \jre/lib/desktop \jre/plugin \jre/lib/deploy* \jre/lib/*javafx* \jre/lib/*jfx* \jre/lib/amd64/libdecora_sse.so \jre/lib/amd64/libprism_*.so \jre/lib/amd64/libfxplugins.so \jre/lib/amd64/libglass.so \jre/lib/amd64/libgstreamer-lite.so \jre/lib/amd64/libjavafx*.so \jre/lib/amd64/libjfx*.so
删除之后,只剩 153M😆。然后给 jdk 目录建立一个软链接:
# ln -s jdk1.8.0_162 java
jdk 依赖
jdk 的 bin 目录下有很多我们需要的命令,这些命令也有依赖的库文件。虽然 jdk 是我们下载而并不是我们安装,但是由于 jdk 中的命令都是编译好的二进制文件,只要满足内核和 glibc 的需求它们就可以运行。
当然,内核和 glibc 只是硬性要求,软性要求就是它们依赖的库文件。由于我的宿主机 Linux 的 CentOS7 而非 debian,因此我们需要将 jdk 挂载到 debian 镜像中,在镜像中使用 ldd 命令来查看 jdk 中命令所依赖的库文件有哪些。
我都查看了一番,结果发现 distroless 镜像中库文件完全能够满足 jdk 所有命令的运行需要,所以不需要另外的文件。当然更新版本的 jdk 中可能存在其他依赖的情况,你可以通过 dive 命令查看 distroless 镜像中存在哪些库文件,然后对比命令依赖的库文件,如果 distroless 中不存在,那你就要提供了。
OK,接下来就是准备 tomcat 了。
准备 tomcat
tomcat 里面没有任何命令,它本身也是依赖 java 启动的,因此它没有任何的库文件依赖。直接去官网下一个就行,我这里使用的是 apache-tomcat-8.5.50
。
我这里同样将之放在 usr/local 目录下:
# ls tomcat_jdk8/usr/local/apache-tomcat-8.5.50 java jdk1.8.0_162
接着下载 jmx_exporter[5],通过它来提供 tomcat 的 jvm 监控。我将其放在 tomcat 根目录,然后为其提供一个名为 config.yml
的配置文件:
---startDelaySeconds: 0ssl: falselowercaseOutputName: falselowercaseOutputLabelNames: falsewhitelistObjectNames: ["org.apache.cassandra.metrics:*"]blacklistObjectNames: ["org.apache.cassandra.metrics:type=ColumnFamily,*"]
再给 tomcat 建立一个软连接,最后 usr/local 下面的内容为:
ls tomcat_jdk8/usr/local/apache-tomcat-8.5.50 config.yml java jdk1.8.0_162 jmx_prometheus_javaagent-0.12.0.jar tomcat
tomcat 的启动我们是通过 catalina.sh 进行的,但是由于它里面用到的命令太多,且我们只需要启动 tomcat,不需要停止或者重启之类的,所以我们完全可以不用 catalina.sh,只需要它启动所需的 java 参数就行。拿到这些参数之后,我们直接传递给 java 后同样可以直接启动。
这个参数其实很好获得,你将 tomcat 和 jdk 同时映射到 debian 容器中,然后定义好 JAVA_HOME
,就可以通过 sh -x catalina.sh run
看到它最终启动的参数了。甚至我怀疑只要在 Linux 服务器上直接执行就行,没必要挂载到 debian 中。
我这里就不演示具体的操作了,直接将它的参数贴出来。当你什么 JAVA 参数都没有设定时,它的启动参数如下:
/usr/local/java/bin/java -Djava.util.logging.config.file=/usr/local/tomcat/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djdk.tls.ephemeralDHKeySize=2048 -Djava.endorsed.dirs=/usr/local/tomcat/endorsed -classpath usr/local/tomcat/bin/bootstrap.jar:/usr/local/tomcat/bin/tomcat-juli.jar -Dcatalina.base=/usr/local/tomcat -Dcatalina.home=/usr/local/tomcat -Djava.io.tmpdir=/usr/local/tomcat/temp org.apache.catalina.startup.Bootstrap start
你如果想要增加 jvm 参数,随便往里面插就行,你定义在 catalina.sh 中的类似于 JAVA_OPS
等,最终都会转换成 java 参数。不过,貌似 jvm 中堆的参数之前要加上 -server
。
-server -Xmx128M -Xms128M
我们希望通过一个环境变量来控制 tomcat jvm 的堆大小,因此我们需要通过一个脚本来启动 tomcat:
#!/bin/bash# 先进行单位转换,然后计算出堆中各个区域的内存,会将容器的 requests 都作为堆内存# 因为堆内存默认占据整个 jvm 的 80%(老外说的,未具体验证),所以 jvm 除了堆之外# 还会占用额外 20% 的内存。所以注意 limits 的值一定要比 requests 大# 至于大多少不好说,因为应用有可能除了使用 jvm 之外,还会使用系统内存作为缓存# 所以容器的内存限制还要看具体的应用,最好先跑段时间,通过监控查看内存使用情况# 不要认为 shell 的算术运算不支持浮点数就丢失精度# 其实就算丢失精度也没啥,因为堆内存比容器最大内存小是应该的# 如果比容器最大内存更大反而会出问题unitConv() {value=$1unit=("" "K" "M" "G")idx=0while (($value > 10240)); do((value/=1024))((++idx))doneecho "$value${unit[idx]}"}if [ -z "$JVM_HEAP" ]; thenif [ -z "$MEM_TOTAL" ]; thenecho "必须指定 JVM_HEAP|MEM_TOTAL 二者之一作为环境变量,且 MEM_TOTAL 必须以字节为单位"exit 1elseheap=$(unitConv $MEM_TOTAL)young=$(unitConv $((MEM_TOTAL/2)))# PermSize 和 MaxPermSize 从 java8 中被移除JVM_HEAP=`echo -Xmx$heap -Xms$heap -Xmn$young`fifi/usr/local/java/bin/java -Djava.util.logging.config.file=/usr/local/tomcat/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -server $JVM_HEAP -XX:SurvivorRatio=10 -XX:MaxTenuringThreshold=3 -XX:TargetSurvivorRatio=50 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+CMSClassUnloadingEnabled -XX:+UseCMSInitiatingOccupancyOnly -XX:ParallelGCThreads=1 -XX:ConcGCThreads=1 -XX:+CMSParallelRemarkEnabled -XX:+CMSScavengeBeforeRemark -XX:+ExplicitGCInvokesConcurrent -XX:+UseTLAB -XX:TLABSize=2048K -Dsun.rmi.dgc.client.gcInterval=2592000000 -Dsun.rmi.dgc.server.gcInterval=2592000000 -Djava.awt.headless=true -Xloggc:/usr/local/tomcat/logs/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Djdk.tls.ephemeralDHKeySize=2048 -Djava.awt.headless=true -javaagent:/usr/local/jmx_prometheus_javaagent-0.12.0.jar=12356:/usr/local/config.yml -Djava.endorsed.dirs=/usr/local/tomcat/endorsed -classpath usr/local/tomcat/bin/bootstrap.jar:/usr/local/tomcat/bin/tomcat-juli.jar -Dcatalina.base=/usr/local/tomcat -Dcatalina.home=/usr/local/tomcat -Djava.io.tmpdir=/usr/local/tomcat/temp org.apache.catalina.startup.Bootstrap start
通过这个脚本有两种指定 tomcat jvm 堆大小的方式,一种是通过 MEM_TOTAL
环境变量,这个变量直接指定堆的总大小(单位是字节),脚本会粗暴的将其二分之一大小作为年轻代的大小。之所以有这种设置,是为了配合 kubernetes 的 resources.requests.memory,可以将它的值作为 MEM_TOTAL 环境变量,然后将 resources.limits.memory 指定大一些,防止应用会使用非堆内存或者系统缓存,这个要看具体的应用。
另一种方式是直接指定 JVM_HEAP
环境变量,通过这个环境变量设置堆、年轻代等大小。可以看到,使用 MEM_TOTAL 最终也是将其转换为 JVM_HEAP。
此外,这个脚本还指定了其他的一些 java 参数,包括使用 cms 垃圾回收(你可以换成 G1)、jmx_prometheus_javaagent 监控 jvm 等,你也可以添加其他的配置,包括将其中的一些变化脚本的做成环境变量的形式。
这个脚本你随便放在哪,只要 CMD 指定执行它就行,这里放在了 tomcat 根目录,脚本名为 start.sh。
使用 busybox
busybox 是一个 Linux 命令,它能够以不到 1M 的大小来模拟 300+ 的 Linux 常用命令,具体底层的实现原理尚不明确,但是非常适用于对存储空间有要求的场景。
我们可以在 debian10 容器中安装 busybox,然后将其通过 docker cp 命令 copy 到 tomcat_jdk/bin 目录下(依赖的库文件 distroless 已经存在)。
它的用法很简单,比如你要通过它来模拟 ls 命令,可以这么做:
./busybox ls etc/
第二种使用方式是创建一个 ls 的软链接文件,指向 busybox:
ln -s busybox ls./ls etc/
这只是拿 ls 命令举例,其他的命令都是这种用法。那么它支持模拟哪些命令呢?通过 busybox --list
可以查看到。你可以通过下面的方式为其支持的所有命令创建软链接:
# docker run --rm -it -v tomcat_jdk8/bin:/opt debian:10 /bin/bashroot@e39f29c2648d:/# cd /opt/root@e39f29c2648d:/opt# for i in $(./busybox --list);do ln -s busybox $i;done
这就相当于你的 tomcat 镜像中存在 300+ 命令。
中文支持
distroless 默认不支持中文,想让它支持中文也很简单,只需要在 debain10 容器中执行如下命令:
# docker run --rm -it debian:10 /bin/bashapt updateapt install -y localeslocaledef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8
命令执行后,会生成 /usr/lib/locale/locale-archive
文件,你只需要使用 docker cp 命令将这个文件 copy 出来,放置到 tomcat_jdk8/usr/lib/locale/locale-archive 即可。
时区
时区默认为 UTC,我们只需将 debian10 容器中的 /usr/share/zoneinfo/Asia/Shanghai 文件放入到 tomcat_jdk8/etc/localtime 即可。
添加普通用户
其实 docker 容器本身就是 Linux 上的一个进程,你运行起来之后是可以在宿主机上通过 ps 命令看到的,并且可以看到进程的运行用户是 root。
有些运维常识的人都应该知道,除非万不得已,进程不应该使用 root 启动。那我们是不是应该在容器中创建一个普通用户,然后使用这个普通用户来启动容器呢?答案是不需要。
因为容器其实就是宿主机上的进程,我们是可以将其运行的用户指定为宿主机上的用户的。docker 启动容器时可以通过 docker run -u
指定宿主机用户名,而 k8s 中则可以通过 pod.spec.containers.securityContext.runAsUser
来指定宿主机的 uid。这么一来,你就可以在运行容器的宿主机上通过 ps 命令看到容器运行的用户为你指定的用户,而无论镜像中是否存在该用户。
所以,我们无需在宿主机中创建普通用户。
如果你已经决定好用使用普通用户来运行 tomcat 容器,那么你最好将 tomcat 的某些目录(比如日志目录 tomcat_jdk8/usr/local/tomcat/logs)的属主改为你准备运行 tomcat 的用户,避免运行后普通用户无法写入日志。
打包
最终“根目录”的文件有这些:
tomcat_jdk8/├── bin│ ├── bash│ ├── beep -> busybox│ ├── blkid -> busybox│ ├── ...├── etc│ ├── localtime├── lib│ └── x86_64-linux-gnu│ └── libtinfo.so.5└── usr├── lib│ └── locale│ └── locale-archive└── local├── apache-tomcat-8.5.50│ ...├── config.yml├── java -> jdk1.8.0_162├── jdk1.8.0_162│ ...├── jmx_prometheus_javaagent-0.12.0.jar└── tomcat -> apache-tomcat-8.5.50/
确保文件都准备完毕后,我们就可以对该目录(这里是 tomcat_jdk8)进行打包了,打包的格式必须是 tar/tar.gz/tar.xz 等。
有一点需要注意,使用 tar 命令无法完成这种操作(或许是我不知道方法?),因为 tar 必须在它上级目录打包,但是这样一来 tar 中就包含 tomcat_jdk8 这个目录名了。这就造成在 distroless 中解压后里面文件都不会直接放在根下,而是还在 tomcat_jdk8 下。
既然 tar 不行,那就使用 Python 进行打包。你不会 Python 不要紧,跟着我走就不会出任何问题。
首先启动 python3 镜像,注意将 debian_file 映射到 python3 的 /opt 目录下(你如果本地有 python 环境的话就没必要这么麻烦)。
docker run -it -v /root/tomcat_jdk8:/opt python:3.6
然后执行下面这些代码:
import tarfile, osos.chdir("/opt")tar = tarfile.open("/tmp/x.tar", "w")for i in os.listdir("."):tar.add(i)tar.close()
这会将 tomcat_jdk8 中的所有文件都打包到容器中的 /tmp/x.tar
文件中。然后使用 docker cp
将其复制到宿主机的 /tmp
下,留作后用。
ok,准备工作都已完成,停止 Python 容器后就可以制作镜像了,方式是将 /tmp/x.tar 解压到 distroless 镜像中。
dockerfile 构建
我们既可以通过 dockerfile 来制作镜像,又可以通过 bazel。这里先使用 dockerfile,因为它够简单。
新建一个目录:
mkdir dockerfilescd dockerfiles
将 /tmp/x.tar 复制到当前目录下之后,新建 Dockerfile
文件:
FROM maxadd/distroless_base-debian10ADD x.tar .ENV JAVA_HOME=/usr/local/java LANG="en_US.utf8" PATH="/bin:/usr/local/java/bin"EXPOSE 8080/tcp 12356/tcpCMD ["/usr/local/tomcat/start.sh"]
将 /tmp/x.tar 复制到当前目录下后,执行 docker build 命令:
docker build -t tomcat:8-jdk8-distroless .
构建成功后需要指定环境变量运行它:
docker run --rm -it -e "MEM_TOTAL=1333222222" --cap-add=SYS_PTRACE -p 11111:12356 tomcat:8-jdk8-distroless
之所以加上 --cap-add=SYS_PTRACE
是为了能够使用 jps 等命令。
你甚至可以通过 jmx 监控来查看它的性能指标:
curl 127.0.0.1:11111/metrics
OK,镜像制作完成。
bazel 构建
bazel 是 Google 推出的编译工具,用于将各种语言的源代码编译成二进制文件,至于有什么优势我没有具体了解 😜。从这点上来看,编译 docker 镜像只是它附带的功能,事实也确实如此,它并非原生支持 docker 镜像的编译。
使用 bazel 编译 docker 镜像的一大优势就是你甚至无需安装 docker,不过真要使用方便,还是得安装。
有一个不幸的消息是,使用 bazel 过程中需要*翻,因此没有这种手段的童鞋就只能抱歉了。
首先安装 bazel:
# cat >/etc/yum.repos.d/bazel.repo <<'EOF'[vbatts-bazel]name=Copr repo for bazel owned by vbattsbaseurl=https://copr-be.cloud.fedoraproject.org/results/vbatts/bazel/epel-7-$basearch/type=rpm-mdskip_if_unavailable=Truegpgcheck=1EOF# yum install bazel
bazel 原生并不支持编译 docker 镜像,不过 GitHub 上面有扩展规则[6]可以帮你完成。
首先创建一个目录,以它作为 WORKSPACE:
# mkdir bazel
然后定义 WORKSPACE,也就是外部依赖。其实我们依赖就是 docker 规则,因此加载它就好。
# cd bazel# vim WORKSPACEload("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")# Download the rules_docker repository at release v0.14.1http_archive(name = "io_bazel_rules_docker",sha256 = "dc97fccceacd4c6be14e800b2a00693d5e8d07f69ee187babfd04a80a9f8e250",strip_prefix = "rules_docker-0.14.1",urls = ["https://github.com/bazelbuild/rules_docker/releases/download/v0.14.1/rules_docker-v0.14.1.tar.gz"],)# OPTIONAL: Call this to override the default docker toolchain configuration.# This call should be placed BEFORE the call to "container_repositories" below# to actually override the default toolchain configuration.# Note this is only required if you actually want to call# docker_toolchain_configure with a custom attr; please read the toolchains# docs in /toolchains/docker/ before blindly adding this to your WORKSPACE.# BEGIN OPTIONAL segment:load("@io_bazel_rules_docker//toolchains/docker:toolchain.bzl",docker_toolchain_configure="toolchain_configure")load("@io_bazel_rules_docker//repositories:repositories.bzl",container_repositories = "repositories",)container_repositories()# This is NOT needed when going through the language lang_image# "repositories" function(s).load("@io_bazel_rules_docker//repositories:deps.bzl", container_deps = "deps")container_deps()load("@io_bazel_rules_docker//container:container.bzl","container_pull",)container_pull(name = "base",# 因为使用的是我上传到 dockerhub 上的 distroless 镜像,因此 registry 需要指定为下面的值registry = "registry-1.docker.io",repository = "maxadd/distroless_base-debian10",)
由于 bazel 的 docker 规则更新很频繁,我这里使用的是 v0.14.1。可能没过多久之后它就行更新了,你还得去 github 上将它上面的最新规则给复制下来,不然你构建可能会失败,因为 bazel build 的时候貌似会拉最新的规则。
定义好依赖之后,我们在 WORKSPACE 所在目录下创建一个空的 BUILD 文件(因为 build 过程中会校验这个文件),然后创建一个包,用来编译镜像。
# touch BUILD# mkdir tomcat# cp /tmp/x.tar . # 将 tar 包复制到当前目录# vim BUILDload("@io_bazel_rules_docker//container:container.bzl","container_image",)container_image(name = "app",base = "@base//image",tars = ["x.tar"],env = {"PATH": "/bin:/usr/local/java/bin:/usr/local/java/jre/bin","JAVA_HOME": "/usr/local/java","LANG": "en_US.utf8"},workdir = "/usr/local/tomcat/webapps",ports = ["8080", "12356"],cmd = ["/usr/local/tomcat/start.sh"])
开始编译:
# cd ..# bazel build //tomcat:app
//tomcat:app
是一个 target,这是 bazel 的概念,使用它来指定我们要编译哪个目录。tomcat 指的是我们创建的 tomcat 目录,因为它下面有 BUILD 文件,所以它也称为一个包。app 则是指 BUILD 文件中 container_image
下面 name 的值,通过 //tomcat:app
就能定位到它的位置。
编译完成后,在当前目录下执行:
bazel-bin/tomcat/app.executable
然后你可以使用通过 docker images 看到 bazel/tomcat
这个镜像了,注意它的 tag 是 app 而非 latest。
你可以运行它,它和 dockefile 构建而成的镜像没有本质的区别,只有细微的不同。
docker run --rm -it -e "MEM_TOTAL=1333222222" --cap-add=SYS_PTRACE -p 11111:12356 bazel/test:app
OK,本篇文章也要结束了,其实使用 bazel 构建镜像是有优势的,比如逼格很高。但是在国内使用起来还是很困难的,包括没有中文文档,且使用过程中要*墙,相比而言,使用它的性价比太低。
如果之前我知道 dockfile ADD 指令可以解压 tar 包,那么我是无论如何都不会去了解它的,因此我对它的使用没有过多的介绍。懂得自然懂,不懂直接使用 dockerfile 就好。
参考资料
dive: https://github.com/wagoodman/dive
[2]distroless: https://github.com/GoogleContainerTools/distroless
[3]dive: https://github.com/wagoodman/dive
[4]oracle 官网: https://www.oracle.com/technetwork/pt/java/javase/downloads/jdk8-downloads-2133151.html?printOnly=1
[5]jmx_exporter: https://github.com/prometheus/jmx_exporter
[6]扩展规则: https://github.com/bazelbuild/rules_docker




