容器和开放容器倡议 (OCI) 图像是重要的开源应用程序打包和交付技术,因 Docker 和 Kubernetes 等项目而广受欢迎。您越了解它们,就越能使用它们来增强项目的一致性和可扩展性。
在本文中,我将用简单的术语描述这项技术,重点介绍开发人员需要理解的图像和容器的基本方面,然后总结讨论开发人员可以遵循的一些最佳实践,以使他们的容器可移植。我还将引导您完成一个简单的实验,演示构建和运行图像和容器。
什么是图像?
图像只不过是软件的打包格式。一个很好的类比是 Java 的 JAR 文件或 Python wheel。JAR(或 EAR 或 WAR)文件只是扩展名不同的 ZIP 文件,而 Python wheel 则作为 gzipped tarball 分发。它们都符合内部的标准目录结构。
图像打包为 tar.gz
(gzipped tarball),它们包含您正在构建和/或分发的软件,但这就是与 JAR 和 wheel 类比的结束。首先,图像不仅打包您的软件,还打包运行您的软件所需的所有支持依赖项,直至包括完整的操作系统。虽然 wheel 和 JAR 通常构建为依赖项,但也可以执行,但图像几乎总是构建为要执行的,而很少作为依赖项。
了解图像中内容的详细信息对于理解如何使用图像或为其编写和设计软件(如果您有兴趣,请阅读 “什么是容器图像?”)。从您的角度来看,特别是从您的软件的角度来看,重要的是要理解您创建的图像将包含一个完整的操作系统。因为图像的打包方式就像它们是从您希望运行的软件的角度来看的完整操作系统,所以它们必然比以更传统方式打包的软件大得多。
请注意,图像是不可变的。一旦构建完成,就无法更改。如果您修改图像上运行的软件,则必须构建一个全新的图像并替换旧的图像。
标签
创建图像时,会使用唯一的哈希值创建它们,但它们通常使用人类可读的名称来标识,例如 ubi
、ubi-minimal
、openjdk11
等。但是,每个名称的图像可能都有不同的版本,这些版本通常通过标签来区分。例如,openjdk11
图像可能标记为 jre-11.0.14.1_1-ubi
和 jre-11.0.14.1_1-ubi-minimal
,表示在 Red Hat ubi
和 ubi minimal
图像上安装的 openjdk11 软件包版本 11.0.14.1_1 的图像构建。
什么是容器?
容器是在主机系统上实现和执行的图像。从图像运行容器是一个两步过程:创建和启动。创建采用图像并为其提供自己的 ID 和文件系统。可以重复创建(例如 docker create
中的创建)多次,以便创建正在运行的图像的多个实例,每个实例都有自己的 ID 和文件系统。启动容器将在主机上启动一个隔离的进程,其中容器内运行的软件的行为就像它在自己的虚拟机中运行一样。因此,容器是主机上的一个隔离进程,具有自己的 ID 和独立的文件系统。
从软件开发人员的角度来看,使用容器的主要原因有两个:一致性和可扩展性。这些彼此相关,并且它们共同使项目可以使用近年来软件开发中最有希望的创新之一,即“一次构建,多次部署”原则。
一致性
由于图像是不可变的,并且包含从操作系统向上运行软件所需的所有依赖项,因此无论您选择在何处部署它,您都可以获得一致性。这意味着无论您是在开发、测试还是任何数量的生产环境中将图像作为容器启动,容器都将以完全相同的方式运行。作为软件开发人员,您不必担心任何这些环境是否在不同的主机操作系统或版本上运行,因为容器每次都运行相同的操作系统。这就是将您的软件及其完整的运行时环境打包的好处,而不是仅仅打包您的软件而不包含运行它所需的完整依赖项集。
这种一致性意味着,在几乎所有情况下,当在一个环境中(例如,生产环境)发现问题时,您可以确信您将能够在开发环境或其他环境中重现该问题,因此您可以确认该行为并专注于修复它。您的项目绝不应该再陷入并因可怕的“但在我的机器上可以工作”问题而受阻。
可扩展性
图像不仅包含您的软件,还包含运行您的软件所需的所有依赖项,包括底层操作系统。这意味着在容器内运行的所有进程都将容器视为主机系统,主机系统对于在容器内运行的进程是不可见的,并且从主机系统的角度来看,容器只是它管理的另一个进程。当然,虚拟机几乎也做同样的事情,这提出了一个有效的问题:为什么要使用容器技术而不是虚拟机?答案在于速度和大小。
容器仅运行支持独立主机所需的软件,而无需模拟硬件的开销。虚拟机必须包含完整的操作系统并模拟底层硬件。后者是一种非常重量级的解决方案,也会导致文件大得多。由于从主机系统的角度来看,容器被视为另一个正在运行的进程,因此它们可以在几秒钟而不是几分钟内启动。当您的应用程序需要快速扩展时,容器在资源和速度方面每次都会胜过虚拟机。容器也更容易缩减规模。
从功能角度来看,扩展超出了本文的范围,因此实验不会演示此功能,但为了理解容器技术代表软件打包和部署的如此重大进步的原因,理解该原则非常重要。
注意:虽然可以运行不包含完整操作系统的容器,但这很少这样做,因为可用的最小图像通常是不够的起点。
如何查找和存储图像
与每种其他类型的软件打包技术一样,容器也需要一个可以共享、查找和重用软件包的地方。这些称为图像注册表,类似于 Java Maven 和 Python wheel 存储库或 npm 注册表。
以下是在互联网上可用的不同图像注册表的示例
- Docker Hub: 最初的 Docker 注册表,托管着许多在世界各地的项目中广泛使用的 Docker 官方镜像,并为个人提供了托管自己的镜像的机会。在 Docker Hub 上托管镜像的组织之一是 adoptopenjdk;查看他们的存储库,了解 openjdk11 项目的镜像和标签示例。
- Red Hat 镜像注册表: Red Hat 的官方镜像注册表为拥有有效 Red Hat 订阅的用户提供镜像。
- Quay:Red Hat 的公共镜像注册表托管了许多 Red Hat 公开可用的镜像,并为个人提供了托管自己的镜像的机会。
使用图像和容器
有两个实用程序旨在管理图像和容器:Docker 和 Podman。它们适用于 Windows、Linux 和 Mac 工作站。从开发人员的角度来看,在执行命令时,它们是完全等效的。它们可以被视为彼此的别名。您甚至可以在许多系统上安装一个软件包,该软件包会自动将 Docker 更改为 Podman 别名。在本文档中提到 Podman 的任何地方,都可以安全地替换为 Docker,结果不会改变。
[ 阅读下一篇: 5 个未充分利用的 Podman 功能,现在就试试 ]
您会立即注意到这些实用程序与 Git 非常相似,因为它们执行标记、推送和拉取。您将定期使用或参考此功能。但是,它们不应与 Git 混淆,因为 Git 还管理版本控制,而图像是不可变的,并且它们的管理实用程序和注册表没有变更管理的概念。如果您将两个具有相同名称和标签的图像推送到同一个存储库,则第二个图像将覆盖第一个图像,而无法查看或了解发生了什么更改。
子命令
以下是您常用的或参考的 Podman 和 Docker 子命令示例
build
:构建图像- 示例:
podman build -t org/some-image-repo -f Dockerfile
- 示例:
image
:在本地管理图像- 示例:
podman image rm -a
将删除所有本地图像。
- 示例:
images
: 列出本地存储的图像tag
: 标记图像container
: 管理容器- 示例:
podman container rm -a
将删除所有停止的本地容器。
- 示例:
run
:create
和start
容器- 还有
stop
和restart
- 还有
pull
/push
:从注册表上的存储库拉取/推送图像
Dockerfiles
Dockerfiles 是定义图像的源文件,并使用 build
子命令进行处理。它们将定义父图像或基础图像,复制或安装您希望在图像中运行的任何额外软件,定义在构建和/或运行时使用的任何额外元数据,并可能指定在运行由您的图像定义的容器时要运行的命令。下面的实验中更详细地描述了 Dockerfile 的剖析以及其中使用的一些更常见的命令。本文末尾提供了指向完整 Dockerfile 参考的链接。
Docker 和 Podman 之间的根本区别
Docker 在类 Unix 系统中是一个守护进程,在 Windows 中是一项服务。这意味着它始终在后台运行,并且以 root 或管理员权限运行。Podman 是二进制文件。这意味着它仅按需运行,并且可以作为非特权用户运行。
这使得 Podman 更安全,系统资源效率更高(如果不需要,为什么要一直运行?)。以 root 权限运行任何东西,顾名思义,都不太安全。在云上使用图像时,托管容器的云可以更安全地管理图像和容器。
Skopeo 和 Buildah
虽然 Docker 是一个单一的实用程序,但 Podman 还有另外两个相关的实用程序,由 GitHub 上的 Containers 组织维护:Skopeo 和 Buildah。两者都提供了 Podman 和 Docker 没有的功能,并且两者都是容器工具软件包组的一部分,Podman 用于在 Red Hat Linux 发行版系列上安装。
在大多数情况下,可以通过 Docker 和 Podman 执行构建,但如果需要更复杂的图像构建,则存在 Buildah。这些更复杂构建的细节远远超出了本文的范围,您几乎永远不会遇到需要它的情况,但我在此处提及此实用程序是为了完整性。
Skopeo 提供了 Docker 没有的两个实用功能:将图像从一个注册表复制到另一个注册表以及从远程注册表中删除图像的功能。同样,此功能超出了本文的讨论范围,但该功能最终可能会对您有用,特别是如果您需要编写一些 DevOps 脚本。
Dockerfiles 实验
以下是一个非常简短的实验(约 10 分钟),它将教您如何使用 Dockerfiles 构建图像以及如何将这些图像作为容器运行。它还将演示如何外部化容器的配置,以充分实现容器开发和“一次构建,多次部署”的好处。
安装
以下实验是在本地运行 Fedora 和在 Red Hat 沙箱环境 中创建和测试的,其中已安装 Podman 和 Git。我相信在 Red Hat 沙箱环境中运行此实验将获得最大收益,但在本地运行它也是完全可以接受的。
您也可以在自己的工作站上安装 Docker 或 Podman 并在本地工作。提醒一下,如果您安装 Docker,对于此实验,podman
和 docker
是完全可互换的。
构建图像
1. 从 GitHub 克隆 Git 存储库
$ git clone https://github.com/hippyod/hello-world-container-lab
2. 打开 Dockerfile
$ cd hello-world-container-lab
$ vim Dockerfile
1 FROM Docker.io/adoptopenjdk/openjdk11:x86_64-ubi-minimal-jre-11.0.14.1_1
2
3 USER root
4
5 ARG ARG_MESSAGE_WELCOME='Hello, World'
6 ENV MESSAGE_WELCOME=${ARG_MESSAGE_WELCOME}
7
8 ARG JAR_FILE=target/*.jar
9 COPY ${JAR_FILE} app.jar
10
11 USER 1001
12
13 ENTRYPOINT ["java", "-jar", "/app.jar"]
此 Dockerfile 具有以下功能
- FROM 语句(第 1 行)定义了将从中构建此新图像的基础(或父)图像。
- USER 语句(第 3 行和第 11 行)定义了在构建期间和执行时运行的用户。最初,root 在构建过程中运行。在更复杂的 Dockerfile 中,我需要成为 root 才能安装任何额外的软件、更改文件权限等等,以完成新图像。在 Dockerfile 的末尾,我切换到 UID 为 1001 的用户,以便每当图像被实现为容器并执行时,用户将不是 root,因此更安全。我使用 UID 而不是用户名,以便主机可以识别哪个用户在容器中运行,以防主机具有增强的安全措施来阻止容器以 root 用户身份运行。
- ARG 语句(第 5 行和第 8 行)定义了只能在构建过程中使用的变量。
- ENV 语句(第 6 行)定义了一个环境变量和值,该变量和值可以在构建过程中使用,但在图像作为容器运行时也将可用。请注意它是如何通过引用先前 ARG 语句定义的变量来获取其值的。
- COPY 语句(第 9 行)将 Spring Boot Maven 构建创建的 JAR 文件复制到图像中。为了方便在没有安装 Java 或 Maven 的 Red Hat 沙箱中运行的用户,我已经预先构建了 JAR 文件并将其推送到 hello-world-container-lab 存储库。在此实验中无需进行 Maven 构建。(注意:还有一个
add
命令可以替代 COPY。由于add
命令可能具有不可预测的行为,因此 COPY 更可取。)
- 最后,ENTRYPOINT 语句定义了在容器启动时应在容器中执行的命令和参数。如果此图像将来成为后续图像定义的基础图像并且定义了新的 ENTRYPOINT,它将覆盖此图像。(注意:还有一个
cmd
命令可以替代 ENTRYPOINT。两者之间的区别在此上下文中无关紧要,并且超出了本文的范围。)
键入 :q
并按 Enter 退出 Dockerfile 并返回到 shell。
3. 构建图像
$ podman build --squash -t test/hello-world -f Dockerfile
您应该看到
STEP 1: FROM docker.io/adoptopenjdk/openjdk11:x86_64-ubi-minimal-jre-11.0.14.1_1
Getting image source signatures
Copying blob d46336f50433 done
Copying blob be961ec68663 done
...
STEP 7/8: USER 1001
STEP 8/8: ENTRYPOINT ["java", "-jar", "/app.jar"]
COMMIT test/hello-world
...
Successfully tagged localhost/test/hello-world:latest
5482c3b153c44ea8502552c6bd7ca285a69070d037156b6627f53293d6b05fd7
除了构建图像外,这些命令还提供以下说明
--squash
标志将通过确保在图像构建完成时仅将一个图层添加到基础图像来减小图像大小。多余的图层会膨胀生成的图像的大小。FROM、RUN 和 COPY/ADD 语句添加图层,最佳做法是在可能的情况下连接这些语句,例如
RUN dnf -y --refresh update && \
dnf install -y --nodocs podman skopeo buildah && \
dnf clean all
上面的 RUN 语句不仅会运行每个语句以仅创建一个图层,而且如果其中任何一个失败,也会使构建失败。
-t flag
用于命名图像。因为我没有为名称显式定义标签(例如 test/hello-world:1.0)
),所以图像将默认标记为 latest。我也未定义注册表(例如 quay.io/test/hello-world
),因此默认注册表将是 localhost。
-f
标志用于显式声明要构建的 Dockerfile。
运行构建时,Podman 将跟踪“blob”的下载。这些是您的图像将基于其构建的图像层。它们最初是从远程注册表中拉取的,并且将在本地缓存以加快将来的构建速度。
Copying blob d46336f50433 done
Copying blob be961ec68663 done
...
Copying blob 744c86b54390 skipped: already exists
Copying blob 1323ffbff4dd skipped: already exists
4. 构建完成后,列出图像以确认已成功构建
$ podman images
您应该看到
REPOSITORY TAG IMAGE ID CREATED SIZE
localhost/test/hello-world latest 140c09fc9d1d 7 seconds ago 454 MB
docker.io/adoptopenjdk/openjdk11 x86_64-ubi-minimal-jre-11.0.14.1_1 5b0423ba7bec 22 hours ago 445 MB
运行容器
5. 运行图像
$ podman run test/hello-world
您应该看到
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.5.4)
...
GREETING: Hello, world
GREETING: Hello, world
输出将继续每三秒打印“Hello, world”,直到您退出
crtl-c
6. 证明 Java 仅安装在容器中
$ java -version
在容器内运行的 Spring Boot 应用程序需要 Java 才能运行,这就是我选择基础图像的原因。如果您在实验室的 Red Hat 沙箱环境中运行,则证明 Java 仅安装在容器中,而不是在主机上
-bash: java: command not found...
外部化您的配置
图像现在已构建,但是当我想让每个部署图像的环境的“Hello, world”消息不同时会发生什么?例如,我可能想要更改它,因为环境用于不同的开发阶段或不同的区域设置。如果我更改 Dockerfile 中的值,我需要构建一个新图像才能看到消息,这打破了容器最基本的好处之一——“一次构建,多次部署”。那么如何使我的图像真正可移植,以便它可以部署到我需要的任何地方?答案在于外部化配置。
7. 使用新的外部欢迎消息运行图像
$ podman run -e 'MESSAGE_WELCOME=Hello, world DIT' test/hello-world
您应该看到
Output:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.5.4)
...
GREETING: Hello, world DIT
GREETING: Hello, world DIT
使用 crtl-c
停止并调整消息
$ podman run -e 'MESSAGE_WELCOME=Hola Mundo' test/hello-world
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.5.4)
...
GREETING: Hola Mundo
GREETING: Hola Mundo
-e
标志定义了一个环境变量和值,以便在启动时注入到容器中。如您所见,即使变量已构建到原始图像中(Dockerfile 中的 ENV MESSAGE_WELCOME=${ARG_MESSAGE_WELCOME}
语句),它也会被覆盖。您现在已经外部化了需要根据部署位置(例如,在 DIT 环境中或对于讲西班牙语的人)进行更改的数据,从而使您的图像可移植。
8. 使用在文件中定义的新消息运行图像
$ echo 'Hello, world from a file' > greetings.txt
$ podman run -v "$(pwd):/mnt/data:Z" \
-e 'MESSAGE_FILE=/mnt/data/greetings.txt' test/hello-world
在这种情况下,您应该看到
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.5.4)
...
GREETING: Hello, world from a file
GREETING: Hello, world from a file
重复直到您按下 crtl-c
停止
在这种情况下,-e
标志定义了 /mnt/data/greetings.txt
文件中文件的路径,该文件是使用 -v
标志从主机的本地文件系统挂载的,位于 $(pwd)/greetings.txt
(pwd
是一个 bash 实用程序,用于输出当前目录的绝对路径,在您的情况下应该是 hello-world-container-lab
)。您现在已经外部化了需要根据部署位置进行更改的数据,但这次您的数据是在您挂载到容器中的外部文件中定义的。环境变量设置对于有限数量的设置来说是可以的,但是当您有多个设置要应用时,文件是将值注入到容器中的更有效方法。
注意:上面卷定义末尾的 :Z
标志用于使用 SELinux 的系统。SELinux 管理许多 Linux 发行版上的安全性,并且该标志允许容器访问该目录。如果没有该标志,SELinux 将阻止读取该文件,并且会在容器中抛出异常。再次尝试运行上面的命令,删除 :Z
以查看演示。
实验到此结束。
为容器开发:外部化配置
“一次构建,多次部署”之所以有效,是因为在不同环境中运行的不可变容器不必担心支持您的特定软件项目所需的硬件或软件差异。此原则使软件开发、调试、部署和持续维护变得更快、更容易。它也不是完美的,并且必须对您的编码方式进行一些小的更改,以使您的容器真正可移植。
在为容器化编写软件时,最重要的设计原则是决定要外部化什么。这些决定最终使您的图像可移植,从而可以充分实现“一次构建,多次部署”的范例。尽管这看起来可能很复杂,但在决定配置数据是否应该可注入到您的运行容器中时,有一些易于记住的因素需要考虑
- 数据是否特定于环境?这包括任何需要根据容器运行位置配置的数据,无论环境是生产环境、非生产环境还是开发环境。此类数据包括国际化配置、数据存储信息以及您希望应用程序在其下运行的特定测试配置文件。
- 数据是否独立于发布? 此类数据可以从功能标志到国际化文件再到日志级别不等 - 基本上,您可能想要或需要在版本之间更改的任何数据,而无需构建和新部署。
- 数据是否是秘密? 凭据绝不应硬编码或存储在图像中。凭据通常需要在与发布计划不匹配的计划上刷新,并且将机密嵌入存储在图像注册表中的图像中存在安全风险。
最佳实践是选择应在何处外部化配置数据(即,在环境变量或文件中),并且仅外部化符合上述标准的那些部分。如果它不符合上述标准,则最好将其保留为不可变图像的一部分。遵循这些准则将使您的图像真正可移植,并使您的外部配置保持合理的大小和可管理性。
[ 免费在线课程: 容器、Kubernetes 和 Red Hat OpenShift 技术概述 ]
总结
本文介绍了针对图像和容器新手软件开发人员的四个关键概念
- 图像是不可变的二进制文件:图像是一种用于打包软件以供以后重用或部署的方法。
- 容器是隔离的进程:创建容器时,容器是图像的运行时实例化。启动容器后,它们将成为主机上的内存进程,这比虚拟机更轻更快。在大多数情况下,开发人员只需要知道后者,但了解前者很有帮助。
- “一次构建,多次部署”:此原则使容器技术非常有用。图像和容器在部署中提供一致性以及与主机的独立性,使您可以在许多不同的环境中自信地部署。由于此原则,容器也易于扩展。
- 外部化配置:如果您的图像具有特定于环境、独立于发布或秘密的配置数据,请考虑使该数据外部于图像和容器。您可以通过注入环境变量或将外部文件挂载到容器中,将此数据注入到正在运行的图像中。
3 条评论