第 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 进程启动的所有子进程。

清理临时文件

删除构建过程中创建的所有临时文件。这也包括通过 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 builddocker build 时,系统都可重复利用 yum 命令的缓存层,仅为 ADD 操作生成新层。

如果您将 Dockerfile 改写为:

FROM foo
ADD myfile /test/myfile
RUN yum -y install mypackage && yum clean all -y

则您每次更改 myfile 和重新运行 podman builddocker build 时,ADD 操作都会导致 RUN 层缓存无效,因此 yum 操作也必须要重新运行。

标记重要端口

EXPOSE 指令使主机系统和其它容器可使用容器中的端口。尽管可以指定应当通过 podman run 调用来公开端口,但在 Dockerfile 中使用 EXPOSE 指令可显式声明您的软件需要运行的端口,让用户和软件更易于使用您的镜像:

  • 公开端口显示在 podman ps 下,与从您的镜像创建的容器关联。
  • 公开端口存在于 podman inspect 返回的镜像元数据中。
  • 当将一个容器链接到另一容器时,公开端口也会链接到一起
设置环境变量

设置环境变量的最佳做法是使用 ENV 指令设置环境变量。一个例子是设置项目版本。这让人可以无需通过查看 Dockerfile 便可轻松找到版本。另一示例是在系统上公告可供其他进程使用的路径,如 JAVA_HOME

避免默认密码

避免设置默认密码。许多人扩展镜像时会忘记删除或更改默认密码。如果在生产环境中的用户被分配了众所周知的密码,则这可能引发安全问题。可以使用环境变量来配置密码。

如果您的确要选择设置默认密码,请确保在容器启动时显示适当的警告消息。消息中应告知用户默认密码的值并说明如何修改密码,例如要设置什么环境变量。

避免 sshd

最好避免在您的镜像中运行 sshd。您可使用 podman execdocker exec 命令访问本地主机上运行的容器。另外,也可使用 oc exec 命令或 oc rsh 命令访问 OpenShift Container Platform 集群上运行的容器。在镜像中安装并运行 sshd 会为安全攻击打开额外通道,因而需要安装安全补丁。

对持久性数据使用卷

镜像应对持久性数据使用。这样,OpenShift Container Platform 便可将网络存储挂载至运行容器的节点,如果容器移至新节点,存储也将重新连接至该节点。通过使用卷来满足所有持久性存储需求,即使容器重启或移动,其内容也会保留下来。如果您的镜像将数据写入容器中的任意位置,则其内容可能无法保留。

所有在容器销毁后仍需要保留的数据都必须写入卷中。容器引擎支持容器的 readonly 标记,可用于严格执行不将数据写入容器临时存储的良好做法。现在围绕该功能设计您的镜像,将更便于以后利用。

Dockerfile 中显式定义卷可方便镜像用户轻松了解在运行您的镜像时必须要定义的卷。

有关如何在 OpenShift Container Platform 中使用卷的更多信息,请参阅 Kubernetes 文档

注意

即使具有持久性卷,您的镜像的每个实例也都有自己的卷,且文件系统不会在实例之间共享。这意味着卷无法用于共享集群中的状态。

4.1.2. OpenShift Container Platform 特定准则

以下是创建 OpenShift Container Platform 上专用的容器镜像时适用的准则。

启用 Source-to-Image (S2I) 的镜像

对于计划运行由第三方提供的应用程序代码的镜像,例如专为运行由开发人员提供的 Ruby 代码而设计的 Ruby 镜像,您可以让镜像与 Source-to-Image (S2I) 构建工具协同工作。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 中进行构建,您可以将项目的构建程序服务帐户 system:serviceaccount:<your-project>:builder 添加至 privileged 安全上下文约束(SCC)。此外,您还可允许所有镜像以任何用户身份运行。

使用服务进行镜像间通信

对于您的镜像需要与另一镜像提供的服务通信的情况,例如需要访问数据库镜像来存储和检索数据的 web 前端镜像,则您的镜像应使用一个 OpenShift Container Platform 服务。服务为访问提供静态端点,该端点不会随着容器的停止、启动或移动而改变。此外,服务还会为请求提供负载均衡。

