×

学习如何基于现成的预构建镜像创建您自己的容器镜像。此过程包括学习编写镜像的最佳实践、定义镜像的元数据、测试镜像以及使用自定义构建器工作流程创建可与 OpenShift Dedicated 一起使用的镜像。

学习容器最佳实践

在创建要在 OpenShift Dedicated 上运行的容器镜像时,作为镜像作者,需要考虑许多最佳实践,以确保这些镜像的使用者拥有良好的体验。由于镜像旨在保持不变并按原样使用,因此以下指南有助于确保您的镜像高度可用且易于在 OpenShift Dedicated 上使用。

通用容器镜像指南

以下指南适用于一般情况下创建容器镜像,而与这些镜像是否在 OpenShift Dedicated 上使用无关。

重用镜像

尽可能使用 `FROM` 语句基于适当的上游镜像构建您的镜像。这确保您的镜像可以在上游镜像更新时轻松获取安全修复程序,而不是您必须直接更新您的依赖项。

此外,在 `FROM` 指令中使用标签,例如 `rhel:rhel7`,以向用户明确说明您的镜像基于哪个版本的镜像。使用除 `latest` 之外的标签可确保您的镜像不会受到可能进入上游镜像 `latest` 版本的重大更改的影响。

在标签内保持兼容性

在标记您自己的镜像时,尝试在标签内保持向后兼容性。例如,如果您提供名为 `image` 的镜像,并且它当前包含版本 `1.0`,则您可以提供 `image:v1` 的标签。当您更新镜像时,只要它继续与原始镜像兼容,您就可以继续将新镜像标记为 `image:v1`,并且此标签的下游使用者能够获得更新而不会被破坏。

如果您以后发布不兼容的更新,则切换到新的标签,例如 `image:v2`。这允许下游使用者随意升级到新版本,而不会被新的不兼容镜像意外破坏。使用 `image:latest` 的任何下游使用者都承担引入任何不兼容更改的风险。

避免多个进程

不要在一个容器内启动多个服务,例如数据库和 `SSHD`。这是不必要的,因为容器很轻量级,可以轻松地链接在一起以编排多个进程。OpenShift Dedicated 允许您通过将相关的镜像分组到单个 Pod 中来轻松地共同定位和共同管理这些镜像。

这种共同定位确保容器共享网络命名空间和用于通信的存储。更新也较少中断,因为每个镜像都可以更不频繁地独立更新。信号处理流程在单个进程中也更清晰,因为您不必管理将信号路由到生成的进程。

在包装脚本中使用 `exec`

许多镜像使用包装脚本在启动正在运行的软件的进程之前进行一些设置。如果您的镜像使用这样的脚本,则该脚本使用exec,以便用您的软件替换脚本的进程。如果不使用exec,则容器运行时发送的信号将发送到您的包装脚本,而不是您的软件进程。这不是您想要的。

如果您有一个包装脚本,它启动某个服务器的进程。例如,您使用podman run -i启动容器,这将运行包装脚本,进而启动您的进程。如果您想使用CTRL+C关闭容器。如果您的包装脚本使用exec启动服务器进程,podman会将SIGINT信号发送到服务器进程,一切都会按预期工作。如果您在包装脚本中没有使用execpodman会将SIGINT信号发送到包装脚本的进程,而您的进程则会继续运行,就像什么也没发生一样。

还要注意,您的进程在容器中运行时为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的顶部非常重要。这样做可以确保相同镜像的后续构建非常快,因为缓存不会因上层更改而失效。

例如,如果您正在处理一个包含ADD命令(用于安装您正在迭代的文件)和RUN命令(用于yum install包)的Dockerfile,最好将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 Dedicated集群上运行的容器。在镜像中安装和运行sshd会打开额外的攻击媒介和安全补丁的要求。

使用卷存储持久数据

镜像使用存储持久数据。这样,OpenShift Dedicated 将网络存储挂载到运行容器的节点上,如果容器移动到新的节点,存储将重新挂载到该节点。通过对所有持久性存储需求使用卷,即使容器重新启动或移动,内容也会保留。如果您的镜像将数据写入容器内的任意位置,则可能无法保留该内容。

