了解如何基于预构建镜像创建您自己的容器镜像,这些预构建镜像已准备好为您提供帮助。该过程包括学习编写镜像的最佳实践、定义镜像元数据、测试镜像,以及使用自定义构建流程创建可用于 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,容器运行时发送的信号会发送到包装脚本而非软件进程,这不是您想要的。
例如,您有一个包装脚本启动某个服务器进程。您使用 docker run -i 启动容器,运行包装脚本,包装脚本再启动服务器进程。如果您想用 CTRL+C 关闭容器,且包装脚本使用了 exec 启动服务器进程,docker 会向服务器进程发送 SIGINT,行为符合预期。如果未使用 exec,docker 会向包装脚本进程发送 SIGINT,服务器进程则继续运行。
另外,您的进程在容器中作为 PID 1 运行。这意味着如果主进程终止,整个容器会停止,您从 PID 1 进程启动的任何子进程也会被取消。
清理临时文件
删除构建过程中创建的所有临时文件,包括使用 ADD 命令添加的文件。例如,在执行 yum install 操作后运行 yum clean 命令。
您可以通过如下方式防止 yum 缓存进入镜像层:
注意,如果写成:
则第一次 yum 调用会在该层留下额外文件,这些文件在后续运行 yum clean 时无法被删除。虽然这些文件在最终镜像中不可见,但它们存在于底层镜像层中。
当前容器构建流程不允许后续层运行的命令缩减前一层删除文件所占用的空间,但未来可能会改变。这意味着如果您在后续层执行 rm 命令,虽然文件被隐藏,但不会减少镜像下载的总体大小。因此,像 yum clean 示例一样,最好在创建文件的同一命令中删除它们,避免写入镜像层。
此外,在单个 RUN 语句中执行多个命令可以减少镜像层数,提升下载和解压速度。
按正确顺序排列指令
容器构建器从上到下读取 Dockerfile 并执行指令。每条成功执行的指令都会创建一个可被下次构建复用的层。将不常更改的指令放在 Dockerfile 顶部非常重要,这样后续构建时缓存不会被上层更改失效,构建速度更快。
例如,如果您在 Dockerfile 中有一个 ADD 命令安装您正在迭代的文件,以及一个 RUN 命令执行 yum install,最好将 ADD 命令放在最后:
这样每次编辑 myfile 并重新构建时,系统会重用 yum 命令的缓存层,只为 ADD 操作生成新层。
如果写成:
每次更改 myfile 并重新构建时,ADD 操作会使 RUN 层缓存失效,导致 yum 操作也必须重新执行。
标记重要端口
EXPOSE 指令使容器中的端口对主机系统和其他容器可用。虽然可以在 docker run 命令中指定端口暴露,但在 Dockerfile 中使用 EXPOSE 指令通过显式声明软件运行所需端口,使人和软件更容易使用您的镜像:
docker ps 中显示,关联到基于您的镜像创建的容器。docker inspect 返回的镜像元数据中。设置环境变量
使用 ENV 指令设置环境变量是良好实践。例如设置项目版本号,方便用户无需查看 Dockerfile 即可了解版本。另一个例子是声明系统路径,如 JAVA_HOME,供其他进程使用。
避免默认密码
避免设置默认密码。许多人在扩展镜像时忘记移除或更改默认密码,这可能导致生产环境用户使用众所周知的密码,带来安全隐患。密码应通过环境变量进行配置。
如果确实设置了默认密码,确保容器启动时显示适当的警告信息,告知用户默认密码的值及如何更改,例如设置哪个环境变量。
避免 sshd
最好避免在镜像中运行 sshd。您可以使用 docker exec 访问本地主机上的容器,在集群中使用 kubectl exec 访问由 Alauda Container Platform 管理的容器。安装和运行 sshd 会增加攻击面并增加补丁负担。
使用卷存储持久数据
镜像应使用卷来存储持久数据。这样,Alauda Container Platform 会将网络存储挂载到运行容器的节点上,如果容器迁移到新节点,存储会重新挂载到该节点。通过使用卷存储所有持久数据,即使容器重启或迁移,内容也能保留。如果镜像将数据写入容器内任意位置,则无法保证数据持久性。
所有需要在容器销毁后仍保留的数据必须写入卷。容器引擎支持容器的 readonly 标志,可严格执行不向容器临时存储写数据的良好实践。现在围绕该能力设计镜像,未来更易利用。
在 Dockerfile 中显式定义卷,方便镜像使用者了解运行镜像时必须定义的卷。
有关卷在 Alauda Container Platform 中使用的更多信息,请参见 Kubernetes 文档。
注意:
即使使用持久卷,您的镜像的每个实例都有自己的卷,实例间文件系统不共享。这意味着卷不能用于集群中共享状态。
定义镜像元数据有助于 Alauda Container Platform 更好地使用您的容器镜像,为开发者提供更佳体验。例如,您可以添加元数据提供镜像的有用描述,或建议可能还需要的其他镜像。
本主题仅定义当前用例所需的元数据,未来可能添加更多元数据或用例。
您可以在 Dockerfile 中使用 LABEL 指令定义镜像元数据。标签类似于环境变量,是附加到镜像或容器的键值对。标签不同于环境变量的是,它们对运行中的应用不可见,且可用于快速查找镜像和容器。
有关 LABEL 指令的更多信息,请参见 Docker 文档。
标签名称通常带有命名空间。命名空间根据将使用标签的项目设置。对于 Kubernetes,命名空间为 io.k8s。
有关格式的详细信息,请参见 Docker 自定义元数据文档。