第 4 章 创建镜像
了解如何基于就绪可用的预构建镜像来创建自己的容器镜像。这一过程包括学习编写镜像、定义镜像元数据、测试镜像以及使用自定义构建程序工作流创建可用于 OpenShift Container Platform 的镜像的最佳实践。创建完镜像后,您可将其推送到内部 registry。
4.1. 学习容器最佳实践
在创建 OpenShift Container Platform 上运行的容器镜像时,镜像创建者需考虑诸多最佳实践,以确保为镜像的使用者提供良好体验。镜像原则上不可变且应按原样使用,所以请遵守以下准则,以确保您的镜像高度可用,且易于在 OpenShift Container Platform 上使用。
4.1.1. 常规容器镜像准则
无论容器镜像是否在 OpenShift Container Platform 中使用,在创建容器镜像时都需要遵循以下指导信息。
重复利用镜像
建议您尽可能使用 FROM
语句将您的镜像基于适用的上游镜像。这可确保,在上游镜像更新时您的镜像也可轻松从中获取安全修复,而不必再直接更新依赖项。
此外,请使用 FROM
指令中的标签(如 rhel:rhel7
),方便用户准确了解您的镜像基于哪个版本的镜像。使用除 latest
以外的标签可确保您的镜像不受 latest
版上游镜像重大更改的影响。
在标签内维持兼任性
在为自己的镜像添加标签时,建议尽量在标签内保持向后兼容性。例如:如果您提供名为 foo 的镜像,当前包含 1.0 版,则可使用 foo:v1 标签。当您更新了镜像时,只要仍与原始镜像兼容,就可继续使用 foo:v1 做为新镜像的标签。而使用这个标签的下游用户就可获得更新,而不会出现问题。
如果后续发布了不兼容的更新,则需要使用新标签,例如 foo:v2。这样,下游用户就可以根据需要选择是否升级到新版本,而不会因为不兼容的新镜像造成问题。下游用户如果使用 foo:latest,则可能要承担引入不兼容更改的风险。
避免多进程
我们不建议在同一容器中启动多个服务,如数据库和 SSHD。因为容器是轻量级的,可轻松链接到一起以编排多个进程,所以没有在同一个容器中启动多个服务的必要。您可以利用 OpenShift Container Platform 将相关镜像分组到一个 pod 中来轻松并置和共同管理镜像。
这种并置可确保容器共享一个网络命名空间和存储进行通信。因为对每个镜像的更新频率较低且可以独立进行,所以更新所可能带来的破坏风险也较小,单一进程的信号处理流程也更加清晰,因为无需管理将信号路由到多个分散进程的操作。
在 wrapper 脚本中使用 exec
很多镜像在启动正在运行的软件的进程前,会先使用 wrapper 脚本进行一些设置。如果您的镜像使用这类脚本,则该脚本应使用 exec
,以便使您的软件可以替代脚本的进程。如果不使用 exec
,则容器运行时发送的信号将进入 wrapper 脚本,而非软件的进程。这并不符合预期,解释如下:
假设您有一个为某些服务器启动进程的 wrapper 脚本。您启动了容器(例如使用 podman run -i
),该容器运行 wrapper 脚本,继而启动您的进程。现在假设您要通过 CTRL+C
终止容器。如果您的 wrapper 脚本使用了 exec 来启动服务器进程,则 podman
会将 SIGINT 发送至服务器进程,一切都将按照您的预期运作。如果没有在 wrapper 脚本中使用 exec,则 podman
会将 SIGINT 发送至 wrapper 脚本的进程,而您的进程将毫不受影响地继续运行。
另请注意,您的进程在容器中运行时,将作为 PID 1 运行。这表示,如果主进程终止,则整个容器都会停止,继而终止您已从 PID 1 进程启动的所有子进程。
如需了解更多影响,请参阅“Docker 和 PID 1 僵尸进程收割问题”博客文章。如需深入了解 PID 1 和 init 系统,请参阅“揭秘 init 系统 (PID 1)”博客文章。
清理临时文件
构建过程中创建的所有临时文件均应删除。这也包括通过 ADD
命令添加的任何文件。例如,我们强烈建议您在执行 yum install
操作后运行 yum clean
命令。
您可按照如下所示创建 RUN
语句来防止 yum
缓存最终留在镜像层:
RUN yum -y install mypackage && yum -y install myotherpackage && yum clean all -y
请注意,如果您改写为:
RUN yum -y install mypackage RUN yum -y install myotherpackage && yum clean all -y
则首次 yum
调用会将额外文件留在该层,后续运行 yum clean
操作时无法删除这些文件。最终镜像中看不到这些额外文件,但它们存在于底层中。
如果在较早层中删除了一些内容时,当前容器构建进程不允许在较晚的层中运行一个命令来缩减镜像使用的空间。但是,这个行为可能会在以后的版本中有所改变。这表示,如果在较晚层中执行 rm
命令,虽然被这个命令删除的文件不会出现在镜像中,但它不会使下载的镜像变小。因此,与 yum clean
示例一样,最好尽可能使用创建文件的同一命令删除文件,以免文件最终写入层中。
另外,在单个 RUN
语句中执行多个命令可减少镜像中的层数,缩短下载和提取时间。
按正确顺序放置指令
容器构建程序读取 Dockerfile
,并自上而下运行指令。成功执行的每个指令都会创建一个层,可在下次构建该镜像或其他镜像时重复使用。务必要将极少变化的指令放置在 Dockerfile
的顶部。这样做可确保下次构建相同镜像会非常迅速,因为缓存不会因为上层变化而失效。
例如:如果您正在使用 Dockerfile
,它包含一个用于安装正在迭代的文件的 ADD
命令,以及一个用于 yum install
软件包的 RUN
命令,则最好将 ADD
命令放在最后:
FROM foo RUN yum -y install mypackage && yum clean all -y ADD myfile /test/myfile
这样,您每次编辑 myfile 和重新运行 podman build
或 docker build
时,系统都可重复利用 yum
命令的缓存层,仅为 ADD
操作生成新层。
如果您将 Dockerfile
改写为:
FROM foo ADD myfile /test/myfile RUN yum -y install mypackage && yum clean all -y
则您每次更改 myfile 和重新运行 podman build
或 docker build
时,ADD
操作都会导致 RUN
层缓存无效,因此 yum
操作也必须要重新运行。
标记重要端口
EXPOSE 指令使主机系统和其它容器可使用容器中的端口。尽管可以指定应当通过 podman run
调用来公开端口,但在 Dockerfile
中使用 EXPOSE 指令可显式声明您的软件需要运行的端口,让用户和软件更易于使用您的镜像:
-
公开端口将显示在
podman ps
下,与从您的镜像创建的容器关联 -
公开端口还存在于通过
podman inspect
返回的镜像元数据中 - 当将一个容器链接到另一容器时,公开端口也会链接到一起
设置环境变量
设置环境变量的最佳做法是使用 ENV
指令设置环境变量。一个例子是设置项目版本。这让人可以无需通过查看 Dockerfile
便可轻松找到版本。另一示例是在系统上公告可供其他进程使用的路径,如 JAVA_HOME
。
避免默认密码
最好避免设置默认密码。很多人扩展镜像时会忘记删除或更改默认密码。如果在生产环境中的用户被分配了众所周知的密码,则这可能引发安全问题。应该使用环境变量来使密码可以被配置。
如果您的确要选择设置默认密码,请确保在容器启动时显示适当的警告消息。消息中应告知用户默认密码的值并说明如何修改密码,例如要设置什么环境变量。
避免 sshd
最好避免在您的镜像中运行 sshd。您可使用 podman exec
或 docker exec
命令访问本地主机上运行的容器。另外,也可使用 oc exec
命令或 oc rsh
命令访问 OpenShift Container Platform 集群上运行的容器。在镜像中安装并运行 sshd 会为安全攻击打开额外通道,因而需要安装安全补丁。
对持久性数据使用卷
镜像应对持久性数据使用卷。这样,OpenShift Container Platform 便可将网络存储挂载至运行容器的节点,如果容器移至新节点,存储也将重新连接至该节点。通过使用卷来满足所有持久性存储需求,即使容器重启或移动,其内容也会保留下来。如果您的镜像将数据写入容器中的任意位置,则其内容可能无法保留。
所有在容器销毁后仍需要保留的数据都必须写入卷中。容器引擎支持容器的 readonly
标记,可用于严格执行不将数据写入容器临时存储的良好做法。现在围绕该功能设计您的镜像,将更便于以后利用。
另外,在 Dockerfile
中显式定义卷可方便镜像用户轻松了解在运行您的镜像时必须要定义的卷。
有关如何在 OpenShift Container Platform 中使用卷的更多信息,请参阅 Kubernetes 文档。
即使具有持久性卷,您的镜像的每个实例也都有自己的卷,且文件系统不会在实例之间共享。这意味着卷无法用于共享集群中的状态。
其它资源
- Docker 文档 - 编写 Dockerfile 的最佳实践
- Project Atomic 文档 - 容器镜像创建者指南
4.1.2. OpenShift Container Platform 特定准则
以下是创建 OpenShift Container Platform 上专用的容器镜像时适用的准则。
启用 Source-to-Image (S2I) 的镜像
对于计划运行由第三方提供的应用程序代码的镜像,例如专为运行由开发人员提供的 Ruby 代码而设计的 Ruby 镜像,您可以让镜像与 Source-to-Image (S2I) 构建工具协同工作。S2I 是一个框架,便于编写以应用程序源代码为输入的镜像和生成以运行汇编应用程序为输出的新镜像。
例如,该 Python 镜像定义了构建各个版本的 Python 应用程序的 S2I 脚本。
支持任意用户 id
默认情况下,OpenShift Container Platform 使用任意分配的用户 ID 来运行容器。这对因容器引擎漏洞而逸出容器的进程提供了额外的安全防护,从而避免在主机节点上出现未授权的权限升级的问题。
对于支持以任意用户身份运行的镜像,由镜像中进程写入的目录和文件应归 root 组所有,并可由该组读/写。待执行文件还应具有组执行权限。
向 Dockerfile 中添加以下内容可将目录和文件权限设置为允许 root 组中的用户在构建镜像中访问它们:
RUN chgrp -R 0 /some/directory && \ chmod -R g=u /some/directory
因为容器用户始终是 root 组的成员,所以容器用户可以读写这些文件。
在修改容器敏感区域的目录和文件权限时,必须小心。
对于敏感区域,如 /etc/passwd
,用户意外地对这些文件进行修改,可能会导致容器或主机被暴露。CRI-O 支持将随机用户 ID 插入容器的 /etc/passwd
中,因此不需要更改其权限。
此外,容器中运行的进程不是以特权用户身份运行,因此不得监听特权端口(低于 1024 的端口)。
如果您的 S2I 镜像不含带有用户 id 的 USER 声明,则您的构建将默认失败。为了允许使用指定用户或 root (0) 用户的镜像在 OpenShift Container Platform 中进行构建,您可向特权安全上下文约束 (SCC) 添加项目的构建程序服务帐户 (system:serviceaccount:<your-project>:builder)。此外,您还可允许所有镜像以任何用户身份运行。
使用服务进行镜像间通信
对于您的镜像需要与另一镜像提供的服务通信的情况,例如需要访问数据库镜像来存储和检索数据的 web 前端镜像,则您的镜像应使用一个 OpenShift Container Platform 服务。服务为访问提供静态端点,该端点不会随着容器的停止、启动或移动而改变。此外,服务还会为请求提供负载均衡。
提供通用库
对于要运行由第三方提供的应用程序代码的镜像,请确保您的镜像包含适用于您的平台的通用库。特别要为平台使用的通用数据库提供数据库驱动程序。例如,在创建 Java 框架镜像时,要为 MySQL 和 PostgreSQL 提供 JDBC 驱动程序。这样做可避免在应用程序汇编期间下载通用依赖项,从而加快应用程序镜像构建。此外还简化了应用程序开发人员为确保满足所有依赖项而需要做的工作。
使用环境变量进行配置
您的镜像用户应在无需基于您的镜像创建下游镜像的情况下也可进行配置。这表示,运行时配置应使用环境变量进行处理。对于简单的配置,运行中的进程可直接使用环境变量。对于更为复杂的配置或对于不支持此操作的运行时,可通过定义启动过程中处理的模板配置文件来配置运行时。在此处理过程中,可将使用环境变量提供的值替换到配置文件中,或用于决定要在配置文件中设置哪些选项。
此外,也可以使用环境变量将证书和密钥等 secret 传递到容器中,这是建议操作。这样可确保 secret 值最终不会提交到镜像中,也不会泄漏到容器镜像 registry 中。
提供环境变量可方便您的镜像用户自定义行为,如数据库设置、密码和性能调优,而无需在镜像顶部引入新层。相反,用户可在定义 pod 时简单定义环境变量值,且无需重新构建镜像也可更改这些设置。
对于极其复杂的场景,还可使用在运行时挂载到容器中的卷来提供配置。但是,如果选择这种配置方式时,您必须确保当不存在必要卷或配置时,您的镜像可在启动时提供清晰的错误消息。
本主题与“使用服务进行镜像间通信”主题之间的相关之处在于,数据源等配置应当根据提供服务端点信息的环境变量来定义。这使得应用程序在不修改应用程序镜像的情况下即可动态使用 OpenShift Container Platform 环境中定义的数据源服务。
另外,调整应通过检查容器的 cgroups 设置来实现。这使得镜像可根据可用内存、CPU 和其他资源自行调整。例如,基于 Java 的镜像应根据 cgroup 最大内存参数调整其堆大小,以确保不超过限值且不出现内存不足错误。
有关如何在容器中管理 cgroup 配额的更多信息,请参阅以下参考资料:
- 博客文章 - Docker 中的资源管理
- Docker 文档 - 运行时指标
- 博客文章 - Linux 容器内存
设置镜像元数据
定义镜像元数据有助于 OpenShift Container Platform 更好地使用您的容器镜像,允许 OpenShift Container Platform 使用您的镜像为开发人员创造更好的体验。例如,您可以添加元数据以提供有用的镜像描述,或针对可能也需要的其他镜像提供建议。
集群
您必须充分了解运行镜像的多个实例的意义。在最简单的情况下,服务的负载均衡功能会处理将流量路由到镜像的所有实例。但是,许多框架必须共享信息才能执行领导选举机制或故障转移状态,例如在会话复制中。
设想您的实例在 OpenShift Container Platform 中运行时如何完成这一通信。尽管 pod 之间可直接相互通信,但其 IP 地址会随着 pod 的启动、停止和移动而变化。因此,集群方案必须是动态的。
日志记录
最好将所有日志记录发送至标准输出。OpenShift Container Platform 从容器收集标准输出,然后将其发送至集中式日志记录服务,以供查看。如果必须将日志内容区分开来,请在输出前添加适当关键字,这样便可过滤消息。
如果您的镜像日志记录到文件,则用户必须通过手动操作进入运行中的容器,并检索或查看日志文件。
存活 (liveness) 和就绪 (readiness) 探针
记录可用于您的镜像的示例存活和就绪探针。有了这些探针,用户便可放心部署您的镜像,确保在容器准备好处理流量之前,流量不会路由到容器,并且如果进程进入不健康状态,容器将重启。
模板
考虑为您的镜像提供一个示例模板。用户借助模板可轻松利用有效的配置快速部署您的镜像。模板应包括与镜像一同记录的存活和就绪探针,以保证完整性。