容器运维最佳实践
本文介绍使容器易于操作的一系列最佳实践。 这些做法涉及从安全性到监控和日志记录的各种主题,其目标在于使应用更容易在 Kubernetes 和一般容器中运行。
本文介绍的许多做法都受到 十二要素方法 的启发,此方法是构建云原生应用的一种优秀资源。
这些最佳实践的重要性并不相同。例如,成功运行某一生产工作负载可能无需其中某些做法,但必须使用其他做法。 特别是,与安全相关的最佳实践的重要性较为主观。是否实现它们取决于您的环境和所受限制。
若要充分理解本文中的内容,您需要了解一些关于 Docker 和 Kubernetes 的知识。
此处介绍的一些最佳实践也适用于 Windows 容器,但大多数做法假定您使用的是 Linux 容器。 如需获取构建容器的建议,请参阅 构建容器的最佳实践。
使用容器的原生日志记录机制
重要性:高
作为应用管理的主要组成部分,日志包含有关应用中发生的事件的宝贵信息。Docker 和 Kubernetes 的目的便是简化日志管理。
在传统服务器上,您可能需要将日志写入特定文件并处理日志轮换以避免填满磁盘。如果您有高级日志记录系统,则可以将这些日志转发到远程服务器以集中管理。
容器提供了一种简单且标准化的方式来处理日志,这是因为您可以将它们写入
stdout
和
stderr
。Docker 会捕获这些日志行,并让您使用docker logs
命令访问它们。
作为应用开发者,您不需要实现高级日志记录机制,使用原生日志记录机制即可。
平台运营商必须提供一个系统来集中管理日志并使其可搜索。在 Kubernetes 发行版中,常用方法包括使用 EFK(Elasticsearch、Fluentd、Kibana)堆栈。
图 1. Kubernetes 中的典型日志管理系统图
JSON
日志
大多数日志管理系统实际上是用于存储时间索引文档的时间序列数据库。这些文档通常以 JSON 格式提供。 在 EFK 中,单个日志行以及一些元数据(有关 pod、容器、节点等的信息)存储为一个文档。
您可以通过不同字段直接以 JSON 格式进行日志记录来充分利用此行为模式的优势。然后,您可以根据这些字段更有效地搜索日志。
例如,考虑将以下日志转换为 JSON 格式:
[2018-01-01 01:01:01] foo - WARNING - foo.bar - There is something wrong.
转换后的日志如下:
{
"date": "2018-01-01 01:01:01",
"component": "foo",
"subcomponent": "foo.bar",
"level": "WARNING",
"message": "There is something wrong."
}
通过此转换,您可以在日志中轻松搜索所有 WARNING
级别的日志或子组件 foo.bar
的所有日志。
如果您决定编写 JSON 格式的日志,请注意每个事件必须在单行上写入才能被正确解析。实际上,它看起来类似如下内容:
{"date":"2018-01-01 01:01:01","component":"foo","subcomponent":"foo.bar","level": "WARNING","message": "There is something wrong."}
如您所见,结果的可读性远不如普通日志行。如果您决定使用此方法,请确保您的团队不会严重依赖手动检查日志。
日志聚合器辅助信息文件模式
某些应用(如 Tomcat)无法轻松配置为将日志写入
stdout
和
stderr
。因为这些应用写入磁盘上的不同日志文件,所以在 Kubernetes 中处理它们的最佳方法是使用辅助信息文件模式进行日志记录。辅助信息文件是一个小容器,与您的应用在同一个 pod
中运行。如需详细了解 sidecar
,请参阅 Kubernetes 官方文档。
在此解决方案中,您需将辅助信息文件容器中的日志记录代理添加到您的应用(在同一 pod 中),并在两个容器之间共享一个 emptyDir 卷,如 GitHub 上的此 YAML 示例所示。 然后,配置应用以将其日志写入共享卷,并配置日志记录代理以在需要时读取和转发日志。
在此模式中,由于您没有使用原生 Docker 和 Kubernetes 日志记录机制,因此必须处理日志轮换。
如果您的日志记录代理不处理日志轮换,则同一 pod
中的另一个辅助信息文件容器可以处理轮换。
图 2. 日志管理的 sidecar 模式
确保容器无状态且不可变
重要性:高
如果您是第一次尝试使用容器,请不要将其视为传统服务器。例如,您可能想要在正在运行的容器内更新应用,或者在出现漏洞时修补正在运行的容器。
从根本上说,容器不是以这种方式运行的。它们被设计为无状态且不可变。
无状态
无状态意味着任何状态(任何类型的持久性数据)均存储在容器之外。这种外部存储可以采取多种形式,具体取决于您的需求:
- 如需存储文件,我们建议使用
S3
OSS
等对象存储。 - 如需存储用户会话等信息,我们建议使用外部低延时键值存储,例如 Redis 或 Memcached。
- 如果需要块级存储(例如对于数据库),则可以使用挂接到容器的外部磁盘。对于云厂商,我们建议使用永久性磁盘。
通过使用这些选项,您可以从容器本身移除数据,这意味着可以随时彻底关闭和销毁容器,而不必担心数据丢失。 如果创建了一个新容器来替换旧容器,只需将新容器连接到同一数据存储区或绑定到同一磁盘即可。
不变性
不可变意味着容器在其生命周期内不会被修改,即没有更新、没有补丁程序,也没有配置更改。如果必须更新应用代码或应用补丁程序,则需要构建新镜像并重新部署。 不变性使部署更安全、更可重复。如果需要回滚,只需重新部署旧镜像即可。此方法允许您在每个环境中部署同一容器镜像,使它们尽可能相同。
如需在不同环境中使用同一容器镜像,我们建议您外部化容器配置(侦听端口、运行时选项等)。容器通常配置有装载在特定路径上的环境变量或配置文件。在 Kubernetes 中,您可以使用 Secrets 和 ConfigMaps 将容器中的配置作为环境变量或文件注入。
如果需要更新配置,请使用更新后的配置部署一个新容器(基于同一镜像)。
图 3. 展示如何使用装载为 pod 中的配置文件的 ConfigMap 更新部署配置的示例。
无状态和不变性的组合是基于容器的基础架构的卖点之一。这种组合允许您自动化部署并提高其频率和可靠性。
避免使用特权容器
重要性:高
在虚拟机或裸机服务器中,您避免以
root
用户身份运行应用。原因很简单,如果应用遭到破解,攻击者就可以拥有对服务器的完整访问权限。出于同样的原因,请避免使用特权容器。特权容器是一种可以访问主机的所有设备的容器,它几乎可以绕过容器的所有安全功能。
如果您认为需要使用特权容器,请考虑以下备选方案:
- 通过 Kubernetes 的 securityContext 选项或 Docker 的
--cap-add
标志为容器提供特定功能。Docker 文档列出了默认启用的功能和必须明确启用的功能。 - 如果您的应用必须修改主机设置才能运行,请在辅助信息文件容器或 init 容器中修改这些设置。 与您的应用不同,这些容器不需要向内部或外部流量公开,因此隔离性更高。
- 如果需要在 Kubernetes 中修改 sysctl,请使用专用注释。
您可以使用 政策控制器 禁止 Kubernetes 中的特权容器。在 Kubernetes 集群中,您无法创建违反使用政策控制器配置的政策的 pod。
使应用易于监控
重要性:高
与日志记录一样,监控是应用管理不可或缺的一部分。在许多方面,监控容器化应用与监控非容器化应用遵循相同的原则。 但是,由于容器化基础架构往往具有高度动态性,其所含的容器会频繁创建或删除,您无法在每次发生这种情况时都重新配置监控系统。
您可以区分两种主要的监控类型,包括“黑盒监控”和“白盒监控”。黑盒监控是指从外部检查您的应用,就像您是最终用户一样。如果您要提供的最终服务可用且有效,则黑盒监控非常有用。由于黑盒监控从基础架构外部进行监控,因此其在传统基础架构和容器化基础架构之间没有区别。
白盒监控是指使用某种特别访问权限检查您的应用,并收集最终用户无法查看的行为指标。由于白盒监控必须检查基础架构的最深层,因此对于传统基础架构和容器化基础架构而言,其差异很大。
在 Kubernetes 社区中,Prometheus 是用于白盒监控的流行选项,该系统可以自动发现它必须监控的 pod。 Prometheus 会根据预期的特定格式,从 pod 中查找指标。 厂商一般提供了代管式 Prometheus 服务,您可以使用该服务来监控应用。借助 Prometheus,您可以在全局范围内监控工作负载并发出提醒,而无需大规模手动管理和操作 Prometheus。
如要充分利用 Prometheus ,您的应用需要以 Prometheus 格式公开指标。以下两种方法展示了如何执行此操作。
指标 HTTP
端点
指标 HTTP 端点的运作方式与后文公开应用的运行状况中提到的端点类似。它通常在 /metrics URI 上公开应用的内部指标。 响应如下:
http_requests_total{method="post",code="200"} 1027
http_requests_total{method="post",code="400"} 3
http_requests_total{method="get",code="200"} 10892
http_requests_total{method="get",code="400"} 97
在此示例中,http_requests_total
是指标,method
和code
是标签,最右侧的数字是这些标签的指标值。在本例中,自启动以来,应用对 HTTP GET
请求已返回
400
错误代码 97 次。
通过 Prometheus 客户端库(提供多种语言)可以轻松生成此 HTTP 端点。 OpenCensus 还可以使用此格式(以及许多其他功能)导出指标。但请注意,切勿将此端点公开给公共互联网。
Prometheus 官方文档详细介绍了该主题。如需详细了解白盒(和黑盒)监控,请阅读《站点可靠性工程》的第 6 章。
用于监控的 sidecar
模式
并非所有应用都可以使用 /metrics
HTTP 端点进行检测。为了保持标准化监控,我们建议使用辅助信息文件模式以正确格式导出指标。
日志聚合器辅助信息文件模式部分介绍了如何使用辅助信息文件容器来管理应用日志。您可以使用相同的模式进行监控,即辅助信息文件容器托管监控代理,该代理将应用公开的指标转换为全局监控系统可以理解的格式和协议。
以 Java 应用和 Java Management Extensions (JMX) 作为具体的例子:许多 Java 应用使用 JMX 公开指标。您可以利用 jmx_exporter,而无需重写应用以 Prometheus 格式公开指标。jmx_exporter 通过 JMX 从应用收集指标,并通过 Prometheus 可读取的 /metrics
端点公开这些指标。这种方法还具有限制 JMX 端点公开的优点,该端点可能被用来修改应用设置。
图 4. 用于监控的 sidecar 模式
公开应用的运行状况
重要性:中
为了便于在生产环境中进行管理,应用必须将其状态传达给整个系统,包括应用是否正在运行?运行状况是否良好?是否已准备好接收流量?应用表现如何?
Kubernetes 有两种类型的健康检查,包括活动性探测和就绪性探测。 每种类型都有特定用途,本部分会详细介绍。您可以通过多种方式(包括在容器内运行命令或检查 TCP 端口)实现这两种类型的探测,但首选方法是使用此最佳实践中介绍的 HTTP 端点。如需详细了解此主题,请参阅 Kubernetes 文档。
HTTP
端点的实际路径可能因应用而异。
活跃性探测
实现活动性探测的推荐方法是让应用公开
/healthz
HTTP 端点。在此端点上收到请求后,如果认为运行状况良好,应用应发送 “200 OK” 响应。在 Kubernetes 中,运行状况良好意味着容器不需要终止或重启。运行状况良好的条件因应用而异,但通常意味着以下情况:
- 该应用正在运行。
- 其主要依赖性得到满足(例如,它可以访问其数据库)。
就绪性探测
实现就绪性探测的推荐方法是让应用公开
/ready
HTTP 端点。当应用在此端点上收到请求时,如果其已准备好接收流量,则应发送 “200 OK” 响应。准备好接收流量意味着以下情况:
- 该应用运行状况良好。
- 任何潜在的初始化步骤均已完成。
- 发送到应用的任何有效请求都不会导致错误。
Kubernetes 使用就绪性探测来编排应用的部署。如果更新部署,Kubernetes 将对属于该部署的 pod 进行滚动更新。 默认更新政策是一次更新一个 pod,即 Kubernetes 会在更新下一个 pod 之前等待新 pod 准备就绪(由就绪性探测指明)。
/healthz
和 /ready
端点合并为一个 /healthz
端点,因为实际上它们的运行状况和就绪状态之间并没有差别。
避免以 root
身份运行
重要性:中
容器提供隔离,具体表现为使用默认设置,Docker 容器内的进程无法访问来自主机或其他并置容器的信息。但是,由于容器共享主机的内核,因此隔离不像虚拟机那样完整,如本博文所述。攻击者可能会找到未知的漏洞(在 Docker 或 Linux 内核中),这些漏洞将允许攻击者从容器中逃脱。如果攻击者确实发现了漏洞并且您的进程在容器内以
root
身份运行,则攻击者将获得对主机的
root
访问权限。
图 5. 左图中的虚拟机使用虚拟化硬件。右图中容器内的应用使用主机内核。
为避免这种可能性,最佳实践是不以
root
身份在容器内运行进程。您可以使用政策控制器在 Kubernetes 中强制执行此行为。在 Kubernetes 中创建 pod 时,使用 runAsUser 选项指定正在运行该进程的 Linux 用户。此方法会覆盖 Dockerfile 的 USER
指令。
实际上,这种方法存在某些困难,因为许多常见的软件包都以
root
身份运行其主要进程。如果要避免以
root
身份运行,请将容器设计为可以使用未知的非特权用户运行该容器。 这种做法通常意味着您必须调整各种文件夹的权限。 如果您遵循每个容器一个应用的最佳实践,并且您以单个用户身份(最好不是 root 用户)运行单个应用,则可以在容器中为所有用户授予对需要写入数据的文件夹和文件的写入权限,并将其他所有文件夹和文件设为仅可由
root
用户写入。
检查容器是否符合此最佳实践的一种简单方法是以某个随机用户身份在本地运行该容器,并测试其是否正常运行。请将 [YOUR_CONTAINER]
替换为您的容器名称。
docker run --user $((RANDOM+1)) [YOUR_CONTAINER]
如果容器需要外部卷,则可以配置 fsGroup Kubernetes选项 以将此卷的所有权授予特定的 Linux 组。此配置解决了外部文件所有权的问题。
如果您的进程由非特权用户运行,则其将无法绑定到 1024 以下的端口。此问题影响不大,因为您可以配置 Kubernetes 服务以将流量从一个端口路由到另一个端口。例如,您可以将 HTTP
服务器配置为绑定到端口 8080,并使用 Kubernetes 服务对端口 80 的流量进行重定向。
谨慎选择镜像版本
重要性:中
当您使用 Docker 镜像时,无论是作为 Dockerfile 中的基本镜像还是 Kubernetes 中部署的镜像,您都必须选择正在使用的镜像的标记。
大多数公共镜像和私有镜像都采用类似于构建容器的最佳实践中所述的标记系统。 如果镜像使用类似语义版本控制的系统,则必须考虑一些标记细节。
最重要的是,"最新"
标记可能会频繁地从一个镜像移至另一个镜像。 导致您无法依赖此标记进行可预测或可重现的构建。例如,以下面的 Dockerfile 为例:
FROM debian:latest RUN apt-get -y update && \ apt-get -y install nginx
如果您在不同的时间基于这个 Dockerfile 构建镜像两次,您将会得到两个不同版本的 Debian 和 NGINX。更好的做法是考虑使用如下的修订版本:
FROM debian:11.6
RUN apt-get -y update && \
apt-get -y install nginx
通过使用更精确的标记,您可以确保生成的镜像始终基于 Debian 的特定次要版本。由于特定 Debian 版本还附带了特定 NGINX 版本,因此您可以更好地控制正在构建的镜像。
这个结果不仅适用于构建时,也适用于运行时。如果您在 Kubernetes 清单中引用"最新"
标记,则无法保证 Kubernetes 将使用的版本。集群的不同节点可能会在不同时刻拉取相同的"最新"
标记。如果在拉取期间,该标记在某个时间点进行更新,则最终可能导致不同的节点运行不同的镜像(这些镜像在某个时间点均标记为"最新"
)。
理想情况下,您应始终在FROM
行中使用不可变标记。此标记可让您具有可重现的构建,但在安全性方面需要做出权衡,即对要使用的版本执行pin
操作的次数越多,安全补丁程序在镜像中的自动化程度就越低。如果您所用的镜像使用正确的语义版本控制,则补丁程序版本(即 "X.Y.Z"
中的 "Z"
)应该不会具有向后兼容的更改,即您可以使用 "X.Y"
标记并自动修复错误。
"X.Y.Z"
标记实际上几乎总是不可变的。
比如有一款名为 SuperSoft
的软件,假设 SuperSoft 的安全进程通过新的补丁程序版本来修复漏洞,那么您要自定义 SuperSoft,就要编写以下 Dockerfile:
FROM supersoft:1.2.3
RUN a-command
一段时间后,供应商发现了一个漏洞,并发布了 SuperSoft 的
1.2.4
版本来解决此问题。在这种情况下,您需要随时关注 SuperSoft 的补丁程序并相应地更新您的 Dockerfile。 如果您不这样做,而是在 Dockerfile 中使用FROM supersoft:1.2
,则软件会自动拉取新版本。
最后,您必须仔细审视正在使用的每个外部镜像的标记系统,确定您对构建这些镜像的人员的信任程度,并确定要使用的标记。