提供通用库

对于要运行由第三方提供的应用程序代码的镜像,请确保您的镜像包含适用于您的平台的通用库。特别要为平台使用的通用数据库提供数据库驱动程序。例如,在创建 Java 框架镜像时,要为 MySQL 和 PostgreSQL 提供 JDBC 驱动程序。这样做可避免在应用程序汇编期间下载通用依赖项,从而加快应用程序镜像构建。此外还简化了应用程序开发人员为确保满足所有依赖项而需要做的工作。

使用环境变量进行配置

您的镜像用户应在无需基于您的镜像创建下游镜像的情况下也可进行配置。这意味着运行时配置使用环境变量进行处理。对于简单的配置,运行中的进程可直接使用环境变量。对于更为复杂的配置或对于不支持此操作的运行时,可通过定义启动过程中处理的模板配置文件来配置运行时。在此处理过程中,可将使用环境变量提供的值替换到配置文件中,或用于决定要在配置文件中设置哪些选项。

此外,也可以使用环境变量将证书和密钥等 secret 传递到容器中,这是建议操作。这样可确保 secret 值最终不会提交到镜像中,也不会泄漏到容器镜像 registry 中。

提供环境变量可方便您的镜像用户自定义行为,如数据库设置、密码和性能调优,而无需在镜像顶部引入新层。相反,用户可在定义 pod 时简单定义环境变量值,且无需重新构建镜像也可更改这些设置。

对于极其复杂的场景,还可使用在运行时挂载到容器中的卷来提供配置。但是,如果选择这种配置方式时,您必须确保当不存在必要卷或配置时,您的镜像可在启动时提供清晰的错误消息。

本主题与“使用服务进行镜像间通信”主题之间的相关之处在于,数据源等配置应当根据提供服务端点信息的环境变量来定义。这使得应用程序在不修改应用程序镜像的情况下即可动态使用 OpenShift Container Platform 环境中定义的数据源服务。

另外,调整应通过检查容器的 cgroups 设置来实现。这使得镜像可根据可用内存、CPU 和其他资源自行调整。例如,基于 Java 的镜像应根据 cgroup 最大内存参数调整其堆大小,以确保不超过限值且不出现内存不足错误。

设置镜像元数据

定义镜像元数据有助于 OpenShift Container Platform 更好地使用您的容器镜像,允许 OpenShift Container Platform 使用您的镜像为开发人员创造更好的体验。例如,您可以添加元数据以提供有用的镜像描述,或针对可能也需要的其他镜像提供建议。

集群

您必须充分了解运行镜像的多个实例的意义。在最简单的情况下,服务的负载均衡功能会处理将流量路由到镜像的所有实例。但是,许多框架必须共享信息才能执行领导选举机制或故障转移状态,例如在会话复制中。

设想您的实例在 OpenShift Container Platform 中运行时如何完成这一通信。尽管 pod 之间可直接相互通信,但其 IP 地址会随着 pod 的启动、停止和移动而变化。因此,集群方案必须是动态的。

日志记录

最好将所有日志记录发送至标准输出。OpenShift Container Platform 从容器收集标准输出,然后将其发送至集中式日志记录服务,以供查看。如果必须将日志内容区分开来,请在输出前添加适当关键字,这样便可过滤消息。

如果您的镜像日志记录到文件,则用户必须通过手动操作进入运行中的容器,并检索或查看日志文件。

存活 (liveness) 和就绪 (readiness) 探针

记录可用于您的镜像的示例存活和就绪探针。有了这些探针,用户便可放心部署您的镜像,确保在容器准备好处理流量之前,流量不会路由到容器,并且如果进程进入不健康状态,容器将重启。

模板

考虑为您的镜像提供一个示例模板。用户借助模板可轻松利用有效的配置快速部署您的镜像。模板应包括与镜像一同记录的存活和就绪探针,以保证完整性。