构建最小 tomcat docker 镜像

更新于 2020-07-11。html

所谓的“最小” tomcat 镜像是相对的,它的大小取决于以下几点:java

  1. 基础镜像是否使用 glibc,也就是是否使用 alpine 做为基础镜像;
  2. 使用 jdk 仍是只使用 jre 做为 tomcat 运行环境;
  3. 使用 openjdk 仍是 oracle jdk。

上述的条件决定了 tomcat 镜像的大小。python

总所周知,alpine 算是基础镜像中最小的了,它还自带包管理器,可是它的缺点也一样明显,就是它没有使用 glibc,所以会带来兼容性的问题。linux

本文会使用基于 glibc 的 distroless(基于 debian)做为基础镜像,而后使用 oracle jdk8 + tomcat8,最终镜像大小为 181M(能够更小点,可是会不实用)。这个大小是有必定浮动空间的,由于即便是使用 oracle jdk8,不一样的小版本之间大小也会相差很大。git

如下是和官方镜像的一个对比:github

镜像名 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 175M 3 glibc

能够看出 alpine 的优点很是大,它足够小,可是它使用的是 jre,且不是 glibc。若是你程序编译是针对 glibc,那么运行起来会有问题。我也不肯定我公司的开发是否对 glibc 有强依赖,可是不敢冒险,并且 alpine 对我来讲并无什么优点。web

你能够看到本文编译使用的 tomcat 的镜像层只有 3 层,你可能会吃惊于它的层数,等你看完你就会明白了,由于这个镜像并非经过 dockerfile 构建出来的。docker

由于我已经将编译好的镜像上传到了 dockerhub,你能够直接使用 docker run maxadd/tomcat:8-jdk8-distroless 运行查看,或者使用 dive 查看其构成。shell

原因

当你选择 tomcat 镜像时,其实要考虑不少东西:apache

  • 是否存在必须的命令;
  • java 是否可以知足须要;
  • 是否足够灵活定制;
  • 是否足够安全(命令和库文件足够少);
  • 是否足够小。

真当你须要用的时候,发现官方镜像使用起来或多或少都有些不顺手,总不是那么使人满意。对我而言,最重要的是官方没有 oracle jdk 的镜像提供,由于狗日的 oracle 要对 oracle jdk 收费。虽然也有人本身提供了基于 oracle jdk 的版本,可是镜像实在太大。

总之基于这样或那样的缘由,我准备手动建立一个本身的 tomcat 镜像,这让我将目光移向了 distroless 镜像。由于 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 命令的视角下,两者构建的镜像也会有所差异。

其实主要是两者的理念不一样,bazel 是直接一次性将镜像构建完成,提供容器运行所需的全部文件;而 dockerfile 使用的是镜像层的概念,每执行一个指令就会在其上增长一层。若是为了这点差别去学 bazel 并不划算,建议直接使用 dockefile。不过两种方式下面都会提到。

有了这样的前提以后,我开始规划个人镜像:

  • 要安装 jdk,由于须要用到 jdk 中的一些命令;
  • 经过 busybox 提供 300+ 基础的命令;
  • 将镜像时区设置为 Asia/Shanghai;
  • 让镜像支持中文;
  • 本身写脚本启动 tomcat,catalina.sh 脚本中涉及到的命令太多,不必使用;
  • 使用 jmx_prometheus_javaagent 监控 tomcat jvm;
  • 要安装 bash,要经过它来设置 jvm 参数(非必要);

下面一步步实现上面的需求。

安装 bash

个人作法是创建一个目录,做为根目录,里面存放须要复制到 distroless 镜像中的全部文件。文件须要放在哪,那就在该目录下建立对应的目录。最终将这个“根目录”打包,并在 distroless 的根目录解压,就能将全部文件一步到位。这也是镜像层只有 3 层的缘由,其中 2 层是 distroless 自带的。

第一步是安装 bash,安装 bash 的目的是为了执行脚本,一样也是为了可以登陆上去执行 jmap、jstack 之类的命令。由于咱们后面会使用 busybox,busybox 会提供 sh,所以要不要安装 bash,你看着办。示例镜像中没有这个。

