Kubernetes部署反模式 – 第一部分
在之前的指南中,我们记录了10 种 Docker 反模式。该指南非常受欢迎,因为它能帮助您入门容器镜像。然而,为应用程序创建容器镜像仅仅是成功的一半。您仍然需要一种方法将这些容器部署到生产环境中,而事实上的标准解决方案是使用 Kubernetes 集群。
我们很快意识到,我们也必须为 Kubernetes 部署创建类似的指南。希望这份指南能让您全面了解如何创建容器镜像以及如何正确部署它(或者至少提醒您注意一些常见的陷阱)。
请注意,本指南专门讨论在 Kubernetes 上部署应用程序,而不是 Kubernetes 集群本身。这意味着我们假设 Kubernetes 集群已经存在(并且配置正确),您只需要在其上部署应用程序即可。未来,我们将完成这三部曲,并记录创建集群过程中的反模式(即讨论基础设施层面而非应用程序层面)。
与其他只抱怨可能出错之处的指南不同,我们始终将每种反模式与相应的解决方案联系起来。这样,您就可以直接检查自己的部署流程并修复任何问题,而无需费力寻找额外信息。
以下是我们今天将要探讨的一些不良做法:
- 在 Kubernetes 部署中使用带有最新标签的容器
- 将配置烘焙到容器镜像中
- 无故将应用程序与 Kubernetes 特性/服务耦合
- 将应用程序部署与基础设施部署混合使用(例如,使用 Terraform 部署带有 Helm 提供程序的应用程序)
- 手动使用 kubectl edit/patch 执行临时部署
- 使用 Kubectl 作为调试工具
- 对 Kubernetes 网络概念的误解
- 使用永久性暂存环境而不是动态环境
- 生产集群与非生产集群混合
- 部署时不受内存和 CPU 限制
- 滥用健康探针
- 不使用 Helm(也不了解 Helm 的功能)
- 缺乏部署指标来了解应用程序的运行情况
- 没有秘密策略/随意处理秘密
- 尝试全面采用 Kubernetes(甚至包括数据库和有状态负载)
顺便一提,如果你还没有看过容器反模式指南,现在就应该去看一看,因为上面提到的一些不良做法会参考它。
反模式 1 – 在 Kubernetes 部署中使用带有最新标签的容器
如果你有过构建容器的经验,这应该不会让你感到意外。使用“latest”标签来标记 Docker 镜像本身就是一种不好的做法,因为“latest”只是标签的名称,它实际上并不代表“最新”或“最后构建”。此外,如果你在讨论容器镜像时没有指定标签,“latest”也是默认标签。
在 Kubernetes 部署中使用“latest”标签更糟糕,因为这样做您将不再知道集群中部署了什么。
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-bad-deployment
spec:
template:
metadata:
labels:
app: my-badly-deployed-app
spec:
containers:
- name: dont-do-this
image: docker.io/myusername/my-app:latest
如果您应用此部署,您将丢失所有关于实际部署的容器标签的信息。容器标签是可变的,因此“最新”标签对任何人来说都没有实际意义。这个容器镜像可能是 3 分钟前创建的,也可能是 3 个月前创建的。您需要查找 CI 系统的所有日志,甚至需要将镜像下载到本地进行检查,才能确定它包含的版本。
如果将“latest”标签与始终拉取策略结合使用,则危险性更大。假设您的 Pod 已停止运行,Kubernetes 决定重启它以使其恢复健康(记住,这正是您最初使用 Kubernetes 的原因)。
Kubernetes 会重新调度 Pod,如果你的拉取策略允许,它会再次从 Docker 镜像仓库拉取“最新”镜像!这意味着,如果在此期间“最新”标签发生了变化,那么这个 Pod 中的镜像版本就会与其他 Pod 中的版本不同。在大多数情况下,这并非你所期望的。
不言而喻,通过手动终止 pod 并等待它们再次拉取“最新”镜像来执行“部署”是成功的秘诀(如果您确实使用了这种形式的“部署”)。
Kubernetes 中正确的部署格式应遵循合理的标签策略。具体的策略并不重要,只要有标签策略即可。
以下是一些建议:
- 使用带有 Git 哈希值的标签(例如 docker.io/myusername/my-app:acef3e)。这种方法实现起来很简单,但可能有点过度设计,因为 Git 哈希值对于非技术人员来说不易理解。
- 使用带有语义版本号的应用程序版本标签(例如 docker.io/myusername/my-app:v1.0.1)。这种方法对开发人员和非开发人员都有很多优势,是我们个人推荐的做法。
- 使用表示连续编号的标签,例如构建编号或构建日期/时间。这种格式操作起来非常复杂,但易于与旧版应用程序兼容。
重要的是,你们应该同意容器标签应被视为不可变的。标记为 v2.0.5 的 Docker 镜像应该只创建一次,并且应该从一个环境迁移到另一个环境。
如果您看到使用带有标签 v2.0.5 的镜像的部署,您应该能够……
- 将此镜像拉取到本地,并确保它与集群上运行的镜像完全相同;
- 轻松追踪创建它的 Git 哈希值。
如果你的部署工作流程以任何方式依赖于使用“最新”标签,那么你就是在坐视不理,埋下一颗定时炸弹。
反模式 2 – 将配置烘焙到容器镜像中
这实际上是构建容器镜像时产生的另一种反模式。你的镜像应该是“通用的”,也就是说它们应该能够在任何环境下运行。
即使在容器出现之前,这也是一种良好的实践,并且已被纳入十二要素应用设计原则中。容器镜像应该只构建一次,然后从一个环境迁移到另一个环境。容器本身不应该包含任何配置。
如果您的容器镜像:
- 具有硬编码的 IP 地址
- 包含密码和秘密
- 提及指向其他服务的特定 URL
- 带有“开发”、“测试”、“生产”等字符串标签
那么你就落入了构建依赖于环境的容器镜像的陷阱。
这意味着对于每个不同的环境,您都必须重新构建镜像,以便部署到生产环境的内容与之前测试的内容不同。
解决这个问题的方法很简单。创建“通用”容器镜像,这些镜像无需了解运行环境的任何信息。配置方面,可以使用任何外部方法,例如 Kubernetes ConfigMap、HashiCorp Consul、Apache ZooKeeper 等。
现在,您只需使用一个镜像即可部署到所有集群。这样更容易理解镜像的内容以及它的创建方式。
另一个优势是,如果您确实需要更改集群配置,只需修改外部配置系统,而无需从头开始重建整个容器镜像。根据您使用的编程语言和框架,您甚至可以在不重启或重新部署的情况下更新实时配置。
反模式 3 – 无缘无故地将应用程序与 Kubernetes 特性/服务耦合
在上一节中,我们解释了为什么不应该将配置存储在容器内,以及容器不应该了解它运行所在的集群的任何信息。
我们可以将此推向极致,要求每个容器甚至完全不知道自身运行在 Kubernetes 内部。除非您开发的应用程序旨在管理集群,否则您的应用程序不应干预 Kubernetes API 或假定位于集群内部的其他外部服务。
这种情况在过度热衷于采用 Kubernetes 的团队中非常常见,因为他们未能将应用程序与集群隔离。一些典型的例子包括:
- 预期与其他 pod 进行数据共享时会采用特定的卷配置。
- 预期服务/DNS 会按照 Kubernetes 网络设置的特定名称进行命名,或者假设存在特定的开放端口。
- 从 Kubernetes 标签和注解中获取信息
- 查询它们自己的 pod 以获取信息(例如,查看它们的 IP 地址)
- 即使在本地工作站上,也需要 init 或 sidecar 容器才能正常运行。
- 直接调用其他 Kubernetes 服务(例如,使用Vault API从假定也存在于集群上的Vault 安装中获取密钥)
- 从本地 kube 配置中读取数据
- 直接从应用程序内部使用 Kubernetes API
当然,如果你的应用程序是 Kubernetes 特有的(例如,你正在创建一个自动扩缩器或 Operator),那么它确实需要直接访问 Kubernetes 服务。但对于其他 99% 的标准 Web 应用程序而言,你的应用程序应该完全不需要感知自身运行在 Kubernetes 环境中。
判断你的应用程序是否与 Kubernetes 紧密相关,关键在于它能否使用 Docker Compose 运行。如果为你的应用程序创建 Docker Compose 文件非常简单,那就意味着你遵循了十二要素应用开发原则,你的应用程序无需特殊设置即可安装在任何集群上。
了解本地 Kubernetes 测试的前提也很重要。目前有多种本地 Kubernetes 部署解决方案(例如 minikube、microk8s 和kind等)。您可能会看到这些解决方案,并认为如果您是一名开发人员,正在开发一个部署到 Kubernetes 的应用程序,那么您也需要自己运行 Kubernetes。
这完全是错误的。如果你的应用程序设计得当,就不需要 Kubernetes 来在本地运行集成测试。只需单独启动应用程序(使用 Docker 或 Docker Compose),然后直接运行测试即可。
如果某些依赖项运行在外部 Kubernetes 集群上,这没问题。但是,在测试应用程序的功能时,应用程序本身不应该运行在 Kubernetes 集群内部。
或者,您也可以使用任何专门用于本地 Kubernetes 开发的解决方案,例如Okteto、garden.io和tilt.dev。
反模式 4 – 将应用程序部署与基础设施部署混为一谈
近年来,Terraform (以及Pulumi等类似工具)的兴起催生了“基础设施即代码”运动,使团队能够以与编写代码相同的方式部署基础设施。
但是,即使你可以按流程部署基础设施,也不意味着基础设施和应用程序的部署应该同时进行。
我们看到很多团队创建了一个单一的流水线,既创建基础设施(例如创建 Kubernetes 集群、容器注册表等),又在其上部署应用程序。
虽然这在理论上效果很好(因为这意味着每次部署都是从零开始),但在资源和时间方面却非常浪费。
大多数情况下,应用程序代码的变更速度远快于基础设施的变更速度。虽然很难一概而论,但大多数情况下,应用程序的变更频率可能是基础设施变更频率的 2 到 10 倍。
如果你的单个管道同时执行这两项操作,那么你仅仅因为想要部署一个新版本的应用程序,就破坏/创建了从未改变过的基础设施。
部署所有内容(基础设施/应用程序)的流水线可能需要 30 分钟,而仅部署应用程序的流水线可能只需要 5 分钟。如果基础设施没有发生变化,那么每次部署都白白浪费 25 分钟。
第二个缺点是,如果单个流水线出现故障,很难确定由谁来负责处理。如果我是一名开发人员,想要将我的应用程序部署到 Kubernetes 上,我并不关心 Terraform 错误、虚拟网络或存储卷。
DevOps 的核心在于赋予开发人员自助服务工具。强迫他们处理不必要的底层基础设施,是一种倒退。
当然,正确的解决方案是将部署和基础设施分别放在各自的流水线中。基础设施流水线的触发频率会低于应用程序流水线,从而加快应用程序部署速度(并缩短交付周期)。
开发人员也知道,当应用程序流水线出现故障时,他们无需处理基础设施错误,也无需关心 Kubernetes 集群是如何创建的。运维人员可以对基础设施流水线进行微调,而不会对开发人员造成任何影响。每个人都可以独立工作。
我们有时会看到这种反模式(将基础设施与应用程序混合),一些公司认为这是唯一的前进方向,因为应用程序需要基础设施管道提供某些东西。
一个经典的例子是使用 Terraform 创建部署,然后将部署的输出(例如 IP 地址)作为应用程序代码的输入传递给流水线的其余部分。如果存在这种限制,则意味着您正受到之前提到的反模式(将应用程序与基础设施细节耦合)的影响,需要消除这种耦合(即,您的应用程序代码不应该需要特定的 IP 地址才能部署)。
请注意,同样的方法也适用于数据库升级。如果您使用管道来处理数据库变更集,那么它们应该独立于应用程序源代码。您应该能够单独更新数据库模式或单独更新应用程序代码,而无需在每次部署时都同时更新两者。
反模式 5 – 手动使用 kubectl edit/patch 执行临时部署
配置漂移是一个众所周知的问题,甚至在 Kubernetes 出现之前就已存在。当两个或多个环境原本应该相同,但经过某些临时部署或更改后,它们的配置不再一致时,就会发生这种情况。
随着时间的推移,这个问题变得更加严重,甚至可能导致机器配置完全未知,必须从运行中的实例进行逆向工程才能确定配置的极端情况。
Kubernetes 也可能遇到这个问题。kubectl 命令功能非常强大,它内置了apply/edit/patch 命令,可以对运行中的集群资源进行原地修改。
不幸的是,这种方法很容易被不负责任的开发者和运维人员滥用。当集群中发生临时更改时,这些更改不会记录在任何其他地方。
部署失败最常见的原因之一是环境配置问题。生产环境部署失败(即使在测试环境中运行正常),是因为两个环境的配置不再一致。
很容易就会落入这个陷阱。“紧急修复”、“快速变通方案”和其他一些可疑的权宜之计,往往是临时性改动背后的主要原因。
绝不应该手动使用 Kubectl 进行部署。所有部署都应该由部署平台处理,理想情况下,还应该按照GitOps 范式记录在 Git 中。
如果你的所有部署都是通过 Git 提交完成的:
- 您可以通过 Git 提交历史记录完整地了解集群中发生的一切。
- 您可以确切地知道每个集群在任何时间点包含的内容,以及不同环境之间的差异。
- 通过读取 Git 配置,您可以轻松地从头开始重新创建或克隆环境。
- 回滚配置非常简单,只需将集群指向之前的提交即可。
最重要的是,如果部署失败,您可以非常快速地找出影响部署的最后一个更改是什么,以及它是如何更改其配置的。
kubectl 的补丁/编辑功能仅应用于实验用途。手动修改生产集群上的实时资源无异于自找麻烦。除了制定完善的部署流程外,您还应该始终避免以这种方式滥用 kubectl。
(未完待续,请见第二部分)
封面照片来自Unsplash。
文章来源:https://dev.to/codefreshio/kubernetes-deployment-antipatterns-part-1-2116