即使在容器被销毁后也需要保留的所有数据都必须写入卷。容器引擎支持容器的readonly标志,可用于严格执行有关不将数据写入容器中临时存储的良好实践。现在围绕该功能设计您的镜像,可以使其以后更容易利用它。

在您的Dockerfile中显式定义卷,使用镜像的用户可以轻松了解他们在运行您的镜像时必须定义哪些卷。

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

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

OpenShift Dedicated 特定指南

以下是专门针对在 OpenShift Dedicated 上使用而创建容器镜像时适用的指南。

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

对于旨在运行第三方提供的应用程序代码的镜像(例如,旨在运行开发者提供的 Ruby 代码的 Ruby 镜像),您可以启用您的镜像以与Source-to-Image (S2I)构建工具一起工作。S2I是一个框架,它简化了编写镜像的过程,这些镜像将应用程序源代码作为输入,并生成一个运行已组装应用程序的新镜像作为输出。

支持任意用户ID

默认情况下,OpenShift Dedicated 使用任意分配的用户 ID 运行容器。这提供了额外的安全性,可以防止由于容器引擎漏洞而导致进程逃逸容器,从而在主机节点上获得提升的权限。

为了使镜像支持以任意用户身份运行,镜像中进程写入的目录和文件必须属于 root 组所有,并且该组可读写。要执行的文件也必须具有组执行权限。

在您的 Dockerfile 中添加以下内容可以设置目录和文件的权限,以允许 root 组中的用户在生成的镜像中访问它们。

RUN chgrp -R 0 /some/directory && \
    chmod -R g=u /some/directory

因为容器用户始终是 root 组的成员,所以容器用户可以读取和写入这些文件。

更改容器敏感区域的目录和文件权限时必须小心。如果应用于敏感区域(例如/etc/passwd文件),此类更改可能会允许意外用户修改这些文件,从而可能泄露容器或主机。CRI-O 支持将任意用户 ID 插入到容器的/etc/passwd文件中。因此,永远不需要更改权限。

此外,/etc/passwd文件不应该存在于任何容器镜像中。如果存在,CRI-O 容器运行时将无法将随机 UID 注入到/etc/passwd文件中。在这种情况下,容器可能难以解析活动 UID。未能满足此要求可能会影响某些容器化应用程序的功能。

此外,由于容器中运行的进程并非以特权用户身份运行,因此它们不得监听特权端口(低于 1024 的端口)。

使用服务进行镜像间通信

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

提供常用库

对于旨在运行第三方提供的应用程序代码的镜像,请确保您的镜像包含平台常用的库。特别是,为与您的平台一起使用的常用数据库提供数据库驱动程序。例如,如果您正在创建 Java 框架镜像,请提供 MySQL 和 PostgreSQL 的 JDBC 驱动程序。这样做可以避免在应用程序组装时下载常用依赖项,从而加快应用程序镜像构建速度。它还可以简化应用程序开发人员所需的工作,以确保满足所有依赖项。

使用环境变量进行配置

您的镜像用户能够对其进行配置,而无需基于您的镜像创建下游镜像。这意味着运行时配置是使用环境变量处理的。对于简单的配置,运行中的进程可以直接使用环境变量。对于更复杂的配置或不支持此功能的运行时,请通过定义在启动期间处理的模板配置文件来配置运行时。在此处理过程中,可以使用环境变量提供的数值替换配置文件中的值,或用于确定在配置文件中设置哪些选项。

也可以建议使用环境变量将证书和密钥等机密传递到容器中。这确保了机密值不会最终提交到镜像中并泄漏到容器镜像注册表中。

提供环境变量允许您的镜像使用者自定义行为,例如数据库设置、密码和性能调整,而无需在您的镜像之上引入新的层。相反,他们只需在定义 Pod 时定义环境变量值,并在无需重建镜像的情况下更改这些设置。

对于极其复杂的场景,也可以使用在运行时挂载到容器中的卷来提供配置。但是,如果您选择这样做,则必须确保您的镜像在缺少必要的卷或配置时在启动时提供清晰的错误消息。