前面已经提到了,我这里使用 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/bash
	linux-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-gnubinetcusr/localusr/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 官网上将 jdk8 下载下来。我这里下载的是 162 版本,没有别的缘由,只是由于公司使用的是这个版本。

注意下载 tar 包并解压,我这里将 java 解压到了 /usr/local 目录下:

# ls tomcat_jdk8/usr/local
jdk1.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.56

我这里一样将之放在 usr/local 目录下:

# ls tomcat_jdk8/usr/local/
apache-tomcat-8.5.56  java  jdk1.8.0_162
复制代码

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/sh 
/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 ${JAVA_OPTS} -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 -Djdk.tls.ephemeralDHKeySize=2048 org.apache.catalina.startup.Bootstrap start
复制代码

不仅是堆大小,任何你须要的参数均可以定义在 JAVA_OPTS 中,可能形成的结果就是这个变量会很是长。这个脚本你随便放在哪,只要 CMD 指定执行它就行,这里放在了 /usr/local/ 目录下,脚本名为 start.sh。

再给 tomcat 创建一个软链接,最后 usr/local 下面的内容为:

ls tomcat_jdk8/usr/local/
apache-tomcat-8.5.56  config.yml  java  jdk1.8.0_162  tomcat
复制代码

jmx 监控 tomcat

tomcat 监控主要手段仍是 jmx,tomcat 或者说 java 都支持 jmx。这里的 jmx 监控经过 jmx_exporter 进行,这是配合 prometheus 使用的。它是一个 java agent,经过 -javaagent 参数启动,关于 java agent 相关的内容这里就很少提了,由于我知道的也很少😂。

下载完成后。我将其放在 usr/local/ 目录,而后在同一目录下为其提供一个名为 config.yml 的配置文件:

---
startDelaySeconds: 0
ssl: false
lowercaseOutputName: false
lowercaseOutputLabelNames: false
whitelistObjectNames: 
 - "com.alibaba.druid:type=DruidDataSource,*"
 - "java.lang:*"
复制代码

这里给 jmx 收集的对象设置了一个白名单,这有对象名符合的才收集。若是你不知道 tomcat 都提供了有哪些对象,可使用 jconsole 链接 jmx 以后,查看 mbean 标签下的内容,会列出当前 jmx 提供的全部对象。

由于咱们的开发使用了 alibaba 的 druid 链接池,因此这里也收集了它的指标。固然,若是整个 jmx 的指标你都要收集的话,将白名单都去掉就好。可是我不建议你这么作,由于指标太多。在示例的镜像中,只保留了 java.lang:*,并无收集 druid 链接池的内容。这里只是示范若是你想要收集其余指标的时候,应该怎么作。

启动 tomcat 默认并不会加载这个 java agnet,你须要将其添加到 JAVA_OPTS 这个环境变量中。启动方式以下:

-javaagent:/usr/local/jmx_prometheus_javaagent-0.13.0.jar=12356:/usr/local/config.yml
复制代码

后面会提到如何经过 docker 启动。这样启动镜像以后,你只要访问 http://IP:12356/metrics(容器 ip),就会打印出全部其收集的 jmx 指标。

使用 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/bash
root@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/bash
apt update
apt install -y locales
localedef -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 的用户,避免运行后普通用户没法写入日志。

示例 docker 镜像中的 tomcat 目录属主属组为 root,若是你要用普通用户运行这个镜像,是没法写入日志的。不过能够经过挂载日志目录的方式进行,这个在后面的日志收集的文章中会提到。

打包

最终“根目录”的文件有这些:

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.56
        │   ...
        ├── config.yml
        ├── java -> jdk1.8.0_162
        ├── jdk1.8.0_162
        │   ...
        ├── jmx_prometheus_javaagent-0.13.0.jar
        └── tomcat -> apache-tomcat-8.5.56/
复制代码

