容器运维最佳实践
本文介绍了一组使容器更易于运维的最佳实践。这些实践涉及安全性、监控和日志记录等广泛的主题,旨在使应用程序更容易在 Kubernetes Engine和一般的容器中运行。这里讨论的许多实践都受到 12因子方法的启发 ,12因素方法是一个构建云原生应用程序的优质资源。
这些最佳实践的重要等级不一样。例如,对于有些实践,你可能在缺少他们的情况下在生产环境中成功运行,但另外一些实践是不可或缺的。特别是,与安全相关的最佳实践的重要性是主观的,是否实现它们取决于你的环境和约束。
这些实践并不适合零基础的读者,你需要事先了解Docker 和Kubernetes 的一些知识。此处讨论的一些最佳实践也适用于Windows容器,但大多情况下数假设你使用的是Linux容器。有关构建容器的建议,请参阅 构建容器的 最佳实践。
使用容器的原生日志记录机制
重要性:高
作为应用程序管理的一部分,日志中包含宝贵的信息,可让人了解应用程序中发生的事件。Docker 和Kubernetes 致力于简化日志管理。
在传统服务器上,你可能需要将日志写入特定文件并处理日志轮换以避免填满磁盘。如果有高级日志系统,则可以将这些日志转发到远程服务器来集中它们。
通过容器可以将日志写入stdout 和stderr,因而容器提供了一种简单且标准化的方式来处理日志。Docker 捕获这些日志行,并允许你使用docker logs 命令访问它们。作为应用程序开发人员,你不需要实现高级日志记录机制,试试用原生的日志记录机制吧。
平台运营商必须提供一个系统来集中日志并进行搜索,你可以使用Kubernetes Engine 提供的 fluentd和 Stackdriver Logging。其他常见方法包括使用 EFK(Elasticsearch,Fluentd,Kibana)栈。
图1.Kubernetes 中典型的日志管理系统图
JSON 日志
大多数日志管理系统实际上是时序数据库,用于存储时间索引文档。这些文档通常以JSON 格式提供。在Stackdriver Logging 和EFK 中,单个日志行和一些元数据(容器组、容器、节点等相关信息)一起被存储为一个文档。
你可以直接通过将不同字段以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)无法通过简单配置来生成日志。这些应用程序在磁盘上写入不同的日志文件,所以在Kubernetes 中处理它们的最佳方法是使用边车模式进行日志记录。边车是一个小容器,与应用程序在同一个pod中运行。有关边车的更详细信息,请参阅 Kubernetes 官方文档。
在这种解决方案中,你为应用程序的边车容器添加一个日志代理(在同一pod中,)并在两个容器之间共享 emptyDir卷,示例: GitHub 上的这个YAML 示例。然后,配置应用程序将日志写入共享卷,接着配置日志代理进行读取,并转发到需要的地方。
在此模式中,因为没有使用Docker 和Kubernetes 原生的日志记录机制,所以必须处理日志轮换。如果你的日志代理程序不处理日志轮换,则同一pod中的另一个边车容器会处理。
图2.日志管理的边车模式
确保容器是无状态且不可变的
重要性:高
如果你是第一次尝试容器,请不要将它们视为传统服务器。比如,你可能想要在正在运行的容器内更新应用程序,或者在出现漏洞时给正在运行的容器打补丁。从根本上 说,容器 不是以这种方式工作的。它们被设计成了 无状态且不可改变。
无状态
无状态意味着任何状态(任何类型的持久数据)都存储在容器之外。这个外部存储可以采取多种形式,具体取决于你的需求:
- 要存储文件,我们建议使用 Cloud Storage等对象存储。
- 要存储用户会话等信息,我们建议使用外部的低延迟键值存储,例如 Redis 或Memcached。
- 如果需要块级存储(例如数据库),则可以使用连接到容器的外部磁盘。对于Kubernetes Engine,我们建议使用 持久化 磁盘。
通过以上方法将数据从容器本身中移出,这意味着可以随时干净地关闭和销毁容器,而不必担心数据丢失。如果创建了一个新容器来替换旧容器,则只需将新容器连接到同一数据存储区或将其绑定到同一磁盘即可。
不变性
不可变意味着容器在其生命周期内不会被修改:没有更新,没有补丁,没有配置更改。如果必须更新应用程序代码或打补丁,则需要构建新镜像并重新部署。不变性使部署更安全,更可重复。如果需要回滚,只需重新部署旧镜像即可。此方法允许你在每个环境中部署相同的容器镜像,使它们尽可能一致。
为了在不同环境中使用相同的容器镜像,我们建议你外部化容器配置(侦听端口,运行时选项等)。容器通常配置有环境变量或挂载到特定路径上的配置文件。在Kubernetes中,你可以使用 保密字典和 配置集作为环境变量或文件将配置注入到容器中。
如果需要更新配置,请使用更新的配置部署新容器(基于相同的镜像)。
图3. 如何更新将配置集挂载为容器组中的配置文件的部署中的配置
无状态和不变性的结合是基于容器的基础设施的卖点之一。这种组合允许你自动化部署并提高其频率和可靠性。
避免使用特权容器
重要性:高
在虚拟机或裸机服务器中,你会避免使用root 用户运行应用程序,原因很简单:如果应用程序受到攻击,攻击者就可以完全访问服务器。出于同样的原因,请避免使用特权容器。特权容器是一个容器,可以访问主机的所有设备,绕过容器的几乎所有安全功能。
如果你认为需要使用特权容器,请考虑以下备选方案:
- 通过 Kubernetes 的 securityContext选项或Docker 的--cap-add 标志为容器提供特定功能 。该 Docker 文档 同时列出了默认启用和必须明确启用的功能。
- 如果你的应用程序必须修改主机设置才能运行,请在边车容器或 初始化容器中修改这些设置 。与你的应用程序不同,这些容器不需要暴露于内部或外部流量,更加独立。
- 如果需要在Kubernetes 中修改sysctls,请使用 专用注解。
在Kubernetes 中,特权容器可以被特定的Pod 安全策略禁止 。Pod安全策略是集群管理员配置和管理的Kubernetes 对象,它强制执行对pod的特定要求。在Kubernetes 集群中,你无法创建违反这些要求的pod。
使应用程序易于监控
重要性:高
与日志一样,监控是应用程序管理的一个组成部分。在许多方面,监控容器化应用的原则与非容器化应用的监控相同。但是,由于容器化的基础架构往往是高度动态的,伴随着频繁创建或删除的容器,你无法每次都去重新配置监控系统。
你可以区分两种主要的监控类型: 黑盒监控和 白盒监控。黑盒监控是指从外部检查应用程序,你就是最终用户。如果你想要最终提供的服务可用且有效,则黑盒监控非常有用。由于它位于基础设施外部,因此 黑盒监控在传统基础设施和容器化基础设施之间没有区别。
白盒监控是指使用某种特权访问检查应用程序,并收集最终用户无法查看的度量指标。由于白盒监控必须检查基础架构的最深层,因此传统基础架构和容器化基础架构的差异很大。
Prometheus是Kubernetes 社区中用于白盒监控的一个流行选择,可以自动发现必须监控的容器。Prometheus 以期望的特定格式获取容器组的指标。 Stackdriver能够监控Kubernetes集群,应用也可以运行自己的的Prometheus 。了解如何 在Kubernetes Engine上启用Stackdriver Kubernetes Monitoring。
以下是一个Stackdriver Kubernetes Monitoring的演示实例:
图4.Stackdriver Kubernetes Monitoring中的仪表板
要从Prometheus 或Stackdriver Kubernetes Monitoring 中受益,应用程序需要按照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 是标签,最右边的数字是该指标对于这些标签的值。上图中所示,自启动以来,该应用程序已使用400错误码响应了97次HTTP 的GET 请求。
通过已有的多种语言的 Prometheus客户端库,可以轻松生成此HTTP 端点 。 OpenCensus还可以使用此格式(以及许多其他功能)导出指标。不要将此端点暴露给公共网络。
Prometheus 官方 文档详细介绍了该主题。你还可以阅读 站点可靠性工程的 第6章,以了解有关白盒(和黑盒)监控的更多信息。
用于监控的边车模式
并非所有应用程序都可以使用/metrics HTTP 端点进行检测。为了保持标准化监控,我们建议使用边车模式以正确的格式导出指标。
日志聚合边车模式部分介绍如何使用边车容器来管理应用程序日志。你可以使用相同的模式进行监控:边车容器托管监控代理程序,该代理程序将应用程序公开的度量标准转换为全局监控系统可以理解的格式和协议。
考虑一个具体示例:Java 应用程序和Java Management Extensions(JMX)。许多Java 应用程序使用JMX 公开指标。利用 jmx_exporter,你可以不必重写应用程序就公开Prometheus 格式的指标。jmx_exporter 通过JMX 从应用程序收集指标,并通过Prometheus 可以读取的/metrics 端点公开它们。这种方法还具有限制JMX 端点暴露的优点,因为它可以用来修改应用程序设置。
图5.用于监控的边车模式
暴露应用程序的健康状况
重要性:中等
为了便于在生产中进行管理,应用程序必须将其状态传达给整个系统:应用程序是否正在运行?它健康吗?它准备好接收流量吗?它是如何表现的?
Kubernetes 有两种类型的健康检查:活性探针(liveness probes )和就绪探针(readiness probes)。如下文所述,每个都有特定的用途。你可以通过多种方式实现这两种方式(包括在容器内运行命令或检查TCP端口),但首选方法是使用此最佳实践中描述的HTTP 端点。有关此主题的更多信息,请参阅 Kubernetes文档。
注意:本节中给出的路径只是一种约定。HTTP 端点的实际路径可能因应用程序而异。
活性探针
实现 活性探针的推荐方法是让应用程序公开/health HTTP 端点。在此端点上收到请求后,如果认为健康,应用程序应发送“200 OK”响应。在Kubernetes 中,健康意味着容器不需要被杀死或重新启动。影响健康的因素因应用程序而异,但通常意味着以下内容:
- 应用程序正在运行。
- 它的主要依赖性得到满足(例如,它可以访问其数据库)。
就绪探针
实现 就绪探针的推荐方法是让应用程序公开/ready HTTP 端点。在此端点上收到请求后,如果应用程序已准备好接收流量,则应发送“200 OK”响应。准备接收流量意味着以下内容:
- 该应用程序是健康的。
- 完成任何潜在的初始化步骤。
- 发送到应用程序的任何有效请求都不会导致错误。
Kubernetes 使用就绪探针来编排应用程序的部署。如果更新 部署,Kubernetes 将对属于该部署的pod进行滚动更新。默认更新策略是一次更新一个pod:Kubernetes 在更新下一个pod之前等待新pod准备就绪(如就绪探针所示)。
注意:在许多应用程序中,/health 和/ready 端点合并为一个/health 端点,因为它们的健康状态和就绪状态之间没有真正的区别。
避免以root 身份运行
重要性:中等
容器提供隔离:使用默认设置,Docker 容器内的进程无法访问来自主机或其他并置容器的信息。但是,由于容器共享主机的内核,因此隔离不像虚拟机那样完整。攻击者可以找到未知的漏洞(在Docker 或Linux 内核本身中),这些漏洞将允许攻击者从容器中逃脱。如果攻击者确实发现了漏洞并且你的进程在容器内以root 身份运行,则他们将获得对主机的root访问权限。
图6.左侧,虚拟机使用虚拟化硬件。右侧,容器中的应用程序使用主机内核。
为避免这种可能性,最佳做法是不在容器内以root 身份运行进程。你可以使用 PodSecurityPolicy在Kubernetes中强制执行此行为 。在Kubernetes中创建pod时,使用 runAsUser 选项指定正在运行该进程的Linux 用户。这种方法会覆盖Dockerfile 中的USER指令。
实际上,存在挑战。许多软件包都以root 身份运行其主进程。如果要避免以root 用户身份运行,设计你的容器使用未知的非特权用户运行。这种做法通常意味着你必须调整各种文件夹的权限。在容器中,如果按照 一个容器一个应用的最佳实践,并且一个应用一个用户( 最好不是root 用户),则授予所有用户对文件夹和文件的读写权限不是问题 。
检查容器是否符合此最佳实践的一种简单方法是在本地使用随机用户运行容器并测试是否正常工作。替换[YOUR_CONTAINER]为你的容器名称。
docker run --user $((RANDOM + 1))[YOUR_CONTAINER]
如果容器需要外部卷,则可以配置 fsGroup Kubernetes选项以将此卷的所有权授予给特定的Linux 组。此配置解决了外部文件所有权的问题。
如果你的进程由非特权用户运行,则它将无法绑定到1024以下的端口。这不是什么大问题,因为你可以配置Kubernetes 服务将流量从一个端口路由到另一个端口。例如,你可以配置HTTP 服务器绑定到8080端口,并通过Kubernetes 服务从80端口将流量重定向回来。
仔细选择镜像版本
重要性:中等
当你使用Docker 镜像时,无论是作为Dockerfile 中的基础镜像,还是作为Kubernetes中部署的镜像,你都必须选择正在使用的镜像的标签。
大多数公共和私有镜像都遵循 构建容器最佳实践中所述的标签系统 。如果镜像使用 语义版本控制的系统 ,则必须考虑一些标签细节。
最重要的是,“latest”标签可以在镜像之间频繁移动。结果是你无法依赖此标签进行可预测或可重现的构建。例如,采用以下Dockerfile:
FROM debian:latest RUN apt-get -y update && \ apt-get -y install nginx
如果你在不同的时间使用这个Dockerfile 构建两次镜像,你最终会得到两个不同版本的Debian 和NGINX。相反,考虑这个修订版:
FROM debian:9.4 RUN apt-get -y update && \ apt-get -y install nginx
通过使用更精确的标签,你可以确保生成的镜像始终基于Debian 的特定子版本。因为特定的Debian 版本还附带了特定的NGINX 版本,所以你可以更好地控制正在构建的镜像。
这个结果不仅适用于构建时,也适用于运行时。如果你在Kubernetes 清单中引用“latest”标签,则无法保证Kubernetes 将使用的版本。集群的不同节点可能会在不同时刻拉取相同的“latest”标签。如果标签已经在拉动之间的某个点更新,则最终可能会在不同的节点运行不同的镜像(这是因为同时打上了“latest”标签)。
理想情况下,你应始终在FROM 行中使用不可变标签。此标签允许你重现构建。但是,存在一些安全性权衡:你固定使用的版本越多,安全补丁在镜像中的自动化程度就越低。如果你使用的镜像使用正确的语义版本控制,则补丁版本(即“X.Y.Z”中的“Z”)不应具有向后不兼容的更改:你可以使用“X.Y”标签并自动修复错误。
注意:标签在Docker 中不是真正不变的。只要镜像的所有者决定更改标签。但是,“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 进行替换,则会自动拉取新版本。
最后,你必须仔细检查正在使用的每个外部镜像的标签系统,判定你对构建这些镜像的人员的信任程度,并确定要使用的标签。
感谢张婵对本文的审校。