发上等愿,结中等缘,享下等福;择高处立,就平处坐,向宽处行。
分类: 虚拟化
2019-09-25 16:18:57
容器的OCI标准定义了容器镜像规范,容器镜像包与传统的压缩包(zip/tgz等)相比有两个关键区别点:1)分层存储;2)打包即部署。
分层存储可以极大减少镜像更新时候拉取镜像包的时间,通常应用程序更新升级都只是更新业务层(如Java程序的jar包),而镜像中的操作系统Lib层、运行时(如Jre)层等文件不会频繁更新。因此新版本镜像实质有变化的只有很小的一部分,在更新升级时候也只会从镜像仓库拉取很小的文件,所以速度很快。
打包即部署是指在容器镜像制作过程包含了传统软件包部署的过程(安装依赖的操作系统库或工具、创建用户、创建运行目录、解压、设置文件权限等等),这么做的好处是把应用及其依赖封装到了一个相对封闭的环境,减少了应用对外部环境的依赖,增强了应用在各种不同环境下的行为一致性,同时也减少了应用部署时间。
基于分层存储与打包即部署的特性,容器可以更快的部署、运行、保持环境一致性,这些特性都是容器相对于传统虚拟机打包部署的优势。这两个点都是通过dockerfile来承载的,因此如何编写高效、安全、规范易用的dockerfile是容器实践中关键的一个环节。
很多基础镜像都提供不同功能集的版本,根据实际情况选择最小功能集的版本,功能越少体积越小,引入安全漏洞的风险越低。 如Node镜像,最小的才不到30M,最大的超过200M。
latest 是一个不确定的版本号,依赖 latest 会导致每次构建出的镜像是不可预知的。
依赖基础镜像版本时候,尽量指定到最具体的版本,这样不确定性最小。如Node镜像,版本号有 12, 12.4, 12.4.0,依赖 12.4.0 是不确定性最小的。
容器镜像仓库中的tag是可以被复写的,同一个tag在不同的时间对应内容可能不同,因此如果条件允许,将依赖的镜像mirror一份到本地,这样可以保证 每次构建时候依赖的基础镜像是不变的。
Docker 1.10 版本开始,Docker官方镜像仓库与Docker-EE/Docker-CE都支持镜像多平台功能,这里的多平台包含不同类型操作系统(Windows/Linux),不同CPU体系(X86/ARM64)。
利用镜像多平台功能,在docker pull ...命令中可以不用显示指定平台类型,docker客户端会根据当前的平台架构与镜像仓库协商(基于manifest list特性),拉取对应平台的镜像。
通过这个特性可以保持用户界面的简洁性,客户端执行 docker pull node:12 命令,在ARM64机器上,则会拉取 arm64v8/node:12 镜像,在Linux机器上,则拉取 node:12 镜像。
但是这种方式也引入了不确定性,不确定性表现为2方面:第一方面为不同平台上版本存在差异,可能依赖的版本只在一个平台有;第二个方面为有些时间可能在Linux环境下构建ARM镜像,这样会导致构建出错误镜像。
通过Label增加镜像作者,构建时间,描述等信息,让使用者得到更多关于镜像信息。
通过Label增加镜像对应应用的 securitytxt 信息。 securitytxt 是 IETF 组织起草的一份规范,目的定义一套互联网服务提供者与安全漏洞发现者之间交互的规范,使得安全漏洞能够被闭环。
RUN, COPY 命令都要使用到绝对路径,定义好 WORKDIR 会使得 Dockerfile 移植性更好,更容易维护。
ENV, ARG 的值都会被记录下来,通过 docker image history 命令可以查看到,因此不要将敏感信息传递给ENV与ARG。
如果需要在dockerfile中使用密钥或凭证,使用 mount secret 方式。
由于 docker build cache 机制,有上下文依赖的命令放到同一个RUN指令中执行。
假如有如下dockefile片段:
...
RUN apt-get update
RUN apt-get install -y nginx
...
过了一段时间之后,需要修改一下上述dockerfile,增加一个安装包
...
RUN apt-get update
RUN apt-get install -y nginx python
...
此时构建镜像,docker 比对缓存,RUN apt-get update 这一层已经存在,使用缓存。这时候apt仓库中的python或nginx可能已经有新版本了,由于没有执行 apt-get update ,因此安装的并不是最新版本。
通过构建参数 build --no-cache 显示使得缓存失效,不过缓存失效会增加构建时长,需要综合考虑。
假如在dockerfile中执行命令 RUN wget -O - | wc -l > /number ,如果 wget -O - 执行失败了,但是 wc -l 是成功的,因此并不会报错退出。
使用 set -o pipefail 避免管道错误被忽略。上述命令修改为: RUN set -o pipefail && wget -O - | wc -l > /number 。
ADD 命令功能相对 COPY 更复杂,包含从internet下载,解压等,优先使用 COPY 。
不推荐使用 ADD 命令从远程下载一个软件包解压到镜像内,推荐使用 curl 下载到本地,然后再解压到镜像内,并将原始文件删除。
root用户运行应用程序存在安全风险,为每个应用单独创建一个运行用户。
由于dockerfile中创建用户/用户组的 UID/GID 是不固定的,如果应用程序依赖 UID/GID,创建用户时候显示指定。
应用程序运行用户设置Shell为 /sbin/nologin 。
通过 EXPOSE 指令申明应用 listen 的端口与协议,让应用运维人员者简单明了得知端口。
EXPOSE 80/tcp
程序持久化或临时文件目录,使用 VOLUME 指令申明,让应用运维人员简单明了得知需要挂载的卷。
日志输出到标准输出与错误输出,方便查看与采集日志。参考 Nginx 的 dockerfile :
# forward request and error logs to docker log collector
RUN ln -sf /dev/stdout /var/log/nginx/access.log \
&& ln -sf /dev/stderr /var/log/nginx/error.log
推荐将容器内的应用程序设置为PID 1,这样对容器内应用管理相对简单,1号进程退出后整个容器也就退出了。
在dockerfile中使用 CMD 或 ENTRYPOINT 指令设置容器镜像的默认启动进程, CMD/ENTRYPOINT 有两种执行方式:exec 与 /bin/sh ,使用 exec 方式直接执行应用程序可执行文件设置PID为1,使用 /bin/sh 方式的话,1号进程就是 /bin/sh 。
exec 方式: CMD["java", "/same/args"], ENTRYPOINT ["java", "/same/args"] , /bin/sh 方式: CMD java /same/args, ENTRYPOINT java /same/args 。
ENTRYPOINT 与 CMD 功能类似,他们区别是 ENTRYPOINT 不能被 docker run 的命令行参数覆盖,而 CMD 可以; ENTRYPOINT 通常配合 CMD 使用,使用 CMD 设置 ENTRYPOINT 的默认参数,同时支持在 docker run 设置参数传递给 ENTRYPOINT。
参考Redis的ENTRYPOINT写法。
对于切换用户,推荐使用 gosu 替换 sudo 。
如果在运行应用程序之前需要做一些准备工作,如检查文件/目录权限等,那么可以在 ENTRYPOINT 的脚本中完成。
通过K8S调度docker镜像可以覆盖dockerfile中的 ENTRYPOINT 与 CMD,他们之间的关系参考如下文章。
1.12 使用hadolint工具检查dockerfile
hadolint 定义了一套规则与检查工具,可以在项目中使用。
参考 1.1.1 。
由于dockerfile构建镜像使用的是分层只读文件系统,如果使用 chmod 更改一个大目录权限,相当于复制了这个目录下所有文件,会导致镜像体积变大。
Docker 17.05 版本中引入了分阶段构建({Multi-stage builds](https://docs.docker.com/develop/develop-images/multistage-build/)),通过分阶段构建可以减少镜像大小。
Docker镜像的分层是子层依赖父层,对应到 Docker build cache 机制中,如果某一层未命中缓存,那么其剩下的层都不会使用缓存。 因此如果需要最大利用缓存机制,推荐将变化频度低的层尽量放上层,变化频度高的层放下层。
apg-get upgrade 会使得镜像大小不可控,不知道更新了多少软件包。
同样是考虑缓存命中率,变化频度一致的命令放到同一层。
Docker在构建镜像时候有一个 build context 概念,build context 在 docker build 指定一个目录,docker 会将 build context 目录内所有文件加载到内存,作为build context。
build context 目录内内容越少,加载速度越快,建议使用独立的目录作为build context,只拷贝需要的文件到 build context 目录。
使用 .dockerignore 文件排除目录下不需要加载到 build context 内的文件或目录。
Docker 支持从stdin输入 dockerfile ,这种方式不会加载任何本地文件到docker的build context中。