创建镜像

了解如何基于可直接使用的预构建镜像创建你自己的容器镜像。这个过程包括学习编写镜像的最佳实践、定义镜像元数据、测试镜像,以及使用自定义构建器工作流创建可用于 Alauda Container Platform Registry 的镜像。创建镜像后,你可以将其推送到 Alauda Container Platform Registry

了解容器最佳实践

在 Alauda Container Platform 上创建用于运行的容器镜像时,作为镜像作者,有许多最佳实践需要考虑,以确保镜像使用者获得良好的体验。由于镜像旨在保持不可变并按原样使用,以下指南有助于确保你的镜像具有很高的可用性,并且易于在 Alauda Container Platform 上使用。

通用容器镜像指南

以下指南适用于创建容器镜像的一般场景,与这些镜像是否在 Alauda Container Platform 上使用无关。

复用镜像

在可能的情况下,使用 FROM 语句将你的镜像基于合适的上游镜像。这可确保当上游镜像更新时,你的镜像能够轻松获取安全修复,而无需你直接更新依赖项。

另外,请在 FROM 指令中使用标签,例如 alpine:3.20,以便用户清楚地知道你的镜像基于哪个具体版本。使用除 latest 之外的标签可确保你的镜像不会受到上游镜像最新版本中可能引入的破坏性更改影响。

在标签内保持兼容性

为你自己的镜像打标签时,请尽量在同一标签内保持向后兼容。例如,如果你提供一个名为 image 的镜像,并且它当前包含版本 1.0,你可以提供一个 image:v1 标签。当你更新镜像时,只要它仍然与原始镜像兼容,就可以继续将新镜像标记为 image:v1,这样使用该标签的下游使用者就可以在不受破坏的情况下获得更新。

如果之后你发布了一个不兼容的更新,那么请切换到新标签,例如 image:v2。这样下游使用者可以按需升级到新版本,而不会被新的不兼容镜像意外破坏。任何使用 image:latest 的下游使用者都要承担引入不兼容更改的风险。

避免多个进程

不要在一个容器中启动多个服务,例如数据库和 SSHD。这样做并非必要,因为容器是轻量级的,可以轻松地关联在一起,以编排多个进程。Alauda Container Platform 允许你通过将相关镜像分组到单个 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 顶部非常重要。这样可以确保后续对同一镜像的构建速度非常快,因为缓存不会因上层更改而失效。

例如,如果你正在处理一个 Dockerfile,其中包含一个用于安装你正在迭代的文件的 ADD 命令,以及一个用于 yum install 软件包的 RUN 命令,那么最好将 ADD 命令放在最后:

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

这样,每次你编辑 myfile 并重新运行 podman build 时,系统都会复用 yum 命令对应的缓存层,并且只会为 ADD 操作生成新层。

如果你反过来将 Dockerfile 编写为:

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

那么每次你更改 myfile 并重新运行 podman build 时,ADD 操作都会使 RUN 层缓存失效,因此 yum 操作也必须重新执行。

标记重要端口

EXPOSE 指令会使容器中的端口对主机系统和其他容器可用。虽然也可以通过 podman run 调用来指定某个端口应被暴露,但在 Dockerfile 中使用 EXPOSE 指令,通过显式声明软件运行所需的端口,可以让人和软件都更容易使用你的镜像:

  • 暴露的端口会显示在由你的镜像创建的容器关联的 podman ps 输出中。
  • 暴露的端口会出现在 podman inspect 返回的镜像元数据中。
  • 当你将一个容器链接到另一个容器时,暴露的端口也会被关联。

设置环境变量

使用 ENV 指令设置环境变量是一种良好实践。其中一个例子是设置项目版本。这样人们无需查看 Dockerfile 就能轻松找到版本。另一个例子是在系统中声明一个可供其他进程使用的路径,例如 JAVA_HOME

避免默认密码

避免设置默认密码。很多人会扩展镜像,却忘记移除或修改默认密码。如果生产环境中的用户被分配了一个众所周知的密码,这可能会导致安全问题。应改为通过环境变量配置密码。

如果你确实选择设置默认密码,请确保容器启动时显示适当的警告消息。该消息应告知用户默认密码的值,并说明如何修改它,例如需要设置哪个环境变量。

避免 sshd

最好不要在镜像中运行 sshd。你可以使用 podman exec 访问在本地主机上运行的容器。在集群中,使用 kubectl exec 访问由 Alauda Container Platform 管理的容器。在镜像中安装并运行 sshd 会扩大攻击面,并增加补丁维护负担。

将卷用于持久数据

镜像使用卷来存储持久数据。这样,Alauda Container Platform 会将网络存储挂载到运行容器的节点上;如果容器迁移到新节点,存储会重新挂载到该节点。通过将卷用于所有持久化存储需求,即使容器重启或迁移,内容也会被保留。如果你的镜像将数据写入容器内的任意位置,这些内容将无法被保留。

所有即使在容器销毁后仍需保留的数据,都必须写入卷。容器引擎支持容器的 readonly 标志,可用于严格强制不向容器中的临时存储写入数据的良好实践。现在就围绕该能力设计镜像,将使你将来更容易利用它。

Dockerfile 中显式定义卷,可以让镜像使用者更容易理解在运行你的镜像时需要定义哪些卷。

有关卷在 Alauda Container Platform 中如何使用的更多信息,请参见 Kubernetes documentation

注意:

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

在镜像中包含元数据

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

当前的元数据集合覆盖了现有用例所需的字段。未来可能会增加更多元数据或用例。

定义镜像元数据

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

有关 LABEL 指令的更多信息,请参见 Dockerfile reference

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