本主题与“使用服务进行镜像间通信”主题相关,因为数据源等配置是根据提供服务端点信息的的环境变量定义的。这允许应用程序动态地使用在 OpenShift Dedicated 环境中定义的数据源服务,而无需修改应用程序镜像。

此外,调整是通过检查容器的cgroups设置来完成的。这允许镜像自行调整到可用的内存、CPU 和其他资源。例如,基于 Java 的镜像会根据cgroup最大内存参数调整其堆大小,以确保它们不会超过限制并出现内存不足错误。

设置镜像元数据

定义镜像元数据有助于 OpenShift Dedicated 更好地使用您的容器镜像,从而使 OpenShift Dedicated 能够为使用您的镜像的开发者创造更好的体验。例如,您可以添加元数据来提供对镜像的有用描述,或提供对所需其他镜像的建议。

集群

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

考虑您的实例在 OpenShift Dedicated 中运行时如何实现这种通信。尽管 Pod 可以彼此直接通信,但它们的 IP 地址在 Pod 启动、停止或移动时都会发生变化。因此,您的集群方案必须是动态的非常重要。

日志记录

最好将所有日志发送到标准输出。OpenShift Dedicated 收集容器的标准输出并将其发送到集中式日志服务,以便查看。如果必须分离日志内容,请在输出前添加适当的关键字,以便过滤消息。

如果您的镜像将日志写入文件,用户必须使用手动操作进入正在运行的容器并检索或查看日志文件。

存活性探针和就绪性探针

文档示例存活性探针和就绪性探针,可用于您的镜像。这些探针允许用户自信地部署您的镜像,确保在容器准备好处理请求之前不会将流量路由到该容器,并且如果进程进入不健康状态,则会重新启动容器。

模板

考虑为您的镜像提供一个示例模板。模板为用户提供了一种简单的方法,可以快速部署您的镜像并使用有效的配置。为完整起见,您的模板必须包含您在镜像中记录的存活性探针和就绪性探针。

在镜像中包含元数据

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

本主题仅定义当前用例集所需的元数据。将来可能会添加其他元数据或用例。

定义镜像元数据

您可以使用Dockerfile中的LABEL指令来定义镜像元数据。标签类似于环境变量,它们是附加到镜像或容器的键值对。标签与环境变量的不同之处在于它们对正在运行的应用程序不可见,并且还可以用于快速查找镜像和容器。

Docker 文档,了解更多关于LABEL指令的信息。

标签名称通常是带命名空间的。命名空间会相应设置以反映将获取标签并使用它们的项目。对于 OpenShift Dedicated,命名空间设置为io.openshift,对于 Kubernetes,命名空间设置为io.k8s

查看Docker 自定义元数据文档,了解有关格式的详细信息。

表 1. 支持的元数据
变量 描述

io.openshift.tags

此标签包含以逗号分隔的字符串值列表表示的标签列表。标签是将容器镜像分类到广泛的功能领域的方式。标签帮助 UI 和生成工具在应用程序创建过程中建议相关的容器镜像。

LABEL io.openshift.tags   mongodb,mongodb24,nosql

io.openshift.wants

指定生成工具和 UI 用于提供相关建议的标签列表,如果您还没有带有指定标签的容器镜像。例如,如果容器镜像需要mysqlredis,而您没有带有redis标签的容器镜像,则 UI 可以建议您将此镜像添加到您的部署中。

LABEL io.openshift.wants   mongodb,redis

io.k8s.description

此标签可用于向容器镜像使用者提供有关此镜像提供的服务或功能的更详细信息。然后,UI 可以将此描述与容器镜像名称一起使用,为最终用户提供更人性化的信息。

LABEL io.k8s.description The MySQL 5.5 Server with master-slave replication support

io.openshift.non-scalable

镜像可以使用此变量来建议它不支持缩放。然后,UI 将此信息传达给该镜像的使用者。不可缩放意味着replicas的值最初不应设置为高于1

LABEL io.openshift.non-scalable     true

io.openshift.min-memoryio.openshift.min-cpu

此标签建议容器镜像正常工作需要多少资源。UI 可以警告用户部署此容器镜像可能会超过其用户配额。值必须与 Kubernetes 数量兼容。

