作者 | Timothy Mugayi
译者 | 弯月,责编 | 夕颜
封图 | CSDN付费下载自视觉中国
出品 | CSDN(ID:CSDNnews)
在使用Docker的大部分时间里,我们并不关心其内部的工作原理。仅凭启动一个Docker容器并且让应用程序运行良好,并不能说明你已经实现了一个良好的解决方案。有时,由于时间限制,我们只能复制粘贴Docker镜像,却没能真正理解实现细节以及如何构建Docker镜像的细微差别。
在本文中,我们将探讨Docker的最佳实践和反模式。反模式是人们对于反复出现的问题的一般解决方案,这些方案没有效率,甚至会完全抵消Docker技术栈带来的好处。
下面我们来看看我们的哪些做法不可取。
我们需要的标签
标签是必不可少的,我们需要通过标签传达有关Docker镜像的信息。你可以将标签视为Docker镜像ID的别称。Git标签负责标记特定的提交,而Docker标签与之类似,可以给不同时间点上的Docker镜像添加版本。忘记打标签是小事,但会带来一些弊端,具体来讲,如果未指定标记,则默认镜像将被标记为latest。
FROM your_image_name:latest
如果你频繁执行该操作,那么很有可能镜像不是最新的,可能指向的是旧版本。因此,请使用适当的标签并遵守某个版本控制标准,例如语义版本控制。这样,Docker镜像使用者才能确保Docker镜像的兼容性并时刻保持最新,还可以有计划地使用正确的版本。
还有一个情况应该避免。你可以利用最新的默认标签(如FROM python3:latest),从Docker镜像仓库中提取最新的镜像。乍一看,这种做法似乎是个好主意,但却有一些意想不到的副作用:每个最新的请求可能都会派生出与以前的构建完全不同的Docker镜像。弄明白Docker镜像损坏的原因将会变得很困难,因为镜像本应该是不可变的。因此,我强力建议使用特定的标签来标记镜像(例如:python3:1.0.1)。这种方法可以确保你的Dockerfile保持不变。
在同一个容器中运行多个服务
虽然你可以在同一个容器中运行多个服务,但我并不建议你这么做,原因有两个。在使用Docker服务时,我们应该努力维持责任单一性。最佳做法是,组成应用程序的每个服务都应在各自的容器中运行,请务必将每项独立的功能都打包到单独的独立容器镜像中。
将多个服务添加到一个Docker镜像的做法似乎很诱人,但是你不应该将容器镜像视为虚拟机。一个容器包含多个服务,可能会导致你的应用程序很难水平扩展。Docker容器核心概念是,它们都是瞬态的,专为分发而设计,这对于现代Web应用程序来说很理想,因为它的瞬态特性、扩展和并发会非常容易。添加多个服务会增加管理分发的难度。
另外,单个容器上的多个服务还会加大管理安全性的难度。庞大的镜像可能会降低CI/CD的速度,你需要小心。
使用LABEL对镜像进行分类
这并不能说是反模式,但我认为值得一提。我在处理各种Docker镜像时注意到了一件事:有时这些镜像的创建者没有使用LABEL maintainer标签。这个标签可在事件中设置镜像的Author字段,当出现问题或需要澄清时,这个标签可以方便大家了解该与何人内部联系;如果镜像是公开共享的,也可以知道该与哪个外部的人联系。
这绝不是唯一可以使用的标签。你可以根据需要定义各种标签,来对镜像进行分类,定义许可信息,也可以定义标签来帮助自动化。
除了maintainer,还可以使用多行标签:
# Set one or more individual labels
LABEL com.example.version="0.0.1-beta"
LABEL vendor1="RBTSB Incorporated"
LABEL vendor2=TIPTAPCODE\ Incorporated
LABEL com.example.release-date="202-04-02"
LABEL com.example.version.production="0.0.1"
Docker 1.10之前的单行标签会创建新的docker层,如果你使用的是最新版的Docker,则不必担心创建额外的层。
LABEL vendor=ACME\ Incorporated \
com.example.is-beta= \
com.example.is-production="" \
com.example.version="0.0.1-beta" \
com.example.release-date="2015-02-12"
我们应该将尽可能多的元数据添加到不可变的Docker镜像,以方便追踪,提高可见性和可维护性。
避免构建依赖特定环境的镜像
在构建Docker镜像时,我们应始终牢记不变性。最好不要使用带有dev、test、staging和production的镜像,因为这会破坏单一来源的原则。另一个问题是,如果在不同环境上验证或调试,则这种做法无法保证镜像的相似。
为什么要使用非Root容器?
在默认情况下,Docker容器以root身份运行。以root用户身份运行的Docker容器可以完全控制主机系统。然而,出于安全考虑,我并不推荐这种做法。使用非root运行的Docker容器镜像可以多一层保护,在生产环境中通常建议使用非root容器。但是,由于这些容器由非root用户运行,因此无法执行需要特殊权限的任务。如果需要利用USER指令指定非root用户(如以下示例所示),则需要进行一些上下文切换。
FROM python:3.6-slim-buster
LABEL maintainer="Timothy Mugayi <timothy.mugayi@gmail.com>"RUN apt-get update && apt-get install -y --no-install-recommends \
wget && rm -rf /var/lib/apt/lists/*# Dumb init
RUN wget -O /usr/local/bin/dumb-init https://github.com/Yelp/dumb-init/releases/download/v1.2.0/dumb-init_1.2.0_amd64
RUN chmod +x /usr/local/bin/dumb-initRUN pip install --upgrade pipWORKDIR /usr/src/appCOPY requirements.txt .RUN pip install -r requirements.txtCOPY helloworld.py .USER 1001ENTRYPOINT ["/usr/local/bin/dumb-init", "python3", "-u", "./helloworld.py"]
如果你使用的基础镜像不是由root生成的,但需要切换回root,则可以执行以下操作:
FROM <namespace>/<image>:<tag_version>USER root
不要在单个容器内运行太多进程
容器的优点以及容器相对于虚拟机的优势在于,通过多个相互交互的容器组成一个完整的应用程序。我们无需在单个容器中运行完整的应用程序。相反,我们应尽可能将应用程序分解为多个服务,并将服务分布到多个容器上。这样可以最大程度地提高灵活性和可靠性。
不要在容器内安装操作系统
使用一段时间Docker后,你就有可能遇到这种情况。虽然你可以在容器内安装和运行完整的Linux操作系统。但是你应该这样做吗?
这可能不是一个好主意。Docker镜像是使用层的概念构建的,因此添加的东西越多,镜像就会膨胀得越大。一个完整的操作系统并不是Docker的理想使用情况。在理想情况下,你的容器内部只应该加载必要的组件。
不要在容器内运行不必要的服务
为了充分利用容器的优势,你应该尽可能保持容器精简。这样可以最大程度地提高性能,并最大程度地降低安全风险。因此,请避免运行并非绝对必要的服务。例如,若非必要请不要在容器内运行SSH服务,你可以选用其他方式(例如Docker exec)登录到容器。
不要在容器内加载不必要的程序
你必须知道这种反模式。在使用Docker时,很多人倾向于在镜像中加载sonar之类的工具,来确保代码覆盖率等。
使用从Docker CE 17.05+开始支持的多阶段构建(Multistage builds)模式,你可以在Dockerfile中使用多个FROM stage。临时构建的阶段容器将被丢弃,因此最终的运行时容器镜像都很精简。举个例子,当你需要从源文件编译一些二进制文件,然后在第二个阶段中将二进制文件复制到最终镜像中。
优点:
构建速度更快,精简的镜像可以让CI/CD过程更快,镜像通过网络进行传输时花费的时间也更少。
需要的存储空间更少。
冷启动(拉取镜像)更快。
潜在的受攻击面更少。
缺点:
容器内的工具较少,但这是保证容器的精简所需付出的一点小小的代价。
在容器内加入供观察的工具
即便没有监视解决方案,你也可以照常运行容器,但需要牢记,从本质上来说,你很难知道容器内部发生了什么,特别是随着容器数量的增加。
Docker本身自带了许多指标,能够公开每个运行容器的CPU、内存、网络和I/O使用情况,可通过Docker远程API的/stats端点访问。App dynamics 和 Newrelic 是两个现成的程序,可以与Docker镜像一起打包,帮助你从应用程序级别了解应用程序和容器的运行状况。
基础镜像的爱恨交织
“你知道是谁构建了这个景象,里面都添加了什么吗?”
这里说的都是可追溯性。牢记安全性是所有软件开发人员都应努力的方向。你需要了解如何跟踪Docker镜像的源,并了解里面有什么。
你需要时刻牢记以下几点:
镜像是怎样创建的。
验证镜像在创建后未经更改。
验证镜像的内容。
扫描镜像是否有安全漏洞。
我们可以通过一些工具对容器进行静态分析。这些工具涉及方方面面,已超出了本文的范围,但是我建议你花一些时间来学习。
Clair是一个有趣的工具,可为你的Docker应用程序提供自动容器漏洞和安全扫描。扫描基于常见的漏洞和公开(CVE)的数据库。如果你本地运行Docker,则可以下载Postgres,然后将Clair连接到Postgres上。以下是启动和运行Clair所需的最低配置:
$ mkdir $PWD/clair_config
$ curl -L https://raw.githubusercontent.com/coreos/clair/master/config.yaml.sample -o $
PWD/clair_config/config.yaml
$ docker run -d -e POSTGRES_PASSWORD="" -p 5432:5432 postgres:9.6
$ docker run --net=host -d -p 6060-6061:6060-6061 -v
PWD/clair_config:/config quay.io/coreos/clair:latest -config=/config/config.yaml
如果你想更改Postgres的端口,请确保同时修改config.yaml文件。如果你的系统上已经运行了另一个Postgres,请注意不要将docker端口更改为5432以外的端口。
clair:
database:
# Database driver
type: pgsql
options:
# PostgreSQL Connection string
# https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING
source: host=localhost port=5432 user=postgres password=123456 sslmode=disable statement_timeout=60000
在镜像启动并执行后,你可以运行Docker ps来确认容器已启动且正常运行:
你需要注意Clair没有Web UI或CLI。你只能通过REST API或第三方CLI工具来使用。
Bayan Collector是一款轻巧的静态分析应用程序,可以从镜像仓库启动容器以进行静态分析,可以运行任意脚本并收集有用的信息(例如已安装的软件包),强制执行策略,以及对镜像进行校验。
Docker Bench for Security是由Docker团队创建的工具,它会在Docker的宿主上运行一个安全最佳做法清单,并标记发现的任何问题。
不要在容器镜像中存储敏感数据
请一定避免这个错误。如果你的镜像对外公开,或者开发人员无意中将镜像推送到公共的Docker镜像仓库中,那么就会导致隐私和敏感信息的泄露。切记永远不要在Dockerfile中对敏感信息使用COPY或`。
为了避免这种情况,请将敏感数据存储在安全文件系统上,并让容器进行连接。一般情况下,这个文件系统应该在容器所在的宿主上,或者可通过AWS Elastic Block Storage(EBS)等块存储或S3等对象存储服务来使用。
此外,你应避免在Docker镜像中存储安全凭证。作为开发人员,有时我们会采用偷懒的做法,在代码中硬编码密码和私钥。你需要习惯使用-e参数在运行时为Docker容器指定环境变量。
你也可以通过env-file,从文件中读取环境变量。通过CMD或`从第三方来源获取凭据的自定义脚本也可用于获取Docker容器所需的相关凭据。
不要在容器内存储数据或日志
容器化改变了日志的性质。容器是瞬态、无状态应用程序的理想选择。本质上,存储在运行容器中的所有数据都应该是短暂的,你可能已经注意到,当容器关闭时,数据将丢失。因此,将数据存储在Docker容器之外的做法更值得推崇。有一些工具可以帮助你提取Docker日志,并将其放在更永久的数据存储中。
在处理Docker日志时需要牢记一点:Docker至少拥有三个级别的日志记录,即Docker容器、Docker服务和宿主操作系统,你选择的日志记录方法应该能够提取所有级别的日志。
不要写入容器的文件系统
每次将内容写入容器的文件系统都会激活“写时复制”策略。这会使用存储驱动程序(deviermapper、overlayfs或其他驱动)创建新的存储层。在实际应用中,这会给存储驱动带来巨大的压力,特别是使用Devicemapper或BTRFS的情况下。
确保容器只向卷中写入数据。对于小型的临时文件可以写入tmpfs,因为tmpfs是仅存在于内存或交换分区中的临时文件系统。
不要运行PID 1
这是许多人都不知道的常见问题。
Docker中的进程运行时没有init进程负责清理子进程,所以容器可能会出现僵尸进程,导致意料之外的错误。
使用tini或dumb-init
PID 1在Unix中很特殊,因此在init系统中忽略它通常会导致进程和信号处理错误。这会导致类似于容器无法优雅地停止,或本应摧毁的容器出现泄露等问题。
僵尸进程指的是运行已经停止,但依然在进程表中占据位置的进程,因为它们的父进程没有调用wait系统调用进行回收。理论上,每个结束的进程都会非常短暂地呈现僵尸状态,但有些进程的僵尸状态会持续很久。
如果某个进程会生成新进程,但信号处理的实现不好,无法捕获子进程的信号并将其终止,那么可以使用Tini或dumb-init。例如,bash脚本就无法正确处理或释放信号。
下面是运行dumb init的示例,其中prepare.sh可以是shell脚本,也可以是用于执行应用程序的命令:
RUN wget -O
/usr/local/bin/dumb-init
https://github.com/Yelp/dumb-init/releases/download/v1.2.0/dumb-init_1.2.0_amd64RUN chmod +x /usr/local/bin/dumb-initENTRYPOINT ["/usr/local/bin/dumb-init", "/usr/bin/prepare.sh"]
或者如果你选择tini,可以参考下面使用Python anaconda conda的示例:
RUN conda install --yes -c
conda-forge tiniENTRYPOINT ["tini", "-g", "--", "/usr/bin/prepare.sh"]
最后,这里是一个更一般的示例,不依赖于任何编程语言:
FROM node:13.12.0-slimMAINTAINER Timothy Mugayi <timothy.mugayi@gmail.com>ENV TINI_VERSION='v0.13.0'
# Add tini init, see https://github.com/krallin/tini
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tiniRUN chmod +x /tini
# Set tini as entrypoint
ENTRYPOINT ["/tini", "--"]
在CI/CD中使用Dockerfile格式检查
跟编程语言一样,Docker也有格式检查工具。
采用格式检查工具有许多好处,因为它会强制你使用最佳实践。在CI/CD过程中加入格式检查,可以帮助团队避免常见的错误,并在构建产品Docker镜像时建立最佳实践。可以从hadolint开始,这是一个用Haskell编写的Dockerfile格式检查工具,能够解析Dockerfile成AST,并在AST上执行规则检查。
我们来运行一个示例。要对Docker进行格式检查,可以执行以下命令:
$ docker run --rm -i
hadolint/hadolint < Dockerfile
格式检查完成后会显示以下结果:
上图中的错误码DL3008“Pin”来自hadolint的错误码描述状态。固定版本号可以强制构建时安装特定版本的包,不论缓存中是什么。这个技巧可以减少依赖包意料之外的改变导致的构建错误。
最后的一点想法
在本文中,我们谈了很多内容,覆盖了从安全到Docker镜像流水线的方方面面。
有许多技巧可以让Docker镜像更好、更安全。希望这篇文章可以给你带来一些启发,帮助你理解应该做什么、不应该做什么,并为你提供一些在构建内部或外部Docker容器镜像时可以应用的方案。
原文链接:
https://medium.com/better-programming/docker-best-practices-and-anti-patterns-e7cbccba4f19
本文为CSDN翻译文章,转载请注明出处。
推荐阅读
大促下的智能运维挑战:阿里如何抗住“双11猫晚”?
20万个法人、百万条银行账户信息,正在暗网兜售
当莎士比亚遇见Google Flax:教你用字符级语言模型和归递神经网络写“莎士比亚”式句子
Hyperledger Fabric 和企业级以太坊,谁才是企业首选?
面试时遇到「看门狗」脖子上挂着「时间轮」,我就问你怕不怕?
同期两篇 Nature:运行温度高于 1K 的量子计算平台问世!
GitHub 标星 10,000+,Apache 顶级项目 ShardingSphere 的开源之路
真香,朕在看了!