确保文件都准备完毕后,咱们就能够对该目录(这里是 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, os
os.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 dockerfiles
cd dockerfiles
复制代码

将 /tmp/x.tar 复制到当前目录下以后,新建 Dockerfile 文件:

FROM maxadd/distroless_base-debian10
ADD x.tar . ENV JAVA_HOME=/usr/local/java LANG="en_US.utf8" PATH="/bin:/usr/local/java/bin"
EXPOSE 8080/tcp 12356/tcp
CMD ["/usr/local/start.sh"] 复制代码

将 /tmp/x.tar 复制到当前目录下后,执行 docker build 命令:

docker build -t tomcat:8-jdk8-distroless .
复制代码

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 vbatts
baseurl=https://copr-be.cloud.fedoraproject.org/results/vbatts/bazel/epel-7-$basearch/
type=rpm-md
skip_if_unavailable=True
gpgcheck=1
EOF

# yum install bazel -y
复制代码

bazel 原生并不支持编译 docker 镜像,不过 GitHub 上面有扩展规则能够帮你完成。

首先建立一个目录,以它做为 WORKSPACE:

# mkdir bazel
复制代码

而后定义 WORKSPACE,也就是外部依赖。其实咱们依赖就是 docker 规则,所以加载它就好。

# cd bazel
# vim WORKSPACE
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")

# Download the rules_docker repository at release v0.14.3
http_archive(
    name = "io_bazel_rules_docker",
    sha256 = "6287241e033d247e9da5ff705dd6ef526bac39ae82f3d17de1b69f8cb313f9cd",
    strip_prefix = "rules_docker-0.14.3",
    urls = ["https://github.com/bazelbuild/rules_docker/releases/download/v0.14.3/rules_docker-v0.14.3.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"
)
#docker_toolchain_configure(
# name = "docker_config",
# # OPTIONAL: Path to a directory which has a custom docker client config.json.
# # See https://docs.docker.com/engine/reference/commandline/cli/#configuration-files
# # for more details.
# client_config="<enter absolute path to your docker config directory here>",
# # OPTIONAL: Path to the docker binary.
# # Should be set explcitly for remote execution.
# docker_path="<enter absolute path to the docker binary (in the remote exec env) here>",
# # OPTIONAL: Path to the gzip binary.
# # Either gzip_path or gzip_target should be set explcitly for remote execution.
# gzip_path="<enter absolute path to the gzip binary (in the remote exec env) here>",
# # OPTIONAL: Bazel target for the gzip tool.
# # Either gzip_path or gzip_target should be set explcitly for remote execution.
# gzip_target="<enter absolute path (i.e., must start with repo name @...//:...) to an executable gzip target>",
# # OPTIONAL: Path to the xz binary.
# # Should be set explcitly for remote execution.
# xz_path="<enter absolute path to the xz binary (in the remote exec env) here>",
# # OPTIONAL: List of additional flags to pass to the docker command.
# docker_flags = [
# "--tls",
# "--log-level=info",
# ],
#
#)
# End of OPTIONAL segment.

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.3。可能没过多久以后它就行更新了,你还得去 github 上将它上面的最新规则给复制下来,否则你构建可能会失败,由于 bazel build 的时候貌似会拉最新的规则。

定义好依赖以后,咱们在 WORKSPACE 所在目录下建立一个空的 BUILD 文件(由于 build 过程当中会校验这个文件),而后建立一个包,用来编译镜像。

# touch BUILD
# mkdir tomcat
# mv /tmp/x.tar . # 将 tar 包移动到当前目录
# vim BUILD
load(
    "@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 'JAVA_OPTS=-Xmx200m -Xms200m -javaagent:/usr/local/jmx_prometheus_javaagent-0.13.0.jar=12356:/usr/local/config.yml' --cap-add=SYS_PTRACE -p 11111:12356 maxadd/tomcat:8-jdk8-distroless
复制代码

之因此加上 --cap-add=SYS_PTRACE 是为了可以使用 jps 等命令。

你甚至能够经过 jmx 监控来查看它的性能指标:

curl 127.0.0.1:11111/metrics
复制代码

OK,本篇文章也要结束了,其实使用 bazel 构建镜像是有优点的,好比逼格很高。可是在国内使用起来仍是很困难的,包括没有中文文档,且使用过程当中要*墙,相比而言,使用它的性价比过低。

若是以前我知道 dockfile ADD 指令能够解压 tar 包,那么我是不管如何都不会去了解它的,所以我对它的使用没有过多的介绍。懂得天然懂,不懂直接使用 dockerfile 就好。