LABEL io.openshift.min-memory 16Gi
LABEL io.openshift.min-cpu     4

使用 Source-to-Image 创建镜像

Source-to-Image (S2I) 是一个框架,它简化了编写镜像的过程,这些镜像将应用程序源代码作为输入,并生成一个运行已组装应用程序的新镜像作为输出。

使用 S2I 构建可重复的容器镜像的主要优点是易于开发人员使用。作为构建器镜像的作者,您必须了解两个基本概念才能使您的镜像提供最佳 S2I 性能:构建过程和 S2I 脚本。

了解 Source-to-Image 构建过程

构建过程由以下三个基本元素组成,它们组合成最终的容器镜像

  • 源代码

  • Source-to-Image (S2I) 脚本

  • 构建器镜像

S2I 使用构建器镜像作为第一个FROM指令生成一个 Dockerfile。然后将 S2I 生成的 Dockerfile 传递给 Buildah。

如何编写 Source-to-Image 脚本

您可以使用任何编程语言编写 Source-to-Image (S2I) 脚本,只要脚本在构建器镜像内可执行即可。S2I 支持多个选项,提供assemble/run/save-artifacts脚本。在每次构建中,都会按以下顺序检查所有这些位置

  1. 在构建配置中指定的脚本。

  2. 在应用程序源代码的.s2i/bin目录中找到的脚本。

  3. 在带有io.openshift.s2i.scripts-url标签的默认镜像 URL 中找到的脚本。

在镜像中指定的io.openshift.s2i.scripts-url标签和在构建配置中指定的脚本都可以采用以下形式之一

  • image:///path_to_scripts_dir:镜像内 S2I 脚本所在的目录的绝对路径。

  • file:///path_to_scripts_dir:主机上 S2I 脚本所在的目录的相对或绝对路径。

  • http(s)://path_to_scripts_dir:S2I 脚本所在的目录的 URL。

表 2. S2I 脚本
脚本 描述

assemble

assemble脚本从源代码构建应用程序工件,并将它们放置到镜像内的适当目录中。此脚本是必需的。此脚本的工作流程为

  1. 可选:恢复构建工件。如果您想支持增量构建,请确保也定义save-artifacts

  2. 将应用程序源代码放置在所需位置。

  3. 构建应用程序工件。

  4. 将工件安装到适合它们运行的位置。

run

run脚本执行您的应用程序。此脚本是必需的。

save-artifacts

save-artifacts脚本收集所有可以加快后续构建过程的依赖项。此脚本是可选的。例如

  • 对于 Ruby,由 Bundler 安装的gems

  • 对于 Java,.m2内容。

这些依赖项被收集到一个tar文件中并流式传输到标准输出。

usage

usage脚本允许您告知用户如何正确使用您的镜像。此脚本是可选的。

test/run

test/run脚本允许您创建一个进程来检查镜像是否正常工作。此脚本是可选的。该进程的建议流程是

  1. 构建镜像。

  2. 运行镜像以验证usage脚本。

  3. 运行s2i build以验证assemble脚本。

  4. 可选:再次运行s2i build以验证save-artifactsassemble脚本的保存和恢复构件功能。

  5. 运行镜像以验证测试应用程序是否正常工作。

建议将test/run脚本构建的测试应用程序放置在镜像仓库中的test/test-app目录。

S2I脚本示例

以下S2I脚本示例使用Bash编写。每个示例都假设其tar内容已解压到/tmp/s2i目录。

assemble脚本
#!/bin/bash

# restore build artifacts
if [ "$(ls /tmp/s2i/artifacts/ 2>/dev/null)" ]; then
    mv /tmp/s2i/artifacts/* $HOME/.
fi

# move the application source
mv /tmp/s2i/src $HOME/src

# build application artifacts
pushd ${HOME}
make all

# install the artifacts
make install
popd
run脚本
#!/bin/bash

# run the application
/opt/application/run.sh
save-artifacts脚本
#!/bin/bash

pushd ${HOME}
if [ -d deps ]; then
    # all deps contents to tar stream
    tar cf - deps
fi
popd
usage脚本
#!/bin/bash

# inform the user how to use the image
cat <<EOF
This is a S2I sample builder image, to use it, install
https://github.com/openshift/source-to-image
EOF
附加资源

关于测试源到镜像镜像

作为源到镜像 (S2I) 构建器镜像的作者,您可以本地测试您的S2I镜像,并使用OpenShift Dedicated构建系统进行自动化测试和持续集成。

S2I需要assemblerun脚本才能成功运行S2I构建。提供save-artifacts脚本可以重用构建构件,提供usage脚本可以确保在有人在S2I外部运行容器镜像时将使用信息打印到控制台。

测试S2I镜像的目标是确保所有这些描述的命令都能正常工作,即使基础容器镜像已更改或命令使用的工具已更新。

了解测试需求

test脚本的标准位置是test/run。此脚本由OpenShift Dedicated S2I镜像构建器调用,它可以是一个简单的Bash脚本或一个静态Go二进制文件。

test/run脚本执行S2I构建,因此您必须在$PATH中提供S2I二进制文件。如有需要,请按照S2I自述文件中的安装说明进行操作。

S2I将应用程序源代码和构建器镜像组合在一起,因此要测试它,您需要一个示例应用程序源代码来验证源代码是否成功转换为可运行的容器镜像。示例应用程序应该很简单,但它应该练习assemblerun脚本的关键步骤。

生成脚本和工具

S2I工具带有强大的生成工具,可以加快创建新的S2I镜像的过程。s2i create命令会生成所有必要的S2I脚本和测试工具以及Makefile

$ s2i create <image_name> <destination_directory>

生成的test/run脚本必须进行调整才能有用,但它提供了一个良好的起点。

s2i create命令生成的test/run脚本要求示例应用程序源代码位于test/test-app目录中。

本地测试

本地运行S2I镜像测试最简单的方法是使用生成的Makefile

如果您没有使用s2i create命令,您可以复制以下Makefile模板并将IMAGE_NAME参数替换为您的镜像名称。

Makefile示例
IMAGE_NAME = openshift/ruby-20-centos7
CONTAINER_ENGINE := $(shell command -v podman 2> /dev/null | echo docker)

build:
	${CONTAINER_ENGINE} build -t $(IMAGE_NAME) .

.PHONY: test
test:
	${CONTAINER_ENGINE} build -t $(IMAGE_NAME)-candidate .
	IMAGE_NAME=$(IMAGE_NAME)-candidate test/run

基本的测试工作流程

test脚本假设您已经构建了要测试的镜像。如有需要,请先构建S2I镜像。运行以下命令之一

  • 如果您使用Podman,请运行以下命令

    $ podman build -t <builder_image_name>
  • 如果您使用Docker,请运行以下命令

    $ docker build -t <builder_image_name>

以下步骤描述了测试S2I镜像构建器的默认工作流程

  1. 验证usage脚本是否正常工作

    • 如果您使用Podman,请运行以下命令

      $ podman run <builder_image_name> .
    • 如果您使用Docker,请运行以下命令

      $ docker run <builder_image_name> .
  2. 构建镜像

    $ s2i build file:///path-to-sample-app _<BUILDER_IMAGE_NAME>_ _<OUTPUT_APPLICATION_IMAGE_NAME>_
  3. 可选:如果您支持save-artifacts,请再次运行步骤2以验证保存和恢复构件是否正常工作。

  4. 运行容器

    • 如果您使用Podman,请运行以下命令

      $ podman run <output_application_image_name>
    • 如果您使用Docker,请运行以下命令

      $ docker run <output_application_image_name>
  5. 验证容器是否正在运行以及应用程序是否正在响应。

运行这些步骤通常足以判断构建器镜像是否按预期工作。

使用OpenShift Dedicated构建镜像

拥有Dockerfile以及构成新的S2I构建器镜像的其他构件后,您可以将它们放在git仓库中,并使用OpenShift Dedicated来构建和推送镜像。定义一个指向您的仓库的Docker构建。

如果您的OpenShift Dedicated实例托管在公共IP地址上,则每次向S2I构建器镜像GitHub仓库推送时都可以触发构建。

您还可以使用ImageChangeTrigger来触发基于您更新的S2I构建器镜像的应用程序的重建。