Kubernetes-企业级指南第三版-全-
Kubernetes 企业级指南第三版(全)
原文:
annas-archive.org/md5/be4cfce1594e9ca80fd4440d730d03e6译者:飞龙
序言
Kubernetes 已经风靡全球,成为 DevOps 团队开发、测试和运行应用程序的标准基础设施。大多数企业要么已经在使用它,要么计划在明年内使用它。从任何一个主要招聘网站上的职位发布可以看出,几乎每个大公司都有 Kubernetes 相关的职位空缺。Kubernetes 的快速普及导致与 Kubernetes 相关的职位在过去四年中增长了超过 2000%。
公司普遍面临的一个常见问题是缺乏企业级的 Kubernetes 知识。虽然 Kubernetes 已经不再是新技术(它刚刚迎来 10 周年!),但企业在构建能够可靠运行集群的团队方面仍然存在困难。它们还面临如何跨多个孤岛和技术栈整合 Kubernetes 工作负载的问题,这些问题在企业环境中很常见。虽然找到具备基础 Kubernetes 技能的人变得更加容易,但要找到具备企业集群所需的专业知识的人仍然是一个挑战。
本书适合谁阅读
我们编写本书是为了帮助 DevOps 团队将技能扩展到 Kubernetes 基础之外。本书的内容源自我们在多个企业环境中与集群合作的多年经验。
市面上有许多书籍介绍 Kubernetes 及其集群安装、创建部署(Deployments)和使用 Kubernetes 对象的基础知识。我们的计划是编写一本超越基本集群的书,为了保持书籍的合理长度,我们没有重新阐述 Kubernetes 的基础知识。读者在阅读本书之前应该有一定的 Kubernetes 和 DevOps 经验。
本书的主要焦点是扩展集群的企业功能,但书的第一部分将复习一些关键的 Docker 主题和 Kubernetes 对象。理解 Kubernetes 对象是非常重要的,这样你才能在后续更高级的章节中充分获得收获。
本书内容概述
第一章,Docker 和容器基础,介绍了 Docker 和 Kubernetes 为开发人员解决的问题。本章将带你了解 Docker,包括 Docker 守护进程、数据、安装以及如何使用 Docker 命令行工具(CLI)。
第二章,使用 KinD 部署 Kubernetes,介绍了如何使用 KinD 创建开发集群。KinD 是一个强大的工具,允许你创建从单节点集群到完整的多节点集群的各种集群。本章不仅介绍了基本的KinD集群,还解释了如何使用运行HAProxy的负载均衡器对工作节点进行负载均衡。通过本章的学习,你将理解 KinD 的工作原理以及如何创建一个自定义的多节点集群,后续章节中的练习将使用这个集群。
第三章,Kubernetes 入门训练营,提供了关于 Kubernetes 的复习内容。本章将覆盖集群中包含的大多数对象,这对于初学者来说非常有帮助。它将通过描述每个对象的功能和在集群中的作用来解释每个对象。这是一个复习篇,或者说是对象的“口袋指南”。它不包含每个对象的详尽细节(那需要另一本书)。
第四章,服务、负载均衡和网络策略,解释了如何使用服务公开 Kubernetes 部署。每种服务类型都将通过示例进行解释,并且您将学习如何使用第七层和第四层负载均衡器公开它们。在本章中,您将超越简单的 Ingress 控制器的基础知识,安装 MetalLB,为服务提供第四层访问。最后,您将学习如何通过使用 Kubernetes 网络策略在 pod 之间提供细粒度控制,从而增强集群内的安全性和合规性。
第五章,外部 DNS 和全局负载均衡,将使您了解两个增强企业集群的附加组件,通过安装一个名为external-dns的孵化器项目,为 MetalLB 所暴露的服务提供动态名称解析。您还将学习如何向您的集群添加全局负载均衡器,使用名为 K8GB 的项目,它提供原生 Kubernetes 全局负载均衡。
第六章,将身份验证集成到您的集群,回答了一个问题:“一旦构建了您的集群,用户将如何访问它?”在本章中,我们将详细介绍 OpenID Connect 的工作原理,以及为什么应该使用它来访问您的集群。您还将学习如何对您的流水线进行身份验证,最后,我们还将涵盖几种应该避免的反模式,并解释为什么应该避免它们。
第七章,RBAC 策略和审计,解释了一旦用户访问集群,您需要知道如何限制他们的访问。无论您是向用户提供整个集群还是只是一个命名空间,您都需要知道 Kubernetes 如何通过其基于角色的访问控制(RBAC)系统授权访问。在本章中,我们将详细介绍如何设计 RBAC 策略,如何调试它们,以及多租户的不同策略。
第八章,管理机密,集中讨论了 Kubernetes 世界中最难实现的问题之一:如何管理机密数据。首先,我们将看看在 Kubernetes 中管理机密的挑战。然后,我们将了解 HashiCorp 的 Vault 用于机密管理。最后,我们将使用 Vault sidecar 和流行的 External Secrets Operator 将我们的集群与 Vault 集成。
第九章,使用 vClusters 构建多租户集群,从单一集群开始,逐步实现将集群拆分为多个租户,采用 Loft 的 vCluster 项目。你将学习 vClusters 的工作原理,它们如何与主机集群交互,如何安全地访问它们,以及如何为你的租户自动化部署。我们还将基于 第八章 的学习,将管理的 Secrets 集成到我们的 vClusters 中!
第十章,部署安全的 Kubernetes Dashboard,讲解了 Kubernetes Dashboard,它通常是用户在集群启动并运行后尝试启动的第一个工具。关于安全性(或缺乏安全性)有许多误解。本章还讨论了集群中可能存在的其他 Web 应用程序,例如网络仪表盘、日志系统和监控仪表盘。本章重点讲解了如何设计仪表盘架构,如何正确地保障其安全,并通过实际案例展示了不当部署的示例,并解释了其中的原因。
第十一章,使用 Open Policy Agent 扩展安全性,为你提供了部署 Open Policy Agent 和 GateKeeper 的指导,帮助你实现一些 RBAC 无法完成的策略。我们将介绍如何部署 Gatekeeper,如何用 Rego 编写策略,并且如何使用 OPA 自带的测试框架来测试这些策略。
第十二章,通过 Gatekeeper 实现节点安全性,探讨了运行你 Pod 的节点安全性。我们将讨论如何安全地设计容器,使它们更难以被滥用,并且如何使用 GateKeeper 编写策略,防止你的容器访问它们不需要的资源。
第十三章,KubeArmor 安全保护运行时环境,介绍了安全性,它是每个人的责任,为应对攻击向量提供工具是运行一个安全且具有弹性的集群的关键。在本章中,你将学习如何使用一个名为 KubeArmor 的 CNCF 项目来保护你的容器运行时。KubeArmor 提供了一种简便的方法,通过易于理解的策略来锁定容器。
第十四章,工作负载备份,介绍了如何使用 Velero 创建集群工作负载的备份,以便灾难恢复或集群迁移。你将亲手操作,使用 MinIO 创建一个兼容 S3 的存储位置,备份示例工作负载和持久存储。然后,你会将备份恢复到一个全新的集群,以模拟集群迁移。
第十五章,监控集群和工作负载,探讨了如何使用Prometheus和OpenSearch来了解集群的健康状况。你将首先了解 Kubernetes 和 Prometheus 如何处理指标,然后我们将部署 Prometheus 堆栈以及Alertmanager和Grafana。你将学习如何确保堆栈的安全,并了解如何扩展堆栈以监控其他工作负载。在完成监控部分后,我们将转向使用 OpenSearch 进行日志聚合。我们将从探索 Kubernetes 中的日志工作原理开始,接着集成 OpenSearch,并最后使用OpenUnison来保护对 OpenSearch 的访问。
第十六章,Istio 简介,解释了许多企业使用服务网格为集群提供高级功能,如安全性、流量路由、身份验证、追踪和可观察性。本章将介绍 Istio——一个流行的开源网格及其架构,并介绍它所提供的最常用资源。你将通过一个示例应用程序将 Istio 部署到 KinD 集群,并学习如何使用一个名为 Kiali 的可观察性工具来观察应用程序的行为。
第十七章,在 Istio 上构建和部署应用程序,意识到一旦你部署了 Istio,你就会希望开发和部署使用它的应用程序!本章首先介绍单体应用和微服务的区别及其部署方式。接着,我们将一步步构建一个运行在 Istio 中的微服务,并深入讨论一些高级话题,如身份验证、授权以及服务到服务的身份验证。你还将学习如何利用 Kubernetes 中现有的角色,通过 OIDC 提供程序和 JSON Web 令牌来保护 Kiali 的访问。你还将学习如何使用 JWT 保护 Istio 服务,以及如何使用令牌交换来安全地访问不同的服务,从一个服务安全地移动到另一个服务。最后,我们将使用 OPA 创建一个自定义授权规则,并在 Istio 中进行配置。
第十八章,构建多租户平台,探讨了如何构建管道,如何自动化它们的创建,以及它们与 GitOps 的关系。我们将探讨用于驱动管道的对象之间的关系,如何建立系统之间的关系,并最终设计一个自服务工作流来自动化管道的部署。
第十九章,构建开发者门户,基于第十八章的设计,构建一个多租户平台,并使用本书中提到的许多工具。我们将从构建一个实验室开始,以运行我们的多租户集群。接下来,我们将部署 Kubernetes 到三个集群,并将它们与 GitLab、Vault、Argo CD、Harbor 和 OpenUnison 集成。最后,我们将介绍如何使用 OpenUnison 的自服务门户将新的 vCluster 租户进行入驻。
要充分利用本书
-
你应该具备 Linux 基础知识,了解基本命令、Git 等工具,并能够使用如 Vi 的文本编辑器。
-
本书的章节包含了理论与实践练习。我们认为,练习有助于巩固理论知识,但理解每个主题并不一定需要做这些练习。如果你想做书中的练习,你需要满足下表中的要求:
| 章节练习要求 | 版本 |
|---|---|
| Ubuntu 服务器 | 22.04 或更高版本 |
所有练习使用 Ubuntu,但大多数也适用于其他 Linux 发行版。
下载示例代码文件
本书的代码包托管在 GitHub 上:github.com/PacktPublishing/Kubernetes-An-Enterprise-Guide-Third-Edition。我们还提供了其他来自我们丰富的图书和视频目录的代码包,网址:github.com/PacktPublishing/。快去看看吧!
下载彩色图像
我们还提供了一个 PDF 文件,包含了本书中使用的截图/图表的彩色图像。你可以在这里下载:packt.link/gbp/9781835086957。
补充内容
这是一个链接,指向 YouTube 频道(由作者 Marc Boorshtein 和 Scott Surovich 创建和管理),其中包含本书实验室的视频,因此在你开始动手之前,你可以先看到它们的演示:packt.link/N5qjd。
使用的约定
本书中使用了多种文本约定。
CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。例如:“--name选项将集群名称设置为cluster01,而--config指示安装程序使用cluster01-kind.yaml配置文件。”
代码块如下所示:
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: grafana
name: grafana
namespace: monitoring
当我们希望引起你对代码块中特定部分的注意时,相关行或项目会以粗体显示:
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
**app:****grafana**
name: grafana
namespace: monitoring
任何命令行输入或输出都会如下所示:
PS C:\Users\mlb> kubectl create ns not-going-to-work
namespace/not-going-to-work created
粗体:表示新术语、重要词汇或屏幕上看到的词汇,例如在菜单或对话框中,也像这样在文本中出现。例如:“点击屏幕底部的完成登录按钮。”
警告或重要说明像这样显示。
提示和技巧像这样显示。
联系我们
我们总是欢迎读者的反馈。
一般反馈:发送电子邮件至 feedback@packtpub.com,并在邮件主题中注明书名。如果你对本书的任何部分有疑问,请通过 questions@packtpub.com 与我们联系。
勘误表:尽管我们已经尽力确保内容的准确性,但错误难免会发生。如果您在本书中发现任何错误,我们将非常感激您能向我们报告。请访问 www.packtpub.com/submit-errata,选择您的书籍,点击“勘误提交表单”链接,并输入详细信息。
盗版:如果您在互联网上发现任何非法复制的我们的作品,无论是什么形式,我们将非常感激您能提供该位置地址或网站名称。请通过 copyright@packtpub.com 联系我们,并附上相关资料链接。
如果您有兴趣成为作者:如果您在某个领域拥有专长并有意写书或为书籍作贡献,请访问 authors.packtpub.com。
分享您的想法
一旦您阅读完 Kubernetes - 企业指南(第三版),我们很想听听您的想法!请 点击此处直接进入亚马逊评价页面,分享您的反馈。
您的评价对我们以及技术社区都非常重要,将帮助我们确保提供优质的内容。
下载本书的免费 PDF 版本
感谢您购买本书!
您是否喜欢随时随地阅读,但又不能随身携带纸质书籍?
您购买的电子书是否与您选择的设备不兼容?
不用担心,现在购买每本 Packt 书籍时,您将免费获得该书的 DRM-free PDF 版本。
在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。
优惠不止于此,您还可以每天在邮箱中收到独家折扣、新闻通讯和精彩的免费内容。
按照这些简单的步骤即可享受福利:
- 扫描二维码或访问以下链接:

packt.link/free-ebook/9781835086957
-
提交您的购买证明。
-
就是这样!我们将直接把您的免费 PDF 和其他福利发送到您的邮箱。
第一章:Docker 和容器基础
容器已经成为一项极受欢迎且具有重大影响的技术,给传统应用带来了显著变化。从科技公司到大型企业再到终端用户,每个人都广泛采用容器来处理日常任务。值得注意的是,传统的安装现成商业应用的方法正逐渐转变为完全容器化的配置。考虑到这一技术变革的巨大规模,信息技术领域的从业者必须掌握并理解容器的概念。
本章将概述容器旨在解决的问题。我们将从强调容器的重要性开始。然后,我们将介绍 Docker,这一在容器化兴起过程中发挥了关键作用的运行时,并讨论它与 Kubernetes 的关系。
本章旨在帮助你理解如何在 Docker 中运行容器。你可能听过一个常见问题:“Docker 和 Kubernetes 之间有什么关系?”好吧,在今天的世界里,Docker 完全与 Kubernetes 无关——你不需要 Docker 来运行 Kubernetes,也不需要它来创建容器。本章讨论 Docker,旨在提供让你在本地运行容器并在部署到 Kubernetes 集群之前测试镜像的技能。
到本章结束时,你将清楚地理解如何安装 Docker,以及如何有效使用常用的 Docker 命令行界面(CLI)命令。
本章将涵盖以下主要内容:
-
理解容器化的需求
-
理解为什么 Kubernetes 移除了 Docker
-
理解 Docker
-
安装 Docker
-
使用 Docker CLI
技术要求
本章有以下技术要求:
-
一台运行 Docker 的 Ubuntu 22.04+ 服务器,至少 4 GB 的内存,建议 8 GB。
-
来自仓库
chapter1文件夹的脚本,你可以通过以下链接访问:github.com/PacktPublishing/Kubernetes-An-Enterprise-Guide-Third-Edition
理解容器化的需求
你可能在办公室或学校经历过这样的对话:
开发者:“这是新应用程序。它经过了几周的测试,你是第一个获得新版本的人。”
….. 一会儿后 …..
用户:“它不工作。当我点击提交按钮时,显示一个关于缺少依赖项的错误。”
开发者:“这很奇怪,它在我的机器上运行得很好。”
在部署应用程序时,遇到此类问题可能会让开发人员感到非常沮丧。这些问题通常是由于最终包中缺少开发人员自己机器上有的某个库所致。有人可能认为一个简单的解决方法是将所有库都包含在发布包中,但如果这个发布包中包含了一个更新版本的库,而这个版本替换了一个较旧版本的库,而另一个应用程序仍依赖于该旧版本的库呢?
开发人员必须仔细考虑他们的新版本及其可能与用户工作站上现有软件产生的冲突。这变成了一个微妙的平衡行为,通常需要更大的部署团队在各种系统配置上彻底测试应用程序。这种情况可能导致开发人员增加额外的工作,或者在极端情况下,使应用程序与现有应用程序完全不兼容。
多年来,已经有几个尝试简化应用程序交付的方案。一种解决方案是 VMware 的ThinApp,其目的是虚拟化一个应用程序(不要与虚拟化整个操作系统(OS)混淆)。它允许你将应用程序及其依赖项打包成一个可执行文件。这样,所有应用程序的依赖项都包含在包内,消除了与其他应用程序依赖项的冲突。这不仅确保了应用程序的隔离性,还增强了安全性并减少了操作系统迁移的复杂性。
你可能直到现在才接触到诸如应用程序打包或“随身应用”之类的术语,但它似乎是解决臭名昭著的“在我的机器上能运行”问题的一个不错的解决方案。然而,这种方法未能如预期那样广泛采用,也有其原因。首先,这个领域的大多数解决方案都是收费的,需要进行大量的投资。此外,它们需要一个“干净的 PC”,也就是说,每次想要虚拟化一个应用程序时,必须从一个全新的系统开始。你创建的包会捕捉基础安装和后续更改之间的差异。这些差异随后会被打包成一个分发文件,可以在任何工作站上执行。
我们提到应用程序虚拟化,是为了突出“在我的机器上能运行”之类的应用程序问题近年来有过不同的解决方案。像ThinApp这样的产品只是其中一种尝试。其他尝试还包括使用Citrix、远程桌面、Linux 容器、chroot 监狱,甚至是虚拟机来运行应用程序。
理解为什么 Kubernetes 移除了 Docker
Kubernetes 在版本 1.24 中移除了对 Docker 作为支持的容器运行时的所有支持。虽然它已被移除作为运行时引擎选项,但你仍然可以使用 Docker 创建新的容器,它们将能够在任何支持 开放容器倡议(OCI)规范的运行时上运行。OCI 是一套关于容器及其运行时的标准,这些标准确保了容器的可移植性,无论容器平台或执行它们的运行时是什么。
当你使用 Docker 创建容器时,你实际上是在创建一个完全符合 开放容器规范(OCI)的容器,因此它仍然可以在运行任何与 Kubernetes 兼容的容器运行时的 Kubernetes 集群上运行。
为了全面解释影响及其支持的替代方案,我们需要了解什么是容器运行时。一个高级定义是,容器运行时是运行和管理容器的软件层。像 Kubernetes 集群中的许多组件一样,运行时并不包含在 Kubernetes 中——它是一个可插拔模块,需要由供应商或你自己提供,以创建一个功能正常的集群。
有许多技术原因导致了弃用和移除 Docker 的决定,但从高层次来看,主要的担忧如下:
-
Docker 在 Docker 运行时内部包含多个组件,用于支持其自己的远程 API 和 用户体验(UX)。而 Kubernetes 只需要可执行文件中的一个组件,dockerd,它是管理容器的运行时进程。可执行文件中的所有其他组件对在 Kubernetes 集群中使用 Docker 没有任何贡献。这些额外的组件使得二进制文件臃肿,并可能导致额外的漏洞、安全性或性能问题。
-
Docker 不符合 容器运行时接口(CRI)标准,后者的引入旨在创建一套标准,以便在 Kubernetes 中轻松集成容器运行时。由于 Docker 不符合该标准,Kubernetes 团队不得不额外做很多工作,仅仅是为了支持 Docker。
在本地容器测试和开发时,你仍然可以在工作站或服务器上使用 Docker。考虑到之前的陈述,如果你在 Docker 上构建一个容器,并且该容器能够在 Docker 运行时系统上成功运行,那么它也能在不使用 Docker 作为运行时的 Kubernetes 集群上运行。
移除 Docker 对大多数新集群中的 Kubernetes 用户影响很小。容器将仍然以任何标准方式运行,就像 Docker 作为容器运行时时一样。如果你恰好管理一个集群,在排查 Kubernetes 节点问题时,你可能需要学习新的命令——因为节点上将没有 Docker 命令来查看正在运行的容器、清理卷等。
Kubernetes 支持多种替代 Docker 的运行时。以下是两个最常用的运行时:
-
containerd
-
CRI-O
虽然这两种是常用的运行时,但也有许多其他兼容的运行时可用。你可以随时在 Kubernetes 的 GitHub 页面查看最新支持的运行时,网址是 github.com/kubernetes/community/blob/master/contributors/devel/sig-node/container-runtime-interface.md。
关于弃用和移除 Docker 的影响的更多细节,请参考 Kubernetes.io 网站上的文章 Don’t Panic: Kubernetes and Docker,网址是 kubernetes.io/blog/2020/12/02/dont-panic-kubernetes-and-docker/。
引入 Docker
无论是行业还是最终用户,都在寻求一个既方便又实惠的解决方案,这就是Docker容器的出现。虽然容器在不同的时间以不同方式被使用,但 Docker 通过为普通用户和开发者提供运行时和工具,带来了变革。
Docker 为大众带来了一个抽象层,它易于使用,不需要为每个应用程序创建包时都清理 PC,从而解决了依赖性问题,但最吸引人的一点是,它是免费的。Docker 成为 GitHub 上许多项目的标准,团队通常会创建一个 Docker 容器,并将 Docker 镜像或 Dockerfile 分发给团队成员,提供标准的测试或开发环境。这种最终被最终用户接受的方式,促使 Docker 进入企业,并最终使其成为今天的标准。
在本书的范围内,我们将重点关注在尝试使用本地 Kubernetes 环境时需要了解的内容。Docker 具有悠久而有趣的发展历史,如何演变成今天我们使用的标准容器镜像格式。我们鼓励你了解这家公司,以及他们如何引领我们今天所知道的容器世界。
虽然我们的重点不是从头到尾教授 Docker,但我们认为对于那些 Docker 新手来说,快速了解一般容器概念会带来帮助。
如果你有一些 Docker 经验,并且理解像是短暂(ephemeral)和无状态(stateless)这样的术语,你可以跳到安装 Docker部分。
Docker 与 Moby
当 Docker 运行时被开发时,它是一个单一的代码库。这个单一的代码库包含了 Docker 提供的所有功能,无论你是否使用过它们。这导致了效率低下,并开始阻碍 Docker 和容器的整体发展。
下表显示了 Docker 和 Moby 项目的区别。
| 功能 | Docker | Moby |
|---|---|---|
| 开发 | 主要贡献者是 Docker,并有一些社区支持 | 它是开源软件,拥有强大的社区开发和支持 |
| 项目范围 | 包括构建和运行容器所需的所有组件的完整平台 | 它是一个模块化平台,用于构建基于容器的组件和解决方案 |
| 所有权 | 这是一个由 Docker, Inc. 提供的品牌产品 | 它是一个开源项目,用于构建各种容器解决方案 |
| 配置 | 包含完整的默认配置,使用户能够快速使用 | 提供更多可用的自定义选项,帮助用户满足特定需求 |
| 商业支持 | 提供完整支持,包括企业级支持 | 作为开源软件提供;Moby 项目不直接提供支持 |
表格 1.1:Docker 与 Moby 的功能对比
总结一下——Moby 是一个由 Docker 启动的项目,但它不是完整的 Docker 运行时。Docker 运行时使用来自 Moby 的组件来创建 Docker 运行时,其中包含 Moby 的开源组件和 Docker 自身的开源组件。
现在,让我们更进一步了解 Docker,看看你如何使用它来创建和管理容器。
理解 Docker
本书假设你已经具备了 Docker 和容器概念的基础理解。然而,我们知道并非每个人都拥有 Docker 或容器的使用经验。因此,我们在书中加入了这部分速成课程,旨在向你介绍容器概念,并指导你如何使用 Docker。
如果你是容器新手,我们建议你阅读 Docker 网站上的文档,获取更多信息:docs.docker.com/。
容器是短暂的
首先需要理解的是,容器是短暂的(暂时存在的)。
“短暂”(ephemeral)一词指的是存在时间较短的事物。容器可以被故意终止,或者在没有用户干预或后果的情况下自动重新启动。为了更好地理解这个概念,我们来看一个例子——假设某人在容器中运行的网页服务器上交互式地添加了文件。这些上传的文件是临时的,因为它们最初并不属于基础镜像的一部分。
这意味着一旦容器被构建并运行,任何对容器所做的更改都不会在容器被移除或销毁后保存。让我们来看一个完整的示例:
-
你在主机上启动一个运行 NGINX 的容器,但没有任何基础的 HTML 页面。
-
使用 Docker 命令,你执行
copy命令将一些网页文件复制到容器的文件系统中。 -
为了验证复制是否成功,你访问网站并确认它是否提供正确的网页。
-
满意结果后,你停止容器并将其从主机中移除。当天稍晚,你想向同事展示网站,便启动了 NGINX 容器。你再次访问网站,但当页面打开时,收到
404错误(页面未找到错误)。
你在停止并从主机上移除容器之前上传的文件去哪了?
你在容器重启后找不到网页的原因是所有容器都是临时性的。每次容器首次启动时,基础容器镜像中的内容就是唯一会被包含的内容。你在容器内部所做的任何更改都是短暂的。
如果你需要向现有镜像添加永久文件,你需要重新构建镜像并将文件包含其中,或者正如我们将在本章稍后的持久化数据部分中解释的,你可以在容器中挂载一个 Docker 卷。
在这一点上,主要的概念是容器是临时的。
等等!你可能会想,“如果容器是临时的,那我怎么把网页添加到服务器中?”临时只是意味着更改不会被保存;它并不阻止你对运行中的容器进行更改。
对运行中的容器所做的任何更改都会写入一个临时层,这个层被称为容器层,它是本地文件系统上的一个目录。Docker 使用存储驱动程序,负责处理使用容器层的请求。存储驱动程序负责在 Docker 主机上管理和存储镜像及容器。它控制与存储和管理它们相关的机制和过程。
这个位置将存储容器文件系统中的所有更改,因此,当你将 HTML 页面添加到容器时,它们会存储在本地主机上。容器层与运行镜像的容器 ID绑定,并且它会一直保留在主机系统上,直到容器从 Docker 中移除,无论是通过 CLI 命令还是运行 Docker 清理任务(参见下一页的图 1.1)。
考虑到容器是临时的并且是只读的,你可能会好奇,如何在容器内部修改数据。Docker 通过利用镜像层叠来解决这个问题,镜像层叠涉及创建相互连接的层,这些层共同作为一个单一的文件系统。通过这种方式,可以对容器的数据进行更改,即使底层镜像保持不可变。
Docker 镜像
一个 Docker 镜像由多个镜像层组成,每个镜像层都附带一个JavaScript 对象表示法(JSON)文件,用于存储该层特定的元数据。当启动容器镜像时,这些层会被组合成用户交互的应用程序。
你可以在 Docker 的 GitHub 上查看更多关于镜像内容的信息,链接:github.com/moby/moby/blob/master/image/spec/v1.1.md。
镜像层
正如我们在前一部分中提到的,运行中的容器使用一个位于基础镜像层“之上”的容器层,如下图所示:

图 1.1:Docker 镜像层
镜像层是只读状态,不能写入,但临时容器层是可写的。你添加到容器中的任何数据都会存储在这个层中,并且只要容器在运行,这些数据就会被保留。
为了高效地处理多个层次,Docker 实现了写时复制(copy-on-write),这意味着如果文件已经存在,它不会被重新创建。然而,如果当前镜像中不存在某个需要的文件,它将会被写入。在容器世界中,如果某个文件存在于较低的层次,位于它之上的层次就不需要再次包含这个文件。例如,如果第一层包含一个名为/opt/nginx/index.xhtml的文件,那么第二层就不需要在它的层次中包含相同的文件。
这解释了系统如何处理已存在或不存在的文件,但如果文件被修改了怎么办?有时你需要替换较低层次中的文件。你可能在构建镜像时需要这么做,或者作为临时修复正在运行的容器问题。写时复制系统知道如何处理这些问题。由于镜像是从上到下读取的,容器只会使用最上层的文件。如果你的系统在第一层有一个/opt/nginx/index.xhtml文件,并且你修改并保存了该文件,那么运行中的容器会将新文件保存在容器层中。由于容器层是最上层,新版本的index.xhtml会始终在镜像层中的旧版本之前被读取。
持久数据
仅限于临时容器会极大限制 Docker 的使用场景。你可能会遇到需要持久存储的情况,或者即使容器停止运行也必须保留数据。
记住,当你将数据存储在容器镜像层时,基础镜像本身并不会改变。当容器从主机中删除时,容器层也会被删除。如果使用相同的镜像启动新容器,则会创建一个新的容器镜像层。虽然容器本身是临时的,但你可以通过结合 Docker 卷来实现数据持久化。通过使用Docker 卷,数据可以存储在容器外部,使其能够在容器生命周期之外持久存在。
访问运行在容器中的服务
与物理机器或虚拟机不同,容器不会直接连接到网络。当容器需要发送或接收流量时,它会通过 Docker 主机系统使用桥接的网络地址转换(NAT)连接。这意味着当你运行容器并希望接收传入的流量请求时,你需要为每个希望接收流量的容器暴露端口。在基于 Linux 的系统中,iptables 有规则将流量转发到 Docker 守护进程,后者将为每个容器分配的端口提供服务。你无需担心 iptables 规则是如何创建的,因为 Docker 会在启动容器时通过使用提供的端口信息来为你处理。如果你是 Linux 新手,iptables 可能对你来说是一个新概念。
从高层次来看,iptables 用于管理网络流量并确保集群内的安全。它控制集群中各个组件之间的网络连接流向,决定哪些连接被允许,哪些被阻止。
这部分内容介绍了容器基础知识和 Docker 概念。在下一部分,我们将引导你完成在主机上安装 Docker 的过程。
安装 Docker
本书中的动手练习要求你拥有一个正常工作的 Docker 主机。为了安装 Docker,我们在本书的 GitHub 仓库中包含了一个脚本,该脚本位于 chapter1 目录下,名为 install-docker.sh。
今天,你可以在几乎所有硬件平台上安装 Docker。每个版本的 Docker 在不同平台上的表现和外观都是一致的,这使得开发跨平台应用程序变得更加容易。通过确保不同平台之间的功能和命令相同,开发者无需学习不同的容器运行时来运行镜像。
以下是 Docker 可用平台的表格。如你所见,有多个操作系统的安装版本,以及多种架构:
| 桌面平台 | x86_64/amd64 | arm64 (Apple Silicon) |
|---|---|---|
| Docker Desktop (Linux) | ||
| Docker Desktop (macOS) | ||
| Docker Desktop (Windows) | ||
| 服务器平台 | x86_64/amd64 | arm64/aarch64 |
| CentOS | ||
| Debian | ||
| Fedora | ||
| Raspberry Pi OS | ||
| RHEL (s390) | ||
| SLES | ||
| Ubuntu |
表 1.2:可用的 Docker 平台
使用单一架构创建的镜像不能在不同架构上运行。这意味着你不能基于 x86 硬件创建一个镜像,并期望它能够在运行 ARM 处理器的 Raspberry Pi 上运行。同样需要注意的是,虽然你可以在 Windows 机器上运行 Linux 容器,但无法在 Linux 机器上运行 Windows 容器。
虽然镜像默认情况下不具备跨架构兼容性,但现在有新的工具可以创建所谓的多平台镜像。多平台镜像是可以在不同架构或处理器上使用的镜像,这样你就不需要为NGINX在 x86 架构、为ARM架构、以及为PowerPC架构分别创建多个镜像,而是可以在一个容器中使用多个架构的镜像。这将帮助你简化容器化应用程序的管理和部署。由于多平台镜像包含不同架构的多个版本,因此在部署镜像时需要指定架构。幸运的是,容器运行时会自动从镜像清单中选择正确的架构。
使用多平台镜像可以为你的容器提供跨云平台、边缘部署和混合基础设施的可移植性、灵活性和可扩展性。随着行业中基于 ARM 的服务器的使用不断增长,以及学习 Kubernetes 的人们广泛使用 Raspberry Pi,跨平台镜像将有助于加快容器的使用,并使其更加容易。
例如,在 2020 年,苹果发布了 M1 芯片,结束了苹果使用英特尔处理器的时代,转而使用 ARM 处理器。我们不打算深入讨论它们之间的差异,只要知道它们是不同的,而这为容器开发人员和用户带来了重要的挑战。Docker 确实有Docker Desktop,这是一款 macOS 工具,允许你使用与 Linux、Windows 或 x86 macOS 上 Docker 安装相同的工作流来运行容器。Docker 会尝试在拉取或构建镜像时匹配底层主机的架构。在基于 ARM 的系统上,如果你尝试拉取没有 ARM 版本的镜像,Docker 会因架构不兼容而抛出错误。如果你尝试构建一个镜像,它将在 macOS 上构建一个 ARM 版本,但这个版本不能在 x86 机器上运行。
创建多平台镜像可能比较复杂。如果你想了解更多关于创建多平台镜像的细节,可以访问 Docker 官网的多平台 镜像页面:docs.docker.com/build/building/multi-platform/。
安装 Docker 的过程在不同平台之间有所不同。幸运的是,Docker 在其官网上记录了许多安装方法:docs.docker.com/install/。
本章将指导你在 Ubuntu 22.04 系统上安装 Docker。如果你没有 Ubuntu 机器进行安装,仍然可以阅读安装步骤,因为每个步骤都会详细解释,并且不需要实际系统就能理解整个过程。如果你使用的是其他 Linux 发行版,可以参考 Docker 官网提供的安装步骤:docs.docker.com/。官网为 CentOS、Debian、Fedora 和 Ubuntu 提供了具体步骤,并为其他 Linux 发行版提供了通用步骤。
准备安装 Docker
现在我们已经介绍了 Docker,下一步是选择安装方法。Docker 的安装方式不仅在不同的 Linux 发行版之间有所不同,甚至同一 Linux 发行版的不同版本也会有所变化。我们的脚本基于使用 Ubuntu 22.04 服务器,因此在其他版本的 Ubuntu 上可能无法使用。你可以通过以下两种方法之一来安装 Docker:
-
将 Docker 仓库添加到主机系统
-
使用 Docker 脚本进行安装
第一个选项被认为是最佳选择,因为它便于安装和更新 Docker 引擎。第二个选项旨在用于测试/开发环境的 Docker 安装,不建议在生产环境中部署。
由于首选方法是将 Docker 仓库添加到主机中,我们将选择此选项。
在 Ubuntu 上安装 Docker
现在我们已经添加了所需的仓库,下一步是安装 Docker。
我们在 Git 仓库的 chapter1 文件夹中提供了一个名为 install-docker.sh 的脚本。当你执行此脚本时,它将自动安装运行 Docker 所需的所有必要二进制文件。
简要概述一下脚本,它首先会修改 /etc/needrestart/needrestart.conf 文件中的特定值。在 Ubuntu 22.04 中,守护进程的重启方式发生了变化,用户可能需要手动选择哪些系统守护进程需要重启。为了简化本书中描述的操作,我们将 needsrestart.conf 文件中的 restart 值更改为“自动”,而不是每次提示重新启动被更改的服务。
接下来,我们安装一些实用工具,如 vim、ca-certificates、curl 和 GnuPG。前三个工具比较常见,而最后一个 GnuPG 可能对一些读者来说是新的,可能需要一些解释。GnuPG 是 GNU 隐私保护工具(GNU Privacy Guard)的缩写,它为 Ubuntu 提供了一系列加密功能,如 加密、解密、数字签名 和 密钥管理。
在我们的 Docker 部署中,我们需要添加 Docker 的 GPG 公钥,这是一个加密密钥对,用于保护通信并保持数据完整性。GPG 密钥使用非对称加密,即使用两把不同但相关的密钥,称为 公钥 和 私钥。这两把密钥一起生成,但它们执行不同的功能。私钥保持机密,用于对下载的文件生成数字签名。公钥是公开的,用于验证由私钥创建的数字签名。
接下来,我们需要将 Docker 仓库添加到我们的本地仓库列表中。当我们将仓库添加到列表时,还需要包含 Docker 证书。docker.gpg 证书由脚本从 Docker 网站下载,并保存在本地服务器的 /etc/apt/keyings/docker.gpg 路径下。当我们将仓库添加到仓库列表时,我们通过在 /etc/apt/sources.list.d/docker.list 文件中使用 signed-by 选项来添加该密钥。完整的命令如下所示:
deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu jammy stable
通过将 Docker 仓库添加到我们本地的 apt 仓库列表中,我们可以轻松地安装 Docker 二进制文件。此过程涉及使用简单的 apt-get install 命令,它将安装 Docker 所需的五个基本二进制文件:docker-ce、docker-ce-cli、containerd.io、docker-buildx-plugin 和 docker-compose-plugin。如前所述,所有这些文件都经过 Docker 的 GPG 密钥签名。由于服务器上已包含 Docker 的密钥,我们可以确信这些文件是安全的,并且来自可靠的来源。
一旦 Docker 成功安装,下一步是使用 systemctl 命令启用并配置 Docker 守护进程,以便在系统启动时自动启动。此过程遵循应用于大多数在 Linux 服务器上安装的系统守护进程的标准程序。
我们没有逐行解释每个脚本中的代码,而是在脚本中加入了注释,帮助你理解每个命令和步骤的执行方式。如果某些主题需要帮助,我们将在章节中提供部分代码供参考。
安装 Docker 后,我们来处理一些配置。首先,你在实际操作中很少以 root 身份执行命令,因此我们需要授权你的用户使用 Docker。
授予 Docker 权限
在默认安装中,Docker 需要 root 权限,因此你需要以 root 身份运行所有 Docker 命令。为了避免每次都使用 sudo 执行 Docker 命令,你可以将你的用户账户添加到服务器上一个新的组中,该组提供 Docker 访问权限,而无需为每个命令使用 sudo。
如果你以普通用户身份登录并尝试运行 Docker 命令,你将收到一个错误提示:
Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get http://%2Fvar%2Frun%2Fdocker.sock/v1.40/images/json: dial unix /var/run/docker.sock: connect: permission denied
为了让你的用户,或你可能希望添加的其他用户,执行 Docker 命令,你需要将这些用户添加到一个名为docker的新组,该组是在安装 Docker 时创建的。以下是你可以使用的命令示例,将当前登录用户添加到该组:
sudo usermod -aG docker $USER
要将新成员添加到你的账户中,你可以选择注销并重新登录 Docker 主机,或者使用newgrp命令激活组变更:
newgrp docker
现在,让我们通过运行标准的hello-world镜像来测试 Docker 是否正常工作(请注意,我们不需要sudo来运行 Docker 命令):
docker run hello-world
你应该看到以下输出,验证你的用户有权限访问 Docker:
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
2db29710123e: Pull complete
Digest: sha256:37a0b92b08d4919615c3ee023f7ddb068d12b8387475d64c622ac30f45c29c51
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
该消息显示你的安装工作正常——恭喜!
为了生成此消息,Docker 执行了以下步骤:
-
Docker 客户端已联系 Docker 守护进程。
-
Docker 守护进程从 Docker Hub 拉取了
hello-world镜像(amd64)。 -
Docker 守护进程从镜像创建了一个新容器,并运行该可执行文件,生成你当前所看到的输出。
-
Docker 守护进程将该输出流式传输到 Docker 客户端,客户端将其发送到你的终端。
如果你想尝试更有挑战性的操作,可以使用以下命令运行一个 Ubuntu 容器:
$ docker run -it ubuntu bash
有关更多示例和想法,请访问docs.docker.com/get-started/。
现在我们已经授予了 Docker 权限,可以开始解锁最常用的 Docker 命令,学习如何使用 Docker CLI。
使用 Docker CLI
当你运行hello-world容器测试安装时,你使用了 Docker CLI。Docker 命令是你与 Docker 守护进程交互时将使用的命令。通过这个单一的可执行文件,你可以执行以下操作,甚至更多:
-
启动和停止容器
-
拉取和推送镜像
-
在活动容器中运行一个 Shell
-
查看容器日志
-
创建 Docker 卷
-
创建 Docker 网络
-
清理旧的镜像和卷
本章并不打算详尽地解释每个 Docker 命令;相反,我们将解释一些你在与 Docker 守护进程和容器交互时需要使用的常见命令。
你可以将 Docker 命令分为两类:一般的 Docker 命令和 Docker 管理命令。标准的 Docker 命令允许你管理容器,而管理命令允许你管理 Docker 选项,如管理卷和网络。
docker help
忘记命令的语法或选项是很常见的,Docker 对此已有考虑。如果你发现自己无法记起某个命令,随时可以依赖docker help命令。它会提供该命令的功能说明以及如何使用它的帮助。
docker run
要运行容器,使用docker run命令并提供相应的镜像名称。但在执行docker run命令之前,你应该了解在启动容器时可以提供的选项。
在最简单的形式下,你可以使用的命令来运行一个 NGINX Web 服务器是 docker run bitnami/nginx:latest。这将启动一个运行 NGINX 的容器,并且它将在前台运行,显示容器中运行应用程序的日志。按下 Ctrl + C 将停止正在运行的容器并终止 NGINX 服务器:
nginx 22:52:27.42
nginx 22:52:27.42 Welcome to the Bitnami nginx container
nginx 22:52:27.43 Subscribe to project updates by watching https://github.com/bitnami/bitnami-docker-nginx
nginx 22:52:27.43 Submit issues and feature requests at https://github.com/bitnami/bitnami-docker-nginx/issues
nginx 22:52:27.44
nginx 22:52:27.44 INFO ==> ** Starting NGINX setup **
nginx 22:52:27.49 INFO ==> Validating settings in NGINX_* env vars
nginx 22:52:27.50 INFO ==> Initializing NGINX
nginx 22:52:27.53 INFO ==> ** NGINX setup finished! **
nginx 22:52:27.57 INFO ==> ** Starting NGINX **
如你所见,当你使用 Ctrl + C 停止容器时,NGINX 也停止了。在大多数情况下,你希望容器启动并继续运行,而不处于前台,这样系统可以运行其他任务,同时容器也能继续运行。要将容器作为后台进程运行,你需要在 Docker 命令中添加 -d 或 --detach 选项,这将使容器以分离模式运行。现在,当你运行一个分离的容器时,你只会看到容器 ID,而不是交互式或附加的屏幕:
[root@localhost ~]# docker run -d bitnami/nginx:latest
13bdde13d0027e366a81d9a19a56c736c28feb6d8354b363ee738d2399023f80
[root@localhost ~]#
默认情况下,容器启动后会被赋予一个随机名称。在我们之前的分离示例中,如果我们列出正在运行的容器,我们将看到容器被赋予了名称 silly_keldysh,如下所示的输出:
CONTAINER ID IMAGE NAMES
13bdde13d002 bitnami/nginx:l
如果没有为容器指定名称,当你在单个主机上运行多个容器时,很容易造成混乱。为了简化管理,你应该始终为容器指定一个名称,这样有助于更好地管理。Docker 提供了另一个选项来配合 run 命令使用:--name 选项。基于我们之前的示例,我们将容器命名为 nginx-test。我们的新 docker run 命令如下所示:
docker run --name nginx-test -d bitnami/nginx:latest
就像运行任何分离镜像一样,这将返回容器 ID,但不会显示你指定的名称。为了验证容器是否以 nginx-test 的名称运行,我们可以使用 docker ps 命令列出容器,接下来我们将对此进行解释。
docker ps
通常,你需要检索正在运行的容器列表或已停止的容器列表。Docker CLI 有一个名为 ps 的标志,它会列出所有正在运行和已停止的容器,方法是将额外的标志添加到 ps 命令中。输出将列出容器,包括它们的容器 ID、镜像标签、entry 命令、创建日期、状态、端口和容器名称。以下是当前正在运行的容器的示例:
CONTAINER ID IMAGE COMMAND CREATED
13bdde13d002 bitnami/nginx:latest "/opt/bitnami/script…" Up 4 hours
3302f2728133 registry:2 "/entrypoint.sh /etc…" Up 3 hours
这对于你要查找的容器当前正在运行时很有帮助,但如果容器已经停止,或者更糟的是,容器未能启动然后停止了呢?你可以通过在 docker ps 命令中添加 -a 标志,查看所有容器的状态,包括以前运行过的容器。当你执行 docker ps -a 时,你将看到与标准 ps 命令相同的输出,但你会注意到列表中可能包含额外的容器。
如何判断哪些容器正在运行,哪些容器已经停止?如果你查看列表中的STATUS字段,正在运行的容器会显示运行时间;例如,Up xx hours 或 Up xx days。然而,如果容器因为某种原因已经停止,状态会显示容器停止的时间;例如,Exited (0) 10 minutes ago。
IMAGE COMMAND CREATED STATUS
bitnami/nginx:latest "/opt/bitnami/script…" 10 minutes ago Up 10 minutes
bitnami/nginx:latest "/opt/bitnami/script…" 12 minutes ago Exited (0) 10 minutes ago
停止的容器并不意味着运行镜像时发生了问题。有些容器可能只执行一个任务,完成后容器可能会优雅地停止。判断退出是否优雅或是否由于启动失败的一种方法是查看退出状态码。有多种退出码可以帮助你查找容器退出的原因。
| 退出码 | 描述 |
|---|---|
0 |
命令成功执行,没有任何问题。 |
1 |
由于意外错误,命令执行失败。 |
2 |
命令无法找到指定的资源或遇到类似的问题。 |
125 |
由于 Docker 相关错误,命令执行失败。 |
126 |
命令执行失败,因为 Docker 二进制文件或脚本无法执行。 |
127 |
命令执行失败,因为无法找到 Docker 二进制文件或脚本。 |
128+ |
由于特定的与 Docker 相关的错误或异常,命令执行失败。 |
表 1.3:Docker 退出码
docker 启动和停止
你可能需要停止一个容器,因为系统资源有限,限制了你同时运行的容器数量。要停止一个正在运行的容器并释放资源,可以使用docker stop命令,后面跟上你要停止的容器名称或容器 ID。
如果你需要在未来某个时刻重新启动该容器进行额外的测试或开发,可以执行docker start <name>,这将以原始启动时的所有选项启动容器,包括任何分配的网络或卷。
docker attach
为了排查问题或查看日志文件,可能需要与容器进行交互。连接到当前正在运行的容器的一种方法是使用docker attach <容器 ID/名称>命令。当你执行此操作时,你将与运行容器的活动进程建立连接。如果你连接到一个正在执行进程的容器,通常不会看到任何提示信息。事实上,很可能你会看到一个空白屏幕,直到容器开始产生并显示在屏幕上的输出。
当连接到一个容器时,你应该始终保持谨慎。很容易不小心停止正在运行的进程,从而停止容器。让我们以连接一个运行 NGINX 的 Web 服务器为例。首先,我们需要通过docker ps验证容器是否正在运行:
CONTAINER ID IMAGE COMMAND STATUS
4a77c14a236a nginx "/docker-entrypoint.…" Up 33 seconds
使用attach命令,我们执行docker attach 4a77c14a236a。
当您附加到一个进程时,您将只能与正在运行的进程交互,并且您看到的唯一输出是发送到标准输出的数据。以 NGINX 容器为例,attach 命令已附加到 NGINX 进程。为了展示这一点,我们将退出附加并从另一个会话中对 Web 服务器执行 curl。一旦我们对容器执行 curl,我们将看到日志输出到附加的控制台:
[root@astra-master manifests]# docker attach 4a77c14a236a
172.17.0.1 - - [15/Oct/2021:23:28:31 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.61.1" "-"
172.17.0.1 - - [15/Oct/2021:23:28:33 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.61.1" "-"
172.17.0.1 - - [15/Oct/2021:23:28:34 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.61.1" "-"
172.17.0.1 - - [15/Oct/2021:23:28:35 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.61.1" "-"
172.17.0.1 - - [15/Oct/2021:23:28:36 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.61.1" "-"
我们提到过,一旦你附加到容器,就需要小心。对于新手来说,可能会附加到 NGINX 镜像,假设服务器上没有发生任何事情,或者进程似乎卡住了,因此他们可能决定使用标准的 Ctrl + C 键盘命令跳出容器。这将停止容器并将他们带回 Bash 提示符,在这里他们可以运行 docker ps 来查看正在运行的容器:
root@localhost:~# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS
root@localhost:~#
NGINX 容器发生了什么?我们没有执行 docker stop 命令,而容器一直在运行,直到我们附加到容器。为什么我们附加到容器后,容器会停止?
正如我们提到的,当附加到容器时,您实际上是附加到正在运行的进程上。所有键盘命令的行为就像您在一台运行 NGINX 的物理服务器上的常规 shell 中一样。这意味着,当用户使用 Ctrl + C 返回到提示符时,他们实际上停止了正在运行的 NGINX 进程。
如果我们按下 Ctrl + C 来退出容器,我们将看到一个输出,显示进程已被终止。以下输出展示了在我们的 NGINX 示例中发生的情况:
2023/06/27 19:38:02 [notice] 1#1: signal 2 (SIGINT) received, exiting2023/06/27 19:38:02 [notice] 31#31: exiting2023/06/27 19:38:02 [notice] 30#30: exiting2023/06/27 19:38:02 [notice] 29#29: exiting2023/06/27 19:38:02 [notice] 31#31: exit2023/06/27 19:38:02 [notice] 30#30: exit2023/06/27 19:38:02 [notice] 29#29: exit2023/06/27 19:38:02 [notice] 32#32: exiting2023/06/27 19:38:02 [notice] 32#32: exit2023/06/27 19:38:03 [notice] 1#1: signal 17 (SIGCHLD) received from 312023/06/27 19:38:03 [notice] 1#1: worker process 29 exited with code 02023/06/27 19:38:03 [notice] 1#1: worker process 31 exited with code 02023/06/27 19:38:03 [notice] 1#1: worker process 32 exited with code 02023/06/27 19:38:03 [notice] 1#1: signal 29 (SIGIO) received2023/06/27 19:38:03 [notice] 1#1: signal 17 (SIGCHLD) received from 292023/06/27 19:38:03 [notice] 1#1: signal 17 (SIGCHLD) received from 302023/06/27 19:38:03 [notice] 1#1: worker process 30 exited with code 02023/06/27 19:38:03 [notice] 1#1: exit
如果容器的运行进程停止,容器也会停止,这就是为什么 docker ps 命令没有显示正在运行的 NGINX 容器。
要退出附加会话,而不是使用 Ctrl + C 返回到提示符,您应该使用 Ctrl + P,然后按 Ctrl + Q,这将退出容器而不停止正在运行的进程。
有一个替代 attach 命令的方法:docker exec 命令。exec 命令与 attach 命令不同,因为您需要提供要在容器中执行的进程。
docker exec
在与正在运行的容器交互时,一个更好的选择是 exec 命令。与其附加到容器,您可以使用 docker exec 命令在容器中执行进程。您需要提供容器名称以及您想在镜像中执行的进程。当然,进程必须包含在运行的镜像中——如果镜像中没有 Bash 可执行文件,在尝试执行 Bash 时将会出现错误。
我们将再次使用 NGINX 容器作为示例。我们将使用 docker ps 来验证 NGINX 是否在运行,然后使用容器 ID 或名称执行进入容器的操作。命令语法是 docker exec <options> <container name> <command>:
root@localhost:~# docker exec -it nginx-test bash
I have no name!@a7c916e7411:/app$
我们包含的选项是-it,它告诉exec在交互式 TTY 会话中运行。在这里,我们希望执行的进程是 Bash。
注意到提示符的名称已经从原来的用户名和主机名发生了变化。主机名是localhost,而容器名称是a7c916e7411。你可能还注意到当前的工作目录从~变成了/app,并且提示符显示不是以 root 用户身份运行(如显示的$提示符)。
你可以像使用标准的SSH连接一样使用这个会话;你在容器中运行 Bash,并且因为我们没有附加到容器中的运行进程,Ctrl + C不会停止任何正在运行的进程。
要退出交互式会话,你只需要输入exit,然后按Enter,这将退出容器。如果你接着运行docker ps,你会注意到容器仍然处于运行状态。
接下来,让我们看看我们能从 Docker 日志文件中学到什么。
docker logs
docker logs命令允许你使用容器名称或容器 ID 从容器中获取日志。你可以查看ps命令中列出的任何容器的日志,无论该容器是否正在运行或已停止。
日志文件通常是排查容器无法启动或处于退出状态的唯一方法。例如,如果你尝试运行一个镜像,镜像启动后突然停止,你可以通过查看该容器的日志来找到问题所在。
要查看容器的日志,你可以使用docker logs <容器 ID 或名称>命令。
要查看容器 ID 为7967c50b260f的容器的日志,你可以使用以下命令:
docker logs 7967c50b260f
这将把容器的日志输出到你的屏幕上,日志内容可能非常长且冗杂。由于许多日志可能包含大量信息,你可以通过为logs命令添加额外选项来限制输出。下表列出了查看日志时可用的选项:
| 日志选项 | 描述 |
|---|---|
-f |
跟踪日志输出(也可以使用--follow)。 |
--tail xx |
从文件末尾开始显示日志输出并检索xx行。 |
--until xxx |
显示在xxx时间戳之前的日志输出。xxx可以是时间戳,例如2020-02-23T18:35:13;xxx也可以是相对时间,例如60m。 |
--since xxx |
显示从xxx时间戳之后的日志输出。xxx可以是时间戳,例如2020-02-23T18:35:13;xxx也可以是相对时间,例如60m。 |
表 1.4:日志选项
查看日志文件是你经常需要做的一个操作,由于日志文件可能非常长,了解像tail、until和since这样的选项可以帮助你更快地找到日志中的信息。
docker rm
一旦你为容器指定了一个名称,除非使用docker rm命令删除它,否则该名称不能用于其他容器。如果你有一个名为nginx-test的容器,并且它已停止运行,尝试启动另一个名为nginx-test的容器时,Docker 守护进程会返回一个错误,提示该名称正在使用:
Conflict. The container name "/nginx-test" is already in use
原始的nginx-test容器没有运行,但守护进程知道该容器名称之前已被使用,并且它仍然存在于之前运行的容器列表中。
当你想要重用一个特定名称时,必须先删除已有的容器,然后才能用相同的名称启动一个新的容器。这种情况通常发生在容器镜像测试期间。你可能最初启动了一个容器,但遇到了应用或镜像问题。在这种情况下,你会停止容器,解决镜像或应用的问题,并希望使用相同的名称重新部署它。然而,由于之前的容器仍存在于 Docker 历史记录中,因此在重新利用该名称之前必须先将其删除。
你还可以向 Docker 命令中添加--rm选项,以在容器停止后自动删除镜像。
要删除nginx-test容器,只需执行docker rm nginx-test:
root@localhost ~:# docker rm nginx-test
nginx-test
root@localhost ~:#
假设容器名称正确并且该容器未运行,你看到的唯一输出将是你已删除的镜像名称。
我们还没有讨论 Docker 卷,但当删除一个附带卷的容器时,最好在删除命令中添加-v选项。将-v选项添加到docker rm命令中将删除任何附加到容器的卷。
docker pull/run
在运行pull时,确保指定架构。docker的pull和run用于拉取镜像或运行镜像。如果你尝试运行一个在 Docker 主机上尚不存在的容器,它将启动一个pull请求来获取该容器并运行它。
当你尝试pull或run一个容器时,Docker 会下载与主机架构兼容的容器。如果你想下载基于不同架构的镜像,可以在build命令中添加--platform标签。例如,如果你使用的是 arm64 架构的系统,并且想要拉取一个 x86 镜像,你需要将linux/arm64作为平台。运行pull时,确保指定架构:
root@localhost ~:# docker pull --platform=linux/amd64 ubuntu:22.04
22.04: Pulling from library/ubuntu6b851dcae6ca: Pull completeDigest: sha256:6120be6a2b7ce665d0cbddc3ce6eae60fe94637c6a66985312d1f02f63cc0bcdStatus: Downloaded newer image for ubuntu:22.04WARNING: image with reference ubuntu was found but does not match the specified platform: wanted linux/amd64, actual: linux/arm64/v8docker.io/library/ubuntu:22.04
添加--platform=linux/amd64就是告诉 Docker 获取正确的平台。你可以对docker run使用相同的参数,以确保使用正确的容器镜像平台。
docker build
与pull和run类似,Docker 会尝试基于主机架构arm64来构建镜像。假设你是在一个基于 arm64 的镜像系统上构建,你可以通过使用buildx子命令来让 Docker 创建一个 x86 镜像:
root@localhost ~:# docker buildx build --platform linux/amd64 --tag docker.io/mlbiam/openunison-kubernetes-operator --no-cache -f ./src/main/docker/Dockerfile .
这个附加选项告诉 Docker 生成 x86 版本,它将在任何基于 x86 的硬件上运行。
总结
在本章中,你学习了 Docker 如何解决常见的开发问题,包括让人头痛的“它在我的机器上能运行”问题。我们还介绍了你在日常使用中会用到的最常用的 Docker CLI 命令。
在下一章中,我们将开始 Kubernetes 之旅,介绍 KinD,这是一个实用工具,提供了一种在单台工作站上运行多节点 Kubernetes 测试服务器的简便方法。
问题
-
单一 Docker 镜像可以在任何 Docker 主机上使用,无论使用什么架构。
-
正确
-
错误
-
答案:b
我们增加了跨平台镜像的主题
-
Docker 用什么来将多个镜像层合并为一个单一的文件系统?
-
合并文件系统
-
NTFS 文件系统
-
EXT4 文件系统
-
联合文件系统
-
答案:d
-
Kubernetes 仅与 Docker 运行时引擎兼容。
-
正确
-
错误
-
答案:b
-
当你交互式编辑容器的文件系统时,变更会写入哪个层?
-
操作系统层
-
最底层
-
容器层
-
临时层
-
答案:c
-
假设镜像包含所需的二进制文件,哪个 Docker 命令允许你访问正在运行的容器的 bash 提示符?
-
docker shell -it <container> /bin/bash -
docker run -it <container> /bin/bash -
docker exec -it <container> /bin/bash -
docker spawn -it <container> /bin/bash
-
答案:c
-
如果你使用简单的
run命令启动一个容器,并且没有任何标志,而且容器被停止,Docker 守护进程将删除容器的所有痕迹。-
正确
-
错误
-
答案:b
-
哪个命令将显示所有容器的列表,包括任何已停止的容器?
-
docker ps -all -
docker ps -a -
docker ps -list -
docker list all
-
答案:b
加入我们书籍的 Discord 空间
加入本书的 Discord 工作区,参加每月一次的问我任何问题环节,与作者们互动:

第二章:使用 KinD 部署 Kubernetes
和许多 IT 专业人士一样,在笔记本电脑上拥有一个 Kubernetes 集群对于展示和测试产品非常有利。在某些情况下,您可能需要运行一个具有多个节点或集群的集群,用于复杂的演示或测试,比如多集群服务网格。这些场景需要多个服务器来创建所需的集群,这也意味着需要大量的 RAM 和一个 虚拟化管理程序 来运行虚拟机。
要对多集群场景进行全面测试,您需要为每个集群创建多个节点。如果您使用虚拟机创建集群,您需要有足够的资源来运行多个虚拟机。每台机器都会有 开销,包括磁盘空间、内存和 CPU 使用率。
想象一下,如果可以仅通过容器来建立一个集群。通过使用容器代替完整的虚拟机,您可以由于减少了系统要求,从而获得运行额外节点的优势。这种方法使您可以在几分钟内使用单个命令快速创建和删除集群。此外,您还可以使用脚本来简化集群创建,甚至在一台主机上运行多个集群。
使用容器来运行 Kubernetes 集群为您提供了一个环境,这个环境对于大多数人来说,由于资源限制,用虚拟机或物理硬件部署是很困难的。幸运的是,有一个名为 KinD(Kubernetes in Docker)的工具可以实现这一目标,它允许我们在一台机器上运行 Kubernetes 集群。与 Minikube 等其他替代工具相比,KinD 更小,甚至比 K3s 更小,使其成为大多数用户在自己系统上运行的理想选择。
我们将使用 KinD 部署一个多节点集群,您将在后续章节中使用该集群来测试和部署组件,例如 Ingress 控制器、身份验证、RBAC(基于角色的访问控制)、安全策略等。
在本章中,我们将讨论以下主要主题:
-
介绍 Kubernetes 组件和对象
-
使用开发集群
-
安装 KinD
-
创建一个 KinD 集群
-
审查您的 KinD 集群
-
添加自定义负载均衡器到 Ingress
让我们开始吧!
技术要求
本章具有以下技术要求:
-
一台运行 Docker 的 Ubuntu 22.04+ 服务器,至少 4 GB 内存,建议使用 8 GB 内存
-
来自
chapter2文件夹的脚本,可以通过访问本书的 GitHub 仓库来获得:github.com/PacktPublishing/Kubernetes-An-Enterprise-Guide-Third-Edition
我们认为必须强调的是,本章将提到各种 Kubernetes 对象,其中一些可能缺乏广泛的上下文。然而,在第三章,Kubernetes Bootcamp 中,我们将深入探讨 Kubernetes 对象,并提供许多示例命令来增强您的理解。为了确保实践学习体验,我们建议在阅读 Bootcamp 章节时拥有一个集群。
本章涵盖的大多数基本 Kubernetes 主题将在后续章节中讨论,所以如果在阅读本章后某些主题变得有些模糊,不必担心!它们将在后面的章节中详细讨论。
介绍 Kubernetes 组件和对象
由于本章将讨论各种常见的 Kubernetes 对象和组件,我们已经包括了一个包含每个术语简要定义的表格。这将为您提供必要的上下文,并帮助您在阅读本章时理解术语。
在第三章,Kubernetes Bootcamp 中,我们将介绍 Kubernetes 组件以及集群中包含的基本对象集。由于我们将在本模块中使用一些基本对象,因此我们在下表中提供了一些常见的 Kubernetes 组件和资源。

图 2.1:Kubernetes 组件和对象
虽然这些仅是 Kubernetes 集群中可用的一些对象,但它们是我们将在本章中讨论的对象。了解每个资源是什么,并对其功能有基本的了解,将有助于您理解本章内容。
与集群交互
要与集群交互,您需要使用 kubectl 可执行文件。我们将在第三章,Kubernetes Bootcamp 中介绍 kubectl,但是由于在本章中我们将使用一些命令,因此我们想提供一张包含我们将使用的基本命令的表格,并解释这些选项提供的功能:

图 2.2:基本 kubectl 命令
在本章中,您将使用这些基本命令来部署我们将在本书中使用的集群部分。
接下来,我们将介绍开发集群的概念,然后重点介绍一种最流行的用于创建开发集群的工具,KinD。
使用开发集群
随着时间的推移,已经开发出多种解决方案来简化开发 Kubernetes 集群的安装,从而使管理员和开发人员能够在本地系统上进行测试。虽然这些工具在基本的 Kubernetes 测试中已证明有效,但它们通常存在某些限制,使它们在更复杂的场景下表现不佳。
以下是一些最常见的解决方案:
-
Docker Desktop
-
K3s
-
KinD
-
kubeadm
-
minikube
-
Rancher Desktop
每个解决方案都有其优点、限制和使用场景。有些解决方案只允许你使用单个节点来运行控制平面和工作节点。其他解决方案虽然支持多节点,但需要额外的资源来创建多个虚拟机。根据你的开发或测试需求,这些解决方案可能无法完全满足你的需求。
要真正深入了解 Kubernetes,你需要拥有一个至少包含一个控制平面和一个工作节点的集群。你可能想要测试一些场景,比如突然移除一个工作节点,看看工作负载如何反应;在这种情况下,你需要创建一个包含一个控制平面节点和三个工作节点的集群。为了创建这些不同类型的集群,我们可以使用 Kubernetes 特别兴趣小组(SIG)中的一个项目,叫做 KinD。
KinD(Kubernetes in Docker)提供了在单一主机上创建多个集群的能力,其中每个集群可以拥有多个控制平面和工作节点。这个特性便于进行高级测试场景,而这些场景如果使用其他解决方案的话,通常需要额外的资源分配。社区对 KinD 的反馈非常积极,从其活跃的 GitHub 社区可以看出这一点,github.com/kubernetes-sigs/kind 和专门的 Slack 频道(#kind)也证明了这一点。
虽然 KinD 是一个很好的开发集群工具,但不要将 KinD 用作生产集群,也不要将 KinD 集群暴露到互联网。尽管 KinD 集群提供了你在生产集群中所需要的大多数功能,但它并非为生产环境设计。
为什么我们选择了 KinD 来编写本书?
当我们开始编写本书时,我们的目标是将理论知识与实际动手经验相结合。KinD 成为了实现这个目标的重要工具,它使我们能够编写脚本快速创建和销毁集群。尽管其他解决方案可能提供类似的功能,但 KinD 的独特之处在于它能够在几分钟内建立多节点集群。我们希望提供一个同时包含控制平面和工作节点的集群,以模拟一个更“现实”的集群环境。为了减少硬件需求并简化 Ingress 配置,我们选择将本书中的大部分练习场景限制为单一的控制平面节点和单一的工作节点。
你们中的一些人可能会问,为什么我们不使用 kubeadm 或其他工具来部署一个具有多个节点的集群,其中包括控制平面和工作节点。正如我们所说,尽管 KinD 不是为了生产环境设计,它所需要的资源更少,能够模拟一个多节点集群,使大多数读者能够在一个表现得像标准企业级 Kubernetes 集群的集群上工作。
可以在几分钟内创建一个多节点集群,测试完成后,集群可以在几秒钟内拆除。能够快速创建和销毁集群,使得 KinD 成为本书练习的完美平台。KinD 的要求非常简单:您只需要一个运行中的 Docker 守护进程即可创建集群。这意味着它与大多数操作系统兼容,包括以下:
-
Linux
-
运行 Docker Desktop 的 macOS
-
运行 Docker Desktop 的 Windows
-
运行 WSL2 的 Windows
在撰写本文时,KinD 尚未正式支持 Chrome OS。KinD 的 Git 仓库中有许多帖子,描述了如何在运行 Chrome OS 的系统上使 KinD 工作的必要步骤;然而,这并没有得到团队的官方支持。
虽然 KinD 支持大多数操作系统,但我们选择了 Ubuntu 22.04 作为主机系统。本书中的一些练习要求文件位于特定的目录中并执行命令;选择单一的 Linux 版本有助于确保这些练习按设计运行。如果您在家中没有访问 Ubuntu 服务器的权限,您可以在像 Google Cloud Platform(GCP)这样的云服务提供商处创建计算实例。谷歌提供 $300 的信用额度,这足以运行一台 Ubuntu 服务器几个星期。您可以访问 cloud.google.com/free/ 查看 GCP 的免费选项。
最后,用于部署练习的脚本都固定了特定版本的 KinD 和其他依赖项。Kubernetes 和云原生世界发展非常迅速,我们不能保证在您阅读本书时,所有内容都能在最新版本的系统中按预期工作。
现在,让我们解释一下 KinD 如何工作以及一个基本的 KinD Kubernetes 集群是什么样子的。在我们继续创建集群之前,我们将首先在书中的练习中使用它。
使用基本的 KinD Kubernetes 集群
从更广泛的角度来看,KinD 集群可以看作是由单个 Docker 容器组成,该容器负责运行控制平面节点和工作节点,创建一个 Kubernetes 集群。为了确保简单和可靠的部署,KinD 将所有 Kubernetes 对象打包成一个统一的镜像,称为节点镜像。这个节点镜像包含了创建单节点或多节点集群所需的所有 Kubernetes 组件。
要查看在 KinD 容器中运行的内容,我们可以利用 Docker 在控制平面节点容器内执行命令,并检查进程列表。在进程列表中,您将观察到在控制平面节点上运行的标准 Kubernetes 组件。如果我们执行以下命令:docker exec cluster01-worker ps -ef。

图 2.3:显示控制平面组件的主机进程列表
如果你进入一个工作节点检查组件,你会看到所有标准的工作节点组件:

图 2.4:显示工作组件的主机进程列表
我们将在第三章、Kubernetes 启动营中介绍标准的 Kubernetes 组件,包括 kube-apiserver、kubelets、kube-proxy、kube-scheduler 和 kube-controller-manager。
除了标准的 Kubernetes 组件外,两个 KinD 节点(控制平面节点和工作节点)还包含一个额外的组件,这个组件并不是大多数标准 Kubernetes 安装的一部分,称为 Kindnet。Kindnet 是一个 容器网络接口 (CNI) 解决方案,它包含在默认的 KinD 部署中,并为 Kubernetes 集群提供网络连接。
Kubernetes CNI 是一个规范,它允许 Kubernetes 利用大量的网络软件解决方案,包括Calico、Flannel、Cilium、Kindnet等。
虽然 Kindnet 是默认的 CNI,但你可以选择停用它并选择其他 CNI,如 Calico,这是我们将为 KinD 集群使用的 CNI。虽然 Kindnet 可以完成我们需要运行的大多数任务,但它并不是一个你会在实际的 Kubernetes 集群中看到的 CNI。由于本书旨在帮助你踏上 Kubernetes 企业之旅,我们希望将 CNI 替换为像 Calico 这样更常用的 CNI。
现在你已经讨论了每个节点和 Kubernetes 组件,让我们来看一下基础 KinD 集群包含了什么。为了展示完整的集群以及正在运行的所有组件,我们可以运行 kubectl get pods --all 命令。这将列出集群上所有正在运行的组件,包括我们将在第三章、Kubernetes 启动营中讨论的基础组件。除了基础集群组件外,你可能还会注意到在名为 local-path-storage 的命名空间中运行着一个 Pod,以及一个名为 local-path-provisioner 的 Pod。这个 Pod 运行的是 KinD 附带的一个附加组件,它为集群提供了自动配置 PersistentVolumeClaims 的能力:

图 2.5:显示 local-path-provisioner 的 kubectl get pods 输出
每种开发集群选项通常提供类似的功能,满足测试部署的基本需求。这些选项通常包括一个 Kubernetes 控制平面、工作节点和一个默认的容器网络接口(CNI)以满足网络需求。虽然大多数选项都能满足这些基本需求,但有些则提供额外的功能。随着 Kubernetes 工作负载的推进,你可能会发现需要像 local-path-provisioner 这样的附加组件。在本书中,我们在各种练习中大量依赖这个组件,因为它在本书中许多示例的部署中发挥了关键作用。如果没有它,完成这些练习将变得非常困难。
为什么在开发集群中使用持久卷如此重要?这关乎你将在使用大多数企业级 Kubernetes 集群时遇到的知识。随着 Kubernetes 的发展,众多组织已经将有状态工作负载迁移到容器中,这就要求为其数据提供持久存储。具备在 KinD 集群中与存储资源互动的能力,为你提供了学习如何处理存储的机会,而这一切都无需额外资源。
本地提供者非常适合开发和测试,但不应在生产环境中使用。大多数运行 Kubernetes 的生产集群都会为开发人员提供持久存储。通常,存储将由基于块存储的存储系统、S3(简单存储服务)或NFS(网络文件系统)支持。
除了 NFS,大多数家庭实验室很少有资源来运行一个功能齐全的存储系统。local-path-provisioner通过提供本地磁盘资源,使用户能够在 KinD 集群中使用一个昂贵存储解决方案所提供的所有功能,从而消除了这一限制。
在第三章,Kubernetes 入门中,我们将讨论一些 Kubernetes 存储相关的 API 对象。我们将讨论 CSIdrivers、CSInodes 和 StorageClass 对象。这些对象被集群用来提供对后端存储系统的访问。一旦安装和配置完成,Pods 会使用 PersistentVolumes 和 PersistentVolumeClaims 对象来消费存储。存储对象非常重要,但在它们首次发布时,大多数人很难进行测试,因为它们不包含在大多数 Kubernetes 开发版本中。
KinD 认识到这一限制,并选择捆绑一个来自 Rancher Labs(现为 SUSE 一部分)的项目 local-path-provisioner,该项目是基于 Kubernetes 本地持久卷框架构建的,该框架最初在 Kubernetes 1.10 中引入。
你可能会想知道为什么需要一个附加组件,因为 Kubernetes 本身就支持本地主机持久卷。虽然 Kubernetes 已经为本地持久存储添加了支持,但 Kubernetes 并没有添加自动配置功能。虽然CNCF(Cloud Native Computing Foundation)确实提供了一个自动配置器,但它必须作为单独的 Kubernetes 组件进行安装和配置。KinD 的配置器去除了这一配置步骤,因此你可以在开发集群中轻松使用持久卷。Rancher 的项目为 KinD 提供了以下功能:
-
当创建
PersistentVolumeClaim请求时,PersistentVolumes将自动创建。 -
一个名为 standard 的默认
StorageClass。
当自动配置器看到PersistentVolumeClaim(PVC)请求命中 API 服务器时,将创建一个PersistentVolume,并且 pod 的 PVC 将与新创建的PersistentVolume绑定。此后,PVC 可以被需要持久存储的 pod 使用。
local-path-provisioner为 KinD 集群添加了一个功能,极大地扩展了你可以运行的潜在测试场景。如果没有自动配置持久磁盘的能力,测试需要持久磁盘的部署将是一项挑战。
在 Rancher 的帮助下,KinD 为你提供了解决方案,使你能够实验动态卷、存储类以及其他存储测试,这些测试在没有昂贵的家庭实验室或数据中心的情况下是无法运行的。
我们将在多个章节中使用配置器为不同的部署提供卷。了解如何在 Kubernetes 集群中使用持久存储是一个非常有用的技能,在未来的章节中,你将看到配置器的实际应用。
现在,让我们继续解释 KinD 节点镜像,它用于部署控制平面和工作节点。
了解节点镜像
节点镜像赋予了 KinD 在 Docker 容器内运行 Kubernetes 的“魔力”。这是一个令人印象深刻的成就,因为 Docker 依赖于一个运行systemd的系统以及其他大多数容器镜像中没有包含的组件。
KinD 从一个基础镜像开始,这是团队开发的一个镜像,包含了 Docker、Kubernetes 和systemd所需的一切。由于节点镜像是基于基础 Ubuntu 镜像的,团队去除了不需要的服务,并为 Docker 配置了systemd。
如果你想了解基础镜像是如何创建的,可以查看 KinD 团队在 GitHub 仓库中的 Docker 文件:github.com/kubernetes-sigs/kind/blob/main/images/base/Dockerfile。
KinD 和 Docker 网络
当使用 KinD 时,KinD 依赖 Docker 或 Red Hat 的 Podman 作为容器引擎来运行集群节点,重要的是要注意,集群通常会有与标准 Docker 容器相关的相同网络限制。虽然这些限制不会妨碍从本地主机测试 KinD Kubernetes 集群,但在尝试从网络中其他机器测试容器时,可能会引发一些复杂问题。
Podman 超出了本书的范围;它被提及为 KinD 现在支持的一个替代方案。从宏观角度来看,它是 Red Hat 提供的一个开源产品,旨在替代 Docker 作为运行时引擎。对于许多企业用例,它在安全性、无需系统守护进程等方面提供了比 Docker 更优的优势。尽管它有这些优势,但对于容器世界新手来说,它也可能带来额外的复杂性。
当你在 Docker 主机上安装 KinD 时,将会创建一个新的 Docker 桥接网络,名为 kind。这个网络配置是在 KinD v0.8.0 中引入的,解决了之前版本使用默认 Docker 桥接网络时的一些问题。大多数用户不会注意到这个变化,但了解这一点还是很重要的;当你开始创建更复杂的 KinD 集群并添加更多容器时,可能需要与 KinD 在同一个网络上运行。如果你需要在 KinD 网络上运行额外的容器,你将需要在 docker run 命令中添加 --net=kind。
除了 Docker 网络配置,Kubernetes CNI 也是我们必须考虑的因素。KinD 支持多个不同的 CNI,包括 Kindnet、Calico、Cilium 等。官方支持的唯一 CNI 是 Kindnet,但你也可以选择禁用默认的 Kindnet 安装,这样就会创建一个没有安装 CNI 的集群。集群部署完成后,你需要部署像 Calico 这样的 CNI。由于许多 Kubernetes 安装,无论是小型开发集群还是企业集群,都使用 Tigera 的 Calico 作为 CNI,因此我们选择将其作为本书练习中的 CNI。
跟踪套娃
部署像 KinD 这样的解决方案,它采用容器内容器的方式,可能会变得令人困惑。我们将其比作俄罗斯套娃的概念,一个娃娃套在另一个娃娃里,依此类推。在使用 KinD 构建自己的集群时,可能会失去对主机、Docker 和 Kubernetes 节点之间通信路径的追踪。为了保持清晰和理智,彻底了解每个容器的位置以及如何与它们交互是至关重要的。
图 2.6展示了 KinD 集群的三层架构及其网络流向。至关重要的是,要认识到每一层只能与其上面直接相邻的层进行交互,因此第三层的 KinD 容器只能与第二层运行的 Docker 镜像进行通信,而 Docker 镜像只能访问第一层操作的 Linux 主机。如果你希望在主机与 KinD 集群中运行的容器之间建立直接通信,你将需要通过 Docker 层来访问第三层的 Kubernetes 容器。
了解这一点非常重要,这样你才能有效地使用 KinD 作为测试环境:

图 2.6:KinD 网络流量
假设你打算将 Web 服务器作为示例部署到 Kubernetes 集群中。在成功部署 Ingress 控制器到 KinD 集群后,你想要使用 Chrome 在你的 Docker 主机或网络上的另一台工作站上测试该网站。然而,当你尝试通过端口80访问主机时,浏览器无法建立连接。为什么会出现这个问题?
这个失败的原因在于,Web 服务器的 Pod 在第 3 层运行,无法直接接收来自主机或网络机器的流量。要从主机访问 Web 服务器,必须将流量从 Docker 层转发到 KinD 层。具体来说,你需要为端口80和端口443启用端口转发。当一个容器通过端口规范启动时,Docker 守护进程会负责将来自主机的传入流量路由到正在运行的 Docker 容器。

图 2.7:主机通过 Ingress 控制器与 KinD 通信
通过在 Docker 容器上暴露端口80和443,Docker 守护进程现在会接受来自端口80和443的请求,从而使 NGINX Ingress 控制器能够接收流量。这之所以可行,是因为我们已经在两个地方暴露了端口80和443,首先是在 Docker 层,然后在 Kubernetes 层通过在主机上运行 NGINX 控制器,使用端口80和443。
现在,让我们来看一下这个示例的流量流向。
在主机上,你发出一个对 Web 服务器的请求,该 Web 服务器在你的 Kubernetes 集群中有一个 Ingress 规则:
-
请求会查看被请求的 IP 地址(在这种情况下,是本地 IP 地址),然后流量会发送到运行在主机上的 Docker 容器。
-
在运行 Kubernetes 节点的 Docker 容器中的 NGINX Web 服务器监听端口
80和443的 IP 地址,因此请求被接受并发送到正在运行的容器。 -
在你的 Kubernetes 集群中的 NGINX Pod 已配置为使用主机的端口
80和443,因此流量会被转发到该 Pod。 -
用户通过 NGINX Ingress 控制器从 Web 服务器接收请求的网页。
这有点让人困惑,但只要您更多地使用 KinD 并与之交互,您会发现变得越来越容易。
为了利用 KinD 集群满足您的开发需求,了解 KinD 的工作原理非常重要。到目前为止,您已经掌握了节点镜像及其在集群创建中的作用。您还熟悉了在 KinD 中,Docker 主机与运行集群的容器之间的网络流量流动。凭借这些基础知识,接下来我们将进入创建 Kubernetes 集群的第一步——安装 KinD。
安装 KinD
截至本文撰写时,KinD 的当前版本为0.22.0,支持 Kubernetes 集群版本最高到 1.30.x。
部署 KinD 所需的文件以及我们在各章中将使用的集群组件,都位于仓库中的 chapter2 文件夹下。位于 chapter2 目录根目录下的脚本 create-cluster.sh 将执行本章其余部分讨论的所有步骤。在阅读本章时,您无需执行命令;虽然可以按照步骤操作,但在执行仓库中的安装脚本之前,必须删除任何可能已经部署的 KinD 集群。
部署脚本包含了行内备注,解释每个步骤;但是,我们将在下一节中详细解释每个安装步骤。
安装 KinD – 前提条件
有多种方法可以安装 KinD,但最简单和最快的方式是下载 KinD 二进制文件以及标准的 Kubernetes kubectl 可执行文件,后者用于与集群进行交互。
安装 kubectl
由于 KinD 是一个单一的可执行文件,它并不会安装 kubectl。如果您没有安装 kubectl 并且使用的是 Ubuntu 22.04 系统,可以通过运行 snap install 安装,或者直接从 Google 下载。
要使用 snap 安装 kubectl,只需要运行一个命令:
sudo snap install kubectl --classic
若要直接从 Google 安装 kubectl,您需要下载二进制文件,授予其执行权限,并将其移动到系统路径中的某个位置。您可以按照下面的步骤完成此操作:
curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl
chmod +x ./kubectl
sudo mv ./kubectl /usr/local/bin/kubectl
看上面的 curl 命令,您可以看到初始 URL 用于查找当前的发布版本,截至本文撰写时是 v1.30.0。通过 curl 命令返回的值,我们可以从 Google 存储下载该版本。
现在您已经拥有了 kubectl,我们可以继续下载 KinD 可执行文件,以便开始创建集群。
安装 KinD 二进制文件
现在我们已经有了kubectl,接下来需要下载 KinD 二进制文件,这是一个单独的可执行文件,用于创建和删除集群。可以通过以下 URL 直接下载 KinD v0.22.0 的二进制文件:github.com/kubernetes-sigs/kind/releases/download/v0.22.0/kind-linux-amd64。create-cluster.sh脚本将下载该二进制文件,重命名为kind,将其标记为可执行文件,并将其移动到/usr/bin。若要手动下载 KinD 并将其移动到/usr/bin,可以执行以下命令,正如脚本所做的那样:
curl -Lo ./kind https://github.com/kubernetes-sigs/kind/releases/download/v0.22.0/kind-linux-amd64
chmod +x ./kind
sudo mv ./kind /usr/bin
KinD 可执行文件提供了你所需要的所有选项来维护集群的生命周期。当然,KinD 可执行文件可以创建和删除集群,但它还提供了以下功能:
-
可以创建自定义构建基础和节点镜像
-
可以导出
kubeconfig或日志文件 -
可以检索集群、节点或
kubeconfig文件 -
可以将镜像加载到节点中
在成功安装 KinD 二进制文件后,你就快要创建你的第一个 KinD 集群了。
由于在本书的一些练习中我们需要其他可执行文件,因此脚本还会下载 Helm 和 jq。若要手动下载这些工具,可以执行以下命令:
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3
chmod 700 get_helm.sh
./get_helm.sh
sudo snap install jq --classic
如果你对这些工具不熟悉,Helm是一个 Kubernetes 包管理器,旨在简化应用程序和服务的部署与管理。它简化了在集群中创建、安装和管理应用程序的过程。另一种选择是,jq允许你提取、过滤、转换和格式化来自文件、命令输出和 API 的 JSON 数据。它提供了一系列处理 JSON 数据的功能,使得数据操作和分析更加简便。
既然我们已经具备了所需的先决条件,就可以继续创建集群。然而,在创建第一个集群之前,理解 KinD 提供的各种创建选项非常重要。了解这些选项可以确保集群创建过程顺利进行。
创建一个 KinD 集群
KinD 工具提供了灵活性,可以创建单节点集群或更复杂的设置,包括多个控制平面节点和工作节点。在本节中,我们将深入探讨 KinD 可执行文件提供的各种选项。到本章结束时,你将拥有一个完全操作的双节点集群,其中包括一个控制平面节点和一个工作节点。
注意
Kubernetes 集群概念,包括控制平面和工作节点,将在下一章《第三章:Kubernetes 入门》中详细讲解。
本书中的练习将设置一个多节点集群。下一节提供的简单集群配置作为入门示例,不应在书中的练习中使用。
创建一个简单的集群
我们将在本章稍后创建一个集群,但在此之前,让我们解释一下如何使用 KinD 创建不同类型的集群。
要创建一个简单的集群,在单个容器中运行控制平面和工作节点,你只需执行带有 create cluster 选项的 KinD 可执行文件。
通过执行此命令,将会创建一个名为 kind 的集群,将所有必要的 Kubernetes 组件包含在一个 Docker 容器中。该 Docker 容器本身将被命名为 kind-control-plane。如果你更喜欢使用自定义集群名称而不是默认名称,你可以在 create cluster 命令中加入 --name <集群名称> 选项,例如 kind create cluster --name custom-cluster:
Creating cluster "kind" ...
 Ensuring node image (kindest/node:v1.30.0) 
 Preparing nodes 
 Writing configuration 
 Starting control-plane 
 Installing CNI 
 Installing StorageClass 
Set kubectl context to "kind-kind"
You can now use your cluster with:
kubectl cluster-info --context kind-kind
Have a question, bug, or feature request? Let us know! https://kind.sigs.k8s.io/#community 
create 命令将创建集群并修改 kubectl config 文件。KinD 会将新集群添加到当前的 kubectl config 文件中,并将新集群设置为默认上下文。如果你是 Kubernetes 新手并且不了解上下文的概念,它是用于访问集群和命名空间的配置文件,并包含一组凭据。
一旦集群部署完成,你可以通过使用 kubectl get nodes 命令列出节点,验证集群是否已成功创建。该命令会返回集群中运行的节点,对于一个基本的 KinD 集群来说,只有一个节点:
NAME STATUS ROLES AGE VERSION
kind-control-plane Ready control-plane,master 32m v1.30.0
部署这个单节点集群的主要目的是向你展示 KinD 如何快速创建一个可以用于测试的集群。对于我们的练习,我们希望将控制平面和工作节点分开,因此我们可以按照下一节中的步骤删除此集群。
删除集群
当你不再需要该集群时,可以使用 KinD delete cluster 命令删除它。delete 命令会快速删除集群,包括与 KinD 集群相关的任何 kubeconfig 文件中的条目。
如果你在执行删除命令时没有提供集群名称,它将只尝试删除一个名为 kind 的集群。在我们之前的示例中,我们在创建集群时没有提供集群名称,因此使用了默认名称 kind。如果你在创建时指定了集群名称,那么 delete 命令将需要 --name 选项来删除正确的集群。例如,如果我们创建了一个名为 cluster01 的集群,我们需要执行 kind delete cluster --name cluster01 来删除它。
虽然快速的单节点集群对许多用例来说很有用,但你可能希望为各种测试场景创建一个多节点集群。创建一个更复杂的集群,包含多个节点,需要你创建一个配置文件。
创建集群配置文件
在创建多节点集群时,例如创建一个带有自定义选项的双节点集群,我们需要创建一个集群配置文件。该配置文件是一个 YAML 文件,格式应该很熟悉。通过在该文件中设置值,你可以自定义 KinD 集群,包括节点数量、API 选项等。本书中用于创建集群的配置文件如下所示——它已包含在本书的代码仓库中,路径为/chapter2/cluster01-kind.yaml:
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
runtimeConfig:
"authentication.k8s.io/v1beta1": "true"
"admissionregistration.k8s.io/v1beta1": true
featureGates:
"ValidatingAdmissionPolicy": true
networking:
apiServerAddress: "0.0.0.0"
disableDefaultCNI: true
apiServerPort: 6443
podSubnet: "10.240.0.0/16"
serviceSubnet: "10.96.0.0/16"
nodes:
- role: control-plane
extraPortMappings:
- containerPort: 2379
hostPort: 2379
extraMounts:
- hostPath: /sys/kernel/security
containerPath: /sys/kernel/security
- role: worker
extraPortMappings:
- containerPort: 80
hostPort: 80
- containerPort: 443
hostPort: 443
- containerPort: 2222
hostPort: 2222
extraMounts:
- hostPath: /sys/kernel/security
containerPath: /sys/kernel/security
文件中每个自定义选项的详细信息如下表所示:
| 配置选项 | 选项详细信息 |
|---|---|
apiServerAddress |
该配置选项告诉安装程序 API 服务器将监听的 IP 地址。默认情况下,它将使用127.0.0.1,但由于我们计划从其他联网的机器访问集群,因此选择监听所有 IP 地址。 |
disableDefaultCNI |
这个设置用于启用或禁用 Kindnet 的安装。默认值是false,但由于我们计划使用 Calico 作为 CNI,因此需要将其设置为true。 |
podSubnet |
设置将被 Pods 使用的 CIDR 范围。 |
serviceSubnet |
设置将被服务使用的 CIDR 范围。 |
Nodes |
这一部分用于定义集群中的节点。对于我们的集群,我们将创建一个控制平面节点和一个工作节点。 |
- role: control-plane |
角色部分允许你为节点设置选项。第一个角色部分是用于control-plane的。 |
- role: worker |
这是第二个节点部分,允许你配置工作节点将使用的选项。由于我们将部署一个 Ingress 控制器,因此我们还添加了 NGINX pod 将使用的额外端口。 |
extraPortMappings |
要向 KinD 节点暴露端口,你需要将它们添加到配置文件的extraPortMappings部分。每个映射有两个值,一个是容器端口,另一个是主机端口。主机端口是你用来访问集群的端口,而容器端口是容器监听的端口。 |
表 2.1:KinD 配置选项
理解可用选项使你能够根据具体需求定制 KinD 集群。这包括集成高级组件如 ingress 控制器,帮助高效地将外部流量路由到集群内的服务。它还提供了在集群内部署多个节点的能力,允许你进行测试和故障/恢复操作,确保集群的弹性和稳定性。通过利用这些功能,你可以微调集群以满足应用程序和基础设施的精确需求。
现在你已经了解了如何创建一个简单的全功能容器来运行集群,并使用配置文件创建多节点集群,接下来我们来讨论一个更复杂的集群示例。
多节点集群配置
如果您只想要一个没有任何额外选项的多节点集群,您可以创建一个简单的配置文件,列出您希望在集群中拥有的节点数量和类型。以下示例 config 文件将创建一个包含三个控制平面节点和三个工作节点的集群:
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: control-plane
- role: control-plane
- role: worker
- role: worker
- role: worker
引入多个控制平面服务器会增加复杂性,因为我们的 kubectl 配置文件只能指向单个主机或 IP。为了使这个解决方案能够在所有三个控制平面节点上工作,需要在集群前面部署一个负载均衡器。这个负载均衡器将帮助将控制平面流量分配到控制平面服务器之间。需要注意的是,默认情况下,HAProxy 不会对工作节点之间的流量进行负载均衡。要对工作节点的流量进行负载均衡更为复杂,我们将在本章后面讨论这个问题。
KinD 已经考虑到了这一点,如果您部署多个控制平面节点,安装过程中将创建一个额外的容器,运行一个 HAProxy 负载均衡器。在创建多节点集群时,您将看到一些额外的行,关于配置额外的负载均衡器、加入额外的控制平面节点以及额外的工作节点——如下所示,我们使用示例集群配置创建了一个包含三个控制平面和工作节点的集群:
Creating cluster "multinode" ...
 Ensuring node image (kindest/node:v1.30.0) 
 Preparing nodes 
 Configuring the external load balancer 
 Writing configuration 
 Starting control-plane 
 Installing StorageClass 
 Joining more control-plane nodes 
 Joining worker nodes 
Set kubectl context to "kind-multinode"
You can now use your cluster with:
kubectl cluster-info --context kind-multinode
Thanks for using kind! 
在上面的输出中,您将看到一行显示 Configuring the external load balancer。这一步会部署一个负载均衡器,将传入的流量路由到 API 服务器,并分配到三个控制平面节点。
如果我们查看来自多节点控制平面配置的正在运行的容器,我们会看到六个节点容器在运行,并且有一个 HAProxy 容器:
| 容器 ID | 镜像 | 端口 | 名称 |
|---|---|---|---|
d9107c31eedb |
kindest/haproxy: haproxy:v20230606-42a2262b |
0.0.0.0:6443 |
multinode-external-load-balancer |
03a113144845 |
kindest/node:v1.30.0 |
127.0.0.1:44445->6443/tcp |
multinode-control-plane3 |
9b078ecd69b7 |
kindest/node:v1.30.0 |
multinode-worker2 |
|
b779fa15206a |
kindest/node:v1.30.0 |
multinode-worker |
|
8171baafac56 |
kindest/node:v1.30.0 |
127.0.0.1:42673->6443/tcp |
multinode-control-plane |
3ede5e163eb0 |
kindest/node:v1.30.0 |
127.0.0.1:43547->6443/tcp |
multinode-control-plane2 |
6a85afc27cfe |
kindest/node:v1.30.0 |
multinode-worker3 |
表 2.2:KinD 配置选项
由于我们只有一个 KinD 主机,每个控制平面节点和 HAProxy 容器必须在不同的端口上运行。为了允许传入请求,必须将每个容器暴露在唯一的端口上,因为只有一个进程可以绑定到一个网络端口。在这种情况下,您可以看到端口 6443 是 HAProxy 容器的分配端口。如果您检查您的 Kubernetes 配置文件,您会看到它指向 https://0.0.0.0:6443,表示分配给 HAProxy 容器的端口。
当使用 kubectl 执行命令时,它会直接发送到 HAProxy 服务器的 6443 端口。通过 KinD 在集群创建时生成的配置文件,HAProxy 容器知道如何在三个控制平面节点之间路由流量,从而为测试提供高可用的控制平面。
包含的 HAProxy 镜像是不可配置的。它仅用于处理控制平面和负载均衡控制平面 API 流量。由于这个限制,如果你想为工作节点使用负载均衡器,你需要提供自己的负载均衡器。本章稍后我们将解释如何部署第二个 HAProxy 实例,用于在多个工作节点之间进行流量负载均衡。
这种配置通常用于需要在多个工作节点之间使用入口控制器的场景。在这种情况下,需要一个负载均衡器来接受 80 和 443 端口的传入请求,并在工作节点之间分配流量,每个工作节点上都托管一个入口控制器实例。本章稍后将展示一个配置,使用自定义的 HAProxy 设置在工作节点之间进行流量负载均衡。
在创建集群时,你经常会发现需要为测试添加额外的 API 设置。在下一节中,我们将展示如何向集群添加额外选项,包括添加 OIDC 值 和启用 功能开关。
自定义控制平面和 Kubelet 选项
你可能希望超越简单的集群,测试诸如 OIDC 集成或 Kubernetes 功能开关 等特性。
OIDC 通过 OpenID Connect 协议为 Kubernetes 提供身份验证和授权,基于用户身份实现对 Kubernetes 集群的安全访问。
Kubernetes 中的 功能开关 用于启用实验性或 Alpha 级功能。它像一个切换开关,允许管理员根据需要激活或禁用 Kubernetes 中的特定功能。
这需要你修改组件的启动选项,比如 API 服务器。KinD 使用与 kubeadm 安装相同的配置,使你能够添加任何所需的可选参数。举个例子,如果你想将集群与 OIDC 提供者集成,你可以将所需的选项添加到配置补丁部分:
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
kubeadmConfigPatches:
- |
kind: ClusterConfiguration
metadata:
name: config
apiServer:
extraArgs:
oidc-issuer-url: "https://oidc.testdomain.com/auth/idp/k8sIdp"
oidc-client-id: "kubernetes"
oidc-username-claim: sub
oidc-client-id: kubernetes
oidc-ca-file: /etc/oidc/ca.crt
nodes:
- role: control-plane
- role: control-plane
- role: control-plane
- role: worker
- role: worker
- role: worker
这只是部署 KinD 集群时可以进行的定制化的一小部分示例。有关可用配置选项的完整列表,请查看 Kubernetes 网站上的 使用 kubeadm 自定义控制平面配置 页面:kubernetes.io/docs/setup/production-environment/tools/kubeadm/control-plane-flags/。
现在你已经创建了集群文件,我们继续讲解如何使用配置文件来创建你的 KinD 集群。
创建一个自定义的 KinD 集群
最后!现在你已经熟悉了 KinD,我们可以继续创建我们的集群。
我们需要创建一个可控的已知环境,因此我们将为集群命名并提供集群配置文件。
在开始之前,请确保你在 chapter2 目录中的克隆仓库内。你将使用我们提供的脚本 create-cluster.sh 来创建整个集群。
这个脚本将使用名为 cluster01-kind.yaml 的配置文件创建一个集群,集群名为 cluster01,并包含一个控制平面和工作节点,在工作节点上暴露 80 和 443 端口供我们的 ingress 控制器使用。
我们没有为每一步提供详细步骤,而是记录了脚本本身。你可以通过查看脚本的源代码来了解每一步的作用。以下是脚本执行的步骤的高级概述:
-
下载 KinD v 0.22.0 二进制文件,将其设为可执行文件,并移动到
/usr/bin。 -
下载
kubectl,将其设为可执行文件,并移动到/usr/bin。 -
下载 Helm 安装脚本并执行它。
-
安装
jq。 -
执行 KinD 来创建集群,使用配置文件并声明要使用的镜像(这样做是为了避免新版本发布和我们的章节脚本之间的任何问题)。
-
为工作节点标记 Ingress。
-
使用
chapter2/calico中的两个清单文件custom-resources.yaml和tigera-operator.yaml来部署 Calico。 -
使用
chapter2/nginx-ingress目录中的nginx-deploy.yaml清单部署 NGINX Ingress。
我们在步骤 7 和 8 中使用的清单是来自 Calico 和 NGINX-Ingress 项目的标准部署清单。我们将它们存储在仓库中,以加快部署速度,并避免在部署更新时出现与 KinD 集群不兼容的问题。
恭喜!现在你已经有了一个完全正常运行的、包含 Calico 和 Ingress 控制器的两节点 Kubernetes 集群。
审查你的 KinD 集群
现在你已经拥有一个完全功能的 Kubernetes 集群,我们可以深入探讨几个关键的 Kubernetes 对象,特别是存储对象。在下一章 Kubernetes Bootcamp 中,我们将深入了解 Kubernetes 集群中可用的其他对象。尽管那一章会探索集群中可用的大量对象,但在此时介绍与存储相关的对象非常重要,因为 KinD 提供了存储功能。
在本节中,我们将熟悉无缝集成在 KinD 中的存储对象。这些专用存储对象扩展了集群中工作负载的持久存储能力,确保数据持久性和弹性。通过熟悉这些存储对象,我们为在 Kubernetes 生态系统中实现无缝数据管理奠定了基础。
KinD 存储对象
请记住,KinD 包括 Rancher 的自动提供程序,为集群提供自动化持久磁盘管理。Kubernetes 具有多个存储对象,但自动提供程序不需要一个对象,因为它使用基本的 Kubernetes 特性:CSIdriver 对象。由于能够使用本地主机路径作为 PVC 的功能是 Kubernetes 的一部分,因此我们在 KinD 集群中不会看到任何用于本地存储的 CSIdriver 对象。
我们 KinD 集群中的第一个对象是 CSInodes。任何能运行工作负载的节点都会有一个 CSInode 对象。在我们的 KinD 集群中,两个节点都有一个 CSInode 对象,您可以通过执行 kubectl get csinodes 来验证:
NAME DRIVERS AGE
cluster01-control-plane 0 20m
cluster01-worker 0 20m
如果我们使用 kubectl describe csinodes <节点名称> 描述其中一个节点,您将看到对象的详细信息:
Name: cluster01-worker
Labels: <none>
Annotations: storage.alpha.kubernetes.io/migrated-plugins:
kubernetes.io/aws-ebs,kubernetes.io/azure-disk,kubernetes.io/azure-file,kubernetes.io/cinder,kubernetes.io/gce-pd,kubernetes.io/vsphere-vo...
CreationTimestamp: Tue, 20 Jun 2023 16:45:54 +0000
Spec:
Drivers:
csi.tigera.io:
Node ID: cluster01-worker
Events: <none>
需要指出的主要内容是输出的 Spec 部分。这部分列出了可能安装以支持后端存储系统的任何驱动程序的详细信息。在驱动程序部分,您将看到一个名为 csi.tigera.io 的驱动程序条目,这是在安装 Calico 时部署的。此驱动程序用于 Calico,以启用 Calico 的 Felix(处理网络策略执行)与 Dikastes(管理 Kubernetes 网络策略转换和执行的 Pod)之间的安全连接,通过挂载共享卷实现。
需要注意的是,此驱动程序不适用于标准 Kubernetes 部署的持久存储。
由于本地提供程序不需要驱动程序,因此我们在集群中看不到用于本地存储的额外驱动程序。
存储驱动程序
在 Kubernetes 中,存储驱动程序在处理容器化应用程序与底层存储基础设施之间的通信中发挥着重要作用。其主要功能是控制为部署在 Kubernetes 集群中的应用程序提供存储资源的配置、附加和管理。
如前所述,您的 KinD 集群对于本地提供程序不需要任何额外的存储驱动程序,但我们为 Calico 的通信提供了一个驱动程序。如果执行 kubectl get csidrivers,您将在列表中看到 csi.tigera.io。
KinD 存储类
要附加到任何集群提供的存储,集群需要一个 StorageClass 对象。Rancher 的提供者创建了一个名为 standard 的默认存储类。它还将该类设置为默认的 StorageClass,因此你不需要在 PVC 请求中提供 StorageClass 名称。如果没有设置默认的 StorageClass,每个 PVC 请求都需要在请求中指定 StorageClass 名称。如果没有启用默认类,且 PVC 请求未能设置 StorageClass 名称,PVC 分配将失败,因为 API 服务器将无法将请求与 StorageClass 关联。
在生产集群中,建议避免设置默认的 StorageClass。这种做法有助于防止部署忘记指定存储类,而默认存储系统不符合部署要求时可能出现的问题。这类问题可能仅在生产环境中变得关键时才会显现,从而影响业务收入和声誉。通过不分配默认类,开发人员将在遇到 PVC 请求失败时识别问题,从而在对业务产生负面影响之前发现并解决问题。此外,这种方法还鼓励开发人员明确选择符合所需性能要求的 StorageClass,使他们能够为非关键系统使用经济实惠的存储,或为关键工作负载选择高速存储。
要列出集群中的存储类,执行 kubectl get storageclasses,或者使用 sc 来代替 storageclasses:
NAME ATTACHREQUIRED PODINFOOwhoNT STORAGECAPACITY
csi.tigera.io true true false
现在你已经了解了 Kubernetes 用于存储的对象,接下来让我们学习如何使用提供者。
使用 KinD 的存储提供者
使用内置的提供者非常简单。由于它可以自动配置存储并被设置为默认类,所有进入的 PVC 请求都会被配置的 Pod 看到,接着它会创建 PersistentVolume 和 PersistentVolumeClaim。
为了展示这个过程,让我们一步步走过必要的步骤。以下是运行 kubectl get pv 和 kubectl get pvc 在基本 KinD 集群中的输出:
kubectl get pv
No resources found
PVs 不是命名空间对象,这意味着它们是集群级资源,因此我们不需要在命令中添加命名空间选项。PVCs 是命名空间对象,因此当我们告诉 Kubernetes 显示可用的 PV 时,需要通过 kubectl get pvc 命令指定命名空间。由于这是一个新的集群,且没有默认的工作负载需要持久磁盘,因此当前没有 PV 或 PVC 对象。
如果没有自动提供者,我们需要在 PVC 能够声明卷之前创建 PV。由于我们的集群中运行着 Rancher 提供者,我们可以通过部署一个带有 PVC 请求的 Pod 来测试创建过程,如下所示,我们将其命名为 pvctest.yaml:
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: test-claim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Mi
这个 PVC 将被命名为 test-claim,位于默认命名空间中,因为我们没有提供命名空间,其卷大小设置为 1 MB。同样,我们确实需要包括 StorageClass 选项,因为 KinD 为集群设置了默认的 StorageClass。
要生成 PVC,我们可以通过执行 kubectl 命令来创建,使用 create 命令以及 pvctest.yaml 文件,命令为 kubectl create -f pvctest.yaml。Kubernetes 会通过确认 PVC 的创建来回应。然而,重要的是要理解,这一确认并不保证 PVC 完整功能的实现。虽然 PVC 对象本身已成功创建,但 PVC 请求中的某些依赖项可能不正确或缺失。在这种情况下,尽管对象已经创建,PVC 请求本身可能不会被履行,甚至可能失败。
创建 PVC 后,你可以通过两种方式之一检查其实际状态。第一种是简单的 get 命令,也就是 kubectl get pvc。由于我们的请求位于默认命名空间中,我无需在 get 命令中包含命名空间值(注意,我们必须缩短卷名称以使其适应页面):
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
test-claim Bound pvc-b6ecf50… 1Mi RWO standard 15s
我们知道,通过提交 PVC 清单创建了 PVC 对象,但我们并没有创建 PV 请求。如果现在查看 PV,我们可以看到一个 PV 已从我们的 PVC 请求中创建。同样,为了将输出缩短到一行,我们也缩短了 PV 的名称:
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM
pvc-b6ecf… 1Mi RWO Delete Bound default/test-claim
鉴于越来越多的工作负载依赖持久磁盘,了解 Kubernetes 工作负载如何与存储系统集成变得尤为重要。在前一部分中,你已经了解了 KinD 如何通过自动配置器增强集群。在 第三章,Kubernetes Bootcamp 中,我们将进一步加深对这些 Kubernetes 存储对象的理解。
在下一部分,我们将讨论使用负载均衡器与我们的 KinD 集群配合,以启用高可用集群的复杂话题,使用 HAproxy。
为 Ingress 添加自定义负载均衡器
我们添加了这一部分,以帮助那些可能想了解如何在多个工作节点之间进行负载均衡的人。
本节讨论了一个复杂的主题,涉及添加一个自定义的 HAProxy 容器,你可以用它来为 KinD 集群中的工作节点进行负载均衡。你不应该在我们将在剩余章节中使用的 KinD 集群上部署这个负载均衡器。
由于你将在大多数企业环境中与负载均衡器交互,我们希望添加一节内容,讲解如何为工作节点配置自己的 HAProxy 容器,以便在三个 KinD 节点之间进行负载均衡。
首先,我们不会在本书的任何章节中使用这个配置。我们希望让每个人都能进行练习,因此为了限制所需的资源,我们将始终使用本章中之前创建的两节点集群。如果你想使用负载均衡器测试 KinD 节点,建议使用不同的 Docker 主机,或者等到你完成本书并删除 KinD 集群后再进行测试。
创建 KinD 集群配置
我们提供了一个名为create-multinode.sh的脚本,位于chapter2/HAdemo目录中,它将为控制平面和工作节点创建一个包含三个节点的集群。该脚本将创建一个名为multimode的集群,这意味着控制平面节点将命名为multinode-control-plane、multinode-control-plane2和multinode-control-plane3,而工作节点将命名为multinode-worker、multinode-worker2和multinode-worker3。
由于我们将使用一个暴露在80和443端口上的 HAProxy 容器,你不需要在KinD配置文件中暴露任何工作节点端口。我们在脚本中使用的配置文件如下所示:
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
apiServerAddress: "0.0.0.0"
disableDefaultCNI: true
apiServerPort: 6443
podSubnet: "10.240.0.0/16"
serviceSubnet: "10.96.0.0/16"
nodes:
- role: control-plane
- role: control-plane
- role: control-plane
- role: worker
- role: worker
- role: worker
请注意,我们没有在任何端口上暴露工作节点以供入口使用,因此无需直接在节点上暴露端口。一旦我们部署了 HAProxy 容器,它将暴露在80和443端口上,并且由于它与 KinD 集群位于同一主机上,HAProxy 容器将能够使用 Docker 网络与节点进行通信。
到目前为止,你应该已经有了一个正常工作的多节点集群,其中包含一个负载均衡器用于 API 服务器,另一个负载均衡器用于工作节点。一个工作节点将运行 NGINX 入口控制器,但它可以是任意一个节点,那么 HAProxy 服务器是如何知道哪个节点在运行 NGINX 的呢?它会对所有节点进行健康检查,任何返回 200(成功连接)的节点都在运行 NGINX,并会被添加到后端服务器列表中。
在下一节中,我们将解释 HAProxy 用来控制后端服务器以及执行健康检查的配置文件。
HAProxy 配置文件
HAProxy 在 Docker Hub 上提供了一个容器,易于部署,只需一个配置文件即可启动容器。
要创建配置文件,你需要知道集群中每个工作节点的 IP 地址。包含的创建集群并部署 HAProxy 的脚本将为你找到这些信息,创建配置文件并启动 HAProxy 容器。
由于配置 HAProxy 对许多人来说可能不太熟悉,我们将对脚本进行详细解析,解释我们配置的主要部分。该脚本通过查询工作节点容器的 IP 地址为我们创建文件:
global log /dev/log local0 log /dev/log local1 notice daemon
defaults log global mode tcp timeout connect 5000 timeout client 50000 timeout server 50000
frontend workers_https bind *:443 mode tcp use_backend ingress_https backend ingress_https option httpchk GET /healthz mode tcp server worker 172.18.0.8:443 check port 80 server worker2 172.18.0.7:443 check port 80 server worker3 172.18.0.4:443 check port 80
frontend stats
bind *:8404
mode http
stats enable
stats uri /
stats refresh 10s
frontend workers_http bind *:80 use_backend ingress_http backend ingress_http mode http option httpchk GET /healthz server worker 172.18.0.8:80 check port 80 server worker2 172.18.0.7:80 check port 80 server worker3 172.18.0.4:80 check port 80
前端部分是配置的关键。这部分告诉 HAProxy 绑定的端口以及用于后端流量的服务器组。让我们来看看第一个前端条目:
frontend workers_https
bind *:443
mode tcp
use_backend ingress_https
这将一个名为workers_https的前端绑定到 TCP 端口443。最后一行use_backend告诉 HAProxy 哪个服务器组将接收端口443上的流量。
接下来,我们声明后端服务器,或运行所需端口或 URL 工作负载的节点集合。第一个后端部分包含属于workers_https组的服务器。
backend ingress_https
option httpchk GET /healthz
mode tcp
server worker 172.18.0.8:443 check port 443
server worker2 172.18.0.7:443 check port 443
server worker3 172.18.0.4:443 check port 443
第一行包含规则的名称;在我们的示例中,我们将规则命名为ingress-https。选项httpchk告诉 HAProxy 如何对每个后端服务器进行健康检查。如果检查成功,HAProxy 将把它添加为健康的后端目标。如果检查失败,服务器将不会将流量转发到失败的节点。最后,我们提供服务器列表;每个端点都有自己的一行,从服务器名称开始,后面跟着名称、IP 地址和需要检查的端口——在我们的示例中是端口443。
你可以使用类似的配置块为任何其他希望 HAProxy 进行负载均衡的端口配置。在我们的脚本中,我们将 HAProxy 配置为监听 TCP 端口80和443,并使用相同的后端服务器。
我们还添加了一个后端部分,以暴露 HAProxy 的状态页面。状态页面必须通过 HTTP 暴露,并且运行在端口8404上。由于状态页面是 Docker 容器的一部分,因此不需要将其转发到任何服务器组。我们只需将其添加到配置文件中,在执行 HAProxy 容器时,需要为端口8404添加端口映射(你将在下一个段落描述的docker run命令中看到)。我们将在下一节中展示状态页面以及如何使用它。
最后的步骤是启动一个运行 HAProxy 的 Docker 容器,并使用我们创建的配置文件,该文件包含三个工作节点,这些节点暴露在 Docker 主机的端口80和443上,并连接到 Docker 中的 KinD 网络:
# Start the HAProxy Container for the Worker Nodes
docker run --name HAProxy-workers-lb --network $KIND_NETWORK -d -p 8404:8404 -p 80:80 -p 443:443 -v ~/HAProxy:/usr/local/etc/HAProxy:ro haproxy -f /usr/local/etc/HAProxy/HAProxy.cfg
现在你已经学习了如何为你的工作节点创建和部署自定义的 HAProxy 负载均衡器,接下来我们来看看 HAProxy 通信是如何工作的。
理解 HAProxy 流量流向
集群将总共运行八个容器。其中六个容器将是标准的 Kubernetes 组件——即三个控制平面服务器和三个工作节点。其他两个容器分别是 KinD 的 HAProxy 服务器和你自己的自定义 HAProxy 容器(由于格式问题,docker ps的输出已被简化):
| 容器 | ID | 名称 |
|---|---|---|
3d876a9f8f02 |
Haproxy |
HAProxy-workers-lb |
183e86be2be3 |
kindest/node:v1.30.1 |
multinode-worker3 |
ce2d2174a2ba |
kindest/haproxy:v20230606-42a2262b |
multinode-external-load-balancer |
697b2c2bef68 |
kindest/node:v1.30.1 |
multinode-control-plane |
f3938a66a097 |
kindest/node:v1.30.1 |
multinode-worker2 |
43372928d2f2 |
kindest/node:v1.30.1 |
multinode-control-plane2 |
baa450f8fe56 |
kindest/node:v1.30.1 |
multinode-worker |
ee4234ff4333 |
kindest/node:v1.30.1 |
multinode-control-plane3 |
表 2.3:集群中运行的八个容器
名为 HAProxy-workers-lb 的容器通过主机的 80 和 443 端口进行暴露。这意味着任何访问主机 80 或 443 端口的请求都会被定向到自定义的 HAProxy 容器,然后由它将流量发送到 Ingress 控制器。
默认的 NGINX Ingress 部署仅有一个副本,这意味着控制器仅运行在单个节点上,但它可以随时迁移到其他节点之一。让我们使用前一部分提到的 HAProxy 状态页面来查看 Ingress 控制器正在运行在哪个节点上。通过浏览器,我们需要指向 Docker 主机的 IP 地址,端口是 8404。在我们的示例中,主机地址是 192.168.149.129,因此在浏览器中输入 http://192.168.149.129:8404,这将打开 HAProxy 状态页面,类似于下方 图 2.8 中展示的内容:

图 2.8:HAProxy 状态页面
在状态页面的详细信息中,您将看到我们在 HAProxy 配置中创建的后端以及每个服务的状态,包括哪个工作节点在运行 Ingress 控制器。为了更详细地说明这一点,让我们关注传入的 SSL 流量的详细信息。在状态页面上,我们将重点关注 ingress_https 部分,如 图 2.9 所示。

图 2.9:HAProxy HTTPS 状态
在 HAProxy 配置中,我们创建了一个名为 ingress_https 的后端,它包含了集群中的所有工作节点。由于我们只有一个副本在运行控制器,只有一个节点会运行 Ingress 控制器。在节点列表中,您会看到有两个节点处于 DOWN 状态,而 worker2 节点处于 UP 状态。DOWN 状态是预期的,因为任何未运行 Ingress 控制器副本的节点,其 HTTPS 健康检查都会失败。
虽然我们在生产环境中通常会运行至少三个副本,但由于我们只有三个节点,我们希望展示当 Ingress 控制器 Pod 从活动节点迁移到新节点时,HAProxy 如何更新后端服务。因此,我们将模拟一个节点故障,以证明 HAProxy 为我们的 NGINX Ingress 控制器提供了高可用性。
模拟 kubelet 故障
在我们的示例中,我们想证明 HAProxy 为 NGINX 提供高可用性支持。为了模拟故障,我们可以停止某个节点上的 kubelet 服务,这将通知 kube-apisever,使其不再在该节点上调度任何额外的 Pod。我们知道运行中的容器在 worker2 上,因此我们希望关闭该节点。
停止 kubelet 最简单的方法是向容器发送 docker exec 命令:
docker exec multinode-worker2 systemctl stop kubelet
您将不会看到该命令的任何输出,但如果您等待几分钟,让集群接收到更新后的节点状态,您可以通过查看节点列表来验证节点是否已关闭:
kubectl get nodes
您将收到以下输出:
NAME STATUS ROLES AGE VERSION
multinode-control-plane Ready control-plane 29m v1.30.0
multinode-control-plane2 Ready control-plane 29m v1.30.0
multinode-control-plane3 Ready control-plane 29m v1.30.0
multinode-worker Ready <none> 28m v1.30.0
multinode-worker2 NotReady <none> 28m v1.30.0
multinode-worker3 Ready <none> 28m v1.30.0
这验证了我们刚刚模拟了kubelet故障,并且worker2现在处于NotReady状态。
在kubelet“故障”发生之前运行的任何 Pods 将继续运行,但kube-scheduler将不会在该节点上调度任何工作负载,直到kubelet问题解决。由于我们知道 Pod 不会在该节点上重新启动,我们可以删除该 Pod,以便它可以重新调度到另一个节点上。
你需要获取 Pod 名称,然后删除它以强制重启:
kubectl get pods -n ingress-nginx
这将返回命名空间中的 Pods,例如以下内容:
nginx-ingress-controller-7d6bf88c86-r7ztq
使用kubectl删除 Ingress 控制器 Pod:
kubectl delete pod nginx-ingress-controller-7d6bf88c86-r7ztq -n ingress-nginx
这将强制调度器在另一个工作节点上启动该容器。它还将导致 HAProxy 容器更新后端列表,因为 NGINX 控制器已迁移到另一个工作节点。
为了验证这一点,我们可以查看 HAproxy 状态页面,你会看到活动节点已经更改为worker3。由于我们模拟的故障发生在worker2上,当我们终止 Pod 时,Kubernetes 会将该 Pod 重新调度到另一个健康的节点上。

图 2.10:HAproxy 后端节点更新
如果你计划使用这个 HA 集群进行额外的测试,你将需要重新启动multinode-worker2上的 kubelet。
如果你计划删除 HA 集群,只需运行 KinD 集群删除命令,所有节点将被删除。由于我们将集群命名为multinode,你可以运行以下命令来删除 KinD 集群:
kind delete cluster –name multinode
你还需要删除我们为工作节点部署的 HAProxy 容器,因为我们是通过 Docker 运行该容器的,而它并不是由 KinD 部署创建的。要清理工作节点的 HAProxy 部署,请执行以下命令:
docker stop HAProxy-workers-lb && docker rm HAProxy-workers-lb
本章已完成!我们在本章中提到了很多不同的Kubernetes服务,但我们仅仅触及了集群中对象的表面。在下一部分中,我们将进行一个训练营,介绍构成集群的组件,并概述集群中的基础对象。
总结
本章提供了 KinD 项目的概述,这是一个 Kubernetes SIG 项目。我们介绍了在 KinD 集群中安装可选组件的过程,例如用于 CNI 的 Calico 和用于 Ingress 控制的 NGINX。此外,我们还探索了 KinD 集群中包含的 Kubernetes 存储对象。
现在你应该理解 KinD 能为你和你的组织带来的潜在好处。它提供了一种用户友好且高度可定制的 Kubernetes 集群部署方式,并且单个主机上的集群数量仅受限于可用的主机资源。
在下一章节,我们将深入探讨 Kubernetes 对象。我们将下一章称为Kubernetes 训练营,因为它将涵盖大多数基本的 Kubernetes 对象以及每个对象的用途。下一章可以视为“Kubernetes 便携指南”,它包含了 Kubernetes 对象的快速参考及其功能,以及使用它们的时机。
这是一个紧凑的章节,旨在为有 Kubernetes 经验的读者提供快速复习;或者,它是为 Kubernetes 新手设计的速成课程。我们编写本书的意图是超越 Kubernetes 的基本对象,因为市面上已经有许多书籍很好地介绍了 Kubernetes 的基础知识。
问题
-
在创建
PersistentVolumeClaim之前必须创建什么对象?-
PVC
-
一个磁盘
-
PersistentVolume -
VirtualDisk
-
答案:c
-
KinD 包括一个动态磁盘供应器。哪个公司创建了这个供应器?
-
微软
-
CNCF
-
VMware
-
Rancher
-
答案:d
-
如果你创建了一个包含多个工作节点的 KinD 集群,你会安装什么来将流量引导到每个节点?
-
负载均衡器
-
代理服务器
-
没有
-
副本数设置为 3
-
答案:a
-
对还是错?一个 Kubernetes 集群只能安装一个 CSIdriver。
-
正确
-
错误
-
答案:b
第三章:Kubernetes 训练营
上一章介绍了如何使用 KinD(Kubernetes in Docker)部署 Kubernetes 集群,这对于在单台机器上使用容器而非虚拟机创建开发集群非常有用。此方法减少了系统资源的需求,并简化了整个设置过程。我们介绍了 KinD 的安装和配置,如何创建集群,包括 Ingress 控制器、Calico 作为 CNI 以及如何使用持久存储。
我们理解,很多人已经具备 Kubernetes 的使用经验,无论是运行生产环境中的集群,还是尝试使用kubeadm、minikube 或 Docker Desktop 等工具。本书的目的是超越 Kubernetes 的基础知识,因此我们不打算重复介绍所有的基础内容。相反,我们在本章中提供了一个针对新手或接触 Kubernetes 较少的人的训练营。
在本章中,我们将探讨 Kubernetes 集群的基本组件,包括控制平面和工作节点。我们将详细解释每个 Kubernetes 资源及其相应的使用场景。如果你以前有 Kubernetes 经验,并且能够熟练使用kubectl,同时了解像DaemonSets、StatefulSets和ReplicaSets这样的 Kubernetes 资源,那么本章将作为一个有益的复习,帮助你为进入第四章做准备,在该章节中我们将深入讨论服务、负载均衡、外部 DNS、全局负载均衡以及K8GB。
由于这是一个训练营章节,我们不会对每个话题进行详细探讨。然而,在本章结束时,你应该能够扎实地理解 Kubernetes 的基础概念,这对于理解接下来的章节至关重要。即使你已经有了 Kubernetes 的扎实基础,本章也能作为一次复习,为接下来深入讨论更高级的主题做好准备。本章将涉及以下内容:
-
Kubernetes 组件概述
-
探索控制平面
-
理解工作节点组件
-
与 API 服务器交互
-
介绍 Kubernetes 资源
到本章结束时,你将对最常用的集群资源有扎实的理解。了解 Kubernetes 资源对集群操作员和集群管理员都非常重要。
技术要求
本章没有技术要求。
如果你在学习资源时想执行命令,可以使用上一章中部署的 KinD 集群。
Kubernetes 组件概述
理解基础设施中系统组件的构成对于高效提供服务至关重要。在当今众多安装选项中,许多 Kubernetes 用户可能没有意识到完全理解不同 Kubernetes 组件的集成是必要的。
几年前,建立 Kubernetes 集群需要手动安装和配置每个组件。这个过程具有陡峭的学习曲线,并且常常会导致挫败感。因此,许多人和组织得出结论:“Kubernetes 过于复杂。”然而,手动安装的好处在于它能提供关于各个组件之间交互的深入理解。如果在安装后集群出现问题,你会清楚知道该从哪里开始调查。
要理解 Kubernetes 组件如何协同工作,首先必须了解 Kubernetes 集群的不同组件。
下图来自Kubernetes.io网站,展示了 Kubernetes 集群组件的高级概览:

图 3.1:Kubernetes 集群组件
如你所见,Kubernetes 集群由多个组件组成。在本章接下来的内容中,我们将讨论这些组件以及它们在 Kubernetes 集群中的作用。
探索控制平面
如其名称所示,控制平面对集群的各个方面具有控制权。如果控制平面无法正常工作,集群将失去调度工作负载、创建新部署和管理 Kubernetes 对象的能力。鉴于控制平面的重要性,强烈建议在部署时支持高可用性(HA),并至少部署三个控制平面节点。许多生产环境甚至使用三个以上的控制平面节点,但关键原则是保持奇数个节点,这样即使丢失一个etcd节点,我们也能保持高可用的控制平面。
现在,让我们深入探讨控制平面的重要性及其组件,全面理解它们在集群中至关重要的作用。
Kubernetes API 服务器
在集群中要理解的第一个组件是kube-apiserver组件。由于 Kubernetes 是应用程序编程接口(API)驱动的,所有进入集群的请求都会通过 API 服务器。让我们来看一个简单的get nodes请求,使用控制平面的 IP 地址,通过 API 端点发送请求。在企业环境中,控制平面通常会通过负载均衡器来前置。在我们的示例中,负载均衡器有一个指向三个控制平面节点的条目,IP 为10.240.100.100:
10.240.100.100:6443/api/v1/nodes?limit=500
如果你尝试在没有任何凭证的情况下进行 API 调用,你将收到权限拒绝的请求。直接使用纯 API 请求是非常常见的做法,尤其是在创建应用程序部署管道或 Kubernetes 附加组件时。然而,用户与 Kubernetes 交互的最常见方式是使用kubectl工具。
每个使用kubectl发出的命令在幕后调用一个 API 端点。在前面的示例中,如果我们执行了kubectl get nodes命令,将向kube-apiserver进程发送一个 API 请求,使用地址10.240.100.100和端口6443。
API 调用请求了/api/vi/nodes端点,返回了集群中节点的列表:
NAME STATUS ROLES AGE VERSION
home-k8s-control-plane Ready control-plane,master 45d v1.27.3
home-k8s-control-plane2 Ready control-plane,master 45d v1.27.3
home-k8s-control-plane3 Ready control-plane,master 45d v1.27.3
home-k8s-worker Ready worker 45d v1.27.3
home-k8s-worker2 Ready worker 45d v1.27.3
home-k8s-worker3 Ready worker 45d v1.27.3
在没有正常运行的 API 服务器的情况下,所有发送到集群的请求都将失败。因此,确保kube-apiserver的持续运行和健康状态至关重要。
通过运行三个或更多的控制平面节点,可以最小化由于控制平面节点丢失可能造成的任何潜在影响。
请记住,从上一章可以得知,当运行多个控制平面节点时,需要在集群的 API 服务器前面使用负载均衡器。Kubernetes API 服务器可以由大多数标准解决方案(包括 F5、HAProxy 和 Seesaw)提供支持。
etcd 数据库
把etcd描述为你的 Kubernetes 集群的基础并不为过。etcd作为一个强大而高效的分布式键值数据库,Kubernetes 依赖它来存储所有集群数据。集群中的每个资源都与etcd数据库中的特定键关联。如果你可以访问托管etcd的节点或 Pod,你可以使用etcdctl可执行文件来探索数据库中存储的所有键。下面提供的代码片段展示了从基于 KinD 的集群中提取的示例:
etcdctl-3.5.12 --endpoints=https://127.0.0.1:2379 --cacert=/etc/kubernetes/pki/etcd/ca.crt --key=/etc/kubernetes/pki/etcd/server.key --cert=/etc/kubernetes/pki/etcd/server.crt get / --prefix --keys-only
前面命令的输出包含太多数据,无法在本章节中全部列出。一个基本的 KinD 集群将返回大约 314 个条目。
所有键都以/registry/<resource>开头。例如,返回的键之一是ClusterRole为cluster-admin的键,如下所示:/registry/clusterrolebindings/cluster-admin。
我们可以使用键名通过略微修改之前的命令来使用etcdctl实用程序检索值,如下所示:
etcdctl-3.5.12 --endpoints=https://127.0.0.1:2379 --cacert=/etc/kubernetes/pki/etcd/ca.crt --key=/etc/kubernetes/pki/etcd/server.key --cert=/etc/kubernetes/pki/etcd/server.crt get /registry/clusterrolebindings/cluster-admin
输出将包含无法由你的 Shell 解释的字符,但你将了解到存储在etcd中的数据。对于cluster-admin键,输出向我们展示了以下内容:
/registry/clusterrolebindings/cluster-admin
k8s
2
rbac.authorization.k8s.io/v1ClusterRoleBinding▒
▒
cluster-admin"*$7b235e81-52de-4001-b354-994dad0279ee2▒▒▒▒Z,
rbac-defaultsb3otstrapping
+rbac.authorization.kubernetes.io/autoupdatetrue▒▒
kube-apiserverUpdaterbac.authorization.k8s.io/v▒▒▒▒FieldsV1:▒
▒{"f:metadata":{"f:annotations":{".":{},"f:rbac.authorization.kubernetes.io/autoupdate":{}},"f:labels":{".":{},"f:kubernetes.io/bootstrapping":{}}},"f:roleRef":{},"f:subjects":{}}B4
Grouprbac.authorization.k8s.iosystem:masters"7
rbac.authorization.k8s.io
cluster-admin" ClusterRole
我们描述了etcd中的条目,以便理解 Kubernetes 如何存储和利用数据来管理集群对象。虽然你已经观察到了cluster-admin键的直接数据库输出,但在典型情况下,你会使用kubectl get clusterrolebinding cluster-admin -o yaml命令来查询 API 服务器获取相同的数据。使用kubectl,该命令将返回以下信息:

图 3.2:kubectl ClusterRoleBinding 输出
如果你查看 kubectl 命令的输出,并与 etcdctl 查询的输出进行比较,你会发现两者信息一致。你很少需要直接与 etcd 进行交互;你只需要执行 kubectl 命令,请求会发送到 API 服务器,后者再查询 etcd 数据库以获取资源信息。
值得注意的是,虽然 etcd 是 Kubernetes 中最常用的后端数据库,但它并不是唯一的。最初为边缘使用场景简化 Kubernetes 的 k3s 项目将 etcd 替换为关系型数据库。当我们深入了解使用 k3s 的 vclusters 时,我们会看到它使用的是 SQLite 而非 etcd。
kube-scheduler
正如其名称所示,kube-scheduler 组件负责监督将 Pod 分配到节点的过程。它的主要任务是持续监控那些尚未分配到任何特定节点的 Pod。调度器随后评估每个 Pod 的资源需求,以确定最合适的放置位置。这个评估考虑了多个因素,包括节点资源的可用性、约束条件、选择器以及亲和性/反亲和性规则。满足这些要求的节点被视为可行节点。最终,调度器从符合条件的节点列表中选择最合适的节点来调度 Pod。
kube-controller-manager
Kubernetes 控制器管理器 是 Kubernetes 控制平面的核心控制系统;它负责管理和协调其他控制器,这些控制器处理集群中的特定任务。
控制器管理器包含多个控制器,每个控制器负责集群中某个特定功能。这些控制器持续监控集群的当前状态,并动态调整以维持所需的配置。
所有控制器都包含在一个可执行文件中,减少了复杂性和管理工作。部分包含的控制器如 表 3.1 所示。
每个控制器为集群提供独特的功能,以下是各个控制器及其功能:
| 控制器 | 职责 |
|---|---|
| Endpoints | 监控新服务并为具有匹配标签的 Pod 创建端点 |
| Namespace | 监控命名空间的操作 |
| Node | 监控集群中节点的状态,检测节点故障或新增节点,并采取适当的行动以维持所需的节点数量 |
| Replication | 监控 Pod 的副本,采取行动以删除或添加 Pod 以达到所需状态 |
| Service Accounts | 监控服务账户 |
表 3.1:控制器及其功能
每个控制器运行一个非终止(永不停歇)的控制循环。这些控制循环监控每个资源的状态,并根据需要进行更改,以使资源的状态恢复正常。例如,如果你需要将一个部署从一个节点扩展到三个节点,复制控制器会发现当前状态是运行了一个 Pod,而期望的状态是有三个 Pod 运行。为了将当前状态移动到期望状态,复制控制器会请求再增加两个 Pod。
cloud-controller-manager
这是一个你可能没有接触过的组件,具体取决于你的集群配置。与 kube-controller-manager 组件类似,这个控制器包含四个控制器,在一个二进制文件中运行。
云控制器提供与特定云提供商的 Kubernetes 服务相关的集成功能,使得可以利用云特定的功能,如负载均衡器、持久存储、自动扩展组等其他特性。
理解工作节点组件
工作节点,顾名思义,负责在 Kubernetes 集群中执行任务。在我们之前讨论控制平面中 kube-scheduler 组件时,我们强调了当一个新的 Pod 需要调度时,kube-scheduler 会选择合适的节点来执行该任务。kube-scheduler 依赖工作节点提供的数据来做出这一决定。这些数据会定期更新,以确保 Pod 在集群中的分布,使集群资源得到充分利用。
每个工作节点都有两个主要组件,kubelet 和 kube-proxy。
kubelet
你可能听到工作节点被称为 kubelet。kubelet 是一个在所有工作节点上运行的代理,负责确保容器在节点上运行并保持健康。
kube-proxy
与名称相反,kube-proxy 根本不是一个代理服务器(尽管它在 Kubernetes 的最初版本中是一个代理服务器)。
根据集群中部署的 CNI,你的节点可能会有也可能没有 kube-proxy 组件。像 Cilium 这样的 CNI 可以与 kube-proxy 一起运行,或者在 kube-proxyless 模式下运行。在我们的 KinD 集群中,我们部署了 Calico,它依赖 kube-proxy 的存在。
当 kube-proxy 被部署时,它的主要目的是管理集群中 Pods 和服务的网络连接,为目标 Pod(s) 提供网络流量路由。
容器运行时
每个节点还需要一个容器运行时。容器运行时负责运行容器。你可能首先想到的是 Docker,虽然 Docker 是一个容器运行时,但它并不是唯一的运行时选项。近年来,其他选项已经取代了 Docker,成为集群中首选的容器运行时。
最突出的两种 Docker 替代品是 CRI-O 和 containerd。在撰写本章时,KinD 仅官方支持 Docker 和 Red Hat 的 Podman。
与 API 服务器进行交互
正如我们之前提到的,你可以通过直接的 API 请求或使用 kubectl 工具与 API 服务器进行交互。虽然我们在本书中主要集中使用 kubectl 进行交互,但在适用的地方我们也会提到如何使用直接的 API 调用。
使用 Kubernetes 的 kubectl 工具
kubectl 是一个单一的可执行文件,它允许你通过 命令行界面(CLI)与 Kubernetes API 进行交互。它支持大多数主流操作系统和架构,包括 Linux、Windows 和 macOS。
注意:我们已经使用 KinD 脚本安装了 kubectl,该脚本创建了我们的集群,详见 第二章。大多数操作系统的安装说明可以在 Kubernetes 网站上找到,网址为 kubernetes.io/docs/tasks/tools/install-kubectl/。由于我们使用 Linux 作为本书练习的操作系统,接下来将介绍如何在 Linux 机器上安装 kubectl。请按照以下步骤操作:
-
要下载最新版本的
kubectl,你可以运行一个curl命令来下载它,如下所示:curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl -
下载后,你需要运行以下命令使该文件变为可执行文件:
chmod +x ./kubectl -
最后,我们将把可执行文件移动到系统路径中,如下所示:
sudo mv ./kubectl /usr/local/bin/kubectl
现在,你的系统上已经安装了最新版本的 kubectl 工具,并且可以从任何工作目录执行 kubectl 命令。
Kubernetes 大约每 4 个月更新一次。这包括对基础 Kubernetes 集群组件和 kubectl 工具的升级。你可能会遇到集群与 kubectl 命令版本不匹配的情况,这时你需要升级或下载新的 kubectl 可执行文件。你可以通过运行 kubectl version 命令来检查两个版本,这个命令会输出 API 服务器和 kubectl 客户端的版本信息。版本检查的输出如下所示——请注意,你的输出可能与我们的示例输出不同:
Client Version: v1.30.0-6+43a0480e94cee1
Kustomize Version: v5.0.4-0.20230601165947-6ce0bf390ce3
Server Version: v1.30.0-6+43a0480e94cee1
从输出结果中可以看到,kubectl 客户端正在运行 1.30.0 版本,集群则运行 1.30.0 版本。两者之间的微小版本差异不会造成任何问题。事实上,官方支持的版本差异最多为一个主版本号。因此,如果你的客户端版本为 1.29,而集群版本为 1.30.0,你仍然在支持的版本差异范围内。尽管这可能是被支持的,但如果你试图使用更高版本中包含的新命令或资源,可能会遇到一些问题。通常情况下,你应该尽量保持集群和客户端版本的一致性,以避免任何问题。
在本章的其余部分,我们将讨论 Kubernetes 资源以及如何与 API 服务器交互来管理每个资源。但在深入了解不同的资源之前,我们想提到一个经常被忽视的 kubectl 工具选项:verbose 选项。
理解详细选项
当你执行 kubectl 命令时,默认情况下你看到的唯一输出是对你的命令的任何直接响应。如果你查看 kube-system 命名空间中的所有 Pod,你将得到一个所有 Pod 的列表。在大多数情况下,这是所期望的输出,但如果你发出 get Pods 请求并收到 API 服务器的错误,怎么办?你如何获取更多信息来找出导致错误的原因?
通过在 kubectl 命令中添加 verbose 选项,你可以获取有关 API 调用本身以及 API 服务器的任何回复的附加信息。通常,API 服务器的回复会包含一些额外的信息,这些信息可能对找到问题的根本原因很有帮助。
verbose 选项有多个级别,范围从 0 到 9,数字越高,输出的内容就越多。
以下截图来自 Kubernetes 官方网站,详细说明了每个级别及其输出内容:

图 3.3:详细程度描述
你可以通过向任何 kubectl 命令添加 -v 或 --v 选项来实验不同的级别。
一般 kubectl 命令
CLI 允许你以命令式和声明式两种方式与 Kubernetes 进行交互。使用命令式命令意味着你告诉 Kubernetes 做什么——例如,kubectl run nginx --image nginx。这告诉 API 服务器创建一个名为 nginx 的 Pod,并运行一个名为 nginx 的镜像。虽然命令式命令在开发、快速修复或测试中很有用,但在生产环境中你将更多使用声明式命令。在声明式命令中,你告诉 Kubernetes 你想要什么。要使用声明式命令,你需要向 API 服务器发送一个声明性清单,该清单使用 JavaScript 对象表示法 (JSON) 或 YAML 不是标记语言 (YAML) 编写,声明你希望 Kubernetes 创建的内容。
kubectl 包含可以提供一般集群信息或有关资源信息的命令和选项。下表包含了一些命令及其用途的小抄。我们将在未来的章节中使用这些命令,因此你将在本书中看到它们的实际应用:
| 集群命令 |
|---|
api-resources |
api-versions |
cluster-info |
| 对象命令 |
get <object> |
describe <object> |
logs <pod name> |
edit <object> |
delete <object> |
label <object> |
annotate <object> |
run |
表 3.2:集群和对象命令
通过理解每个 Kubernetes 组件以及如何使用命令式命令与 API 服务器交互,我们现在可以继续了解 Kubernetes 资源以及如何使用 kubectl 来管理它们。
介绍 Kubernetes 资源
在这一部分,我们将提供大量的信息。然而,由于这是一个训练营,我们不会深入讨论每个资源的详尽细节。值得注意的是,每个资源都可能需要单独一章,甚至是一本书的多个章节。由于许多关于基础 Kubernetes 的书籍已经广泛讨论了这些资源,我们将集中讨论每个资源的基本理解所必需的核心内容。在接下来的章节中,我们将随着通过书中练习扩展集群,补充更多关于资源的细节。
在深入全面理解 Kubernetes 资源之前,让我们先介绍一下 Kubernetes 清单的概念。
Kubernetes 清单
我们将用来创建 Kubernetes 资源的文件被称为清单。清单可以使用 YAML 或 JSON 创建——大多数清单使用 YAML,这也是我们在本书中使用的格式。
需要注意的是,在使用 YAML 文件时,kubectl 会将所有 YAML 转换为 JSON 来与您的 API 服务器交互。所有 API 调用都是使用 JSON,即使清单是用 YAML 编写的。
清单的内容会根据将要创建的资源或多个资源而有所不同。最基本的,所有清单都需要包含一个基础配置,其中包括 apiVersion、资源的 kind 和 metadata 字段,如下所示:
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: grafana
name: grafana
namespace: monitoring
上面的清单本身并不完整;我们仅展示了一个完整 Deployment 清单的开头。如您所见,文件中开始部分包含了所有清单必须具备的三个字段:apiVersion、kind 和 metadata 字段。
您可能还会注意到文件中的字段格式。YAML 格式非常严格,如果任何一行的格式错位了哪怕一个空格,您在尝试部署清单时会收到错误。这需要时间来适应,即使是创建清单很长时间的人,格式问题也时常会出现。
什么是 Kubernetes 资源?
当你想要添加或删除集群中的某个资源时,你实际上是在与 Kubernetes 资源进行交互。这种交互是你声明所需资源状态的方式,可能是创建、删除或扩展资源。根据期望的状态,API 服务器会确保当前状态与期望状态一致。例如,如果你有一个从单个副本开始的部署,你可以将该部署资源的副本数从 1 更改为 3。当 API 服务器发现当前状态为 1 时,它会通过创建额外的 2 个 Pod 将部署扩展到 3 个副本。
要获取集群支持的资源列表,可以使用kubectl api-resources命令。API 服务器将返回一个资源列表,其中包括所有有效的短名称、命名空间支持以及支持的 API 组。
一个 Kubernetes 集群大约包含 58 个基础资源,但在生产集群中,通常会有超过 58 个资源。许多附加组件,如 Calico,会通过新对象扩展 Kubernetes API。随着集群中部署了不同的附加组件,别对 100+个资源的列表感到惊讶。
显示了以下最常见资源的简要列表:
| NAME | SHORT NAMES | API VERSION | NAMESPACED |
|---|---|---|---|
| apiservices | apiregistration.k8s.io/v1 | FALSE | |
| certificatesigningrequests | Csr | certificates.k8s.io/v1 | FALSE |
| clusterrolebindings | rbac.authorization.k8s.io/v1 | FALSE | |
| clusterroles | rbac.authorization.k8s.io/v1 | FALSE | |
| componentstatuses | Cs | v1 | FALSE |
| configmaps | Cm | v1 | TRUE |
| controllerrevisions | apps/v1 | TRUE | |
| cronjobs | Cj | batch/v1 | TRUE |
| csidrivers | storage.k8s.io/v1 | FALSE | |
| csinodes | storage.k8s.io/v1 | FALSE | |
| csistoragecapacities | storage.k8s.io/v1 | TRUE | |
| customresourcedefinitions | crd,crds | apiextensions.k8s.io/v1 | FALSE |
| daemonsets | Ds | apps/v1 | TRUE |
| deployments | Deploy | apps/v1 | TRUE |
| endpoints | Ep | v1 | TRUE |
| endpointslices | discovery.k8s.io/v1 | TRUE | |
| events | Ev | v1 | TRUE |
| events | Ev | events.k8s.io/v1 | TRUE |
| flowschemas | flowcontrol.apiserver.k8s.io/v1beta3 | FALSE | |
| horizontalpodautoscalers | Hpa | autoscaling/v2 | TRUE |
| ingressclasses | networking.k8s.io/v1 | FALSE | |
| ingresses | Ing | networking.k8s.io/v1 | TRUE |
| jobs | batch/v1 | TRUE | |
| limitranges | Limits | v1 | TRUE |
| localsubjectaccessreviews | authorization.k8s.io/v1 | TRUE | |
| mutatingwebhookconfigurations | admissionregistration.k8s.io/v1 | FALSE | |
| namespaces | Ns | v1 | FALSE |
| networkpolicies | Netpol | networking.k8s.io/v1 | TRUE |
| nodes | No | v1 | FALSE |
| persistentvolumeclaims | Pvc | v1 | TRUE |
| persistentvolumes | pv | v1 | FALSE |
| poddisruptionbudgets | pdb | policy/v1 | TRUE |
| pods | po | v1 | TRUE |
| podtemplates | v1 | TRUE | |
| priorityclasses | pc | scheduling.k8s.io/v1 | FALSE |
| prioritylevelconfigurations | flowcontrol.apiserver.k8s.io/v1beta3 | FALSE | |
| profiles | projectcalico.org/v3 | FALSE | |
| replicasets | rs | apps/v1 | TRUE |
| replicationcontrollers | rc | v1 | TRUE |
| resourcequotas | quota | v1 | TRUE |
| rolebindings | rbac.authorization.k8s.io/v1 | TRUE | |
| roles | rbac.authorization.k8s.io/v1 | TRUE | |
| runtimeclasses | node.k8s.io/v1 | FALSE | |
| secrets | v1 | TRUE | |
| selfsubjectaccessreviews | authorization.k8s.io/v1 | FALSE | |
| selfsubjectrulesreviews | authorization.k8s.io/v1 | FALSE | |
| serviceaccounts | sa | v1 | TRUE |
| services | svc | v1 | TRUE |
| statefulsets | sts | apps/v1 | TRUE |
| storageclasses | sc | storage.k8s.io/v1 | FALSE |
| subjectaccessreviews | authorization.k8s.io/v1 | FALSE | |
| tokenreviews | authentication.k8s.io/v1 | FALSE | |
| validatingwebhookconfigurations | admissionregistration.k8s.io/v1 | FALSE | |
| volumeattachments | storage.k8s.io/v1 | FALSE |
表 3.3: Kubernetes API 资源
由于本章作为入门训练营,我们将简要概述表 3.3中列出的资源。为了有效理解后续章节,您需要对这些对象及其各自的功能有深入了解。
一些资源将在后续章节中更详细地解释,包括 Ingress、RoleBindings、ClusterRoles、StorageClasses 等。
审查 Kubernetes 资源
集群中的大多数资源运行在命名空间中,要创建、编辑或读取它们,您应当在任何kubectl命令中提供-n <namespace>选项。要查找接受命名空间选项的资源列表,您可以参考表 3.3的输出。如果某个资源可以通过命名空间进行引用,则NAMESPACED列会显示TRUE。如果资源仅能通过集群级别进行引用,则NAMESPACED列会显示FALSE。
Apiservices
Apiservices 提供 Kubernetes 组件与所有外部资源(如用户、应用程序和其他服务)之间通信和交互的主要入口点。它们提供一组端点,允许用户和应用程序执行各种操作,例如创建、更新和删除 Kubernetes 资源(如 Pods、Deployments、Services 和 Namespaces)。
Apiservices 处理请求的身份验证、授权和验证,只有经过授权的用户和应用程序才能访问或修改资源。它们还处理资源版本控制和 Kubernetes 集群的其他关键方面。
它们还可以通过开发自定义控制器、操作器或其他与 API Services 交互的组件来扩展 Kubernetes 功能,从而管理和自动化集群行为的各个方面。一个例子是我们的 CNI 插件 Calico,它为集群添加了 31 个额外的 api-resources。
CertificateSigningRequests
证书签名请求(CSR)允许你向证书授权机构请求证书。这些请求通常用于获得可信证书,以保障 Kubernetes 集群内的通信安全。
ClusterRoles
ClusterRole是权限的集合,它使得与集群 API 的交互成为可能。它将操作或动词与 API 组配对,定义特定的权限。例如,如果你想限制持续集成/持续交付(CI/CD)管道只能执行补丁操作以更新镜像标签,你可以使用类似于以下的ClusterRole:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: patch-deployment
rules:
- apiGroups: ["apps/v1"]
resources: ["deployments"]
verbs: ["get", "list", "patch"]
ClusterRole可以应用于集群和命名空间级别的 API。
ClusterRoleBindings
一旦你指定了ClusterRole,下一步是通过ClusterRoleBinding创建ClusterRole与主体之间的关联。这个绑定将ClusterRole与用户、组或服务账户关联,授予它们在ClusterRole中定义的权限。
我们将在第七章,RBAC 策略和审计中详细探讨ClusterRoleBinding。
ComponentStatus
Kubernetes 控制平面是集群的关键组成部分,对于集群的正常运行至关重要。ComponentStatus是一个对象,展示不同 Kubernetes 控制平面组件的健康状况和状态。它提供了组件整体健康的指示,显示组件是否正常运行或是否存在错误。
ConfigMaps
ConfigMap 是一种存储键值对数据的资源,使配置与应用程序分离。ConfigMaps可以保存各种类型的数据,包括文字值、文件或目录,从而在管理应用程序配置时提供灵活性。
这里是一个命令式示例:
kubectl create configmap <name> <data>
<data>选项将根据ConfigMap的来源而有所不同。要使用文件或目录,你需要提供--from-file选项,并指定文件路径或整个目录,如下所示:
kubectl create configmap config-test --from-file=/apps/nginx-config/nginx.conf
这将创建一个新的名为config-test的ConfigMap,其中nginx.conf键包含nginx.conf文件的内容作为值。
如果你需要在一个ConfigMap中添加多个键,可以将每个文件放入一个目录,并使用该目录中的所有文件创建ConfigMap。例如,你在~/config/myapp目录中有三个文件,分别是config1、config2和config3。每个文件都包含数据。为了创建一个将每个文件添加为键的ConfigMap,你需要提供--from-file选项,并指向该目录,如下所示:
kubectl create configmap config-test --from-file=/apps~/config/myapp
这将创建一个新的ConfigMap,包含三个键值config1、config2和config3。每个键的值将等于目录中每个文件的内容。
为了快速显示一个 ConfigMap,使用上述示例,我们可以通过 get 命令 kubectl get configmaps config-test 来检索 它,并得到以下输出:
NAME DATA AGE
config-test 3 7s
ConfigMap 包含三个键,DATA 列下有数字 3 表示这一点。为了更详细地检查,我们可以使用带有额外“-o yaml”选项的 kubectl get 命令,附加到 kubectl get configmaps config-test 命令中。这将显示每个键的值,以 YAML 格式表示,如下所示:
apiVersion: v1
data:
config1: |
First Configmap Value
config2: |
Yet Another Value from File2
config3: |
The last file - Config 3
kind: ConfigMap
metadata:
creationTimestamp: "2023-06-10T01:38:51Z"
name: config-test
namespace: default
resourceVersion: "6712"
uid: a744d772-3845-4631-930c-e5661d476717
通过检查输出,显示每个 ConfigMap 中的键与目录中的文件名相对应——config1、config2 和 config3。每个键保留从其相应文件中的数据获得的值。
ConfigMaps 的一个限制是,数据对于任何拥有资源访问权限的人来说都很容易访问。从前面的输出可以看出,一个简单的 get 命令就能以明文显示数据。
由于这个设计,你永远不应该在 ConfigMap 中存储敏感信息,如密码。在本节后面,我们将介绍一个专门用于存储秘密数据的资源,叫做 Secret。
ControllerRevisions
ControllerRevision 类似于控制器设置的特定版本或更新的快照。它主要由特定控制器使用,比如 StatefulSet 控制器,用来跟踪和管理随着时间推移对其配置所做的更改。
每当有控制器管理的资源的配置被修改或更新时,都会创建一个新的 ControllerRevision。每个修订版包含控制器的期望设置和修订号。这些修订版存储在 Kubernetes API 服务器中,允许你在需要时参考或恢复它们。
CronJobs
如果你曾使用过 Linux 的 cronjobs,那么你已经知道 Kubernetes 的 CronJob 资源是什么。如果你没有 Linux 背景,cronjob 用于创建一个计划任务。举个例子,如果你是 Windows 用户,它类似于 Windows 的计划任务。
以下代码片段显示了一个创建 CronJob 的示例清单:
apiVersion: batch/v1
kind: CronJob
metadata:
name: hello-world
spec:
schedule: "*/1 * * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: hello-world
image: busybox
args:
- /bin/sh
- -c
- date; echo Hello World!
restartPolicy: OnFailure
schedule 格式遵循标准的 cron 格式。从左到右,每个 * 代表以下内容:
-
分钟(0–59)
-
小时(0–23)
-
天(1–31)
-
月(1–12)
-
星期几(0–6)(星期天 = 0,星期六 = 6)
CronJob 接受步骤值,这允许你创建一个可以每分钟、每 2 分钟或每小时执行的计划。
我们的示例清单将创建一个 CronJob,每分钟运行一个名为 hello-world 的镜像,并在 Pod 日志中输出 Hello World!。
CSI 驱动程序
Kubernetes 使用 CsiDriver 资源将节点连接到存储系统。
你可以通过执行 kubectl get csidriver 命令列出集群中所有可用的 CSI 驱动程序。在我们的一个实验室集群中,我们使用 NetApp 的 SolidFire 存储,因此我们的集群已安装 Trident CSI 驱动程序,如下所示:
NAME CREATED AT
csi.trident.netapp.io 2019-09-04T19:10:47Z
CSI 节点
为了避免将存储信息存储在节点的 API 资源中,CSINode 资源被添加到 API 服务器中,以存储由 CSI 驱动程序生成的信息。存储的信息包括将 Kubernetes 节点名称映射到 CSI 节点名称、CSI 驱动程序的可用性和存储卷拓扑。
CSIStorageCapacities
CSIStorageCapacity 是一个存储有关给定 CSI 存储容量的组件,表示给定 StorageClass 的可用存储容量。当 Kubernetes 决定在哪里创建新的 PersistentVolumes 时,使用这些信息。
CustomResourceDefinitions
CustomResourceDefinition(CRD)是一种让用户在 Kubernetes 集群中创建自定义资源的方式。
它概述了自定义资源的结构、格式和行为,包括其 API 端点和支持的操作。一旦 CRD 被创建并添加到集群中,它就成为一个内建的资源类型,可以使用常规的 Kubernetes 工具和 API 来管理。
DaemonSets
DaemonSet 使得在集群中的每个节点或特定节点集上部署 pod 成为可能。它通常用于部署诸如日志记录等在集群每个节点上都需要的关键组件。一旦设置了 DaemonSet,它会自动在每个现有节点上创建一个 pod。
此外,随着新节点加入集群,DaemonSet 会确保在新加入的节点上部署一个 pod。
Deployments
我们之前提到过,你不应该直接部署一个 pod。这样做的一个原因是,你无法对 pod 进行扩缩容或执行滚动升级。Deployments 为你提供了许多优势,包括以声明方式管理升级以及能够回滚到先前的修订版本。创建一个 Deployment 实际上是一个三步过程,由 API 服务器执行:首先创建一个 Deployment,然后创建一个 ReplicaSet,接着该 ReplicaSet 会为应用程序创建 pod(s)。
即使你不打算对应用程序进行扩缩容或执行滚动升级,默认情况下你仍然应该使用 Deployments,以便将来能够利用其功能。
Endpoints
Endpoint 将一个服务映射到一个或多个 pod。当我们解释 Service 资源时,这将更容易理解。现在,你只需要知道,你可以使用 CLI 执行 kubectl get endpoints 命令来检索端点。在一个新的 KinD 集群中,你将看到默认命名空间中的 Kubernetes API 服务器值,如下的代码片段所示:
NAMESPACE NAME ENDPOINTS AGE
default Kubernetes 172.17.0.2:6443 22h
输出显示,集群中有一个名为 kubernetes 的服务,其端点位于 互联网协议 (IP) 地址 172.17.0.2 的 6443 端口。我们示例中返回的 IP 是分配给 Docker 控制平面容器的地址。
稍后,你将看到如何通过查看端点来解决服务和入口问题。
EndPointSlices
Endpoints 的扩展性较差——它们将所有端点存储在一个单一的资源中。当处理一个小规模的部署时,可能只有几个 Pod,这时不会出现问题。然而,随着集群的扩展和应用程序的规模增长,端点数量也会增加,这会影响控制平面的性能并造成额外的网络流量,因为端点会发生变化。
EndPointSlices 旨在处理更大规模的场景,这些场景需要可扩展性并对网络端点进行精确控制。默认情况下,每个 EndPointSlice 最多可以容纳 100 个端点,可以通过向 kube-controller-manager 添加 --max-endpoints-per-slice 选项来增加该数量。
假设你在 Kubernetes 中部署了一个包含大量 Pod 的服务。如果其中一个 Pod 被删除,Kubernetes 只会更新包含该 Pod 信息的特定切片。当更新后的切片分发到集群时,它将只包含更小子集的 Pod 详细信息。通过这种方式,集群网络保持高效,避免因过多数据而被淹没。
Events
Events 资源将显示命名空间中的所有事件。要获取 kube-system 命名空间的事件列表,可以使用 kubectl get events -n kube-system 命令。
FlowSchemas
Kubernetes 集群具有预定义的设置来管理并发请求的处理,以确保流量不会超载 API 服务器。然而,你可以灵活地自定义并配置自己的流量模式和请求的优先级级别,以便根据你的具体要求和工作负载调整 API 服务器的行为。
例如,你有一个命名空间,其中部署了一个重要的应用程序。你可以创建一个高优先级的 FlowSchema,这样 API 服务器就会先处理该命名空间的请求,再处理其他请求。
HorizontalPodAutoscalers
在 Kubernetes 集群上运行工作负载的最大优势之一就是能够自动扩展你的 Pods。虽然你可以使用 kubectl 命令或通过编辑清单的副本数量来进行扩展,但这些都不是自动化的,需要手动干预。
水平 Pod 自动扩展器 (HPAs) 提供了根据一组标准扩展应用程序的能力。使用 CPU 和内存使用率或你自己定义的自定义指标,你可以设置规则,当你需要更多的 Pod 来维持服务水平时,就会扩展 Pod 数量。
在冷却期过后,Kubernetes 将根据策略将应用程序的 Pod 数量缩减到最小值。
要快速为 NGINX 部署创建一个 HPA,我们可以执行一个 kubectl 命令,并使用 autoscale 选项,具体如下:
kubectl autoscale deployment nginx --cpu-percent=50 --min=1 --max=5
你还可以创建一个 Kubernetes 清单来创建你的 HPA。使用与 CLI 中相同的选项,我们的清单看起来是这样的:
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
name: nginx-deployment
spec:
maxReplicas: 5
minReplicas: 1
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: nginx-deployment
targetCPUUtilizationPercentage: 50
两个选项都将创建一个 HPA,当 Deployment 的 CPU 利用率达到 50% 时,nginx-deployment 会扩展到 5 个副本。一旦 Deployment 使用量降到 50% 以下,并且冷却期已达到(默认情况下为 5 分钟),副本数量将减少到 1。
IngressClasses
IngressClasses 允许你定义和管理各种类型的 Ingress 控制器。它们提供了根据特定需求个性化和调整这些控制器行为的能力,提供可定制的传入流量路由到服务的功能。IngressClasses 使你能够管理和精细调整 Ingress 控制器,确保流量以符合你需求的方式进行处理。
IngressClass 最重要的作用是让你在单一集群中定义多个 Ingress 控制器。例如,Kubernetes Dashboard 版本 3 使用特定的 IngressClass,确保其 Ingress 对象绑定到没有 LoadBalancer 的 NGINX 实例,这样它就不能从集群外部访问。你也可以使用这个功能将 Ingress 控制器连接到不同的网络。
Ingress
Ingress 资源是一个工具,允许你为传入的 HTTP 和 HTTPS 流量创建规则,通过主机名、路径或请求头等选项来服务。它充当外部流量与集群中运行的服务之间的中介。通过使用 Ingress,你可以定义不同类型的流量应该如何路由到特定的服务,从而对传入请求的流量进行细粒度的控制。
我们将在下一章深入讨论 Ingress,但快速描述 Ingress 提供的功能是,它允许你使用指定的 URL 将你的应用程序暴露到外部世界。
Jobs
Jobs 允许你执行一定数量的 Pod 或 Pod 执行。与 CronJob 资源不同,这些 Pod 不会按固定计划运行,而是会在创建时执行一次。
LimitRanges
我们将在本章后面讨论Quota资源,但LimitRange是一种配置,允许你为指定命名空间内的 Pod 和容器建立并强制执行资源分配的特定边界和限制。通过使用LimitRanges,你可以定义资源限制,例如 CPU、内存和存储,确保 Pod 和容器高效运行,并防止对整个集群环境造成负面影响。
LocalSubjectAccessReview
LocalSubjectAccessReview是一个功能,帮助你检查集群中某个用户或用户组是否具备在本地资源上执行特定操作所需的权限。它使你能够直接在集群内审查访问权限,而无需依赖外部 API 请求。
使用LocalSubjectAccessReview,你可以指定用户或用户组的身份以及你希望评估的操作和资源。然后,Kubernetes API 服务器将根据本地访问控制策略验证权限,并响应请求的操作是允许还是拒绝。
变更 Webhook 配置(MutatingWebhookConfiguration)
MutatingWebhookConfiguration用于创建能够拦截并修改发送到 API 服务器请求的 webhook,提供了一种自动修改请求资源的方式。
MutatingWebhookConfiguration包含一组规则,决定哪些请求应该被拦截并由 Webhook 处理。当请求与定义的规则匹配时,MutatingWebhookConfiguration会触发相应的 webhook,这些 webhook 可以修改请求的数据包。修改内容可以包括添加、删除或修改正在创建或更新的资源中的字段和注释。
命名空间(Namespaces)
Namespace是一个资源,用于将集群划分为逻辑单元。每个Namespace允许对资源进行细粒度的管理,包括权限、配额和报告。
Namespace资源用于命名空间任务,这是集群级别的操作。使用namespace资源,你可以执行包括create、delete、edit和get等命令。
命令的语法是kubectl <verb> ns <namespace name>。
例如,要描述kube-system命名空间,我们将执行kubectl describe namespaces kube-system命令。
这将返回该命名空间的信息,包括任何标签、注释和分配的配额,如下面的代码片段所示:
Name: kube-system
Labels: <none>
Annotations: <none>
Status: Active
No resource quota.
No LimitRange
resource.
在前面的输出中,你可以看到该命名空间没有分配任何标签、注释或资源配额。
本节仅旨在介绍命名空间的概念,作为多租户集群中的管理单元。如果你计划运行具有多个租户的集群,你需要了解如何使用命名空间来保护集群。
网络策略(NetworkPolicies)
NetworkPolicy 资源允许你定义网络流量的进出方式(即入站流量和出站流量)。它们使你能够使用 Kubernetes 的本地构造定义哪些 Pod 可以与其他 Pod 通信。如果你曾经在 Amazon Web Services(AWS)中使用过安全组来限制两个系统组之间的访问,那么它的概念类似。举个例子,以下策略将允许来自任何具有 app.kubernetes.io/name: ingress-nginx 标签的命名空间的流量访问 myns 命名空间中的端口 443 的 Pod(这是 nginx-ingress 命名空间的默认标签):
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-from-ingress
namespace: myns
spec:
PodSelector: {}
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
app.kubernetes.io/name: ingress-nginx
ports:
- protocol: TCP
port: 443
NetworkPolicy 是你可以用来保护集群的另一个资源。它们应该在所有生产集群中使用,但在多租户集群中,应该被视为必需的,以确保集群中每个命名空间的安全。
Nodes
nodes 资源是一个集群级别的资源,用于与集群的节点进行交互。此资源可以与各种操作一起使用,包括 get、describe、label 和 annotate。
要使用 kubectl 获取集群中所有节点的列表,你需要执行 kubectl get nodes 命令。在一个新创建的 KinD 集群中运行一个简单的单节点集群时,显示内容如下:
NAME STATUS ROLES AGE VERSION
kind-control-plane Ready master 22h v1.30.0
你还可以使用节点资源通过 describe 命令获取单个节点的详细信息。要获取先前列出的 KinD 节点描述,我们可以执行 kubectl describe node kind-control-plane,这将返回节点的详细信息,包括消耗的资源、运行的 Pod、IP 无类域间路由(CIDR)范围等。
PersistentVolumeClaims
PVC 是一个命名空间资源,供 Pod 使用以消耗持久存储。PVC 使用持久卷(PV)来映射实际的存储资源,这些资源可以位于任何支持的存储系统上,包括NFS和iSCSI。
与我们讨论的大多数资源一样,你可以对 PVC 资源执行 get、describe 和 delete 命令。由于这些是由命名空间中的 Pod 使用的,因此 PVC 必须在与将使用该 PVC 的 Pod 相同的命名空间中创建。
PersistentVolumes
PV 被 PVC 使用来创建 PVC 与底层存储系统之间的链接。手动维护 PV 是一项繁琐的手动任务,应避免进行。相反,Kubernetes 包括通过容器存储接口(CSI)管理大多数常见存储系统的能力。
大多数在企业集群中使用的 CSI 解决方案提供自动配置支持,正如我们在第二章中介绍 Rancher 的本地配置器时所讨论的那样。支持自动配置的解决方案可以消除手动创建 PV 所需的管理工作负担,自动处理 PV 的创建和与 PVC 的映射。
PodDisruptionBudgets
PodDisruptionBudget(PDB)是一种资源,用于限制在任何给定时间内不可用的 Pod 的最大数量。其目的是防止多个 Pod 同时终止,导致服务中断或故障。通过定义最小可用 Pod 数量,称为 "minAvailable" 参数,你可以确保在维护或其他中断事件期间,特定数量的 Pod 保持可用。
在云环境中,kube-scheduler 将使用这些信息来确定如何在升级过程中替换节点。使用 PodDisruptionBudget 时需要小心,因为你可能会遇到升级被暂停的情况。
Pods
Pod 资源用于与运行容器的 Pod 进行交互。使用 kubectl 工具,你可以使用诸如 get、delete 和 describe 等命令。例如,如果你想列出 kube-system 命名空间中的所有 Pod,可以执行 kubectl get Pods -n kube-system 命令,它会返回该命名空间中的所有 Pod,如下所示:
NAME READY STATUS RESTARTS AGE
calico-kube-controllers-c6c8dc655-vnrt7 1/1 Running 0 15m
calico-node-4d9px 1/1 Running 0 15m
calico-node-r4zsj 1/1 Running 0 15m
coredns-558bd4d5db-8mxzp 1/1 Running 0 15m
coredns-558bd4d5db-fxnkt 1/1 Running 0 15m
etcd-cluster01-control-plane 1/1 Running 0 15m
kube-apiserver-cluster01-control-plane 1/1 Running 0 15m
kube-controller-manager-cluster01-control-plane 1/1 Running 0 15m
kube-proxy-npxqd 1/1 Running 0 15m
kube-proxy-twn7s 1/1 Running 0 15m
kube-scheduler-cluster01-control-plane 1/1 Running 0 15m
尽管你可以直接创建 Pod,但除非你仅使用 Pod 进行快速故障排除,否则不应直接创建。直接创建的 Pod 无法使用 Kubernetes 提供的许多功能,包括扩展、自动重启或滚动升级。
与其直接创建 Pod,不如使用 Deployment、StatefulSet 或在某些特殊情况下使用 ReplicaSet 资源或复制控制器。
PodTemplates
PodTemplates 提供了一种创建 Pod 模板或蓝图的方法。它们作为可重用的配置,包括 Pod 的所需规格和设置。它们包含 Pod 的元数据和规格,包括名称、标签、容器、卷以及其他属性。
PodTemplates 通常用于其他 Kubernetes 对象,如 ReplicaSets、Deployments 和 StatefulSets。这些资源依赖于 PodTemplate 来生成和管理具有一致配置和行为的 Pod 集合。
优先级类(PriorityClasses)
PriorityClasses 提供了一种根据 Pod 重要性来优先处理的方式。这使得 Kubernetes 调度器能够在资源分配和 Pod 调度方面做出更好的决策。
定义 PriorityClasses 时,你需要创建一个新的 PriorityClass 资源,并为其分配数字值,以表示优先级水平。具有更高优先级值的 Pod 会在资源分配和调度时优先于较低优先级的 Pod。
使用 PriorityClasses,你可以确保关键工作负载在资源分配和调度时优先获得所需的资源,从而保证其平稳运行。
优先级配置(PriorityLevelConfigurations)
PriorityLevelConfigurations 是帮助定义请求发送到 API 服务器时的优先级的对象。它们控制 API 请求在集群中如何处理和优先级排序。通过使用 PriorityLevelConfigurations,您可以建立多个优先级,并为特定属性分配优先级。这些属性包括设置每秒最大查询数(QPS)和特定优先级下的并发请求数量。这使得根据不同 API 请求的重要性,能够更有效地管理和分配资源。
PriorityLevelConfigurations 允许您强制执行策略,确保关键请求始终能够获得足够的资源,从而提供灵活性来管理 API 请求的处理和资源分配。
ReplicaSets
ReplicaSets 可以用来创建一个 Pod 或一组 Pod(副本)。与 ReplicationController 资源类似,ReplicaSet 会保持副本计数中定义的 Pod 数量。如果 Pod 数量过少,Kubernetes 会补充差额并创建缺失的 Pod。如果 ReplicaSet 中的 Pod 数量过多,Kubernetes 会删除 Pod,直到数量与设置的副本数相等。
一般来说,您应避免直接创建 ReplicaSets。相反,您应该创建一个 Deployment,它将创建并管理一个 ReplicaSet。
复制控制器
复制控制器将管理正在运行的 Pod 数量,始终保持所需副本的运行。如果您创建一个复制控制器并将副本计数设置为 5,控制器将始终保持五个应用程序 Pod 运行。
复制控制器已被 ReplicaSet 资源所取代,我们已经在其单独的部分进行了讨论。虽然您仍然可以使用复制控制器,但应考虑使用 Deployment 或 ReplicaSet。
ResourceQuotas
在多个团队共享一个 Kubernetes 集群的情况下,多租户集群变得非常常见。由于您将有多个团队在同一个集群中工作,您应该创建配额,以限制单个租户消耗集群或节点上所有资源的潜力。
大多数集群资源都可以设置限制,包括以下内容:
-
中央处理单元(CPU)
-
内存
-
PVCs
-
ConfigMaps -
部署
-
Pods,等等
设置限制后,一旦达到限制,将停止创建任何额外的资源。如果您为命名空间设置了 10 个 Pod 的限制,而用户创建了一个新的 Deployment,试图启动 11 个 Pod,第 11 个 Pod 将无法启动,用户将收到错误信息。
创建内存和 CPU 配额的基本清单文件如下所示:
apiVersion: v1
kind: ResourceQuota
metadata:
name: base-memory-cpu
spec:
hard:
requests.cpu: "2"
requests.memory: 8Gi
limits.cpu: "4"
limits.memory: 16Gi
这将设置命名空间可以用于 CPU 和内存请求及限制的总资源量限制。
你可以在配额中设置的许多选项是显而易见的,如 pods、PVCs、services 等。当你设置限制时,这意味着设置的限制是该命名空间中该资源的最大允许值。例如,如果你将 pod 的限制设置为 5,当尝试在该命名空间中创建第六个 pod 时,它将被拒绝。
一些配额有多个可以设置的选项:特别是 CPU 和内存。在我们的示例中,两个资源都设置了请求和限制。理解这两个值对于确保资源的高效使用以及限制应用程序的潜在可用性非常重要。
请求本质上是对特定资源的预留。当 pod 被部署时,你应该始终为 CPU 和内存设置请求,且该值应为启动应用程序所需的最小值。调度程序将使用这个值来寻找满足请求的节点。如果没有节点有可用的请求资源,pod 将无法调度。
现在,由于请求会保留资源,这意味着一旦集群中的所有节点都分配了 100% 的请求,任何额外的 pod 创建都将被拒绝,因为请求已经达到 100%。即使你实际集群的 CPU 或内存使用率只有 10%,由于请求或 预留 已达到 100%,pod 仍然无法调度。如果请求没有经过仔细考虑,就会导致资源浪费,从而增加运行平台的成本。
CPU 和内存的限制设置了 pod 可以使用的最大值。这与请求不同,因为限制不是对资源的预留。然而,从应用程序的角度来看,限制仍然需要仔细规划。如果你将 CPU 限制设置得太低,应用程序可能会出现性能问题,如果你将内存限制设置得太低,pod 将被终止,影响可用性,直到重新启动。
一旦创建了配额,你可以使用 kubectl describe 命令查看资源的使用情况。在我们的示例中,我们将 ResourceQuota 命名为 base-memory-cpu。
要查看使用情况,我们将执行 kubectl get resourcequotas base-memory-cpu 命令,结果如下所示:
Name: base-memory-cpu
Namespace: default
Resource Used Hard
-------- ---- ----
limits.cpu 0 4
limits.memory 0 16Gi
requests.cpu 0 2
requests.memory 0 8Gi
ResourceQuotas 作为一种手段,用于管理和控制集群内资源的分配。它们允许你为每个命名空间分配特定的 CPU 和内存资源,确保每个租户都有足够的资源来有效地运行他们的应用程序。此外,ResourceQuotas 还充当保护机制,防止优化不良或资源密集型的应用程序对集群中其他应用程序的性能产生不利影响。
RoleBindings
RoleBinding 资源用于将 Role 或 ClusterRole 与主体和命名空间关联起来。例如,以下的 RoleBinding 将允许 aws-codebuild 用户将 patch-openunison ClusterRole 应用到 openunison 命名空间:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: patch-openunison
namespace: openunison
subjects:
- kind: User
name: aws-codebuild
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: patch-deployment
apiGroup: rbac.authorization.k8s.io
尽管这里引用了一个 ClusterRole,它只会应用于 openunison 命名空间。如果 aws-codebuild 用户尝试修补另一个命名空间中的 Deployment,API 服务器会阻止它。
Roles
与 ClusterRole 类似,Roles 结合了 API 组和操作,定义了一组可以分配给主体的权限。ClusterRole 和 Role 的区别在于,Role 只能拥有在命名空间级别定义的资源,并且仅适用于特定的命名空间。
RuntimeClasses
RuntimeClasses 用于设置和定制不同的运行时环境,以便运行容器。它们提供了灵活性,可以选择和配置最适合工作负载的容器运行时。通过使用 RuntimeClasses,你可以根据特定的需求来微调容器运行时。
每个 RuntimeClass 都与特定的容器运行时(如 Docker 或 Containerd)相关联。它们包含可配置的参数,定义所选容器运行时的行为。这些参数包括资源限制、安全配置和环境变量。
Secrets
之前,我们描述了如何使用 ConfigMap 资源来存储配置信息。我们提到过,ConfigMap 永远不应该用于存储任何类型的敏感数据。这是 Secret 的职责。
Secrets 作为 Base64 编码的字符串存储,这并不是一种加密方式。那么,为什么要将 Secrets 与 ConfigMap 分开存储呢?提供一个独立的资源类型可以更容易地维护访问控制,并且能够使用外部的秘密管理系统来注入敏感信息。
Secrets 可以通过文件、目录或文字字符串来创建。例如,我们有一个 MySQL 镜像,想要执行它,并且希望通过 Secret 将密码传递给 Pod。在我们的工作站中,我们的当前工作目录下有一个名为 dbpwd 的文件,里面存储了密码。使用 kubectl 命令,我们可以通过执行 kubectl create secret generic mysql-admin --from-file=./dbpwd 来创建一个 Secret。
这将创建一个名为 mysql-admin 的新 Secret,内容为 dbpwd 文件中的内容。使用 kubectl,我们可以通过运行 kubectl get secret mysql-admin -o yaml 命令来获取该 Secret 的输出,结果如下:
apiVersion: v1
data:
dbpwd: c3VwZXJzZWNyZXQtcGFzc3dvcmQK
kind: Secret
metadata:
creationTimestamp: "2020-03-24T18:39:31Z"
name: mysql-admin
namespace: default
resourceVersion: "464059"
uid: 69220ebd-c9fe-4688-829b-242ffc9e94fc
type: Opaque
从前面的输出可以看到,data 部分包含了我们的文件名,然后是一个 Base64 编码的值,这是由文件内容生成的。
如果我们从 Secret 中复制 Base64 值,并将其传递给 base64 工具,我们可以轻松解码密码,如下所示:
echo c3VwZXJzZWNyZXQtcGFzc3dvcmQK | base64 -d
supersecret-password
使用echo命令对字符串进行 Base64 编码时,添加-n标志以避免添加额外的\n。改用echo -n 'test' | base64,而不是echo 'test' | base64。
所有内容都存储在etcd中,但我们担心有人可能会入侵etcd节点并窃取etcd数据库的副本。一旦有人获得了数据库的副本,他们可以轻松使用etcdctl工具浏览内容,检索我们所有的 Base64 编码的 Secrets。幸运的是,Kubernetes 添加了一个功能,在写入数据库时加密Secrets。
启用此功能对于许多用户来说可能相当复杂,尽管它听起来是个好主意,但它确实存在一些潜在的问题,你应该在实施之前考虑这些问题。如果你想阅读有关加密静态 Secrets 的步骤,可以访问 Kubernetes 网站的kubernetes.io/docs/tasks/administer-cluster/encrypt-data/。
另一个保护 Secrets 的选项是使用第三方秘密管理工具,如 HashiCorp 的 Vault 或 CyberArk 的 Conjur。我们将在第九章,Kubernetes 中的 Secrets 管理中介绍与秘密管理工具的集成。
SelfSubjectAccessReviews
SelfSubjectAccessReviews对象使用户或实体能够检查他们在命名空间内执行特定操作所需的权限。
要使用SelfSubjectAccessReviews,用户提供用户名以及要检查的操作和资源。集群根据命名空间中的访问控制策略评估提供的用户权限,API 服务器会响应是否允许或拒绝请求的操作。
SelfSubjectAccessReviews和下一个资源SelfSubjectRulesReviews看起来非常相似,但它们的功能不同。记住SelfSubjectAccessReviews的关键点是,它评估针对特定操作和资源的单独访问权限。
SelfSubjectRulesReviews
SelfSubjectRulesReviews对象用于确定用户或实体在命名空间内具有权限的规则集,提供了调查自己操作和资源的访问控制规则的能力。
要使用SelfSubjectRulesReview,你需要提供你的身份,API 服务器将评估在命名空间中与该身份关联的权限。
SelfSubjectRulesReviews提供比SelfSubjectAccessReviews更全面的视图,深入理解用户在命名空间内的权限规则集。
服务账户
Kubernetes 使用ServiceAccounts来启用工作负载的访问控制。当你创建Deployment时,可能需要访问其他服务或 Kubernetes 资源。
由于 Kubernetes 是一个安全的系统,你的应用程序尝试访问的每个资源或服务将评估基于角色的访问控制(RBAC)规则,以接受或拒绝请求。
使用清单创建服务账户是一个简单的过程,只需要在清单中添加几行代码。以下代码片段展示了一个服务账户清单,用于为 Grafana 部署创建一个服务账户:
apiVersion: v1
kind: ServiceAccount
metadata:
name: grafana
namespace: monitoring
你将服务账户与角色绑定和Roles结合,允许访问所需的服务或对象。
我们将在第六章《将企业身份验证集成到你的集群》中深入讲解如何使用ServiceAccounts。
服务
当你创建一个 Pod 时,它将从集群创建时分配的 CIDR 范围中获得一个 IP 地址。在大多数集群中,分配的 IP 仅在集群内部可访问,这种模式称为“岛屿模式”。由于 Pod 是临时性的,分配的 IP 地址可能会在应用生命周期中发生变化,这在任何服务或应用需要连接到 Pod 时会造成问题。为了解决这个问题,我们可以创建一个 Kubernetes 服务,它也会获得一个 IP 地址,但由于服务在应用生命周期中不会被删除,因此地址将保持不变。
服务将动态维护一个 Pod 列表,以根据与服务选择器匹配的标签来确定目标 Pod,创建服务的端点列表。
服务存储有关如何暴露应用程序的信息,包括运行应用程序的 Pod 以及访问这些 Pod 的网络端口。
每个服务都有一个在创建时分配的网络类型,包括以下几种:
-
ClusterIP:一种仅能在集群内部访问的网络类型。通过使用 Ingress 控制器,仍然可以用于外部请求,这将在后续章节中讨论。ClusterIP类型是默认类型,当创建服务时,如果未指定类型,则会使用此类型。 -
NodePort:一种将服务暴露到30000至32767之间随机端口的网络类型。此端口可以通过目标集群中任何工作节点的分配NodePort来访问。创建后,集群中的每个节点将收到端口信息,传入请求将通过kube-proxy进行路由。 -
LoadBalancer:此类型需要集群内的附加组件。如果你在公共云提供商上运行 Kubernetes,此类型将创建一个外部负载均衡器,并为你的服务分配一个 IP 地址。大多数本地 Kubernetes 安装不支持LoadBalancer类型,但像谷歌的 Anthos 等一些产品支持此类型。在后续章节中,我们将解释如何将一个开源项目MetalLB添加到 Kubernetes 集群中,以支持LoadBalancer类型。 -
ExternalName:这种类型与其他三种类型不同。与其他三种选项不同,这种类型不会为服务分配 IP 地址。相反,它用于将内部 Kubernetes 域名系统(DNS)名称映射到外部服务。
作为示例,我们已经部署了一个运行 Nginx 的 Pod,监听端口80。我们希望创建一个服务,使得这个 Pod 能够在集群内部接收端口80的传入请求。相关代码如下所示:
apiVersion: v1
kind: Service
metadata:
labels:
app: nginx-web-frontend
name: nginx-web
spec:
ports:
- name: http
port: 80
targetPort: 80
selector:
app: nginx-web
在我们的清单中,我们创建了一个名为app的标签,并将其值设置为nginx-web-frontend。我们将服务本身命名为nginx-web,并将服务暴露在端口80上,目标 Pod 端口为80。清单的最后两行用于指定服务将转发到的 Pod,也称为端点(Endpoints)。在这个清单中,任何具有app标签并且值为nginx-web的 Pod 都会被添加为服务的端点。最后,你可能已经注意到,我们没有在清单中指定服务类型。由于没有指定类型,它将创建为默认的ClusterIP服务类型。
StatefulSets
StatefulSets在创建 Pod 时提供了一些独特的功能。它们提供了其他 Pod 创建方法没有的功能,包括以下内容:
-
已知的 Pod 名称
-
有序部署和扩展
-
有序更新
-
持久化存储创建
理解StatefulSet的优势最好的方法是查看 Kubernetes 网站上的示例清单,如下截图所示:

图 3.4:StatefulSet 清单示例
现在,我们可以查看StatefulSet创建的资源。
该清单指定应该有三个名为nginx的 Pod 副本。当我们获取 Pod 列表时,你会看到三个 Pod 是以nginx为名称创建的,并附加了一个短横线和递增的数字。这就是我们在概述中提到的 Pod 将以已知名称创建的意思,如下代码片段所示:
NAME READY STATUS RESTARTS AGE
web-0 1/1 Running 0 4m6s
web-1 1/1 Running 0 4m2s
web-2 1/1 Running 0 3m52s
Pod 也是按顺序创建的——web-0必须完全部署后,web-1才能创建,最后是web-2。
最后,对于这个示例,我们还在清单中使用VolumeClaimTemplate为每个 Pod 添加了一个 PVC。如果你查看kubectl get pvc命令的输出,你会看到三个 PVC 已经被创建,并且命名符合我们的预期(注意,由于空间限制,我们删除了VOLUME列),如下代码片段所示:
NAME STATUS CAPACITY ACCESS MODES STORAGECLASS AGE
www-web-0 Bound 1Gi RWO nfs 13m
www-web-1 Bound 1Gi RWO nfs 13m
www-web-2 Bound 1Gi RWO nfs 12m
在清单的VolumeClaimTemplate部分,你会看到我们将 PVC 声明命名为www。当你在StatefulSet中分配一个卷时,PVC 名称将结合声明模板中使用的名称和 Pod 的名称。通过这种命名方式,你可以理解为什么 Kubernetes 将 PVC 命名为www-web-0、www-web-1和www-web-2。
存储类
存储类用于定义存储端点。每个存储类可以分配标签和策略,使开发人员能够选择最佳的存储位置来存储其持久化数据。您可以为一个后端系统创建一个存储类,该系统拥有所有非易失性内存快闪存储器(NVMe)硬盘,并将其命名为 fast,同时为运行标准硬盘的 NetApp 网络文件系统(NFS)卷分配一个不同的存储类,命名为 standard。
当请求 PVC 时,用户可以分配他们希望使用的 StorageClass。当 API 服务器收到请求时,它会找到匹配的名称,并使用 StorageClass 配置通过供给器在存储系统中创建卷。
从高层次来看,StorageClass 清单不需要太多信息。以下是一个使用 Kubernetes 孵化项目中的供给器来提供 NFS 自动供给卷的存储类示例,名为 nfs:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs
provisioner: nfs
存储类允许您为用户提供多种存储解决方案。您可以创建一个便宜、较慢的存储类,同时提供另一个支持高吞吐量以满足高数据需求的存储类。通过为每个提供的存储解决方案提供不同的存储类,您允许开发者选择最适合其应用的选项。
SubjectAccessReviews
SubjectAccessReviews 用于检查实体是否有权限在资源上执行特定操作。它们允许用户请求访问审查并获取有关其权限的信息。通过提供身份、所需操作和资源,API 服务器确定该操作是否被允许或拒绝。这有助于用户验证他们对资源的权限,进而帮助识别 Kubernetes 资源的访问问题。
例如,Scott 想要验证自己是否有权限在名为 sales 的命名空间中创建 pod。为此,Scott 在 sales 命名空间中创建了一个 SubjectAccessReview,其中包含他的用户名、创建操作和目标资源(pods)。
API 服务器验证用户是否有权限在 sales 命名空间中创建 pod,并返回响应。API 服务器的响应包括请求的操作是否被允许或拒绝。
知道一个实体是否有权限在资源上执行操作,有助于减少由于权限问题导致部署失败时的挫败感。
TokenReviews
TokenReviews 是用于验证和验证与集群中的用户或实体相关联的身份验证令牌的 API 对象。如果令牌有效,API 服务器将检索与该令牌关联的用户或实体的详细信息。
当用户向 Kubernetes API 服务器提交身份验证令牌时,服务器会将令牌与内部身份验证系统进行验证。它会验证令牌是否合法,并确定与其相关联的用户或实体。
API 服务器提供有关令牌有效性以及用户或实体的信息,包括用户名、用户标识符(UID)和组成员信息。
ValidatingWebhookConfigurations
ValidatingWebhookConfiguration 是一组规则,决定哪些准入请求会被 Webhook 拦截和处理。每条规则都包含 Webhook 应处理的特定资源和操作。
它提供了一种通过对准入请求应用验证逻辑来强制执行特定策略或规则的方法。许多 Kubernetes 的附加组件提供 ValidatingWebhookConfiguration,其中最常见的之一是 NGINX ingress 控制器。
您可以通过执行 kubectl get validatingwebhookconfigurations 查看集群中所有的 ValidatingWebhookConfigurations。对于我们部署的 KinD 集群,您将看到一个针对 NGINX Ingress 准入的条目:
NAME WEBHOOKS AGE ingress-nginx-admission 1 3d10h
VolumeAttachments
VolumeAttachments 在集群中的外部存储卷和节点之间创建连接。它们控制持久化卷与特定节点的关联,使得节点能够访问和利用存储资源。
总结
在本章中,您接受了一个快速节奏的 Kubernetes 启动训练营,您接触到了大量的技术信息。请记住,随着您深入 Kubernetes 的世界,一切将变得更加可管理,且更容易掌握。需要注意的是,本章中讨论的许多资源将在后续章节中进一步探讨和解释,帮助您更深入地理解。
您深入了解了每个 Kubernetes 组件及其相互依赖关系,这些组件共同构成了集群。凭借这些知识,您现在具备了调查和识别集群内错误或问题根源的必要技能。我们探讨了控制平面,其中包括 api-server、kube-scheduler、etcd 和控制器管理器。此外,您还熟悉了运行 kubelet 和 kube-proxy 组件的 Kubernetes 节点,以及容器运行时。
我们还深入探讨了 kubectl 工具的实际应用,它将是您与集群互动的主要工具。您了解了几个基本命令,如访问日志和提供描述性信息的命令,这些命令将是您日常使用的工具。
在下一章中,我们将创建一个开发用的 Kubernetes 集群,它将作为剩余章节的基础集群。在本书的剩余部分,我们将引用本章中呈现的许多资源,并通过实际案例来帮助解释这些资源。
问题
-
以下哪个组件不包含在 Kubernetes 控制平面中?
-
api-server
-
kube-scheduler
-
etcd
-
Ingress 控制器
-
答案:d
-
保持所有集群信息的组件是什么名称?
-
api-server
-
主控制器
-
kubelet
-
etcd
-
答案:d
-
哪个组件负责选择将运行工作负载的节点?
-
kubelet
-
api-server
-
kube-scheduler
-
Pod 调度器
-
答案:c
-
你会在
kubectl命令中添加哪个选项,以查看命令的附加输出?-
详细模式
-
-v
-
–verbose
-
-log
-
答案:b
-
哪种服务类型会创建一个随机生成的端口,使得所有进入分配端口的工作节点流量都可以访问该服务?
-
负载均衡器
-
ClusterIP
-
无—这是所有服务的默认设置
-
NodePort
-
答案:d
-
如果你需要在 Kubernetes 集群上部署一个需要已知 Pod 名称和控制每个 Pod 启动顺序的应用程序,你会创建哪个对象?
-
StatefulSet
-
Deployment
-
ReplicaSet
-
ReplicationController
-
答案:a
加入我们书籍的 Discord 空间
加入本书的 Discord 工作区,参加每月与作者的 问我任何问题(Ask Me Anything)环节:

第四章:服务、负载均衡和网络策略
在上一章中,我们开始了 Kubernetes Bootcamp,为你提供了一个简明但全面的 Kubernetes 基础和对象介绍。我们首先分析了 Kubernetes 集群的主要部分,重点讲解了控制平面和工作节点。控制平面是集群的大脑,负责管理所有任务,包括调度任务、创建部署以及跟踪 Kubernetes 对象。工作节点用于运行应用程序,包括kubelet服务,保持容器健康,并通过kube-proxy处理网络连接。
我们研究了如何使用kubectl工具与集群交互,这个工具允许你直接运行命令,或者使用 YAML 或 JSON 清单声明你希望 Kubernetes 执行的操作。我们还探讨了大部分 Kubernetes 资源。我们讨论的一些常见资源包括DaemonSets,它确保 Pod 在所有或特定的节点上运行;StatefulSets,用于管理具有稳定网络身份和持久存储的有状态应用;以及ReplicaSets,用于保持一定数量的 Pod 副本运行。
Bootcamp 章节应该帮助你建立对 Kubernetes 架构、关键组件和资源以及基本资源管理的扎实理解。拥有这些基础知识将为你在接下来的章节中深入学习更高级的主题打下基础。
在本章中,你将学习如何管理和路由网络流量到你的 Kubernetes 服务。我们将首先解释负载均衡器的基本概念,并展示如何配置它们来处理访问你应用程序的请求。你将理解使用服务对象的重要性,以确保即使 Pod 的 IP 地址是临时的,连接也能保持可靠。
此外,我们还将讲解如何使用 Ingress 控制器将你的 Web 服务暴露给外部流量,以及如何使用LoadBalancer服务处理更复杂的非 HTTP/S 工作负载。你将通过部署 Web 服务器亲身体验这些概念的实际操作。
由于许多读者可能没有 DNS 基础设施来支持名称解析,而这是 Ingress 正常工作的前提,我们将使用一个免费的互联网服务 nip.io 来管理 DNS 名称。
最后,我们将探讨如何使用网络策略来保护你的 Kubernetes 服务,确保内部和外部的通信都受到保护。
本章将涵盖以下主题:
-
负载均衡器简介及其在流量路由中的作用。
-
理解 Kubernetes 中的服务对象及其重要性。
-
使用 Ingress 控制器暴露 Web 服务。
-
使用
LoadBalancer服务处理复杂工作负载。 -
部署 NGINX Ingress 控制器并设置 Web 服务器。
-
使用 nip.io 服务来管理 DNS 名称。
-
使用网络策略保护服务,确保通信安全。
本章结束时,你将深入理解在 Kubernetes 集群中暴露和保护工作负载的各种方法。
技术要求
本章有以下技术要求:
-
一台运行 Docker 的 Ubuntu 22.04+ 服务器,至少 4 GB 的 RAM,建议 8 GB。
-
从仓库中的
chapter4文件夹中获取脚本,你可以通过访问本书的 GitHub 仓库来获取:github.com/PacktPublishing/Kubernetes-An-Enterprise-Guide-Third-Edition。
将工作负载暴露给请求
通过我们的经验,我们意识到 Kubernetes 中有三个概念可能会让人感到困惑:Services、Ingress 控制器和 LoadBalancer Services。了解这些概念对将你的工作负载暴露给外部世界至关重要。理解这些对象的功能及其不同选项是非常关键的。接下来,我们将深入探讨这些话题。
理解 Services 的工作原理
正如我们之前提到的,当工作负载在 pod 中运行时,它会被分配一个 IP 地址。然而,存在某些情况,pod 可能会重启,重启时它会获得一个新的 IP 地址。因此,直接针对 pod 的工作负载进行访问并不是一个好主意,因为其 IP 地址可能会发生变化。
Kubernetes 最酷的一点是它能够扩展你的部署。当你扩展一个部署时,Kubernetes 会添加更多的 pod 以应对增加的资源需求。这些 pod 每个都有自己独特的 IP 地址。但有一点需要注意:大多数应用程序设计时仅针对单个 IP 地址或名称。
想象一下,如果你的应用程序从运行一个 pod 扩展到突然运行 10 个 pod,你如何利用这些额外的 pod,因为你只能针对一个 IP 地址?这就是我们接下来要探索的内容。
Kubernetes 中的 Services 利用标签在服务和处理工作负载的 pods 之间建立连接。当 pods 启动时,它们会被分配标签,所有具有相同标签的 pods(如部署中定义的标签)会被分组在一起。
以 NGINX 网页服务器为例。在我们的 Deployment 中,我们会创建一个像这样的清单:
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
run: nginx-frontend
name: nginx-frontend
spec:
replicas: 3
selector:
matchLabels:
run: nginx-frontend
template:
metadata:
labels:
run: nginx-frontend
spec:
containers:
- image: bitnami/nginx
name: nginx-frontend
这个部署将创建三个 NGINX 服务器,每个 pod 将标记为 run=nginx-frontend。我们可以通过使用 kubectl 列出 pods,并添加 --show-labels 选项 kubectl get pods --show-labels 来验证 pods 是否被正确标记。
这将列出每个 pod 及其相关标签:
nginx-frontend-6c4dbf86d4-72cbc 1/1 Running 0 19s pod-template-hash=6c4dbf86d4,run=nginx-frontend
nginx-frontend-6c4dbf86d4-8zlwc 1/1 Running 0 19s pod-template-hash=6c4dbf86d4,run=nginx-frontend
nginx-frontend-6c4dbf86d4-xfz6m 1/1 Running 0 19s pod-template-hash=6c4dbf86d4,run=nginx-frontend
在这个示例中,每个 pod 都会被分配一个标签 run=nginx-frontend。这个标签在为你的应用程序配置服务时起着至关重要的作用。通过在服务配置中利用这个标签,服务将自动生成所需的端点,无需手动干预。
创建一个服务
在 Kubernetes 中,Service 是使应用程序能够被其他程序或用户访问的一种方式。可以将其视为应用程序的网关或入口点。
Kubernetes 中有四种不同类型的服务,每种类型都有其特定的用途。本章将详细介绍每种类型,但现在让我们简单地了解一下它们:
| 服务类型 | 描述 |
|---|---|
ClusterIP |
创建一个仅能从集群内部访问的服务。 |
NodePort |
创建一个可以通过分配的端口从集群内部或外部访问的服务。 |
LoadBalancer |
创建一个可以从集群内部或外部访问的服务。对于外部访问,需要额外的组件来创建负载均衡对象。 |
ExternalName |
创建一个不针对集群中端点的服务。相反,它用于提供一个服务名称,该名称将任何外部 DNS 名称作为端点。 |
表 4.1:Kubernetes 服务类型
还可以创建一种额外的服务类型,称为无头服务(headless service)。Kubernetes 的无头服务是一种服务类型,它允许与单个 pod 进行直接通信,而不是像其他服务那样将流量分配到多个 pod 上。与将固定 IP 地址分配给一组 pod 的常规 Service 不同,Headless Service 不分配集群 IP。
通过在 Service 定义中为 clusterIP 规格指定 none 来创建一个 Headless Service。
要创建服务,您需要创建一个包含 kind、selector、type 以及将用于连接服务的任何端口的 Service 对象。以我们的 NGINX Deployment 示例为例,我们希望将 Service 暴露在 80 和 443 端口上。我们已将部署标记为 run=nginx-frontend,因此在创建清单时,我们将使用该名称作为选择器:
apiVersion: v1
kind: Service
metadata:
labels:
run: nginx-frontend
name: nginx-frontend
spec:
selector:
run: nginx-frontend
ports:
- name: http
port: 80
protocol: TCP
targetPort: 80
- name: https
port: 443
protocol: TCP
targetPort: 443
type: ClusterIP
如果在服务清单中未定义类型,Kubernetes 将默认分配 ClusterIP 类型。
现在服务已经创建,我们可以通过几个 kubectl 命令来验证它是否正确定义。我们将执行的第一个检查是验证服务对象是否已创建。要检查我们的服务,我们使用 kubectl get services 命令:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx-frontend ClusterIP 10.43.142.96 <none> 80/TCP,443/TCP 3m49s
在验证服务已创建后,我们可以验证 Endpoints/Endpointslices 是否已创建。请记住,正如 Bootcamp 章节所述,Endpoints 是任何具有与我们服务中使用的标签匹配的 pod。使用 kubectl,我们可以通过执行 kubectl get ep <service name> 来验证 Endpoints:
NAME ENDPOINTS
nginx-frontend 10.42.129.9:80,10.42.170.91:80,10.42.183.124:80 + 3 more...
我们可以看到 Service 显示了三个 Endpoints,但它还显示了 +3 more 在端点列表中。由于输出被截断,因此 get 输出是有限的,不能显示所有端点。由于无法看到完整列表,我们可以通过描述端点来获得更详细的列表。使用 kubectl,您可以执行 kubectl describe ep <service name> 命令:
Name: nginx-frontend
Namespace: default
Labels: run=nginx-frontend
Annotations: endpoints.kubernetes.io/last-change-trigger-time: 2020-04-06T14:26:08Z
Subsets:
Addresses: 10.42.129.9,10.42.170.91,10.42.183.124
NotReadyAddresses: <none>
Ports:
Name Port Protocol
---- ---- --------
http 80 TCP
https 443 TCP
Events: <none>
如果你比较 get 命令和 describe 命令的输出,可能会发现 Endpoints 似乎不匹配。get 命令显示了总共六个 Endpoints:它显示了三个 IP Endpoints,并且因为输出被截断了,所以还列出了 +3,总共是六个 Endpoints。而 describe 命令的输出仅显示了三个 IP 地址,并没有显示六个。为什么这两个输出看起来有不同的结果?
get 命令会列出每个端点和端口的地址列表。由于我们的服务定义了暴露两个端口,每个地址会有两个条目,分别对应每个暴露的端口。地址列表始终会包含该服务的每个 socket,这可能会导致每个端点地址出现多次,每次对应一个 socket。
describe 命令的处理方式不同,它将地址列在一行上,并将所有端口列在地址下面。乍一看,可能会觉得 describe 命令遗漏了三个地址,但由于它将输出分成了多个部分,它只会列出地址一次。所有端口都会被分开列在地址列表下方;在我们的示例中,它显示了端口 80 和 443。
两个命令显示相似的数据,但它们以不同的格式呈现。
现在服务已经暴露到集群中,你可以使用分配的服务 IP 地址连接到应用程序。虽然这样可行,但如果Service对象被删除并重新创建,地址可能会发生变化。因此,最好不要直接使用 IP 地址,而是使用在创建服务时分配给服务的 DNS。
在接下来的章节中,我们将解释如何使用内部 DNS 名称来解析服务。
使用 DNS 解析服务
到目前为止,我们已经向你展示了在 Kubernetes 中创建某些对象时,系统会为这些对象分配 IP 地址。问题是,当你删除像 pod 或 service 这样的对象时,重新部署该对象时,它可能会收到不同的 IP 地址。由于 Kubernetes 中的 IP 是临时的,我们需要一种方法,通过其他方式(而非变化的 IP 地址)来定位对象。这就是 Kubernetes 集群中内建 DNS 服务的作用。
当创建一个服务时,会自动生成一个内部 DNS 记录,允许集群内的其他工作负载通过名称查询它。如果所有 pod 都位于同一个命名空间内,我们可以方便地使用像 mysql-web 这样的简单短名称来访问服务。然而,在服务被多个命名空间使用,并且工作负载需要与不同命名空间中的服务通信时,必须使用服务的完整名称来进行访问。
以下表格提供了如何从不同命名空间访问服务的示例:
集群名称: cluster.local 目标服务: mysql-web 目标服务命名空间: database |
|---|
| Pod 命名空间 |
database |
kube-system |
productionweb |
表 4.2:内部 DNS 示例
如上表所示,你可以通过使用标准命名约定 .<namespace>.svc.<cluster name> 来访问另一个命名空间中的服务。在大多数情况下,当你访问不同命名空间中的服务时,无需添加集群名称,因为集群名称应自动附加。
为了进一步扩展服务的整体概念,让我们深入探讨每种服务类型的具体内容,了解它们如何被用来访问我们的工作负载。
了解不同的服务类型
创建服务时,你可以指定服务类型,但如果未指定类型,默认将使用 ClusterIP 类型。分配的服务类型将配置服务是暴露给集群内部还是外部流量。
ClusterIP 服务
最常用且经常被误解的服务类型是 ClusterIP。如果你回顾一下表 4.1,你会发现 ClusterIP 类型的描述指出,服务允许从集群内部连接该服务。ClusterIP 类型不允许任何外部通信连接到暴露的服务。
仅将服务暴露给集群内部工作负载的想法可能是一个令人困惑的概念。在下一个示例中,我们将描述一个只将服务暴露给集群自身的用例,这样做既有意义又能提高安全性。
暂时把外部流量放到一边,专注于我们当前的部署。我们的主要目标是理解每个组件如何协同工作来形成我们的应用。以 NGINX 为例,我们将通过添加一个后端数据库来增强该部署,Web 服务器将使用这个数据库。
到目前为止,这是一个简单的应用:我们创建了部署,名为 web frontend 的 NGINX 服务器服务,以及名为 mysql-web 的数据库服务。为了配置 Web 服务器的数据库连接,我们决定使用一个 ConfigMap,该配置映射将指向数据库服务。
你可能会想,既然我们使用的是单一数据库服务器,是否可以直接使用 pod 的 IP 地址。虽然这样一开始可以正常工作,但每次 pod 重启时,地址会发生变化,导致 Web 服务器无法连接到数据库。由于 pod IP 是临时性的,因此即使只是针对单个 pod,也应始终使用服务。
虽然我们可能希望在某个时候将 Web 服务器暴露给外部流量,但为什么我们需要暴露mysql-web数据库服务呢?因为 Web 服务器在同一个集群中,并且在这种情况下,处于相同的命名空间,我们只需要使用ClusterIP地址类型,这样 Web 服务器就可以连接到数据库服务器。由于数据库无法从集群外部访问,因此它更安全,因为它不允许任何来自集群外部的流量。
通过使用服务名称而不是 Pod IP 地址,当 Pod 重启时我们不会遇到问题,因为服务是通过标签来定位的,而不是 IP 地址。我们的 Web 服务器将简单地查询Kubernetes DNS 服务器来查找mysql-web服务名称,该服务名称将包含任何与mysql-web标签匹配的 Pod 的端点。
NodePort 服务
NodePort服务为集群中的服务提供内部和外部访问。一开始,它似乎是暴露服务的理想选择,因为它可以让每个人都能访问。然而,它是通过在节点上分配一个端口来实现的(默认情况下使用30000-32767范围内的端口)。依赖NodePort可能会让用户在需要通过网络访问服务时感到困惑,因为他们需要记住分配给服务的具体端口。稍后你将看到如何通过NodePort访问服务,展示为什么我们不建议在生产工作负载中使用它。
尽管在大多数企业环境中,你不应将NodePort服务用于任何生产工作负载,但还是有一些合理的理由可以使用它,主要是用于排查访问工作负载的问题。当我们接到来自应用程序的报告,且问题被归咎于 Kubernetes 平台或 Ingress 控制器时,我们可以临时将服务从ClusterIP更改为NodePort,以测试连接性而不使用 Ingress 控制器。通过使用NodePort访问应用程序,我们绕过了 Ingress 控制器,将该组件排除为潜在问题源。如果我们能够使用NodePort访问工作负载并且它正常工作,就知道问题不在应用程序本身,我们可以将工程资源指向查看 Ingress 控制器或其他潜在根本原因。
要创建一个使用NodePort类型的服务,只需在清单中将类型设置为NodePort。我们可以使用之前用于暴露ClusterIP示例中 NGINX 部署的相同清单,只需将type更改为NodePort:
apiVersion: v1
kind: Service
metadata:
labels:
run: nginx-frontend
name: nginx-frontend
spec:
selector:
run: nginx-frontend
ports:
- name: http
port: 80
protocol: TCP
targetPort: 80
- name: https
port: 443
protocol: TCP
targetPort: 443
type: NodePort
我们可以像查看ClusterIP服务那样使用kubectl查看端点。运行kubectl get services将显示新创建的服务:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx-frontend NodePort 10.43.164.118 <none> 80:31574/TCP,443:32432/TCP 4s
输出显示了类型是NodePort,并且我们已暴露了服务的 IP 地址和端口。如果你查看这些端口,你会注意到,与ClusterIP服务不同,NodePort服务显示了两个端口而不是一个。第一个端口是暴露的端口,集群内部的服务可以访问它,第二个端口号是随机生成的端口,可以从集群外部访问。
由于我们为服务暴露了80和443两个端口,我们将会有两个NodePort被分配。如果有人需要从集群外部访问该服务,他们可以通过任何工作节点和相应的端口来访问服务。

图 4.1:使用 NodePort 的 NGINX 服务
每个节点都维护着一个NodePort和其分配服务的列表。由于这个列表是与所有节点共享的,你可以使用任何正常工作的节点并通过该端口进行访问,Kubernetes 会将请求路由到正在运行的 pod。
为了可视化流量,我们创建了一个图形,展示了向 NGINX pod 发送的 Web 请求:

图 4.2:NodePort 流量概览
使用NodePort暴露服务时,有一些问题需要考虑:
-
如果你删除并重新创建服务,分配的
NodePort会发生变化。 -
如果你目标节点处于离线状态或遇到问题,您的请求将失败。
-
使用
NodePort为太多服务可能会变得混乱。你需要记住每个服务的端口,并且要记住服务没有外部名称。这可能会让目标集群内服务的用户感到困惑。
由于这里列出的限制,你应该限制使用NodePort服务。
LoadBalancer 服务
许多初学者在阅读 Kubernetes 服务时发现,LoadBalancer类型会为服务分配一个外部 IP 地址。由于外部 IP 地址可以被网络中的任何机器直接访问,这对于服务来说是一个有吸引力的选项,这也是许多人首先尝试使用它的原因。不幸的是,由于许多用户可能从使用本地 Kubernetes 集群开始,他们会在尝试创建LoadBalancer服务时遇到失败。
LoadBalancer服务依赖于一个与 Kubernetes 集成的外部组件来创建分配给服务的 IP 地址。大多数本地 Kubernetes 安装没有包括这种类型的服务。没有额外的组件,当你尝试使用LoadBalancer服务时,你会发现服务的EXTERNAL-IP状态栏显示为<pending>。
我们将在本章稍后解释LoadBalancer服务以及如何实现它。
ExternalName 服务
ExternalName 服务是一种独特的服务类型,具有特定的使用场景。当你查询使用 ExternalName 类型的服务时,最终的端点不是集群中运行的 pod,而是一个外部 DNS 名称。
为了使用你可能熟悉的 Kubernetes 之外的例子,这类似于使用 c-name 来别名主机记录。当你查询 DNS 中的 c-name 记录时,它会解析为主机记录,而不是 IP 地址。
在使用这种服务类型之前,你需要了解它可能对应用程序造成的潜在问题。如果目标端点使用 SSL 证书,可能会遇到问题。因为你查询的主机名可能与目标服务器证书上的名称不一致,因此由于名称不匹配,你的连接可能会失败。如果遇到这种情况,你可以尝试使用添加了主题备用名称(SANs)的证书。为证书添加备用名称,可以将多个名称与证书关联。
为了说明为什么你可能想使用 ExternalName 服务,让我们使用以下示例:
FooWidgets 应用程序需求
FooWidgets 在他们的 Kubernetes 集群上运行一个应用程序,该应用程序需要连接到运行在名为 sqlserver1.foowidgets.com(192.168.10.200)的 Windows 2019 服务器上的数据库服务器。
当前应用程序部署在一个名为 finance 的命名空间中。
SQL 服务器将在下个季度迁移到容器中。
你有两个需求:
-
配置应用程序,使其仅使用集群的 DNS 服务器连接外部数据库服务器。
-
在 SQL 服务器迁移后,FooWidgets 不能对应用程序进行任何配置更改。
根据需求,使用 ExternalName 服务是完美的解决方案。那么,我们如何完成这些需求呢?(这是一个理论性练习,你无需在 KinD 集群上执行任何操作。)以下是步骤:
-
第一步是创建一个清单,以创建用于数据库服务器的
ExternalName服务:apiVersion: v1 kind: Service metadata: name: sql-db namespace: finance spec: type: ExternalName externalName: sqlserver1.foowidgets.com -
创建服务后,下一步是配置应用程序使用我们新服务的名称。由于服务和应用程序位于同一命名空间中,你可以配置应用程序以使用
sql-db名称。 -
现在,当应用程序查询
sql-db时,它将解析为sqlserver1.foowidgets.com,并将 DNS 请求转发到外部 DNS 服务器,在那里名称被解析为192.168.10.200的 IP 地址。
这完成了初步需求,通过仅使用 Kubernetes DNS 服务器将应用程序连接到外部数据库服务器。
你可能会想,为什么我们不直接配置应用程序使用数据库服务器的名称?关键在于第二个需求:限制在 SQL 服务器迁移到容器后进行任何重新配置。
由于一旦 SQL 服务器迁移到集群,我们将无法重新配置应用程序,因此我们不能在应用程序设置中更改 SQL 服务器的名称。如果我们将应用程序配置为使用原始名称sqlserver1.foowidgets.com,那么迁移后应用程序将无法正常工作。通过使用ExternalName服务,我们可以通过将ExternalHost服务名称替换为指向 SQL 服务器的标准 Kubernetes 服务,从而更改内部 DNS 服务名称。
为了实现第二个目标,请按照以下步骤操作:
-
由于我们已经为
sql-db名称在 DNS 中创建了一个新的条目,所以应该删除ExternalName服务,因为它已经不再需要。 -
创建一个名为
sql-db的新服务,使用app=sql-app作为选择器。清单将如下所示:apiVersion: v1 kind: Service metadata: labels: app: sql-db name: sql-db namespace: finance spec: ports: - port: 1433 protocol: TCP targetPort: 1433 name: sql selector: app: sql-app type: ClusterIP
由于我们为新服务使用相同的服务名称,因此不需要对应用程序进行任何更改。应用程序仍然会使用sql-db名称,这个名称现在指向集群中部署的 SQL 服务器。
现在你了解了服务后,我们可以继续讨论负载均衡器,负载均衡器将允许你通过标准的 URL 名称和端口将服务暴露到外部。
负载均衡器简介
在第二部分中,我们将讨论使用第 7 层(Layer 7)和第 4 层(Layer 4)负载均衡器的基础知识。要理解不同类型负载均衡器之间的区别,首先需要了解开放系统互联(OSI)模型。理解 OSI 模型的不同层次将帮助你了解不同解决方案如何处理传入的请求。
理解 OSI 模型
在 Kubernetes 中,有多种方法可以将应用程序暴露出去,你将经常遇到关于第 7 层或第 4 层负载均衡的提及。这些术语表示它们在 OSI 模型中的位置,每一层提供不同的功能。每个在第 7 层运行的组件,与第 4 层的相比,提供了不同的能力。
首先,让我们简要概述一下七个层次并描述每一层。在本章中,我们主要关注两个高亮部分,第 4 层和第 7 层:
| OSI 层 | 名称 | 描述 |
|---|---|---|
| 7 | 应用层 | 提供应用程序流量,包括 HTTP 和 HTTPS |
| 6 | 表示层 | 形成数据包并进行加密 |
| 5 | 会话层 | 控制流量 |
| 4 | 传输层 | 设备之间的通信流量,包括 TCP 和 UDP |
| 3 | 网络层 | 设备之间的路由,包括 IP |
| 2 | 数据链路层 | 执行物理连接(MAC 地址)的错误检查 |
| 1 | 物理层 | 设备的物理连接 |
表 4.3:OSI 模型层
你不需要成为 OSI 层的专家,但你应该理解第 4 层和第 7 层负载均衡器提供的功能,以及如何在集群中使用它们。
让我们更深入地了解第 4 层和第 7 层的细节:
-
第 4 层:正如图表中所述,第 4 层负责设备间流量的通信。运行在第 4 层的设备可以访问 TCP/UDP 信息。基于第 4 层的负载均衡器为你的应用程序提供了服务所有 TCP/UDP 端口的能力。
-
第 7 层:第 7 层负责为应用程序提供网络服务。当我们说到应用程序流量时,我们并不是指像 Excel 或 Word 这样的应用程序;而是指支持这些应用程序的协议,如 HTTP 和 HTTPS。
这对于一些人来说可能是全新的,要完全理解每一层需要多章节的内容——这超出了本书的范围。我们希望你从这个介绍中得到的主要信息是,像数据库这样的应用程序不能通过第 7 层负载均衡器暴露到外部。要暴露不使用 HTTP/S 流量的应用程序,必须使用第 4 层负载均衡器。
在接下来的章节中,我们将解释每种负载均衡器类型以及如何在 Kubernetes 集群中使用它们来暴露你的服务。
第 7 层负载均衡器
Kubernetes 提供了作为第 7 层负载均衡器的 Ingress 控制器,这些控制器提供了一种访问你的应用程序的方法。你可以在 Kubernetes 集群中启用 Ingress 的各种选项,包括以下几种:
-
NGINX
-
Envoy
-
Traefik
-
HAProxy
你可以把第 7 层负载均衡器看作是网络流量的指挥员。它的作用是将进入的请求分发到托管网站或应用程序的多个服务器上。
当你访问一个网站或使用一个应用程序时,你的设备会向服务器发送请求,要求获取特定的网页或数据。在使用第 7 层负载均衡器时,你的请求不会直接到达单一服务器,而是通过负载均衡器转发。第 7 层负载均衡器会检查请求的内容,了解请求的是哪个网页或数据。通过分析后端服务器健康状况、当前负载,甚至是你的位置,负载均衡器智能地选择最佳服务器来处理你的请求。
第 7 层负载均衡器确保所有服务器得到了高效利用,用户体验顺畅且响应迅速。可以把它想象成在一个有多个结账柜台的商店,商店经理会引导顾客到最不繁忙的柜台,减少等待时间,确保每个人都能及时服务。
总结来说,第 7 层负载均衡器优化了整体系统的性能和可靠性。
名称解析和第 7 层负载均衡器
要在 Kubernetes 集群中处理第 7 层流量,你需要部署一个 Ingress 控制器。Ingress 控制器依赖于传入的域名来将流量路由到正确的服务。这比传统的服务器部署模型要简单得多,后者需要在用户通过域名外部访问应用程序之前,先创建 DNS 记录并将其映射到 IP 地址。
部署在 Kubernetes 集群上的应用程序也不例外——用户将使用分配的 DNS 名称来访问应用程序。最常见的实现方式是创建一个新的通配符域名,通过外部负载均衡器(如 F5、HAProxy 或 Seesaw)指向 Ingress 控制器。通配符域名会将所有流量指向同一目的地。例如,如果你的通配符域名是 foowidgets.com,则该域名的主要入口会是 *.foowidgets.com。使用通配符域名分配的任何 Ingress URL 名称,其流量都会被指向外部负载均衡器,然后通过你的 Ingress 规则 URL 将流量指向定义的服务。
以 foowidgets.com 域名为例,我们有三个 Kubernetes 集群,通过外部负载均衡器和多个 Ingress 控制器端点进行管理。我们的 DNS 服务器将为每个集群设置条目,使用指向负载均衡器虚拟 IP 地址的通配符域名:
| 域名 | IP 地址 | K8s 集群 |
|---|---|---|
*.clusterl.foowidgets.com |
192.168.200.100 |
Production001 |
*.cluster2.foowidgets.com |
192.168.200.101 |
Production002 |
*.cluster3.foowidgets.com |
192.168.200.102 |
Development001 |
表 4.4:Ingress 的通配符域名示例
下图展示了请求的完整流动过程:

图 4.3:多名称 Ingress 流量流动
图 4.3 中的每一步都在这里详细说明:
-
使用浏览器,用户请求此 URL:
https://timesheets.cluster1.foowidgets.com。 -
DNS 查询被发送到 DNS 服务器。DNS 服务器查找
cluster1.foowidgets.com的区域详细信息。DNS 区域中有一个条目解析为分配给该域的负载均衡器上的虚拟 IP(VIP)地址。 -
负载均衡器的
cluster1.foowidgets.com的 VIP 地址有三个后端服务器分配,指向部署了 Ingress 控制器的三个工作节点。 -
使用其中一个端点,请求被发送到 Ingress 控制器。
-
Ingress 控制器将请求的 URL 与 Ingress 规则列表进行比较。当找到匹配的请求时,Ingress 控制器会将请求转发到分配给该 Ingress 规则的服务。
为了更好地理解 Ingress 的工作原理,创建一个集群上的 Ingress 规则并观察其运行情况会很有帮助。目前,最关键的要点是,Ingress 使用请求的 URL 将流量定向到正确的 Kubernetes 服务。
使用 nip.io 进行名称解析
许多个人开发集群,例如我们的 KinD 安装,可能无法访问 DNS 基础设施或没有权限添加记录。为了测试 Ingress 规则,我们需要将唯一的主机名映射到由 Ingress 控制器指向的 Kubernetes 服务。如果没有 DNS 服务器,你需要创建一个本地文件,其中包含多个名称,并指向 Ingress 控制器的 IP 地址。
例如,如果你部署了四个 web 服务器,你需要将这四个名称添加到本地 hosts 文件中。下面是一个示例:
192.168.100.100 webserver1.test.local
192.168.100.100 webserver2.test.local
192.168.100.100 webserver3.test.local
192.168.100.100 webserver4.test.local
这也可以用单行而不是多行表示:
192.168.100.100 webserver1.test.local webserver2.test.local webserver3.test.local webserver4.test.local
如果你使用多台机器进行部署测试,你需要编辑每台你计划用于测试的机器上的 host 文件。在多台机器上维护多个文件是一场行政噩梦,会导致问题并使测试变得具有挑战性。
幸运的是,我们可以使用免费的 DNS 服务,而无需为 KinD 集群配置复杂的 DNS 基础设施。
nip.io 是我们将用于 KinD 集群名称解析的服务。以我们之前的 web 服务器示例为例,我们无需创建任何 DNS 记录。我们仍然需要将不同服务器的流量发送到运行在192.168.100.100上的 NGINX 服务器,以便 Ingress 可以将流量路由到相应的服务。nip.io 使用一种命名格式,将 IP 地址包含在主机名中,从而将名称解析为 IP 地址。例如,假设我们有四个需要测试的 web 服务器,分别是 webserver1、webserver2、webserver3 和 webserver4,并且这些服务器的 Ingress 规则运行在 192.168.100.100 上的 Ingress 控制器中。
如前所述,我们无需创建任何记录即可实现这一点。相反,我们可以使用命名约定,让 nip.io 为我们解析名称。每个 web 服务器将使用以下命名标准的名称:
<desired name>.<INGRESS IP>.nip.io
四个 web 服务器的名称列在下面的表格中:
| Web 服务器名称 | Nip.io DNS 名称 |
|---|---|
webserverl |
webserver1.192.168.100.100.nip.io |
webserver2 |
webserver2.192.168.100.100.nip.io |
webserver3 |
webserver3.192.168.100.100.nip.io |
webserver4 |
webserver4.192.168.100.100.nip.io |
表格 4.5: nip.io 示例域名
当你使用上述任意名称时,nip.io 会将它们解析为 192.168.100.100。你可以在以下截图中看到每个名称的示例 ping:

图 4.4: 使用 nip.io 进行名称解析示例
请记住,Ingress 规则需要唯一的名称才能正确地将流量路由到正确的服务。虽然在某些场景下可能不需要知道服务器的 IP 地址,但在 Ingress 规则中它变得至关重要。每个名称应该是唯一的,通常使用完整名称的第一部分。在我们的示例中,唯一名称是 webserver1、webserver2、webserver3 和 webserver4。
通过提供此服务,nip.io 使你能够在 Ingress 规则中使用任何名称,而无需在开发集群中设置 DNS 服务器。
现在你知道如何使用 nip.io 来解析集群的名称,接下来让我们解释如何在 Ingress 规则中使用 nip.io 名称。
创建 Ingress 规则
请记住,Ingress 规则使用名称将传入请求路由到正确的服务。
以下是一个图形表示,展示了传入请求如何通过 Ingress 路由流量:

图 4.5:Ingress 流量流动
图 4.5 显示了 Kubernetes 如何处理传入的 Ingress 请求的高层概览。为了更深入地解释每个步骤,让我们详细介绍五个步骤。通过使用 图 4.5 中提供的图形,我们将逐一解释每个编号步骤,展示 Ingress 如何处理请求:
-
用户在浏览器中请求名为
http://webserver1.192.168.200.20.nio.io的 URL。一个 DNS 请求被发送到本地 DNS 服务器,最终传递给nip.ioDNS 服务器。 -
nip.io服务器将域名解析为192.168.200.20的 IP 地址,并将其返回给客户端。 -
客户端将请求发送给运行在
192.168.200.20上的 Ingress 控制器。请求包含完整的 URL 名称,webserver1.192.168.200.20.nio.io。 -
Ingress 控制器会在配置的规则中查找请求的 URL 名称,并将其匹配到一个服务。
-
服务端点将用于将流量路由到分配的 pods。
-
请求被路由到运行 Web 服务器的端点 pod。
使用前述的流量流动示例,我们来创建一个 NGINX pod、服务和 Ingress 规则,看看它是如何工作的。在 chapter4/ingress 目录下,我们提供了一个名为 nginx-ingress.sh 的脚本,它将部署 Web 服务器并使用 webserver.w.x.y.nip.io 的 Ingress 规则将其暴露。当你执行脚本时,它将输出一个完整的 URL,你可以用来测试 Ingress 规则。
脚本将执行以下步骤来创建我们的新 NGINX 部署并使用 Ingress 规则将其暴露:
-
一个新的名为
nginx-web的 NGINX 部署被创建,Web 服务器使用端口8080。 -
我们创建了一个名为
nginx-web的服务,使用端口8080上的ClusterIP服务(默认设置)。 -
主机的 IP 地址被发现并用于创建一个新的 Ingress 规则,将使用主机名
webserver.w.x.y.z.nip.io。w.x.y.z的 Web 服务器将被替换为你主机的 IP 地址。
部署后,你可以通过使用脚本提供的 URL,从本地网络上的任何机器访问该 Web 服务器来进行测试。在我们的例子中,主机的 IP 地址是 192.168.200.20,所以我们的 URL 将是 webserver.192.168.200.20.nip.io。

图 4.6:使用 nip.io 的 NGINX Web 服务器作为 Ingress
根据本节提供的细节,可以为多个容器生成使用唯一主机名的 ingress 规则。需要注意的是,你并不局限于使用像 nip.io 这样的服务进行名称解析;你可以使用在你的环境中可访问的任何名称解析方法。在生产集群中,通常会有企业级 DNS 基础设施。然而,在实验环境中,比如我们的 KinD 集群,nip.io 是一个非常适合测试需要准确命名约定的场景的工具。
由于我们将在本书中使用 nip.io 命名标准,因此在继续下一章之前,理解命名约定非常重要。
在 Ingress 控制器中解析名称
如前所述,Ingress 控制器主要是第七层负载均衡器,主要关注的是 HTTP/S。那么,Ingress 控制器是如何获取主机名的呢?你可能认为它包含在网络请求中,但事实并非如此。客户端使用的是 DNS 名称,但在网络层面上,没有名称,只有 IP 地址。
那么,Ingress 控制器是如何知道你要连接哪个主机的呢?这取决于你是否使用 HTTP 或 HTTPS。如果你使用 HTTP,Ingress 控制器将从 Host HTTP 头部获取主机名。例如,这里有一个从 HTTP 客户端到集群的简单请求:
GET / HTTP/1.1
Host: k8sou.apps.192-168-2-14.nip.io
User-Agent: curl/7.88.1
Accept: */*
第二行告诉 Ingress 控制器你希望请求去往哪个主机以及哪个 Service。这在 HTTPS 中更为复杂,因为连接是加密的,解密需要发生在你能够读取 Host 头部之前。
你会发现,当使用 HTTPS 时,Ingress 控制器会根据你要连接的 Service 以及主机名,提供不同的证书。为了在还无法访问 Host HTTP 头部的情况下进行路由,Ingress 控制器将使用一种叫做服务器名称指示(SNI)的协议,该协议将请求的主机名作为 TLS 密钥交换的一部分。通过使用 SNI,Ingress 控制器能够在请求被解密之前,确定适用于请求的 Ingress 配置对象。
使用 Ingress 控制器处理非 HTTP 流量
使用 SNI 提供了一个有趣的副作用,这意味着 Ingress 控制器在使用 TLS 时可以在某种程度上充当 4 层负载均衡器。大多数 Ingress 控制器提供了一个名为 TLS passthrough 的功能,在这种情况下,Ingress 控制器不会解密流量,而是根据请求的 SNI 将其路由到相应的 Service。以我们之前提到的 web 服务器的后端数据库为例,如果你为你的 Ingress 对象配置了 TLS passthrough 注解(每个控制器的配置不同),那么你就可以通过 Ingress 来暴露你的数据库。
鉴于创建 Ingress 对象是如此容易,你可能会认为这是一项安全问题。这也是本书大量内容都专注于安全的原因。环境配置错误是非常容易发生的!
使用 TLS passthrough 的一个主要缺点是,除了潜在的安全问题之外,还会失去很多 Ingress 控制器的本地路由和控制功能。例如,如果你正在部署一个维护自己会话状态的 Web 应用程序,你通常会配置 Ingress 对象使用粘性会话,以便每个用户的请求都返回到相同的容器。这通常是通过在 HTTP 响应中嵌入 cookies 来实现的,但如果控制器只是传递流量,它就无法做到这一点。
像 NGINX Ingress 这样的 7 层负载均衡器常用于各种工作负载,包括 Web 服务器。然而,其他部署可能需要更为复杂的负载均衡器,在 OSI 模型的更低层次上运行。随着我们向下移动模型,我们将获得一些更低层次的特性,这些特性对于某些工作负载是必需的。
在继续讲解 4 层负载均衡器之前,如果你在集群中部署了 NGINX 示例,你应该先删除所有对象再继续。为了方便删除对象,你可以在 chapter4/ingress 目录下执行 ngnix-ingress-remove.sh 脚本。该脚本会删除部署、服务和 ingress 规则。
4 层负载均衡器
类似于 7 层负载均衡器,4 层负载均衡器也是网络的流量控制器,但与 7 层负载均衡器相比,它有许多不同之处。
7 层负载均衡器理解传入请求的内容,并根据请求的具体信息(如请求的网页或数据)做出决策。4 层负载均衡器则在更低层次上工作,查看传入网络流量中的基本信息,例如 IP 地址和端口,而不检查实际的数据内容。
当你访问一个网站或使用一个应用时,你的设备会向服务器发送一个请求,其中包含一个独特的 IP 地址和一个特定的端口号——也称为套接字。第四层负载均衡器观察这个地址和端口,以便高效地将传入的流量分配到多个服务器上。为了帮助你理解第四层负载均衡器的工作方式,可以将其想象为一个交通警察,能够高效地将进入的汽车引导到高速公路的不同车道上。负载均衡器并不知道每辆车的具体目的地或用途;它只会查看车牌号,并将它们引导到合适的车道,以确保交通流畅。
通过这种方式,第四层负载均衡器确保服务器公平地接收到传入的请求,并确保网络高效运行。它是确保网站和应用程序能够处理大量用户而不被压垮的必备工具,帮助维护一个稳定可靠的网络。
在此过程中有一些较低层次的网络操作,超出了本书的范围。HAProxy 在其官网上提供了术语的良好总结和示例配置,www.haproxy.com/fr/blog/loadbalancing-faq/。
总结来说,第四层负载均衡器是一种根据 IP 地址和端口号分配传入流量的网络工具,它使得网站和应用程序能够高效运行,提供无缝的用户体验。
第四层负载均衡器选项
如果你想为 Kubernetes 集群配置第四层负载均衡器,有多个选项可供选择。以下是一些选项:
-
HAProxy
-
NGINX Pro
-
Seesaw
-
F5 Networks
-
MetalLB
每个选项都提供第四层负载均衡,但在本书中,我们将使用MetalLB,它已成为为 Kubernetes 集群提供第四层负载均衡器的流行选择。
使用 MetalLB 作为第四层负载均衡器
记住,在第二章,使用 KinD 部署 Kubernetes中,我们展示了一个图表,显示了工作站和 KinD 节点之间的流量流动。由于 KinD 运行在嵌套的 Docker 容器中,所以在网络连接性方面,第四层负载均衡器会遇到一些限制。如果没有对 Docker 主机进行额外的网络配置,你将无法访问 Docker 主机外部使用LoadBalancer类型的服务。然而,如果你将MetalLB部署到运行在主机上的标准 Kubernetes 集群中,就不再受限于只能在主机内访问服务。
MetalLB 是一个免费的、易于配置的第四层负载均衡器。它包括强大的配置选项,使其能够在开发实验室或企业集群中运行。由于其高度的可配置性,它已成为许多需要第四层负载均衡的集群的流行选择。
我们将重点介绍以层 2 模式安装 MetalLB。这是一种简单的安装方式,适用于开发或小型 Kubernetes 集群。MetalLB 还提供了使用 BGP 模式部署的选项,这允许你建立对等伙伴以交换网络路由。如果你想了解 MetalLB 的 BGP 模式,可以访问 MetalLB 网站阅读:metallb.universe.tf/concepts/bgp/。
安装 MetalLB
在我们部署 MetalLB 并查看其运行效果之前,我们应当从新集群开始。虽然这不是必须的,但它可以避免之前章节中你可能测试过的资源引起的任何问题。要删除集群并重新部署一个新的集群,请按照以下步骤操作:
-
使用
kind delete命令删除集群。kind delete cluster --name cluster01 -
要重新部署一个新集群,切换到
chapter2目录(即你克隆仓库的地方) -
使用
chapter2目录根目录中的create-cluster.sh创建一个新集群。 -
部署完成后,切换到
chapter4/metallb目录
我们在 chapter4/metallb 目录中包含了一个名为 install-metallb.sh 的脚本。该脚本将使用名为 metallb-config.yaml 的预构建配置文件部署 MetalLB v0.13.10。完成后,集群将会部署 MetalLB 组件,包括控制器和发言人。
你可以查看脚本,了解每一步做了什么,查看注释后,执行以下步骤在集群中部署 MetalLB:
-
MetalLB 已经部署到集群中,脚本会等待 MetalLB 控制器完全部署好。
-
脚本将会查找 Docker 网络中使用的 IP 范围。这些范围将用于创建两个不同的池,用于负载均衡器服务。
-
脚本将根据地址池的值,将 IP 范围注入到两个资源中——
metallb-pool.yaml和metallb-pool-2.yaml。 -
第一个池通过
kubectl apply部署,并且它还会部署l2advertisement资源。 -
脚本将显示 MetalLB 命名空间中的 Pod,以确认它们已经被部署。
-
最后,一个名为
nginx-lb的 NGINX Web 服务器 Pod 将被部署,并且通过 MetalLB 的 IP 地址提供对部署的访问的负载均衡器服务。
MetalLB 资源,如地址池和 l2advertisement 资源,将在接下来的章节中进行讲解。
如果你想了解在部署 MetalLB 时可用的选项,可以访问 MetalLB 网站上的安装页面:metallb.universe.tf/installation/。
既然 MetalLB 已经部署到集群中,接下来我们来解释 MetalLB 配置文件,了解它如何处理请求。
了解 MetalLB 的自定义资源
MetalLB是通过两个自定义资源进行配置的,这些资源包含了 MetalLB 的配置。我们将在第二层模式下使用 MetalLB,并创建两个自定义资源:第一个是用于 IP 地址范围的IPAddressPool,第二个是配置哪些池被广播,称为L2Advertisement 资源。
OSI 模型和各层可能对许多读者来说是新的——第二层是指 OSI 模型中的一层;它在启用本地网络内的通信中起着至关重要的作用。这一层决定了设备如何利用网络基础设施,比如以太网电缆,并确定如何识别其他设备。第二层只处理本地网络段的事务;它不负责不同网络之间流量的转发。这是第三层(网络层)在 OSI 模型中的责任。
简单来说,你可以将第二层视为使同一网络内的设备能够通信的媒介。它通过为设备分配 MAC 地址(唯一地址)并提供发送和接收数据的方式来实现这一点,这些数据被组织成网络包。我们已经在chapter4/metallb目录中提供了预配置的资源,分别是metallb-pool.yaml和l2advertisement.yaml。这些文件将以第二层模式配置 MetalLB,并使用 Docker 网络的一部分 IP 地址范围,通过 L2Advertisement 资源进行广播。
为了简化配置,我们将使用 KinD 运行的 Docker 子网中的一个小范围。如果你在标准 Kubernetes 集群中运行 MetalLB,你可以分配任何在你的网络中可路由的范围,但 KinD 集群在处理网络流量时有一些限制。
让我们来详细了解一下我们是如何创建自定义资源的。首先,我们需要广告的 IP 范围,对于我们的 KinD 集群,这意味着我们需要知道 Docker 使用的网络范围。我们可以通过检查 KinD 所使用的桥接网络来获取子网,使用docker的network inspect命令:
docker network inspect kind | grep -i subnet
在输出中,你将看到分配的子网,类似于以下内容:
"Subnet": "172.18.0.0/16"
这是一个完整的B 类地址范围。我们知道我们不会使用所有的 IP 地址来运行容器,因此我们将在 MetalLB 配置中使用子网中的一个小范围。
注意:B 类这个术语是指 IP 地址如何根据不同网络规模的地址范围和结构进行分类。主要的分类有A 类、B 类和C 类。每个类别有一个特定的地址范围,并用于不同的目的。
这些类别有助于有效地组织和分配 IP 地址,确保不同规模的网络拥有适当的地址空间。对于私有网络——即未直接连接到互联网的网络——每个类别都有一个专门的 IP 范围保留用于内部使用:
-
A 类私有地址范围:
10.0.0.0到10.255.255.255 -
B 类私有地址范围:
172.16.0.0到172.31.255.255 -
C 类私有地址范围:
192.168.0.0到192.168.255.255
理解子网和类别范围非常重要,但超出了本书的范围。如果你是 TCP/IP 的新手,建议你阅读有关子网划分和类别范围的资料。
如果我们查看metallb-pool.yaml配置文件,我们将看到IPAddressPool的配置:
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: pool-01
namespace: metallb-system
spec:
addresses:
- 172.18.200.100-172.18.200.125
这个清单定义了一个新的IPAddressPool,名为pool-01,位于metallb-system命名空间中,IP 范围设置为172.18.200.100 到 172.18.200.125。
IPAddressPool仅定义了将分配给LoadBalancer服务的 IP 地址。要发布这些地址,你需要将池与L2Advertisement资源关联。在chapter4/metallb目录中,我们有一个预定义的L2Advertisement,名为l2advertisement.yaml,它与我们创建的地址池关联,如下所示:
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: l2-all-pools
namespace: metallb-system
在检查前面的清单时,你可能会注意到配置内容很少。正如我们之前提到的,IPAddressPool需要与L2Advertisement关联,但在我们当前的配置中,我们并未指定任何与我们创建的地址池的链接。那么,现在的问题是,我们的L2Advertisement如何发布或使用我们创建的IPAddressPool?
如果在L2Advertisement资源中没有指定任何池,则每个创建的IPAddressPool都会被公开。然而,如果你的场景只需要公开少数几个地址池,你可以将池的名称添加到L2Advertisement资源中,这样只会公开已分配的池。例如,如果我们在集群中有三个名为pool1、pool2和pool3的池,而我们只希望公开pool1和pool3,我们可以像下面这样创建一个L2Advertisement资源:
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: l2-all-pools
namespace: metallb-system
spec:
ipAddressPools:
- pool1
- pool3
配置完成后,我们将继续解释 MetalLB 的各个组件如何协作,将 IP 地址分配给服务。
MetalLB 组件
我们的部署使用了 MetalLB 项目提供的标准清单,将创建一个Deployment来安装 MetalLB 控制器,并创建一个DaemonSet,将第二个组件部署到所有节点,名为发言者。
控制器
控制器将从每个工作节点上的发言者接收公告。这些公告显示了每个请求了LoadBalancer服务的服务,并展示了控制器为该服务分配的 IP 地址:
{"caller":"main.go:49","event":"startUpdate","msg":"start of service update","service":"my-grafana-operator/grafana-operator-metrics","ts":"2020-04-21T21:10:07.437701161Z"}
{"caller":"service.go:98","event":"ipAllocated","ip":"10.2.1.72","msg":"IP address assigned by controller","service":"my-grafana-operator/grafana-operator-metrics","ts":"2020-04-21T21:10:07.438079774Z"}
{"caller":"main.go:96","event":"serviceUpdated","msg":"updated service object","service":"my-grafana-operator/grafana-operator-metrics","ts":"2020-04-21T21:10:07.467998702Z"}
在前面的示例输出中,一个名为my-grafana-operator/grafana-operator-metrics的Service已被部署,MetalLB 分配了 IP 地址10.2.1.72。
发言者
speaker 组件是 MetalLB 用于将 LoadBalancer 服务的 IP 地址公告到本地网络的组件。该组件在每个节点上运行,确保网络配置和路由器知晓分配给 LoadBalancer 服务的 IP 地址。这使得 LoadBalancer 可以在分配的 IP 地址上接收流量,而无需在每个节点上进行额外的网络接口配置。
MetalLB 中的 speaker 组件负责告诉本地网络如何访问你在 Kubernetes 集群中设置的服务。可以把它看作是传递信息的信使,告诉网络中的其他设备如何路由数据到达你的应用程序。
它主要负责四个任务:
-
服务检测:当在 Kubernetes 中创建服务时,speaker 组件始终在监视
LoadBalancer服务。 -
IP 地址管理:speaker 负责管理 IP 地址。它决定哪些 IP 地址应被分配,以使服务能够进行外部通信。
-
路由公告:在 MetalLB 的 speaker 确定需要外部访问的服务并分配 IP 地址后,它会在本地网络中传播路由信息。它向网络提供如何通过指定的 IP 地址连接到服务的指示。
-
负载均衡:MetalLB 执行网络负载均衡。如果你有多个 pod,所有应用程序都应该有,speaker 会将传入的网络流量分配到各个 pod,确保负载均衡,从而提高性能和可靠性。
默认情况下,它作为 DaemonSet 部署以实现冗余——无论部署了多少个 speaker,任何时候只有一个是活动的。主 speaker 会将所有 LoadBalancer 服务请求公告给控制器,如果该 speaker pod 出现故障,另一个 speaker 实例将接管公告任务。
如果我们查看某个节点的 speaker 日志,我们可以看到类似以下示例的公告:
{"caller":"main.go:176","event":"startUpdate","msg":"start of service update","service":"my-grafana-operator/grafana-operator-metrics","ts":"2020-04-21T21:10:07.437231123Z"}
{"caller":"main.go:189","event":"endUpdate","msg":"end of service update","service":"my-grafana-operator/grafana-operator-metrics","ts":"2020-04-21T21:10:07.437516541Z"}
{"caller":"main.go:176","event":"startUpdate","msg":"start of service update","service":"my-grafana-operator/grafana-operator-metrics","ts":"2020-04-21T21:10:07.464140524Z"}
{"caller":"main.go:246","event":"serviceAnnounced","ip":"10.2.1.72","msg":"service has IP, announcing","pool":"default","protocol":"layer2","service":"my-grafana-operator/grafana-operator-metrics","ts":"2020-04-21T21:10:07.464311087Z"}
{"caller":"main.go:249","event":"endUpdate","msg":"end of service update","service":"my-grafana-operator/grafana-operator-metrics","ts":"2020-04-21T21:10:07.464470317Z"}
上述公告是为 Grafana 组件发出的。在公告中,你可以看到该服务被分配了 10.2.1.72 的 IP 地址——此公告也会发送到 MetalLB 控制器,正如我们在前面的部分所展示的那样。
现在你已经安装了 MetalLB,并且了解了各组件如何创建服务,让我们在 KinD 集群上创建第一个 LoadBalancer 服务。
创建 LoadBalancer 服务
在第七层负载均衡部分,我们创建了一个运行 NGINX 的部署,通过创建服务和 Ingress 规则将其暴露。在这一部分结束时,我们删除了所有资源以为此次测试做准备。如果你按照 Ingress 部分的步骤操作并且还没有删除服务和 Ingress 规则,请在创建 LoadBalancer 服务之前删除它们。
MetalLB 的部署脚本包含了一个带有LoadBalancer服务的 NGINX 服务器。它将创建一个带有LoadBalancer服务的 NGINXDeployment,并将监听端口80。LoadBalancer服务将从我们定义的地址池中分配一个 IP 地址,由于这是第一个使用地址池的服务,可能会分配到172.18.200.100。
你可以通过在 Docker 主机上使用curl来测试该服务。使用分配给该服务的 IP 地址,输入以下命令:
curl 172.18.200.100
你将收到以下输出:

图 4.7:使用curl访问运行 NGINX 的 LoadBalancer 服务的输出
将 MetalLB 添加到集群中,允许你暴露那些无法通过 7 层负载均衡器暴露的应用程序。将 7 层和 4 层服务添加到你的集群中,使你能够暴露几乎任何类型的应用程序,包括数据库。
在接下来的章节中,我们将解释一些用于创建高级IPAddressPool配置的高级选项。
高级池配置
MetalLB 的IPAddressPool资源提供了许多在不同场景中有用的高级选项,包括禁用自动分配地址、使用静态 IP 地址和多个地址池、将池范围限制在某个命名空间或服务中,以及处理网络故障。
禁用自动地址分配
当创建一个池时,它会自动开始为任何请求LoadBalancer类型的服务分配地址。虽然这是常见的实现方式,但你可能有特殊的用例,要求池只有在明确请求时才分配地址。
为服务分配静态 IP 地址
当一个服务从池中分配到 IP 地址时,服务将保持该 IP,直到服务被删除并重新创建。根据创建的LoadBalancer服务的数量,有可能在重新创建时分配相同的 IP 地址,但不能保证,我们必须假设 IP 可能会发生变化。
如果我们有像external-dns这样的附加组件(将在下一章介绍),你可能不关心服务的 IP 地址变化,因为你可以使用与分配的 IP 地址注册的名称。在某些场景下,你可能无法选择是否使用 IP 或名称来访问服务,如果地址在重新部署时发生变化,可能会遇到问题。
截至本文写作时,Kubernetes 包括了通过向服务资源中添加spec.loadBalancerIP并指定所需的 IP 地址,来为服务分配 IP 地址的能力。通过使用此选项,你可以“静态”地为服务分配 IP 地址,并且如果服务被删除并重新部署,它将保持不变。这在多种场景中非常有用,包括将已知 IP 添加到其他系统,如Web 应用防火墙(WAF)和防火墙规则中。
从Kubernetes 1.24开始,loadBalancerIP规格已被废弃,尽管在Kubernetes 1.27中仍然有效,但该字段可能会在未来的 K8s 版本中被移除。由于该选项将来会被移除,建议使用你部署的四层负载均衡器中包含的解决方案。对于 MetalLB,它们添加了一个注释metallb.universe.tf/loadBalancerIPs,用于分配 IP 地址。将此字段设置为所需的 IP 地址,将实现与已废弃的spec.loadBalancerIP相同的目标。
你可能会认为分配静态 IP 可能会带来一些潜在风险,比如 IP 冲突,导致连接问题。幸运的是,MetalLB 提供了一些功能来缓解这些潜在风险。如果 MetalLB 不是所请求地址的所有者,或者该地址已经被其他服务使用,IP 分配将失败。如果发生这种情况,MetalLB 将生成一个警告事件,可以通过运行kubectl describe service <service name>命令来查看。
以下清单展示了如何使用 Kubernetes 原生的loadBalancerIP和 MetalLB 的注释来为服务分配静态 IP 地址。第一个示例展示了已经废弃的spec.loadBalancerIP,将 IP 地址172.18.200.210分配给服务:
apiVersion: v1
kind: Service
metadata:
name: nginx-web
spec:
ports:
- port: 80
targetPort: 8080
selector:
app: nginx-web
type: LoadBalancer
loadBalancerIP: 172.18.200.210
以下示例展示了如何设置 MetalLB 的注释以分配相同的 IP 地址:
apiVersion: v1
kind: Service
metadata:
name: nginx-web
annotations:
metallb.universe.tf/loadBalancerIPs: 172.18.200.210
spec:
ports:
- port: 80
targetPort: 8080
selector:
app: nginx-web
type: LoadBalancer
下一部分将讨论如何将额外的地址池添加到你的 MetalLB 配置中,以及如何使用新池为服务分配 IP 地址。
使用多个地址池
在我们原来的示例中,我们为集群创建了一个单节点池。对于一个集群来说,拥有一个单一地址池并不罕见,但在更复杂的环境中,你可能需要添加额外的池来将流量引导到某个特定网络,或者仅仅因为原始池的地址用尽而需要添加额外的池。
你可以在集群中创建任意数量的地址池。我们在第一个池中分配了若干个地址,现在需要添加一个额外的池来处理集群中的工作负载数量。要创建一个新的池,我们只需要部署一个新的IPAddressPool,如下所示:
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: pool-02
namespace: metallb-system
spec:
addresses:
- 172.18.201.200-172.18.201.225
当前版本的 MetalLB 需要重新启动 MetalLB 控制器,以便新地址池可用。
请注意,这个池的名称是pool-01,其范围是172.18.201.200 – 172.18.201.225,而我们原来的池是pool-01,其范围是172.18.200.200 – 172.18.200.225。由于我们已经部署了一个L2Advertisement资源,它公开了IPAddressPools,因此我们无需为新池进行任何额外的配置。
现在我们在集群中有两个活动的池,我们可以使用 MetalLB 的一个注释metallb.universe.tf/address-pool,在服务中指定我们想要从中获取 IP 地址的池,如下所示:
apiVersion: v1
kind: Service
metadata:
name: nginx-web
annotations:
metallb.universe.tf/address-pool: pool-02
spec:
ports:
- port: 80
targetPort: 8080
selector:
app: nginx-web
type: LoadBalancer
如果我们部署此服务清单,并查看命名空间中的服务,我们会看到它已从新的地址池pool-02中分配了一个 IP 地址:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx-lb-pool02 LoadBalancer 10.96.52.153 172.18.201.200 80:30661/TCP 3m8s
我们的集群现在为LoadBalancer服务提供了基于工作负载需求选择使用pool-01或pool-02的选项。
你可能会想知道,如果一个服务请求没有明确指定使用哪个地址池,多个地址池是如何工作的。这个问题非常好,我们可以通过在创建地址池时为其设置一个值,称为优先级,来控制这一点,从而定义分配 IP 地址的池的顺序。
地址池是一个强大的功能,提供了高度可配置和灵活的解决方案,将适当的 IP 地址池提供给特定的服务。
MetalLB 的灵活性不仅仅体现在地址池上。你可能会发现有需求需要创建一个仅允许特定命名空间使用的池。这被称为IP 池作用域,在下一节中,我们将讨论如何配置作用域以根据命名空间限制池的使用。
当多个IPAddressPools可用时,MetalLB 通过根据优先级对匹配的池进行排序来确定 IP 地址的可用性。排序从最高优先级(最低优先级数字)开始,然后按优先级从高到低排列。如果多个IPAddressPools具有相同的优先级,MetalLB 会随机选择其中一个。如果一个池没有设置特定的优先级或优先级为 0,它将被视为最低优先级,仅在无法使用具有定义优先级的池时才会进行分配。
在以下示例中,我们创建了一个新的地址池,名为pool-03,并设置了50的优先级,另一个池名为pool-04,优先级为70:
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: pool-03
namespace: metallb-system
spec:
addresses:
- 172.168.210.0/24
serviceAllocation:
priority: 50
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: pool-04
namespace: metallb-system
spec:
addresses:
- 172.168.211.0/24
serviceAllocation:
priority: 70
如果你创建一个服务而没有选择池,请求将匹配前面显示的两个池。由于pool-03的优先级数字较低,因此它的优先级较高,将优先于pool-04使用,除非该池已用完地址,这时请求将使用来自pool-04地址池的 IP。
如你所见,地址池功能强大且灵活,提供了多种选项来满足不同的工作负载需求。我们已经讨论了如何使用注释选择池,以及不同优先级的池是如何工作的。在下一节中,我们将讨论如何将地址池与特定命名空间关联,从而限制哪些工作负载可以从特定的地址池请求 IP 地址。
IP 池作用域
多租户集群在企业环境中很常见,并且默认情况下,MetalLB 地址池可供任何部署的LoadBalancer服务使用。虽然对于许多组织来说这可能不是问题,但你可能需要限制某个或某些池,仅允许特定命名空间使用,从而限制哪些工作负载可以使用特定的地址池。
要定义地址池的作用域,我们需要向IPAddressPool资源添加一些字段。在我们的示例中,我们希望部署一个地址池,将整个C 类范围仅限于web和sales两个命名空间:
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: ns-scoped-pool
namespace: metallb-system
spec:
addresses:
- 172.168.205.0/24
serviceAllocation:
priority: 50
namespaces:
- web
- sales
部署该资源后,只有位于web或sales命名空间中的服务才能从该池中请求地址。如果从任何其他命名空间请求ns-scoped-pool,则会被拒绝,并且不会分配172.168.205.0范围内的 IP 地址给该服务。
我们将在下一节讨论的最后一个选项是处理有问题的网络。
处理有问题的网络
MetalLB 有一个字段,某些网络可能需要处理以.0或.255结尾的 IP 块。较旧的网络设备可能会将该流量标记为潜在的Smurf攻击并阻止它。如果你碰到这种情况,你需要将IPAddressPool资源中的AvoidBuggyIPs字段设置为true。
从高层次来看,Smurf攻击会向特殊地址发送大量网络消息,这些消息会到达网络上的所有计算机。流量使所有计算机都认为流量来自某个特定地址,导致所有计算机向该特定机器发送响应。此流量会导致拒绝服务攻击,使机器脱机,并中断正在运行的任何服务。
为了避免这个问题,设置AvoidBuggyIPs字段可以防止使用.0和.255地址。这里展示了一个示例清单:
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: buggy-example-pool
namespace: metallb-system
spec:
addresses:
- 172.168.205.0/24
avoidBuggyIPs: true
将 MetalLB 作为第 4 层负载均衡器添加到集群中,可以帮助你迁移可能不适用于简单第 7 层流量的应用程序。
随着更多应用程序被迁移或重构为容器,你将遇到许多需要多个协议才能支持单一服务的应用程序。在下一节中,我们将解释一些需要为单一服务使用多个协议的场景。
使用多个协议
早期版本的 Kubernetes 不允许为LoadBalancer服务分配多个协议。如果你尝试为单一服务同时分配 TCP 和 UDP,你会收到一个错误,提示不支持多协议,且该资源将无法部署。
尽管 MetalLB 仍然提供对这个功能的支持,但由于 Kubernetes 的更新版本在1.20版本中引入了一个名为MixedProtocolLBService的 alpha 功能开关,使用这些注释的动力已经不大。这个功能自 Kubernetes 版本1.26开始提供通用可用性,成为基础功能,允许在定义多个端口时为LoadBalancer类型的服务使用不同协议。
以CoreDNS为例,我们需要将 CoreDNS 暴露给外部世界。我们将在下一章中解释一个用例,其中需要使用 TCP 和 UDP 同时暴露一个 CoreDNS 实例。
由于 DNS 服务器在某些操作中使用 TCP 和 UDP 端口53,我们需要创建一个服务,将我们的服务暴露为LoadBalancer类型,监听 TCP 和 UDP 的端口53。通过以下示例,我们创建一个同时定义了 TCP 和 UDP 的服务:
apiVersion: v1
kind: Service
metadata:
name: coredns-ext
namespace: kube-system
spec:
selector:
k8s-app: kube-dns
ports:
- name: dns-tcp
port: 53
protocol: TCP
targetPort: 53
- name: dns-udp
port: 53
protocol: UDP
targetPort: 53
type: LoadBalancer
如果我们部署了清单文件,然后查看kube-system命名空间中的服务,我们会看到服务已成功创建,并且 TCP 和 UDP 的端口53都已暴露:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
coredns-ext LoadBalancer 10.96.192.4 172.18.200.101 53:31889/TCP,53:31889/UDP 6s
您将看到新创建的服务coredns-ext,被分配了 IP 地址172.18.200.101,并在 TCP 和 UDP 端口53上暴露。这将允许服务通过这两种协议接受端口53上的连接。
许多负载均衡器面临的一个问题是,它们没有为服务 IP 提供名称解析。用户更愿意使用容易记住的名称而不是随机的 IP 地址来访问服务。Kubernetes 不提供为服务创建外部可访问名称的能力,但有一个孵化器项目可以启用此功能。在第五章中,我们将解释如何为 Kubernetes 服务提供外部名称解析。
在本章的最后部分,我们将讨论如何使用网络策略来保护我们的工作负载。
引入网络策略
安全性是所有 Kubernetes 用户从第一天起就应该关注的内容。默认情况下,集群中的每个 Pod 都可以与集群中的任何其他 Pod 通信,甚至是您可能不拥有的其他命名空间。虽然这是 Kubernetes 的基本概念,但对于大多数企业来说并不理想,尤其是在使用多租户集群时,这会成为一个巨大的安全隐患。我们需要提高工作负载的安全性和隔离性,这可能是一项非常复杂的任务,而这正是网络策略发挥作用的地方。
NetworkPolicies为用户提供了控制其网络流量的能力,适用于出站和入站流量,使用一组定义的规则来控制 Pods、命名空间和外部端点之间的流量。可以将网络策略视为集群的防火墙,基于各种参数提供细粒度的访问控制。使用网络策略,您可以控制哪些 Pods 可以与其他 Pods 通信,限制流量到特定的协议或端口,并强制执行加密和身份验证要求。
像我们讨论的其他大多数 Kubernetes 对象一样,网络策略允许基于标签和选择器进行控制。通过匹配网络策略中指定的标签,Kubernetes 可以确定哪些 Pods 和命名空间应该被允许或拒绝网络访问。
网络策略是 Kubernetes 中的一个可选功能,集群中使用的 CNI 必须支持它们才能使用。在我们创建的 KinD 集群中,我们部署了Calico,它支持网络策略。然而,并非所有网络插件都能开箱即用地支持网络策略,因此在部署集群之前,计划好您的需求非常重要。
在本节中,我们将解释网络策略提供的选项,以增强您应用程序和集群的整体安全性。
网络策略对象概述
网络策略提供了许多选项来控制ingress和egress流量。它们可以非常精细地控制,允许特定的 Pod、命名空间甚至 IP 地址来管理网络流量。
网络策略有四个部分。每个部分在下表中进行了描述。
| Spec | 描述 |
|---|---|
podSelector |
使用标签选择器来限制策略应用的工作负载范围。如果未提供选择器,策略将影响命名空间中的所有 Pod。 |
policyTypes |
这定义了策略规则。有效的类型有ingress和egress。 |
ingress |
(可选)定义了进入流量的规则。如果没有定义规则,它将匹配所有进入的流量。 |
egress |
(可选)定义了外出流量的规则。如果没有定义规则,它将匹配所有外出的流量。 |
表 4.6:网络策略的组成部分
策略中的ingress和egress部分是可选的。如果你不想阻止任何egress流量,只需省略egress部分。如果没有定义部分,则所有流量将被允许。
Pod 选择器
podSelector字段用于告诉你网络策略将影响哪些工作负载。如果你希望策略只影响某个部署,你可以定义一个与部署中的标签匹配的标签。标签选择器不限于单个条目;你可以将多个标签选择器添加到网络策略,但所有选择器必须匹配才能将策略应用于 Pod。如果你希望策略应用于所有 Pod,留空podSelector,它将把策略应用于命名空间中的每个 Pod。
在以下示例中,我们定义了该策略只会应用于匹配标签app=frontend的 Pod:
spec:
podSelector:
matchLabels:
app: frontend
下一字段是策略类型,这是定义ingress和egress策略的位置。
策略类型
policyType字段指定所定义的策略类型,决定NetworkPolicy的范围和行为。policyType有两个可用选项:
| policyType | 描述 |
|---|---|
ingress |
ingress控制 Pod 的进入网络流量。它定义了控制允许访问与NetworkPolicy中指定的podSelector匹配的 Pod 的源的规则。流量可以从特定的 IP CIDR 范围、命名空间或集群内的其他 Pod 允许通过。 |
egress |
egress控制从 Pod 发出的网络流量。它定义了控制 Pod 允许通信的目标的规则。可以通过特定的 IP CIDR 范围、命名空间或集群内的其他 Pod 来限制外出流量。 |
表 4.7:策略类型
策略可以包含ingress、egress或两者。如果未包含某种策略类型,则不会影响该流量类型。例如,如果你只包括ingress的policyType,则所有的 egress 流量将在网络的任何位置都被允许。
如我们所述,当你为ingress或egress流量创建规则时,你可以提供没有标签、单个标签或多个标签,必须匹配这些标签才能使策略生效。以下示例展示了一个具有三个标签的ingress块;为了使策略影响一个工作负载,所有三个声明的字段必须匹配:
ingress:
- from:
- ipBlock:
cidr: 192.168.0.0/16
- namespaceSelector:
matchLabels:
app: backend
- podSelector:
matchLabels:
app: database
在前面的示例中,任何传入的流量都需要来自一个在192.168.0.0子网中的工作负载,位于标签为app=backend的命名空间,并且请求的 Pod 必须具有app=database的标签。
虽然示例展示了ingress流量的选项,但相同的选项也适用于egress流量。
现在我们已经涵盖了网络策略中可用的选项,让我们继续使用一个实际示例创建完整的策略。
创建网络策略
我们在本书的 GitHub 仓库中的chapter4/netpol目录下包含了一个网络策略脚本,名为netpol.sh。当你执行该脚本时,它将创建一个名为sales的命名空间,其中包含几个带有标签的 Pod 和一个网络策略。创建的策略将是我们在本节中讨论的策略的基础。
当你创建网络策略时,你需要理解所需的网络限制。最了解应用程序流量流动的人最适合帮助创建有效的策略。你需要考虑应该能够通信的 Pod 或命名空间,哪些协议和端口应该被允许,以及是否需要额外的安全措施,如加密或身份验证。
像其他 Kubernetes 对象一样,你需要使用NetworkPolicy对象创建一个清单,并提供元数据,比如策略的名称和它将被部署的命名空间。
让我们举一个例子,假设你有一个后台 Pod 在与 Web 服务器同一个命名空间中运行PostgreSQL。我们知道,唯一需要与数据库服务器通信的 Pod 是 Web 服务器本身,其他任何通信都不应允许访问数据库。
首先,我们需要创建我们的清单,它将通过声明 API、类型、策略名称和命名空间来开始:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backend-netpol
namespace: sales
下一步是添加 Pod 选择器,以指定将受策略影响的 Pod。这是通过创建一个podSelector部分来完成的,在这里你定义了基于匹配标签的 Pod 选择器。对于我们的示例,我们希望将策略应用于作为后台数据库应用程序一部分的 Pod。应用程序的 Pod 都已标记为app=backend-db:
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: postgresql
现在我们已经声明了要匹配的 pods,我们需要定义ingress和egress规则,这些规则通过策略的spec.ingress或spec.egress部分进行定义。对于每种规则类型,您可以设置允许的协议和端口,并控制外部请求从哪里访问该端口。以我们的示例为基础,我们希望添加一个ingress规则,允许带有标签app=backend的 pods 从端口5432访问:
spec:
podSelector:
matchLabels:
app: frontend
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 5432
作为最后一步,我们将定义我们的策略类型。由于我们只关心 PostgreSQL pod 的传入流量,我们只需要声明一种类型,ingress:
policyTypes:
- Ingress
一旦该策略部署,sales命名空间中具有app=backend-db标签的 pods 只会接收来自带有app=frontend标签的 pods 在 TCP 端口5432上的流量。来自前端 pod 的任何非端口5432的请求都会被拒绝。该策略使得我们的 PostgreSQL 部署非常安全,因为任何传入流量都会严格锁定到特定工作负载和 TCP 端口。
当我们从仓库执行脚本时,它将部署 PostgreSQL 到集群并向部署中添加一个标签。我们将使用该标签将网络策略与 PostgreSQL pod 绑定。为了测试连接性和网络策略,我们将运行一个 netshoot pod,并使用 telnet 测试连接到端口 5432 上的 pod。
我们需要知道 IP 地址来测试网络连接。要获取数据库服务器的 IP 地址,我们只需使用-o wide选项列出命名空间中的 pods,以列出 pods 的 IP 地址。现在 PostgreSQL 正在运行,我们将通过运行一个标签与 app: frontend 不匹配的 netshoot pod 来模拟连接,这将导致连接失败。请看以下内容:
kubectl get pods -n sales -o wide
NAME READY STATUS RESTARTS AGE IP
db-postgresql-0 0/1 Running 0 45s 10.240.189.141
kubectl run tmp-shell --rm -i --tty --image nicolaka/netshoot --labels app=wronglabel -n sales
tmp-shell ~ telnet 10.240.189.141:5432
由于传入的请求带有标签app=wronglabel,连接最终会超时。策略会查看传入请求的标签,如果没有匹配的标签,它将拒绝连接。
最后,让我们看看我们是否正确地创建了策略。我们将再次运行netshoot,但这次使用正确的标签,我们将看到连接成功:
kubectl run tmp-shell --rm -i --tty --image nicolaka/netshoot --labels app=frontend -n sales
tmp-shell ~ telnet 10.240.189.141:5432
Connected to 10.240.189.141:5432
注意那一行,写着Connected to 10.240.189.141:5432。这表明 PostgreSQL pod 接受了来自 netshoot pod 的传入请求,因为该 pod 的标签与网络策略匹配,而网络策略在寻找标签app=frontend。
那么,为什么网络策略只允许端口5432呢?我们没有设置任何选项来拒绝流量;我们只定义了允许的流量。网络策略遵循默认的拒绝所有规则,对于任何未定义的策略都会拒绝所有流量。在我们的示例中,我们只定义了端口5432,因此任何不在端口5432上的请求都会被拒绝。对于任何未定义的通信执行拒绝所有策略有助于通过避免任何无意的访问来保障您的工作负载的安全。
如果你想创建一个拒绝所有流量的网络策略,你只需创建一个新策略,其中包含ingress和egress,而不添加其他值。以下是一个示例:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all
namespace: sales
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
在这个示例中,我们将podSelector设置为{},这意味着该策略将应用于命名空间中的所有 Pods。在spec.ingress和spec.egress选项中,我们没有设置任何值,默认行为是拒绝任何没有规则的通信,因此此规则将拒绝所有的入站和出站流量。
创建网络策略的工具
手动创建网络策略可能会很困难。尤其是在你不是应用程序所有者的情况下,知道需要打开哪些端口可能会有挑战。
在第十三章中,KubeArmor 保障你的运行时,我们将讨论一个名为KubeArmor的工具,它是一个由名为AccuKnox的公司捐赠的CNCF项目。KubeArmor 最初是一个用于保护容器运行时的工具,但最近他们添加了监控 Pods 之间网络流量的功能。通过监控流量,它们可以了解 Pod 的“正常行为”,并为每个观察到的网络策略在命名空间中创建一个ConfigMap。
我们将在第十三章中详细介绍;现在,我们只是想让你知道,有一些工具可以帮助你创建网络策略。
在下一章中,我们将学习如何使用CoreDNS通过一个名为external-dns的孵化器项目在 DNS 中创建服务名称条目。我们还将介绍一个令人兴奋的 CNCF 沙盒项目——K8GB,它提供了一个具有 Kubernetes 原生全局负载均衡功能的集群。
总结
在本章中,你学习了如何将你的工作负载暴露给 Kubernetes 中的其他集群资源和外部流量。
本章的第一部分介绍了服务及其可以分配的多种类型。三种主要的服务类型是ClusterIP、NodePort和LoadBalancer。请记住,选择服务类型将配置你的应用如何暴露。
在第二部分中,我们介绍了两种负载均衡器类型,分别是第 4 层和第 7 层,每种类型都有其独特的功能来暴露工作负载。你通常会使用ClusterIP服务与入口控制器一起提供访问第 7 层的服务。有些应用可能需要额外的通信,这些通信第 7 层负载均衡器无法提供。这些应用可能需要使用第 4 层负载均衡器来暴露它们的服务。在负载均衡部分,我们展示了如何安装和使用MetalLB,一种流行的开源第 4 层负载均衡器。
我们在本章最后讨论了如何保护 ingress 和 egress 网络流量。由于 Kubernetes 默认允许集群中所有 pods 之间的通信,大多数企业环境需要一种方法来保护工作负载之间的通信,仅限于应用所需的流量。网络策略是保护集群并限制进出流量的一种强大工具。
你可能对暴露工作负载还有一些问题,例如:我们如何处理使用 LoadBalancer 类型的服务的 DNS 条目?或者,也许,我们如何在两个集群之间使部署具有高可用性?
在下一章,我们将扩展讨论一些有用的工具,它们可以帮助你暴露工作负载,例如名称解析和全局负载均衡。
问题
-
服务如何知道哪些 pods 应该作为服务的端点?
-
通过服务端口
-
通过命名空间
-
作者
-
通过选择器标签
-
答案:d
-
哪个
kubectl命令可以帮助你排查可能无法正常工作的服务?-
kubectl get services <service name> -
kubectl get ep <service name> -
kubectl get pods <service name> -
kubectl get servers <service name>
-
答案:b
-
所有 Kubernetes 发行版都支持使用
LoadBalancer类型的服务。-
正确
-
错误
-
答案:b
-
哪种负载均衡器类型支持所有 TCP/UDP 端口,并且接受无论数据包内容如何的流量?
-
第七层
-
Cisco 层
-
第二层
-
第四层
-
答案:d
第五章:外部 DNS 和全局负载均衡
在本章中,我们将在第四章的基础上进行讲解。我们将讨论一些负载均衡器功能的局限性,以及如何配置集群来解决这些局限性。
我们知道 Kubernetes 有一个内置的 DNS 服务器,它动态地为资源分配名称。这些名称用于应用程序在集群内部进行通信。虽然这个功能对集群内部通信有利,但它并不为外部工作负载提供 DNS 解析。既然它提供 DNS 解析,为什么我们还说它有局限性呢?
在上一章中,我们使用动态分配的 IP 地址来测试我们的LoadBalancer服务工作负载。虽然我们的示例对于学习很有帮助,但在企业环境中,没人想通过 IP 地址访问运行在集群上的工作负载。为了解决这一限制,Kubernetes SIG 开发了一个名为ExternalDNS的项目,它提供了动态为我们的LoadBalancer服务创建 DNS 条目的功能。
此外,在企业环境中,您通常会运行多个集群上的服务,以为您的应用提供故障转移。到目前为止,我们讨论的选项无法处理故障转移场景。在本章中,我们将解释如何实现一个解决方案,为工作负载提供自动故障转移,使其在多个集群之间高度可用。
本章您将学习以下内容:
-
外部 DNS 解析和全局负载均衡简介
-
在 Kubernetes 集群中配置和部署 ExternalDNS
-
自动化 DNS 名称注册
-
将 ExternalDNS 与企业 DNS 服务器集成
-
使用 GSLB 提供跨多个集群的全局负载均衡
现在,让我们进入这一章的内容!
技术要求
本章具有以下技术要求:
-
运行 Docker 的 Ubuntu 22.04+服务器,至少需要 4GB 的内存,推荐 8GB
-
运行MetalLB的 KinD 集群——如果您已完成第四章,那么您应该已经拥有一个运行 MetalLB 的集群
-
仓库中
chapter5文件夹中的脚本,您可以通过访问本书的 GitHub 仓库来获取:github.com/PacktPublishing/Kubernetes-An-Enterprise-Guide-Third-Edition
使服务名称对外可用
如我们在简介中提到的,您可能还记得我们曾使用 IP 地址来测试我们创建的LoadBalancer服务,而对于我们的Ingress示例,我们使用了域名。为什么我们必须使用 IP 地址而不是主机名来访问我们的LoadBalancer服务呢?
尽管 Kubernetes 负载均衡器为服务分配了一个标准的 IP 地址,但它不会自动为工作负载生成一个 DNS 名称来访问服务。相反,你必须依赖 IP 地址来连接集群中的应用程序,这变得混乱且低效。此外,手动为每个由 MetalLB 分配的 IP 注册 DNS 名称会带来维护挑战。为了提供更接近云的体验并简化 LoadBalancer 服务的名称解析,我们需要一个能够解决这些限制的附加组件。
类似于维护 KinD 的团队,有一个 Kubernetes SIG 正在为 Kubernetes 开发这个功能,叫做 ExternalDNS。该项目的主页面可以在 SIG 的 GitHub 上找到:github.com/kubernetes-sigs/external-dns。
在编写时,ExternalDNS 项目支持 34 种兼容的 DNS 服务,包括以下内容:
-
AWS Cloud Map
-
亚马逊的 Route 53
-
Azure DNS
-
Cloudflare
-
CoreDNS
-
Google Cloud DNS
-
Pi-hole
-
RFC2136
根据你所运行的主 DNS 服务器类型,有多种方法可以扩展 CoreDNS 来解析外部名称。许多支持的 DNS 服务器会动态注册任何服务。ExternalDNS 会看到创建的资源并使用本机调用自动注册服务,像 Amazon’s Route 53。并非所有 DNS 服务器默认都允许这种类型的动态注册。
在这些情况下,你需要手动配置主 DNS 服务器,将所需的域请求转发到运行在集群中的 CoreDNS 实例。这就是我们将在本章示例中使用的内容。
我们的 Kubernetes 集群目前使用 CoreDNS 来处理集群的 DNS 名称解析。然而,可能不太为人所知的是,CoreDNS 提供的不仅仅是内部集群的 DNS 解析。它可以扩展其功能,执行外部名称解析,实际上可以解析由 CoreDNS 部署管理的任何 DNS 区域的名称。
现在,让我们继续介绍 ExternalDNS 的安装过程。
设置 ExternalDNS
现在,我们的 CoreDNS 只为内部集群名称解析名称,因此我们需要为新的 LoadBalancer DNS 条目设置一个区域。
对于我们的示例,一个公司 FooWidgets 想让所有 Kubernetes 服务都进入 foowidgets.k8s 域,因此我们将使用该域作为新的区域。
集成 ExternalDNS 和 CoreDNS
ExternalDNS 不是一个实际的 DNS 服务器;它是一个控制器,负责监控请求新 DNS 条目的对象。一旦控制器看到请求,它会将信息发送到实际的 DNS 服务器(如 CoreDNS)进行注册。
服务注册的过程如下面的图所示。

图 5.1:ExternalDNS 注册流程
在我们的示例中,我们使用 CoreDNS 作为我们的 DNS 服务器;然而,正如我们之前提到的,ExternalDNS 支持 34 种不同的 DNS 服务,并且支持的服务列表还在不断增加中。因为我们将使用 CoreDNS 作为我们的 DNS 服务器,我们需要添加一个组件来存储 DNS 记录。为了实现这一点,我们需要在集群中部署一个 ETCD 服务器。
对于我们的示例部署,我们将使用 ETCD Helm chart。
Helm 是一个用于 Kubernetes 的工具,它使得部署和管理应用程序变得更加容易。它使用 Helm charts,这些模板包含了应用程序所需的配置和资源值。它自动设置复杂应用程序的环境,确保它们在配置上保持一致和可靠。这是一个强大的工具,您会发现许多项目和供应商默认使用 Helm charts 提供他们的应用程序。您可以在他们的主页 v3.helm.sh/ 上了解更多关于 Helm 的信息。
Helm 如此强大的一个原因是它能够使用自定义选项,在运行 helm install 命令时声明这些选项。同样的选项也可以在通过 -f 选项传递给安装的文件中声明。这些选项使得部署复杂系统变得更加简单和可重现,因为可以在任何部署中使用相同的值文件。
对于我们的部署示例,我们已经包含了一个位于 chapter5/etcd 目录中的 values.yaml 文件,我们将使用它来配置我们的 ETCD 部署。
现在,最后,让我们部署 ETCD 吧!我们在 chapter5/etcd 目录中包含了一个名为 deploy-etcd.sh 的脚本,它将在名为 etcd-dns 的新命名空间中部署一个单副本的 ETCD。在 chapter5/etcd 目录中执行该脚本。
由于 Helm 的帮助,脚本只需两个命令 – 它将创建一个新的命名空间,然后执行一个 Helm install 命令来部署我们的 ETCD 实例。在实际环境中,您可能希望将副本数量更改至至少 3 以实现高可用的 ETCD 部署,但我们希望限制我们的 KinD 服务器的资源需求。
现在我们已经为我们的 DNS 配置了 ETCD,我们可以继续将我们的 CoreDNS 服务与我们的新 ETCD 部署集成。
添加一个 ETCD 区域到 CoreDNS
正如我们在上一节的图表中展示的那样,CoreDNS 将在一个 ETCD 实例中存储 DNS 记录。这要求我们配置一个 CoreDNS 服务器,其中包括我们想要在其中注册名称的 DNS 区域以及将存储记录的 ETCD 服务器。
为了保持资源需求较低,我们将使用大多数 Kubernetes 安装中包含的 CoreDNS 服务器作为我们新域的基本集群创建的一部分。在实际环境中,您应该部署一个专用的 CoreDNS 服务器来处理仅限于 ExternalDNS 注册的任务。
在本节结束时,你将执行一个脚本来部署一个完全配置的 ExternalDNS 服务,其中包含本节中讨论的所有选项和配置。本节中使用的命令仅供参考;你不需要在集群中执行这些命令,因为脚本将为你完成这些操作。
在我们可以集成 CoreDNS 之前,我们需要知道新 ETCD 服务的 IP 地址。你可以通过使用kubectl列出etcd-dns命名空间中的服务来获取地址:
kubectl get svc etcd-dns -n etcd-dns
这将显示我们的 ETCD 服务以及该服务的 IP 地址:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
etcd-dns ClusterIP 10.96.149.223 <none> 2379/TCP,2380/TCP 4m
你在服务列表中看到的Cluster-IP将用于配置新的 DNS 区域,作为存储 DNS 记录的位置。
部署 ExternalDNS 时,你可以通过两种方式配置 CoreDNS:
-
向 Kubernetes 集成的 CoreDNS 服务添加区域。
-
部署一个新的 CoreDNS 服务,该服务将用于 ExternalDNS 注册。
为了便于测试,我们将向 Kubernetes CoreDNS 服务添加一个区域。这需要我们编辑位于kube-system命名空间中的 CoreDNS ConfigMap。当你执行本节末尾的脚本时,修改将自动完成,它会在ConfigMap中添加下方加粗的部分。
apiVersion: v1
data: Corefile: | .:53 {
errors health {
lameduck 5s }
ready kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure fallthrough in-addr.arpa ip6.arpa ttl 30 }
prometheus :9153 forward . /etc/resolv.conf **etcd****foowidgets.k8s** **{**
**stubzones****path****/skydns****endpoint****http://10.96.149.223:2379** **}**
cache 30 loop reload loadbalance }
添加的行配置了一个名为foowidgets.k8s的区域,该区域集成了 ETCD。我们添加的第一行告诉 CoreDNS,该区域名称foowidgets.com已与 ETCD 服务集成。
下一行stubzone告诉 CoreDNS 允许你将 DNS 服务器设置为某个区域的“存根解析器”。作为存根解析器,这个 DNS 服务器直接查询特定名称服务器的区域信息,而不需要在整个 DNS 层次结构中进行递归解析。
第三项添加是path /skydns选项,这可能看起来有些混淆,因为它没有提到 CoreDNS。尽管该值是skydns,但它也是 CoreDNS 集成的默认路径。
最后一行告诉 CoreDNS 将记录存储在哪里。在我们的示例中,我们运行了一个 ETCD 服务,使用 IP 地址10.96.149.223,并在默认的 ETCD 端口2379上运行。
你可以在这里使用服务的主机名而不是 IP。我们使用 IP 来展示 Pod 和服务之间的关系,但etcd-dns.etcd-dns.svc这个名称也可以使用。你选择哪种方式取决于你的具体情况。在我们的 KinD 集群中,我们不需要担心丢失 IP,因为集群是可丢弃的。在实际应用中,你应该使用主机名来避免 IP 地址变化带来的问题。
现在你理解了如何在 CoreDNS 中添加 ETCD 集成的区域,下一步是更新 ExternalDNS 所需的部署选项,以便与 CoreDNS 集成。
ExternalDNS 配置选项。
ExternalDNS 可以配置为注册入口或服务对象。这在 ExternalDNS 的部署文件中使用 source 字段进行配置。以下示例显示了我们将在本章中使用的部署选项部分。
我们还需要配置 ExternalDNS 将使用的提供程序,由于我们使用的是 CoreDNS,因此我们将提供程序设置为coredns。
最后的选项是我们想要设置的日志级别,我们将其设置为info,以使日志文件更小且更易于阅读。我们将使用的参数如下所示:
spec:
serviceAccountName: external-dns
containers:
- name: external-dns
image: registry.k8s.io/external-dns/external-dns:v0.13.5
args:
- --source=service
- --provider=coredns
- --log-level=info
现在,我们已经讲解了 ETCD 选项和部署,如何配置一个新的区域以使用 ETCD,以及如何配置 ExternalDNS 使用 CoreDNS 作为提供程序,我们可以在集群中部署 ExternalDNS 了。
我们在chapter5/externaldns文件夹中包含了一个名为deploy-externaldns.sh的脚本。在该目录下执行脚本,以将 ExternalDNS 部署到您的 KinD 集群中。当您执行该脚本时,它将完全配置并部署一个集成了 ETCD 的 ExternalDNS。
注意
如果在脚本更新ConfigMap时看到警告,您可以安全地忽略它。由于我们的kubectl命令使用apply来更新对象,Kubernetes 会查找最后应用的配置注释(last-applied-configuration),如果有设置的话。由于您可能在现有对象中没有该注释,因此会看到缺失的警告。这只是一个警告,不会阻止ConfigMap的更新,您可以通过查看kubectl更新命令的最后一行来确认,那里显示ConfigMap已被更新:configmap/coredns configured
现在,我们已经为开发人员添加了创建动态注册 DNS 名称的能力,接下来,让我们通过创建一个新的服务来看它的实际效果,该服务将自己注册到我们的 CoreDNS 服务器中。
创建一个与 ExternalDNS 集成的 LoadBalancer 服务
我们的 ExternalDNS 将监视所有服务,查找包含所需 DNS 名称的注释。这只是一个单一的注释,格式为annotation external-dns.alpha.kubernetes.io/hostname,值为您想要注册的 DNS 名称。以我们的示例为例,我们想要注册名称nginx.foowidgets.k8s,因此我们会在 NGINX 服务中添加一个注释:external-dns.alpha.kubernetes.io/hostname: nginx.foowidgets.k8s。
在chapter5/externaldns目录下,我们包含了一个清单文件,该文件将通过一个包含注册 DNS 名称注释的LoadBalancer服务来部署 NGINX Web 服务器。
使用kubectl create -f nginx-lb.yaml部署清单,这将把资源部署到默认命名空间。该部署是一个标准的 NGINX 部署,但服务具有必要的注释,告诉 ExternalDNS 服务您希望注册一个新的 DNS 名称。服务的清单如下所示,注释部分为粗体:
apiVersion: v1
kind: Service
metadata:
annotations:
external-dns.alpha.kubernetes.io/hostname: nginx.foowidgets.k8s
name: nginx-ext-dns
namespace: default
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: nginx
type: LoadBalancer
当 ExternalDNS 看到该注释时,它会在区域中注册请求的名称。来自注释的主机名将在 ExternalDNS Pod 中记录一条条目 – 我们的新条目nginx.foowidgets.k8s的注册信息如下所示:
time="2023-08-04T19:16:00Z" level=debug msg="Getting service (&{ 0 10 0 \"heritage=external-dns,external-dns/owner=default,external-dns/resource=service/default/nginx-ext-dns\" false 0 1 /skydns/k8s/foowidgets/a-nginx/39ca730e}) with service host ()"
time="2023-08-04T19:16:00Z" level=debug msg="Getting service (&{172.18.200.101 0 10 0 \"heritage=external-dns,external-dns/owner=default,external-dns/resource=service/default/nginx-ext-dns\" false 0 1 /skydns/k8s/foowidgets/nginx/02b5d1d5}) with service host (172.18.200.101)"
time="2023-08-04T19:16:00Z" level=debug msg="Creating new ep (nginx.foowidgets.k8s 0 IN A 172.18.200.101 []) with new service host (172.18.200.101)"
正如您在日志的最后一行看到的,记录已作为 A 记录添加到 DNS 服务器中,指向 IP 地址172.18.200.101。
确认 ExternalDNS 完全正常工作的最后一步是测试与应用程序的连接。由于我们使用的是 KinD 集群,因此必须从集群中的 Pod 进行测试。为了将新名称提供给外部资源,我们需要配置主 DNS 服务器,将对foowidgets.k8s域的请求转发到 CoreDNS 服务器。在本节末尾,我们将展示如何将 Windows DNS 服务器(这可以是您网络中的任何主 DNS 服务器)与 Kubernetes CoreDNS 服务器集成的步骤。
现在我们可以使用从注释中获得的 DNS 名称测试 NGINX 部署。由于您没有将 CoreDNS 服务器作为主 DNS 提供商,我们需要使用集群中的容器来测试名称解析。有一个很棒的工具叫做Netshoot,它包含许多有用的故障排除工具;这是一个很好的工具,可以用来测试和排查集群及 Pod 的问题。
要运行 Netshoot 容器,我们可以使用kubectl run命令。我们只需要在使用它进行集群测试时运行 Pod,因此我们将告诉kubectl run命令运行一个交互式 Shell,并在退出后删除 Pod。要运行 Netshoot,请执行:
kubectl run tmp-shell --rm -i --tty --image nicolaka/netshoot -- /bin/bash
Pod 可能需要一两分钟才能变得可用,但一旦启动,您将看到tmp-shell提示符。在此提示符下,我们可以使用nslookup来验证 DNS 条目是否已成功添加。如果您尝试查找nginx.foowidgets.k8s,应该会收到该服务的 IP 地址作为回应。
nslookup nginx.foowidgets.k8s
您的回复应类似于以下示例:
Server: 10.96.0.10
Address: 10.96.0.10#53
Name: nginx.foowidgets.k8s
Address: 172.18.200.101
这确认了我们的注释是成功的,并且 ExternalDNS 已在 CoreDNS 服务器中注册了我们的主机名。
nslookup仅能证明nginx.foowidgets.k8s有条目;它并没有测试应用程序。为了证明我们有一个成功的部署,并且当有人在浏览器中输入该名称时它能够工作,我们可以使用 Netshoot 中包含的curl工具。

图 5.2:使用 ExternalDNS 名称的 curl 测试
curl 输出确认我们可以使用动态创建的服务名称访问 NGINX Web 服务器。
我们意识到这些测试可能不太令人兴奋,因为无法通过标准浏览器进行测试。为了让 CoreDNS 能够在集群外部使用,我们需要将 CoreDNS 与您的主 DNS 服务器集成,这需要将 CoreDNS 中区域的所有权委托给主 DNS 服务器。当您委托一个区域时,任何针对主 DNS 服务器的请求,如果请求的主机位于该委托的区域中,都会将请求转发到包含该区域的 DNS 服务器。
在下一节中,我们将集成在集群中运行的 CoreDNS 与 Windows DNS 服务器。虽然我们使用 Windows 作为 DNS 服务器,但不同操作系统和 DNS 服务器之间的区域委托概念是相似的。
将 CoreDNS 与企业 DNS 服务器集成
本节将展示如何使用主 DNS 服务器将 foowidgets.k8s 区域的名称解析转发到运行在 Kubernetes 集群中的 CoreDNS 服务器。
这里提供的步骤是将企业 DNS 服务器与 Kubernetes DNS 服务集成的示例。由于外部 DNS 的要求和额外的设置,这些步骤仅供参考,不应在您的 KinD 集群上执行。
要在委托的区域中查找记录,主 DNS 服务器使用一种称为递归查询的过程。递归查询是由 DNS 解析器发起的 DNS 查询,代表用户进行操作。在递归查询过程中,DNS 解析器承担了通过层级模式联系多个 DNS 服务器的任务。其目标是找到请求域的权威 DNS 服务器,并启动请求的 DNS 记录检索。
下图展示了通过将区域委托给 CoreDNS 服务器来提供 DNS 解析的流程,在企业环境中尤为适用。

图 5.3:DNS 委托流程
-
本地客户端将查看其 DNS 缓存中的请求名称。
-
如果名称不在本地缓存中,将向主 DNS 服务器请求
nginx.foowidgets.k8s。 -
DNS 服务器接收到查询后,会查看它知道的区域。它找到
foowidgets.k8s区域,并发现该区域已被委托给运行在192.168.1.200上的 CoreDNS。 -
主 DNS 服务器将查询发送到委托的 CoreDNS 服务器。
-
CoreDNS 在
foowidgets.k8s区域中查找名称nginx。 -
CoreDNS 将
foowidgets.k8s的 IP 地址返回给主 DNS 服务器。 -
主 DNS 服务器将包含
nginx.foowidgets.k8s地址的回复发送给客户端。 -
客户端通过从 CoreDNS 返回的 IP 地址连接到 NGINX 服务器。
让我们继续来看一个现实世界的例子。在我们的场景中,主 DNS 服务器运行在 Windows 2019 服务器上,我们将把一个区域委托给 CoreDNS 服务器。
部署的组件如下:
-
我们的网络子网是
10.2.1.0/24 -
运行 DNS 的 Windows 2019 或更高版本服务器
-
一个 Kubernetes 集群
-
一个 MetalLB 地址池,范围为
10.2.1.70-10.2.1.75 -
使用
LoadBalancer服务部署的 CoreDNS 实例,分配了来自我们的 IP 池的 IP 地址10.2.1.74 -
部署的附加组件,使用本章中的配置,包括 ExternalDNS、CoreDNS 的 ETCD 部署,以及一个新的 CoreDNS ETCD 集成区域
-
Bitnami NGINX 部署用于测试委托
现在,让我们通过配置步骤来集成我们的 DNS 服务器。
将 CoreDNS 暴露给外部请求
我们已经覆盖了如何部署大多数你需要集成的资源——ETCD、ExternalDNS,并通过一个新的 ETCD 集成区域配置 CoreDNS。为了提供对 CoreDNS 的外部访问,我们需要创建一个新的服务,将 CoreDNS 显示在 TCP 和 UDP 端口 53 上。完整的服务清单如下所示。
apiVersion: v1
kind: Service
metadata:
labels:
k8s-app: kube-dns
kubernetes.io/cluster-service: "true"
kubernetes.io/name: CoreDNS
name: kube-dns-ext
namespace: kube-system
spec:
ports:
- name: dns
port: 53
protocol: UDP
targetPort: 53
selector:
k8s-app: kube-dns
type: LoadBalancer
loadBalancerIP: 10.2.1.74
服务中有一个新的选项我们尚未讨论——我们在部署中添加了spec.loadBalancerIP。这个选项允许你为服务分配一个 IP 地址,即使服务被重新创建,它仍然会保持一个稳定的 IP 地址。我们需要一个静态 IP,因为我们需要启用从主 DNS 服务器到 Kubernetes 集群中的 CoreDNS 服务器的转发。
一旦 CoreDNS 通过端口 53 使用 LoadBalancer 公开,我们可以配置主 DNS 服务器将 foowidgets.k8s 域中的主机请求转发到我们的 CoreDNS 服务器。
配置主 DNS 服务器
在我们的主 DNS 服务器上,首先要做的就是创建一个条件转发器,指向运行 CoreDNS Pod 的节点。
在 Windows DNS 主机上,我们需要为 foowidgets.k8s 创建一个新的条件转发器,指向我们分配给新 CoreDNS 服务的 IP 地址。在我们的示例中,CoreDNS 服务已分配给主机 10.2.1.74:

图 5.4:Windows 条件转发器设置
这配置了 Windows DNS 服务器,将任何对 foowidgets.k8s 域中主机的请求转发到运行在 IP 地址 10.2.1.74 上的 CoreDNS 服务。
测试 DNS 转发到 CoreDNS
为了测试配置,我们将使用一台已配置为使用 Windows DNS 服务器的主网络工作站。
我们将运行的第一个测试是对 MetalLB 注解创建的 NGINX 记录进行 nslookup:
从命令提示符中,我们执行 nslookup nginx.foowidgets.k8s 命令:
Server: AD2.hyper-vplanet.com
Address: 10.2.1.14
Name: nginx.foowidgets.k8s
Address: 10.2.1.75
由于查询返回了我们期望的记录 IP 地址,我们可以确认 Windows DNS 服务器正在正确地将请求转发到 CoreDNS。
我们可以从笔记本浏览器上再做一个额外的 NGINX 测试。在 Chrome 中,我们可以使用在 CoreDNS 中注册的 URL nginx.foowidgets.k8s。

图 5.5:使用 CoreDNS 从外部工作站成功浏览
一个测试确认转发工作正常,但我们还想创建一个额外的部署来验证系统完全正常运行。
为了测试一个新服务,我们部署了一个不同的 NGINX 服务器,名为 microbot,并为其服务添加了一个注解,指定了名称microbot.foowidgets.k8s。MetalLB 为该服务分配了 IP 地址 10.2.1.65。
如同我们之前的测试一样,我们使用nslookup测试名称解析:
Name: AD2.hyper-vplanet.com
Address: 10.2.1.65
为了确认 Web 服务器是否正确运行,我们从工作站浏览该 URL:

图 5.6:使用 CoreDNS 从外部工作站成功浏览
成功!我们现在已将企业 DNS 服务器与运行在 Kubernetes 集群上的 CoreDNS 服务器集成。这一集成使用户能够通过简单地向服务添加注释来动态注册服务名称。
跨多个集群的负载均衡
配置多个集群中运行的服务有多种方法,通常涉及复杂且昂贵的附加组件,如全局负载均衡器。全局负载均衡器可以看作是一个交通警察——它知道如何在多个端点之间指引传入流量。从高层次来看,你可以创建一个新的 DNS 条目,供全局负载均衡器控制。这个新的条目将有后端系统被添加到端点列表中,并根据健康状况、连接数或带宽等因素,它将把流量指向这些端点节点。如果某个端点因任何原因不可用,负载均衡器将把它从端点列表中移除。通过将其从列表中移除,流量只会发送到健康的节点,从而提供流畅的终端用户体验。对客户来说,最糟糕的情况就是当他们尝试访问网站时,遇到“网站未找到”的错误。

图 5.7:全局负载均衡流量流程
上图展示了一个健康的工作流,其中两个集群都在运行我们进行负载均衡的应用程序。当请求到达负载均衡器时,它将以轮询方式在两个集群之间分配流量。nginx.foowidgets.k8s请求最终将流量发送到nginx.clustera.k8s或nginx.clusterb.k8s。
在图 5.8中,我们看到集群 B 中的 NGINX 工作负载发生故障。由于全局负载均衡器对正在运行的工作负载进行了健康检查,它将从nginx.foowidgets.k8s条目中移除集群 B 的端点。

图 5.8:全局负载均衡流量流程(带站点故障)
现在,对于任何请求nginx.foowidgets.k8s进入负载均衡器的流量,唯一会被用于流量的端点是在集群 A 上运行的。一旦集群 B 的问题得到解决,负载均衡器将自动将集群 B 的端点重新添加到nginx.foowidgets.k8s记录中。
这样的解决方案在企业中广泛应用,许多组织利用F5、Citrix、Kemp和A10等公司提供的产品,以及像Route 53和Traffic Director这样的 CSP 本地解决方案来管理跨多个集群的工作负载。然而,也有一些与 Kubernetes 集成的具有类似功能的项目,而且这些项目几乎不需要成本。虽然这些项目可能没有一些供应商解决方案的所有功能,但它们通常能够满足大多数用例的需求,而无需完整的昂贵功能。
其中一个项目是 K8GB,一个创新的开源项目,将 全球服务器负载均衡 (GSLB) 引入 Kubernetes。通过 K8GB,组织可以轻松地将传入的网络流量分配到位于不同地理位置的多个 Kubernetes 集群。通过智能地路由请求,K8GB 保证了低延迟、最佳响应时间和冗余,为任何企业提供了卓越的解决方案。
本节将介绍 K8GB,但如果你想了解更多关于该项目的内容,请访问项目的主页 www.k8gb.io。
由于我们使用 KinD 和单一主机来搭建集群,本书的这一部分旨在向你介绍该项目及其带来的好处。本节内容仅供参考,因为这是一个复杂的话题,涉及多个组件,其中一些组件超出了 Kubernetes 的范畴。如果你决定自己实现解决方案,我们已经在本书的代码库中包含了示例文档和脚本,位于 chapter5/k8gs-example 目录下。
K8GB 是一个 CNCF 沙箱项目,这意味着它处于早期阶段,在本章写作后,任何更新的版本都可能对对象和配置做出更改。
介绍 Kubernetes 全球负载均衡器
为什么你应该关心像 K8GB 这样的项目?
让我们以一个内部企业云为例,企业在生产站点运营一个 Kubernetes 集群,并在灾难恢复站点运营另一个集群。为了确保顺畅的用户体验,重要的是让应用程序能够在这些数据中心之间无缝过渡,且在灾难恢复事件期间不需要任何人工干预。挑战在于,当多个集群同时为这些应用程序提供服务时,如何满足企业对微服务高可用性的需求。我们需要有效地解决跨地理分布的 Kubernetes 集群中持续和不中断服务的需求。
这就是 K8GB 的作用。
是什么使得 K8GB 成为解决我们高可用性需求的理想方案?正如其网站所记录的,关键特性包括以下内容:
-
负载均衡是通过一个非常可靠、适用于全球部署的抗时间考验的 DNS 协议提供的
-
不需要管理集群
-
没有单点故障
-
它使用原生 Kubernetes 健康检查来做负载均衡决策
-
配置和创建一个 Kubernetes CRD 一样简单
-
它适用于任何 Kubernetes 集群 —— 无论是本地集群还是云端集群
-
它是免费的!
正如你将在本节中看到的,K8GB 提供了一种简单直观的配置方式,使得为你的组织提供全球负载均衡变得容易。这可能让 K8GB 看起来没有做太多事情,但在幕后,它提供了许多先进的功能,包括:
-
全球负载均衡:促进将传入的网络流量分配到位于不同地理区域的多个 Kubernetes 集群。由此,它能够优化应用交付,减少延迟并改善用户体验。
-
智能流量路由:利用复杂的路由算法,智能地将客户端请求引导至最近或最合适的 Kubernetes 集群,考虑诸如接近度、服务器健康状况和特定应用规则等因素。这种方法确保了高效且响应迅速的流量管理,优化了应用的性能。
-
高可用性和冗余性:通过在集群、应用或数据中心发生故障时自动重定向流量,确保应用的高可用性和容错性。这种故障转移机制最大限度减少了灾难恢复场景中的停机时间,确保了对用户的不间断服务交付。
-
自动故障切换:通过启用数据中心之间的自动故障切换,简化了操作,免去了人工干预的需求。这消除了对人为触发的灾难恢复(DR)事件或任务的要求,确保了快速和不间断的服务交付以及精简的运营。
-
与 Kubernetes 的集成:提供与 Kubernetes 的无缝集成,简化了为部署在集群中的应用配置 GSLB 的设置过程。通过利用 Kubernetes 的原生能力,K8GB 提供了一个可扩展的解决方案,提升了全球负载均衡操作的整体管理和效率。
-
本地部署和云服务商支持:为企业提供了一种有效管理多个 Kubernetes 集群的 GSLB 方式,使复杂的多区域部署和混合云场景的处理变得无缝。这确保了在不同环境中优化应用交付,提升了基础设施的整体性能和韧性。
-
定制化和灵活性:为用户提供定义个性化流量路由规则和策略的自由,使组织能够灵活定制 GSLB 配置,精确满足其独特需求。这使企业能够根据具体需求优化流量管理,并确保能无缝适应不断变化的应用需求。
-
监控、度量和追踪:包括监控、度量和追踪功能,使管理员能够访问跨多个集群的流量模式、健康状况和性能指标的洞察。这提供了增强的可视性,帮助管理员做出明智的决策,并优化 GSLB 设置的整体性能和可靠性。
现在我们已经讨论了 K8GB 的主要特性,接下来我们将深入了解细节。
K8GB 的要求
对于像全球负载均衡这样提供复杂功能的产品,K8GB 不需要大量的基础设施或资源就可以为您的集群提供负载均衡。最新版本(截至本章节编写时)是0.12.2——它只有少数几个要求:
- CoreDNS 服务器的负载均衡器 IP 地址,使用命名标准
gslb-ns-<k8gb-name>-gb.foowidgets.k8s——例如,gslb-ns-us-nyc-gb.foowidgets.k8s和gslb-ns-us-buf-gb.foowidgets.k8s
如果您使用的是像 Route 53、Infoblox 或 NS1 这样的服务,CoreDNS 服务器将自动添加到域中。由于我们的示例使用的是运行在 Windows 2019 服务器上的本地 DNS 服务器,我们需要手动创建记录。
-
一个 Ingress 控制器
-
在集群中部署的 K8GB 控制器,它将部署:
-
K8GB 控制器
-
配置了 CoreDNS CRD 插件的 CoreDNS 服务器——这已经包含在 K8GB 的部署中
-
由于我们在前面的章节中已经探讨了 NGINX Ingress 控制器,现在我们将重点关注额外的要求:在集群内部署和配置 K8GB 控制器。
在下一节中,我们将讨论实现 K8GB 的步骤。
将 K8GB 部署到集群
我们已在 GitHub 仓库的 chapter5/k8gb-example 目录下包含了示例文件。脚本基于我们将在本章余下部分中使用的示例。如果您决定在开发集群中使用这些文件,您需要满足以下要求:
-
两个 Kubernetes 集群(每个集群可以使用一个单节点的
kubeadm集群) -
在每个集群中部署的 CoreDNS
-
在每个集群中部署 K8GB
-
一个边缘 DNS 服务器,您可以用来委派 K8GB 的域
安装 K8GB 已经变得非常简单——您只需要使用已为您的基础设施配置好的 values.yaml 文件部署一个单一的 Helm 图表。
要安装 K8GB,您需要将 K8GB 仓库添加到您的 Helm 仓库列表中,然后更新图表:
helm repo add k8gb https://www.k8gb.io
helm repo update
在执行 helm install 命令之前,我们需要为每个集群部署自定义 Helm values.yaml 文件。我们已经在 chapter5/k8gb-example 目录中包含了我们示例中使用的两个集群的 values 文件,分别是 k8gb-buff-values.yaml 和 k8gb-nyc-values.yaml。这些选项将在 自定义 Helm 图表值 一节中讨论。
了解 K8GB 的负载均衡选项
在我们的示例中,我们将配置 K8GB 作为两个本地集群之间的故障切换负载均衡器;然而,K8GB 不仅仅局限于故障切换。像大多数负载均衡器一样,K8GB 提供多种解决方案,可以根据每个负载均衡的 URL 配置不同的策略。它提供了最常用的策略,包括轮询、加权轮询、故障切换和 GeoIP。
每个策略的描述如下:
-
轮询(Round Robin):如果未指定策略,它将默认为简单的轮询负载均衡配置。使用轮询意味着请求将在配置的集群之间进行分配——请求 1 将发送到集群 1,请求 2 将发送到集群 2,请求 3 将发送到集群 1,请求 4 将发送到集群 2,以此类推。
-
加权轮询(Weighted Round Robin):类似于轮询,该策略提供了指定流量百分比发送到某个集群的能力;例如,75%的流量将发送到集群 1,15%的流量将发送到集群 2。
-
故障转移(Failover):除非某个部署的所有 Pod 不可用,否则所有流量将发送到主集群。如果集群 1 中的所有 Pod 都宕机,集群 2 将接管工作负载,直到集群 1 中的 Pod 恢复可用,届时集群 1 将重新成为主集群。
-
GeoIP: 将请求定向到与客户端连接最近的集群。如果最近的主机宕机,它将使用另一个集群,类似于故障转移策略的工作方式。要使用此策略,你需要创建一个 GeoIP 数据库(示例可参考此处:
github.com/k8gb-io/coredns-crd-plugin/tree/main/terratest/geogen),并且你的 DNS 服务器需要支持EDNS0扩展。
EDNS0基于 RFC 2671,该规范概述了 EDNS0 的工作原理及其各个组成部分,包括启用 EDNS0 的 DNS 消息格式、EDNS0 选项的结构以及其实现指南。RFC 2671 的目标是提供一种标准化的方法,用于扩展 DNS 协议的功能,突破其原有的局限性,允许加入新功能、选项和增强特性。
现在你已经了解了可用的策略,我们来回顾一下我们集群的示例基础设施:
| 集群/服务器详情 | 详情 |
|---|---|
企业 DNS 服务器 – 纽约市 IP:10.2.1.14 |
主要企业区域foowidgets.k8sCoreDNS 服务器的主机记录gslb-ns-us-nyc-gb.foowidgets.k8s``gslb-ns-us-buf-gb.foowidgets.k8s全球域名配置,委派给集群中的 CoreDNS 服务器gb.foowidgets.k8s |
纽约市,纽约 – 集群 1 主站 CoreDNS 负载均衡 IP:10.2.1.221Ingress IP:10.2.1.98 |
使用 HostPort 公开的 NGINX Ingress Controller,使用 MetalLB 公开的 CoreDNS 部署 |
布法罗,纽约 – 集群 2 次要站 CoreDNS 负载均衡 IP:10.2.1.224Ingress IP:10.2.1.167 |
使用 HostPort 公开的 NGINX Ingress Controller,使用 MetalLB 公开的 CoreDNS 部署 |
表 5.1:集群详情
我们将使用上表中的详情来解释如何在我们的示例基础设施中部署 K8GB。
通过基础设施的详细信息,我们现在可以为每个部署创建我们的 Helm values.yaml文件。在接下来的部分,我们将展示我们需要使用示例基础设施配置的值,并解释每个值的含义。
自定义 Helm 图表值
每个集群将有一个类似的值文件;主要的变化是我们使用的标签值。下面的值文件是纽约市集群的配置:
k8gb:
dnsZone: "gb.foowidgets.k8s"
edgeDNSZone: "foowidgets.k8s"
edgeDNSServers:
- 10.2.1.14
clusterGeoTag: "us-buf"
extGslbClustersGeoTags: "us-nyc"
coredns:
isClusterService: false
deployment:
skipConfig: true
image:
repository: absaoss/k8s_crd
tag: v0.0.11
serviceAccount:
create: true
name: coredns
serviceType: LoadBalancer
对于 NYC 集群,除了 clusterGeoTag 和 extGslbClustersGeoTags 值外,其他文件内容相同,对于 NYC 集群,这些值需要设置为:
clusterGeoTag: "us-nyc"
extGslbClustersGeoTags: "us-buf"
如你所见,这个配置并不冗长,仅需要少数选项来配置通常复杂的全局负载均衡配置。
现在,让我们来了解一下我们使用的部分主要配置项。
我们将解释的主要内容是 K8GB 部分的配置值,这些值配置了 K8GB 用于负载均衡的所有选项。
| Chart 值 | 描述 |
|---|---|
dnsZone |
这是你将用于 K8GB 的 DNS 区域——基本上,这是用于存储我们全局负载均衡的 DNS 记录的 DNS 区域。 |
edgeDNSZone |
包含用于前一个选项(dnsZone)的 CoreDNS 服务器的 DNS 记录的主要 DNS 区域。 |
edgeDNSServers |
边缘 DNS 服务器——通常是用于名称解析的主要 DNS 服务器。 |
clusterGeoTag |
如果你有多个 K8GB 控制器,使用此标签来指定彼此之间的实例。在我们的示例中,我们将其设置为 us-buf 和 us-nyc 来表示我们的集群。 |
extGslbClusterGeoTags |
指定要与之配对的其他 K8GB 控制器。在我们的示例中,每个集群会添加另一个集群的 clusterGeoTags —— Buffalo 集群添加 us-nyc 标签,NYC 集群添加 us-buf 标签。 |
isClusterService |
设置为 true 或 false。用于服务升级;你可以在 www.k8gb.io/docs/service_upgrade.xhtml 阅读更多信息。 |
exposeCoreDNS |
如果设置为 true,将创建一个 LoadBalancer 服务,公开在 k8gb 命名空间中部署的 CoreDNS,使用端口 53/UDP 供外部访问。 |
deployment.skipConfig |
设置为 true 或 false。设置为 false 时,告诉部署使用 K8GB 附带的 CoreDNS。 |
image.repository |
配置用于 CoreDNS 镜像的仓库。 |
image.tag |
配置拉取镜像时使用的标签。 |
serviceAccount.create |
设置为 true 或 false。设置为 true 时,将创建一个服务帐户。 |
serviceAccount.name |
设置来自前一个选项的服务帐户名称。 |
serviceType |
配置 CoreDNS 的服务类型。 |
表 5.2:K8GB 配置选项
使用 Helm 安装 K8GB
完成 K8GB 概述和 Helm 值文件配置后,我们可以开始在集群中安装 K8GB。我们已经包含了将 K8GB 部署到 Buffalo 和 NYC 集群的脚本。在 chapter5/k8gb-example/k8gb 目录下,你会看到两个脚本,分别是 deploy-k8gb-buf.sh 和 deploy-k8gb-nyc.sh —— 这些脚本应在各自的集群中运行。
脚本将执行以下步骤:
-
将 K8GB Helm 仓库添加到服务器的仓库列表中
-
更新仓库
-
使用适当的 Helm 值文件部署 K8GB
-
创建一个 Gslb 记录(在下一部分中讲解)
-
创建一个名为
demo的命名空间中的部署,用于测试
部署完成后,您将看到在 k8gb 命名空间中运行的两个 pod,一个是 k8gb 控制器,另一个是用于解析负载均衡名称的 CoreDNS 服务器:
NAME READY STATUS RESTARTS AGE
k8gb-8d8b4cb7c-mhglb 1/1 Running 0 7h58m
k8gb-coredns-7995d54db5-ngdb2 1/1 Running 0 7h37m
我们还可以验证已经创建了服务来处理传入的 DNS 请求。由于我们使用 LoadBalancer 类型进行了暴露,我们将看到在端口 53 上使用 UDP 协议的 LoadBalancer 服务:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
k8gb-coredns LoadBalancer 10.96.185.56 10.2.1.224 53:32696/UDP 2d18h
在 K8GB 部署完成并在两个集群中验证后,让我们继续下一部分,解释如何为我们的负载均衡区创建边缘委派。
委派我们的负载均衡区
在我们的示例中,我们使用 Windows 服务器作为边缘 DNS 服务器,在这里我们的 K8s DNS 名称将被注册。在 DNS 方面,我们需要为 CoreDNS 服务器添加两个 DNS 记录,然后我们需要将负载均衡区委派给这些服务器。
这两个 A 记录需要位于主边缘 DNS 区域中。在我们的示例中,这是 foowidgets.k8s 区域。在此区域中,我们需要为通过 LoadBalancer 服务暴露的 CoreDNS 服务器添加两个条目:

图 5.9:将我们的 CoreDNS 服务器添加到边缘区域
接下来,我们需要创建一个新的委派区,用于我们的负载均衡服务名称。在 Windows 中,这是通过 右键单击 区域并选择 新建委派 来完成的;在委派向导中,系统会要求您提供 委派域。在我们的示例中,我们将委派 gb 域作为我们的域。

图 5.10:创建一个新的委派区
在输入区域名称并点击 下一步 后,您将看到一个新的屏幕,要求您为委派域添加 DNS 服务器;点击 添加 后,您将输入 CoreDNS 服务器的 DNS 名称。记住我们在主域 foowidgets.com 中创建了两个 A 记录。当您添加条目时,Windows 会验证输入的名称是否正确解析,DNS 查询是否正常工作。添加了两个 CoreDNS 服务器后,摘要屏幕将显示它们的 IP 地址。

图 5.11:为 CoreDNS 服务器添加 DNS 名称
这完成了边缘服务器的配置。对于某些边缘服务器,K8GB 会自动创建委派记录,但只有少数几台服务器支持此功能。对于那些不能自动创建委派服务器的边缘服务器,您需要像本节中那样手动创建它们。
现在我们已经在集群中部署了 CoreDNS,并且委派了负载均衡区,接下来我们将部署一个具有全球负载均衡的应用程序来测试我们的配置。
使用 K8GB 部署一个高可用应用程序
启用应用程序的全局负载均衡有两种方法。你可以使用 K8GB 提供的自定义资源创建一个新记录,或者你也可以为 Ingress 规则添加注释。为了演示 K8GB,我们将在集群中部署一个简单的 NGINX Web 服务器,并使用原生提供的自定义资源将其添加到 K8GB。
使用自定义资源将应用程序添加到 K8GB
当我们部署 K8GB 时,一个新的 自定义资源定义 (CRD) 名为 Gslb 被添加到了集群中。这个 CRD 扮演着管理标记为全局负载均衡的应用程序的角色。在 Gslb 对象中,我们定义了 Ingress 名称的规范,格式与常规的 Ingress 对象相同。标准 Ingress 和 Gslb 对象之间唯一的区别在于清单的最后部分,即策略。
策略定义了我们想要使用的负载均衡类型,在我们的示例中是故障转移策略,以及该对象要使用的主 GeoTag。在我们的示例中,NYC 集群是我们的主集群,因此我们的 Gslb 对象将设置为 us-buf。
要部署一个将利用负载均衡的应用程序,我们需要在两个集群中创建以下内容:
-
应用程序的标准部署和服务。我们将把部署命名为
nginx,使用标准的 NGINX 镜像。 -
每个集群中的一个
Gslb对象。对于我们的示例,我们将使用以下清单,该清单将声明 Ingress 规则,并将策略设置为故障转移,使用us-buf作为主要的 K8GB。由于Gslb对象包含 Ingress 规则的信息,你无需创建 Ingress 规则;Gslb会为我们创建 Ingress 对象。以下是一个示例:apiVersion: k8gb.absa.oss/v1beta1 kind: Gslb metadata: name: gslb-failover-buf namespace: demo spec: ingress: ingressClassName: nginx rules: - host: fe.gb.foowidgets.k8s # Desired GSLB enabled FQDN http: paths: - backend: service: name: nginx # Service name to enable GSLB for port: number: 80 path: / pathType: Prefix strategy: type: failover # Global load balancing strategy primaryGeoTag: us-buf # Primary cluster geo tag
当你部署 Gslb 对象的清单时,它将创建两个 Kubernetes 对象,分别是 Gslb 对象和 Ingress 对象。
如果我们查看 Buffalo 集群中的 demo 命名空间下的 Gslb 对象,我们将看到以下内容:
NAMESPACE NAME STRATEGY GEOTAG
demo gslb-failover-buf failover us-buf
如果我们查看 NYC 集群中的 Ingress 对象,我们会看到:
NAME CLASS HOSTS ADDRESS PORTS AGE
gslb-failover-buf nginx fe.gb.foowidgets.k8s 10.2.1.167 80 15h
我们还将在 NYC 集群中有类似的对象,具体内容将在 理解 K8GB 如何提供全局负载均衡 部分进行解释。
使用 Ingress 注释将应用程序添加到 K8GB
将应用程序添加到 K8GB 的第二种方法是向标准的 Ingress 规则添加两个注释,这主要是为了允许开发人员将现有的 Ingress 规则添加到 K8GB。
要将 Ingress 对象添加到全局负载均衡列表中,你只需要向 Ingress 对象添加两个注释:strategy 和 primary-geotag。以下是注释的示例:
k8gb.io/strategy: "failover"
k8gb.io/primary-geotag: "us-buf"
这将使用 us-buf GeoTag 作为主标签,通过故障转移策略将 Ingress 添加到 K8GB。
现在我们已经部署了所有必需的基础设施组件和所有必需的对象,以便为应用程序启用全局负载均衡,让我们看看它的实际运行情况。
理解 K8GB 如何提供全局负载均衡
K8GB 的设计很复杂,但一旦你部署了应用程序并了解了 K8GB 如何维护区域文件,理解起来就会变得容易。这个话题相当复杂,它假设你对 DNS 如何工作有一些先验知识,但在本节结束时,你应该能够解释 K8GB 是如何工作的。
保持 K8GB CoreDNS 服务器同步
第一个要讨论的话题是 K8GB 如何保持两个或更多区域文件同步,以提供我们部署的无缝故障切换。无缝故障切换是一个确保即使在系统出现问题或故障时,应用程序仍能平稳运行的过程。它会自动切换到备份系统或资源,保持用户体验不中断。
如前所述,每个 K8GB CoreDNS 服务器必须在主 DNS 服务器中有一条记录。
这是我们为边缘值在 values.yaml 文件中配置的 DNS 服务器和区域:
edgeDNSZone: "foowidgets.k8s"
edgeDNSServer: "10.2.1.14"
所以,在边缘 DNS 服务器(10.2.1.14)中,我们为每个 CoreDNS 服务器使用所需的 K8GB 命名约定配置了主机记录:
gslb-ns-us-nyc-gb.gb.foowidgets.k8s 10.2.1.221 (The NYC CoreDNS load balancer IP)
gslb-ns-us-buf-gb.gb.foowidgets.k8s 10.2.1.224 (The BUF CoreDNS load balancer IP)
K8GB 会在所有 CoreDNS 服务器之间进行通信,并更新任何因新增、删除或更新而需要更新的记录。
通过一个例子,这个过程变得更容易理解。使用我们的集群示例,我们已部署了一个 NGINX Web 服务器,并在两个集群中创建了所有必需的对象。部署后,我们将在每个集群中拥有一个 Gslb 和一个 Ingress 对象,如下所示:
集群:NYC 部署:nginx Gslb: gslb-failover-nyc Ingress:fe.gb.foowidgets.k8s NGINX Ingress IP:10.2.1.98 |
集群:Buffalo(主集群)部署:nginx Gslb: gslb-failover-buf Ingress:fe.gb.foowidgets.k8s NGINX Ingress IP:10.2.1.167 |
|---|
表 5.3:每个集群中的对象
由于部署在两个集群中都处于健康状态,CoreDNS 服务器将为 fe.gb.foowidgets.k8s 配置一个 IP 地址 10.2.1.167,这是主部署。我们可以通过在任何使用边缘 DNS 服务器(10.2.1.14)的客户端机器上运行 dig 命令来验证这一点:
; <<>> DiG 9.16.23-RH <<>> fe.gb.foowidgets.k8s
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 6654
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4000
;; QUESTION SECTION:
;fe.gb.foowidgets.k8s. IN A
;; ANSWER SECTION:
fe.gb.foowidgets.k8s. 30 IN A 10.2.1.167
;; Query time: 3 msec
;; SERVER: 10.2.1.14#53(10.2.1.14)
;; WHEN: Mon Aug 14 08:47:12 EDT 2023
;; MSG SIZE rcvd: 65
正如你在 dig 输出中看到的,由于应用程序在主集群中处于健康状态,主机解析为 10.2.1.167。如果我们使用 curl 查询该 DNS 名称,将看到 Buffalo 中的 NGINX 服务器回应:
# curl fe.gb.foowidgets.k8s
<html>
<h1>Welcome</h1>
</br>
<h1>Hi! This is a webserver in Buffalo for our K8GB example... </h1>
我们将通过将 Buffalo 集群中部署的副本数缩放为 0 来模拟故障,这将使 K8GB 看起来像是应用程序故障。当 NYC 集群中的 K8GB 控制器发现应用程序不再有任何健康的端点时,它将更新所有服务器中的 CoreDNS 记录,使用备用 IP 地址将服务切换到辅助集群。
一旦缩小规模,我们可以使用 dig 来验证返回了哪个主机:
; <<>> DiG 9.16.23-RH <<>> fe.gb.foowidgets.k8s
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 46104
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4000
;; QUESTION SECTION:
;fe.gb.foowidgets.k8s. IN A
;; ANSWER SECTION:
fe.gb.foowidgets.k8s. 27 IN A 10.2.1.98
;; Query time: 1 msec
;; SERVER: 10.2.1.14#53(10.2.1.14)
;; WHEN: Mon Aug 14 08:49:27 EDT 2023
;; MSG SIZE rcvd: 65
我们将再次使用 curl 来验证工作负载是否已移动到 NYC 集群。当我们执行 curl 时,将看到 NGINX 服务器现在位于 NYC 集群中:
# curl fe.gb.foowidgets.k8s
<html>
<h1>Welcome</h1>
</br>
<h1>Hi! This is a webserver in NYC for our K8GB example... </h1>
</html>
请注意,返回的 IP 地址现在是Buffalo集群中的部署 IP 地址,即次要集群的10.2.1.98。这证明了 K8GB 正常工作并为我们提供了一个由 Kubernetes 控制的全球负载均衡器。
一旦应用程序在主集群中变得健康,K8GB 将更新 CoreDNS,任何请求将再次解析到主集群。为了测试这一点,我们将Buffalo集群中的部署规模重新扩展到1,并运行了另一个 dig 测试:
; <<>> DiG 9.16.23-RH <<>> fe.gb.foowidgets.k8s
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 6654
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4000
;; QUESTION SECTION:
;fe.gb.foowidgets.k8s. IN A
;; ANSWER SECTION:
fe.gb.foowidgets.k8s. 30 IN A 10.2.1.167
;; Query time: 3 msec
;; SERVER: 10.2.1.14#53(10.2.1.14)
;; WHEN: Mon Aug 14 08:47:12 EDT 2023
;; MSG SIZE rcvd: 65
我们可以看到 IP 已经更新,反映了位于10.2.1.167的纽约 Ingress 控制器,这是主位置。
最后,再次使用 curl 验证工作负载是否来自Buffalo集群:
# curl fe.gb.foowidgets.k8s
<html>
<h1>Welcome</h1>
</br>
<h1>Hi! This is a webserver in Buffalo for our K8GB example... </h1>
</html>
K8GB 是一个独特且令人印象深刻的 CNCF 项目,它提供类似于其他更昂贵产品的全球负载均衡。
这是一个我们在密切关注的项目,如果你需要在多个集群中部署应用程序,你应该考虑随着 K8GB 项目的成熟而进行深入了解。
总结
在本章中,你学习了如何为任何使用LoadBalancer服务的服务提供自动 DNS 注册。你还学习了如何使用 CNCF 项目 K8GB 来部署一个高可用性服务,K8GB 为 Kubernetes 集群提供全球负载均衡。
这些项目已成为许多企业的核心,提供了之前需要多个团队努力且常常需要大量文书工作的功能,帮助将应用程序交付给客户。现在,你的团队可以使用标准的敏捷实践迅速部署和更新应用程序,为你的组织提供竞争优势。
在下一章《将身份验证集成到你的集群》中,我们将探讨实现 Kubernetes 中安全身份验证的最佳方法和实践。你将学习如何使用 OpenID Connect 协议集成企业身份验证,以及如何使用 Kubernetes 假冒身份。我们还将讨论在集群中管理凭据的挑战,并提供实际解决方案,以便对用户和流水线进行身份验证。
问题
-
Kubernetes 不支持同时在服务中使用 TCP 和 UDP。
-
正确
-
错误
-
答案:b
-
ExternalDNS 只与 CoreDNS 集成。
-
正确
-
错误
-
答案:b
-
你需要在边缘 DNS 服务器上配置什么,以便 K8GB 为一个域提供负载均衡?
-
什么都不需要,它在没有额外配置的情况下就能正常工作
-
它必须指向一个由云提供的 DNS 服务器
-
你必须委托一个指向你集群 IP 的区域
-
创建一个委托指向你的 CoreDNS 实例
-
答案:d
-
K8GB 不支持什么策略?
-
故障转移
-
轮询
-
随机分布
-
GeoIP
-
答案:c
加入我们书籍的 Discord 空间
加入本书的 Discord 工作区,参加每月一次的问我任何问题环节,与作者互动:

第六章:将身份验证集成到你的集群中
一旦集群建立,用户需要安全地与其交互。对于大多数企业而言,这意味着对个体用户和管道进行身份验证,确保他们只能访问完成工作所需的内容。这被称为最小特权访问。最小特权原则是一种安全实践,旨在为用户、系统、应用程序或进程提供执行任务所需的最低访问权限。对于 Kubernetes 来说,这可能会很具挑战性,因为集群是一个 API 集合,而不是具有前端的应用程序,无法提示身份验证,也没有提供管理凭据的安全方式。
未能创建身份验证策略可能导致集群被接管。一旦集群可能被攻击者入侵,几乎无法确定攻击者是否已被清除,你将不得不重新开始。被攻击的集群还可能导致其他系统的漏洞,如应用程序可能访问的数据库。身份验证是确保这一切不发生的第一步。
在本章中,你将学习如何使用 OpenID Connect 协议和 Kubernetes 身份伪造将企业身份验证集成到你的集群中。我们还将介绍几个反模式,并解释为什么你应该避免使用它们。为了结束本章,你还将学习如何安全地将你的管道集成到你的集群中。
本章将涵盖以下主题:
-
了解 Kubernetes 如何识别你的身份
-
了解 OpenID Connect
-
配置 KinD 以支持 OpenID Connect
-
引入身份伪造将身份验证与云托管的集群集成
-
配置集群进行身份伪造
-
在没有 OpenUnison 的情况下配置身份伪造
-
从管道进行身份验证
让我们开始吧!
技术要求
为了完成本章中的练习,你将需要以下资源:
-
一台拥有 8 GB 内存的 Ubuntu 22.04 服务器
-
使用 第二章 中配置的配置运行的全新 KinD 集群,使用 KinD 部署 Kubernetes
你可以在以下 GitHub 仓库访问本章的代码:github.com/PacktPublishing/Kubernetes-An-Enterprise-Guide-Third-Edition/tree/main/chapter6。
获取帮助
我们尽力测试所有内容,但在我们的集成实验室中,有时会有六个或更多的系统。鉴于技术的流动性,有时在我们的环境中有效的东西在你的环境中可能不起作用。别担心——我们在这里帮助你!在我们的 GitHub 仓库中提交问题:github.com/PacktPublishing/Kubernetes-An-Enterprise-Guide-Third-Edition/issues,我们将很乐意帮助你!
了解 Kubernetes 如何识别你的身份
在 1999 年的科幻电影 黑客帝国 中,Neo 在等待见 Oracle 时与一个孩子交谈。孩子向他解释说,操控矩阵的诀窍在于意识到“没有勺子”。
这是在 Kubernetes 中查看用户的一种很好的方式,因为它们并不存在。除了稍后讨论的服务帐户外,Kubernetes 中没有名为“用户”或“组”的对象。每次 API 交互都必须包含足够的信息,告知 API 服务器用户是谁,以及该用户属于哪些组。此声明可以根据你计划如何将身份验证集成到集群中,采取不同的形式。
在本节中,我们将深入探讨 Kubernetes 可以通过不同方式将用户与集群关联的详细信息。
外部用户
从集群外部访问 Kubernetes API 的用户通常会使用两种身份验证方法之一:
-
证书:你可以通过使用包含有关你信息的客户端证书来声明你是谁,例如你的用户名和组。该证书在 TLS 协商过程中作为一部分使用。
-
持有者令牌:嵌入在每个请求中,持有者令牌可以是一个自包含的令牌,包含验证自身所需的所有信息,或者是一个可以通过 API 服务器中的 Webhook 交换获取这些信息的令牌。
还有一种第三种身份验证方法,即使用 服务帐户。然而,强烈不推荐使用服务帐户从集群外部访问 API 服务器。我们将在 其他身份验证选项 部分讨论使用服务帐户的风险和问题。
Kubernetes 中的组
不同的用户可以通过组分配相同的权限,而无需为每个用户单独创建 RoleBinding 对象。Kubernetes 包含两种类型的组:
-
系统分配的:这些组以
system:前缀开始,由 API 服务器分配。例如,system:authenticated组会分配给所有经过身份验证的用户。另一个系统分配的组示例是system:serviceaccounts:namespace组,其中Namespace是包含该组中命名的命名空间内所有服务帐户的命名空间的名称。 -
用户声明组:这些组由身份验证系统声明,可能包含在提供给 API 服务器的令牌中,或通过身份验证 Webhook 提供。对于这些组的命名没有标准或要求。与用户一样,组并不是 API 服务器中的对象。组是在身份验证时由外部用户声明的,并且系统生成的组在本地进行跟踪。在声明用户的组时,唯一标识符和组之间的主要区别是唯一标识符应该是唯一的,而组则不需要。
尽管你可能通过组获得访问权限,但所有访问仍然是根据用户的唯一标识符进行跟踪和审计的。
服务帐户
服务账户是 API 服务器中的对象,用于追踪哪些 Pods 可以访问各种 API。服务账户令牌被称为 JSON Web Tokens,或 JWTs,根据令牌的生成方式,有两种方法可以获取服务账户:
-
第一个方法是通过 Kubernetes 在创建 ServiceAccount 时生成的秘密。
-
第二种方法是通过
TokenRequestAPI,它用于通过挂载点或从集群外部将密钥注入 Pods。所有服务账户都通过将令牌作为请求头注入 API 服务器来使用。API 服务器将其识别为服务账户并进行内部验证。
我们将在本章稍后部分讨论如何在特定上下文中创建这些令牌。
与用户不同,服务账户 不能 被分配到任意的组。服务账户只能是预定义组的成员;你不能创建特定的服务账户组来分配角色。
现在我们已经探讨了 Kubernetes 如何识别用户的基本原理,接下来我们将探讨这一框架如何融入 OpenID Connect(OIDC)协议中。OIDC 提供了大多数企业所需的安全性,并且是基于标准的,但 Kubernetes 并未像许多 Web 应用程序那样使用它。理解这些差异以及 Kubernetes 为什么需要这些差异,是将集群集成到企业安全环境中的一个重要步骤。
理解 OpenID Connect
OpenID Connect 是一种标准的身份联合协议。它基于 OAuth2 规范,并具有一些非常强大的功能,使其成为与 Kubernetes 集群交互的首选方案。
OpenID Connect 的主要好处如下:
-
短期令牌:如果令牌被泄露,例如通过日志消息或安全漏洞,你希望令牌尽快过期。在 OIDC 中,你可以指定令牌的有效期为 1 到 2 分钟,这意味着攻击者在尝试使用令牌时,令牌很可能已经过期。
-
用户和组成员资格:当我们开始讨论 第七章:RBAC 策略和审计 中的授权时,我们会立即看到,通过组管理访问权限比直接引用用户更为重要。OIDC 令牌可以嵌入用户的标识符及其所属组,从而简化访问管理。
-
与超时策略相关的刷新令牌:使用短期令牌时,你需要能够在需要时刷新它们。刷新令牌的有效期可以限定在企业的 Web 应用程序空闲超时策略内,从而保持集群与其他基于 Web 的应用程序的合规性。
-
kubectl 无需插件:
kubectl二进制文件本身原生支持 OpenID Connect,因此无需任何额外的插件。如果你需要从跳板机或虚拟机访问集群,而无法将 命令行界面 (CLI) 工具直接安装到工作站,这尤其有用。不过,也有一些方便的插件,我们将在本章稍后讨论。 -
更多的多因素认证选项:许多最强的多因素认证选项需要使用网页浏览器。例如,FIDO 和 WebAuthn,它们使用硬件令牌。
OIDC 是一个经过同行评审的标准,已经使用了几年,并且正在迅速成为身份联合的首选标准。使用现有的标准而不是定制开发的标准,意味着 Kubernetes 利用了这些同行评审的现有专业知识,而不是创建一个新的身份验证协议,这些经验尚未经过验证。
身份联合是用于描述在不共享用户机密信息或密码的情况下进行身份数据和认证的术语。身份联合的经典例子是登录到你的员工网站,并能够访问你的福利提供商,而无需再次登录。你的员工网站不会将你的密码与福利提供商共享。相反,员工网站会声明你在某个时间和日期登录,并提供一些关于你的信息。通过这种方式,你的账户在两个孤岛(你的员工网站和福利门户)之间实现了联合,而福利门户并不知道你员工网站的密码。
如你所见,OIDC 有多个组件。为了充分理解 OIDC 的工作原理,我们首先来了解一下 OpenID Connect 协议。
OpenID Connect 协议
我们将关注 OIDC 协议的两个方面:
-
使用令牌与
kubectl和 API 服务器进行交互 -
刷新令牌以保持令牌的最新状态
我们不会过多关注获取令牌的过程。虽然获取令牌的协议遵循标准,但登录过程并不遵循标准。从身份提供者那里获取令牌的方式会有所不同,这取决于你选择实现你的 OIDC 身份提供者 (IdP) 的方式。
OIDC 登录过程生成三个令牌:
-
access_token:此令牌用于向身份提供者可能提供的 Web 服务发起认证请求,例如获取用户信息。Kubernetes 不使用此令牌,并且可以丢弃。此令牌没有标准格式,可能是 JWT,也可能不是。 -
id_token:这是一个 JWT 令牌,封装了你的身份信息,包括你的唯一标识符、组信息以及 API 服务器可以用来授权访问的过期信息。JWT 由你的身份提供者的证书签名,并且 Kubernetes 只需要通过检查 JWT 的签名来验证它。这就是你在每次请求时传递给 Kubernetes 用于身份验证的令牌。 -
refresh_token:kubectl知道如何在id_token过期后自动为你刷新令牌。为此,它会调用你的 IdP 的token端点,使用refresh_token获取一个新的id_token。refresh_token只能使用一次,并且是不可见的,意味着作为令牌持有者的你无法查看它的格式,实际上你也不需要关心。它要么有效,要么无效。refresh_token永远不会传递给 Kubernetes(或任何其他应用)。它仅用于与 IdP 的通信。
refresh_token 在特定情况下可以允许多次使用。Kubernetes Go SDK 在多个进程几乎同时尝试从相同的 kubectl 配置文件刷新令牌时,通常会出现一些已知的问题,这会导致用户的会话丢失,迫使用户重新登录以获取一组新的令牌。许多身份提供者以不同的方式处理这个过程。有些允许 refresh_token 在不同的时间内重新使用。审查身份提供者时,检查这个功能部分非常重要,因为它通常默认设置得更“开放”,以提供更好的用户体验。允许 refresh_token 长期复用会使 refresh_token 提供的安全性大部分失效,因此应该非常小心地使用。
一旦获得了你的令牌,你就可以使用它们与 API 服务器进行身份验证。使用令牌的最简单方法是将它们添加到kubectl配置中,通过命令行参数进行配置:
kubectl config set-credentials username --auth-provider=oidc --auth-provider-arg=idp-issuer-url=https://host/uri --auth-provider-arg=client-id=kubernetes --auth-provider-arg=refresh-token=$REFRESH_TOKEN --auth-provider-arg=id-token=$ID_TOKEN
config set-credentials 有一些需要提供的选项。我们已经解释了 id-token 和 refresh-token,但还有两个额外的选项:
-
idp-issuer-url:这是我们用于配置 API 服务器的相同 URL,指向用于 IdP 发现 URL 的基础 URL。 -
client-id:这是你的 IdP 用来标识你配置的字段。它对于 Kubernetes 部署是唯一的,并且不被认为是秘密信息。
OpenID Connect 协议有一个可选元素,称为 client_secret,它在 OIDC 客户端和 IdP 之间共享。它用于在发起请求之前“验证”客户端,例如刷新令牌。虽然 Kubernetes 将其作为一个选项支持,但建议不要使用它,而是配置你的 IdP 使用公共端点(该端点根本不使用秘密)。
客户端密钥没有实际价值,因为您需要与每个潜在用户共享它,而且由于它是密码,您的企业合规框架可能要求定期更换,从而带来支持上的困难。总体而言,就安全性而言,它的潜在缺点并不值得。
不要使用客户端密钥,您应该确保您的端点使用 代码交换证明密钥 (PKCE) 协议。这个协议最初是为了在没有客户端密钥的 OIDC 请求中增加一层随机性而创建的。虽然在刷新过程中 kubectl 命令不会利用这个协议,但您可能会将集群中的多个应用程序集成到身份提供者中(例如仪表板),这些应用程序可能具有 CLI 组件,并且无法使用客户端密钥。ArgoCD,我们将在最后两章中进行集成,就是一个很好的例子。它的 CLI 工具支持单点登录(SSO),但不同于 kubectl,它会为您启动 SSO。启动时,它会包含 PKCE,因为在每个用户的工作站上您都不会有 client_secret。
Kubernetes 要求您的身份提供者支持发现 URL 端点,这是一个提供 JSON 的 URL,告知您可以在哪里获取用于验证 JWT 的密钥以及可用的各种端点。要访问发现端点,请将任何发布者 URL 后加/.well-known/openid-configuration,该 URL 将提供 OIDC 端点信息。
经过对 OpenID Connect 协议和令牌如何与 Kubernetes 配合工作进行探讨后,接下来我们将逐步了解 Kubernetes 和 kubectl 中的各个组件如何相互作用。
遵循 OIDC 和 API 的交互
一旦kubectl配置完成,所有 API 交互将按照以下顺序进行:

图 6.1:Kubernetes/kubectl OpenID Connect 顺序图
上述图表来自 Kubernetes 的认证页面:kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens。认证请求涉及以下步骤:
-
登录到您的身份提供者(IdP):这对于每个 IdP 都不同。可能需要在 Web 浏览器中的表单中提供用户名和密码、一个多因素令牌,或者一个证书。具体内容将取决于每个实现。
-
向用户提供令牌:一旦认证通过,用户需要一种方式来生成
kubectl访问 Kubernetes API 所需的令牌。这可以是一个应用程序,使用户可以轻松地将令牌复制并粘贴到配置文件中,或者可以是一个需要下载的新文件。 -
这个步骤是将
id_token和refresh_token添加到kubectl配置中的步骤。如果令牌是在浏览器中展示给用户的,它们可以手动添加到配置中。或者,一些解决方案在此步骤提供了一个新的kubectl配置供下载。还有一些kubectl插件,会启动一个 Web 浏览器来开始身份验证过程,一旦完成,便会为你生成配置。 -
注入 id_token:一旦调用了
kubectl命令,每个 API 调用都会包括一个额外的头部,称为 Authorization 头部,里面包含id_token。 -
JWT 签名验证:一旦 API 服务器从 API 调用中接收到
id_token,它会使用身份提供者提供的公钥对签名进行验证。API 服务器还会验证签发者是否与 API 服务器配置中的签发者匹配,并且接收者是否与 API 服务器配置中的客户端 ID 匹配。 -
检查 JWT 的过期时间:令牌只有在有限的时间内有效。API 服务器会确保令牌没有过期。如果令牌已经过期,API 服务器将返回 401 错误代码。
-
授权检查:现在用户已经通过身份验证,API 服务器将通过将提供的
id_token中的用户标识符和声明的组与内部政策匹配,来确定该用户是否能够执行请求的操作。 -
执行 API:所有检查完成后,API 服务器会执行请求,生成一个响应并将其发送回
kubectl。 -
格式化响应以供用户使用:一旦 API 调用(或一系列 API 调用)完成,响应将以 JSON 格式被
kubectl格式化并呈现给用户。
从一般意义上讲,身份验证是验证你是否是你自己的过程。我们大多数人遇到这个过程时,是在将用户名和密码输入到网站时;我们是在证明我们是谁。在企业环境中,授权则是决定我们是否被允许做某件事的过程。首先,我们进行身份验证,然后进行授权。
构建在 API 安全性基础上的标准并不假设进行身份验证,而是直接进行授权,基于某种令牌。并不假设调用者必须被识别。例如,当你使用物理钥匙开门时,门并不知道你是谁,只知道你拥有正确的钥匙。这个术语可能会让人感到困惑,所以如果你有些迷茫也不必感到不好意思,你并不孤单!
id_token 是自包含的;API 服务器所需了解的所有关于你的信息都包含在该令牌中。API 服务器通过使用身份提供者提供的证书来验证 id_token,并验证该令牌是否过期。只要这些信息匹配,API 服务器就会根据自己的 RBAC 配置继续授权你的请求。我们稍后会详细介绍这个过程。最后,假设你已获得授权,API 服务器将提供响应。
请注意,Kubernetes 从未接触过你的密码或任何其他你所知道的秘密信息。唯一共享的是 id_token,而且它是临时的。这导致几个重要的要点:
-
由于 Kubernetes 从不接触你的密码或其他凭证,它无法泄露这些信息。这可以为你节省大量与安全团队合作的时间,因为所有与保护密码相关的任务和控制都可以跳过!
-
id_token是自包含的,这意味着如果它被泄露,除了重新设置身份提供者(identity provider)的密钥外,你无法采取任何措施来防止它被滥用。这就是为什么你的id_token必须具有短生命周期的原因。在 1 到 2 分钟内,攻击者获取id_token、识别它并加以滥用的可能性非常低。
如果在执行调用时,kubectl 发现 id_token 已过期,它将尝试通过使用 refresh_token 调用 IdP 的令牌端点来刷新它。如果用户的会话仍然有效,IdP 会生成新的 id_token 和 refresh_token,并将其存储在 kubectl 配置中。这个过程是自动进行的,无需用户干预。此外,refresh_token 只能使用一次,因此如果有人试图使用已使用过的 refresh_token,你的 IdP 将会失败,无法完成刷新过程。
一旦发生突发的安全事件,某人可能需要立即被锁定;可能是他们被带走了,或者他们的会话已被泄露。令牌的撤销取决于你的 IdP,因此在选择 IdP 时,确保它支持某种形式的会话撤销。
最后,如果 refresh_token 已过期或会话已被撤销,API 服务器将返回一个 401 Unauthorized 消息,表示它将不再支持该令牌。
我们花费了相当多的时间来研究 OIDC 协议。现在,让我们深入了解一下 id_token。
id_token
id_token 是一个 JSON Web 令牌,它是经过 base64 编码并数字签名的。该 JSON 包含一系列属性,称为声明(claims),在 OIDC 中有一些标准的声明,但大多数情况下,你最关心的声明如下:
-
iss:发行者,必须与kubectl配置中的发行者匹配 -
aud:你的客户端 ID -
sub:你的唯一标识符 -
groups:这不是一个标准声明,但它应该填充与 Kubernetes 部署特别相关的组。
许多部署尝试通过你的电子邮件地址来识别你。这是一种反模式,因为你的电子邮件地址通常是基于你的名字,而名字是会变化的。sub 声明应该是一个唯一的标识符,它是不可变的,永远不会改变。这样,如果你的电子邮件因名字的变化而改变,也不会有问题。虽然这可能会使得调试“谁是 cd25d24d-74b8-4cc4-8b8c-116bf4abbd26?”变得更加困难,但它会提供一个更简洁、更易于维护的集群。
还有其他几个声明,表示何时不再接受 id_token。这些声明的单位都是从纪元(1970 年 1 月 1 日)UTC 时间起的秒数:
-
exp:id_token过期的时间。 -
iat:id_token创建的时间。 -
nbf:id_token应该允许的最早时间。
为什么令牌不能只有一个过期时间?
系统生成 id_token 的时钟与评估它的系统的时钟可能不会完全一致。通常会有一些偏差,取决于时钟设置的方式,这个偏差可能是几分钟。除了过期时间外,再加上一个不早于时间(not-before)可以为标准时间偏差提供一些余地。
在 id_token 中还有其他一些声明,这些声明实际上并不重要,但它们为附加上下文提供了信息。示例包括你的名字、联系信息、组织等。
虽然令牌的主要用途是与 Kubernetes API 服务器交互,但它们并不仅限于 API 交互。除了访问 API 服务器外,webhook 调用也可能接收到你的 id_token。
你可能已经将 OPA 部署为集群上的验证 webhook。当有人提交 pod 创建请求时,webhook 会接收到用户的 id_token,这个令牌可以用于做出决策。开放策略代理(OPA)是一个用于验证和授权请求的工具。它通常在 Kubernetes 中作为一个准入控制器 webhook 部署。如果你还没有接触过 OPA 或准入控制器,我们将在第十一章,使用开放策略代理扩展安全性中深入介绍这两者。
一个示例是在准入控制器检查用户的 id_token 时,你希望确保 PVC 根据提交者的组织映射到特定的 PV。组织信息包含在 id_token 中,并被传递到 Kubernetes,然后传递到 OPA webhook。由于令牌已被传递给 webhook,信息就可以在你的 OPA 策略中使用。
我们已经花了大量时间讲解 OpenID Connect 及其如何用于身份验证 API 调用到 Kubernetes 集群。虽然它可能是最好的整体选项,但它并不是唯一的选项。在接下来的部分中,我们将查看其他选项以及何时适合使用它们。
其他身份验证选项
在本节中,我们重点介绍了 OIDC,并阐述了它为何是最佳的认证机制。它当然不是唯一的选项,我们将在本节中介绍其他选项,并讲解它们何时适用。
证书
这通常是每个人第一次认证到 Kubernetes 集群。
一旦 Kubernetes 安装完成,一个包含证书和私钥的预构建 kubectl config 文件就会被创建并准备好使用。这个文件的创建位置取决于发行版。这个文件应该仅在“紧急情况下打破玻璃”时使用,当其他所有身份验证方式都不可用时。它应该受到贵组织的特权访问标准的控制。当使用此配置文件时,它无法识别用户,并且很容易被滥用,因为它无法提供简单的审计跟踪。
虽然这是证书认证的标准用例,但并不是唯一的用例。正确实施的证书认证是行业内最强的认证凭证之一。
美国联邦政府在其最重要的任务中使用证书认证。高层次上,证书认证涉及使用客户端密钥和证书来协商与 API 服务器的 HTTPS 连接。API 服务器可以获取用于建立连接的证书,并将其与证书颁发机构(CA)证书进行验证。一旦验证通过,系统将从证书中提取属性并映射到用户和 API 服务器可识别的组。
为了获得证书认证的安全性好处,私钥需要在隔离的硬件上生成,通常是以智能卡的形式,并且永远不能离开该硬件。生成证书签名请求并提交给证书颁发机构(CA),CA 会签名公钥,从而创建证书,之后将其安装在专用硬件上。CA 在任何时候都不会获取私钥,因此即使 CA 被攻破,也无法获得用户的私钥。如果需要撤销证书,它会被添加到撤销列表中,该列表可以从LDAP目录或文件中提取,也可以通过OCSP协议进行检查。
这可能看起来是一个吸引人的选项,那为什么不在 Kubernetes 中使用证书呢?
-
智能卡集成使用的标准是PKCS11,但是
kubectl或 API 服务器都不支持该标准。 -
API 服务器无法检查证书撤销列表或使用OCSP,因此一旦证书被签发,就无法撤销它。由于 API 服务器无法撤销证书,任何拥有证书的人都可以继续使用它,直到它过期。
此外,正确生成密钥对的过程很少被使用。它需要构建一个复杂的界面,这对用户来说很难操作,并且需要运行命令行工具。为了解决这个问题,证书和密钥对通常是为你生成的,你只需下载它们或通过邮件接收,这就削弱了整个过程的安全性。
你不应使用证书认证的另一个原因是,它很难利用群组。尽管你可以将群组嵌入证书的主题中,但你无法撤销证书。因此,如果用户的角色发生变化,你可以为他们颁发新的证书,但无法阻止他们使用旧的证书。虽然你可以在 RoleBindings 和 ClusterRoleBindings 中直接引用用户,但这是一种反模式,会使得在即使是小型集群中也难以跟踪访问权限。
正如本节介绍中所述,使用证书进行身份验证在“紧急情况时打破玻璃”是一种良好的证书身份验证方式。如果所有其他身份验证方法出现问题,证书认证可能是进入集群的唯一途径。
在证书之后,最常见的替代方案是使用 ServiceAccount 令牌。接下来我们将讨论这个问题,并解释为什么不应该从集群外部使用它们。
服务账户
ServiceAccount 旨在为在集群中运行的容器提供身份,以便当这些容器调用 API 服务器时,它们可以进行身份验证并应用 RBAC 规则。不幸的是,用户开始使用与 ServiceAccount 对象关联的令牌从集群外部访问 API 服务器,这带来了多个问题:
-
令牌的安全传输:服务账户是自包含的,不需要任何解锁或验证所有权的机制,因此如果令牌在传输过程中被盗取,你无法阻止其使用。你可以建立一个系统,让用户登录并下载包含令牌的文件,但你现在拥有的是一个安全性大大降低的 OIDC 版本。
-
没有过期时间:当你解码一个传统的服务账户令牌时,并没有任何信息告诉你令牌何时过期。这是因为令牌永不过期。你可以通过删除服务账户并重新创建它来撤销令牌,但这意味着你需要建立一个系统来执行这个操作。再次说明,你已经构建了一个能力较弱的 OIDC 版本。
-
审计:服务账户一旦密钥被获取,所有者可以轻松地将其发放出去。如果多个用户共享一个密钥,那么审计账户使用情况就变得非常困难。
从 Kubernetes 1.24 开始,静态的 ServiceAccount 令牌默认被禁用,并用短期有效的令牌替代,这些令牌通过 TokenRequest API 被“投影”到你的容器中。我们将在下一节详细讲解这些令牌。这里提供生成静态令牌的说明,作为反模式的示例。虽然在某些狭窄的使用场景中,静态令牌是有用的,但它们应避免在集群外部使用。它们通常由管道使用,接下来我们将在本章中探讨替代方法。
服务账户看似提供了一种简便的访问方法。创建它们很简单。以下命令创建一个服务账户对象,并创建一个与之关联的密钥,用来存储该服务账户的令牌:
kubectl create sa mysa -n default
kubectl create -n default -f - <<EOF
apiVersion: v1
kind: Secret
metadata:
name: mysa-secret
annotations:
kubernetes.io/service-account.name: mysa
type: kubernetes.io/service-account-token
EOF
上述步骤:
-
创建一个
ServiceAccount对象 -
创建一个与
ServiceAccount绑定的令牌的Secret
接下来,以下命令将以 JSON 格式检索服务账户的令牌,并只返回令牌的值。然后可以使用此令牌访问 API 服务器:
kubectl get secret mysa-secret -o json | jq -r '.data.token' | base64 -d
为了演示这一点,让我们直接调用 API 端点,不提供任何凭证(确保你使用的是自己本地控制平面的端口):
curl -v --insecure https://0.0.0.0:6443/api
你将收到以下信息:
.
.
.
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {
},
"status": "Failure",
"message": "forbidden: User \"system:anonymous\" cannot get path
\"/api\"",
"reason": "Forbidden",
"details": {
},
"code": 403
* Connection #0 to host 0.0.0.0 left intact
默认情况下,大多数 Kubernetes 发行版不允许匿名访问 API 服务器,因此我们收到了 403 错误,因为我们没有指定用户。
现在,让我们将我们的服务账户添加到 API 请求中:
export KUBE_AZ=$(kubectl get secret mysa-secret -o json | jq -r '.data.token' | base64 -d)
curl -H "Authorization: Bearer $KUBE_AZ" --insecure
https://0.0.0.0:6443/api
{
"kind": "APIVersions",
"versions": [
"v1"
],
"serverAddressByClientCIDRs": [
{
"clientCIDR": "0.0.0.0/0",
"serverAddress": "172.17.0.3:6443"
}
]
}
成功了!我们成功使用了一个静态的 ServiceAccount 令牌来验证 API 服务器。正如我们之前所说,这是一个反模式。除了令牌本身的问题外,我们还会发现无法将服务账户放入任意组中。这意味着 RBAC 绑定要么直接绑定到服务账户,要么使用服务账户所在的预构建组。我们将在讨论授权时探讨为何这是个问题,但这里是为什么这是个问题的一个示例:直接绑定意味着,为了知道一个用户是否应该访问,你需要处理每一个绑定,逐一查找用户,而不是简单地查看一个已经将用户按组组织好的外部数据库,这增加了合规负担。
最后,服务账户从未设计为在集群外部使用。这就像用锤子拧螺丝一样。用足够的力气和耐心,你可能能把它拧进去,但这不美观,最终结果也不会让任何人满意。
现在我们已经讨论了 ServiceAccount 令牌的工作原理,并且了解到不应将它们用于用户,接下来我们将探讨为何应利用 TokenRequest API 为你的 ServiceAccounts 生成短期有效的令牌。
TokenRequest API
TokenRequest API 是 Kubernetes 1.24+ 中生成 ServiceAccount 令牌的方式。该 API 消除了使用静态传统服务账户,取而代之的是将账户投影到你的 Pod 中。这些投影的令牌是短生命周期的,并且对每个独立的 Pod 都是唯一的。最后,这些令牌一旦与其关联的 Pod 被销毁就会失效。这使得嵌入 Pod 中的服务账户令牌更加安全。
该 API 提供了另一个很棒的功能:你可以与第三方服务一起使用它。一个例子是使用 HashiCorp 的 Vault 秘密管理系统来验证 Pod,而无需对 API 服务器进行令牌审核 API 调用来验证它。我们将在 第八章,管理机密 中探索这种方法。
这个功能使得你的 Pod 更容易且更安全地调用外部 API。
TokenRequest API 允许你为特定作用域请求一个短生命周期的服务账户。尽管它提供了稍微更好的安全性,因为它会过期并且具有有限的作用域,但它仍然绑定于一个服务账户,这意味着没有组,并且仍然存在安全地将令牌传递给用户和审计其使用的问题。
从 1.24 开始,所有服务账户令牌默认通过 TokenRequest API 投影到 Pod 中。尽管新令牌有效期为一年,因此并不算非常短生命周期!不过,即使令牌设置为快速过期,API 服务器也不会拒绝它。它会记录有人在使用过期令牌。这是为了让从无限期令牌到短生命周期令牌的过渡更加平滑。
有些人可能会被诱惑用令牌进行用户身份验证。然而,由 TokenRequest API 生成的令牌仍然是为 Pod 与集群通信或与第三方 API 通信而设计的;它们并不适用于用户使用。为了使用它们,你需要创建并安全地传输它们。由于它们仍然是持有者令牌,这可能导致令牌丢失并最终发生泄露。如果你处于需要使用它们的情况,因为没有其他技术选择:
-
尽可能使令牌的生命周期尽量短
-
创建自动化的轮换过程
-
确保你的 SIEM 监控这些账户在预期场景之外的使用情况
类似于静态 ServiceAccount 令牌,在某些情况下,你可能需要一个可以从集群外部使用的令牌,例如引导集成或简单的测试。现在,kubectl 命令包含 token 子命令,可以为 ServiceAccount 生成一个短生命周期的令牌,而无需创建静态 Secret:
$ export KUBE_AZ=$(kubectl create token mysa -n default)
$ curl -H "Authorization: Bearer $KUBE_AZ" --insecure \
https://0.0.0.0:6443/api
{
"kind": "APIVersions",
"versions": [
"v1"
],
"serverAddressByClientCIDRs": [
{
"clientCIDR": "0.0.0.0/0",
"serverAddress": "172.17.0.3:6443"
}
]
}
我们从 kubectl 获取的令牌有效期为一个小时。这是可以调整的,但对于需要外部令牌的少数使用场景,这比创建静态令牌要好得多。
自定义身份验证 Webhook
如果你已经拥有一个不使用现有标准的身份平台,可以通过自定义身份验证 Webhook 来集成它,而不必自定义 API 服务器。这一功能通常由托管 Kubernetes 实例的云提供商使用。
你可以定义一个身份验证 Webhook,API 服务器将调用它并使用一个令牌来验证该令牌并获取用户信息。除非你管理的是一个公共云并且在为其构建 Kubernetes 发行版,否则不要这么做。编写自己的身份验证系统就像是编写自己的加密算法 —— 还是别做了。我们见过的每一个 Kubernetes 自定义身份验证系统最终都变成了 OIDC 的模仿版本,或者是“传递密码”。就像用锤子拧螺丝一样,你能做到,但会非常痛苦。主要原因是,你更有可能把螺丝钉打进自己的脚,而不是把它拧进木板。
到目前为止,我们专注于 Kubernetes 身份验证的基本原理,探讨了推荐的模式和反模式。接下来,让我们通过配置 Kubernetes 集群中的身份验证,将这些理论付诸实践。
配置 KinD 以支持 OpenID Connect
对于我们的示例部署,我们将使用来自客户 FooWidgets 的场景。FooWidgets 拥有一个 Kubernetes 集群,他们希望通过 OIDC 集成。拟议的解决方案需要满足以下要求:
-
Kubernetes 必须使用我们的中央身份验证系统 —— Active Directory
-
我们需要能够将 Active Directory 组映射到我们的 RBAC
RoleBinding对象中 -
用户需要访问 Kubernetes Dashboard
-
用户需要能够使用 CLI
-
必须满足所有企业合规性要求
-
其他集群管理应用程序也需要集中管理
让我们详细探讨每一个要求,并解释我们如何满足客户的需求。
满足要求
我们企业的需求涉及多个移动部件,既包括集群内部,也包括集群外部。我们将逐一检查这些组件及其与构建身份验证集群的关系。
使用 LDAP 和 Active Directory 与 Kubernetes 配合
目前大多数企业使用 Active Directory 来存储用户及其凭据的信息,且通常使用 Microsoft™ 的解决方案。根据企业的规模,通常会有多个域或森林存储用户数据。
我们需要一个能够与每个域通信的解决方案。您的企业可能拥有用于 OpenID Connect 集成的多种工具和产品,或者您可能只想通过 LDAP 进行连接。LDAP,即轻量级目录访问协议,是一种标准协议,已经使用了 30 多年,至今仍是与 Active Directory 直接通信的标准方式。通过使用 LDAP,您可以查找用户并验证他们的密码。这也是最简单的启动方式,因为它不需要与身份提供者集成。您只需要一个服务账户和凭据!
对于 FooWidgets,我们将直接连接到 Active Directory 进行所有身份验证。
别担心——您无需提前准备好 Active Directory 就可以进行这个练习。我们将一步步通过将演示目录部署到我们的 KinD 集群中来进行操作。
将 Active Directory 组映射到 RBAC 角色绑定
当我们开始讨论授权时,这一点将变得非常重要。Active Directory 会列出用户所在的所有组,并将其列在memberOf属性中。我们可以直接从已登录用户的帐户中读取此属性,以获取他们的组。这些组将嵌入到我们的id_token中的groups声明中,并可以直接在 RBAC 绑定中引用。这样,我们就可以集中管理授权,而不必手动操作 RBAC 绑定,从而简化管理并减少我们在集群中需要管理和维护的对象数量。
Kubernetes 仪表盘访问
仪表盘是快速访问集群信息并进行快速更新的强大工具。与人们普遍认为仪表盘存在安全问题不同,当正确部署时,它不会产生任何安全隐患。正确的部署方式是不授予任何权限,而是依赖用户自己的凭据。我们将通过反向代理实现这一点,反向代理会在每次请求时注入用户的 OIDC 令牌,然后仪表盘在调用 API 服务器时将使用该令牌。通过这种方法,我们将能够像管理任何其他 Web 应用程序一样限制对仪表盘的访问。
使用kubectl内置的代理和端口转发并不是访问仪表盘的最佳策略,原因有几个。许多企业不会在本地安装 CLI 工具,这迫使您必须通过跳板机访问像 Kubernetes 这样的特权系统,这意味着端口转发将无法使用。即使您可以在本地运行kubectl,在回环地址(127.0.0.1)上打开端口意味着您的系统上的任何程序都可以使用它,而不仅仅是您在浏览器中使用。尽管浏览器有控制措施来阻止恶意脚本访问回环地址上的端口,但这并不能阻止您工作站上的其他程序。最后,这也不是一个理想的用户体验。
我们将在第十章中深入探讨这一机制的细节及其为何有效,部署安全的 Kubernetes 仪表盘。
Kubernetes CLI 访问
大多数开发人员希望能够访问kubectl以及其他依赖kubectl配置的工具。例如,Visual Studio Code 的 Kubernetes 插件不需要任何特别的配置。它只是使用kubectl内建的配置。大多数企业会严格限制你能够安装的二进制文件,因此我们希望最小化任何额外的工具和插件。
企业合规要求
云原生并不意味着你可以忽视企业的合规要求。大多数企业有一些要求,例如要求 20 分钟的空闲超时、特权访问的多因素认证等。我们实施的任何解决方案都必须通过控制电子表格的审批才能上线。而且,毫无疑问,所有内容都必须加密(我指的是所有内容)。
整合所有内容
为了满足这些需求,我们将使用OpenUnison。它提供了预构建的配置,可与 Kubernetes、仪表板、CLI 和 Active Directory 配合使用。
它的部署速度也非常快,因此我们不需要专注于特定提供商的实现细节,而是可以专注于 Kubernetes 的配置选项。我们的架构将是这样的:

图 6.2:身份验证架构
尽管我们在此实例中使用的是“Active Directory”,但你的企业可能已经有现成的身份提供者,如Okta、Entra(前身为 Azure Active Directory)、KeyCloak等。在这些情况下,仍然建议在集群中使用身份提供者,不仅支持集群内的 SSO,还支持集群管理应用程序。随着我们继续进行本书的内容,我们将集成监控系统、日志记录、GitOps 系统等。从管理角度来看,配置 SSO 与所有这些应用程序可能会很困难,因此拥有一个由你作为集群所有者控制的身份提供者,可以为你提供更大的灵活性,使得通过将管理应用程序与企业认证集成来提供更好的安全性,而不是依赖像端口转发这样的未经认证的方法。
在我们的实现中,我们将使用两个主机名:
-
k8s.apps.X-X-X-X.nip.io:访问 OpenUnison 门户,我们将在这里启动登录并获取令牌 -
k8sdb.apps.X-X-X-X.nip.io:访问 Kubernetes 仪表板
简单回顾一下,nip.io是一个公共 DNS 服务,它会返回你主机名中嵌入的 IP 地址。在实验环境中,这非常有用,因为设置 DNS 可能会非常麻烦。在我们的示例中,X-X-X-X是你 Docker 主机的 IP。
当用户尝试访问 https://k8s.apps.X-X-X-X.nip.io/ 时,系统将要求他们输入用户名和密码。用户提交后,OpenUnison 会通过 Active Directory 查找用户,检索用户的个人资料信息。此时,OpenUnison 会在 OpenUnison 命名空间中创建用户对象,用于存储用户信息并创建 OIDC 会话。
之前我们描述了 Kubernetes 没有用户对象的情况。Kubernetes 允许你通过 自定义资源定义(CRDs)扩展基础 API。OpenUnison 定义了一个用户 CRD 来帮助实现高可用性,并避免需要一个数据库来存储状态。这些用户对象不能用于 RBAC。
一旦用户登录 OpenUnison,他们可以获取 kubectl 配置来使用 CLI 或 Kubernetes Dashboard (kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/) 从浏览器访问集群。当用户准备好时,他们可以退出 OpenUnison,这将结束他们的会话并使他们的 refresh_token 失效,这样他们就无法在重新登录之前使用 kubectl 或仪表板。如果他们在午餐时未退出,回到办公桌时,refresh_token 将会过期,因此他们需要重新登录才能与 Kubernetes 交互。
现在我们已经走过了用户如何登录并与 Kubernetes 交互的过程,接下来我们将部署 OpenUnison 并将其集成到集群中进行身份验证。
部署 OpenUnison
我们已经自动化了 OpenUnison 的部署,因此没有任何手动步骤。由于我们想从一个新的集群开始,我们将删除当前的集群,并执行 chapter2 文件夹中的 create-cluster.sh 脚本来创建一个全新的 KinD 集群。我们还在 chapter6 目录中添加了一个名为 deploy_openunison_imp_noimpersonation.sh 的脚本。你可以按照以下步骤创建新集群并集成 OIDC:
cd Kubernetes-An-Enterprise-Guide-Third-Edition/chapter2
kind delete cluster -n cluster01
./create-cluster.sh
cd ../chapter6/user-auth
./deploy_openunison_imp_noimpersonation.sh
这可能需要几分钟,具体取决于你的硬件。这个脚本做了几件事情:
-
使用一个名为 ApacheDS 的项目创建一个替代的“Active Directory”。你不需要了解 ApacheDS 的任何内容,除了它充当我们“Active Directory”的角色。
-
部署 Kubernetes Dashboard 版本 2.7。
-
下载
ouctl工具和 OpenUnison helm charts。 -
更新
values.yaml文件,以便与您的 Ubuntu 虚拟机的 IP 配合使用。 -
部署 OpenUnison。
你可以通过使用分配的 nip.io 地址,从网络中的任何机器登录到 OIDC 提供程序。由于我们将使用仪表板测试访问,你可以使用任何带浏览器的机器。
在浏览器中打开 network.openunison_host,它位于你运行上述脚本时创建的 /tmp/openunison-values.yaml 文件中。系统会提示输入用户名 mmosley 和密码 start123,然后点击 Sign in。
有关如何在存储库的 README 文件中添加自己的用户帐户的说明,请查看 chapter6 目录。

图 6.3:OpenUnison 登录界面
当你这样做时,你会看到这个屏幕:

图 6.4:OpenUnison 主屏幕
点击Kubernetes 仪表盘链接来测试 OIDC 提供者。当你看到初始仪表盘屏幕时不要惊慌,会看到类似以下的内容:

图 6.5:在完成与 API 服务器的 SSO 集成之前的 Kubernetes 仪表盘
看起来像是很多错误!我们在仪表板上,但似乎没有任何权限。这是因为 API 服务器不信任 OpenUnison 生成的令牌。为了解决这个问题,下一步是告诉 Kubernetes 信任 OpenUnison 作为其 OpenID Connect 身份提供者。
配置 Kubernetes API 使用 OIDC
到此为止,你已将 OpenUnison 部署为 OIDC 提供者,并且它已经在运行,但是你的 Kubernetes 集群尚未配置为使用它作为提供者。
要配置 API 服务器使用 OIDC 提供程序,您需要向 API 服务器添加 OIDC 选项,并提供 OIDC 证书,以便 API 将信任 OIDC 提供程序。
因为我们使用的是 KinD,所以可以使用几个 kubectl 和 docker 命令来添加所需的选项。
要向 API 服务器提供 OIDC 证书,我们需要提取证书并将其复制到 KinD 主服务器上。我们可以在 Docker 主机上使用两个命令来完成这个操作:
-
第一个命令从其密钥中提取 OpenUnison 的 TLS 证书。这与 OpenUnison 的 Ingress 对象引用的相同密钥。我们使用
jq实用程序从密钥中提取数据,然后对其进行 Base64 解码:kubectl get secret ou-tls-certificate -n openunison -o json | jq -r '.data["tls.crt"]' | base64 -d > ou-ca.pem -
第二个命令将证书复制到主服务器的
/etc/kubernetes/pki目录中:docker cp ou-ca.pem cluster01-control-plane:/etc/kubernetes/pki/ou-ca.pem -
正如我们之前提到的,要将 API 服务器与 OIDC 集成,我们需要具有 API 选项的 OIDC 值。要列出我们将使用的选项,请在
openunison命名空间中描述api-server-configConfigMap:kubectl describe configmap api-server-config -n openunison Name: api-server-config Namespace: openunison Labels: <none> Annotations: <none> Data ==== oidc-api-server-flags: ---- --oidc-issuer-url=https://k8sou.apps.192-168-2-131.nip.io/auth/idp/k8sIdp --oidc-client-id=Kubernetes --oidc-username-claim=sub --oidc-groups-claim=groups --oidc-ca-file=/etc/kubernetes/pki/ou-ca.pem -
接下来,编辑 API 服务器配置。通过在 API 服务器上更改标志来配置 OpenID Connect。这就是为什么托管的 Kubernetes 通常不提供 OpenID Connect 作为选项的原因,但我们将在本章后面详细介绍。每个发行版处理这些更改的方式都不同,因此请参考您供应商的文档。对于 KinD,请进入控制平面并更新清单文件:
docker exec -it cluster01-control-plane bash apt-get update apt-get install vim -y vi /etc/kubernetes/manifests/kube-apiserver.yaml -
在
command下添加 ConfigMap 输出的标志。确保在前面加上空格和破折号(-)。确保更新网址以匹配你的网址。完成后应如下所示:- --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname - --oidc-issuer-url=https://k8sou.apps.192-168-2-131.nip.io/auth/idp/k8sIdp - --oidc-client-id=Kubernetes - --oidc-username-claim=sub - --oidc-groups-claim=groups - --oidc-ca-file=/etc/kubernetes/pki/ou-ca.pem - --proxy-client-cert-file=/etc/kubernetes/pki/front-proxy-client.crt -
退出 vim 和 Docker 环境(Ctrl + D),然后查看
api-serverpod:kubectl get pod kube-apiserver-cluster01-control-plane -n kube-system NAME READY STATUS RESTARTS AGE kube-apiserver-cluster-auth-control-plane 1/1 Running 0 73s
请注意,它只有 73s 的历史。这是因为 KinD 检测到清单发生了变化,并重新启动了 API 服务器。
API 服务器 pod 被称为静态 pod。这个 pod 不能直接修改;它的配置必须从磁盘上的清单文件进行修改。这使得你能够管理由 API 服务器作为容器管理的进程,但如果出现问题,你不需要直接编辑 etcd 中的 pod 清单。
一旦你更新了 API 服务器标志,下一步是验证你是否可以登录到你的集群。接下来让我们逐步走过这些步骤。
验证 OIDC 集成
一旦 OpenUnison 和 API 服务器已集成,我们需要测试连接是否正常:
-
为了测试集成,重新登录 OpenUnison,然后再次点击 Kubernetes Dashboard 链接。
-
点击右上角的铃铛,你会看到一个不同的错误:

图 6.6:SSO 已启用,但用户未授权访问任何资源
OpenUnison 和 Kubernetes 之间的 SSO 已经工作!然而,新的错误 service is forbidden: User https://... 是一个授权错误,而非认证错误。此时,API 服务器已经知道我们是谁,但未允许我们访问 API。
-
我们将在下一章深入探讨 RBAC 和授权的细节,但现在,创建这个 RBAC 绑定:
kubectl create -f - <<EOF apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: ou-cluster-admins subjects: - kind: Group name: cn=k8s-cluster-admins,ou=Groups,DC=domain,DC=com apiGroup: rbac.authorization.k8s.io roleRef: kind: ClusterRole name: cluster-admin apiGroup: rbac.authorization.k8s.io EOF clusterrolebinding.rbac.authorization.k8s.io/ou-cluster-admins created -
最后,返回仪表盘,你会看到你对集群有了完全访问权限,并且所有错误消息都消失了。
API 服务器和 OpenUnison 已经连接。此外,已经创建了一个 RBAC 策略,使我们的测试用户能够以管理员身份管理集群。通过登录 Kubernetes Dashboard 验证了访问权限,但大多数交互将通过 kubectl 命令进行。下一步是验证我们是否能够使用 kubectl 访问集群。
使用你的令牌与 kubectl 配合使用
本节假设你有一台网络上的机器,已安装浏览器并运行 kubectl。
使用仪表盘有其使用场景,但在大多数情况下,你可能会使用 kubectl 与 API 服务器进行交互,而不是使用仪表盘。在本节中,我们将解释如何获取你的 JWT 以及如何将其添加到 Kubernetes 配置文件中,以便你可以使用 kubectl:
- 你可以从 OpenUnison 仪表盘中获取你的令牌。导航到 OpenUnison 首页,点击标有 Kubernetes Tokens 的密钥。你将看到如下的界面:

图 6.7:OpenUnison kubectl 配置工具
OpenUnison 提供了一个命令行,你可以将其复制并粘贴到主机会话中,以便将所有所需的信息添加到你的配置中。
-
首先,点击 kubectl Command(如果你是 Windows 用户,则点击 kubectl Windows Command)旁边的双文档按钮,将你的
kubectl命令复制到剪贴板。保持网页浏览器在后台打开。 -
在粘贴 OpenUnison 中的
kubectl命令之前,你可能想备份原始配置文件:export KUBECONFIG=$(mktemp) kubectl get nodes W0804 13:43:26.624417 3878806 loader.go:222] Config not found: /tmp/tmp.tqcXxwBh0H to the server localhost:8080 was refused - did you specify the right host or port? -
然后,转到你的主机控制台,将命令粘贴到控制台中(以下输出已被简化,但你的粘贴操作将以相同的输出开始):
export TMP_CERT=$(mktemp) && echo -e "-----BEGIN CER. . . Cluster "no-impersonation" set. Context "no-impersonation" created User "mmosley@no-impersonation" set. Switched to context "no-impersonation". -
现在,验证你是否可以使用
kubectl get nodes查看集群节点:kubectl get nodes NAME STATUS ROLES AGE VERSION cluster01-control-plane Ready control-plane 7m47s v1.27.3 cluster01-worker Ready <none> 7m26s v1.27.3 -
你现在使用的是登录凭据,而不是主证书!在工作过程中, session 会自动刷新。你可以使用
kubectl auth whoami命令验证你的身份:kubectl auth whoami ATTRIBUTE VALUE Username https://k8sou.apps.192-168-2-82.nip.io/auth/idp/k8sIdp#mmosley Groups [cn=group2,ou=Groups,DC=domain,DC=com cn=k8s-cluster-admins,ou=Groups,DC=domain,DC=com system:authenticated]
此命令将告诉你 API 服务器认为你是谁,包括你的用户组。这在调试授权时非常有用。
当我在 2015 年首次开始使用 Kubernetes 时,我打开的第一个问题就是为这个特性而开。我在调试 OpenUnison 与 Kubernetes 集成时,看到它在 1.0.26 版本中初步实现,并在 1.0.28 版本中正式发布,我感到非常激动。它在 1.27 中是一个 beta 特性,我们已经预配置了 KinD 集群来支持它。如果你想在其他集群中使用这个特性,可能需要与供应商合作,因为它需要 API 服务器的命令行参数。
-
注销 OpenUnison 并观察节点列表。在一两分钟内,你的令牌将过期并停止工作:
kubectl get nodes Unable to connect to the server: failed to refresh token: oauth2: cannot fetch token: 401 Unauthorized
恭喜!你现在已经设置好了集群,使其完成以下操作:
-
使用 LDAP 进行身份验证,利用企业现有的身份验证系统
-
使用来自集中式身份验证系统的用户组来授权访问 Kubernetes(我们将在下一章详细讲解如何操作)
-
使用集中式凭据,授予用户对 CLI 和 Dashboard 的访问权限
-
通过使用短期有效的令牌,维护企业的合规要求,并提供过期机制
-
确保所有内容都使用 TLS,从用户的浏览器到 Ingress 控制器,再到 OpenUnison、Dashboard,最终到 API 服务器
你已经将本章中的大部分建议集成到你的集群中。你还使得访问变得更容易,因为你不再需要预配置配置文件了。
接下来,你将学习如何将集中式身份验证集成到你的托管集群中。
引入模拟身份功能,以将身份验证与云托管集群集成
使用来自 Google、Amazon、Microsoft 和 DigitalOcean(以及其他许多云服务商)的托管 Kubernetes 服务非常流行。
这些服务通常非常快速地启动并运行,它们有一个共同点:它们大多不支持 OpenID Connect(Amazon 的 EKS 现在支持 OpenID Connect,但集群必须运行在公共网络上并拥有商业签名的 TLS 证书)。
在本章前面,我们讨论了 Kubernetes 如何通过 webhooks 支持自定义身份验证解决方案,并且你永远不应该使用这种方法,除非你是一个公共云服务提供商或其他托管 Kubernetes 系统的主机。事实上,几乎每个云服务商都有自己使用 webhooks 的方式,这些方式使用了各自的身份和访问管理实现。那么,为什么不直接使用供应商提供的呢?有几个原因可能使你不想使用云服务商的 IAM 系统:
-
技术性:你可能希望以安全的方式支持云服务商未提供的功能,比如仪表盘。
-
组织性:将管理 Kubernetes 的访问与云服务的 IAM 紧密结合,会给云团队带来额外的负担,这意味着他们可能不愿意管理对你的集群的访问。
-
用户体验:你的开发人员和管理员可能需要跨多个云平台工作。提供一致的登录体验使他们更轻松,并且需要学习的工具更少。
-
安全性与合规性:云实现可能不提供符合企业安全要求的选项,如短期令牌和空闲超时。
话虽如此,使用云服务商的实现可能也有其理由。然而,你需要平衡这些需求。如果你希望继续使用集中式的身份验证和授权来管理托管的 Kubernetes,你将需要学习如何使用冒充。
什么是 impersonation(冒充)?
Kubernetes 冒充是一种告诉 API 服务器你是谁的方法,而无需知道你的凭证或强制 API 服务器信任 OpenID Connect 身份提供者。这在你无法配置 OpenID Connect(如托管 Kubernetes 服务通常是这种情况),或者你希望支持多个身份提供者的访问时非常有用。
当你使用kubectl时,API 服务器不会直接接收你的id_token,而是会接收一个服务账户或身份认证证书,这些证书将被授权冒充用户,并且包含一组头信息,告诉 API 服务器代理代表谁行事:

图 6.8:用户使用冒充时与 API 服务器交互的示意图
反向代理负责确定如何从用户提供的id_token(或任何其他令牌)映射到Impersonate-User和Impersonate-Group HTTP 头。仪表盘不应以具有特权身份的方式部署,因为冒充功能属于这种身份的范围。
要允许 2.x 仪表盘使用冒充功能,使用类似的模型,但不是访问 API 服务器,而是访问仪表盘:

图 6.9:带有冒充功能的 Kubernetes 仪表盘
用户与反向代理的交互方式与任何 Web 应用程序相似。反向代理使用自己的服务账户并添加模拟头信息。仪表板通过所有请求将此信息传递给 API 服务器。仪表板从不拥有自己的身份。
现在我们了解了模拟是什么,以及它如何帮助我们安全访问 Kubernetes 仪表板和 Kubernetes API,我们将详细介绍在实施时从安全角度考虑的事项。
安全考虑
服务账户有一定的超级权限:它可以被用来模拟任何人(取决于您的 RBAC 定义)。如果您从集群内部运行您的反向代理,则服务账户是可以接受的,特别是如果结合 TokenRequest API 以保持令牌的短寿命。
在本章的早些时候,我们谈到了用于 ServiceAccount 对象的遗留令牌没有过期。这在这里很重要,因为如果您将您的反向代理托管在集群外,那么如果它被 compromise,某人可以使用该服务账户作为任何人访问 API 服务。确保您经常轮换该服务账户。如果您在集群外运行代理,最好使用短寿命证书而不是服务账户。
在集群上运行代理时,您希望确保它被锁定。至少它应该在自己的命名空间中运行,也不要在 kube-system 中。您希望尽量减少可以访问的人数。始终使用多因素身份验证来进入该命名空间是一个好主意,同样使用控制哪些 Pod 可以访问反向代理的网络策略也是如此。
基于我们刚学到的有关模拟的概念,下一步是更新我们集群的配置,以使用模拟而不是直接使用 OpenID Connect。您不需要云管理的集群来处理模拟。
配置您的集群进行模拟
让我们为我们的集群部署一个模拟代理。就像直接将我们的集群集成到 OpenUnison 中使用 OpenID Connect 一样,我们已经自动化部署,因此您不需要手动配置 OpenUnison。我们将清理旧的集群并重新开始:
cd Kubernetes-An-Enterprise-Guide-Third-Edition/chapter2
kind delete cluster -n cluster01
./create-cluster.sh
cd ../chapter6/user-auth
./deploy_openunison_imp_impersonation.sh
此脚本与我们原始的脚本的区别在于:
-
配置 OpenUnison 生成
NetworkPolicy对象以限制仅允许来自我们的 NGINXIngress控制器和 API 服务器的请求 -
配置 OpenUnison 的
ServiceAccount令牌,使其仅有效 10 分钟,而不是典型的一小时或一天 -
配置 OpenUnison 的
values.yaml以部署 kube-oidc-proxy 处理传入的 API 服务器请求。 -
创建 cluster-admin 的
ClusterRoleBinding,以便您的用户可以与您的集群一起工作
一旦脚本运行完成,您可以像之前一样使用相同的账户 mmosley 登录。
OpenUnison 的 helm charts 会创建 NetworkPolicies,并将其 ServiceAccount token 的生命周期约束为与我们上面讨论的安全最佳实践一致。我们必须确保任何不应与我们假扮代理交互的系统不会与其交互,从而减少潜在的攻击面,确保任何能够假扮其他用户的 token 会迅速过期。
接下来,我们将通过测试基于假扮的集成来进行演示。
测试假扮
现在,让我们测试一下我们的假扮设置。按照以下步骤操作:
-
在浏览器中输入你的 OpenUnison 部署的 URL。这与最初用于 OIDC 部署的 URL 相同。
-
登录到 OpenUnison,然后点击仪表板。
-
点击右上角的小圆形图标,查看你当前以谁的身份登录。
-
接下来,返回到 OpenUnison 主面板,点击 Kubernetes Tokens 标签。
-
注意,传递给
kubectl的--server标志不再是 IP 地址,而是来自/tmp/openunison-values.yaml文件中的network.api_server_host的主机名。这就是假扮。你不再直接与 API 服务器交互,而是通过kube-oidc-proxy的反向代理进行交互。 -
最后,让我们将 OpenUnison tokens 页面上的
kubectl命令复制并粘贴到 shell 中:export TMP_CERT=$(mktemp) && echo -e "-----BEGIN CERTIFI... Cluster "impersonation" set. Context "impersonation" created. User "mmosley@impersonation" set. Switched to context "impersonation". -
要验证你是否有访问权限,请列出集群节点:
kubectl get nodes NAME STATUS ROLES AGE VERSION cluster01-control-plane Ready control-plane 37m v1.27.3 cluster01-worker Ready <none> 37m v1.27.3 -
就像在 OIDC 集成中一样,你可以使用
kubectl auth whoami来验证你的身份:kubectl auth whoami ATTRIBUTE VALUE Username mmosley Groups [cn=group2,ou=Groups,DC=domain,DC=com cn=k8s-cluster-admins,ou=Groups,DC=domain,DC=com system:authenticated]
使用假扮而不是 OIDC 集成时,你的身份的主要区别在于你的用户名前面没有身份提供者的 URL。
-
就像你在集成 OpenID Connect 的初始部署时一样,一旦你退出 OpenUnison 页面,几分钟内 token 将会过期,你将无法刷新它们:
kubectl get nodes Unable to connect to the server: failed to refresh token: oauth2: cannot fetch token: 401 Unauthorized
你现在已经验证了集群在假扮模式下能正常工作。你不再直接向 API 服务器进行身份验证,而是通过假扮反向代理(OpenUnison)将所有请求转发到 API 服务器,并附带正确的假扮头部。你仍然满足企业需求,提供了登录和登出过程,并集成了你的 Active Directory 群组。
你还会注意到,你现在可以从网络上的任何系统访问你的集群!这可能会使得本书中接下来的示例更加容易执行。
假扮不仅仅是访问你的集群。接下来,我们将看看如何从 kubectl get debug 授权策略中使用假扮。
使用假扮进行调试
模拟操作可用于调试身份验证和授权配置。当你开始编写 RBAC 策略时,它将变得更加有用。作为管理员,你可以通过在 kubectl 命令中添加 --as 和 --as-groups 参数来作为其他人运行命令。例如,如果你以一个随机用户的身份运行 kubectl get nodes,它将失败:
kubectl get nodes --as somerandomuser
Error from server (Forbidden): nodes is forbidden: User "somerandomuser" cannot list resource "nodes" in API group "" at the cluster scope
然而,如果我们添加我们的管理员组:
kubectl get nodes --as somerandomuser --as-group=cn=k8s-cluster-admins,ou=Groups,DC=domain,DC=com
NAME STATUS ROLES AGE VERSION
cluster01-control-plane Ready control-plane 17m v1.27.3
cluster01-worker Ready <none> 17m v1.27.3
你可以看到它工作了。这是因为 API 服务器将我们的用户视为属于我们为其创建 RBAC 绑定的组 cn=k8s-cluster-admins,ou=Groups,DC=domain,DC=com。实际上,如果我们运行 kubectl auth whoami 并带上这些参数,我们将看到 API 服务器是如何看待我们的:
kubectl auth whoami --as=someuser --as-group=cn=k8s-cluster-admins,ou=Groups,DC=domain,DC=com
ATTRIBUTE VALUE
Username someuser
Groups [cn=k8s-cluster-admins,ou=Groups,DC=domain,DC=com system:authenticated]
Extra: originaluser.jetstack.io-groups [cn=group2,ou=Groups,DC=domain,DC=com cn=k8s
cluster-admins,ou=Groups,DC=domain,DC=com]
Extra: originaluser.jetstack.io-user [mmosley]
在上述示例中,API 服务器看到请求来自 someuser,并根据 kubectl 发送的模拟头部,将其与适当的组进行匹配。
额外的 Extra 属性存在是因为在我们从 kubectl -> kube-oidc-proxy 执行模拟请求时,kube-oidc-proxy 会进行一次独立的模拟操作,并使用新的头部,添加 extra-info 头部以包含在内,从而让审计日志显示出发起请求的原始用户是谁。在将请求转发到 API 服务器之前,kube-oidc-proxy 会先执行 SubjectAccessReview,确保用户 mmosley 和其组有权模拟 someuser 及该组。
我们能够快速配置使用 OpenUnison 的模拟操作,其中大部分实现细节对你是隐藏的。如果你想在没有 OpenUnison 的情况下配置一个模拟代理怎么办?
在没有 OpenUnison 的情况下配置模拟
OpenUnison 自动化了几个关键步骤来使模拟操作生效。你可以使用任何能生成正确头信息的反向代理。自己进行配置时,有三个关键项目需要理解:RBAC、默认组和入站模拟。
模拟 RBAC 策略
RBAC 将在下一章中讲解,但现在,授权服务账户进行模拟的正确策略如下:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: impersonator
rules:
- apiGroups:
- ""
resources:
- users
- groups
verbs:
- impersonate
为了限制可以被模拟的账户,可以在规则中添加 resourceNames。例如,如果你只想允许模拟用户 mmosley:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: impersonator
rules:
- apiGroups:
- ""
resources:
- users resourceNames:
- mmosley
verbs:
- impersonate
上述第一个 ClusterRole 告诉 Kubernetes,成员可以模拟所有用户和组(如果指定了 resourceNames,也可以是特定用户或组)。一定要小心哪些账户被授予此 ClusterRole,因为它使你基本上成为了一个 cluster-admin,因为你可以模拟 system:masters 组,绕过 RBAC,允许任何获得此角色授权的人成为全球管理员,进而以任何方式破坏你的集群。
在配置特定用户和组的模拟时,将 ClusterRole 拆分为多个单独的 ClusterRole,每个用户或组对应一个 ClusterRole。这样,你就不会有人以创建该用户的组名来模拟一个组,从而避免不必要的后果。
在配置了 RBAC 之后,下一步的要求是将默认组添加到模拟请求中。
默认组
当模拟用户时,Kubernetes 不会将默认组 system:authenticated 添加到模拟组的列表中。当使用一个反向代理,并且该代理不知道特别需要为这个组添加头部时,需要手动配置代理来添加该头部。否则,像调用 /api 端点这样的简单操作会失败,因为除了集群管理员外,其他任何人都没有授权。
本章的大部分内容集中在对将与 API 服务器交互的用户进行身份验证。Kubernetes 及其提供的 API 的一个主要优势是能够自动化你的系统。接下来,我们将探讨如何将我们迄今为止学到的知识应用于自动化系统的身份验证。
入站模拟
我们已经展示了如何使用带有 --as 和 --as-group 参数的 kubectl 命令来模拟用户以进行调试。如果你使用模拟来管理对集群的访问,模拟代理如何知道试图模拟其他用户的用户是否确实有权这么做?在 Kubernetes 中,你需要构建一个 ClusterRole 和 ClusterRoleBinding 来使特定用户可以模拟其他特定用户,但你的代理如何知道你有权限模拟某人呢?
在我们之前的示例中:
kubectl auth whoami --as=someuser --as-group=cn=k8s-cluster-admins,ou=Groups,DC=domain,DC=com
ATTRIBUTE VALUE
Username someuser
Groups [cn=k8s-cluster-admins,ou=Groups,DC=domain,DC=com system:authenticated]
Extra: originaluser.jetstack.io-groups [cn=group2,ou=Groups,DC=domain,DC=com cn=k8s
cluster-admins,ou=Groups,DC=domain,DC=com]
Extra: originaluser.jetstack.io-user [mmosley]
我们看到 mmosley 模拟了 someuser。Kubernetes 允许这种模拟,因为 mmosley 是 cn=k8s-cluster-admins,ou=Groups,DC=domain,DC=com 组的成员,而该组有一个绑定到 cluster-admin ClusterRole 的 ClusterRoleBinding。然而,这个请求是通过 kube-oidc-proxy 传递的,那么 kube-oidc-proxy 是如何知道集群会授权这个请求的呢?在每次包含模拟头部的请求发送到 kube-oidc-proxy 时,都会创建一个 SubjectAccessReview 来检查 mmosley 是否被允许模拟 someuser。如果检查失败,kube-oidc-proxy 会拒绝该请求。
你的模拟代理也需要做出相同的选择。这里有三种方法:
-
删除并忽略所有入站的模拟头部:你的代理将忽略并移除所有入站的模拟头部,使得
--as和--as-group标志失效。这可以限制访问,但也会限制功能。 -
保持自定义授权方案:在生成模拟头部之前,代理可以拥有自己的授权系统来确定哪些用户被允许模拟其他用户。这意味着需要维护一个额外的授权系统,这可能会导致配置错误,最终引发安全漏洞。
-
查询 Kubernetes 授权决策:这是 kube-oidc-proxy 和 Pinniped(VMware 的一个工具,扮演类似于 OpenUnison 的角色)用来确保入站模拟已被授权的方法。这是最好的方法,因为它使用与集群相同的规则来管理访问,简化了管理并降低了配置错误导致安全漏洞的可能性。
即使你已经授权了入站模拟,也很重要记录模拟已发生。kube-oidc-proxy 项目在两个地方执行此操作:
-
代理日志:每次入站模拟都会被记录到控制台(这些日志应该由日志聚合器捕获)
-
API 服务器审计日志:额外的信息头部会告诉 API 服务器原始用户是谁,这些信息会被包含在审计日志中。我们将在下一节看到如何设置和检查审计日志。
入站模拟是一个非常难以管理的过程。如果你希望允许这种操作,应该使用专门的模拟代理。否则,最好的做法是删除所有入站模拟头部,以避免账户被接管。
到目前为止,我们只讨论了整体的用户,没有加入任何上下文。许多企业要求与集群交互进行管理工作的用户拥有超出其常规账户的权限。接下来,我们将看看如何在 Kubernetes 中实施特权访问管理。
对集群的特权访问
除了管理身份验证外,大多数企业还要求有“特权访问管理”概念,不仅限制用户的访问,还限制时间。大多数企业要求某种变更控制过程,以确保对生产系统的变更受到跟踪和批准。这项要求通常来自大型企业中所需的各种合规性和监管框架。
在 Kubernetes 中,一般有三种方法来管理特权访问,我们将介绍这三种方法及其优缺点。
使用特权用户账户
企业通常要求管理员拥有两个账户,一个用于日常任务,另一个用于进行管理变更。这种做法通常通过特权访问管理器(PAM)来实现,当管理员被授权执行工作时,PAM 会为用户生成一个新密码。这种方法符合大多数框架的要求,因为需要通过某人批准来解锁管理员账户。一旦管理员完成工作,他们将账户归还到 PAM 中,从而将其锁定。或者,通常会设置账户可以被借用的时间限制,一旦时间到期,账户将由 PAM 自动锁定。
这种方法的主要好处是特权账户的管理是在 Kubernetes 之外进行的。这是其他人的责任,消除了集群所有者需要管理的部分,无论是通过之前提到的 PAM 还是其他引擎。值得注意的是,作为集群管理员,你仍然负责授权访问,因此本章中的相同建议仍然适用。
采用这种方法的另一个原因是防止针对管理员的钓鱼攻击。例如,如果你的集群与 Active Directory 集成,并允许桌面 SSO,恶意行为者可能会向你的管理员发送一封电子邮件,执行一个以管理员身份运行的命令,而不需要知道管理员的凭证!如果你至少要求密码,攻击者需要采取额外的步骤。
有人认为这种方法不是最有效或最安全的,但它通常是现有的做法。你会发现,沿用现有框架比重新发明它们要容易得多。
模拟特权用户
除了使用外部 PAM 通过密码解锁用户外,另一种方法是通过kubectl的--as命令行参数模拟特权用户。其思路是模拟 Unix 中的sudo命令,提升你的权限,以防止意外的管理操作。
这种方法更可能带来弊大于利。为了使其生效,你至少需要为每个用户创建一个 RBAC ClusterRole和ClusterRoleBinding,以维护单独的特权账户。如果你有 100 个管理员,那么在授权访问资源之前,你就需要创建 200 个额外的对象。除了创建这些对象外,当时机成熟时,你还需要删除它们。虽然自动化可以提供帮助,但对象的激增使得隐藏配置错误变得更容易。对象越少越好。
任何太复杂以至于难以追踪的安全性,更有可能会产生安全漏洞。在这种情况下,你可能会决定通过只创建一个用于模拟的ClusterRole和一个包含多个Subjects的ClusterRoleBinding,来减少创建的对象数量。这实际上并没有减少管理这种解决方案的复杂性,因为:
-
你仍然需要管理一个可能快速增长的 Subjects 列表。
-
你的特权用户现在看起来与 API 服务器具有相同的身份,从而失去了大量的粒度和价值。
值得注意的是,API 服务器确实跟踪并记录了最初请求者的信息,但现在它位于一个不同的字段中,你的系统需要查找该字段。
这种额外的工作几乎没有任何好处。如果没有某种额外的自动化工具,你无法有效地限制访问时间,仅仅要求在kubectl中添加命令行参数不太可能阻止某人通过按上箭头查找先前运行的命令(其中包含--as参数),即使他们并非故意这么做。
这种方法弊大于利。它不会提供任何有意义的安全性,反而会以更复杂的方式管理集群,可能会比修补漏洞更容易造成安全隐患。
临时授权特权
假设你根据组编写了 RBAC 策略,那么你真正需要做的就是临时将用户分配到特权组中,从而提升其权限。工作流程与使用特权账户类似,但你使用的是标准账户,而不是一个完全独立的账户。例如,在我们当前的集群中,假设mmosley不是 AD 组cn=k8s-cluster-admins,ou=Groups,DC=domain,DC=com的成员。外部工作流引擎将在需要批准的工作完成后将他们加入该组。完成后,mmosley执行其任务,并且在任务完成后,其在cn=k8s-cluster-admins,ou=Groups,DC=domain,DC=com中的成员资格将被撤销。
这样我们可以获得与特权账户相同的好处,而无需管理额外的账户。这种方法存在多个风险:
-
钓鱼攻击:如果你使用的是用于日常任务(如电子邮件)的标准账户,那么你的凭据被盗的风险会更高。
-
逗留过久:长期有效的凭证,如令牌或证书,可能会在政策要求访问已过期时仍授予访问权限。
为了应对这些风险,特权用户必须:
-
需要重新认证:确保管理员必须重新输入凭据有助于防范恶意脚本和可执行文件。
-
使用多因素认证:要求管理员提供第二个身份验证因素,最好是一个无法被钓鱼攻击的因素,将有助于防范大多数攻击。
-
使用短期令牌:如果你的令牌有效期为八小时,那么四小时的变更窗口还有什么意义?
通过这些额外的缓解措施,特权授权将为集群所有者减少最少的工作量,因为所有的工作都被外部化。只需按组授权即可!
尽管这样做提供了最佳的用户体验,但大多数大型企业可能已经拥有特权访问管理器,因此这种方法最有可能被采纳。
在了解了多种与我们集群交互的用户身份验证方法后,下一步是看看流水线和自动化如何进行身份验证。
从流水线进行身份验证
到目前为止,本章专注于用户如何进行 Kubernetes 认证。无论是运维人员还是开发人员,用户通常会与集群交互,更新对象、调试问题、查看日志等等。然而,这并没有涵盖所有的使用场景。大多数 Kubernetes 部署与管道配合使用,管道是将代码从源代码传递到二进制文件,再到容器,最终运行在集群中的过程。我们将在第十八章,提供多租户平台中详细讨论管道。现在,主要的问题是,“你的管道如何安全地与 Kubernetes 进行通信?”
如果你的管道运行在正在更新的同一个集群中,这个问题很容易回答。你可以通过 RBAC 授予管道的服务账户相应的权限来完成它需要做的事情。这就是服务账户存在的原因——为集群内的进程提供身份。
如果你的管道运行在集群外呢?Kubernetes 是一个 API,本章中呈现的所有选项都适用于管道,正如它们适用于用户一样。传统的服务账户令牌没有过期时间,容易被滥用。TokenRequest API 可以为你提供短期令牌,但你仍然需要身份验证才能获取它。如果你的集群和管道运行在同一云提供商上,你可能可以使用其集成的 IAM 系统。例如,你可以在 Amazon CodeBuild 中生成一个 IAM 角色,该角色可以与 EKS 集群进行通信,而无需静态的服务账户。对于 Azure DevOps 和 AKS 同样适用。
如果云提供商的 IAM 功能无法满足你的需求,有三种选择。第一种是动态生成一个令牌给管道,和为用户生成令牌的方式相同,通过身份提供者进行身份验证,然后使用返回的 id_token 来进行 API 调用。第二种是生成可以与 API 服务器一起使用的证书。最后,你可以利用模拟来认证管道的令牌。让我们来看看这三种选择,并了解我们的管道如何使用它们。
使用令牌
Kubernetes 不区分来自人类用户还是管道的 API 调用。鉴于本章中提到的可能丢失令牌的风险,短期令牌是与 API 服务器交互的一个不错的方式。大多数 Kubernetes 的客户端 SDK 知道如何刷新这些令牌。最大的问题是,如何获取一个管道可以使用的令牌?
大多数企业已经有某种类型的服务账户管理系统。在这里,“服务账户”是一个通用术语,指的是由某种服务使用的账户,而不是 Kubernetes 中的 ServiceAccount 对象。这些服务账户管理系统通常有自己的方式来处理任务,如凭证轮换和授权管理。它们还拥有自己的合规工具,使得通过安全审查流程变得更加容易!
假设你有一个企业服务帐户用于你的管道,如何将该凭据转换为令牌?我们基于我们在 OIDC 集成身份提供者中的凭据生成令牌;如果能在我们的管道中使用这个,那就太好了!通过 OpenUnison,这非常简单,因为为我们提供令牌的页面实际上只是 API 的前端。下一个问题是如何向 OpenUnison 进行身份验证。我们可以编写一些代码来模拟浏览器并逆向工程登录过程,但这看起来很丑陋。如果表单更改,我们的代码也会失效。最好是配置 API,使用更适合 API 的身份验证方式,比如 HTTP 基本身份验证。
OpenUnison 可以通过创建配置自定义资源来扩展。实际上,OpenUnison 的大部分配置都是通过这些自定义资源完成的。目前的令牌服务假设你正在使用默认的 OpenUnison 表单登录机制进行身份验证,而不是适合管道的基本身份验证。为了让 OpenUnison 支持 API 身份验证,我们需要告诉它:
-
通过定义认证机制,启用通过 HTTP 基本身份验证进行身份验证
-
创建一个认证链,使用基本身份验证机制完成认证过程
-
定义一个可以提供令牌 API 的应用程序,使用新创建的认证链进行身份验证
我们不会深入讲解如何在 OpenUnison 中实现这一点,而是专注于最终结果。chapter6文件夹包含了一个 Helm 图表,你可以使用它来配置这个 API。使用你部署 OpenUnison 时使用的相同openunison-values.yaml文件运行它:
cd chapter6/pipelines
helm install orchestra-token-api token-login -n openunison -f /tmp/openunison-values.yaml
NAME: orchestra-token-api
LAST DEPLOYED: Mon Jul 24 18:47:04 2023
NAMESPACE: openunison
STATUS: deployed
REVISION: 1
TEST SUITE: None
部署完成后,我们可以使用curl进行测试:
$ export KUBE_AZ=$(curl --insecure -u 'pipeline_svc_account:start123' \
https://k8sou.apps.192-168-2-114.nip.io/k8s-api-token/token/user\
| jq -r '.token.id_token')
curl --insecure -H "Authorization: Bearer $KUBE_AZ" https://k8sapi.apps.192-168-2-114.nip.io/api
{
"kind": "APIVersions",
"versions": [
"v1"
],
"serverAddressByClientCIDRs": [
{
"clientCIDR": "0.0.0.0/0",
"serverAddress": "172.18.0.2:6443"
}
]
}
如果你使用直接集成 OpenID Connect,请将k8sapi.apps.192-168-2-114.nip.io替换为0.0.0.0:6443,直接对 API 服务器运行curl命令。
现在,等待一两分钟,再次尝试curl命令,你会看到你不再被认证。这种示例对于运行单个命令很有用,但大多数管道会运行多个步骤,而且单个令牌的生命周期不足以支撑。我们可以编写代码来利用refresh_token,但大多数 SDK 会为我们处理这个问题。与其仅获取id_token,不如生成一个完整的kubectl配置:
$ export KUBECONFIG=$(mktemp)
$ kubectl get nodes
The connection to the server localhost:8080 was refused – did you specify the right host or port?
curl --insecure -u 'pipeline_svc_account:start123' https://k8sou.apps.192-168-2-114.nip.io/k8s-api-token/token/user 2>/dev/null | jq -r '.token["kubectl Command"]' | bash
Cluster "impersonation" set.
Context "impersonation" created.
User "pipelinex-95-xsvcx-95-xaccount@impersonation" set.
Switched to context "impersonation".
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
cluster01-control-plane Ready control-plane 130m v1.27.3
cluster01-worker Ready <none> 129m v1.27.3
我们正在安全地获取一个短期令牌,同时使用我们的标准工具与 API 服务器进行交互!这个解决方案仅在你的服务帐户存储并通过 LDAP 目录访问时有效。如果不是这种情况,你可以扩展 OpenUnison 的配置来支持任意数量的配置选项。欲了解更多信息,请访问 OpenUnison 的文档:openunison.github.io/。
这个解决方案是针对 OpenUnison 的,因为目前没有标准将用户凭证转换为 id_token。这是由每个身份提供者处理的细节。你的身份提供者可能有一个 API,可以轻松生成 id_token,但更可能的是你需要一些工具来充当代理,因为身份提供者不知道如何生成完整的 kubectl 配置。
使用证书
前述过程工作良好,但需要 OpenUnison 或类似工具。如果你希望采用中立供应商的方式,可以使用证书作为凭据,而不是尝试生成令牌。在本章早些时候,我提到过,证书认证不应被用户使用,因为 Kubernetes 缺乏撤销支持,并且大多数证书的部署并不正确。这两个问题通常通过管道容易缓解,因为部署可以自动化。
如果你的企业要求你使用一个中央存储库来管理服务账户,这种方法可能行不通。这个方法的另一个潜在问题是,你可能希望使用企业 CA 来为服务账户生成证书,但 Kubernetes 不知道如何信任第三方 CA。目前关于启用该功能的讨论还在进行中,但尚未实现。
最后,你不能为许多托管集群生成证书。大多数托管的 Kubernetes 发行版,如 EKS,不会将用于通过内置 API 签名请求的私钥提供给集群直接使用。在这种情况下,你将无法生成集群接受的证书。
话虽如此,让我们一起走过这个过程:
-
首先,我们将生成一个密钥对和 证书签名请求 (CSR):
$ openssl req -out sa_cert.csr \ -new -newkey rsa:2048 -nodes -keyout sa_cert.key \ -subj '/O=k8s/O=sa-cluster-admins/CN=sa-cert/' Generating a RSA private key ..........+++++ .................................+++++ writing new private key to 'sa_cert.key' ----- -
接下来,我们将把 CSR 提交给 Kubernetes:
$ cat <<EOF | kubectl apply -f - apiVersion: certificates.k8s.io/v1 kind: CertificateSigningRequest metadata: name: sa-cert spec: request: $(cat sa_cert.csr | base64 | tr -d '\n') signerName: kubernetes.io/kube-apiserver-client usages: - digital signature - key encipherment - client auth EOF -
一旦 CSR 提交给 Kubernetes,我们需要批准该提交:
$ kubectl certificate approve sa-cert certificatesigningrequest.certificates.k8s.io/sa-cert approved -
审核通过后,我们将下载生成的证书并保存为
pem文件:$ kubectl get csr sa-cert -o jsonpath='{.status.certificate}' | base64 --decode > sa_cert.crt -
接下来,我们将配置
kubectl来使用我们新批准的证书:$ cp ~/.kube/config ./sa-config $export KUBECONFIG=./sa-config $ kubectl config set-credentials kind-cluster01 --client-key=./sa_cert.key \ --client-certificate=./sa_cert.crt $ kubectl get nodes Error from server (Forbidden): nodes is forbidden: User "sa-cert" cannot list resource "nodes" in API group "" at the cluster scope
API 服务器已接受我们的证书,但尚未授权它。我们的 CSR 中的 o 是名为 sa-cluster-admins 的主题,Kubernetes 将其翻译为“用户 sa-cert 属于 sa-cluster-admins 组”。我们接下来需要授权该组为集群管理员:
$ export KUBECONFIG=
$ kubectl create -f chapter6/pipelines/sa-cluster-admins.yaml
$ export KUBECONFIG=./sa-config
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
cluster01-control-plane Ready control-plane 138m v1.27.3
cluster01-worker Ready <none> 138m v1.27.3
现在你有了一对密钥,可以在你的管道中使用与集群进行交互!在自动化这个过程中要小心。提交给 API 服务器的 CSR 可以设置任何它想要的组,包括 system:masters。如果生成的证书在主题中包含 system:masters 作为 o,它不仅可以在你的集群中执行任何操作,还会绕过所有的 RBAC 授权。事实上,它将绕过所有授权!
如果你打算走证书的路线,可以考虑一些替代方案,例如使用身份提供者的证书,而不是直接连接到 API 服务器。这与我们基于令牌的身份验证类似,但不同的是,它不使用 HTTP 基本身份验证中的用户名和密码,而是使用证书。这为你提供了一个强大的凭证,可以由企业证书颁发机构颁发,同时避免了使用密码。
接下来,我们将探索如何使用管道自身的身份进行身份验证。
使用管道的身份
在过去的一两年里,关于提高供应链安全性的讨论已经成为 Kubernetes 和安全专业人员的核心话题。这一讨论的一部分促使更多的管道系统为工作流提供唯一的身份,这些身份可以用于与远程系统(如 Kubernetes 集群)进行交互。这提供了最佳的解决方案,因为每个工作流都是独特的,并且它可以拥有一个短期的令牌,不需要 Kubernetes 集群与工作流之间共享密钥。
使用工作流身份与 Kubernetes 集群的挑战在于,一个集群只能接受一个 OpenID Connect 发行者,而且托管集群甚至无法做到这一点。前面我们探讨了如何通过模拟身份验证 API 请求来访问集群,而无需直接在 API 服务器标志中启用 OpenID Connect。事实证明,这种方法在 CI/CD 管道中也同样适用。你可以将模拟代理配置为信任为你的工作流颁发令牌的身份提供者,而不是信任为用户颁发令牌的身份提供者。
我们将通过 CI/CD Proxy 来演示这个过程(cicd-proxy.github.io)。这是 Tremolo Security 围绕 kube-oidc-proxy 项目构建的一组 Helm 图表,旨在简化与管道的集成。kube-oidc-proxy 是由 JetStack 创建的,但该项目的开发在 2021 年初结束。Tremolo Security 对该项目进行了分叉,添加了几个功能,并根据需要更新其依赖关系和修复了 bug。如果你之前在本章中进行过模拟身份验证的实验,你已经运行了 Tremolo 的 kube-oidc-proxy。OpenUnison Helm 图表会为你自动集成它。
我们将模拟一个工作流,使用 CI/CD 代理删除集群中的一些 pods。虽然我们将在本书的后面部分使用 GitLab,但为了展示管道如何安全地进行身份验证,这个部署非常复杂。为了模拟我们的工作流,我们将运行一个简单的Job,通过TokenRequest API 挂载一个令牌,且令牌的受众是我们的 CI/CD 代理,而不是 API 服务器。然后,我们的 CI/CD 代理会模拟投影令牌中的sub声明中的 ServiceAccount,该账户被允许删除我们命名空间中的 pods。CI/CD 代理将被配置为信任我们集群的 OIDC 发现 URL,从而完成信任的循环。让我们来看看这些信任是如何汇聚的。

图 6.10:工作流认证顺序
上面的图示展示了以下事件顺序:
-
当 CI/CD 代理启动时,它会访问集群的 OIDC 发现文档,以拉取正确的密钥来验证传入的令牌。由于我们信任自己集群的令牌,我们使用
kubernetes.default.svc.cluster.local/作为发行者,因此我们将拉取kubernetes.default.svc.cluster.local/.well-known/openid-configuration。 -
当我们的工作流
Job启动时,它会被投影一个令牌,该令牌的受众是我们的 CI/CD 代理。这是与每个pod默认提供的ServiceAccount令牌一起使用的。如果我们检查这个令牌,我们会看到它与标准令牌的不同之处。![]()
图 6.11:令牌比较
上图是一个典型的ServiceAccount令牌(左侧)与一个用于 CI/CD 代理的令牌(右侧)的并排对比。两者都绑定到特定的pod,但左侧的令牌用于 API 服务器,而右侧的令牌则用于我们的代理。即使它们由同一组密钥签名,并且具有相同的发行者,它们也不能互换使用。如果你尝试使用右侧的令牌访问 API 服务器,它会因为受众无效而被拒绝。如果你尝试使用左侧的令牌访问我们的代理,也是如此。这两个令牌的另一个主要区别是,右侧的令牌在创建后的 10 分钟内过期。这意味着如果攻击者获得了此令牌,他们只能使用它 10 分钟,从而增加了令牌的安全性。
-
一旦我们的
Job使用kubectl调用我们的代理,而不是直接调用 API 服务器,代理会检查令牌以确保它被正确签名并构建。然后,代理将请求转发到 API 服务器,但使用的是它自己的令牌,并且添加了模拟头信息。 -
最后,API 服务器会像处理由我们的
Job发出的请求一样,处理该请求。
在整个操作过程中,不需要分发或轮换共享密钥。几乎没有任何可以被泄露的内容。由于 OIDC 发现文档由我们的身份提供商控制,如果密钥需要轮换,我们的代理会自动获取更新。理论讲解完毕,接下来让我们部署示例。
首先,从一个全新的集群开始:
cd Kubernetes-An-Enterprise-Guide-Third-Edition/chapter2
kind delete cluster -n cluster01
./create-cluster.sh
一旦集群创建完成,接下来部署 CI/CD 代理。我们不想陷入具体的步骤,因此我们自动化了部署过程:
cd ../chapter6/pipelines/cicd-proxy
./deploy-proxy.sh
这个脚本运行大约需要一两分钟。它做了几件事:
-
从 JetStack 部署 cert-manager 项目并创建一个内部 CA,用于签发证书
-
启用匿名访问 API 服务器的 OIDC 发现文档
-
使用 Tremolo Security 的 Helm 图表部署 CI/CD 代理
-
创建一个目标命名空间和
Deployment,我们可以用来测试删除 Pods 的操作 -
为我们模拟的用户创建一个 RBAC 绑定,使其能够列出并删除目标命名空间中的 Pods
一旦一切部署完成,下一步是创建我们的 Job 并检查日志:
./run_workflow.sh
kubectl logs -l job-name=workflow -n cicd-ns
User "remote" set.
Context "remote" created.
Switched to context "remote".
pod "test-pods-777b69dc55-4bmwd" deleted
我们可以看到,我们成功地使用投影令牌删除了我们的 Pods!
这看起来似乎需要做很多工作来删除一个 Pod。或许直接创建一个 ServiceAccount 令牌并将其存储在我们的工作流能够访问的地方会更简单。但那样会是一种安全和 Kubernetes 反模式。意味着只要 Pod 存在于集群的 etcd 数据库中,就可以不受限制地使用它。你可以创建一个轮换系统,但就像自定义认证一样,你现在是在模仿 OpenID Connect 已有的安全性。同时,你也在构建额外的自动化,这些自动化也需要被保护。因此,看起来需要额外工作,但实际上会为你节省时间,并让你的安全团队更加满意!
在讨论如何从管道正确地认证到集群之后,接下来我们来探讨管道认证中的一些反模式。
避免反模式
事实证明,适用于用户认证的大部分反模式同样适用于管道认证。鉴于认证代码的性质,有一些特定的事项需要留意。
首先,不要使用个人账户进行管道操作。这很可能违反你所在企业的政策,并且可能会暴露你的账户,甚至可能对你的雇主造成问题。你的企业账户(分配给企业中其他所有人)通常附带一些规则。仅仅在代码中使用它可能会违反这些规则。我们接下来讨论的其他反模式也会增加风险。
接下来,绝不要将你的服务账户凭证放入 Git 中,即使是加密过的。将凭证直接包含在存储在 Git 中的对象中是很常见的做法,因为你现在有了变更控制,但这样做很容易不小心将 Git 仓库推送到公共空间。安全性在很大程度上是关于保护用户免受可能泄露敏感信息的意外情况。即使 Git 中的凭证经过加密,如果加密密钥也存储在 Git 中,也可能被滥用。每个云服务提供商都有一个秘密管理系统,将你的凭证同步到 Kubernetes Secret 对象中。你也可以使用 Vault,稍后在本书中我们将介绍。这个方法要好得多,因为这些工具专门设计用于管理敏感数据。Git 的目的是方便共享和协作,这使得它不适合用于秘密管理。
最后,不要使用集群外的旧版服务账户令牌。我知道我在这一章中说了十几次,但这非常重要。当使用持票令牌时,任何携带该令牌的东西都是潜在的攻击面。例如,有些网络提供商会泄漏令牌。这是一种常见的反模式。如果供应商告诉你生成服务账户令牌,请反对——你正在将企业的数据置于风险之中。
摘要
本章详细介绍了 Kubernetes 如何识别用户及其所属的群组。我们详细说明了 API 服务器如何与身份交互,并探讨了几种认证选项。最后,我们详细介绍了 OpenID Connect 协议以及它如何应用于 Kubernetes。
学习 Kubernetes 如何认证用户以及 OpenID Connect 协议的细节是构建集群安全的重要部分。理解这些细节及其如何应用于常见企业需求,将帮助你决定最适合的认证方式,同时也为我们探讨的反模式提供了避免的正当理由。
在下一章中,我们将应用认证过程来授权访问 Kubernetes 资源。仅仅知道某人是谁不足以保护你的集群。你还需要控制他们可以访问的内容。
问题
-
OpenID Connect 是一个经过广泛同行评审和使用的标准协议。
-
真实的
-
错误的
-
-
Kubernetes 使用哪个令牌来授权你访问 API?
-
access_token -
id_token -
refresh_token -
certificate_token
-
-
在什么情况下,证书认证是一个好主意?
-
管理员和开发人员的日常使用
-
来自外部 CI/CD 流水线和其他服务的访问
-
在紧急情况下使用“破玻璃”机制,当所有其他认证解决方案不可用时
-
-
你应该如何识别访问集群的用户?
-
电子邮件地址
-
Unix 登录 ID
-
Windows 登录 ID
-
一个不基于用户姓名的不可变 ID
-
-
在 Kubernetes 中,OpenID Connect 配置选项设置在哪里?
-
取决于发行版
-
在 ConfigMap 对象中
-
在一个秘密中
-
设置为 Kubernetes API 服务器可执行文件的标志
-
-
使用集群时,用户带入的群组是唯一需要的群组。
-
正确
-
错误
-
-
仪表盘应具有自己的特权身份以正常工作。
-
正确
-
错误
-
答案
-
a: 正确
-
b:
id_token -
c: 在紧急情况下打破玻璃,当其他所有身份验证解决方案不可用时
-
d: 一个不基于用户姓名的不可变 ID
-
d: 设置为 Kubernetes API 服务器可执行文件的标志
-
b: 错误
-
b: 错误
第七章:RBAC 策略与审计
身份验证仅是管理集群访问的第一步。一旦授予对集群的访问权限,就需要限制账户的操作权限,具体取决于账户是为自动化系统还是用户所使用。授权访问资源是防止意外问题和恶意行为者滥用集群的重要部分。
本章将详细介绍 Kubernetes 如何通过其 基于角色的访问控制 (RBAC) 模型授权访问。本章的第一部分将深入探讨 Kubernetes RBAC 如何配置、可用的选项以及如何将理论应用于实际示例。调试和故障排除 RBAC 策略将是第二部分的重点。
本章将涵盖以下主题:
-
RBAC 介绍
-
将企业身份映射到 Kubernetes 以授权访问资源
-
实现命名空间多租户
-
Kubernetes 审计
-
使用
audit2rbac来调试策略
完成本章后,你将掌握通过 Kubernetes 的集成 RBAC 模型管理集群访问权限并调试出现问题时的工具。接下来,让我们深入探讨本章的技术要求。
技术要求
本章具有以下技术要求:
-
运行 Docker 的 Ubuntu 22.04+ 服务器,至少需要 4 GB 的 RAM,建议使用 8 GB。
-
来自仓库中
chapter7文件夹的脚本,您可以通过访问本书的 GitHub 仓库来获取:github.com/PacktPublishing/Kubernetes-An-Enterprise-Guide-Third-Edition
RBAC 介绍
RBAC 代表 基于角色的访问控制。它的核心思想是构建权限集,这些权限集被称为 Role,以及这些权限适用的主体(用户)列表。在本章中,我们将通过构建角色及其相应的绑定,来构建集群中的权限。
什么是 Role?
在 Kubernetes 中,Role 是一种将权限绑定到标准化的特定架构对象中的方式。通过将 Role 规范化为这种架构,你可以标准化并自动化它们的创建和管理。
角色包含规则,规则是资源和动词的集合。倒推过来,我们有以下内容:
-
动词:可以在 API 上执行的操作,例如读取(
get)、写入(create、update、patch和delete),或者列出和监视。 -
资源:要将动词应用于的 API 名称,例如
services、endpoints等。也可以列出特定的子资源,如logs和status。可以命名特定的资源,以便为对象提供非常具体的权限。
角色并不指定谁可以对资源执行动词——这一点由 RoleBindings 和 ClusterRoleBindings 处理。我们将在 RoleBindings 和 ClusterRoleBindings 部分了解更多内容。
“角色”一词可以有多种含义,RBAC 在其他上下文中也经常使用。在企业界,“角色”一词通常与业务角色相关联,并用来表示该角色的授权,而非某个特定人员。

图 7.1:RBAC 与基于授权的访问控制
例如,在 图 7.1 中,一个用户是应付账款角色的成员。作为该角色的成员,自动获得“开支票”的授权。如果该用户的工作变为应收账款,他们的权限会自动发生变化,因为权限与用户的角色相关联。在企业 RBAC 模型中,将用户与“角色”绑定的通常是某种上下文,而不是特定的组成员身份或属性值。例如,用户可能根据他们的“角色”位于公司目录的不同位置。
这与 Kubernetes 使用“角色”一词表示权限列表的方式不同,这些权限并非因为业务角色而绑定在一起,而是基于技术需求。正如我们在学习绑定时将看到的,Kubernetes 角色与账户和组紧密绑定,虽然 Role 的权限是为了特定功能而分组的,但该功能的定义低于“企业”角色的技术层次。
现在我们已经区分了“企业”角色的定义与 Kubernetes 中 Role 的定义,让我们深入了解如何构建 Role。
角色所基于的每个资源通过以下方式标识:
-
apiGroups:资源所属的组列表 -
resources:资源的对象类型名称(以及可能的子资源) -
resourceNames:应用此规则的特定对象的可选列表
每个规则 必须 包含 apiGroups 和 resources 列表。resourceNames 是可选的。
一旦在规则中识别出资源,就可以指定动词。动词是对资源可以执行的操作,它提供对 Kubernetes 中对象的访问。
如果对对象的期望访问是 all,则无需添加每个动词;相反,可以使用通配符字符来标识所有的 verbs、resources 或 apiGroups。
识别角色
Kubernetes 授权页面(kubernetes.io/docs/reference/access-authn-authz/rbac/)使用以下 Role 作为示例,允许某人获取 Pod 的详细信息及其日志:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: default
name: pod-and-pod-logs-reader
rules:
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list"]
在定义Role管理什么之前,需要注意的是,Role对象是命名空间的,因此Role创建所在的namespace意味着它定义的权限仅适用于该命名空间。在这个示例中,Role仅适用于default namespace。
反向推理如何定义此Role,我们将从resources开始,因为这是最容易找到的部分。Kubernetes 中的所有对象都是通过 URL 表示的。如果你想获取default namespace中所有 Pod 的信息,你可以调用/api/v1/namespaces/default/pods URL;如果你想获取某个特定 Pod 的日志,你可以调用/api/v1/namespaces/default/pods/mypod/log URL。
该 URL 模式适用于所有命名空间范围的对象。pods 与resources对齐,pods/log也一样。当试图识别你想要授权的资源时,可以使用 Kubernetes API 文档中的api-reference文档,网址为kubernetes.io/docs/reference/#api-reference。
如果你尝试访问对象名称之后的额外路径组件(如 Pod 上的status和logs),则需要明确授权。授权Pods并不会立即授权logs或status。
基于 URL 映射到resources,你接下来的想法可能是verbs字段将是 HTTP 动词。但事实并非如此,Kubernetes 中没有GET动词。动词是由 API 服务器中的对象模式定义的。好消息是,HTTP 动词和 RBAC 动词之间存在静态映射关系(kubernetes.io/docs/reference/access-authn-authz/authorization/#determine-the-request-verb)。查看这个 URL,你会注意到除了 HTTP 动词外,还有用于模拟的动词。这是因为 RBAC 模型不仅用于授权特定的 API,还用于授权谁可以模拟用户。本章的重点将是标准 HTTP 动词的映射。
最后需要识别的组件是apiGroups。API 将属于一个 API 组,而该组将是其 URL 的一部分。你可以通过查看你要授权的对象的 API 文档或使用kubectl api-resources命令来找到该组。例如,要获取Ingress对象的apiGroups,你可以运行:
kubectl api-resources -o wide | grep Ingress
ingressclasses networking.k8s.io/v1 false IngressClass [create delete deletecollection get list patch update watch]
ingresses ing networking.k8s.io/v1 true Ingress [create delete deletecollection get list patch update watch]
第二个结果展示了你在Ingress对象的 YAML 版本中的apiVersion。可以将其用于apiGroups,但不包括版本号。要将Role应用于Ingress对象,apiGroups应该是networking.k8s.io。
RBAC 模型中的不一致性可能使调试变得困难,至少可以说是这样。本章的最后一个实验将带你逐步走过调试过程,并大大减少定义规则时的猜测。
现在我们已经定义了角色的内容以及如何定义特定权限,重要的是要注意,角色可以在命名空间和集群级别都进行应用。
角色与集群角色
RBAC 规则可以作用于特定的命名空间,或者作用于整个集群。以之前的示例为例,如果我们将其定义为 ClusterRole 而不是 Role,并移除命名空间,那么我们将拥有一个 Role,它授权某人查看整个集群中所有 pod 的详细信息和日志。这个新的 Role 还可以在各个命名空间中使用,将权限分配给特定命名空间中的 pod:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: cluster-pod-and-pod-logs-reader
rules:
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list"]
是否将该权限应用于整个集群,还是仅在特定命名空间的范围内应用,取决于它与所适用的主体的绑定方式。这将在 RoleBindings 和 ClusterRoleBindings 部分进行介绍。
除了在集群中应用一组规则外,ClusterRoles 还用于将规则应用于那些未映射到命名空间的资源,例如 PersistentVolume 和 StorageClass 对象。
在了解了如何定义 Role 之后,接下来让我们探讨为特定目的设计 Role 的不同方式。在接下来的部分中,我们将看一下定义 Role 的不同模式及其在集群中的应用。
负面角色
最常见的授权请求之一是“我可以写一个角色,允许我做所有事情,除了 xyz”。在 RBAC 中,答案是不行。RBAC 要求要么允许每个资源,要么列举出特定资源和动词。在 RBAC 中有两个原因:
-
通过简化实现更好的安全性:能够强制执行一个规则,表示每个秘密都允许,除了这个,需要一个比 RBAC 提供的更复杂的评估引擎。引擎越复杂,测试和验证就越难,且更容易出错。一个更简单的引擎在编写和保持安全性方面要简单得多。
-
意外后果:允许某人做所有事情除了 xyz,会在集群增长并添加新功能时,以意想不到的方式打开出现问题的大门。
首先,具有这种能力的引擎既难以构建也难以维护。同时,它也使得规则的追踪变得更加困难。为了表达这种类型的规则,你不仅需要有授权规则,还需要对这些规则进行排序。例如,要表达我想允许所有内容,除了这个秘密,你首先需要一个规则来表示允许一切,然后再有一个规则表示拒绝这个秘密。如果你将规则顺序调换,变成拒绝这个秘密,再允许一切,那么第一个规则就会被覆盖。你可以为不同的规则分配优先级,但这会让它变得更加复杂。
有几种方法可以实现这种模式,要么通过使用自定义授权 webhook,要么通过使用控制器动态生成 RBAC Role 对象。这两种方法都应被视为安全反模式,因此在本章中不会进行讲解。
第二点涉及到意外后果。支持使用操作员模式支持不是 Kubernetes 的基础设施的配置正变得越来越普遍,其中自定义控制器寻找新的 CustomResourceDefinition (CRD) 的实例来配置基础设施,例如数据库。
亚马逊网络服务为此目的发布了一个操作员(github.com/aws/aws-controllers-k8s)。这些操作员在其自己的命名空间中以其云的管理凭据运行,以查找其对象的新实例以供资源配给。如果您的安全模型允许“除了…”之外的所有内容,那么一旦部署,您集群中的任何人都可以配置具有实际成本并可能创建安全漏洞的云资源。从安全角度来看,列举您的资源是了解正在运行的内容及其访问权限的重要部分。
Kubernetes 集群的趋势是通过自定义资源 API 在集群外部提供对基础设施的更多控制。您可以为任何类型的 API 驱动的云基础设施提供从虚拟机到额外节点的任何内容的资源配给。除了 RBAC 之外,还有其他工具可以用来减轻某人可能创建不应有的资源的风险,但这些工具应作为辅助措施。
到目前为止,我们已经看到如何为特定用例创建权限。如果您需要某些灵活性,以能够动态定义权限而不是我们现在提供的静态列表,那么接下来我们将发现如何使用聚合的 ClusterRoles 提供权限列表的动态方法。
聚合的 ClusterRoles
ClusterRoles 很快就会变得混乱并且难以维护。最好将它们拆分成更小的 ClusterRoles,以便根据需要进行组合。以 admin ClusterRole 为例,旨在允许某人在特定命名空间内执行几乎任何操作。当我们查看 admin ClusterRole 时,它列举了几乎所有的资源。你可能会认为有人编写了这个 ClusterRole,以便它包含所有这些资源,但那样做效率非常低,而且当新的资源类型添加到 Kubernetes 时会发生什么呢?admin ClusterRole 是一个聚合的 ClusterRole。看一看 ClusterRole:
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: admin
labels:
kubernetes.io/bootstrapping: rbac-defaults
annotations:
rbac.authorization.kubernetes.io/autoupdate: 'true'
rules:
.
.
.
aggregationRule:
clusterRoleSelectors:
- matchLabels:
rbac.authorization.k8s.io/aggregate-to-admin: 'true'
关键在于 aggregationRule 部分。此部分告诉 Kubernetes 将所有带有 rbac.authorization.k8s.io/aggregate-to-admin 标签为 true 的 ClusterRoles 的规则结合起来。当创建新的 CRD 时,管理员无法创建该 CRD 的实例,除非添加包含此标签的新的 ClusterRole。为允许命名空间管理员用户创建新的 myapi/superwidget 对象的实例,请创建一个新的 ClusterRole:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: aggregate-superwidget-admin
labels:
# Add these permissions to the "admin" default role.
rbac.authorization.k8s.io/aggregate-to-admin: "true"
rules:
- apiGroups: ["myapi"]
resources: ["superwidgets"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
下次查看 admin ClusterRole 时,它将包括 myapi/superwidgets。您还可以直接引用此 ClusterRole 获取更具体的权限。
到目前为止,我们一直专注于通过Roles和ClusterRoles创建权限列表。接下来,我们将研究如何将这些权限分配给用户和服务。
RoleBindings 和 ClusterRoleBindings
一旦定义了权限,就需要将其分配给某个对象才能启用。“某个对象”可以是用户、组或服务帐户。这些选项称为主体。与Roles和ClusterRoles一样,RoleBinding将Role或ClusterRole绑定到特定的命名空间,而ClusterRoleBinding则在整个集群中应用ClusterRole。一个绑定可以有多个主体,但只能引用一个Role或ClusterRole。为了将本章前面创建的pod-and-pod-logs-reader Role分配给默认命名空间中的服务帐户mysa、名为podreader的用户或任何属于podreaders组的人,可以创建一个RoleBinding:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: pod-and-pod-logs-reader
namespace: default
subjects:
- kind: ServiceAccount
name: mysa
namespace: default
apiGroup: rbac.authorization.k8s.io
- kind: User
name: podreader
- kind: Group
name: podreaders
roleRef:
kind: Role
name: pod-and-pod-logs-reader
apiGroup: rbac.authorization.k8s.io
前面的RoleBinding列出了三个不同的主体:
-
ServiceAccount:集群中的任何服务帐户都可以被授权到RoleBinding。必须包括命名空间,因为RoleBinding可以在任何命名空间中授权服务帐户,而不仅仅是定义RoleBinding的命名空间。 -
User:用户是通过认证过程确认的。请记住,在第六章,将认证集成到集群中中提到,Kubernetes 中没有表示用户的对象。 -
Group:与用户一样,组是作为认证过程的一部分进行确认的,也没有与之关联的对象。
最后,引用了我们之前创建的Role。以类似的方式,为了为相同的主体赋予跨集群读取 pod 及其日志的能力,可以创建一个ClusterRoleBinding来引用本章前面创建的cluster-pod-and-pod-logs-reader ClusterRole:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: cluster-pod-and-pod-logs-reader
subjects:
- kind: ServiceAccount
name: mysa
namespace: default
apiGroup: rbac.authorization.k8s.io
- kind: User
name: podreader
- kind: Group
name: podreaders
roleRef:
kind: ClusterRole
name: cluster-pod-and-pod-logs-reader
apiGroup: rbac.authorization.k8s.io
ClusterRoleBinding绑定到相同的主体,但绑定的是ClusterRole而不是命名空间绑定的Role。现在,这些用户可以读取所有命名空间中的所有 pod 详细信息和Pod/logs,而不仅仅是默认命名空间中的 pod 详细信息和Pod/logs。
到目前为止,我们的重点一直是在将Role与RoleBinding和ClusterRole与ClusterRoleBinding结合。如果你想定义作用于多个命名空间的相同权限,你需要一种方法来做到这一点,而不必每次都重复创建相同的Role。接下来,我们将介绍如何通过结合ClusterRoles和RoleBindings来简化Role管理。
合并 ClusterRoles 和 RoleBindings
我们有一个用例,日志聚合器希望从多个命名空间的 pod 中提取日志,但并非所有命名空间都需要。这时ClusterRoleBinding过于宽泛。虽然可以在每个命名空间中重新创建Role,但这种做法效率低下且维护困难。相反,定义一个ClusterRole,并在适用的命名空间中通过RoleBinding引用它。这样可以重用权限定义,同时将这些权限应用到特定命名空间。通常,注意以下几点:
-
ClusterRole+ClusterRoleBinding= 集群级别的权限 -
ClusterRole+RoleBinding= 命名空间特定权限
若要在特定命名空间中应用我们的ClusterRoleBinding,需要创建一个Role,引用ClusterRole而不是命名空间特定的Role对象:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: pod-and-pod-logs-reader
namespace: default
subjects:
- kind: ServiceAccount
name: mysa
namespace: default
apiGroup: rbac.authorization.k8s.io
- kind: User
name: podreader
- kind: Group
name: podreaders
roleRef:
kind: ClusterRole
name: cluster-pod-and-pod-logs-reader
apiGroup: rbac.authorization.k8s.io
上述的RoleBinding让我们可以重用现有的ClusterRole。这样减少了需要在集群中跟踪的对象数量,也便于在需要更改ClusterRole权限时更新集群中的权限。
在构建了我们的权限并定义了如何分配之后,接下来我们将看看如何将企业身份映射到集群策略中。
将企业身份映射到 Kubernetes 中以授权访问资源
集中化身份验证的一个好处是利用企业现有的身份,而无需创建用户与集群交互时需要记住的新凭证。了解如何将策略映射到这些集中化用户非常重要。在第六章,将身份验证集成到集群中中,你创建了一个集群,并将其与“企业Active Directory”进行了集成。为了完成集成,创建了以下ClusterRoleBinding:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: ou-cluster-admins
subjects:
- kind: Group
name: cn=k8s-cluster-admins,ou=Groups,DC=domain,DC=com
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.io
这个绑定允许所有属于cn=k8s-cluster-admins,ou=Groups,DC=domain,DC=com组的用户拥有完整的集群访问权限。当时,重点是身份验证,因此没有提供关于为什么创建这个绑定的很多细节。
如果我们想直接授权给我们的用户怎么办?那样,我们就能控制谁可以访问我们的集群。我们的 RBAC ClusterRoleBinding 将会有所不同:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: ou-cluster-admins
subjects:
- kind: User
name: https://k8sou.apps.192-168-2-131.nip.io/auth/idp/k8sIdp#mmosley
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.io
使用与之前相同的ClusterRole,这个ClusterRoleBinding将只会赋予我的测试用户cluster-admin权限。
第一个需要指出的问题是,用户的用户名前面有我们OpenID Connect发行者的 URL。当 OpenID Connect 最初被引入时,认为 Kubernetes 会与多个身份提供者以及不同类型的身份提供者集成,因此开发者希望你能够轻松地区分来自不同身份源的用户。例如,域 1 中的 mmosley 与域 2 中的 mmosley 是不同的用户。为了确保用户身份不会与跨身份提供者的其他用户冲突,Kubernetes 要求身份提供者的发行者 URL 被添加到用户名的前面。如果你在 API 服务器标志中定义的用户名声明是 mail,则此规则不适用。如果你使用的是证书或模拟身份,也不适用。
除了不一致的实现要求外,这种方法可能在几个方面引发问题:
-
更改身份提供者 URL:今天,你正在使用某个身份提供者的一个 URL,但明天你决定更换它。现在,你需要遍历每个
ClusterRoleBinding并更新它们。 -
审计:你无法查询与某个用户关联的所有
RoleBindings。你需要枚举每个绑定。 -
大规模绑定:根据用户数量的不同,绑定可能变得非常庞大,难以追踪。
尽管有工具可以帮助你管理这些问题,但将绑定与组关联,而不是与单个用户关联,会更容易一些。你可以使用 mail 属性来避免 URL 前缀,但这被视为一种反模式,如果因为任何原因更改了电子邮件地址,将导致集群出现同样复杂的问题。
到目前为止,我们已经学习了如何定义访问策略并将这些策略映射到企业用户。接下来,我们需要确定如何将集群划分为租户。
实现命名空间多租户
部署给多个利益相关者或租户的集群应该按命名空间进行划分。这是 Kubernetes 从一开始就设计的边界。在部署命名空间时,通常会为命名空间中的用户分配两个 ClusterRoles:
-
admin:这个聚合型ClusterRole提供对 Kubernetes 附带的几乎所有资源和操作的访问权限,使得admin用户成为其命名空间的管理者。唯一的例外是任何可能影响整个集群的命名空间范围对象,例如ResourceQuotas。 -
edit:与admin类似,但没有创建 RBACRoles或RoleBindings的权限。
需要注意的是,admin ClusterRole 本身无法对命名空间对象进行更改。命名空间是集群范围的资源,因此只能通过 ClusterRoleBinding 来分配权限。
根据你的多租户策略,admin ClusterRole 可能不合适。生成 RBAC Role 和 RoleBinding 对象的能力意味着命名空间管理员可能会授予自己更改资源配额的权限。这是 RBAC 往往崩溃的地方,并需要一些额外的选项:
-
不要授予 Kubernetes 访问权限:许多集群拥有者希望将 Kubernetes 排除在用户之外,并限制用户与外部 CI/CD 工具的交互。这对于微服务非常有效,但在多个线路上开始变得不太适用。首先,将更多传统应用迁移到 Kubernetes 意味着更多传统管理员需要直接访问其命名空间。其次,如果 Kubernetes 团队将用户排除在集群之外,那么他们就需要为此负责。拥有 Kubernetes 的人可能不希望成为应用程序所有者不满的原因,而且通常,应用程序所有者希望能够控制自己的基础设施,以确保他们能够处理任何影响其性能的情况。
-
将访问视为特权:大多数企业要求特权用户访问基础设施。这通常通过特权访问模型来实现,其中管理员拥有一个独立账户,该账户需要“签出”后才能使用,并且只有在某些时刻获得授权,通常是由“变更委员会”或流程批准。这些账户的使用会受到严格监控。如果你已经有一个现成的系统,特别是与企业的中央认证系统集成的系统,这种方法是非常好的。
-
为每个租户提供一个集群:此模型将多租户管理从集群层移至基础设施层。你并没有消除问题,只是将其处理的位置进行了更改。这可能导致管理上的扩展,变得难以控制,并且根据你实施 Kubernetes 的方式,成本可能会迅速飙升。在第九章,使用 vClusters 构建多租户集群中,我们将探讨如何为每个租户提供独立的集群,而不必过多担心扩展问题。
-
准入控制器:这些增强了 RBAC,通过限制可以创建的对象。例如,准入控制器可以决定阻止创建一个 RBAC 策略,即使 RBAC 明确允许创建该策略。这个话题将在第十一章,使用开放策略代理扩展安全性中进行讨论。
除了授权访问命名空间和资源外,多租户解决方案还需要知道如何为租户提供服务。这个话题将在最后几章中讨论——第十八章,为多租户平台提供服务,以及第十九章,构建开发者门户。
现在我们已经有了实施授权策略的策略,接下来我们需要一种方法来调试这些策略,并且要知道这些策略何时被违反。Kubernetes 提供了审计功能,接下来的部分将重点介绍此功能,我们将向 KinD 集群添加审计日志,并调试 RBAC 策略的实施。
Kubernetes 审计
Kubernetes 审计日志是你从 API 角度追踪集群活动的地方。它采用 JSON 格式,直接读取较为困难,但使用像 OpenSearch 这样的工具进行解析则更加容易。在 第十五章,管理集群与工作负载 中,我们将介绍如何使用 OpenSearch 堆栈创建一个完整的日志记录系统。
创建审计策略
策略文件用于控制记录哪些事件以及将日志存储在哪里,可以是标准日志文件或 webhook。我们在 GitHub 仓库的 chapter7 目录中包含了一个示例审计策略,接下来我们会将其应用于我们在全书中使用的 KinD 集群。
审计策略 是一组规则,告诉 API 服务器哪些 API 调用需要记录以及如何记录。当 Kubernetes 解析策略文件时,所有规则按顺序应用,并且只会应用第一个匹配的策略事件。如果你为某个事件设置了多个规则,可能不会在日志文件中收到预期的数据。因此,你需要小心确保事件创建正确。
策略使用 audit.k8s.io API 和 Policy 清单类型。以下示例展示了策略文件的开始部分:
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: Request
userGroups: ["system:nodes"]
verbs: ["update","patch"]
resources:
- group: "" # core
resources: ["nodes/status", "pods/status"]
omitStages:
- "RequestReceived"
虽然策略文件看起来像是一个标准的 Kubernetes 清单,但你不能使用 kubectl 应用它。策略文件是与 API 服务器使用 --audit-policy-file API 标志一起应用的。这将在 启用集群审计 部分中解释。
为了理解规则及其日志记录内容,我们将详细讲解每个部分。
规则的第一部分是 level,它决定了事件将记录哪些类型的信息。事件可以分配四个级别:

表 7.1:Kubernetes 审计级别
userGroups、verbs 和 resources 的值告诉 API 服务器哪个对象和操作会触发审计事件。在这个示例中,只有来自 system:nodes 的请求,并且尝试对 core API 上的 node/status 或 pod/status 执行 update 或 patch 操作时,才会创建事件。
omitStages 告诉 API 服务器跳过在某个阶段的日志记录事件,这有助于你限制日志记录的数据量。API 请求经过四个阶段:

表 7.2:审计阶段
在我们的示例中,我们设置了事件来忽略RequestReceived事件,这告诉 API 服务器不要记录任何传入 API 请求的数据。
每个组织都有自己的审计策略,策略文件可能变得又长又复杂。在你掌握可以创建哪些类型事件之前,不要害怕设置一个记录所有内容的策略。记录所有内容并不是一个好的实践,因为日志文件会变得非常大。即使将日志推送到外部系统,如 OpenSearch,在处理和管理方面依然会有成本。微调审计策略是一项需要随着时间积累经验才能掌握的技能,随着你对 API 服务器了解的深入,你将逐渐学会哪些事件最值得审计。
策略文件只是启用集群审计的起点,现在我们已经理解了策略文件,接下来让我们解释如何在集群上启用审计。
在集群上启用审计
启用审计是 Kubernetes 每个发行版特有的。在本节中,我们将启用 KinD 中的审计日志,以便了解底层步骤。快速回顾一下,上一章的最终成果是启用了模拟的 KinD 集群(而不是直接与 OpenID Connect 集成)。本章中其余的步骤和示例假设我们使用的是这个集群。从一个新的集群开始,并在 第六章 中使用模拟部署 OpenUnison:
cd Kubernetes-An-Enterprise-Guide-Third-Edition/chapter2
kind delete cluster -n cluster01
./create-cluster.sh
cd ../chapter6/user-auth
./deploy_openunison_imp_impersonation.sh
接下来,我们将配置 API 服务器将审计日志数据发送到文件中。这比设置一个开关更复杂,因为 KinD 所依赖的安装程序 kubeadm 将 API 服务器作为静态 Pod(s) 运行。API 服务器是 Kubernetes 内部的一个容器!这意味着,为了让我们告诉 API 服务器将日志数据写入哪里,我们首先必须有一个存储位置,然后配置 API 服务器的 Pod 将该位置用作卷。我们将手动演练这个过程,以便让你熟悉修改 API 服务器上下文的操作。
你可以手动按照本节中的步骤操作,或者可以执行 GitHub 仓库中 chapter7 目录下的包含脚本 enable-auditing.sh:
-
首先,将示例审计策略从
chapter7目录复制到 API 服务器:$ cd chapter7 $ docker exec -ti cluster01-control-plane mkdir /etc/kubernetes/audit $ docker cp cm/k8s-audit-policy.yaml cluster01-control-plane:/etc/kubernetes/audit/ -
接下来,在 API 服务器上创建存储审计日志和策略配置的目录。由于我们需要在下一步中修改 API 服务器文件,因此我们将
exec进入容器:$ docker exec -ti cluster01-control-plane mkdir /var/log/k8s
到目前为止,你已经在 API 服务器上配置了审计策略,并且可以启用 API 选项以使用该文件。
- 在 API 服务器上,编辑
kubeadm配置文件(你需要安装一个编辑器,如 vi,通过运行apt-get update; apt-get install vim),文件路径为/etc/kubernetes/manifests/kube-apiserver.yaml,这也是我们之前为了启用 OpenID Connect 而更新的文件。为了启用审计,我们需要添加三个值。
需要注意的是,许多 Kubernetes 集群可能只需要文件和 API 选项。由于我们使用 KinD 集群进行测试,所以我们需要第二步和第三步。
首先,为 API 服务器添加启用审计日志的命令行标志。除了策略文件外,我们还可以添加选项来控制日志文件的轮换、保留和最大大小:
- --tls-private-key-file=/etc/kubernetes/pki/apiserver.key
**-****--audit-log-path=/var/log/k8s/audit.log**
**-****--audit-log-maxage=1**
**-****--audit-log-maxbackup=10**
**-****--audit-log-maxsize=10**
**-****--audit-policy-file=/etc/kubernetes/audit/k8s-audit-policy.yaml**
请注意,选项指向的是您在上一步复制的策略文件。
-
接下来,将存储策略配置和生成日志的目录添加到
volumeMounts部分:- mountPath: /usr/share/ca-certificates name: usr-share-ca-certificates readOnly: true **-****mountPath:****/var/log/k8s** **name:****var-log-k8s** **readOnly:****false** **-****mountPath:****/etc/kubernetes/audit** **name:****etc-kubernetes-audit** **readOnly:****true** -
最后,将hostPath配置添加到
volumes部分,以便 Kubernetes 知道在哪里挂载本地路径:- hostPath: path: /usr/share/ca-certificates type: DirectoryOrCreate name: usr-share-ca-certificates **-****hostPath:** **path:****/var/log/k8s** **type:****DirectoryOrCreate** **name:****var-log-k8s** **-****hostPath:** **path:****/etc/kubernetes/audit** **type:****DirectoryOrCreate** **name:****etc-kubernetes-audit** -
保存并退出文件。
-
与所有 API 选项更改一样,您需要重启 API 服务器以使更改生效;然而,KinD 会检测到文件已更改,并自动重启 API 服务器的 Pod。
退出附加的 Shell 并检查kube-system命名空间中的 Pod:
$ kubectl get pod kube-apiserver-cluster01-control-plane -n kube-system
NAME READY STATUS RESTARTS AGE
kube-apiserver-cluster01-control-plane 1/1 Running 0 47s
API 服务器被突出显示,表明它仅运行了 47 秒,显示其成功重启。
验证 API 服务器正在运行后,让我们查看审计日志,以确认其正常工作。要检查日志,您可以使用docker exec命令来查看audit.log:
$ docker exec cluster01-control-plane tail /var/log/k8s/audit.log
该命令生成以下日志数据:
{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"Metadata","auditID":"451ddf5d-763c-4d7c-9d89-7afc6232e2dc","stage":"ResponseComplete","requestURI":"/apis/discovery.k8s.io/v1/namespaces/default/endpointslices/kubernetes","verb":"get","user":{"username":"system:apiserver","uid":"7e02462c-26d1-4349-92ec-edf46af2ab31","groups":["system:masters"]},"sourceIPs":["::1"],"userAgent":"kube-apiserver/v1.21.1 (linux/amd64) kubernetes/5e58841","objectRef":{"resource":"endpointslices","namespace":"default","name":"kubernetes","apiGroup":"discovery.k8s.io","apiVersion":"v1"},"responseStatus":{"metadata":{},"code":200},"requestReceivedTimestamp":"2021-07-12T08:53:55.345776Z","stageTimestamp":"2021-07-12T08:53:55.365609Z","annotations":{"authorization.k8s.io/decision":"allow","authorization.k8s.io/reason":""}}
这个 JSON 中有很多信息,直接查看日志文件很难找到特定事件。幸运的是,现在您已经启用了审计,可以将事件转发到中央日志服务器。我们将在第十五章,使用 Prometheus 监控集群和工作负载中做到这一点,在那里我们将部署EFK堆栈。
现在我们已经启用了审计,下一步是练习调试 RBAC 策略。
使用 audit2rbac 调试策略
有一个工具叫做audit2rbac,可以将审计日志中的错误逆向工程为 RBAC 策略对象。在本节中,我们将使用该工具生成一个 RBAC 策略,因为我们发现我们的一个用户无法执行他们需要做的操作。这是一个典型的 RBAC 调试过程,学习如何使用该工具可以节省您数小时的时间来隔离 RBAC 问题:
-
在前一章中,我们创建了一个通用的 RBAC 策略,允许
cn=k8s-cluster-admins,ou=Groups,DC=domain,DC=com组的所有成员成为我们集群的管理员。如果您已经登录到 OpenUnison,请退出登录。 -
现在,使用用户名
jjackson和密码start123重新登录。 -
接下来,点击登录。登录后,转到仪表板。就像 OpenUnison 首次部署时一样,由于集群管理员的 RBAC 策略不再适用,您将不会看到任何命名空间或其他信息。
-
接下来,从令牌屏幕中复制您的
kubectl配置,并确保将其粘贴到不是主 KinD 终端的窗口中,以免覆盖主配置。 -
一旦您的令牌设置完成,尝试创建一个名为
not-going-to-work的命名空间:PS C:\Users\mlb> kubectl create ns not-going-to-work Error from server (Forbidden): namespaces is forbidden: User "jjackson" cannot create resource "namespaces" in API group "" at the cluster scope
这里有足够的信息可以反向推断出一个 RBAC 策略。
-
为了消除此错误消息,请使用您的 KinD 管理员用户创建一个包含
"namespaces"资源、apiGroups设置为""、操作符为"create"的ClusterRole:apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: cluster-create-ns rules: - apiGroups: [""] resources: ["namespaces"] verbs: ["create"] -
接下来,为用户和此
ClusterRole创建一个ClusterRoleBinding:apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: cluster-create-ns subjects: - kind: User name: jjackson apiGroup: rbac.authorization.k8s.io roleRef: kind: ClusterRole name: cluster-create-ns apiGroup: rbac.authorization.k8s.io -
一旦
ClusterRole和ClusterRoleBinding创建完成,再次运行命令,它就会成功:PS C:\Users\mlb> kubectl create ns not-going-to-work namespace/not-going-to-work created
不幸的是,这可能不是大多数 RBAC 调试的常见方式。大多数情况下,调试 RBAC 不会这么清晰或简单。通常,调试 RBAC 意味着系统之间会出现意外的错误消息。例如,如果您正在部署用于监控的kube-prometheus项目,通常希望通过Service对象进行监控,而不是明确列出 Pod。为了实现这一点,Prometheus 的ServiceAccount需要能够列出您希望监控的服务所在命名空间中的Service对象。Prometheus 不会告诉您需要进行此操作;您只是看不到列出的服务。调试的更好方法是使用一个可以读取审计日志并基于日志中的失败反向推断出角色和绑定的工具。
audit2rbac工具是执行此操作的最佳方式。它会读取审计日志,并为您提供一组有效的策略。这些策略可能不是完全需要的策略,但它将提供一个很好的起点。让我们试试看:
-
首先,连接到您集群的
control-plane容器,并从 GitHub 下载该工具(github.com/liggitt/audit2rbac/releases):root@cluster01-control-plane:/# curl -L https://github.com/liggitt/audit2rbac/releases/download/v0.8.0/audit2rbac-linux-amd64.tar.gz 2>/dev/null > audit2rbac-linux-amd64.tar.gz root@cluster01-control-plane:/# tar -xvzf audit2rbac-linux-amd64.tar.gz -
使用该工具之前,请确保关闭包含 Kubernetes 仪表板的浏览器,以避免污染日志。另外,删除之前创建的
cluster-create-nsClusterRole和ClusterRoleBinding。最后,尝试创建still-not-going-to-work命名空间:PS C:\Users\mlb> kubectl create ns still-not-going-to-work Error from server (Forbidden): namespaces is forbidden: User "jjackson" cannot create resource "namespaces" in API group "" at the cluster scope -
接下来,使用
audit2rbac工具检查您的测试用户是否存在任何失败:root@cluster01-control-plane:/# ./audit2rbac --filename=/var/log/k8s/audit.log --user=jjackson Opening audit source... Loading events... Evaluating API calls... Generating roles... apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: annotations: audit2rbac.liggitt.net/version: v0.8.0 labels: audit2rbac.liggitt.net/generated: "true" audit2rbac.liggitt.net/user: jjackson name: audit2rbac:jjackson rules: - apiGroups: - "" resources: - namespaces verbs: - create --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: annotations: audit2rbac.liggitt.net/version: v0.8.0 labels: audit2rbac.liggitt.net/generated: "true" audit2rbac.liggitt.net/user: jjackson name: audit2rbac:jjackson roleRef:| apiGroup: rbac.authorization.k8s.io kind: ClusterRole| name: audit2rbac:jjackson subjects: - apiGroup: rbac.authorization.k8s.io kind: User name: jjackson Complete! -
此命令生成的策略将允许测试用户创建命名空间。然而,这变成了一种反模式,明确授权用户访问权限。
-
为了更好地利用这个策略,最好使用我们的组:
apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: create-ns-audit2rbac rules: - apiGroups: - "" resources: - namespaces verbs: - create --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: create-ns-audit2rbac roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: create-ns-audit2rbac subjects: - apiGroup: rbac.authorization.k8s.io kind: Group name: cn=k8s-create-ns,ou=Groups,DC=domain,DC=com
主要的变化已经突出显示。现在,ClusterRoleBinding不再直接引用用户,而是引用了cn=k8s-create-ns,ou=Groups,DC=domain,DC=com组,这样该组的任何成员现在都可以创建命名空间。
总结
本章的重点是RBAC策略创建和调试。我们探讨了 Kubernetes 如何定义授权策略以及如何将这些策略应用于企业用户。我们还了解了这些策略如何帮助在集群中实现多租户功能。最后,我们在 KinD 集群中启用了审计日志,并学习了如何使用audit2rbac工具来调试 RBAC 问题。
使用 Kubernetes 内建的 RBAC 策略管理对象可以让你启用集群中进行操作和开发所需的访问权限。了解如何设计策略有助于限制问题的影响,增强让用户自主完成更多工作的信心。
在下一章,第八章,管理秘密,我们将学习 Kubernetes 如何管理秘密数据,以及如何使用 HashiCorp Vault 和 External Secrets Operator 将外部秘密集成到集群中。
问题
-
对错 – ABAC 是授权访问 Kubernetes 集群的首选方法。
-
正确
-
错误
-
-
Role的三个组成部分是什么?-
主语、名词和动词
-
资源、操作和组
-
apiGroups、资源和动词 -
组、资源和子资源
-
-
你可以去哪里查找资源信息?
-
Kubernetes API 参考
-
图书馆
-
教程和博客文章
-
-
如何在命名空间之间重用
Roles?-
你不能;你需要重新创建它们。
-
定义一个
ClusterRole并在每个命名空间中通过RoleBinding引用它。 -
在一个命名空间中引用
Role,通过其他命名空间的RoleBindings进行引用。 -
以上都不是。
-
-
绑定应该如何引用用户?
-
直接列出每个用户。
-
RoleBindings应该仅引用服务账户。 -
只有
ClusterRoleBindings应该引用用户。 -
在可能的情况下,
RoleBindings和ClusterRoleBindings应该引用组。
-
-
对错 – RBAC 可以授权访问除了一个资源外的所有资源。
-
正确
-
错误
-
-
对错 – RBAC 是 Kubernetes 中唯一的授权方法。
-
正确
-
错误
-
答案
-
a: 错误
-
b: 资源、操作和组
-
a: Kubernetes API 参考
-
b: 定义一个
ClusterRole并在每个命名空间中通过RoleBinding引用它。 -
d: 在可能的情况下,
RoleBindings和ClusterRoleBindings应该引用组。 -
b: 错误
-
b: 错误
加入我们书籍的 Discord 空间
加入本书的 Discord 工作区,参加每月与作者的问我任何问题环节:

第八章:管理机密
每个人都有秘密,Kubernetes 集群也不例外。机密可以用于存储连接数据库的凭据、加密或身份验证的私钥,或任何其他被认为是机密的信息。在本章中,我们将探讨为何机密数据需要与其他配置数据以不同方式处理,如何对集群的机密进行威胁建模,以及如何将外部机密管理器集成到您的集群中。
在第六章《将身份验证集成到您的集群》中,我们为OpenUnison创建了一些机密。这些机密是简单的 Kubernetes 对象,并没有与其他配置数据有所不同地处理。这使得我们很难遵循常见的企业机密数据要求,例如定期轮换和使用情况跟踪。了解企业为什么通常有这些要求,以及如何实施这些要求,非常重要。同时,也要能够从现实角度进行威胁建模,并避免通过试图提高安全性而创建安全漏洞。
本章将介绍为何需要将机密数据与其他配置数据区别对待,并提供您所需的工具,以确定您的机密管理需求并构建您的机密管理平台。我们将涵盖:
-
检查机密与配置数据之间的区别
-
了解机密管理器
-
将机密集成到您的部署中
技术要求
本章具有以下技术要求:
-
一台运行 Docker 的 Ubuntu 22.04+服务器,至少需要 4 GB 内存,建议 8 GB 内存
-
从
chapter8文件夹中的脚本,您可以通过访问本书的 GitHub 仓库来获取:github.com/PacktPublishing/Kubernetes-An-Enterprise-Guide-Third-Edition
获取帮助
我们尽力测试所有内容,但在我们的集成实验室中,有时会有六个或更多系统。鉴于技术的流动性,有时候在我们的环境中有效的东西在您的环境中可能不起作用。别担心,我们在这里帮忙!在我们的 GitHub 仓库上打开一个问题:github.com/PacktPublishing/Kubernetes-An-Enterprise-Guide-Third-Edition/issues,我们将很乐意为您提供帮助!
检查机密与配置数据之间的区别
什么使得Secret与存储在ConfigMap或CRD中的配置数据不同?从 Kubernetes 的角度来看,唯一的实际区别是,ConfigMap和 CRD 都表示为文本,而Secret则表示为base64编码的字符串,这使得机密可以包含二进制数据。
如果你对 base64 不熟悉,它是一种编码过程,使用 64 个字符集将二进制数据转换为 ASCII 字符字符串。这提供了一种可靠的方法,将二进制信息在传输过程中作为文本发送,当直接的二进制支持不可用或纯文本传输可能导致数据损坏的风险时,它非常有用,因此适用于传输图像、音频和二进制文件。
编码和加密这两个术语之间可能会有一些混淆。加密需要密钥来解码,而编码则不需要。虽然编码可能为文本提供一些模糊性,但它并不保护文本。如果你在编码数据时不需要密钥,那就不是加密。
现在,让我们来看看一个Secret对象:
apiVersion: v1
kind: Secret
metadata:
creationTimestamp: "2023-07-30T00:04:51Z"
name: orchestra-secrets-source
namespace: openunison
resourceVersion: "2958"
uid: 2236389e-e751-4030-8d51-96325d302815
type: Opaque
data:
AD_BIND_PASSWORD: JHRhcnQxMjM=
K8S_DB_SECRET: VHloc...
OU_JDBC_PASSWORD: c3RhcnR0MTIz
SMTP_PASSWORD: c3RhcnQxMjM=
unisonKeystorePassword: R2VHV3...
这看起来与ConfigMap非常相似,但有两个不同之处:
-
添加
type指令告诉 Kubernetes 这是什么类型的Secret。 -
data部分的所有字段都是 base64 编码的。
type指令告诉 Kubernetes 你正在创建哪种类型的Secret。在这种情况下,type Opaque意味着Secret的data部分没有特定格式。这将是你最常见的type。
type指令没有任何要求;如果你希望提供自己的值,可以随意指定。然而,如果你提供了 Kubernetes 预定义的类型之一,集群会验证格式是否匹配。你可以在 Kubernetes 的Secret文档中找到预定义类型的列表:kubernetes.io/docs/concepts/configuration/secret/#secret-types
例如,如果你将type设置为kubernetes.io/tls,那么你的Secret必须包含一个名为tls.crt(即,base64 编码的 PEM 证书)的键,以及一个名为tls.key(即,base64 编码的 PEM 私钥)的键,否则 API 服务器将无法创建你的Secret并返回错误。以下是一个示例:
$ kubectl create -f - <<EOF
heredoc> apiVersion: v1
kind: Secret
metadata:
name: not-tls
namespace: default
type: kubernetes.io/tls
data:
AD_BIND_PASSWORD: JHRhcnQxMjM=
EOF
The Secret "not-tls" is invalid:
* data[tls.crt]: Required value * data[tls.key]: Required value
数据的 base64 编码有一个非常简单的原因,但也是许多困惑的来源。Kubernetes 的Secret数据必须是 base64 编码的,因为机密数据通常是区分大小写的或是二进制数据,因此必须编码以确保它在从 YAML - JSON - 二进制存储的转换过程中能够正常存活。
要理解 YAML 在 Kubernetes API 服务器中如何存储非常重要,因为这有助于理解为什么需要这种方式。
当我们处理 YAML 文件时,文本在文件中由一个字节(或多个字节)表示。然后,kubectl将该 YAML 转换为 JSON 以与 Kubernetes API 进行交互。发送到 API 服务器的 JSON 在存储时会被转换为二进制格式。我们在许多基于文本的格式中遇到的问题是,有多种方法可以以二进制格式表示文本数据。
例如,UTF-8 是最常见的编码方式之一,它可以使用从一个到四个字节来表示某个字符。UTF-16 使用一个到四个 16 位的“代码单元”。ASCII 只能编码英文字母、阿拉伯数字和常见的英文标点符号。如果 YAML、JSON 和二进制之间的编码转换涉及到编码类型的切换,数据可能会丢失或损坏。
在文本中保留二进制数据就是 base64 标准的应用所在。Base64 允许将任何数据以 ASCII 文本的形式存储,这是一种在不同编码类型中通用的子集。这意味着 base64 编码的数据可以可靠地跨编码类型进行传输。
如果你仍然对为什么将你的机密数据进行 base64 编码感到怀疑,试想一下你是否曾将一个在 Windows 上创建的文件复制到 Linux 系统,并开始在文本中看到 ^M?这就是跨系统的额外风险:不同的系统用不同的控制字符表示换行。将机密数据进行 base64 编码意味着 YAML 文件中的信息与存储的字节逐字节相同。
一件非常重要的事情是,base64 编码并不是加密。编码没有安全性上的好处,它不能阻止别人窃取你的机密。
既然我们知道为什么 Secret data 是 base64 编码的,为什么 Secrets 要作为独立的对象存在?为什么不直接对 ConfigMaps 进行 base64 编码呢?答案是为了更容易使用 RBAC 限制访问权限。在上一章中,我们探讨了 Kubernetes 的 RBAC 系统,用于授权对资源的访问。在之前的一章中,我们探讨了 Kubernetes 如何创建 ServiceAccount 令牌,并将其存储在 Secret 对象中。结合这两章的知识,我们可以看到,将敏感数据存储在 ConfigMap 中可能会带来意想不到的后果,特别是在考虑到 view ClusterRole 时,它旨在为 namespace 提供只读访问权限。这个 ClusterRole 不包括 Secret 类型,因此 view 只会允许你读取 ConfigMaps、查看 Pod 状态等,但不能读取 Secret。这是因为 Secret 可能包含一个绑定到更高权限的 Role 或 ClusterRole 的 ServiceAccount 令牌,如果不小心,拥有只读访问权限的用户可能会提升访问权限。如果将机密数据存储在 ConfigMaps 中,RBAC 就需要支持某种方法来排除资源或列举出应允许 view ClusterRole 查看特定的 ConfigMap 对象,这使得它可能会被使用不当。
现在我们知道了 Secret 与其他配置对象的不同之处,接下来我们将探讨为什么你需要将 Secret 对象与 ConfigMaps 和 CRDs 区别对待。
在企业中管理 Secrets
这本书的标题中包含“企业”一词,秘密数据管理是一个非常重要的领域。大多数企业对于秘密管理有非常具体的规则。其中一些规则要求能够审计秘密何时被使用,另一些规则则要求秘密定期轮换。遵循这些规则的过程被称为“合规”,并且通常是任何企业部署中的最大成本驱动因素之一。
安全性和合规性通常被放在一起讨论,但它们并不意味着相同的事情。构建一个 100% 合规的系统是很容易的,这样会让你的审计员满意,但却可能无法保护你的应用和数据的安全。这就是为什么理解你在平台中构建某些功能的目的非常重要。你需要问自己,它们是在满足合规性、安全性,还是两者兼顾?
为了回答这个问题,你需要理解数据和系统面临的威胁。这个过程被称为威胁建模,并且已经有许多书籍专门讨论这一主题。在本章中,我们将基于 Kubernetes 秘密在应用程序部署中的位置,构建一个非常基础的威胁模型。我们将从静态秘密开始。
静态秘密的威胁
当一个秘密,比如凭证或密钥,处于存储状态时,它被称为“静态”,因为数据没有被移动。几乎每个合规框架都要求静态数据必须进行加密。这是有道理的;你为什么不加密存储中的数据呢?在 Kubernetes 中,你可以配置 etcd 来加密静态数据,你可能认为自己不仅满足了合规要求(通常称为“打勾”),而且提高了集群的安全性!但事实远比这复杂。
在我们深入探讨 Kubernetes 如何加密静态数据之前,先快速回顾一下加密的基本原理。所有的加密都涉及三个基本组件:
-
数据:可以是加密后的密文,也可以是解密后的明文。
-
密钥:用于将明文加密为密文,也用于将密文解密为明文。
-
算法:将密钥和数据结合的某种过程,用于加密或解密。
每本关于加密的书或课程都会教你,密钥永远是一个秘密,而算法的保密性不应被依赖。这意味着,如果攻击者知道你使用的是 AES-256,这其实并不重要,因为密钥才是秘密。
这里重要的是,如果你使用加密,你必须有一个密钥来加密和解密数据。不同算法的工作方式、块加密与流加密的区别、密钥的生成方式等存在许多细微差别。虽然这些差别非常有趣,但它们对本讨论并不重要。关键是,你需要在同一时间将密钥和数据放在一起,这限制了加密静态数据的安全影响,因为只有当数据和密钥分开时,安全性才会得到提高。
完成了关于加密的旁白,你可能开始意识到在 Kubernetes 数据库中加密数据的问题。Kubernetes 确实支持数据加密,我们不会在这里详细讨论它,因为涉及的复杂性,除非从高层次进行描述。
Kubernetes 加密通过配置 EncryptionConfiguration 对象来工作,该对象指定了哪些数据被加密以及使用什么密钥进行加密。这个对象可以从运行 API 服务器的主机访问。你看出这个问题了吗?如果有人可以访问集群,他们就有密钥!
如果你的 etcd 实例运行在不同的服务器上,那么会有一些额外的安全性,但是当你需要为密钥轮换进行解密和重新加密时,这种好处是否能抵消相关的风险呢?这是你需要自己做出的决定。
加密静态数据会让你的集群更加安全吗?考虑一下“CIA 三位一体”安全模型,它通常用于描述系统的安全需求和影响。CIA 代表:
-
机密性:我们能确保只有我们自己能够访问这些数据吗?
-
完整性:我们怎么确定数据没有被篡改?
-
可用性:我们需要数据时,它是否可用?
加密数据有助于保护 CIA 三位一体中的 C+I 部分,前提是我们可以信任密钥(我们稍后会谈到)。如果密钥轮换需要停机,或者停机风险较高,那么我们的“A”(可用性)可能会受到影响。
有一种观点认为加密并不能提供数据完整性保证,因为加密数据可能会被破坏,需要使用签名来验证数据。签名本质上是一种使用私钥加密、可以通过公钥验证的加密方式,所以这仍然是加密。如果有人可以篡改加密数据,绕过你的密钥或使用你的密钥,那么数据就无法信任。这就是为什么我把完整性视为加密的一个好处。
说到密钥,或许我们并不把密钥存储在本地。Kubernetes 确实支持外部密钥管理系统。我们将在本章后面深入探讨集群如何与保管库或KMS进行身份验证。现在,重要的是要确保生成的身份验证令牌被正确地分配给目标系统,你需要一个本地密钥用于身份验证,这会带来与将解密密钥存储在集群本地相同的影响:本地的安全漏洞意味着攻击者会同时拥有密钥和加密数据,从而能够解密数据。
所以,你已经完成了加密集群数据的过程,并且打了勾。你的集群安全吗?这时,安全和合规性并不完全相同。如果你将这个加密的数据库部署到一个拥有多个用户共享的单一用户账户的系统上,你就创造了一种新的攻击方式,而你却以为自己是安全的!需要注意的是,大多数合规框架仍然要求某种形式的授权管理,但许多供应商将此推给了另一个系统,通常的回答是“我们把密钥保存在保管库中”,这就形成了一个循环合规问题。这些复杂性正是使得保障 Kubernetes 安全并保持合规性变得如此困难的原因。
在研究了如何加密 Kubernetes 静态数据后,我们接下来将探讨 Kubernetes 系统之间传输中的机密威胁。
传输中的机密威胁
在花时间研究静态数据加密的问题后,你可能会认为相同的问题也适用于传输中的数据。密钥需要和数据在同一个地方,那么为什么还要费劲呢?
事实证明,情况并非如此。像JetStack的 cert-manager 这样的 Kubernetes 和 API 驱动的证书颁发机构使得证书管理几乎不存在。在认证章节中,我们已经使用内部证书部署了 cert-manager,并测试了管道认证。我们部署了 cert-manager,并使用了一个私钥和一个自签名的根证书,该证书有效期为十年。我们在整个集群中信任这个证书,并配置了我们的 Ingress 对象,以使用这个内部 CA 生成三个月有效期的证书。NGINX 和 cert-manager 的结合确保我们不需要再考虑证书的续期问题。
对于集群内部的通信,你可以使用相同的方法,或者部署像 Istio 这样的服务网格来生成证书并提供 TLS。我们将在本书后面深入探讨这一内容。
从可用性的角度来看,传输中的数据比静态数据更加短暂。如果由于证书过期而导致可用性中断,现有技术可以执行重试操作来缓解这一风险。
关键是,没理由不对传输中的数据进行加密。虽然 CA 和私钥仍然存在于集群中,因此一旦集群被攻破,流量就可能被解密,但由于密钥轮换,系统的可用性下降的可能性大大降低,这使得这个决策变得更加容易。
如果加密传输中的数据能增加安全性,它是否符合合规性要求?这就是与“静态数据”场景相反的地方。从技术角度看,建立一个证书授权机构(CA)是相对简单的。早在我们还没有 cert-manager 或 Kubernetes 之前,我为一个客户构建了一个基于 API 的 CA,他们希望使用 Java 和 openssl 命令来限制移动应用程序访问 API。构建一个符合合规性的 CA 则要困难得多。它通常涉及大量的管理工作和规定。因此,尽管大多数大型企业拥有可以使用的内部 CA,但你无法在 Kubernetes 中使用它。如果你的集群不符合 CA 的所有规则,那就会使这些规则的合规性失效,从而破坏合规性。
常见的妥协方式是让你的入口控制器使用通配符证书,而集群内部通信使用内部 CA。
有一种强烈的观点认为,自动化的增加克服了集群内 CA 密钥带来的弱点,但由于合规性通常是法律要求,这些观点通常会失败。这也是为什么理解安全性与合规性之间的差异如此重要。在这两个用例中,我们展示了它们如何发生冲突,并且为什么你需要理解这些冲突,以便做出设计决策。
走过如何加密传输中的秘密数据后,最后一个需要探讨的场景是当秘密数据被用于你的应用程序时的情况。
在应用程序中保护秘密数据
让我们走一遍一个潜在的且非常常见的场景。你已经建立了一个“安全”的集群。你的秘密数据全部存储在一个设计良好的秘密管理工具中。你遵循了所有关于如何管理这些数据的指导。每个连接都被加密。你的应用程序加载后,获取密码并连接到数据库。结果发现,两年前有人发现了你解析库中的一个漏洞,攻击者借此漏洞在你的应用上打开了一个 shell,获取了那个密码,而由于你需要连接到数据库,他们能够连接并提取所有数据!
出了什么问题?回到前面提到的两个场景,我们多次强调过,必须掌握秘密数据才能使用它。这意味着,如果你的应用程序存在安全漏洞,那么无论你的秘密管理过程设计得多么完善,它都将成为最薄弱的环节。
这并不意味着我们应该放弃秘密管理。供应链安全是一个独立的关注点,我们将在本书稍后讨论。重点是,当你考虑如何构建秘密管理系统和流程时,要记住,应用程序很可能是最容易失控的地方,你必须据此进行规划。例如,增加影响自动化的额外层级可能不会带来额外的安全性,但你可能会迫使开发人员花时间绕过你的系统,或不必要地提高成本。
现在我们已经了解了秘密如何被攻击,我们可以探讨秘密管理器是如何工作的,并查看在集群中管理秘密数据的不同策略。
理解秘密管理器
我们已经讲解了 Secrets 的特殊之处以及如何处理秘密数据,现在需要讨论如何管理它们。大多数集群管理 Secrets 的方式有四种:
-
Kubernetes Secrets:将所有秘密存储为
Secret对象,而无需任何外部管理。 -
Sealed Secrets:秘密数据在存储在 Git 中的文件中加密。
-
外部秘密管理器:使用外部服务,如 HashiCorp 的 Vault 或基于云的秘密管理器,来存储你的集群的秘密。
-
混合模式:通过将外部秘密管理器中的秘密数据同步到通用的 Kubernetes
Secret对象中,你可以获得一种方法,既能使用SecretsAPI,又能保持关于秘密数据的真实来源在集群外部。
让我们逐一讲解管理秘密的每种方法。
将秘密存储为 Secret 对象
第一个选项看起来是最简单的。利用 Kubernetes Secret 对象提供了几个好处:
-
访问
Secret对象有一个标准 API。 -
API 大部分可以通过 RBAC 进行限制。
-
容器有多种方式可以访问
Secret对象,而无需进行 API 调用。
最后两点可能是把双刃剑。当 Kubernetes 最初创建时,其中一个目标是让应用程序开发者能够在 Kubernetes 上运行工作负载,而无需应用程序了解任何 Kubernetes 相关的内容。这意味着,拥有一个标准的 Secret API 比提供应用程序访问秘密数据的简便方式要次要。为此,Kubernetes 提供了最简单的方式来访问秘密数据,即将秘密挂载为容器中的虚拟文件,或将它们设置为环境变量。我们将在本章稍后讨论这两种方法的优缺点。这一设计决策的影响是,虽然你可以通过使用 RBAC 限制谁可以通过 API 访问 Secret,但你无法限制谁可以将 Secret 挂载到 Namespace 内的 Pod 中。
本书中之前已经提到过这一点,并且会经常重复。Namespace 是 Kubernetes 中的安全边界。如果你想限制对特定 Secrets 的访问,就该创建一个新的 Namespace。
2022 年 4 月,Mac Chaffee 写了一篇很棒的博客文章,标题为Plain Kubernetes Secrets are fine(www.macchaffee.com/blog/2022/k8s-secrets/),他很好地总结了从安全角度来看,使用普通 Kubernetes Secrets是可以的。这篇博客文章指出,在决定如何保护秘密数据之前,你需要先评估这些数据可能面临的威胁。你可能会在这篇博客文章中看到上一部分的很多相同论点。Mac 在表达我一直认为正确的观点时做得更好,我真的很喜欢他的处理方式。文章的“简短总结”是:
-
像Vault这样的秘密管理器,通常并不会以一种能增加额外安全性的方式部署,这和其他任何键/值数据库没有区别。
-
对秘密进行静态加密并不能解决任何问题。
-
你的应用程序是最有可能丢失秘密的地方。
如果一个 Kubernetes Secret已经足够好,为什么我们还要使用秘密管理器呢?这里有两个原因:合规性和GitOps。
从合规性角度来看,大多数合规框架要求你不仅要知道秘密数据何时发生变化,还要知道它何时被使用。例如,NIST-800-53要求你持续监控凭证的使用情况(凭证构成了大部分秘密数据)。虽然你可以在 Kubernetes 中设置日志记录来追踪这一点,但将其集中在一个地方进行审计会更方便。
我们应该评估秘密管理器的下一个原因是GitOps。在接下来的两章中,我们将探讨 GitOps,其中一个重要部分是将我们的配置存储在 Git 仓库中。你绝对不应该,也绝对不能将秘密数据存储在 Git 仓库中,无论是以明文形式还是加密数据的形式。Git 仓库设计上容易被分叉。一旦被分叉,你就失去了对该仓库的控制。回到合规性,这带来了一个大风险,因为你无法知道是否有开发人员将你的内部仓库分叉并不小心推送到了公开的 GitHub 仓库中。还有其他原因表明在 Git 中存储秘密应该被视为反模式,但我们会在讨论密封秘密时再深入探讨。使用秘密管理器使我们能够将秘密数据从集群中外部化,尽管Secret对象对于大多数集群来说可能已经足够。
了解了为什么从安全角度来看常规的 Kubernetes Secrets通常是可以的,现在让我们来看看什么是密封秘密以及为什么它们是反模式。
密封秘密
如果你将 Kubernetes 清单外部化到 Git 仓库中,你可能会想要将敏感的秘密数据也存储在那里。不过,你不希望任何人获取这些数据,于是你决定加密它。然而,现在你需要解密它,以便将其重新导入到集群中。Bitnami(现为 VMware 所拥有)发布了一款名为 Sealed Secrets 的工具(github.com/bitnami-labs/sealed-secrets),它正是为了解决这个问题。你可以将操作员安装到集群中,当它看到一个 SealedSecret 时,它会为你解密它。
这看起来是一个简单而优雅的解决方案,用于安全地外部化敏感数据。不幸的是,它表面上的简洁性正是导致这个解决方案成为反模式的原因。
Sealed Secrets 的第一个问题是敏感数据存储在 Git 中。我们在前一节中已经指出,从安全的角度来看,这个做法是一个坏主意。秘密管理的主要目标之一是能够追踪秘密的使用情况。开发者很容易将一个内部仓库推送到 GitHub 或 GitLab 等公共服务上。看看这个简单的命令行:
$ git remote set-url origin https://github.com/new-repository.git
$ git push
只要用户登录了 GitHub,你的内部仓库现在就是公开的!理论上,你可以限制对公共 Git 仓库的访问,但那可能适得其反。我的一般建议是,永远不要将你不希望出现在 GitHub 上的内容放入 Git。一旦代码被推送到 GitHub 或任何其他远程仓库,你就失去了所有控制权。
针对将数据推送到公共仓库这个问题,你的第一反应可能是:“但它是加密的!”作为一个物种,我们很不擅长保守秘密。作为一个行业,我们更不擅长保护用来加密秘密的密钥。如果你丢失了仓库,很有可能你也会丢失密钥。
“三个人可以保守一个秘密,前提是其中两个已经死了。”——本杰明·富兰克林
这不仅仅在 Kubernetes 世界中成立,实际上在任何技术领域都是如此。这就是为什么计划好如何应对丢失敏感数据,并能够快速更换它们是如此重要的原因。如果你的 Git 仓库包含敏感数据(无论是否加密),并且它被推送到了你公司外部,你会希望:
-
对所有 Sealed Secrets 进行操作并生成新的秘密数据(即凭证)。
-
生成一个新的加密密钥。
-
重新加密并重新推送所有 Sealed Secrets 到 Git。
根据你的集群大小以及密钥管理的情况,这可能会很快变成一项艰巨的任务。事实证明,秘密管理工具在处理这种失败模式时表现得非常出色。
除了能够处理丢失仓库的失败模式之外,你还需要考虑丢失用来加密和解密敏感数据的密钥。如果你丢失了用来加密秘密的密钥,并且丢失了秘密…可以说,你正为一个糟糕的一周做准备,或者某些人所称的简历建设事件。
尽管 Sealed Secrets 看起来是处理密钥管理的一种简单方式,但它们在应对大多数安全漏洞后的失败时没有考虑到可接受的处理方式。虽然你不希望将机密数据存储在 Git 中,但将有关机密的元数据存储在 Git 中是可以接受的。我们将在下一节中看到,密钥管理器可以通过描述获取密钥数据位置的元数据集成到你的集群中,而无需在存储库中存储任何机密。
外部密钥管理器
在上一节中,我们讨论了为什么将 Secrets 存储在 Git 中,无论是加密还是未加密,都是一种反模式。我们还讨论了,你可能希望将密钥数据管理外部化,以便使合规性和 GitOps 更加简便。满足这些要求的最常见方法是使用密钥管理器。
密钥管理器是键值数据库,具有一些通用键值数据库中通常没有的附加功能:
-
认证:密钥管理器通常可以使用多种方法进行认证。最佳解决方案允许你直接使用 Pod 的凭证或使用从中派生的凭证。这使得密钥管理器能够跟踪哪些工作负载正在处理机密数据,并为管理对这些数据的访问提供更丰富的策略。
-
策略:大多数密钥管理器提供比通用数据库更丰富的策略框架。结合灵活的认证,密钥管理器的选项可以帮助将密钥锁定到工作负载,同时跟踪使用情况,而无需管理员参与每次的入驻过程。
-
审计:除了跟踪更改外,密钥管理器还会跟踪读取操作。这是一个关键的合规性要求。
密钥管理器的认证工具非常重要。如果你使用通用凭证来访问密钥管理器,它实际上并不会增加太多的安全性或合规性。在第六章,将认证集成到集群中,我们讨论了每个 pod 如何根据其 ServiceAccount 获取唯一的身份,并且如何通过使用集群的 OIDC 发现文档或提交 TokenReview 来验证该 token 是否仍然有效。当与密钥管理器进行通信时,应该使用此 token。如果你在云托管的 Kubernetes 上运行,这个身份也可以是云提供的身份。关键是,你使用的是本地身份,而不是静态密钥。这个本地身份会出现在你的审计日志中,允许你的安全团队和审计员知道是谁在访问密钥。
最后,利用你的Pod身份来访问秘密管理工具,使得上手和自动化更为简便。我们将在本书稍后讨论多租户的多种形式,这些形式都具备自动化的共同点。大多数秘密管理工具都简化了设计策略的过程,这些策略允许根据身份验证令牌中的信息(如namespace)来划分秘密访问权限。这意味着在为新租户引导时,你不需要每次都向秘密管理工具发出 API 请求。
有多个秘密管理工具可用;每个主要云服务商都有自己的解决方案,并且有多个开源管理工具:
-
HashiCorp Vault:
github.com/hashicorp/vault -
CyberArk Conjur:
github.com/cyberark/conjur -
VMware Tanzu Secrets Manager:
github.com/vmware-tanzu/secrets-manager
我们不希望你必须注册云服务,因此在本章的示例中(以及本书其余部分需要秘密时),我们将使用 HashiCorp 的 Vault。
2023 年 8 月,HashiCorp 宣布其项目,包括Vault,的许可变更,从Mozilla 公共许可证(MPL)变更为商业源代码许可证(BUSL)。虽然 BUSL 并不是开放源代码协会(Open Source Institute)认可的开放源代码™许可证,但它允许在生产环境和非生产环境中免费使用。我们决定继续使用 Vault,因为尽管 HashiCorp 项目的社区提出了分叉或转移的呼声,但企业已经在软件、人员和自动化方面为 Vault 部署投入了数十万美元。它仍然是最常见的秘密管理工具,且可能会持续一段时间。
要部署 Vault,请从一个新的集群开始,并运行chapter8/vault/deploy_vault.sh脚本:
$ cd chapter8/vault/
$ ./deploy_vault.sh
该脚本通过以下方式将 Vault 部署到集群中:
-
部署带有自签名 CA 的 cert-manager 用于入口证书
-
安装Vault Helm 图表
-
使用 UI 和
ClusterIPService对象将 Vault 部署到集群中 -
检索用于解封 Vault 数据库的密钥
-
解封 Vault 数据库
-
部署
Ingress对象后,你可以通过 NGINX 访问 UI 和 Web 服务
Vault 加密其数据,因此当你启动 Pod 时,需要“解封”它以便管理。你可以通过先从部署生成的~/unseal-keys.json文件中检索令牌,登录到你的 Vault 实例:
$ jq -r '.root_token' < ~/unseal-keys.json
hvs.OFotf6LlPTI1bRhqNDMyYf7N
接下来,使用此令牌通过访问vault.apps.IP.nip.io登录 Vault,其中IP是你的 Kubernetes 集群的 IP 地址,点被替换为连字符。例如,我们的集群的 IP 是192.168.2.82,因此我们的 Vault URL 是vault.apps.192-168-2-82.nip.io/。
我们不会深入讨论 Vault 如何配置,除了一些具体的例子来说明如何将外部的密钥管理器集成到你的集群中。如果你想了解所有选项,可以在线查看文档:developer.hashicorp.com/vault/docs。同时也需要指出,这并不是一个适用于生产的部署,因为它没有高可用性,也没有围绕启动和管理入驻流程的任何机制。
部署 Vault 后,下一步是将 Vault 集成到我们的集群中。之前我们讨论过,使用Pod的身份与密钥管理器交互是很重要的。我们将通过配置 Vault 来提交TokenReview,以验证令牌是否由我们的集群颁发,并且与该身份关联的 Pod 是否仍然运行。

图 8.1:Vault 与 Kubernetes 的集成。
上图展示了流程:
-
一个 Pod 使用其通过
TokenRequestAPI 投射的ServiceAccount令牌向 Vault API 发出请求。 -
Vault 向 API 服务器提交
TokenReview请求,携带Pod的令牌。 -
API 服务器验证令牌是否仍然有效。
使用上述过程使我们能够验证Pod的令牌,从而确认与该令牌关联的 Pod 仍然有效。如果攻击者窃取了 Pod 的令牌并试图在 Pod 被销毁后使用它,那么TokenReview请求将被拒绝。
除了使用TokenReview API 外,Vault 还可以配置为使用 OIDC 来验证令牌,而无需回调 API 服务器。我们不会走这条路,因为我们希望 Vault 验证与令牌关联的 Pod 是否仍然有效。
为了将 Vault 集成到我们的集群中,运行chapter8/vault/vault_integrate_cluster.sh:
$ cd chapter8/vault/
$ ./vault_integrate_cluster.sh
这个脚本将通过以下方式将你的 Vault 部署与 KinD 集群集成:
-
创建
vault-integration命名空间和vault-client ServiceAccount。 -
为
vault-client ServiceAccount创建一个ClusterRoleBinding,将其绑定到system:auth-delegator ClusterRole。 -
为
vault-client的ServiceAccount创建一个大约有效一年期的令牌。 -
为我们的 API 服务器创建一个
Ingress,使 Vault 能够与其通信。 -
创建与我们集群的 Vault Kubernetes 认证配置。
Vault 的部署被视为独立部署,尽管它运行在我们的集群上,因为 Vault 在企业部署中通常是这样运行的。Vault 是一个复杂的系统,运行它需要高度专业的知识,因此将 Vault 知识集中到一个集中团队中会更容易。
你可能还会注意到,我们正在为 Vault 创建一个有效期为一年的令牌,用来与集群进行交互。这违反了我们使用短期令牌的目标。这是一个先有鸡还是先有蛋的问题,因为 Vault 需要对 Kubernetes 进行身份验证才能验证令牌。集群可以配置为允许匿名的 TokenReview 访问,这样就可能暴露于潜在的升级攻击中。
如果 Vault 支持像我们在 第六章《将身份验证集成到您的集群》中定义的工作负载那样,使用它自己的 OIDC 令牌与 Kubernetes 进行通信,那就太好了,但目前还不具备这样的能力。
我们已经介绍了外部机密管理器与其他键值存储的不同,并部署了 HashiCorp 的 Vault。Vault 已经部署并集成到集群中。现在你已经建立了一个与外部化机密数据工作并探索将其集成到集群的不同方式的基础。接下来,我们将讨论使用外部机密管理器和原生 Kubernetes Secrets 的混合方法。
使用外部机密管理和机密对象的混合方式
到目前为止,我们已经讨论了使用通用 Kubernetes Secret 对象、将机密数据存储在 Git 中的加密文件中的反模式,以及最终使用外部机密管理器。由于我们已经确定基于我们的威胁模型,普通的 Kubernetes Secrets 不太可能构成重大风险,但我们更倾向于将机密外部化到像 Vault 这样的工具中,因此,如果我们能使用 Kubernetes Secrets API 来访问外部保管库中的机密数据,那就太好了。
使用 Secret API 访问外部机密数据不太可能发生。然而,我们可以将机密数据从我们的 Vault 同步到 Kubernetes Secret 对象中。这种混合方法使我们能够结合两者的优点:
-
集中化的机密数据:我们机密数据的真实来源是我们的 Vault。
Secret对象中的数据是该数据的副本。 -
元数据可以存储在 Git 中:用于描述机密数据存储位置的元数据本身并不是机密的。它可以存储在 Git 中,而不会像存储实际的机密数据那样带来负面后果。
-
审计数据:可以在 API 服务器的访问日志和 Vault 的访问日志中配置访问审计日志。
有多个项目支持将机密从保管库同步到 Kubernetes。最常见的两个项目是:
-
Kubernetes Secret Store CSI Driver (https://secrets-store-csi-driver.sigs.k8s.io/introduction):Secret Store CSI 驱动程序是 Kubernetes 项目中的特别兴趣小组(SIG),为访问 Vault 等密钥存储提供存储驱动程序。它包括一个同步引擎,将生成通用的
Secret对象。使用这个项目的主要挑战是,在你能够将 Vault 中的数据同步到Secret之前,需要先将其挂载到 Pod。 -
外部密钥操作员 (
external-secrets.io/latest/):外部密钥操作员项目提供将密钥数据直接同步到Secret对象。
本节将重点介绍如何使用外部密钥操作员项目。我们选择使用外部密钥操作员,因为它不要求 Pod 在同步到Secret之前先挂载密钥数据。首先,部署同步操作员:
$ cd chapter8/external-secrets
$ ./install_external_secrets.sh
上述脚本完成了以下几件事情:
-
部署外部密钥操作员
-
创建一个
Namespace来存储同步后的 Secret 对象 -
创建一个
ServiceAccount来访问 Vault -
在 Vault 中创建一个密钥密码
-
在 Vault 中创建一个策略,以通过上述
ServiceAccount访问密钥 -
创建一个
ExternalSecret对象,告诉操作员在哪里以及如何将我们的密钥数据同步到集群中
这里有很多操作。操作员本身是通过 Helm chart 部署的。我们用来存储Secret及其相关ServiceAccount的namespace基于 Vault 集成构建,允许 Pod 使用特定的身份,而不是使用静态的ServiceAccount令牌。在创建了Namespace和ServiceAccount之后,会创建 Vault 策略,允许ServiceAccount读取密钥数据。最后,创建一个SecretStore和ExternalSecret对象,告诉操作员如何同步密钥数据。接下来,我们来看一下这些对象。首先,我们创建了SecretStore,告诉操作员 Vault 的位置以及如何访问它:
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-backend
namespace: my-ext-secret
spec:
provider:
vault:
server: "https://vault.apps.192-168-2-82.nip.io"
path: "secret"
version: "v1"
caProvider:
type: "ConfigMap"
name: "cacerts"
key: "tls.crt"
namespace: "my-ext-secret"
auth:
mountPath: "kubernetes"
serviceAccountRef:
name: "ext-secret-vault"
第一个对象SecretStore告诉外部密钥操作员密钥存储的位置以及如何访问它。在这种情况下,我们通过 URL https://vault.apps.192-168-2-82.nip.io连接到 Vault,并使用存储在cacerts ConfigMap中的证书来进行 TLS 信任。auth部分告诉操作员如何进行身份验证,使用ServiceAccount ext-secret-vault的令牌。定义了SecretStore后,下一步是开始定义需要创建的Secret对象。
为了将密钥数据同步到Secret对象,需要有一个ExternalSecret对象:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: my-external-secret
namespace: my-ext-secret
spec:
refreshInterval: 1m
secretStoreRef:
kind: SecretStore
name: vault-backend
target:
name: secret-to-be-created
creationPolicy: Owner
data:
- secretKey: somepassword
remoteRef:
key: /data/extsecret/config
property: some-password
ExternalSecret对象定义了如何将数据从 Vault 同步到你的集群。在这里,数据是从与 Vault 部署通信的SecretStore中提取的。该对象指示External Secrets Operator从 Vault 中的/data/extsecret/config对象中获取值,并在Secret secret-to-be-created中创建somepassword密钥,该值来自some-password属性。
为了提供一些背景信息,以下是脚本中的 Vault 配置:
vault kv put secret/data/extsecret/config some-password=mysupersecretp@ssw0rd
一旦同步过程运行,我们可以看到现在在我们的Secret中有来自 Vault 的数据:
$ kubectl get secret secret-to-be-created -n my-ext-secret -o json | jq -r '.data.somepassword' | base64 -d
mysupersecretp@ssw0rd
基于External Secrets Operator项目提供的方法,可以在 git 仓库中创建和存储访问机密数据的元数据,而不会对安全性产生不利影响。集群能够使用定义良好的Secrets API 访问机密,同时仍能享受将机密数据外部化的好处。
在下一节中,我们将查看如何从工作负载中使用现在在集群中可用的机密数据。
将机密集成到你的部署中
到目前为止,本章重点讨论了如何存储和管理机密数据。我们已经介绍了管理机密的不同策略及其相关的风险和收益。本节将重点讨论如何在工作负载中使用这些机密数据。
工作负载有四种方式可以消耗机密数据:
-
卷挂载:与从
PersistentVolumeClaim读取文件类似,机密可以挂载到 Pod 中并作为文件访问。此方法可以用于外部机密和Secret对象。当与安全团队合作时,这通常是首选方法。如果在 Pod 运行时更新了Secret,该卷最终会更新,尽管这可能需要一些时间,具体取决于你的 Kubernetes 发行版。 -
环境变量:机密数据可以注入到环境变量中,并像其他任何环境变量一样被工作负载使用。这通常被称为“不安全”,因为应用程序开发人员常常为了调试目的而转储环境变量。在生产环境中,调试组件意外地泄漏环境变量并不罕见。如果可能的话,最好避免这种方法。需要注意的是,如果在 Pod 运行时更新了
Secret,运行中的 Pod 中的环境变量将不会更新。 -
Secrets API:Kubernetes 是一个 API,
Secrets可以通过SecretAPI 直接访问。与环境变量或卷挂载相比,这种方法提供了更多灵活性,但需要了解如何调用 API。如果你需要能够动态地检索Secrets,这是一个不错的方法,但对于大多数应用来说可能是过度设计。 -
Vault API:每个外部 Vault 都提供一个 API。虽然我们正在使用像External Secrets Operator或 sidecar 这样的工具与这些 API 交互,但没有任何东西阻止开发者自己调用这些 API。这会减少外部配置,但代价是将系统紧密绑定到特定的项目或供应商。
接下来,我们将逐步走过这些选项,看看它们是如何实现的。
卷挂载
向工作负载添加Secrets的首选方式是将它们视为文件并加载到应用程序中。这种方法比上面列出的其他方法有多个优势:
-
在调试过程中泄露的可能性较小:没有任何东西可以阻止开发者将文件内容打印到日志或输出流中,但调用
env命令不会自动打印出机密数据。 -
可以更新:当文件被更新时,该更新会反映在你的 pod 中。对于通过卷挂载的机密数据也是如此。如果 pod 中的应用程序知道检查更新,它最终会获取到这些更新。
-
更丰富的选项:挂载到卷上的配置文件不仅仅是名称/值对。它可以是完整的配置文件,从而简化管理。
将机密作为卷挂载自 Kubernetes 的Secrets功能已经有一段时间了。在这一节中,我们将介绍如何挂载通用 Kubernetes Secret对象,并直接使用Vault Sidecar与我们的 Vault 部署进行交互。
使用 Kubernetes Secrets
将 Kubernetes Secret作为卷挂载到 pod 中,只需要在 spec 中命名该Secret。例如,如果你从chapter8/integration/volumes/volume-secrets.yaml创建了 pod:
apiVersion: v1
kind: Pod
metadata:
labels:
run: test-volume
name: test-volume-secrets
namespace: my-ext-secret
spec:
containers:
- image: busybox
name: test
resources: {}
command:
- sh
- -c
- 'cat /etc/secrets/somepassword'
volumeMounts:
- name: mypassword
mountPath: /etc/secrets
volumes:
- name: mypassword
secret:
secretName: secret-to-be-created
dnsPolicy: ClusterFirst
restartPolicy: Never
它将生成以下日志:
$ kubectl logs test-volume-secrets -n my-ext-secret
myN3wP@ssw0rd
这个 pod 将我们从 Vault 同步的Secret直接添加到 pod 中。我们可以在 Vault 中更新该机密,并查看它在长期运行的 pod 中的挂载值发生了什么变化。首先,创建chapter8/integration/volumes/volume-secrets-watch.yaml中的 pod:
$ cd chapter8/integration/volumes
$ kubectl create -f ./volume-secrets-watch.yaml
$ kubectl logs -f test-volumes-secrets-watch -n my-ext-secret
Fri Sep 15 14:52:45 UTC 2023
myN3wP@ssw0rd
----------
Fri Sep 15 14:52:46 UTC 2023
myN3wP@ssw0rd
----------
Fri Sep 15 14:52:47 UTC 2023
myN3wP@ssw0rd
----------
现在我们在监视挂载的卷,让我们更新 Vault 中的机密:
$ . ../../vault/vault_cli.sh
$ vault kv put secret/data/extsecret/config some-password=An0therPassw0rd
Success! Data written to: secret/data/extsecret/config
$ kubectl get secret secret-to-be-created -n my-ext-secret -o json | jq -r '.data.somepassword' | base64 -d
An0therPassw0rd
可能需要一两分钟,但我们集群中的Secret会被同步。接下来,让我们查看我们正在运行的 pod:
$ kubectl logs -f test-volumes-secrets-watch -n my-ext-secret
Fri Sep 15 14:52:45 UTC 2023
myN3wP@ssw0rd
----------
.
.
.
Fri Sep 15 14:56:33 UTC 2023
An0therPassw0rd
----------
Fri Sep 15 14:56:34 UTC 2023
An0therPassw0rd
----------
Fri Sep 15 14:56:35 UTC 2023
An0therPassw0rd
----------
我们可以看到卷确实得到了更新。所以,如果我们有一个长时间运行的 pod,我们可以监视挂载的卷,查找更新。
在这一节中,我们将Secret直接集成到 pod 中。当使用外部机密库(如 HashiCorp 的 Vault)时,这需要使用像External Secrets Operator这样的工具来同步Secret。接下来,我们将使用 Vault 的 injector sidecar 直接从 Vault 创建一个卷。
使用 Vault 的 Sidecar Injector
在上一节中,我们将一个通用的 Kubernetes Secret集成到我们的 pod 中。在这一节中,我们将直接使用 Vault 的 injector sidecar与 Vault 进行集成。
Sidecar是与主工作负载一起运行的特殊容器,用于执行额外的功能,且透明地独立于主工作负载。Sidecar 模式使你能够创建拦截网络流量、管理日志,或者在 Vault 的情况下,注入机密的容器。从 1.28 版本开始,sidecar 从一个广为人知的模式转变为成为一个一等配置选项。这种方法仍然非常新,尚未被大多数实现采用。你可以在 Kubernetes 博客中了解更多有关 sidecar 变化的内容,链接:kubernetes.io/blog/2023/08/25/native-sidecar-containers/。
Vault 的sidecar 注入器有两个主要组件,让我们能够将机密数据注入到 Pod 中:
-
注入器变异接收控制器:我们将在第十一章中更详细地讨论接收控制器,通过 Open Policy Agent 扩展安全性。现在,你需要知道的是,这个控制器会寻找具有特定
annotations的 Pod,以配置与 Vault 服务交互的 sidecar。 -
Sidecar:Sidecar 负责与我们的 Vault 部署进行交互的工作。你不需要手动配置 sidecar,接收控制器变异器会根据
annotations为你完成这个任务。
首先,让我们看一下从 Vault 直接获取机密数据的 Pod:
apiVersion: v1
kind: Pod
metadata:
labels:
run: watch-vault-volume
name: test-vault-vault-watch
namespace: my-ext-secret
annotations: **vault.hashicorp.com/agent-inject:****"true"**
**vault.hashicorp.com/service:****"https://vault.apps.192-168-2-82.nip.io"**
**vault.hashicorp.com/log-level:****trace**
**vault.hashicorp.com/role:****extsecret**
**vault.hashicorp.com/tls-skip-verify:****"true"**
**vault.hashicorp.com/agent-inject-secret-myenv:****'secret/data/extsecret/config'**
**vault.hashicorp.com/secret-volume-path-myenv:****'/etc/secrets'**
**vault.hashicorp.com/agent-inject-template-myenv:****|**
**{{****-****with****secret****"secret/data/extsecret/config"****-****}}**
**MY_SECRET_PASSWORD="{{****index****.Data****"some-password"****}}"**
**{{****-****end** **}}**
spec:
containers:
- image: ubuntu:22.04
name: test
resources: {}
command:
- bash
- -c
- 'while [[ 1 == 1 ]]; do date && cat /etc/secrets/myenv && echo "" && echo "----------" && sleep 1; done'
dnsPolicy: ClusterFirst
restartPolicy: Never
**serviceAccountName:****ext-secret-vault**
**serviceAccount:****ext-secret-vault**
让我们集中讨论 Vault 的连接选项:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/service: "https://vault.apps.192-168-2-82.nip.io" vault.hashicorp.com/log-level: trace
vault.hashicorp.com/role: extsecret
vault.hashicorp.com/tls-skip-verify: "true"
第一个annotation告诉接收控制器我们希望为这个 Pod 生成 sidecar 配置。接下来的annotation告诉 Vault Vault 服务的位置。然后我们启用详细日志记录,并设置认证的角色。这个角色在 Vault 中是在chapter8/external-secrets/install_external_secrets.sh中创建的,具体如下:
vault write auth/kubernetes/role/extsecret \
bound_service_account_names=ext-secret-vault \
bound_service_account_namespaces=my-ext-secret \
policies=extsecret \
ttl=24h
角色将我们的ServiceAccount映射到一个允许的策略。最后,我们告诉代理跳过 TLS 验证。在生产环境中,你不应该跳过 TLS 验证。我们可以挂载我们的 CA 证书,这也是你在生产环境中应该做的。
请注意,我们没有指定用于认证的密钥或Secret。这是因为我们使用的是我们Pod本身的身份。Pod 上的serviceAccount和serviceAccountName选项决定了使用哪个身份。当我们配置外部机密时,我们使用了ext-secret-vault ServiceAccount,因此我们在这里复用了该身份。
定义了如何与 Vault 通信后,接下来让我们看一下如何定义我们的数据:
vault.hashicorp.com/agent-inject-secret-myenv: 'secret/data/extsecret/config'
vault.hashicorp.com/secret-volume-path-myenv: '/etc/secrets'
vault.hashicorp.com/agent-inject-template-myenv: |
{{- with secret "secret/data/extsecret/config" -}}
MY_SECRET_PASSWORD="{{ index .Data "some-password" }}"
{{- end }}
第一个 annotation 说:“创建一个名为myenv的配置,指向 Vault 对象/secret/data/extsecret/config。”myenv就像一个变量,允许你在多个annotations中跟踪配置选项。接下来的annotation表示我们希望将所有内容放入/etc/secrets/myenv文件中。如果我们没有指定这个 annotation,sidecar 将把生成的myenv文件放入/vault/secrets目录。
最终的 annotation 创建了 myenv 文件的内容。如果语法看起来像 Helm,那是因为它使用了相同的模板引擎。在这里,我们创建了一个名称/值对的文件。这也可以是一个配置文件模板。
现在我们已经完成了配置,让我们来创建对象:
$ cd chapter8/integration/volumes
$ ./create-vault.sh
$ kubectl logs -f test-vault-vault-watch -n my-ext-secret
Defaulted container "test" out of: test, vault-agent, vault-agent-init (init)
Fri Sep 15 18:24:09 UTC 2023
MY_SECRET_PASSWORD="An0therPassw0rd"
----------
Fri Sep 15 18:24:10 UTC 2023
MY_SECRET_PASSWORD="An0therPassw0rd"
我们的原始工作负载并不了解 Vault,但能够从生成的模板中检索秘密数据。有些场景中 Vault 会刷新模板,但这些是 Vault 特有的配置选项,我们不打算深入探讨。
基于注解的 sidecar 注入是与 Kubernetes 集成的秘密管理中的一种常见模式。如果你打算集成其他外部秘密管理系统,这是一个一致的使用方式。
我们已经看过如何使用卷将秘密绑定到工作负载,既可以使用标准的 Kubernetes Secret 对象,也可以使用我们的外部 Vault。这两种方法都允许你将秘密外部化。接下来,我们将讨论如何将秘密数据注入为环境变量。
环境变量
使用环境变量是获取任何数据的最简单方法。每种语言和平台都有一种标准的方式来访问环境变量。尽管这种方式很容易访问,但开发者常常会为了调试而打印出或转储所有的环境变量。这样一来,数据可能会出现在日志中,甚至更糟,出现在打印出环境变量的调试网页上。这种机制通常会被安全团队标记出来,因此如果可能的话应该避免使用。然而,一些工作负载确实需要环境变量,因此我们来看一下如何将 Kubernetes Secrets 和来自外部 Vault 的秘密集成到 Pod 中。
使用 Kubernetes Secrets
Kubernetes 可以将 Secret 对象的数据直接插入到容器的 Pod 定义中的环境变量里。这里是一个简单的示例:
apiVersion: v1
kind: Pod
metadata:
labels:
run: test
name: test-envvars-secrets
namespace: my-ext-secret
spec:
containers:
- image: busybox
name: test
resources: {}
command:
- env
**env:**
**-****name:****MY_SECRET_PASSWORD**
**valueFrom:**
**secretKeyRef:**
**name:****secret-to-be-created**
**key:****somepassword**
dnsPolicy: ClusterFirst
restartPolicy: Never
看 .spec.containers[name=test].env,你可以看到我们从现有的 Secret 创建了一个环境变量。这个容器的命令仅仅是 env 命令,它会打印出所有的环境变量。要查看这个容器的运行效果,应用来自 chapter8/integration/envvars/envars-secrets.yaml 的 YAML:
$ cd chapter8/integration/envvars
$ kubectl create -f ./envars-secrets.yaml
$ kubectl logs -f -l run=test -n my-ext-secret
**MY_SECRET_PASSWORD=An0therPassw0rd**
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_PORT=tcp://10.96.0.1:443
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_PORT_443_TCP_PORT=443
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
KUBERNETES_SERVICE_HOST=10.96.0.1
KUBERNETES_SERVICE_PORT=443
HOME=/root
如果我们更新 Secret 会发生什么?Kubernetes 不会更新环境变量。让我们验证一下这一点。首先,创建一个监视我们环境变量的 Pod:
$ cd chapter8/integration/envvars
$ kubectl create -f ./envars-secrets-watch.yaml
$ kubectl logs -f -l run=watch-env -n my-ext-secret
Fri Sep 15 12:43:30 UTC 2023
MY_SECRET_PASSWORD=An0therPassw0rd
Fri Sep 15 12:43:31 UTC 2023
MY_SECRET_PASSWORD=An0therPassw0rd
这个 Pod 将持续回显我们创建的环境变量。接下来,让我们更新 Vault:
$ . ../../vault/vault_cli.sh
$ vault kv put secret/data/extsecret/config some-password=myN3wP@ssw0rd
Success! Data written to: secret/data/extsecret/config
$ kubectl get secret secret-to-be-created -n my-ext-secret -o json | jq -r '.data.somepassword' | base64 -d
myN3wP@ssw0rd
我们的新密码同步到 Secret 中可能需要最多一分钟的时间。请等待同步完成。一旦同步完成,让我们查看日志:
ubuntu@book-v3:~/Kubernetes-An-Enterprise-Guide-Third-Edition/chapter8/vault$ kubectl logs -f -l run=watch-env -n my-ext-secret
**Fri Sep 15 12:48:41 UTC 2023**
**MY_SECRET_PASSWORD=An0therPassw0rd**
**Fri Sep 15 12:48:42 UTC 2023**
**MY_SECRET_PASSWORD=An0therPassw0rd**
**Fri Sep 15 12:48:43 UTC 2023**
**MY_SECRET_PASSWORD=An0therPassw0rd**
Pod 中的环境变量未更新。你可以继续运行它,但它不会发生变化。如果你希望支持环境变量的动态变化,你需要某些机制来监控 Secret 并在需要时重启工作负载。
现在我们能够将 Kubernetes Secret 作为环境变量进行使用,接下来,我们将与 Vault 侧车一起工作,将这个变量集成到 pod 中。
使用 Vault 侧车
Vault 侧车不支持直接注入环境变量,因为侧车镜像无法共享环境变量。如果你的 pod 确实需要环境变量,你需要生成一个包含导出这些变量的脚本的文件。这里有一个例子:
apiVersion: v1
kind: Pod
metadata:
labels:
run: watch-vault-env
name: test-envvars-vault-watch
namespace: my-ext-secret
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/log-level: trace
vault.hashicorp.com/role: extsecret
vault.hashicorp.com/tls-skip-verify: "true"
vault.hashicorp.com/agent-inject-secret-myenv: 'secret/data/extsecret/config'
vault.hashicorp.com/agent-inject-template-myenv: |
{{- with secret "secret/data/extsecret/config" -}}
export MY_SECRET_PASSWORD="{{ index .Data "some-password" }}"
{{- end }}
spec:
containers:
- image: ubuntu:22.04
name: test
resources: {}
command:
- bash
- -c
**-****'echo "sleeping 5 seconds"; sleep 5;source /vault/secrets/myenv ; env | grep MY_SECRET_PASSWORD'**
dnsPolicy: ClusterFirst
restartPolicy: Never
serviceAccountName: ext-secret-vault
serviceAccount: ext-secret-vault
这个 pod 定义几乎与我们之前创建的基于卷的 Vault 集成完全相同。主要的区别在于,我们在容器中运行一个命令,从我们在注释中创建的模板生成环境变量。虽然这种方法可以工作,但它意味着需要更新你的清单,以便它们查找特定的文件。这种方法打破了清单可重用性的既定模式,并且会使得使用外部生成的镜像变得困难。
尝试通过外部秘密管理器生成环境变量的问题强化了这一点:这是一种反模式,应当避免。接下来,我们将直接使用 Kubernetes API 来获取 Secrets。
使用 Kubernetes Secrets API
到目前为止,我们主要关注与 API 交互的抽象,以便我们的工作负载不需要了解 Kubernetes API。对于大多数工作负载来说,这是合理的,因为秘密元数据通常是静态的。例如,你的应用程序可能需要与一个数据库通信,虽然凭证可能会发生变化,但你需要凭证这一事实不会改变。那如果你需要更动态的东西呢?如果你正在构建一个为其他系统提供服务的系统,可能就需要这种动态性。在本节中,我们将介绍为什么你会直接从 pod 中调用 Secrets API。
你可能会问的第一个问题是“为什么?”在 Kubernetes Secrets API 上有多种抽象选项的情况下,为什么你还要直接调用 Kubernetes API?最近有一些有趣的趋势,可能会让这种做法成为现实:
-
更多的单体架构:近几个月来,重新审视微服务是否是大多数系统的正确架构成为一种趋势。向单体架构转变最显著的例子之一是亚马逊的 Prime Video 服务质量(QoS)从 Lambda 转向单体架构。我们在第十七章《在 Istio 上构建和部署应用程序》中讨论了这些方法之间的权衡。如果你要构建一个单体应用,可能需要提供更多的灵活性来访问你的
Secrets。静态元数据定义可能不足以满足需求。 -
Kubernetes 作为数据中心 API:事实证明,Kubernetes 的 API 足够简单,可以轻松适应多个使用场景,同时又足够强大,能够进行扩展。一个很好的例子是KubeVirt (
kubevirt.io/) 项目,它让你的 Kubernetes 集群能够管理和部署虚拟机。越来越多的工作负载开始使用自定义资源定义(CRDs)来存储配置信息,并且由于你永远不会将秘密存储在 CRD 中,因此你可能需要与SecretsAPI 进行交互,以访问你的秘密数据。 -
平台工程:越来越多的团队开始转向创建内部开发者平台(IDP)的理念,该平台提供一站式自服务访问,允许访问 Kubernetes 等服务。如果你的 IDP 是基于 Kubernetes 构建的,你可能需要与
SecretsAPI 进行交互。本书花了相当多的篇幅讨论 Kubernetes 中的身份问题,并且会经常提到 IdP。这里的“d”很重要,因为IdP是身份提供者,而IDP是内部开发者门户。
考虑到这些要点,你可能会发现自己需要直接与 Kubernetes API 交互以检索Secrets。好消息是,你在Secrets API 上的操作与任何其他 API 没什么不同。大多数 Kubernetes 客户端 SDK 甚至处理从Secret对象中解码 base64 数据的工作。
由于这不是一本编程书籍,而且与 API 交互有很多种方式,我们不会深入探讨任何 SDK 的具体内容。我们将提供一些具体的指导:
-
使用你的 Pod 身份:就像与外部 Vault 交互一样,使用 Pod 自身的身份与 API 服务器进行交互。不要使用硬编码的 Secret。
-
使用 SDK:这是一个很好的通用建议。是的,你可以通过直接的 RESTful 调用使用 Kubernetes API,但让 SDK 为你处理这项工作。它会让生活更轻松,并减少安全问题(谁没有在测试 HTTPS 调用时不小心记录了令牌呢?我说的是,除了我之外)。
-
在 CRD 中存储元数据:任何需要描述秘密的情况都应在 CRD 中进行。这为你提供了一个模式语言,借此你可以生成自己的 SDK。
虽然与Secrets API 进行交互比使用 Kubernetes 的多个抽象层来与秘密交互更困难,但它提供了其他常见抽象层所没有的巨大灵活性。接下来,我们将探讨直接与我们的 Vault API 交互是否能提供相同的好处。
使用 Vault API
每个 Vault 服务都有一个 API。这就是与其集成的 sidecar 如何与 Vault 进行交互,从而将秘密注入到你的工作负载中的方式。在前面的部分中,我们讲解了直接调用 Kubernetes Secrets API 的优点。那么,是否可以将相同的逻辑应用于 Vault API?直接调用 Vault API 或任何秘密管理 API 的理由与调用 Secrets API 相同。
直接调用 Vault API 的缺点是什么?主要的缺点是你现在将工作负载紧密绑定到一个特定的供应商或项目上。Kubernetes API 的一个好处是它在不同的实现中相对一致。虽然这并非总是如此,但至少在 Secret API 中是这样。
不幸的是,目前没有一个标准的秘密 API,未来也可能永远不会有。HashiCorp、AWS、微软、谷歌、VMWare 等公司都有自己关于如何管理秘密的想法,且没有太多动力去创建标准。语言绑定的集成也没有标准。例如,数据库领域通常有用于编程语言集成的常见标准,如 Java 中的 JDBC 标准。如果 Kubernetes 能够使 Secrets API 可插拔,那将是很棒的,但这永远不会发生。技术和非技术层面的复杂性使得 Kubernetes 不可能承担这样的任务。
话虽如此,在将工作负载直接集成到 Vault API 时,应该遵循与使用 Kubernetes Secrets API 时相同的建议。确保你使用了语言 SDK,并依赖于工作负载的身份。
总结
本章讲解了秘密管理的多个方面。我们首先讨论了秘密数据和更通用的配置数据之间的区别。我们考虑了为什么 Kubernetes 将 Secret 对象存储并表示为 base64 编码的文本,以及为什么不应将秘密数据存储在 git 中。还讨论了在 Kubernetes 集群中对秘密数据进行威胁建模。接下来,我们介绍了存储和管理秘密数据的各种方式,包括 Secret 对象、外部保险库、Sealed Secrets 和混合方法。最后,我们讲解了如何通过卷挂载、环境变量以及直接通过 API 将秘密集成到工作负载中。
完成本章后,你应该已经拥有足够的信息和示例来为你的集群构建自己的秘密管理策略。
在下一章中,我们将开始关注虚拟集群的多租户。
问题
-
合规性和安全性是同一回事。
-
正确
-
错误
-
-
Base64 是一种加密方式。
-
正确
-
错误
-
-
身份验证外部 Vault 的最佳方式是:
-
使用密码
-
使用
Pod的身份 -
使用每三个月更换一次的密码
-
-
在更新秘密时,哪种集成类型会在 pod 中自动更新?
-
带有环境变量的
Secret -
一个带有环境变量的外部保险库
-
一个带有卷的外部保险库
-
一个带有卷的
Secret
-
-
在讨论 IT 安全时,CIA 代表什么?
-
保密性、完整性、可用性
-
中央情报局
-
保密性、趣味性、可用性
-
美国烹饪学院
-
答案
-
b – 错误
-
b – 错误
-
c – 使用你的
Pod身份 -
d – 一个带有卷的
Secret -
a – 保密性、完整性、可用性
第九章:使用 vClusters 构建多租户集群
我们在前几章中提到了多租户,但这是第一章,我们将重点讨论 Kubernetes 中的多租户挑战,以及如何通过一种相对较新的技术“虚拟集群”来应对这些挑战。在本章中,我们将探索虚拟集群的使用案例,它们是如何实现的,如何以自动化的方式部署它们,以及如何通过Pod的身份与外部服务进行交互。最后,我们将通过构建和部署一个自助式多租户门户来结束本章内容。
本章将覆盖以下内容:
-
多租户的好处与挑战
-
使用 vClusters 作为租户
-
构建一个具有自助服务的多租户集群
技术要求
本章将涉及比前几章更大的工作量,因此需要一个更强大的集群。 本章有以下技术要求:
-
一台运行 Docker 的 Ubuntu 22.04+服务器,最低需要 8 GB 内存,建议 16 GB
-
来自
chapter9文件夹的脚本,可以通过访问本书的 GitHub 仓库获取:github.com/PacktPublishing/Kubernetes-An-Enterprise-Guide-Third-Edition
获取帮助
我们尽力测试所有内容,但有时我们的集成实验室中可能有六个或更多的系统。鉴于技术的不断变化,有时在我们的环境中能正常工作,而在你的环境中却不能。别担心,我们在这里为你提供帮助!请在我们的 GitHub 仓库上打开一个问题:github.com/PacktPublishing/Kubernetes-An-Enterprise-Guide-Third-Edition/issues,我们很乐意帮助你!
多租户的好处与挑战
在深入探讨虚拟集群和vCluster项目之前,让我们首先探讨一下是什么使得多租户 Kubernetes 如此有价值,却又如此难以实现。到目前为止,我们提到了多租户的挑战,但我们的重点一直是配置和构建单一集群。这是我们第一次直接解决多租户及其实现方法。本章的第一个主题是多租户 Kubernetes 是什么,以及为什么你应该考虑使用它。
探索多租户的好处
Kubernetes 通过 API 和数据库协调工作负载的资源分配。这些工作负载通常由需要特定计算资源和内存的 Linux 进程组成。在 Kubernetes 发展初期的五到六年里,主流的趋势是为每个“应用程序”分配一个专用集群。需要注意的是,当我们提到“应用程序”时,它可以指一个单体应用程序、一组微服务或几个相互关联的单体应用程序。这种方法明显效率低下,导致管理复杂性激增并可能浪费资源。为了更好地理解这一点,让我们来看看单个集群中的所有元素:
-
etcd 数据库:你应该始终拥有奇数个
etcd实例;至少需要三个etcd实例以保持高可用性。 -
控制平面节点:与
etcd类似,你至少需要两个控制平面节点,但为了高可用性,通常需要三个节点。 -
工作节点:无论基础设施上的负载如何,你至少需要两个节点。
控制平面需要资源,因此即使大多数 Kubernetes 发行版不再需要专门的控制平面节点,它仍然是额外的组件需要管理。看看你的工作节点,这些节点的利用率是多少?如果你有一个高负载的系统,你将能充分利用硬件,但你的所有应用程序是否总是如此高负载?让多个“应用程序”共享单一基础设施可以大大降低硬件利用率,减少所需集群数量,无论你是在预付费基础设施还是按需付费基础设施上运行。过度配置资源将增加功耗、冷却需求、机架空间等,这些都会增加额外的成本。更少的服务器还意味着减少硬件的维护需求,进一步降低成本。
除了硬件成本,集群每应用程序的方法还带来了人力成本。Kubernetes 技能非常昂贵且难以在市场上找到。这可能就是你正在阅读这本书的原因!如果每个应用程序组都需要维护自己的 Kubernetes 专业知识,那么就意味着要重复获取那些难以获得的技能,也增加了成本。通过集中基础设施资源,可以建立一个专门负责 Kubernetes 部署的集中团队,消除其他团队(其主要关注点不在基础设施开发)重复这些技能的必要性。
多租户还有重要的安全优势。通过共享基础设施,更容易集中执行安全要求。以身份验证为例,如果每个应用都有自己的集群,如何执行统一的身份验证要求?如何以自动化的方式添加新集群?如果你使用 OIDC,是不是要为每个集群集成一个身份提供者?
集中基础设施的另一个好处是密钥管理。如果你有一个集中式 Vault 部署,你是否希望为每个应用集成一个新集群?在第八章中,我们将一个集群与 Vault 集成;如果你有大量集群扩展,相同的集成需要在每个单独的集群上完成——这如何实现自动化和管理?
迁移到多租户架构可以通过减少运行工作负载所需的基础设施数量,从而降低长期成本。它还减少了管理基础设施所需的管理员人数,并使得集中安全和策略执行变得更加容易。
虽然多租户提供了显著的优势,但它也带来了一些挑战。接下来,我们将探讨实现多租户 Kubernetes 集群的挑战。
多租户 Kubernetes 的挑战
我们探讨了多租户 Kubernetes 的好处,尽管多租户的采用正在增长,但为什么它不像每应用一个集群的方法那样普遍?
创建一个多租户 Kubernetes 集群需要考虑安全性、可用性和管理等多个方面。实施这些挑战的解决方案通常是非常具体的,并且需要与第三方工具集成,这些工具往往不在大多数发行版的范围内。
大多数企业在管理孤岛的基础上增加了额外的复杂性。应用程序所有者根据自己的标准和管理进行评判。任何收入与某些标准挂钩的人都会想确保尽可能多地控制这些标准,即使这是出于错误的原因。这种孤岛效应可能对任何没有给予应用程序所有者适当控制的集中化努力产生负面影响。由于这些孤岛对每个企业来说都是独特的,因此单一发行版无法以易于推广的方式考虑这些问题。与其处理这些额外的复杂性,供应商更容易推销每应用一个集群的方法。
鉴于市场上很少有多租户 Kubernetes 发行版,下一个问题是:“使一个通用的 Kubernetes 集群支持多租户有哪些挑战?”
我们将通过影响的角度来分析这个问题的答案:
-
安全性:大多数容器实际上只是 Linux 进程。我们将在第十二章,使用 Gatekeeper 进行节点安全中详细讨论这一点。目前需要理解的是,主机上的进程之间几乎没有安全隔离。如果你在运行来自多个应用程序的进程,你需要确保一个突破不会影响其他进程。
-
容器突破:尽管这对任何集群都很重要,但在多租户集群中这是必不可少的。我们将在第十三章,KubeArmor 保护你的运行时中讨论如何保护我们的容器运行时。
-
影响:任何集中式基础设施的问题都会对多个应用程序产生不利影响。这通常被称为“爆炸半径”。如果一次升级出现问题或失败,谁会受到影响?如果发生容器突破,谁会受到影响?
-
资源瓶颈:虽然集中式基础设施确实可以更好地利用资源,但它也可能会造成瓶颈。你多快能将一个新租户加入进来?应用程序所有者在自己的租户中有多少控制权?授予或撤销访问权限有多困难?如果你的多租户解决方案跟不上应用程序所有者的步伐,应用程序所有者将自行承担基础设施。这将导致资源浪费、配置漂移以及所有集群的报告和审计困难。
-
限制:一个过于限制的集中式平台会导致应用程序所有者选择维护自己的基础设施或将基础设施外包给第三方解决方案。这是任何集中式服务中最常见的问题之一,最能说明这一点的就是平台即服务(PaaS)实现的兴衰,这些实现未能提供应用工作负载所需的灵活性。
尽管这些问题可以应用于任何集中式服务,但它们对 Kubernetes 确实有一些独特的影响。例如,如果每个应用程序都有自己的命名空间,那么应用程序如何在该命名空间内正确划分不同的逻辑功能?是否应该为每个应用程序授予多个命名空间?
Kubernetes 集群设计的另一个重大影响是自定义资源定义(CRDs)的部署和管理。CRDs 是集群级别的对象,几乎不可能在同一个集群中运行多个版本;正如我们在前面的章节中指出的,CRDs 作为存储配置数据的一种方式越来越受欢迎。多租户集群可能会遇到版本冲突,需要进行管理。
这些挑战将在后续章节中得到解决,但在本章中,我们将重点讨论两个方面:
-
租户边界:租户的范围是什么?租户在该边界内拥有多少控制权?
-
自助服务:集中式 Kubernetes 服务如何与用户交互?
我们将通过向集群中添加两个组件来解决这两个方面的问题。OpenUnison 已经被引入以处理身份验证,并将扩展其自服务功能,提供命名空间即服务(namespace as a Service)。另一个外部系统是 Loft Labs 提供的 vCluster。
由于我们已经使用 OpenUnison 演示了命名空间即服务,接下来我们可以继续探讨其他挑战,例如使用 vCluster 项目处理 CRD 版本控制问题。
为租户使用 vCluster
在 KinD 章节中,我们解释了 KinD 是如何嵌套在 Docker 中为我们提供完整的 Kubernetes 集群的。我们将其与套娃进行比较,组件嵌入到其他组件中,这可能会让刚接触容器和 Kubernetes 的用户感到困惑。vCluster 是一个类似的概念——它在主宿主集群中创建一个虚拟集群,虽然它看起来是一个标准的 Kubernetes 集群,但它实际上是嵌套在宿主集群中的。在阅读本章其余部分时,请记住这一点。
在上一节中,我们讲解了多租户的好处与挑战,以及这些挑战如何影响 Kubernetes。本节将介绍 Loft Labs 的 vCluster 项目,它允许你在一个无特权的命名空间内运行 Kubernetes 控制平面。这使得每个租户可以获得自己的“虚拟”Kubernetes 基础设施,完全控制而不会影响其他租户的实现或“主集群”中的其他工作负载。

图 9.1:vCluster 的逻辑布局
在上面的图示中,每个租户都获得自己的命名空间,其中运行一个 vCluster。vCluster 是由三部分组成的:
-
数据库:用于存储 vCluster 内部信息的地方。这可以是
etcd或关系型数据库,具体取决于你部署 vCluster 的集群类型。 -
API 服务器:vCluster 包含一个自己的 API 服务器,供其 Pod 与之交互。这个 API 服务器由 vCluster 管理的数据库支持。
-
同步引擎:虽然 vCluster 有自己的 API 服务器,但其所有 Pod 都运行在宿主集群中。为了实现这一点,vCluster 在宿主集群和 vCluster 之间同步某些对象。我们将在接下来的部分详细介绍这一点。
vCluster 方法的好处在于,从 Pod 的视角来看,它是在一个私有集群内工作,尽管它实际上运行在一个主宿主集群中。租户可以将自己的集群划分为适合的命名空间,并根据需要部署 CRD 或操作器。

图 9.2:Pod 视角下的 vCluster
在上面的示意图中,我们看到 Pod 部署到租户的命名空间中,但它并不是与宿主集群的 API 服务器通信,而是与 vCluster 的 API 服务器通信。这是因为从 vCluster 同步到宿主集群的 Pod 定义,其环境变量和 DNS 被覆盖,指示 Pod 将宿主的 kubernetes.default.svc 指向 vCluster,而不是宿主集群的 API 服务器。
由于 Pod 运行在宿主集群中,而 vCluster 也运行在宿主集群中,因此所有的 Pods 都受到命名空间中设置的 ResourceQuotas 的约束。这意味着任何部署到 vCluster 中的 Pod 都受制于与直接部署到命名空间中的 Pod 相同的规则,包括由配额、策略或其他准入控制器创建的限制。在第十一章,使用 Open Policy Agent 扩展安全性,你将学习如何使用准入控制器在集群中强制执行策略。由于 Pod 运行在宿主集群中,你只需将这些策略应用到宿主集群上。这大大简化了我们的安全实施,因为现在可以将 cluster-admin 访问权限授予虚拟集群中的租户,而不会危及宿主集群的安全性。
另一个重要的说明是,宿主集群负责运行你的 Pod,因此它也负责所有 vCluster 的 Ingress 流量。你不必在每个 vCluster 上重新部署 Ingress 控制器——它们共享宿主的 Ingress 控制器,减少了维护额外的 Ingress 部署或为每个 vCluster 创建多个通配符域名的需要。
现在,我们基本理解了什么是 vCluster,以及它如何帮助解决 Kubernetes 中多租户的一些挑战,接下来的步骤是部署一个 vCluster 并查看其内部工作原理。
部署 vClusters
在上一节中,我们关注了 vCluster 的工作原理及其实现方式。在本节中,我们将部署一个 vCluster 和一个简单的工作负载,以便查看运行在 vCluster 中的 Pod 与部署到宿主集群中的 Pod 之间发生了什么变化。
第一步是创建一个新的集群——我们将删除现有的 KinD 集群并部署一个新的集群。然后,我们将在 chapter9 目录中执行一个名为 deploy_vcluster_cli.sh 的脚本:
$ kind delete cluster -n cluster01
$ cd chapter2
$ ./create-cluster.sh
$ cd ../chapter9
$ ./deploy_vcluster_cli.sh
此时,我们已经有了一个新的集群和部署 vCluster 的 CLI 工具。
下一步是创建一个 vCluster。首先,我们将创建一个名为 tenant1 的新 namespace,然后使用 vCluster 工具在新命名空间中创建一个名为 myvcluster 的新 vCluster:
$ kubectl create ns tenant1
$ vcluster create myvcluster --distro k3s -n tenant1
一旦 vcluster 命令完成,我们就会拥有一个正在运行的 vCluster,它是使用 k3s 构建的,并在宿主集群中运行。从逻辑上讲,它是一个独立的集群,我们可以通过 vCluster 的 kubeconfig 或 vcluster 工具直接“连接”到它。
vCluster 旨在支持多个 Kubernetes 集群实现。默认且最常见的是 k3s,这是一种 Kubernetes 实现,它用关系数据库替代了 etcd,并将多个二进制文件替换为一个单一的二进制文件。它最初是为边缘部署而开发的,但对于单租户环境也非常适用。我们可以使用 Mirantis 的 k0s 或甚至一个原生 Kubernetes,但 k3s 对于大多数情况来说表现很好。
让我们断开连接,看看在我们的主机上运行了什么:
$ vcluster disconnect
info Successfully disconnected from vcluster: myvcluster and switched back to the original context: kind-cluster01
$ kubectl get pods -n tenant1
NAME READY STATUS RESTARTS AGE
coredns-864d4658cb-mdcx5-x-kube-system-x-myvcluster 1/1 Running 0 4m45s
myvcluster-0 2/2 Running 0 5m12s
如上面的输出所示,在 tenant1 命名空间下有两个 pod 正在运行:CoreDNS 和我们的 vCluster。如果我们查看我们命名空间中的服务,你会看到一个类似下面的服务列表:
$ kubectl get svc -n tenant1
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kube-dns-x-kube-system-x-myvcluster ClusterIP 10.96.142.24 <none> 53/UDP,53/TCP,9153/TCP 7m18s
myvcluster NodePort 10.96.237.247 <none> 443:32489/TCP,10250:31188/TCP 7m45s
myvcluster-headless ClusterIP None <none> 443/TCP 7m45s
myvcluster-node-cluster01-worker ClusterIP 10.96.209.122 <none> 10250/TCP 7m18s
有几个服务已配置指向我们 vCluster 的 API 服务器和 DNS 服务器,从而提供对 vCluster 的访问,使其在逻辑上看起来像是一个“完整”的标准集群。
现在让我们连接到我们的 vCluster 并部署一个 pod。在 chapter9/simple 目录下,我们有一个 pod 清单,将用于我们的示例。首先,我们将连接到集群并使用 kubectl 在 chapter9/simple 目录下部署示例 pod:
$ vcluster connect myvcluster -n tenant1
$ cd chapter9/simple
$ kubectl create -f ./virtual-pod.yaml
$ kubectl logs -f virtual-pod
Wed Sep 20 17:50:03 UTC 2023
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_SERVICE_PORT=443
HOSTNAME=virtual-pod
PWD=/
HOME=/root
KUBERNETES_PORT_443_TCP=tcp://10.96.237.247:443
SHLVL=1
KUBERNETES_PORT_443_TCP_PROTO=tcp
**KUBERNETES_PORT_443_TCP_ADDR=10.96.237.247**
KUBERNETES_SERVICE_HOST=10.96.237.247
KUBERNETES_PORT=tcp://10.96.237.247:443
KUBERNETES_PORT_443_TCP_PORT=443
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
_=/usr/bin/env
search default.svc.cluster.local svc.cluster.local cluster.local
**nameserver 10.96.142.24**
请注意,Pod 的环境变量使用 10.96.237.247 作为 API 服务器的 IP 地址;这是在主机上运行的 mycluster 服务的 ClusterIP。此外,名字服务器是 10.96.142.24,这是我们 vCluster 的 kube-dns 服务 在主机上的 ClusterIP。就 pod 而言,它认为自己是在 vCluster 内部运行的。它对主机集群一无所知。接下来,断开与 vCluster 的连接,并查看我们主机集群中 tenant1 命名空间下的 pods:
$ vcluster disconnect
$ kubectl get pods -n tenant1
NAME READY STATUS RESTARTS AGE
coredns-864d4658cb-mdcx5-x-kube-system-x-myvcluster 1/1 Running 0 22m
myvcluster-0 2/2 Running 0 22m
**virtual-pod-x-default-x-myvcluster 1/1 Running 0 7m11s**
请注意,您的 vCluster 的 pod 正在您的主机集群中运行。主机上 Pod 的名称包括 pod 的名称和来自 vCluster 的命名空间。让我们来看一下 pod 的定义。我们不会在这里放入所有输出,因为它会占用多页。我们想指出的是,除了我们的原始定义外,pod 还包括一个硬编码的 env 部分:
env:
- name: KUBERNETES_PORT
value: tcp://10.96.237.247:443
- name: KUBERNETES_PORT_443_TCP
value: tcp://10.96.237.247:443
- name: KUBERNETES_PORT_443_TCP_ADDR
value: 10.96.237.247
- name: KUBERNETES_PORT_443_TCP_PORT
value: "443"
- name: KUBERNETES_PORT_443_TCP_PROTO
value: tcp
- name: KUBERNETES_SERVICE_HOST
value: 10.96.237.247
- name: KUBERNETES_SERVICE_PORT
value: "443"
- name: KUBERNETES_SERVICE_PORT_HTTPS
value: "443"
它还包括自己的 hostAliases:
hostAliases:
- hostnames:
- kubernetes
- kubernetes.default
- kubernetes.default.svc
ip: 10.96.237.247
因此,尽管 Pod 正在我们的主机集群中运行,但所有告诉 pod 它在哪里运行的内容都指向了我们的 vCluster。
在这一部分,我们启动了我们的第一个 vCluster 和一个在该 vCluster 中运行的 pod,以观察它如何被改变以便在我们的主机集群中运行。在下一部分,我们将研究如何访问我们的 vCluster,同时保持与主机集群相同的企业安全要求。
安全访问 vClusters
在上一部分,我们部署了一个简单的 vCluster,并使用 vcluster connect 命令访问了 vCluster。该命令首先创建一个端口转发到 vCluster 的 API 服务器 Service,然后将一个带有主证书的上下文添加到我们的 kubectl 配置文件中,这类似于我们的 KinD 集群。
我们在第六章,将认证集成到你的集群中,花费了大量篇幅讲解了为什么这是一个反模式,这些理由同样适用于虚拟集群(vClusters)。你仍然需要将企业认证集成到你的虚拟集群中。让我们看看两种方法:
-
去中心化:你可以将认证留给集群所有者处理。这会消解多租户的一些优点,并且要求每个集群都当作一个新的集成来接入企业的身份系统。
-
集中化:如果你在主机集群中托管身份提供者(IdP),你可以将每个虚拟集群与该 IdP 关联,而不是直接连接到集中式身份存储。除了提供集中认证外,这种方法还使得自动化新虚拟集群的加入变得更容易,并且限制了需要存储在虚拟集群中的凭证等敏感信息的数量。
在多租户环境中,选择是明确的;你的主机集群也应该承载中央认证功能。
关于虚拟集群访问,接下来需要理解的是到虚拟集群的网络路径。vcluster命令会创建一个本地的端口转发到你的 API 服务器。这意味着每当用户想要使用他们的 API 服务器时,都需要设置端口转发到 API 服务器。这并不是一个理想的用户体验(UX),并且容易出错。最好是直接连接到我们虚拟集群的 API 服务器,就像我们为任何标准 Kubernetes 集群所做的一样。设置直接网络访问到虚拟集群的 API 服务器的挑战在于,尽管它是NodePort,但节点通常不会直接暴露到网络上。它们通常会被负载均衡器包围,并依赖Ingress控制器来提供对集群资源的访问。
答案是使用我们主机集群已经为虚拟集群提供的应用基础设施。
在第六章,将认证集成到你的集群中,我们讨论了如何在云管理的集群中使用代理伪装。相同的场景也可以应用于虚拟集群。虽然你可以配置k3s使用 OIDC 进行认证,但使用代理伪装大大简化了网络管理,因为我们不需要创建新的负载均衡器或基础设施来支持我们的虚拟集群。

图 9.3:带认证的虚拟集群
在上面的图示中,我们可以看到网络和身份验证如何在我们的主集群中结合。主集群将拥有一个 OpenUnison,用于验证用户身份并连接到我们的 Active Directory。我们的 vCluster 将有自己的 OpenUnison,并与主集群的 OpenUnison 建立信任关系。vCluster 将使用 kube-oidc-proxy 将 OpenUnison 的身份验证令牌转换为模拟头,以便传递给 vCluster 的 API 服务器。这种方式为我们提供了一个集中式的身份验证和网络系统,同时也使得 vCluster 所有者能够轻松地整合自己的管理应用程序,而无需主集群团队的介入。像ArgoCD和Grafana这样的本地集群管理应用程序都可以集成到 vCluster 的 OpenUnison 中,而不是主集群的 OpenUnison 中。
为了展示我们的设置如何运行,首先需要更新我们的 vCluster,以便它能够通过 vcluster 工具将Ingress对象从 vCluster 同步到主集群。在chapter/host目录中,我们有一个名为vcluster-values.yaml的更新值文件;我们将使用此值文件来升级tenant1命名空间中的 vCluster:
$ cd chapter9/host
$ vcluster create myvcluster --upgrade -f ./vcluster-values.yaml -n tenant1
这个命令将更新我们的 vCluster,以便将我们在 vCluster 中创建的Ingress对象同步到主集群中。接下来,我们需要在主集群中运行 OpenUnison:
$ vcluster disconnect
$ ./deploy_openunison_imp_impersonation.sh
在部署之前,我们需要确保我们是在主集群上运行,而不是在 vCluster 上。我们刚才运行的脚本与第六章中的内容相似,将身份验证集成到集群中;它将把“Active Directory”和 OpenUnison 部署到 vCluster 中。一旦 OpenUnison 被部署,最后一步是运行 OpenUnison 的卫星部署过程:
$ vcluster disconnect
$ ./deploy_openunison_vcluster.sh
这个脚本类似于主集群的部署脚本,但有一些关键的不同之处:
-
我们 OpenUnison 的值不包含任何身份验证信息。
-
我们 OpenUnison 的值与主集群有不同的集群名称。
-
脚本并未运行
ouctl install-auth-portal,而是运行ouctl install-satelite,该命令设置 OpenUnison 在卫星集群和主集群之间使用 OIDC。这个命令为我们创建了values.yaml文件中的oidc部分。
脚本执行完毕后,您可以像在第六章中一样登录 OpenUnison。在浏览器中,访问https://k8sou.apps.X-X-X-X.nip.io,其中 X-X-X-X 是您集群的 IP 地址,但使用破折号代替点。由于我们的集群位于192.168.2.82,我们使用https://k8sou.apps.192-168-2-82.nip.io/。登录时,请使用用户mmosley和密码start123。
登录后,您会看到现在有一个树形结构,其中包含主集群和tenant1的选项。您可以点击tenant1,然后点击tenant1 Tokens标签。

图 9.4:OpenUnison 门户页面
新页面加载后,您可以获取您的kubectl配置并将其粘贴到终端中:

图 9.5:OpenUnison kubectl 配置生成器
根据您正在使用的客户端,您可以将此命令粘贴到 Windows 或 Linux/macOS 终端中,并开始使用您的 vCluster,而无需分发vcluster CLI 工具,并且可以使用您企业的身份验证要求。
在本节中,我们探讨了如何将企业身份验证集成到我们的 vCluster 中,以及如何为我们的 vCluster 提供一致的网络访问。在下一节中,我们将探讨如何将我们的 vCluster 与外部服务(如 HashiCorp 的 Vault)集成。
从 vCluster 访问外部服务
在上一章中,我们将 HashiCorp Vault 实例集成到我们的集群中。我们的 Pods 使用投影到 Pod 中的令牌与 Vault 进行通信,使我们能够在没有共享密钥或令牌的情况下进行 Vault 身份验证,并使用短期令牌。依赖短期令牌减少了被泄露的令牌被用于攻击集群的风险。
与 Vault 一起使用的基于 Pod 的身份在 vCluster 中变得更加复杂,因为用于创建Pod令牌的密钥是 vCluster 特有的。此外,Vault 需要了解每个 vCluster,以便验证在 vCluster 中使用的投影令牌。
如果我们在宿主集群中运行自己的 Vault,我们可以自动化入驻过程,以便每个新的 vCluster 都能作为独立集群注册到 Vault 中。这种方法的挑战在于 Vault 是一个复杂的系统,通常由专门的团队运行,并有自己的入驻流程。以适合拥有 Vault 的团队的方式添加新 vCluster,可能并不像调用一些 API 那么简单。因此,在我们实施将 vCluster 集成到 Vault 的策略之前,需要先了解 vCluster 如何处理身份。
运行在 vCluster 中的 Pod 有两个不同的身份:
-
vCluster 身份:从 vCluster 投影到我们 Pod 中的令牌作用范围仅限于 vCluster 的 API 服务器。该令牌由宿主集群无法识别的唯一密钥签名。它与 Pod 在 vCluster 的 API 服务器内运行时所使用的
ServiceAccount相关联。 -
宿主集群身份:尽管 Pod 在 vCluster 中定义,但它是在宿主集群中执行的。这意味着 Pod 的安全上下文将以宿主集群为基础运行,并且需要与 vCluster 不同的身份。它将拥有自己的名称和签名密钥。
如果我们检查从 vCluster 同步到宿主集群的 Pod,我们会看到一个注解,其中包含一个令牌:
vcluster.loft.sh/token-ejijuegk: >-
eyJhbGciOiJSUzI1NiIsImtpZCI6IkVOZDVhZnEzUzdtLXBSR2JUM3RJUkRHM0FqWkhzQV9KSkNZcm8yMHdNVUUifQ…
这个令牌是通过 Pod 中后续的 fieldPath 配置注入到我们的 Pod 中的。这可能是一个安全问题,因为任何记录 Pod 创建的日志(如审计日志)现在都可能泄露令牌。vCluster 项目有一个配置,用于在主集群中为项目令牌生成 Secret 对象,这样它们就不会出现在 Pod 清单中。将以下内容添加到我们的 values.yaml 文件中将解决这个问题:
syncer:
extraArgs:
- --service-account-token-secrets=true
完成这些操作后,让我们更新集群并重新部署所有 Pods:
$ vcluster disconnect
$ vcluster create myvcluster --upgrade -f ./vcluster-values-secrets.yaml -n tenant1
$ kubectl delete pods --all --all-namespaces --force
一会儿,vCluster 中的 Pod 将恢复。检查 Pod 后,我们看到令牌不再出现在 Pod 的清单中,而是被挂载到了主机中的 Secret 上。这无疑是一个改进,因为审计系统通常会对记录 Secrets 的内容更加谨慎。接下来,让我们检查一下令牌的声明。
如果我们检查这个令牌,我们会看到一些问题:
{
"aud": [
"https://kubernetes.default.svc.cluster.local"
],
**"exp": 2010881742,**
**"iat": 1695521742,**
"iss": "https://kubernetes.default.svc.cluster.local",
"kubernetes.io": {
"namespace": "openunison",
"pod": {
"name": "openunison-orchestra-d7bc468bc-qhpts",
"uid": "fb6874f7-c01c-4ede-9062-ce5409509200"
},
"serviceaccount": {
"name": "openunison-orchestra",
"uid": "3d9c9147-d0e0-43f6-9e4d-a0d7db0c6b8c"
}
},
"nbf": 1695521742,
"sub": "system:serviceaccount:openunison:openunison-orchestra"
}
exp 和 iat 声明是加粗的,因为当你将其从 Unix 纪元时间转换为人类可以理解的时间时,该令牌有效的时间是从2023 年 9 月 24 日星期天凌晨 2:15:42到2033 年 9 月 21 日星期三凌晨 2:15:42。这可是一个十年的令牌!这忽略了令牌在 Pod 中被配置为只在十分钟内有效的事实。这是 vCluster 中的已知问题。好消息是,令牌本身是投影的,因此当它们投影到的 Pod 死亡时,API 服务器将不再接受这些令牌。
vCluster 令牌长度的问题出现在访问外部服务时,因为这对我们生成的任何令牌都适用,不仅仅是 vCluster API 服务器的令牌。当我们在上一章中将集群集成到 Vault 时,我们使用了 Pod 的身份,这样我们就能利用短期有效且不静态的令牌,并且它们有明确的过期时间。十年的令牌实际上是一个没有过期时间的令牌。主要的缓解措施是,我们配置了 Vault 来验证令牌的状态,在接受之前检查令牌是否有效,因此绑定到已销毁 Pod 的令牌将被 Vault 拒绝。
使用 vCluster 注入的身份的替代方法是利用主集群注入的身份。这个身份将受到与主集群中 TokenRequest API 生成的其他身份相同的规则约束。采用这种方法有两个问题:
-
vCluster 禁用主机令牌:在主机同步的 Pod 中,
automountServiceAccountToken为 false。这是为了防止 vCluster 和主集群之间发生冲突,因为我们的 Pod 不应该知道主集群的存在!我们可以通过创建一个变异 webhook 来解决这个问题,它将在主集群中添加一个TokenRequestAPI 投影,供我们的 Pod 访问。 -
主机令牌没有 vCluster 命名空间:当我们为已同步的 pod 生成主机令牌时,命名空间将嵌入在
ServiceAccount的名称中,而不是作为令牌中的声明。这意味着大多数外部服务的策略语言将无法基于主机令牌接受策略,而是通过命名空间配置,无需为每个 vCluster 命名空间创建新策略。
这两种方法都有优缺点。使用 vCluster 令牌的最大好处是,你可以轻松创建一个策略,允许你基于 vCluster 内的命名空间来限制对机密的访问,而无需为每个命名空间创建新的策略。缺点是 vCluster 令牌存在问题,并且现在你需要将每个单独的 vCluster 纳入到 Vault 中。使用主机令牌可以更好地缓解 vCluster 令牌的问题,但你无法轻松为 Vault 中的每个 vCluster 创建通用策略。
在本节中,我们花时间了解了 vCluster 如何管理 pod 身份,以及这些身份如何用于与外部服务(如 Vault)进行交互。在下一节中,我们将讨论创建高可用 vCluster 和管理操作所需的内容。
创建和操作高可用 vClusters
到目前为止,本章我们已经专注于 vCluster 如何工作、如何安全地访问 vCluster,以及 vCluster 如何处理 pod 身份以与外部系统交互。在本节中,我们将专注于如何部署和管理高可用 vClusters。许多 vCluster 的文档和示例都将其作为开发或测试工具来讨论。对于本章前面讨论的用例,我们希望专注于创建能够运行生产工作负载的 vClusters。构建生产就绪 vClusters 的第一步是理解如何以允许个别组件发生故障或停机的方式运行 vCluster,而不会影响 vCluster 的运行能力。
理解 vCluster 高可用性
让我们定义一个高可用 vCluster 的目标以及我们当前部署与该目标之间的差距。当你拥有一个高可用 vCluster 时,你需要确保:
-
在升级或迁移到另一个物理节点时,你可以继续与 API 服务器交互,无论是在主机集群还是 vCluster 中。
-
如果发生灾难性问题,你可以从备份中恢复。
在运行 vCluster 时,关于能够与 API 服务器交互的第一个要点会在升级主机集群节点时变得清晰。在升级过程中,你希望 API 服务器能够继续运行。你希望能够继续从虚拟 API 服务器同步对象到主机;你还希望与 API 服务器交互的 pod 在主机升级时仍然能够正常工作。例如,如果你使用的是 OpenUnison,那么你希望它能够创建会话对象,以便用户可以在主机集群操作进行时与他们的 vCluster 交互。
灾难恢复的第二个要点也非常重要。我们希望永远不需要它,但如果我们的 vCluster 被不可逆地损坏,怎么办?我们能否恢复到一个已知是正常的状态?
了解如何运行高可用 vCluster 的第一个方面是,它将需要多个实例的 pod 来运行 API 服务器、同步器和 CoreDNS。如果我们查看 tenant1 命名空间,我们会看到 vCluster 中有一个 pod 与一个 StatefulSet 关联,该 StatefulSet 托管 vCluster 的 API 服务器和同步器。还有一个 pod 是从 vCluster 内部同步的,用于 CoreDNS。我们希望每个 pod 至少有两个实例(最好是三个),这样我们就可以告诉 API 服务器使用 PodDisruptionBudget,确保我们有最少数量的实例运行,以便在发生某些事件时,可以停掉其中一个实例。
第二个要理解的方面是 vCluster 如何管理数据。我们当前的部署使用了 k3s,它使用本地 SQLite 数据库,并将数据存储在 PersistentVolume 上。这对于开发来说效果不错,但对于生产集群,我们希望每个 vCluster 组件都能使用相同的数据。对于基于 k3s 的 vCluster,这意味着需要使用支持的关系型数据库或 etcd。我们可以部署 etcd,但关系型数据库通常更易于管理。我们将在集群内部署数据库,但使用外部数据库也是完全可行的。在我们的练习中,我们将使用 MySQL。我们不会为示例构建高可用数据库,因为每个数据库都有其自身的高可用性机制。不过,如果这是生产部署,你需要确保数据库是按照项目推荐的高可用部署方式构建的,并且有定期的备份和恢复计划。话虽如此,让我们从拆除当前集群并创建新集群开始:
$ kind delete cluster -n cluster01
$ cd chapter2/HAdemo
$ ./create-multinode.sh
等待新的多节点集群启动完成。一旦它运行起来,部署 MySQL:
$ cd ../../chapter9/ha
$ ./deploy_mysql.sh
如果 deploy_mysql.sh 脚本因“无法通过套接字连接到本地 MySQL 服务器”而失败,请稍等片刻然后重新运行。重新运行是安全的。该脚本:
-
使用自签名的
ClusterIssuers部署 cert-manager 项目。 -
为 MySQL 创建 TLS 密钥对。
-
安装 MySQL 作为
StatefulSet并配置它以接受 TLS 认证。 -
为我们的集群创建一个数据库,并配置一个用户通过 TLS 认证进行身份验证。
在部署并配置好 MySQL 的 TLS 认证后,我们接下来将创建tenant命名空间,并创建一个证书来映射到我们的数据库用户:
$ kubectl create -f ./vcluster-tenant1.yaml
最后,我们可以部署我们的 vCluster:
$ vcluster create tenant1 --distro k3s --upgrade -f ./vcluster-ha-tenant1-vaules.yaml -n tenant1 --connect=false
这将花费几分钟,但你将在tenant1命名空间中拥有四个 Pod:
NAME READY STATUS RESTARTS AGE
coredns-6ccdd78696-r5kmd-x-kube-system-x-tenant1 1/1 Running 0 88s
coredns-6ccdd78696-rw9lv-x-kube-system-x-tenant1 1/1 Running 0 88s
tenant1-0 2/2 Running 0 2m16s
tenant1-1 0 2m16s
现在我们可以利用PodDisruptionBudget来告诉 Kubernetes,在升级过程中保持一个 vCluster Pod 持续运行。
说到升级,下一个问题是如何升级我们的 vCluster。现在我们有了一个高可用性的 vCluster,我们可以考虑将 vCluster 升级到新版本。
升级 vCluster
你需要了解如何升级你的 vCluster。你要确保你的 vCluster 和主集群不会分离得太远。虽然同步到主集群中的 Pod 将与 vCluster 的 API 服务器进行通信,但对同步 Pod(和其他同步对象)的任何影响都可能影响你的工作负载。
鉴于保持最新状态的重要性,值得报告的是,升级 vCluster 非常简单。重要的是要记住,vCluster 负责编排集群并同步对象,但集群本身是由它们自己的实现进行管理的。在我们的部署中,我们使用k3s,它会在部署新 Pod 时升级数据库中的数据存储。由于vcluster create命令是 Helm 的一个包装器,我们只需要用新镜像更新我们的值并重新部署:
$ kubectl get pod tenant1-0 -n tenant1 -o json | jq -r '.spec.initContainers[0].image'
rancher/k3s:v1.29.5-k3s1
$ vcluster create tenant1 --upgrade -f ./vcluster-ha-tenant1-vaules-upgrade.yaml -n tenant1 --connect=false
该命令将我们的 vCluster 升级为使用k3s 1.30镜像,实际上就是对我们安装的 Helm 图表执行升级。你正在利用 Kubernetes 的强大功能来简化升级!运行完成后,你可以检查 Pod 是否已经在运行k3s 1.30:
$ kubectl get pod tenant1-0 -n tenant1 -o json | jq -r '.spec.initContainers[0].image'
rancher/k3s:v1.30.1-k3s1
我们已经涵盖了如何创建高可用集群以及如何升级 vCluster。这些内容足以开始构建一个多租户集群。在下一节中,我们将整合所学内容,构建一个每个租户都有自己 vCluster 的多租户集群。
使用自助服务构建多租户集群
在前面的章节中,我们探讨了多租户如何工作,vCluster 项目如何帮助解决多租户挑战,以及如何配置具有安全访问和高可用性的 vCluster。每个独立的组件都作为一个单独的部分进行讨论。接下来的问题是如何将这些组件整合成一个单一的服务。在本节中,我们将演示如何为多租户集群创建一个自助平台。
多租户的一个最重要方面是可重复性。你能否以一致的方式创建每个租户?除了确保方法是可重复的外,客户需要花费多少工作来获取一个新租户?记住,本书聚焦于企业,企业几乎总是有合规性要求。你还需要考虑如何将合规性要求整合到入驻过程当中。
对可重复性和合规性的需求通常会导致需要为新租户提供自助服务门户。创建自助服务门户已经成为许多项目的重点,通常作为“平台工程”计划的一部分。我们将从 OpenUnison 的命名空间作为服务门户来构建我们的自助服务平台。以 OpenUnison 作为起点,可以让我们专注于组件如何集成,而不是深入编写集成代码的具体细节。这个多租户自助入驻门户将作为起点,随着我们在本书中探索更多多租户方面的内容,逐步完善。
我们将首先通过定义需求来接近我们的多租户集群,然后分析每个需求如何得到满足,最后推出我们的集群和门户。完成本节后,你将拥有一个可以构建的多租户平台的起始框架。
分析需求
我们对每个租户的要求将类似于物理集群的要求。我们将希望:
-
通过授权隔离租户:谁应该有权限更新每个租户?什么驱动了访问权限?到目前为止,我们主要关注集群管理员,但现在我们需要关注租户管理员。
-
强制企业身份验证:当开发者或管理员访问租户时,我们需要确保使用的是企业身份验证。
-
外部化秘密:我们要确保秘密数据的真实来源位于集群之外,这样可以让我们的安全团队更容易进行审计。
-
高可用性与灾难恢复:在某些情况下,我们需要关注租户的 API 服务器是否在运行。我们需要依赖 Kubernetes 确保即使在这些情况下,租户也能继续进行工作。
-
传输中的加密:所有组件之间的连接都需要加密。
-
Helm Charts 中无秘密:将秘密数据保存在图表中意味着它作为
Secret存储在我们的命名空间中,从而违反了外部化秘密数据的要求。
我们在本章中已经处理了大多数这些需求。关键问题是,“我们如何将一切整合并自动化?”通过阅读本章并查看脚本,你应该能大致看到这个实现的方向。就像任何企业项目一样,我们需要了解孤岛效应如何影响我们的实施。对于我们的平台,我们假设:
-
Active Directory 不能自动更新:通常情况下,你不会被赋予通过 API 在 Active Directory(AD)中创建自己组的权限。尽管与 AD 的交互只需要 LDAP 功能,但合规性要求通常规定必须遵循正式流程来创建组并将成员添加到这些组中。
-
Vault 可以自动化:由于 Vault 已启用 API,并且我们与 Vault 团队有良好的关系,他们将允许我们直接自动化新租户的入驻过程。
-
集群内部通信不需要企业 CA:企业通常拥有自己的 证书颁发机构(CAs)来生成 TLS 证书。这些 CA 通常不会暴露给外部 API,也不能颁发可以由本地证书管理器实例使用的中间 CA。我们将使用特定于我们集群的 CA 来颁发所有用于集群内的证书。
-
主机集群托管 MySQL:我们将在集群上托管 MySQL 实例,但不会深入探讨 MySQL 相关的操作。我们假设它已经以高可用方式部署。数据库管理是一个独立的学科,我们不会假装能在这一部分覆盖它。
在掌握了这些需求和假设后,下一步是规划如何实现我们的多租户平台。
设计多租户平台
在上一节中,我们定义了需求。现在,让我们构建一个工具矩阵,来告诉我们每个组件的责任:
| 需求 | 组件 | 备注 |
|---|---|---|
| 门户认证 | OpenUnison + Active Directory | OpenUnison 将捕获凭据,Active Directory 将验证凭据。 |
| 租户 | Kubernetes 命名空间 + vCluster | 每个租户将在主机集群中获得自己的命名空间,并部署一个 vCluster。 |
| 租户认证 | OpenUnison | 每个租户将获得自己的 OpenUnison 实例。 |
| 授权 | OpenUnison 与 Active Directory 组 | 每个租户将拥有一个唯一的 Active Directory 组,该组提供管理权限。 |
| 证书生成 | cert-manager 项目 |
cert-manager 将生成用于 vCluster 与 MySQL 之间通信所需的密钥。 |
| 秘密管理 | 集中化 Vault | 每个租户将获得自己的 Vault 数据库,并启用 Kubernetes 认证。 |
| 编排 | OpenUnison | 我们将使用 OpenUnison 的工作流引擎来进行新租户的入驻。 |
表 9.1:实现矩阵
根据我们的需求和实施矩阵,我们的多租户平台将如下所示:

图 9.6:多租户平台设计
使用上述图示,让我们走一遍实现平台所需的步骤:
-
OpenUnison 将创建一个命名空间并将 RoleBinding 绑定到我们的 Active Directory 组,赋予其
adminClusterRole权限。 -
OpenUnison 将在租户的命名空间中生成一个
Certificate对象,vCluster 将使用它与 MySQL 通信。 -
OpenUnison 将在 MySQL 中为 vCluster 创建一个数据库,并创建与步骤 2 中生成的证书关联的用户。
-
OpenUnison 将部署一个
Job,该任务将运行vcluster命令并部署租户的 vCluster。 -
OpenUnison 将部署一个
Job,该任务将部署 Kubernetes Dashboard,部署 OpenUnison,并将 vCluster OpenUnison 集成到宿主集群的 OpenUnison 中。 -
OpenUnison 将在 Vault 中创建一个身份验证策略,允许租户的 vCluster 的令牌使用本地 Pod 身份验证 Vault。它还将运行一个
Job,该任务将在我们的集群中安装 Vault sidecar。
我们将在集中式 Vault 中为 vCluster 提供检索机密的能力。在企业部署中,你还需要控制谁可以使用 CLI 和 Web 界面登录到 Vault,并使用与我们集群相同的身份验证和授权来定制访问权限,但这超出了本章节(以及本书)的范围。
最后,你可以使用任何你喜欢的自动化引擎来执行这些任务,例如 Terraform 或 Pulumi。如果你想使用其中一种工具,仍然可以应用相同的概念,并将其转换为特定实现的细节。现在我们已经设计好了我们的入驻流程,让我们开始部署它。
部署我们的多租户平台
上一部分主要介绍了我们多租户平台的要求和设计。在本部分,我们将部署该平台,并演示如何部署一个租户。第一步是从一个新的集群开始:
$ kind delete cluster -n cluster01
$ kind delete cluster -n multinode
$ cd chapter2
$ ./create-cluster.sh
一旦集群启动,下一步是部署门户。我们已经将所有步骤脚本化:
$ cd chapter9/multitenant/setup/
$ ./deploy_openunison.sh
这个脚本完成了很多工作:
-
部署配置了我们内部 CA 的
cert-manager以供我们的集群使用 -
部署配置了我们内部 CA 的 MySQL
-
部署 OpenUnison,使用模拟身份,并部署我们为 vCluster 定制的内容
-
部署 Vault
-
集成 Vault 和我们的控制平面集群
-
使 OpenUnison 能够在 Vault 中创建新的身份验证机制和策略
根据您的基础设施性能,这个脚本可能需要十到十五分钟才能运行完毕。部署完成后,第一步是登录到门户网站https://k8sou.apps.IP.nip.io/,其中 IP 是您的 IP 地址,点号替换为破折号。我的集群 IP 是192.168.2.82,因此 URL 为https://k8sou.apps.192-168-2-102.nip.io/。使用用户mmosley和密码start123登录。您会看到一个新的徽章,名为新 Kubernetes 命名空间。点击该徽章。

图 9.7:带有新 Kubernetes 命名空间徽章的 OpenUnison 首页
在下一个屏幕中,您将被要求为新命名空间(和租户)提供一些信息。我们在“Active Directory”中创建了两个组来管理对租户的访问。虽然 OpenUnison 默认支持ClusterRole的 admin 和 view 角色映射,但我们将重点关注 admin ClusterRole映射。我们命名空间的 admin 组也将成为租户 vCluster 的cluster-admin。这意味着任何被添加到此 Active Directory 组的用户都将获得对该租户 vCluster 的cluster-admin权限。按照图 9.8中所示填写表单,并点击保存。

图 9.8:新命名空间
保存后,关闭此标签页以返回到主门户并点击刷新。您将看到左侧有一个新的菜单选项,名为开放审批。OpenUnison 的设计基于自服务,因此假设租户所有者会请求部署新的租户。在这种情况下,mmosley 将同时作为租户所有者和审批人。点击开放审批,然后点击处理请求。

图 9.9:审批屏幕
填写理由字段,点击批准请求并确认审批。这将批准用户的请求并启动一个工作流,执行我们在图 9.6中设计的步骤。这个工作流会持续五到十分钟,具体取决于集群的性能。通常,OpenUnison 会在工作流完成后向请求者发送电子邮件,但我们在这里使用了一个 SMTP 黑洞来收集所有生成的邮件,以便简化实验室实现。您需要等到tenant1命名空间被创建并且 OpenUnison 实例正在运行。如果您查看主机集群中的tenant1命名空间,您会看到vault-agent-injector pod 正在运行。这表示部署已完成。
要跟踪您的 vCluster 部署,有三个 pod 需要查看:
-
onboard-vcluster-openunison-tenant1– 该 Job 中的 pod 包含用于创建和部署 vCluster 到您租户命名空间的日志。 -
deploy-helm-vcluster-teant1– 该作业中的 pod 集成了 Vault。 -
openunison-orchestra– 该部署中的 pod 运行 OpenUnison 的入门工作流。
如果过程中出现任何错误,您可以在这里找到它们。
现在我们的租户已经部署完毕,我们可以登录并部署一个 pod。退出 OpenUnison,然后使用用户名 jjackson 和密码 start123 重新登录。jjackson 用户是我们 Active Directory 中管理员组的成员,因此他们将立即能够访问并管理 tenant1 命名空间中的 vCluster。

图 9.10:访问 tenant1 vCluster
jjackson 用户能够像使用主集群一样与我们的 vCluster 交互,可以通过仪表盘或直接通过 CLI 进行操作。我们将使用 jjackson 的会话登录到我们的 tenant vCluster,并部署一个使用 Vault 中密钥数据的 pod。首先,在新会话中 ssh 进入您的集群主机,并为我们的 pod 创建一个 Vault 密钥:
$ ssh ubuntu@192.168.2.82
$ cd chapter9/multitenant/setup/vault
$ . ./vault_cli.sh
$ vault kv put secret/data/vclusters/tenant1/ns/default/config some-password=mysupersecretp@ssw0rd
最后一条命令在 Vault 中创建了我们的密钥数据。请注意,我们创建的路径指定了我们正在使用 default namespace 中的 tenant1 vCluster。根据我们的集群部署方式,只有 tenant1 vCluster 中 default namespace 下的 pod 才能访问 some-password 值。
接下来,让我们使用 jjackson 登录到我们的 vCluster。首先,将 KUBECONFIG 变量设置为临时文件,并使用 tenant1 令牌设置 jjackson 的会话:
$ export KUBECONFIG=$(mktemp)$ export TMP_CERT=$(mktemp)…Cluster "tenant1" set.
Context "tenant1" created.
User "jjackson@tenant1" set.
Switched to context "tenant1".
$ cd ../../examples
$ ./create-vault.sh
$ kubectl logs test-vault-vault-watch -n default
Defaulted container "test" out of: test, vault-agent, vault-agent-init (init)
Wed Oct 11 16:30:37 UTC 2023
MY_SECRET_PASSWORD="mysupersecretp@ssw0rd"
该 pod 能够使用其自身的 ServiceAccount 身份认证连接到我们的 Vault 以检索密钥!为了让我们的 vCluster 连接到 Vault,我们确实需要对 pod 进行两项更新:
annotations:
vault.hashicorp.com/service: "https://vault.apps.192-168-2-82.nip.io"
**vault.hashicorp.com/auth-path: "auth/vcluster-tenant1"**
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/log-level: trace
**vault.hashicorp.com/role: cluster-read**
vault.hashicorp.com/tls-skip-verify: "true"
vault.hashicorp.com/agent-inject-secret-myenv: **'secret/data/vclusters/tenant1/ns/default/config'**
vault.hashicorp.com/secret-volume-path-myenv: '/etc/secrets'
vault.hashicorp.com/agent-inject-template-myenv: |
{{- with secret "**secret/data/vclusters/tenant1/ns/default/config"** -}}
MY_SECRET_PASSWORD="{{ index .Data "some-password" }}"
{{- end }}
我们需要添加一个新的 annotation 来告诉 Vault sidecar 在哪里进行身份认证。auth/vcluster-tenant1 认证路径是由我们的入门工作流创建的。我们还需要将请求的角色设置为 cluster-read,该角色也是由入门工作流创建的。最后,我们需要告诉 sidecar 在哪里查找我们的密钥数据。
到此,我们已经构建了自助式多租户门户的开端!随着我们深入探索更多与多租户相关的重要话题,我们将扩展这个门户。如果您想深入了解我们如何自动化 vCluster 入门过程的代码,chapter9/multitenant/vlcluster-multitenant 是包含自定义工作流的 Helm chart,而 templates/workflows/onboard-vcluster.yaml 是所有工作的起始点。我们将每个主要步骤拆分到各自的工作流中,以便更易阅读。
总结
多租户是现代 Kubernetes 部署中的一个重要话题。为多个租户提供共享基础设施可以减少资源使用,并且在创建所需的隔离以维护安全性和合规性的同时,提供更多的灵活性。在本章中,我们讨论了 Kubernetes 中多租户的好处与挑战,介绍了 vCluster 项目,并学习了如何部署 vCluster 来支持多个租户。最后,我们演示了如何实现一个自助式多租户门户,并集成了我们的 Vault 部署,以便租户能够拥有自己的密钥管理。
在下一章,我们将深入研究 Kubernetes 仪表板的安全性。我们在前几章中使用并部署了它,现在我们将了解其安全性是如何运作的,以及这些经验如何应用到其他集群管理系统中。
问题
-
Kubernetes 自定义资源定义可以支持多个版本。
-
正确
-
错误
-
-
Kubernetes 中的安全边界是什么?
-
Pods
-
容器
-
网络策略
-
命名空间
-
-
vCluster 中的 Pods 运行在哪里?
-
在 vCluster 中
-
在主集群中
-
没有 Pods
-
-
vClusters 有自己的
Ingress控制器。-
正确
-
错误
-
-
vClusters 与主集群共享密钥。
-
正确
-
错误
-
答案
-
b – 错误 – 虽然有一些版本管理,但通常只能拥有一个 CRD 的版本。
-
d – 命名空间是 Kubernetes 中的安全边界。
-
b – 当在 vCluster 中创建一个 Pod 时,同步器会在主集群中创建一个匹配的 Pod 进行调度。
-
b – 错误 – 通常,
Ingress对象会同步到主集群中。 -
b – 错误 – 每个 vCluster 都有自己独特的密钥来标识它。
加入我们本书的 Discord 空间
加入本书的 Discord 工作空间,每月与作者进行一次 问我任何问题 的环节:

第十章:部署安全的 Kubernetes Dashboard
Kubernetes Dashboard 是一个非常有用的工具,可以帮助你了解集群的运行情况。它通常是学习 Kubernetes 时第一个安装的工具,因为它能直观地展示内容。即使在入门阶段之后,仪表盘也能以一种 kubectl 无法提供的方式,快速提供大量信息。在一个屏幕上,你可以迅速看到哪些工作负载正在运行、在哪里运行、使用了多少资源,如果需要更新它们,也能很快完成。仪表盘常常被称为“不安全”或“难以访问”。在本章中,我们将向你展示,仪表盘实际上是相当安全的,并且如何让它变得易于访问。
除了 Kubernetes Dashboard,Kubernetes 集群还包括 API 服务器和 kubelet 以外的其他组件。集群通常由需要进行安全保护的附加应用程序组成,例如容器注册表、源代码控制系统、流水线服务、GitOps 应用程序和监控系统。集群的用户往往需要直接与这些应用程序进行交互。
虽然许多集群专注于验证用户访问应用程序和服务的权限,但集群解决方案并未获得同等的优先待遇。用户通常被要求使用 kubectl 的端口转发或代理功能来访问这些系统。从安全性和用户体验的角度来看,这种访问方式是一种反模式。用户和管理员首次接触到这种反模式的地方就是 Kubernetes Dashboard。本章将详细解释为什么这种访问方式是反模式,以及如何正确地访问仪表盘。我们将向你展示如何避免部署不安全的 Web 应用程序,并指出相关问题和风险,以便你能知道在获取关于如何访问管理应用程序的建议时应注意什么。
我们将以 Kubernetes Dashboard 为例,学习 Web 应用程序的安全性以及如何在自己的集群中应用这些模式。这些课程不仅适用于仪表盘,还适用于其他集群相关的应用程序,例如用于 Istio 的 Kiali Dashboard、Grafana、Prometheus、ArgoCD 和其他集群管理应用程序。
最后,我们将花一些时间讨论本地仪表盘以及如何评估它们的安全性。这是一个流行的趋势,但并非普遍适用。了解这两种方法的安全性非常重要,本章将探讨这些内容。
本章将涵盖以下主题:
-
仪表盘是如何识别你的身份的?
-
理解仪表盘安全风险
-
使用反向代理部署仪表盘
-
与 OpenUnison 集成仪表盘
-
Kubernetes Dashboard 7.0 有什么变化
在概述了本章的工作内容之后,接下来让我们一起研究本章的技术要求。
技术要求
要进行本章中的练习,你需要一个新的 KinD 集群,参考 第二章,使用 KinD 部署 Kubernetes。
你可以在以下 GitHub 仓库中访问本章的代码:github.com/PacktPublishing/Kubernetes-An-Enterprise-Guide-Third-Edition/tree/main/chapter10。
获取帮助
我们尽力测试一切,但在我们的集成实验室中,有时会涉及多达六个或更多的系统。鉴于技术的流动性,有时在我们的环境中有效的东西在你的环境中却不一定有效。别担心,我们会帮助你!在我们的 GitHub 仓库上创建一个问题,网址是 github.com/PacktPublishing/Kubernetes-An-Enterprise-Guide-Third-Edition/issues,我们会很高兴为你提供帮助!
仪表盘是如何识别你的身份的?
Kubernetes 仪表盘是一个强大的 Web 应用程序,可以通过浏览器快速访问你的集群。它让你浏览命名空间、查看节点状态,甚至提供一个可以直接访问 Pod 的 Shell。使用仪表盘和 kubectl 有一个根本的区别。作为一个 Web 应用程序,仪表盘需要管理你的会话,而 kubectl 则不需要。这意味着,在部署过程中,仪表盘有一套不同的安全问题,通常未被考虑到,可能会导致严重后果。在本节中,我们将探讨仪表盘如何识别用户并与 API 服务器进行交互。
仪表盘架构
在深入了解仪表盘如何认证用户之前,了解仪表盘的基本工作原理非常重要。仪表盘从高层次来看,分为三个逻辑层:
-
用户界面:这是显示在你浏览器中的 Angular + HTML 前端,你与之进行交互
-
中间层:前端与一组托管在仪表盘容器中的 API 交互,将前端的请求转换为 Kubernetes API 请求
-
API 服务器:中间层的 API 直接与 Kubernetes API 服务器进行交互
Kubernetes 仪表盘的三层架构可以在以下图示中看到:

图 10.1:Kubernetes 仪表盘逻辑架构
当用户与仪表盘交互时,用户界面会调用中间层,而中间层会进一步调用 API 服务器。仪表盘本身并不知晓如何收集凭证;也没有地方可以提供用户名或密码以登录仪表盘。它有一个基于 Cookie 的非常简单的会话机制,但大多数时候,仪表盘并不真正关心当前登录用户是谁。仪表盘唯一关心的是在与 API 服务器通信时使用什么令牌。
尽管这是逻辑架构,但物理架构将这些组件分布在不同的容器中:

图 10.2:Kubernetes 仪表板容器架构
从版本 7.0 开始,仪表板被拆分为五个独立的容器组件:
-
Web:仪表板的用户界面,负责提供 HTML 和 JavaScript,由你的浏览器渲染。此组件没有身份验证,因此不需要身份验证。
-
API:该容器托管着仪表板的核心功能。它是与 API 服务器交互的组件,代表用户进行操作。此容器需要知道用户身份。
-
Auth:auth 容器用于告诉前端用户令牌是否有效。如果令牌无效,UI 会通过将令牌提供给 API 服务器进行验证,来将用户重定向到登录页面。
-
Metrics:该容器为 Kubernetes 仪表板提供Prometheus的度量端点。
-
Ingress 控制器:由于这些容器都提供来自同一主机的各自路径,因此需要某个组件将它们合并为一个统一的 URL。默认部署包括 Kong 的
Ingress控制器。
如果你查看了第六章中运行的 Pods,你会发现Kong和auth容器都没有在运行。我们稍后会讲解这个。现在我们已经理解了仪表板的架构,那么仪表板是如何知道你是谁的呢?让我们来逐步了解一下这些选项。
身份验证方法
仪表板可以通过两种方式来确定用户身份:
-
来自登录/上传的 kubectl 配置的令牌:仪表板可以提示用户提供
kubectl配置文件或用于身份验证的承载令牌。一旦提供了令牌,UI 会将其作为头信息发送给 API 容器。没有会话管理。当令牌不再有效时,用户会被重定向回登录界面以上传新的令牌。 -
来自反向代理的令牌:如果从用户界面到中间层的请求中包含一个带有承载令牌的授权头,中间层将使用该令牌向 API 服务器发起请求。这是最安全的选项,也是本章将详细讲解的实现方法。
如果你阅读了我们之前的版本,或者使用过之前的仪表板版本,可能会好奇为什么不再使用仪表板自己的身份验证并跳过登录步骤。版本 7.x 的架构变化意味着取消了这一选项。不管怎样,每个请求中必须包含一个带令牌的Authorization头。这是一个非常积极的变化,因为它使得部署一个容易被匿名请求接管的仪表板变得极为困难。实际上,我们删除了本章中谈论如何破解不当部署的仪表板的部分,因为那个攻击路径已经不再有效。
在本章接下来的内容中,将探讨作为访问仪表盘的反模式的第一个选项,并解释为什么反向代理模式从安全性和用户体验的角度来看是访问集群仪表盘实现的最佳选项。
现在,让我们尝试理解仪表盘的安全风险。
理解仪表盘的安全风险
当设置新集群时,仪表盘的安全性问题经常被提及。确保仪表盘安全归结为仪表盘的部署方式,而不是仪表盘本身是否安全。回到仪表盘应用程序的架构,根本没有“安全”被内建进去。中间层只是简单地将令牌传递给 API 服务器。
在谈论任何类型的 IT 安全时,重要的是从深度防御的角度来看待它。这一理念认为,任何系统都应该具有多层安全保护。如果一层失败,其他层可以弥补漏洞,直到可以解决失败的层。单一失败不会直接为攻击者提供访问权限。
与仪表盘安全性相关的最常见事件是 2018 年特斯拉被加密矿工攻击。攻击者能够访问特斯拉集群中运行的 Pod,因为仪表盘没有进行安全保护。
集群的 Pod 访问了提供攻击者访问特斯拉云提供商的令牌,攻击者在这些云服务上运行他们的加密挖矿系统。值得注意的是,这次攻击在 7.x 版本及以上无法奏效,因为 api 容器不会接受没有 Authorization 头的请求。
一般来说,仪表盘往往是攻击的切入点,因为它们使攻击者容易找到他们寻找的内容,并且容易部署得不安全。以此为例,在 2019 年的 KubeCon NA 上,展示了一个夺旗赛(CTF)挑战,其中一个场景是开发者“意外”暴露了集群的仪表盘。
CTF 挑战可以作为家庭实验室通过securekubernetes.com/进行访问。这是一个非常推荐的资源,适合任何学习 Kubernetes 安全性的人。除了具有教育意义(同时也令人恐惧),它也非常有趣!
由于我们不能再在没有某种身份验证的情况下部署仪表盘,我们将重点关注使用 ServiceAccount 令牌和默认设置的安全问题。此外,Kong 和下游服务之间没有加密。
探索仪表盘安全问题
版本 7.x 的仪表盘取消了不需要登录即可部署仪表盘的功能,但默认部署仍然存在一些安全问题,需要解决。
首先,将仪表盘部署到你的集群中:
helm repo add kubernetes-dashboard https://kubernetes.github.io/dashboard/
helm upgrade --install kubernetes-dashboard kubernetes-dashboard/kubernetes-dashboard --create-namespace --namespace kubernetes-dashboard
当你检查 pod 时,你会注意到现在有我们之前描述的五个容器。还有一个 NodePort 服务用于 Kong 入口网关。虽然你可以选择部署不同的 Ingress,但我们将专注于默认设置。你还会看到,Kong 到 pod 之间也没有加密。
使用令牌登录
用户可以在登录屏幕上将令牌上传到仪表板。如前所述,仪表板会接受用户的承载令牌并将其与所有请求一起发送到 API 服务器。虽然这看起来是一个安全的解决方案,但它也带来了自己的问题。仪表板不是 kubectl,不知道如何在令牌过期时刷新令牌。这意味着令牌需要具有较长的生命周期才能有效。它要么需要创建可以使用的服务帐户,要么需要使 OpenID Connect 的 id_tokens 生命周期更长。这两种选择都会消除利用 OpenID Connect 进行身份验证所带来的大部分安全性。
正如本书中反复提到的,ServiceAccount 令牌从未打算在集群外使用。你需要分发该令牌,当然由于它是一个承载令牌,容易丢失,可能会被提交到 git 仓库,或者可能被某些有问题的代码泄露。且仪表板和 Kubernetes 使用的令牌没有区别,因此一个泄露的令牌可以直接用来访问 Kubernetes API。虽然这个解决方案存在是为了让使用仪表板变得相对容易,但不应在生产环境中使用。
在查看了令牌登录仪表板的问题后,我们接下来将看看默认安装和缺乏加密的问题。
未加密的连接
默认的仪表板 Helm 图表不会加密从 Ingress 控制器(默认是 Kong)到各个容器的连接。无论你如何认证到仪表板,这都可能是一个严重的安全反模式。正如我们所讨论的,承载令牌可以被任何具有网络访问权限的人使用,这意味着来自未加密网络连接的丢失令牌可能导致严重的安全漏洞。即使使用短期令牌,这也是一个令人担忧的设计选择。每当你构建安全性时,使用深度防御的方法非常重要,永远不要有单点故障。在这种情况下,缺乏加密意味着你可能会有单点故障,无法做出任何备份。
部署仪表板时,应该启用反向代理与 API 容器之间的加密。我们将在下一节中介绍如何操作。Web 和 metrics 容器的重要性较低。auth 容器不支持加密,这虽然是一个问题,但通过正确的配置可以绕过。
另一种方法是依赖像 Istio 这样的服务网格。如果你启用网格,可以依赖它来进行加密,但这会增加一个额外的组件。
由于创建内部证书颁发机构(CA)非常简单,实际上没有理由不加密这些连接。
虽然我们聚焦于默认 Kubernetes 仪表盘安装的安全性问题,接下来我们将讨论如何正确部署仪表盘。
使用反向代理部署仪表盘
代理是 Kubernetes 中一种常见的模式;在 Kubernetes 集群的每一层都有代理。大多数 Kubernetes 服务网格实现也使用代理模式,创建 sidecar 来拦截请求。此处描述的反向代理与这些代理的区别在于它们的目的。微服务代理通常不携带会话,而 web 应用程序需要会话来管理状态。
以下图示展示了带反向代理的 Kubernetes 仪表盘架构:

图 10.3:带反向代理的 Kubernetes 仪表盘
图 10.3 中展示的反向代理执行四个角色:
-
路由:仪表盘使用的每个容器都有自己独立的路径,基于主机 URL。反向代理负责将请求路由到正确的容器。
-
认证:反向代理拦截未认证的请求(或过期的会话),并触发与 OpenID Connect 身份提供者的认证过程来验证用户。
-
会话管理:Kubernetes 仪表盘是面向用户的应用程序。它应该具有典型的控制措施,以支持会话超时和撤销。要警惕将所有会话数据存储在 cookie 中的反向代理方法,因为这些方法很难撤销。
-
身份注入:一旦代理认证了用户,它需要能够在每个请求中注入一个 HTTP 授权头,该头是一个 JWT,标识已登录的用户,由相同的 OpenID Connect 身份提供者签名,并且与 API 服务器具有相同的发行者和接收者。例外情况是使用假冒身份(impersonation),正如在 第六章《将认证集成到你的集群》中所讨论的那样,注入特定的头信息到请求中。
重要的是,在配置反向代理时,它应该:
-
加密 api 和 auth 容器的流量:这两个容器是需要用户令牌的容器,因此加密非常重要。由于 auth 容器不支持加密,你可能会选择完全绕过这个容器。我们将在下一节中解释更多内容,讨论 OpenUnison 如何与仪表盘集成。
-
管理和更新令牌:使用反向代理时没有必要使用长生命周期的令牌。它应该能够根据令牌的有效期自动续期。
合并这些意味着消除 Kong Ingress 控制器。由于你的认证反向代理已经在执行工作,因此不再需要它。
反向代理不需要运行在集群中。根据你的设置,可能会更有利,特别是在使用集群模拟时。使用模拟时,反向代理会使用服务账户的令牌,因此最好让该令牌永远不要离开集群。
本章的重点是 Kubernetes 项目的仪表板。仪表板功能有多种选择。接下来,我们将探讨这些仪表板如何与 API 服务器交互以及如何评估它们的安全性。
本地仪表板
第三方仪表板的一个共同特点是,它们通常在你的工作站上本地运行,并使用 Kubernetes SDK 以与 API 服务器进行类似 kubectl 的交互。这些工具的优势在于,不需要部署额外的基础设施来确保它们的安全性。
Visual Studio Code 的 Kubernetes 插件是一个本地应用程序,利用直接的 API 服务器连接。启动插件时,Visual Studio Code 会访问你当前的 kubectl 配置,并使用该配置与 API 服务器进行交互。它甚至会在 OpenID Connect 令牌过期时刷新令牌:

图 10.4:带 Kubernetes 插件的 Visual Studio Code
Visual Studio Code 的 Kubernetes 插件能够刷新其 OpenID Connect 令牌,因为它是使用 client-go SDK 构建的,这与 kubectl 使用的客户端库相同。在评估客户端仪表板时,确保它与你的认证方式兼容,即使它不是 OpenID Connect。许多 Kubernetes 的 SDK 并不支持 OpenID Connect 令牌的刷新。直到最近(本书发布之时),Java 和 Python SDK 才开始像 client-go SDK 一样支持 OpenID Connect 令牌的刷新。在评估本地仪表板时,确保它能够利用你的短期令牌,并在需要时刷新它们,就像 kubectl 一样。
在 Kubernetes 生态系统中,有各种各样的仪表板,每个仪表板都有自己独特的管理方式。我不想仅仅列出这些仪表板,而是希望为你提供它们的优缺点和安全影响的深入评估。相反,我们来专注于在评估你想使用哪个仪表板时,应该关注的关键因素:
-
如果仪表板是基于 Web 的:
-
它是否直接支持 OpenID Connect?
-
它是否能够在反向代理后运行,并接受令牌和模拟头信息?
-
它是否需要为其自己的服务账户授予权限?这些权限是否遵循最小权限原则?
-
-
如果仪表板是本地的:
- 客户端 SDK 是否支持 OpenID Connect,能够像
kubectl一样自动刷新令牌,使用 client-go SDK?
- 客户端 SDK 是否支持 OpenID Connect,能够像
这些是重要的评估问题,不仅仅是针对 Kubernetes 仪表盘,也适用于你可能用于其他集群管理应用程序的仪表盘。例如,TektonCD 仪表盘,这是一个用于管理管道的 Web 应用程序,需要删除几个 RBAC 绑定,以确保仪表盘必须使用用户身份,无法被篡改为使用其 ServiceAccount 身份。
其他集群级应用程序
本章的介绍讨论了一个集群除了 Kubernetes 之外,由多个应用程序组成。其他应用程序可能会遵循与仪表盘相同的安全模型,并且反向代理方法比 kubectl 端口转发更适合暴露这些应用程序,即使该应用程序没有内置安全性。以常见的 Prometheus 堆栈为例,Grafana 支持用户身份验证,但 Prometheus 和 Alert Manager 不支持。
你将如何跟踪谁访问了这些系统,或者他们何时通过端口转发访问的?
使用反向代理,每个 URL 的日志以及经过身份验证访问该 URL 的用户可以转发到中央日志管理系统,并由 安全信息和事件管理器 (SIEM) 分析,提供集群使用的额外可视化层。
与仪表盘一样,使用反向代理与这些应用程序结合使用提供了一种分层安全方法。它将会话管理从相关应用中卸载,并提供增强身份验证措施的能力,例如多因素认证和会话撤销。这些优势将导致集群更加安全且易于使用。
现在让我们讨论如何将仪表盘与 OpenUnison 集成。
将仪表盘与 OpenUnison 集成
有关 OpenUnison 如何通过冒充注入身份标头的话题已在 第六章,将身份验证集成到集群中 中讨论过,但没有涉及 OpenUnison 如何在集成 OpenID Connect 的集群中将用户身份注入到仪表盘中。它是可行的,但没有解释。 本节将以 OpenUnison 实现为例,说明如何为仪表盘构建反向代理。通过本节中的信息,你可以更好地理解 API 安全性,或者为仪表盘认证构建自己的解决方案。
OpenUnison 部署包含两个集成的应用程序:
-
OpenID Connect 身份提供者与登录门户:此应用程序托管登录过程和 API 服务器用于获取验证
id_token所需密钥的发现 URL。它还托管获取kubectl令牌的界面。 -
仪表盘:一个反向代理应用程序,它向集成的 OpenID Connect 身份提供者进行身份验证,并将用户的
id_token注入到每个请求中。
这个图表展示了仪表板的用户界面如何与其服务器端组件进行交互,同时通过反向代理注入用户的id_token:

图 10.5: OpenUnison 与仪表板的集成
仪表板使用与 API 服务器相同的 OpenID Connect 身份提供者,但并不使用它提供的id_token。相反,OpenUnison 有一个插件,可以独立于身份提供者生成一个新的id_token,其中包含用户的身份数据。OpenUnison 之所以能够做到这一点,是因为生成用于 OpenID Connect 身份提供者的id_token的密钥(该密钥由kubectl和 API 服务器使用)存储在 OpenUnison 中。这与将仪表板与 KeyCloak 或 Dex 集成的方式不同,因为那样你需要额外的组件来认证用户并维护注入请求中的id_token。这通常通过 OAuth2 代理来完成,该代理需要与身份提供者(即 Dex 或 KeyCloak)、仪表板和入口控制器进行集成。OpenUnison 为你完成了所有这些步骤。
一个新的、短期有效的令牌是与kubectl使用的 OpenID Connect 会话分开生成的。这样,令牌可以独立于kubectl会话进行刷新。这个过程提供了 1 到 2 分钟的令牌有效期,同时还保持了直接登录过程的便利性。
你还会注意到没有认证容器。认证容器的唯一作用是返回一些 JSON 数据,告诉用户界面用户仍然处于认证状态。由于这个容器不支持任何加密,我们就不调用它,而是直接在 OpenUnison 中生成 JSON 数据。这消除了对认证容器的需求,并避免了因没有 TLS 网络连接和承载令牌而可能出现的问题。
如果你对安全性有敏锐的眼光,你可能会指出,这种方法在安全模型中有一个明显的单点故障:用户的凭证!攻击者通常只需要请求凭证就能获得它们。这通常通过电子邮件进行,在一种叫做网络钓鱼的攻击中,攻击者会向受害者发送一个看似登录页面的链接,但实际上这个页面只是用来收集凭证的。这就是为什么多因素认证对于基础设施系统如此重要。
在 2019 年的一项研究中,谷歌展示了多因素认证阻止了 99%的自动化和钓鱼攻击(security.googleblog.com/2019/05/new-research-how-effective-is-basic.xhtml)。将多因素认证添加到身份提供者 OpenUnison 认证中,或直接将其集成到 OpenUnison 中,是确保仪表板和集群安全的最有效方法之一。
接下来,我们将看看仪表板新版本发布后的变化。
Kubernetes 仪表板 7.0 版本的变化
我们在本章中讨论了 7.0 版本的仪表板,但正如企业中常见的情况一样,旧版 2.7 仪表板仍在使用,并且可能还会使用一段时间。2.7 版本和即将发布的 7.0 版本之间的主要区别是,在 7.0 版本中,API 层和前端层被拆分为多个容器。维护者这样做是为了更好地支持更复杂的使用案例,因此请关注这个项目!
总结
在本章中,我们详细探讨了 Kubernetes 仪表板的安全性。首先,我们介绍了架构以及仪表板如何将您的身份信息传递给 API 服务器。然后,我们探讨了仪表板如何受到攻击,最后,详细说明了如何正确地安全部署仪表板。
拥有这些知识后,您现在可以为用户提供一个安全的工具。许多用户更喜欢通过 Web 浏览器访问仪表板的简便性。添加多因素认证会增加额外的安全层和安心感。当您的安全团队质疑仪表板的安全性时,您将能够提供所需的答案以解决他们的担忧。
前三章重点讨论了 Kubernetes API 的安全性。接下来,在第十一章,使用 Open Policy Agent 扩展安全性中,我们将探讨如何确保每个 Kubernetes 部署的软肋——节点的安全!
问题
-
仪表板是不安全的。
-
正确
-
错误
-
-
仪表板如何识别用户?
-
通过反向代理注入或由登录表单提供的令牌
-
用户名和密码
-
服务帐户
-
多因素认证
-
-
仪表板如何跟踪会话状态?
-
会话存储在 etcd 中
-
会话存储在名为
DashboardSession的自定义资源对象中 -
没有会话
-
如果上传了令牌,它会被加密并作为 cookie 存储在浏览器中
-
-
使用令牌时,仪表板多久可以刷新一次令牌?
-
每分钟一次
-
每三十秒
-
当令牌过期时
-
以上都不是
-
-
部署仪表板的最佳方法是什么?
-
使用
kubectl端口转发 -
使用
kubectl代理 -
使用带有秘密 Ingress 主机
-
在反向代理后面
-
-
仪表板不支持冒充。
-
正确
-
错误
-
-
OpenUnison 是唯一支持仪表板的反向代理。
-
正确
-
错误
-
答案
-
b
-
a – 必须有一个令牌
-
c – 当令牌过期时,系统会要求您提供一个新的令牌
-
d – 仪表板无法刷新令牌
-
d – 更好的安全性和可用性
-
b
-
b
第十一章:使用开放策略代理扩展安全性
到目前为止,我们已经涵盖了 Kubernetes 的内置认证和授权功能,这有助于保护集群。虽然这将涵盖大多数用例,但并不涵盖所有用例。一些 Kubernetes 无法处理的安全最佳实践包括预授权容器注册表和确保 Ingress 对象不重叠(尽管大多数 Ingress 控制器都会检查,例如 NGINX)。
这些任务由外部系统完成,并称为动态准入控制器。开放策略代理(OPA)及其 Kubernetes 本地子项目Gatekeeper是处理这些用例最流行的方式之一。本章将详细介绍 OPA 和 Gatekeeper 的部署方法,OPA 的架构以及如何编写策略。
在本章中,我们将涵盖以下主题:
-
动态准入控制器简介
-
OPA 是什么以及它是如何工作的?
-
使用 Rego 编写策略
-
强制执行 Ingress 策略
-
改变对象和默认值
-
创建不使用 Rego 的策略
完成本章后,您将开始为集群和工作负载开发和实施重要的策略。
技术要求
要完成本章的实践练习,您需要一台 Ubuntu 22.04 服务器。
您可以在以下 GitHub 存储库中访问本章的代码:github.com/PacktPublishing/Kubernetes-An-Enterprise-Guide-Third-Edition/tree/main/chapter11。
动态准入控制器简介
准入控制器是 Kubernetes 中的一种专用 Webhook,当对象被创建、更新或删除时运行。当发生这三个事件之一时,API 服务器将对象和操作的信息发送到 Webhook。准入控制器可用于确定是否应执行操作,或者给集群操作员修改对象定义的机会,然后再由 API 服务器处理。我们将研究如何使用此机制来强制执行安全性并扩展 Kubernetes 的功能。
扩展 Kubernetes 的两种方法:
-
构建自定义资源定义,以便定义您自己的对象和 API。
-
实现一个 Webhook,监听 API 服务器的请求并回复必要的信息。您可能还记得,在第六章,将认证集成到您的集群中中,我们解释了可以使用自定义 Webhook 来验证令牌。
从 Kubernetes 1.9 开始,Webhook 可以被定义为动态准入控制器,在 1.16 中,动态准入控制器 API 变为普遍可用(GA)。
有两种类型的动态准入控制器,验证型和变更型。验证型准入控制器验证新的对象、更新或删除是否可以继续进行。变更型准入控制器允许 Webhook 修改对象的创建、删除或更新的负载。本节将重点介绍准入控制器的细节。我们将在下一章中更详细地讨论变更控制器,第十二章,使用 GateKeeper 的节点安全性。
协议非常简单。每当创建或编辑某种类型的对象时,只要为该类型的对象注册了动态准入控制器,Webhook 就会通过 HTTP POST 被调用。然后,Webhook 预计返回 JSON,表示是否允许该操作。
从 1.16 版本开始,admission.k8s.io/v1 已进入 GA 阶段。所有示例将使用 GA 版本的 API。
提交到 Webhook 的请求由多个部分组成。由于 Admission 对象可能非常大,我们在此不提供示例,但我们将使用 github.com/PacktPublishing/Kubernetes-An-Enterprise-Guide-Third-Edition/blob/main/chapter11/example_admission_request.json 作为示例:
-
对象标识符:
resource和subResource属性标识对象、API 和组。如果对象的版本正在升级,则会指定requestKind、requestResource和requestSubResource。此外,namespace和operation用于提供对象的位置以及它是CREATE、UPDATE、DELETE还是CONNECT操作。在我们的示例中,正在创建一个Deployment资源,并且其subResource是Scale,用于在my-namespace命名空间中扩展我们的Deployment。 -
提交者标识符:
userInfo对象标识提交者的用户和组。提交者和创建原始请求的用户不总是相同的。例如,如果用户创建了一个Deployment,则userInfo对象不会是创建原始Deployment的用户;它将是ReplicaSet控制器的服务账户,因为Deployment创建了一个ReplicaSet,该ReplicaSet创建了 pod。在我们的示例中,uid为 admin 的用户提交了扩展请求。 -
对象:
object代表提交的对象的 JSON,而oldObject代表在更新操作中被替换的内容。最后,options指定请求的其他选项。在我们的示例中,新的 pod 在扩展操作后将提交,包含新的副本数。
Webhook 的响应将简单地包含两个属性,即请求中的原始 uid 和 allowed,其值可以是 true 或 false。例如,为了允许我们的扩展操作完成:
{
"uid": "705ab4f5-6393-11e8-b7cc-42010a800002"
"allowed": true
}
userInfo 对象可能会迅速引发复杂问题。由于 Kubernetes 通常使用多个层次的控制器来创建对象,因此根据与 API 服务器交互的用户来跟踪使用创建可能会变得很困难。
基于 Kubernetes 中的对象进行授权会更加高效,例如命名空间标签或其他对象。
一个常见的用例是允许开发者拥有一个他们是管理员的 沙箱,但是该沙箱的容量非常有限。与其验证某个特定用户是否请求了过多的内存,不如在个人命名空间上添加一个限制注解,这样准入控制器就有了一个具体的参考依据,不管用户是提交了 Pod 还是 Deployment。这样,策略就会检查 namespace 上的注解,而不是检查单独的用户。为了确保只有命名空间的所有者能够在其中创建资源,可以使用 RBAC 来限制访问。
关于通用验证 Webhook 的最后一点:没有办法指定密钥或密码。这是一个匿名请求。虽然理论上,验证 Webhook 可以用于实现对集群的更新,但并不推荐这样做。例如,你可以使用验证 Webhook 在创建 Namespace 时创建 ClusterRoleBinding,但这意味着你的策略检查就无法重复进行。最好将策略检查和工作流分开。
现在我们已经讨论了 Kubernetes 如何实现动态访问控制器,接下来我们将查看 OPA 中最受欢迎的一个选项。
什么是 OPA,它是如何工作的?
OPA 是一个轻量级的授权引擎,非常适合在 Kubernetes 中使用。它并非一开始就诞生于 Kubernetes,但无疑在这里找到了合适的定位。OPA 并不要求构建动态准入控制器,但它在这方面非常出色,并且有大量资源和现成的策略可以用来开始构建你的策略库。
本节提供了 OPA 及其组件的高级概述,接下来的章节将详细介绍 OPA 在 Kubernetes 中的实现。
OPA 架构
OPA 由三个组件组成——HTTP 监听器、策略引擎和数据库:

图 11.1:OPA 架构
OPA 使用的数据库是内存中的,并且是短暂的。它不会持久化用于做出策略决策的信息。一方面,这使得 OPA 非常具备可扩展性,因为它本质上是一个授权微服务。另一方面,这意味着每个 OPA 实例都必须独立维护,并且必须与权威数据保持同步:

图 11.2:Kubernetes 中的 OPA
当 OPA 用于 Kubernetes 时,它通过一个名为 kube-mgmt 的 sidecar 填充其数据库,该 sidecar 会对你想导入到 OPA 中的对象设置监视。当对象被创建、删除或更改时,kube-mgmt 会更新其 OPA 实例中的数据。这意味着 OPA 与 API 服务器是“最终一致”的,但它不一定是 API 服务器中对象的实时表示。由于整个 etcd 数据库基本上被一遍又一遍地复制,因此必须特别小心,避免将敏感数据(如 Secrets)复制到 OPA 数据库中。
现在,让我们介绍一下 OPA 策略语言——Rego。
Rego,OPA 策略语言
我们将在下一节详细介绍 Rego。这里要提到的关键点是,Rego 是一种策略评估语言,而不是通用编程语言。对于习惯使用 Golang、Java 或 JavaScript 这类支持复杂逻辑(如迭代器和循环)的开发者来说,Rego 可能会显得有些困难。Rego 的设计目的是评估策略,因此它被精简成了一个专注于此的语言。例如,如果你想用 Java 编写代码来检查一个 pod 中所有容器镜像是否以指定的注册表之一为开头,代码可能会是这样的:
public boolean validRegistries(List<Container> containers,List<String> allowedRegistries) {
for (Container c : containers) {
boolean imagesFromApprovedRegistries = false;
for (String allowedRegistry : allowedRegistries) {
imagesFromApprovedRegistries =
imagesFromApprovedRegistries || c.getImage().startsWith(allowedRegistry);
}
if (! imagesFromApprovedRegistries) {
return false;
}
}
return true;
}
这段代码遍历每一个容器和每一个允许的注册表,确保所有的镜像都符合正确的策略。Rego 中相同的代码要简洁得多:
invalidRegistry {
ok_images = [image | startswith(input_images[j],input.parameters.registries[_]) ; image = input_images[j] ]
count(ok_images) != count(input_images)
}
如果容器中的任何镜像来自未授权的注册表,上述规则将评估为 true。我们将在本章稍后的部分详细讲解这段代码是如何工作的。理解这段代码为何如此简洁的关键在于,Rego 推断了大部分循环和测试的样板代码。第一行生成了一个符合要求的镜像列表,第二行确保符合要求的镜像数量与镜像总数相匹配。如果它们不匹配,那么其中一个或多个镜像必定来自无效的注册表。能够编写简洁的策略代码是 Rego 非常适合用作准入控制器的原因。
到目前为止,我们主要集中讨论了通用的 OPA 和 Rego。在早期,你需要使用 ConfigMaps 将 Kubernetes 直接集成到 OPA 中以存储策略;然而,这证明是非常不方便的。微软开发了一个名为 GateKeeper 的工具,它是 Kubernetes 原生的,使得在 Kubernetes 中更容易充分利用 OPA。现在,让我们介绍一下 Gatekeeper。
Gatekeeper
到目前为止,讨论的内容都是关于 OPA 的通用性。章节开头提到,OPA 并不是最早在 Kubernetes 中出现的。早期的实现有一个 sidecar,它将 OPA 数据库与 API 服务器保持同步,但你需要手动创建作为ConfigMap对象的策略,并手动为 webhook 生成响应。2018 年,微软推出了 Gatekeeper(github.com/open-policy-agent/gatekeeper),提供了一个原生的 Kubernetes 体验。
除了从ConfigMap对象迁移到适当的自定义资源之外,Gatekeeper 还增加了审计功能,让你可以针对现有对象测试策略。如果某个对象违反了策略,就会创建一个违规条目来跟踪它。通过这种方式,你可以获得集群中现有策略违规的快照,或者知道在 Gatekeeper 因升级而停机时是否遗漏了某些内容。
Gatekeeper 与通用 OPA 的一个主要区别在于,在 Gatekeeper 中,OPA 的功能不是通过任何人都能调用的 API 暴露的。OPA 是嵌入式的,Gatekeeper 直接调用 OPA 来执行策略并保持数据库的最新状态。决策只能基于 Kubernetes 中的数据,或者在评估时提取数据。
部署 Gatekeeper
将使用的示例假设使用的是 Gatekeeper,而不是通用的 OPA 部署。
首先,创建一个新的集群来部署 GateKeeper:
$ cd chapter2
$ ./create-cluster.sh
新集群启动后,按照 Gatekeeper 项目的指示,使用以下命令:
$ kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/master/deploy/gatekeeper.yaml
这将启动 Gatekeeper 命名空间的 pod 并创建验证 webhook。部署完成后,继续下一节。我们将在本章剩余部分详细讲解如何使用 Gatekeeper。
自动化测试框架
OPA 为你的策略提供了一个内置的自动化测试框架。这是 OPA 最有价值的方面之一。在部署前能够持续测试策略,可以节省大量的调试时间。在编写策略时,创建一个与策略文件同名但在名称中带有_test的文件。例如,如果你的策略文件是mypolicies.rego,则测试用例应保存在同一目录下的mypolicies_test.rego文件中。运行opa test命令将执行你的测试用例。我们将在下一节展示如何使用它来调试代码。
在介绍了 OPA 的基础知识以及其构建方式后,下一步是学习如何使用 Rego 编写策略。
使用 Rego 编写策略
Rego 是一种专门为编写策略而设计的语言。它不同于你可能编写过的大多数编程语言。典型的授权代码可能看起来像这样:
//assume failure
boolean allowed = false;
//on certain conditions allow access
if (someCondition) {
allowed = true;
}
//are we authorized?
if (allowed) {
doSomething();
}
授权码通常默认为未授权状态,必须满足特定条件才能允许最终操作被授权。而 Rego 采取了不同的方法。Rego 通常会授权所有操作,除非发生特定的一组条件。
Rego 与一般编程语言之间的另一个主要区别是,没有明确的if/then/else控制语句。当一行 Rego 代码需要做出决定时,该行代码的含义是“如果这行代码为假,则停止执行。”例如,以下 Rego 代码表示:“如果镜像以myregistry.lan/开头,则停止执行策略并通过此检查;否则,生成错误信息”:
not startsWith(image,"myregistry.lan/")
msg := sprintf("image '%v' comes from untrusted registry", [image])
相同的代码在 Java 中可能如下所示:
if (! image.startsWith("myregistry.lan/")) {
throw new Exception("image " + image + " comes from untrusted registry");
}
隐式控制语句和显式控制语句之间的这种差异通常是学习 Rego 时最陡峭的部分。虽然这可能比其他语言有更陡峭的学习曲线,但 Rego 通过使测试和构建策略变得简单、自动化和可管理,弥补了这一点。Rego 的另一个优点是它可以用于应用级的授权。我们将在本书后面讲解 Istio 时详细讨论这一点。
OPA 可以用于自动化策略的测试。这在编写依赖于集群安全性的代码时非常重要。自动化测试有助于加快开发进程,并通过捕捉由新工作代码引入的 bug,从而提高安全性。接下来,让我们一起走过编写 OPA 策略、测试它并将其部署到集群的生命周期。
开发 OPA 策略
使用 OPA 的一个常见示例是限制 Pod 可以来自哪些注册表。这是在集群中常见的安全措施,用于限制可以在集群中运行的 Pod。例如,我们曾提到比特币矿工。如果集群只接受来自你自己内部注册表的 Pod,那么这就是阻止恶意行为者滥用集群的一步。首先,让我们编写我们的策略,取自 OPA 文档网站 (www.openpolicyagent.org/docs/latest/kubernetes-introduction/):
package k8sallowedregistries
invalidRegistry {
input_images[image]
not startswith(image, "quay.io/")
}
input_images[image] {
image := input.review.object.spec.containers[_].image
}
input_images[image] {
image := input.review.object.spec.template.spec.containers[_].image
}
这段代码的第一行声明了我们策略所在的package。OPA 中的所有内容,包括数据和策略,都存储在一个包中。
OPA 中的包类似于文件系统中的目录。当你将策略放入一个包中时,所有内容都是相对于该包的。在本例中,我们的策略位于k8sallowedregistries包中。
下一节定义了一个规则。如果我们的 pod 使用来自quay.io的镜像,这个规则最终将是undefined。如果 pod 没有来自quay.io的镜像,则规则返回true,表示注册表无效。当在动态准入审核期间评估 pod 时,Gatekeeper 会将其视为失败,并向 API 服务器返回false。
接下来的两个规则非常相似。第一个input_images规则表示“对对象的spec.container中的每个container进行调用规则的评估”,直接匹配提交给 API 服务器的 pod 对象,并提取每个container的所有image值。第二个input_images规则表示“对对象的spec.template.spec.containers中的每个container进行调用规则的评估”,以短路Deployment对象和StatefulSets。
最后,我们添加 Gatekeeper 需要的规则,以便在评估失败时通知 API 服务器:
violation[{"msg": msg, "details": {}}] {
invalidRegistry
msg := "Invalid registry"
}
如果注册表有效,这个规则将返回一个空的msg。将代码拆分为做出策略决策的代码和响应反馈的代码是一个好主意。这使得测试更容易,接下来我们就来做这件事。
测试 OPA 策略
一旦我们编写完策略,我们希望设置一个自动化测试。与测试任何其他代码一样,重要的是你的测试用例能够覆盖预期和意外的输入。同样,测试正面和负面的结果也很重要。仅仅验证我们的策略允许正确的注册表是不够的;我们还需要确保它能阻止无效的注册表。以下是我们代码的八个测试用例:
package k8sallowedregistries
test_deployment_registry_allowed {
not invalidRegistry with input as {"apiVersion"...
}
test_deployment_registry_not_allowed {
invalidRegistry with input as {"apiVersion"...
}
test_pod_registry_allowed {
not invalidRegistry with input as {"apiVersion"...
}
test_pod_registry_not_allowed {
invalidRegistry with input as {"apiVersion"...
}
test_cronjob_registry_allowed {
not invalidRegistry with input as {"apiVersion"...
}
test_cronjob_registry_not_allowed {
invalidRegistry with input as {"apiVersion"...
}
test_error_message_not_allowed {
control := {"msg":"Invalid registry","details":{}}
result = violation with input as {"apiVersion":"admissi…
result[_] == control
}
test_error_message_allowed {
result = violation with input as {"apiVersion":"admissi…
result == set()
}
总共有八个测试:两个测试确保在出现问题时返回正确的错误消息,六个测试涵盖三种输入类型的两种使用情况。我们正在测试简单的 pod 定义、Deployment 和 CronJob。为了验证预期的成功或失败,我们为每种输入类型包括了具有image属性的定义,其中包含docker.io和quay.io。代码为了打印而做了简化,但可以从github.com/PacktPublishing/Kubernetes-An-Enterprise-Guide-Third-Edition/tree/main/chapter11/simple-opa-policy/rego/下载。
要运行测试,首先按照 OPA 网站上的说明安装 OPA 命令行可执行文件:www.openpolicyagent.org/docs/latest/#running-opa。下载后,进入simple-opa-policy/rego目录并运行测试:
$ opa test .
data.kubernetes.admission.test_cronjob_registry_not_allowed: FAIL (248ns)
--------------------------------------------------------------
PASS: 7/8
FAIL: 1/8
七个测试通过,但 test_cronjob_registry_not_allowed 测试失败了。提交为 input 的 CronJob 不应被允许,因为其 image 使用了 docker.io。它之所以被漏掉,是因为 CronJob 对象遵循与 Pods 和 Deployments 不同的模式,因此我们的两个 input_image 规则无法加载任何来自 CronJob 的容器对象。好消息是,当 CronJob 最终提交 pod 时,Gatekeeper 不会验证它,从而防止它运行。坏消息是,直到 pod 应该被运行时,才会发现这个问题。确保我们在检查容器对象时,除了其他容器对象外,还能检查 CronJob 对象,这样会使调试变得更加容易,因为 CronJob 不会被接受。
为了让所有测试通过,在 GitHub 仓库中的 limitregistries.rego 文件中添加一个新的 input_container 规则,以匹配 CronJob 使用的容器:
input_images[image] {
image := input.review.object.spec.jobTemplate.spec.template.spec.containers[_].image
}
现在,运行测试将显示一切通过:
$ opa test .
PASS: 8/8
在策略经过测试后,下一步是将策略集成到 Gatekeeper 中。
将策略部署到 Gatekeeper
我们创建的策略需要部署到 Gatekeeper,Gatekeeper 提供 Kubernetes 自定义资源,策略需要加载到这些资源中。第一个自定义资源是 ConstraintTemplate,它存储了我们策略的 Rego 代码。这个对象让我们能够指定与策略执行相关的参数,接下来我们将介绍这个。为了简单起见,创建一个没有参数的模板:
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
name: k8sallowedregistries
spec:
crd:
spec:
names:
kind: K8sAllowedRegistries
validation: {}
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sallowedregistries
.
.
.
这个模板的完整源代码可以在 github.com/PacktPublishing/Kubernetes-An-Enterprise-Guide-Third-Edition/blob/main/chapter11/simple-opa-policy/yaml/gatekeeper-policy-template.yaml 查看。
一旦创建,下一步是通过基于模板创建约束来应用策略。约束是 Kubernetes 中的对象,基于 ConstraintTemplate 的配置。请注意,我们的模板定义了一个自定义资源定义。它会被添加到 constraints.gatekeeper.sh API 组中。如果查看集群中的 CRD 列表,你会看到 k8sallowedregistries 被列出:

图 11.3:由 ConstraintTemplate 创建的 CRD
创建约束意味着创建一个由模板定义的对象实例。
为了避免在我们的集群中造成过多的混乱,我们将把此策略限制在 testpolicy 命名空间中:
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRegistries
metadata:
name: restrict-openunison-registries
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
- apiGroups: ["apps"]
kinds:
- StatefulSet
- Deployment
- apiGroups: ["batch"]
kinds:
- CronJob
namespaces: ["testpolicy"]
这个约束限制了我们编写的策略,只允许在testpolicy命名空间中创建Deployment、CronJob和Pod对象。一旦我们的策略创建完成,如果我们尝试在testpolicy命名空间中创建一个来自docker.io的 Pod,它会失败,因为镜像来自docker.io而不是quay.io。首先,让我们创建testpolicy命名空间并创建一个会违反此策略的示例Deployment:
$ chapter11/simple-opa-policy/yaml
**$ kubectl create ns testpolicy**
**$ kubectl create deployment nginx-prepolicy --image=docker.io/nginx/nginx-ingress -n testpolicy**
**$ kubectl create -f ./gatekeeper-policy.yaml**
**$ kubectl create deployment nginx-withpolicy --image=docker.io/nginx/nginx-ingress -n testpolicy**
error: failed to create deployment: admission webhook "validation.gatekeeper.sh" denied the request: [restrict-openunison-registries] Invalid registry
最后一行尝试创建一个引用docker.io而不是quay.io的新Deployment,但由于我们的策略阻止了它,所以创建失败了。但我们也在部署策略之前创建了一个违反此规则的Deployment,这意味着我们的准入控制器从未收到过创建命令。这是 Gatekeeper 相比于通用 OPA 的一个非常强大的特性:Gatekeeper 会根据新策略审计现有基础设施。通过这种方式,你可以迅速找到违规的部署。
接下来,查看策略对象。你会看到在对象的status部分有几个违规项:
$ kubectl get k8sallowedregistries.constraints.gatekeeper.sh restrict-openunison-registries -o json | jq -r '.status.violations'
[
{
"enforcementAction": "deny",
"group": "",
"kind": "Pod",
"message": "Invalid registry",
"name": "nginx-prepolicy-8bd5cbfc9-szzs4",
"namespace": "testpolicy",
"version": "v1"
},
{
"enforcementAction": "deny",
"group": "apps",
"kind": "Deployment",
"message": "Invalid registry",
"name": "nginx-prepolicy",
"namespace": "testpolicy",
"version": "v1"
}
]
部署了你的第一个 Gatekeeper 策略后,你可能很快会注意到它有一些问题。第一个问题是注册表是硬编码的。这意味着我们需要为每个注册表的变化复制代码。它对于命名空间来说也不够灵活。例如,Tremolo Security 的镜像分布在多个github.io注册表中,所以我们可能不希望限制特定的注册表服务器,而是希望对每个命名空间提供灵活性并允许多个注册表。接下来,我们将更新我们的策略来提供这种灵活性。
构建动态策略
我们当前的注册表策略是有限制的。它是静态的,只支持一个注册表。Rego 和 Gatekeeper 都提供了构建动态策略的功能,这些策略可以在我们的集群中重复使用,并根据各个命名空间的要求进行配置。这使得我们只需要一个代码库来开发和调试,而不必维护重复的代码。我们将使用的代码在github.com/PacktPublishing/Kubernetes-An-Enterprise-Guide-Third-Edition/tree/main/chapter11/parameter-opa-policy。
当检查rego/limitregistries.rego时,parameter-opa-policy和simple-opa-policy之间的主要区别在于invalidRegistry规则:
invalidRegistry {
ok_images = [image | startswith(input_images[i],input.parameters.registries[_]) ; image = input_images[i] ]
count(ok_images) != count(input_images)
}
规则第一行的目标是使用推理来确定哪些镜像来自经过批准的注册表。推理提供了一种基于某些逻辑构建集合、数组和对象的方式。在这种情况下,我们只想将来自input.parameters.registries中任何允许的注册表的镜像添加到ok_images数组中。
要理解一条 comprehension,从括号的类型开始。我们的从方括号开始,所以结果将是一个数组。对象和集合也可以被生成。开括号和管道符号(|)之间的单词叫做头部,这是如果满足条件,该变量将被添加到我们的数组中的变量。管道符号(|)右侧的内容是一组规则,用于确定 image 应该是什么,并且是否应该有一个值。如果规则中的任何语句解析为 undefined 或 false,则该迭代将退出。
我们理解的第一个规则是大部分工作的所在。startswith 函数用于确定每个镜像是否以正确的注册表名称开头。我们没有将两个字符串传递给该函数,而是传递了数组。第一个数组包含一个我们尚未声明的变量 i,另一个则使用下划线(_)代替通常会出现索引的位置。i 被 Rego 解释为“对数组中的每个值执行此操作,递增 1,并让它在整个理解过程中被引用。” 下划线是 Rego 中的简写,表示“对所有值执行此操作。” 由于我们指定了两个数组,两个数组的每种组合都会作为输入传递给 startswith 函数。
这意味着如果有两个容器和三个潜在的预批准注册表,那么 startswith 将被调用六次。当任何组合从 startswith 返回 true 时,下一条规则将被执行。这样就将 image 变量设置为具有索引 i 的 input_image,这意味着该镜像会被添加到 ok_images。在 Java 中,类似的代码大致如下:
ArrayList<String> okImages = new ArrayList<String>();
for (int i=0;i<inputImages.length;i++) {
for (int j=0;j<registries.length;j++) {
if (inputImages[i].startsWith(registries[j])) {
okImages.add(inputImages[i]);
}
}
}
一行 Rego 代码消除了七行主要是模板代码的 Java 代码。
规则的第二行将 ok_images 数组中的条目数量与已知容器镜像的数量进行比较。如果它们相等,我们就知道每个容器都包含一个有效的镜像。
随着我们更新了支持多个注册表的 Rego 规则,下一步是部署一个新的策略模板(如果你还没有这么做,请删除旧的 k8sallowedregistries ConstraintTemplate 和 restrict-openunison-registries K8sAllowedRegistries):
$ kubectl delete -f ./gatekeeper-policy.yaml k8sallowedregistries.constraints.gatekeeper.sh "restrict-openunison-registries" deleted
$ kubectl delete -f ./gatekeeper-policy-template.yaml constrainttemplate.templates.gatekeeper.sh "k8sallowedregistries" deleted
这是我们更新后的 ConstraintTemplate:
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
name: k8sallowedregistries
spec:
crd:
spec:
names:
kind: K8sAllowedRegistries
validation:
openAPIV3Schema:
properties:
registries:
type: array
items: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sallowedregistries
.
.
.
除了包含我们的新规则外,突出显示的部分显示我们在模板中添加了一个 schema。这样可以使模板在具有特定参数的情况下重复使用。这个 schema 将进入将要创建的 CustomResourceDefinition 中,并用于验证我们将创建的 K8sAllowedRegistries 对象的输入,以便执行我们的预授权注册表列表。创建这个新的策略定义:
$ cd chapter11/parameter-opa-policy/yaml/
$ kubectl create -f gatekeeper-policy-template.yaml
最后,让我们为 testpolicy 命名空间创建我们的策略。由于这个命名空间中运行的唯一容器应该来自 NGINX 的 docker.io 注册表,我们将使用以下策略将所有 pod 限制为 docker.io/nginx/:
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRegistries
metadata:
name: restrict-openunison-registries
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
- apiGroups: ["apps"]
kinds:
- StatefulSet
- Deployment
- apiGroups: ["batch"]
kinds:
- CronJob
namespaces: ["testpolicy"]
parameters:
registries: ["docker.io/nginx/"]
与我们之前的版本不同,这个策略指定了哪些注册表是有效的,而不是直接将策略数据嵌入到我们的 Rego 中。在我们的策略到位后,接下来让我们尝试在 testpolicy 命名空间中运行 BusyBox 容器以获取一个 shell:
$ kubectl create -f ./gatekeeper-policy.yaml
$ kubectl run tmp-shell --rm -i --tty --image docker.io/busybox -n testpolicy -- /bin/bash
Error from server (Forbidden): admission webhook "validation.gatekeeper.sh" denied the request: [restrict-openunison-registries] Invalid registry
$ kubectl create deployment nginx-withpolicy --image=docker.io/nginx/nginx-ingress -n testpolicy
deployment.apps/nginx-withpolicy created
在上面的示例中,我们成功地阻止了 BusyBox 容器的部署,但 NGINX Deployment 仍然被创建,因为我们能够限制在 docker.io 上的特定容器注册表。
使用这个通用的策略模板,我们可以限制命名空间能够拉取的注册表。例如,在多租户环境中,你可能希望限制所有的 pod 只从所有者自己的注册表中拉取。如果一个命名空间被用于商业产品,你可以规定只有该供应商的容器可以在其中运行。在进入其他使用场景之前,理解如何调试代码并处理 Rego 的特性非常重要。
调试 Rego
调试 Rego 可能具有挑战性。与 Java 或 Go 等通用编程语言不同,Rego 没有办法通过调试器逐步调试代码。以我们刚刚编写的检查注册表的通用策略为例,所有的工作都在一行代码中完成。逐步调试并不会有什么帮助。
为了让 Rego 更易于调试,OPA 项目提供了在命令行设置详细输出时,所有失败测试的追踪记录。这是使用 OPA 内建测试工具的另一个好理由。
为了更好地利用这个追踪功能,Rego 提供了一个名为 trace 的函数,该函数接受一个字符串。将这个函数与 sprintf 结合使用,可以更轻松地跟踪代码中未按预期工作的地方。在 chapter11/parameter-opa-policy-fail/rego 目录中,有一个会失败的测试。还添加了一个包含多个追踪选项的 invalidRegistry 规则:
invalidRegistry {
trace(sprintf("input_images : %v",[input_images]))
ok_images = [image |
trace(sprintf("image %v",[input_images[j]]))
startswith(input_images[j],input.parameters.registries[_]) ;
image = input_images[j]
]
trace(sprintf("ok_images %v",[ok_images]))
trace(sprintf("ok_images size %v / input_images size %v",[count(ok_images),count(input_images)]))
count(ok_images) != count(input_images)
}
当测试运行时,OPA 会输出每个比较和代码路径的详细追踪记录。每当它遇到 trace 函数时,就会在追踪记录中添加一个“注释”。这相当于在代码中添加 print 语句以进行调试。OPA 的追踪输出非常详细,包含的文本太多,无法在打印中全部列出。运行 opa test . -v 命令将在此目录中提供完整的追踪记录,你可以使用这些记录来调试代码。
使用现有策略
在深入了解 OPA 和 Gatekeeper 的高级使用场景之前,理解 OPA 的构建和使用方式非常重要。如果你检查我们在上一节中处理的代码,你可能会注意到我们没有检查 initContainer。我们只关注主容器。initContainer 是在 pod 中列出的容器预期结束之前运行的特殊容器。它们通常用于准备挂载的文件系统或执行其他应该在 pod 中的容器运行之前完成的“初始化”任务。如果有恶意用户试图启动一个带有拉取比特币挖矿工具(或更糟)的 initContainer 的 pod,我们的策略将无法阻止它。
在设计和实施策略时,重要的是要非常详细。确保在构建策略时没有遗漏某些东西的一种方法是使用那些已经存在并经过测试的策略。Gatekeeper 项目在其 GitHub 仓库中维护了几种已测试的策略及其使用方法,地址为github.com/open-policy-agent/gatekeeper-library。在尝试自己构建策略之前,先看看那里是否已经有现成的策略。
本节概述了 Rego 及其在策略评估中的工作原理。虽然没有涵盖所有内容,但应该能为你使用 Rego 的文档提供一个很好的参考点。接下来,我们将学习如何构建依赖于请求之外数据的策略,例如集群中的其他对象。
强制执行 Ingress 策略
到目前为止,我们已经构建了自包含的策略。当检查镜像是否来自预授权的注册表时,我们所需的唯一数据来自策略和容器。但这通常不足以做出策略决策。在本节中,我们将构建一个依赖于集群中其他对象的策略,以便做出策略决策。
在开始实现之前,让我们先讨论一下使用案例。限制哪些命名空间可以拥有Ingress对象是很常见的做法。如果一个命名空间托管的工作负载不需要任何入站访问,为什么还要允许存在Ingress对象呢?你可能认为可以通过限制租户能够使用Role和RoleBinding来强制执行这一点,但这种方式有一些局限性:
-
admin和编辑ClusterRoles是默认的聚合型ClusterRoles,因此你需要创建一个新的ClusterRole,列举出除了Ingress之外你希望命名空间管理员能够创建的所有对象。 -
如果你的新
ClusterRole包含了RoleBindings,命名空间的拥有者可以轻松授予自己创建Ingress的权限。
使用带有注释或标签的准入控制器是一种强制执行命名空间是否可以拥有Ingress的好方法。Namespace对象是集群范围的,因此admin无法提升其在命名空间中的权限并添加标签。
在接下来的示例中,我们将编写一个只允许在具有正确标签的命名空间中使用Ingress对象的策略。伪代码大致如下:
if (! hasIngressAllowedLabel(input.review.object.metdata.namespace)) {
generate error;
}
这里的难点是理解命名空间是否有标签。Kubernetes 有一个 API,你可以查询它,但这意味着要么将一个密钥嵌入策略中,以便它可以与 API 服务器通信,要么允许匿名访问。这两种选项都不是好主意。查询 API 服务器的另一个问题是,自动化测试变得困难,因为你现在依赖于 API 服务器在运行测试时的可用性。
我们之前讨论过,OPA 可以将 API 服务器中的数据复制到它自己的数据库中。Gatekeeper 使用这个功能创建一个可以进行测试的缓存对象。一旦这个缓存被填充,我们可以将其复制到本地,为我们的策略测试提供数据。
启用 Gatekeeper 缓存
Gatekeeper 缓存通过在 gatekeeper-system 命名空间中创建 Config 对象来启用。将此配置添加到你的集群中:
apiVersion: config.gatekeeper.sh/v1alpha1
kind: Config
metadata:
name: config
namespace: "gatekeeper-system"
spec:
sync:
syncOnly:
- group: ""
version: "v1"
kind: "Namespace"
$ cd chapter11/enforce-ingress/yaml/
$ kubectl create -f ./config.yaml
这将开始在 Gatekeeper 的内部 OPA 数据库中复制 Namespace 对象。让我们创建一个带有允许 Ingress 对象的标签和没有允许 Ingress 对象的标签的 Namespace:
apiVersion: v1
kind: Namespace
metadata:
name: ns-with-ingress
labels:
allowingress: "true"
spec: {}
---
apiVersion: v1
kind: Namespace
metadata:
name: ns-without-ingress
spec: {}
稍等片刻,数据应该会被存储到 OPA 数据库中,并准备好查询。
Gatekeeper 服务帐户在默认安装下对集群中的所有内容都有读取权限。这包括秘密对象。请小心你在 Gatekeeper 缓存中复制的数据,因为 Rego 策略内部没有安全控制。如果不小心,策略可能会轻易地记录下秘密对象数据。同时,确保控制谁有权限访问 gatekeeper-system 命名空间。任何获取服务帐户令牌的人都可以用它来读取集群中的任何数据。
现在我们已经设置好 Gatekeeper 并准备开始执行策略,那么我们如何测试这些策略呢?我们可以直接在集群中测试它们,但那会减慢我们的开发周期。接下来,我们将看到如何模拟测试数据,以便我们可以在 Kubernetes 集群外自动化测试。
模拟测试数据
为了自动化测试我们的策略,我们需要创建测试数据。在之前的示例中,我们使用了注入到 input 变量中的数据。缓存数据存储在 data 变量中。具体来说,为了访问我们的命名空间标签,我们需要访问 data.inventory.cluster["v1"].Namespace["ns-with-ingress"].metadata.labels。这是你从 Gatekeeper 中使用 Rego 查询集群数据的标准方法。如果你想查询命名空间内部的对象,它看起来应该像 data.inventory.namespace["myns"]["v1"]["ConfigMaps"]["myconfigmap"]。就像我们处理输入一样,我们可以通过创建数据对象来注入一个模拟版本的数据。下面是我们 JSON 数据的样子:
{
"cluster": {
"v1": {
"Namespace": {
"ns-with-ingress": {
"metadata": {
"labels": {
"allowingress": "true"
}
}
},
"ns-without-ingress": {
"metadata": {
}}}}}}
当你查看 chapter11/enforce-ingress/rego/enforceingress_test.rego 文件时,你会看到测试中使用了 with input as {…} with data as {…},前面的文档作为我们的控制数据。这让我们能够使用 GateKeeper 中存在的数据来测试策略,而无需在集群中部署我们的代码。
构建并部署我们的策略
就像之前一样,我们在编写策略之前已经编写了测试用例。接下来,我们将检查我们的策略:
package k8senforceingress
violation[{"msg":msg,"details":{}}] {
missingIngressLabel
msg := "Missing label allowingress: \"true\""
}
missingIngressLabel {
data.inventory.cluster["v1"].Namespace[input.review.object.metadata.namespace].metadata.labels["allowingress"] != "true"
}
missingIngressLabel {
not data.inventory.cluster["v1"].Namespace[input.review.object.metadata.namespace].metadata.labels["allowingress"]
}
这段代码应该看起来很熟悉。它遵循了与我们之前策略类似的模式。第一个规则violation是 Gatekeeper 的标准报告规则。第二个和第三个规则具有相同的名称,但逻辑不同。这是因为 Rego 会将规则中的所有语句视为 AND 操作,因此为了规则成立,所有语句必须为真。如果我们只有第一个missingIngressLabel规则,用来检查allowingress标签是否为真,那么没有此标签的Ingress对象将违反规则,从而绕过我们的要求。我们可以有一个要求标签必须设置的规则,但那会导致糟糕的用户体验。更好的做法是设置我们的策略,使其在标签不为真或标签根本未设置时失败。
为了设置“如果标签值不为真或标签不存在”的逻辑,我们需要有两个具有相同名称的规则。一个规则检查标签的值,另一个则验证标签是否存在。Rego 将执行两个missingIngressLabel规则,只要其中一个通过,执行就会继续。在我们的例子中,如果Ingress对象所在的命名空间没有正确的allowingress值,或者根本没有allowingress标签,那么违规规则将完成,并返回错误给用户。
这是 Rego 与其他语言之间的一个关键区别。Rego 不像 Java、Go 或 JavaScript 那样按顺序执行。它是一种评估的策略语言,因此执行路径不同。在编写 Rego 时,重要的是要记住,你并不是在使用典型的编程语言。
要部署,请将chapter11/enforce-ingress/yaml/gatekeeper-policy-template.yaml和chapter11/enforce-ingress/yaml/gatekeeper-policy.yaml添加到你的集群中。
为了测试,我们将在ns-without-ingress命名空间中尝试创建一个Ingress对象:
$ kubectl create ingress test --rule="foo.com/bar=svc1:8080,tls=my-cert" -n ns-without-ingress
error: failed to create ingress: admission webhook "validation.gatekeeper.sh" denied the request: [require-ingress-label] Missing label allowingress: "true"
你可以看到我们的策略阻止了Ingress对象的创建。接下来,我们将在具有正确标签的ns-with-ingress命名空间中尝试创建相同的Ingress对象:
$ kubectl create ingress test --rule="foo.com/bar=svc1:8080,tls=my-cert" -n ns-with-ingress
ingress.networking.k8s.io/test created
这一次,我们的策略允许创建Ingress对象!
本章大部分内容都花在了编写策略上。接下来,我们将介绍如何通过变更 Webhook 为你的对象提供合理的默认值。
变更对象和默认值
直到目前为止,我们讨论的所有内容都是如何使用 Gatekeeper 来强制执行策略。Kubernetes 还有一个叫做变更准入 Webhook 的功能,允许在 API 服务器处理对象并运行验证准入控制器之前,通过 Webhook 来修改或变更对象。
变更 webhook 的常见用法之一是显式设置没有设置安全上下文信息的 pod。例如,如果你创建一个没有spec.securityContext.runAsUser的 pod,那么该 pod 将以 Docker 容器构建时使用的USER指令(或者默认的 root)作为用户身份运行。这是不安全的,因为这意味着你可能是以 root 用户身份运行,特别是当容器来自 Docker Hub 时。虽然你可以有一个策略来阻止以 root 身份运行,你也可以有一个变更 webhook,如果没有指定用户,则会设置一个默认的用户 ID。这为开发者提供了更好的体验,因为现在,作为开发者,我不必担心我的容器是以哪个用户身份构建的,只要它设计成可以与任何用户一起工作。
这引出了一个常见的问题:默认值与显式配置。存在两种不同的思路。第一种认为,在可能的情况下,应该提供合理的默认值,以减少开发者为使典型工作负载运行所需要知道的内容。这可以创造一致性,并使得识别异常更容易。另一种思路要求显式配置安全上下文,以便一眼就能看出工作负载的期望。这可以简化审计,特别是如果配合 GitOps 来管理清单,但会产生相当多的重复 YAML。
我个人是理性默认值的支持者。绝大多数工作负载不需要任何特权,应该将其视为如此。这并不意味着你不需要执行策略,而是提供更好的开发者体验。它还使得进行全局更改变得更加容易。想要更改默认的用户 ID 或安全上下文?你只需要在变更 webhook 中进行更改,而不是在成千上万的清单中进行修改。Kubernetes 的大多数部分就是这么构建的。你不直接创建 pod 对象;你创建Deployments和StatefulSets,然后通过控制器创建 pod。回到我们关于 RBAC 的讨论,聚合角色也是这样工作的。Kubernetes 并不是为命名空间管理员创建一个巨大的ClusterRole,而是通过控制器动态生成ClusterRole,基于标签选择器,这样更容易维护。根据我的经验,这个示例也应该应用于安全默认设置。
Gatekeeper 的变更功能不像它的验证策略那样基于 Rego 构建。虽然你可以用 Rego 编写变更 webhook,我可以从经验中告诉你,它并不适合这种用途。Rego 作为一个优秀的策略定义语言,使得它在构建变更时非常困难。
现在我们知道了哪些变更是有用的,并且可以使用 Gatekeeper,让我们构建一个变更,它将在没有指定用户时将所有容器配置为以默认用户身份运行。
首先,让我们部署一些可以测试变更的内容:
$ kubectl create ns test-mutations
$ kubectl create deployment test-nginx --image=ghcr.io/openunison/openunison-k8s-html:latest -n test-mutations
现在,我们可以在chapter11/defaultUser/addDefaultUser.yaml中部署该策略:
apiVersion: mutations.gatekeeper.sh/v1
kind: Assign
metadata:
name: default-user
spec:
applyTo:
- groups: [""]
kinds: ["Pod"]
versions: ["v1"]
match:
scope: Namespaced
excludedNamespaces:
- kube-system
location: "spec.securityContext.runAsUser"
parameters:
assign:
value: 70391
pathTests:
- subPath: "spec.securityContext.runAsUser"
condition: MustNotExist
让我们一步步地走过这个变更过程。spec的第一部分,applyTo,告诉 Gatekeeper 你希望变更应用到哪些对象。对于我们来说,我们希望它作用于所有 pod。
接下来的部分,match,让你有机会指定我们希望变更应用到哪些 pod。在我们的例子中,我们应用于所有 pod,除了 kube-system 命名空间。一般来说,我倾向于避免更改 kube-system 命名空间中的任何内容,因为这是管理集群的人负责的领域。
在那里做更改可能会对集群产生永久性影响。除了指定你不希望应用变更的命名空间外,你还可以指定额外的条件:
-
kind– 要匹配的对象类型 -
labelSelectors– 对象上的标签,必须匹配 -
namespaces– 要应用变更策略的命名空间列表 -
namespaceSelector– 容器命名空间上的标签
我们将在第十二章,使用 GateKeeper 进行节点安全中详细讨论标签匹配。
在定义如何匹配要变更的对象后,我们指定要执行的变更。对于我们来说,如果没有指定用户 ID,我们希望将 spec.securityContext.runAsUser 设置为随机选择的用户 ID。最后一部分,pathTests,允许我们在 spec.securityContext.runAsUser 尚未设置时设置该值。
一旦应用了变更策略,验证测试 pod 是否没有以特定用户身份运行:
$ kubectl get pods -l app=test-nginx -o jsonpath='{.items[0].spec.securityContext}' -n test-mutations
{}
现在,删除 pod 并重新检查:
$ kubectl delete pods -l app=test-nginx -n test-mutations
pod " test-nginx-f6c8578fc-qkd5h" deleted
$ kubectl get pods -l app=test-nginx -o jsonpath='{.items[0].spec.securityContext}' -n test-mutations
{"runAsUser":70391}
我们的 pod 现在以用户 70391 运行!现在,让我们编辑 deployment 以便设置用户身份:
$ kubectl patch deployment test-nginx --patch '{"spec":{"template":{"spec":{"securityContext":{"runAsUser":19307}}}}}' -n test-mutations
deployment.apps/test-nginx patched
$ kubectl get pods -l app=test-nginx -o jsonpath='{.items[0].spec.securityContext}' -n test-mutations
{"runAsUser":19307}
我们的变更没有生效,因为我们在 Deployment 对象中已经指定了一个用户。
关于设置值的最后一点:你会发现你经常需要为列表中的对象设置一个值。例如,你可能想要创建一个策略,使任何容器默认为非特权(unprivileged),除非明确设置为特权(privileged)。在 chapter11/defaultUser/yaml/setUnprivileged.yaml 中,我们的 location(以及 subPath)已发生变化:
location: "spec.containers[image:*].securityContext.privileged"
这段话的意思是:“匹配 spec.containers 列表中所有具有 image 属性的对象。”由于每个容器都必须有镜像,这将匹配所有容器。应用这个对象后,再次在测试 pod 上测试:
$ kubectl get pods -l app=test-nginx -o jsonpath='{.items[0].spec.containers[0].securityContext}' -n test-mutations
$ kubectl delete pods -l app=test-nginx -n test-mutations
pod " test-nginx-ccf9bfcd-wt97v" deleted
$ kubectl get pods -l app=test-nginx -o jsonpath='{.items[0].spec.containers[0].securityContext}' -n test-mutations
{"privileged":false}
现在我们的 pod 被标记为非特权(unprivileged)!
在本节中,我们讨论了如何使用 Gatekeeper 的内置变更支持设置默认值。我们讨论了变更 webhook 设置默认值的好处,启用了 Gatekeeper 对变更的支持,并创建了设置默认用户身份和禁用特权容器的策略。通过本节所学,您可以使用 GateKeeper 不仅执行策略,还可以设置合理的默认值,帮助开发人员更容易地遵循合规要求。使用 GateKeeper 进行策略管理很不错,但它确实需要额外的技能和管理额外的系统。接下来,我们将学习如何创建不依赖 Rego 或使用 Kubernetes 新内置策略引擎的替代方案。
创建没有 Rego 的策略
Rego 是一种非常强大的方式,用于构建复杂的策略,随后由 GateKeeper 项目实现。虽然它强大,但也伴随着陡峭的学习曲线和复杂性。它可能不适合您或您的集群。它并不是实现准入控制器的唯一方式。我们不会深入讨论太多细节,因为这些其他项目各自都有值得探索的能力,我无法在一个部分中详细介绍它们。
GateKeeper 的两个最常见替代方案是:
-
Kyverno:Kyverno 是 Kubernetes 的一个专用策略引擎。它不像 OPA 那样设计为通用授权引擎,因此可以做出一些假设,从而提供更简化的 Kubernetes 策略构建体验(
kyverno.io/)。 -
jsPolicy:jsPolicy 项目允许您使用 JavaScript 或 TypeScript 构建策略,而不是像 Rego 那样使用领域特定语言(DSL)。其理念是,许多由于 Rego 作为策略语言而非编程语言所带来的怪癖,通过使用像 JavaScript 这样的通用语言得以消除(
github.com/loft-sh/jspolicy)。
这两个项目各有其优势,我鼓励您根据自己的使用场景评估它们。如果您的策略比较简单,不需要这些引擎的强大功能,您还可以考虑 Kubernetes 的新内置功能,这也是我们接下来要介绍的内容。
使用 Kubernetes 的验证入站策略
在 Kubernetes 1.28 中,验证入站策略进入了 Beta 阶段,这使得您可以在不需要外部准入控制器的情况下创建更简单的策略。对于简单的策略,这消除了一个需要部署的组件。我们不会深入探讨如何构建入站策略,但我们希望为您提供一个概览,让您知道这是一个选项。
从策略开发的角度来看,使用 Gatekeeper 和验证准入策略之间的最大区别在于,Gatekeeper 使用 Rego,而验证准入策略使用通用表达式语言(CEL)。CEL 不是图灵完备的语言,这意味着它不像 JavaScript 那样具有表现力和能力,但它更容易安全。CEL 正被集成到 Kubernetes 的多个层次中。它用于为自定义资源定义提供更具表现力的验证,也正在集成到正在开发的新身份验证配置选项中。你可以在github.com/google/cel-spec上了解更多关于 CEL 的信息。
从功能角度来看,你可以使用 CEL 来验证要创建的对象中的任何数据。构建验证准入策略有两个组件:
-
ValidatingAdmissionPolicy:这是描述策略和在该策略中执行的表达式的对象。这类似于 Gatekeeper 中的ConstraintTemplate。 -
ValidatingAdmissionPolicyBinding:这是 Kubernetes 了解何时应用我们的ValidatingAdmissionPolicy的方式。
为了实现我们上面的示例,我们希望将 Ingress 对象限制在具有特定标签的命名空间中,首先,我们将创建ValidatingAdmissionPolicy(chapter11/enforce-ingress-vap/vap-ingress.yaml):
apiVersion: admissionregistration.k8s.io/v1beta1
kind: ValidatingAdmissionPolicy
metadata:
name: "vap-ingress"
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: ["networking.k8s.io"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["ingresses"]
validations:
- expression: |-
namespaceObject.metadata.labels.allowingress == "true"
如果将Ingress添加到的命名空间没有allowingress标签,且其值为true,上述策略将会失败。接下来,我们需要告诉 Kubernetes 绑定我们的策略。我们希望这适用于所有命名空间,但类似于 Gatekeeper 的策略实现,我们也可以指定特定的命名空间或命名空间标签。我们通过使用ValidatingAdmissionPolicy(chapter11/enforce-ingress-vap/vap-binding-ingress.yaml)来实现:
apiVersion: admissionregistration.k8s.io/v1beta1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: "vap-binding-ingress"
spec:
policyName: "vap-ingress"
validationActions: [Deny]
这个绑定将把我们的策略绑定到所有命名空间,并且在失败时拒绝请求。我们也可以警告用户或仅仅审核事件。在我们的案例中,我们希望请求失败。创建了这两个对象后,我们可以再次尝试创建Ingress对象:
$ kubectl create ingress test --rule="foo.com/bar=svc1:8080,tls=my-cert" -n ns-without-ingress
error: failed to create ingress: ingresses.networking.k8s.io "test" is forbidden: ValidatingAdmissionPolicy 'vap-ingress' with binding 'vap-binding-ingress' denied request: expression 'namespaceObject.metadata.labels.allowingress == "true"' resulted in error: no such key: allowingress
$ kubectl create ingress test --rule="foo.com/bar=svc1:8080,tls=my-cert" -n ns-with-ingress
ingress.networking.k8s.io/test created
就像我们的 Gatekeeper 示例一样,我们可以看到如果命名空间没有适当的标签,我们能够拒绝创建Ingress规则。
将验证访问策略添加到 Kubernetes 为其增加了一个强大的工具,但它也有一定的限制。很容易说我们会将验证访问策略用于简单的用例,将 Gatekeeper 用于更复杂的用例,但除了实现复杂性之外,还有其他需要考虑的因素。首先,你如何监控失败?如果你同时使用这两种解决方案,即使你可能有一些更简单的规则实现,你仍然需要审核这两种解决方案,这将增加工作量。
尽管我们引入了验证访问策略,以便让你了解它们的能力,但我们将继续在后续章节中聚焦于 OPA 和 Gatekeeper。在下一章中,我们将应用所学的 OPA 和 Gatekeeper 知识来帮助确保 Kubernetes 节点的安全。
总结
本章我们探讨了如何使用 Gatekeeper 作为动态准入控制器,为 Kubernetes 内建的 RBAC 功能提供额外的授权策略。我们了解了 Gatekeeper 和 OPA 的架构。然后,我们学习了如何在 Rego 中构建、部署和测试策略。最后,你将看到如何使用 Gatekeeper 内置的变异支持在 pod 中创建默认的配置选项。
扩展 Kubernetes 的策略可以增强集群的安全性,并提供对正在运行的工作负载完整性的更大信心。
使用 Gatekeeper 还可以通过其持续审计的应用,捕获之前遗漏的策略违规行为。利用这些功能将为你的集群提供更强大的基础。
本章关注的是是否根据我们的特定策略启动 pod。在下一章中,我们将学习如何保护节点免受运行在这些 pod 中的进程的影响。
问题
-
OPA 和 Gatekeeper 是同一回事吗?
-
是的
-
否
-
-
Rego 代码如何存储在 Gatekeeper 中?
-
它作为被监视的
ConfigMap对象存储。 -
Rego 必须挂载到 pod 上。
-
Rego 需要作为密钥对象存储。
-
Rego 被保存为一个
ConstraintTemplate。
-
-
如何测试 Rego 策略?
-
在生产环境中
-
使用直接内嵌于 OPA 的自动化框架
-
通过首先编译为 WebAssembly
-
-
在 Rego 中,如何编写
for循环?-
你不需要这样做;Rego 会识别迭代步骤。
-
通过使用
for all语法。 -
通过在循环中初始化计数器。
-
Rego 中没有循环。
-
-
调试 Rego 策略的最佳方法是什么?
-
使用 IDE 连接到集群中的 Gatekeeper 容器。
-
在生产环境中。
-
向代码中添加 trace 函数,并使用
-v参数运行opa test命令来查看执行追踪。 -
包含
System.out语句。
-
-
所有约束都需要硬编码。
-
正确
-
错误
-
-
Gatekeeper 可以替代 pod 安全策略。
-
正确
-
错误
-
答案
-
b – 不,Gatekeeper 是一个基于 OPA 构建的 Kubernetes 原生策略引擎。
-
d – Rego 被保存为一个
ConstraintTemplate -
b – 请不要在生产环境中进行测试!
-
a – 一切都建立在策略上,而非迭代控制循环。
-
c – 向代码中添加 trace 函数,并使用
-v参数运行opa test命令来查看执行追踪 -
b – 错误。你可以有变量约束。
-
a – 正确,我们将在下一章中详细讲解!
加入我们书籍的 Discord 交流群
加入本书的 Discord 交流群,参加每月的 问我任何问题 环节,与作者互动:

第十二章:使用 Gatekeeper 进行节点安全
到目前为止,讨论的绝大多数安全问题都集中在保护 Kubernetes API 上。认证意味着对 API 调用进行认证。授权意味着授权访问某些 API。即使关于仪表板的讨论,也主要集中在如何通过仪表板安全地认证到 API 服务器。
本章将有所不同,因为我们将重点转向保护我们的节点。我们将学习如何使用 Gatekeeper 项目来保护 Kubernetes 集群的节点。我们的重点将是容器如何在集群的节点上运行,以及如何防止这些容器获得超过应有的权限。在本章中,我们将详细探讨节点未被保护时,如何利用漏洞来获得对集群的访问权限。我们还将探索即便是在不需要节点访问权限的代码中,如何也可能被利用进行攻击。
本章将涵盖以下主题:
-
技术要求
-
什么是节点安全?
-
使用 Gatekeeper 强化节点安全
-
使用 Pod 安全标准来强化节点安全
到本章结束时,你将更好地理解 Kubernetes 如何与运行你工作负载的节点交互,以及如何更好地保护这些节点。
技术要求
要完成本章的实操练习,你将需要一台 Ubuntu 22.04 服务器。
你可以在以下 GitHub 仓库中访问本章的代码:github.com/PacktPublishing/Kubernetes-An-Enterprise-Guide-Third-Edition/tree/main/chapter12。
什么是节点安全?
每个在集群中启动的 Pod 都运行在一个节点上。这个节点可以是一个虚拟机,一个“裸金属”服务器,甚至是另一种计算服务,本身可能也是一个容器。每个由 Pod 启动的进程都在该节点上运行,并且根据其启动方式,可能在该节点上拥有一系列意外的能力,例如访问文件系统、突破容器以获得该节点的 shell,甚至访问节点用于与 API 服务器通信的密钥。确保只有在授权的情况下才允许请求特权的进程,且即使如此,也只能出于特定的目的进行操作,这一点非常重要。
许多人有物理和虚拟服务器的使用经验,而且大多数人知道如何保护在其上运行的工作负载。当谈到每个工作负载的安全时,容器需要被以不同的方式考虑。要理解为什么 Kubernetes 安全工具,如 Open Policy Agent (OPA) 存在,你需要理解容器与 虚拟机 (VM) 之间的区别。
理解容器和虚拟机(VM)之间的区别
“容器是轻量级虚拟机”通常是用来描述容器和 Kubernetes 新手的一种方式。虽然这提供了一个简单的类比,但从安全的角度来看,这个比较是危险的。容器在运行时是一个在节点上运行的进程。在 Linux 系统中,这些进程通过一系列 Linux 技术进行隔离,从而限制它们对底层系统的可见性。
去 Kubernetes 集群中的任何一个节点,运行 top 命令,所有来自容器的进程都会列出。例如,尽管 Kubernetes 在 KinD 中运行,但运行 ps -A -elf | grep java 会显示 OpenUnison 和 operator 容器进程:
4 S k8s 1193507 1193486 1 80 0 - 3446501 - Oct07 ? 06:50:33 java -classpath /usr/local/openunison/work/webapp/
WEB-INF/lib/*:/usr/local/openunison/work/webapp/WEB-INF/classes:/tmp/quartz -Djava.awt.headless=true -Djava.security.egd=file:/dev/./urandom -DunisonEnvironmentFile=/etc/openunison/ou.env -Djavax.net.ssl.trustStore=/etc/openunison/cacerts.jks com.tremolosecurity.openunison.undertow.OpenUnisonOnUndertow /etc/openunison/openunison.yaml
0 S k8s 2734580 2730582 0 80 0 - 1608 pipe_w 13:13 pts/0 00:00:00 grep --color=auto java
相比之下,虚拟机正如其名称所示,是一个完整的虚拟系统。它模拟自己的硬件,拥有独立的内核等等。虚拟机管理程序(Hypervisor)为虚拟机提供隔离,甚至到硅层,而容器之间在节点上的隔离非常少。
有一些容器技术会在自己的虚拟机上运行容器。容器仍然只是一个进程。
当容器不在运行时,它们仅仅是一个“tarball 的 tarballs”,其中每个文件系统层都存储在一个文件中。镜像仍然存储在主机系统、多个主机系统,或是容器之前运行或拉取过的地方。
如果你对“tarball”这个术语不熟悉,它是由 tar Unix 命令创建的一个文件,该命令用于将多个文件归档并压缩成一个文件。术语“tarball”是由创建该文件的命令 tar(即 磁带归档 的缩写)和 ball(指的是文件的捆绑包)组合而成的。
相反,虚拟机有自己的虚拟磁盘,用于存储整个操作系统。尽管有一些非常轻量级的虚拟机技术,但虚拟机和容器之间的大小通常存在数量级的差异。
虽然有些人把容器称为轻量级虚拟机,但这完全不正确。它们的隔离方式不同,而且需要更加关注它们在节点上运行的细节。
从这一部分,你可能会觉得我们在暗示容器不安全。实际上,真相完全相反。保护 Kubernetes 集群及其上运行的容器需要对细节的关注,以及对容器与虚拟机不同之处的理解。由于许多人了解虚拟机,容易把它们与容器进行比较,但这样做会让你处于不利地位,因为它们是截然不同的技术。
一旦你理解了默认配置的限制以及由此带来的潜在危险,就可以解决这些“问题”。
容器突破
容器越界指的是容器内的进程访问了底层节点。一旦进入节点,攻击者就可以访问所有其他 Pod 以及节点在环境中的所有能力。容器越界还可能是挂载本地文件系统的问题。来自 securekubernetes.com 的一个示例,最初由 Isovalent 的 Field CTO Duffie Cooley 提出,使用容器挂载本地文件系统。在 KinD 集群上运行此操作会打开对节点文件系统的读写权限:
kubectl run r00t --restart=Never -ti --rm --image lol --overrides '{"spec":{"hostPID": true, "containers":[{"name":"1","image":"alpine","command":["nsenter","--mount=/proc/1/ns/mnt","--","/bin/bash"],"stdin": true,"tty":true,"imagePullPolicy":"IfNotPresent","securityContext":{"privileged":true}}]}}'
If you don't see a command prompt, try pressing Enter.
上述代码中的run命令启动了一个容器,该容器添加了一个对本示例至关重要的选项,hostPID: true,它允许容器共享主机的进程命名空间。你可以看到其他几个选项,如--mount和设置privileged为true的安全上下文设置。所有这些选项加起来将允许我们写入主机的文件系统。
现在你已经进入容器,执行 ls 命令查看文件系统。注意,提示符是 root@r00t:/#,这表明你已经在容器中,而不是在主机上:
root@r00t:/# ls
bin boot build dev etc home kind lib lib32 lib64 libx32 media mnt opt proc root run sbin srv sys tmp usr var
为了证明我们已经将主机的文件系统映射到容器中,请创建一个名为 this_is_from_a_container 的文件,并退出容器:
root@r00t:/# touch this_is_from_a_container
root@r00t:/# exit
最后,让我们查看主机的文件系统,以确认容器是否创建了该文件。由于我们使用 KinD 部署了一个单一工作节点,因此我们需要使用 Docker 命令 exec 进入工作节点。如果你使用的是本书中的 KinD 集群,工作节点的名称为 cluster01-worker:
docker exec -ti cluster01-worker ls /
bin boot build dev etc home kind lib lib32 lib64 libx32 media mnt opt proc root run sbin srv sys this_is_from_a_container tmp usr var
就是这样!在这个例子中,启动了一个容器并挂载了本地文件系统。从 Pod 内部创建了 this_is_from_a_container 文件。退出 Pod 后进入节点容器时,文件仍然存在。一旦攻击者获取了节点文件系统的访问权限,他们也就能够访问 kubelet 的凭证,从而可能暴露整个集群。
想象一下,确实不难想象一系列事件可能导致一个比特币矿工(甚至更糟)在集群上运行。一场钓鱼攻击获得了开发者用于集群的凭证。即使这些凭证只访问一个命名空间,也会创建一个容器来获取 kubelet 的凭证,然后从那里启动容器,悄悄地在环境中部署矿工。确实有多种缓解措施可以防止这种攻击,其中包括以下几种:
-
多因素认证,可以防止钓鱼凭证被滥用
-
只授权特定容器
-
Gatekeeper 策略,通过停止容器以
privileged身份运行,防止了此攻击 -
正确保护的镜像
需要注意的是,Kubernetes 默认并不会提供这些缓解措施,这也是你正在阅读这本书的主要原因之一!
我们在之前的章节中已经讨论了身份验证及其多因素认证的重要性。我们甚至通过仪表板设置了一个矿工!这再次说明了身份验证在 Kubernetes 中是如此重要的主题。
接下来的两种方法可以使用 Gatekeeper 实现。我们在第十一章《使用 Open Policy Agent 扩展安全性》中讨论了如何预先授权容器和镜像仓库。本章将重点介绍如何使用 Gatekeeper 执行以节点为中心的策略,例如,是否应以特权模式运行 Pod。
最后,安全的核心是正确设计的镜像。在物理机器和虚拟机的情况下,这通过保护基础操作系统来实现。当你安装操作系统时,并不会选择所有可能的选项进行安装。在服务器上运行任何非必需的程序被认为是一种不良做法,尤其是对于其角色或功能不需要的服务。这个做法也应延续到运行在集群中的镜像上,镜像中应仅包含应用所需的必要二进制文件。
鉴于在集群中正确保护镜像的重要性,接下来的部分将从安全的角度探讨容器设计。虽然这与 Gatekeeper 的策略执行没有直接关系,但它是节点安全的一个重要起点。理解如何安全地构建容器同样重要,以便更好地调试和管理节点安全策略。构建一个锁定的容器可以使节点安全管理变得更加容易。
正确设计容器
在探索如何使用 Gatekeeper 保护节点之前,重要的是先了解容器的设计。使用策略来减轻对节点的攻击时,最困难的部分通常是许多容器被构建为以 root 身份运行。一旦应用了受限策略,即使容器在策略应用后运行正常,重新加载时也不会启动。这在多个层面上都是一个问题。多年来,系统管理员已经学会了在网络计算中不要以 root 身份运行进程,尤其是像 Web 服务器这样的服务,这些服务通过不受信任的网络进行匿名访问。
所有网络都应被视为“不受信任的”。假设所有网络都是敌对的,这会导致更安全的实施方式。它还意味着需要安全的服务必须经过身份验证。这种概念被称为零信任(zero trust)。它已被身份专家使用并倡导多年,但由 Google 的 BeyondCorp 白皮书(cloud.google.com/beyondcorp)在 DevOps 和云原生世界中普及。零信任的概念也应适用于你的集群内部!
代码中的漏洞可能导致对底层计算资源的访问,从而可能导致容器的突破。如果在不需要时以 root 用户身份在特权容器中运行,则如果通过代码漏洞被利用,可能会导致突破。
2017 年,Equifax 的数据泄露事件利用了 Apache Struts Web 应用框架中的一个漏洞,在服务器上执行代码,然后被用来渗透并提取数据。如果这个易受攻击的 Web 应用程序运行在 Kubernetes 上并且使用特权容器,那么这个漏洞可能会导致攻击者获得对集群的访问权限。
构建容器时,至少应遵循以下几点:
-
以非 root 用户身份运行:绝大多数应用程序,尤其是微服务,不需要 root 权限。不要以 root 身份运行。
-
仅写入卷:如果你不向容器写入内容,就不需要写入权限。卷可以由 Kubernetes 控制。如果需要写入临时数据,使用
emptyVolume对象,而不是写入容器的文件系统。这样可以更容易检测到那些试图在运行时对容器进行更改的恶意行为,比如替换二进制文件或文件。 -
最小化容器中的二进制文件:这可能会比较棘手。有些人主张使用“无发行版”容器,这些容器只包含应用程序的二进制文件,静态编译——没有 shell,没有工具。这在调试应用程序为什么无法按预期运行时可能会带来问题。这是一个微妙的平衡。在 Kubernetes 1.25 中,引入了短暂容器来简化这一过程。我们将在后续部分详细介绍。
-
扫描容器中的已知常见漏洞和暴露(CVE),并经常重建:容器的一大优势是可以轻松扫描已知的 CVE。有几种工具和注册表可以为你完成这项工作,例如 Anchor 的Grype(
github.com/anchore/grype)或 Aqua Security 的Trivy(github.com/aquasecurity/trivy)。一旦 CVE 被修复,就应重新构建容器。一个几个月甚至几年没有重建的容器,就像一个没有打补丁的服务器一样危险。
让我们深入探讨调试“无发行版”镜像和扫描容器。
使用和调试无发行版镜像
“无发行版”镜像的概念并不新鲜。谷歌是最早推动其广泛使用的公司之一(github.com/GoogleContainerTools/distroless),最近,Chainguard 也开始发布并维护基于其Wolfi(github.com/wolfi-dev)Linux 发行版构建的无发行版镜像。其理念是,基础镜像不是 Ubuntu、Red Hat 或其他常见的 Linux 发行版,而是一个运行系统所需的最小二进制文件集。例如,Java 17 镜像仅包含 OpenJDK,没有工具,也没有其他实用程序,只有 JDK。从安全角度来看,这非常好,因为可被用来破坏环境的“东西”更少了。当攻击者不需要一个 shell 来运行命令时,为什么要让他们的工作更轻松呢?
这种方法有两个主要缺点:
-
调试运行中的容器:没有
dig或nslookup,你怎么知道问题出在 DNS 上?我们可能知道问题总是出在 DNS 上,但你仍然需要证明它。你可能还需要调试网络服务、连接等。如果没有调试这些服务所需的常用工具,你怎么确定问题呢? -
支持和兼容性:使用常见发行版作为基础镜像的好处之一是,你的企业可能已经与发行版供应商签订了支持合同。Google 的 Distroless 基于 Debian Linux,而 Debian 没有官方的供应商支持。Wolfi 基于 Alpine,而 Alpine 也没有自己的支持(尽管 Chainguard 为其镜像提供商业支持)。如果你的容器出现问题,且你怀疑问题出在基础镜像中,你不会从其他发行版的供应商那里获得太多帮助。
支持和兼容性的问题其实并不是技术问题,而是需要你的团队处理的风险管理问题。如果你使用的是商业产品,通常这是由你的支持合同涵盖的。如果你谈论的是自家开发的容器,理解风险和潜在的缓解措施非常重要。
调试无发行版容器现在比以前容易多了。在 1.25 版本中,Kubernetes 引入了临时容器的概念,允许你将一个容器附加到正在运行的 Pod 上。这个临时容器可以包含那些你在无发行版镜像中没有的调试工具。kubectl debug命令也被添加了,方便使用。
首先,在新集群中启动 Kubernetes 仪表盘,然后尝试附加一个 shell:
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.7.0/aio/deploy/recommended.yaml
.
.
.
$ export K8S_DB_POD=$(kubectl get pods -l k8s-app=kubernetes-dashboard -n kubernetes-dashboard -o json | jq -r '.items[0].metadata.name')
$ kubectl exec -ti $K8S_DB_POD -n kubernetes-dashboard -- sh
error: Internal error occurred: error executing command in container: failed to exec in container: failed to start exec "24b9dba21332299828b4d8f46c360c8afe0cadfd693e6651694a63917d28b910": OCI runtime exec failed: exec failed: unable to start container process: exec: "sh": executable file not found in $PATH: unknown
最后一行告诉我们,kubernetes-dashboard Pod 中没有 shell。当我们尝试exec进入 Pod 时,它失败了,因为找不到可执行文件。现在,我们可以附加一个调试 Pod,允许我们使用调试工具:
$ kubectl debug -it --attach=true -c debugger --image=busybox $K8S_DB_POD -n kubernetes-dashboard
If you don't see a command prompt, try pressing enter.
/ # ps -A
PID USER TIME COMMAND
1 root 0:00 sh
14 root 0:00 ps -A
/ # ping
BusyBox v1.36.1 (2023-07-17 18:29:09 UTC) multi-call binary.
Usage: ping [OPTIONS] HOST
.
.
.
/ # nslookup
BusyBox v1.36.1 (2023-07-17 18:29:09 UTC) multi-call binary.
Usage: nslookup [-type=QUERY_TYPE] [-debug] HOST [DNS_SERVER]
Query DNS about HOST
QUERY_TYPE: soa,ns,a,aaaa,cname,mx,txt,ptr,srv,any
我们能够将 busybox 镜像附加到仪表盘 Pod,并使用我们的工具!
这肯定有助于调试,但如果我们需要进入仪表盘进程怎么办?请注意,当我运行ps命令时,没有看到仪表盘进程。那是因为一个 Pod 中的容器都运行各自的进程空间,并且共享点(如volumeMounts)有限。所以,虽然这可能有助于测试网络资源,但它离我们的工作负载还不够接近。我们可以在部署中添加shareProcessNamespace: true选项,但现在我们的容器都共享同一个进程空间,失去了一层隔离性。你可以在需要时修补一个正在运行的部署,但现在你正在重新启动 Pod,这可能会自动清除问题。
Distroless 镜像是最小化镜像,可以通过增加攻击者利用您的 Pod 作为攻击向量的难度来降低您的安全风险。减少镜像确实存在权衡,重要的是要记住这些权衡。虽然您将获得一个更小且更难被妥协的镜像,但这可能会增加调试操作的难度,并可能对供应商支持协议产生影响。
接下来,我们将看看如何将扫描已知漏洞的镜像应用到您的构建流程中。
扫描已知漏洞的镜像
如何知道您的镜像是否存在漏洞?有一个常见的软件漏洞报告位置,称为通用漏洞和暴露(CVE)数据库,由 MITRE 提供。理论上,这是研究人员和用户可以向供应商报告漏洞的常见地方。理论上,用户可以从中了解他们运行的软件中是否存在已知漏洞的地方。
这只是寻找漏洞问题的极度简化。在过去几年中,对 CVE 数据质量持极端批评的观点主要是因为 CVE 数据库的管理和维护方式。不幸的是,这确实是唯一的常见数据库。话虽如此,扫描容器并比对容器中的软件版本与 CVE 数据库中的内容是常见做法。如果找到漏洞,这将促使团队采取纠正措施。
一方面,快速修补已知漏洞是减少安全风险的最佳方法之一。另一方面,快速修补可能不需要修补的漏洞可能会导致系统故障。这是一个需要不仅理解扫描器工作原理,还需要自动化和测试来增强更新基础设施信心的难以平衡的行为。
扫描 CVE 是报告安全问题的标准方式。应用程序和操作系统供应商将通过补丁更新 CVE 中的代码以修复问题。然后,安全扫描工具将使用此信息在容器存在已知已修复问题时采取行动。
有几个开源扫描选项。我喜欢使用 Anchore 的 Grype,但 AquaSecurity 的 Trivy 也是一个很好的选择。这里重要的是,这两个扫描工具都会拉取你的容器,将已安装的包和库与 CVE 数据库进行比对,并告诉你有哪些 CVE 以及它们是否已修复。例如,如果你扫描两个 OpenUnison 镜像,结果会稍有不同。当扫描 ghcr.io/openunison/openunison-k8s:1.0.37-a207c4 时,发现有 43 个已知漏洞。这个镜像大约有六天历史,所以有大约 15 个中等风险的 CVE 已经被修复。今晚,当我们的流程运行时(稍后我会解释),将会生成一个新的容器来修复这些 CVE。如果我们对一个两周前构建的容器运行 Grype,则会发现 45 个已知的 CVE,其中 2 个是在最新构建中修复的。
这项工作的重点是,扫描工具在容器卫生方面非常有用。在 Tremolo Security,我们每晚都会扫描已发布的镜像,如果有新的操作系统级 CVE 被修复,我们就会重建镜像。这确保了我们的镜像始终保持最新。
容器扫描并不是唯一的扫描方式。我们还使用 snyk.io 扫描我们的构建和依赖项,以查找已知漏洞,并将它们升级到可用的修复版本。我们之所以有信心这么做,是因为我们的自动化测试包括数百个自动化测试,能够捕捉到这些升级中的问题。我们的目标是在发布时没有可修复的 CVE。除非有绝对关键的漏洞,比如 2021 年的 Log4J 事件,否则我们通常每年发布四到五个版本。
作为一个开源项目的维护者,我不得不提到社区中常见的一个痛点。容器扫描工具会告诉你某个库是否存在,但它们不会告诉你这个库是否存在漏洞。对于什么构成漏洞,这里面有很多细微的差别。一旦你在扫描工具中发现了一个“命中”某个项目的结果,请不要立即在 GitHub 上打开一个问题。这会给项目维护者带来大量几乎没有价值的工作。
最后,你可能会过于依赖扫描工具。一些非常有才华的安全专家(Brad Geesaman、Ian Coldwater、Rory McCune 和 Duffie Cooley)在 KubeCon EU 2023 上讨论了如何欺骗扫描工具,主题是恶意合规:反思信任容器扫描工具:kccnceu2023.sched.com/event/1Hybu/malicious-compliance-reflections-on-trusting-container-scanners-ian-coldwater-independent-duffie-cooley-isovalent-brad-geesaman-ghost-security-rory-mccune-datadog。我强烈推荐花时间观看这个视频,以及它提出的关于扫描工具依赖性的问题。
一旦你扫描过容器并限制了容器的运行方式,如何知道它们是否能正常工作呢?在一个限制性的环境中进行测试是非常重要的。截止写作时,市场上任何 Kubernetes 发行版中最严格的默认配置属于 Red Hat 的 OpenShift。除了合理的默认策略,OpenShift 还会以随机用户 ID 运行 Pod,除非 Pod 定义中指定了特定的 ID。
即使 OpenShift 不是你生产环境使用的发行版,测试你的容器在 OpenShift 上运行也是个好主意。如果一个容器能够在 OpenShift 上运行,那它很可能能在任何集群上运行,不论集群使用什么样的安全策略。最简单的方法是使用 Red Hat 的 CodeReady Containers (developers.redhat.com/products/codeready-containers)。这个工具可以在你的本地笔记本上运行,并启动一个最小化的 OpenShift 环境,用于测试容器。
虽然 OpenShift 自带了非常严格的安全控制,但它并不使用Pod 安全策略(PSP)、Pod 安全标准或 Gatekeeper。它有自己的策略系统,早于 PSP 出现,叫做安全上下文约束(SCC)。SCC 与 PSP 类似,但不使用 RBAC 来与 Pod 关联。
现在我们已经探讨了如何创建安全的容器镜像,下一步是确保我们的集群被正确构建,防止那些不符合这些标准的镜像运行。
使用 Gatekeeper 强制执行节点安全
到目前为止,我们已经看到当容器在没有任何安全策略的情况下允许在节点上运行时可能发生的情况。我们还审视了构建安全容器的内容,这将使得执行节点安全更加容易。下一步是研究如何使用 Gatekeeper 设计并构建策略,以锁定你的容器。
那么,Pod 安全策略怎么样呢?
Kubernetes 难道没有内建的机制来强制执行节点安全吗?有的!在 2018 年,Kubernetes 项目决定 Pod 安全策略(PSP)API 永远不会离开 beta 阶段。这个配置过于复杂,是 Linux 聚焦配置选项与 RBAC 分配的混合体。最终确定修复这个问题可能会导致当前发布与最终版本不兼容。因此,项目做出了一个艰难的决定,废弃并移除了这个 API。
当时,曾声明 PSP API 只有在替代方案准备好发布时才会被移除。2020 年,Kubernetes 项目采纳了一项新政策,规定任何 API 都不能在 beta 阶段停留超过三个版本。这迫使该项目重新评估如何继续替代 PSP。在 2021 年 4 月,Tabitha Sable 撰写了一篇关于 PSP 未来的博客文章(kubernetes.io/blog/2021/04/06/podsecuritypolicy-deprecation-past-present-and-future/)。简而言之,PSP 在 1.21 版本正式弃用,并在 1.25 版本中移除。它的替代方案 Pod Security Standards(PSS)在 1.26 版本成为 GA(通用可用)。我们将在介绍如何使用 Gatekeeper 保护节点免受 pod 威胁后,详细讨论这些内容。
PSP、PSA 和 Gatekeeper 之间有哪些区别?
在深入了解如何使用 Gatekeeper 实现节点安全之前,让我们先看看遗留的 PSP、全新的 Pod Security Admission(PSA)和 Gatekeeper 有何不同。如果你熟悉 PSP,这将是迁移的有用指南。如果你从未使用过 PSP,这可以帮助你了解在遇到问题时应查看哪些内容。
这三种技术的共同点是它们都作为准入控制器实现。正如我们在 第十一章《使用 Open Policy Agent 扩展安全性》中所学,准入控制器用于提供 API 服务器原生功能之外的额外检查。在 Gatekeeper、PSP 和 PSA 的情况下,准入控制器确保 pod 的定义具有正确的配置,以便在最小权限下运行。这通常意味着以非 root 用户身份运行,限制对主机的访问等。如果未满足所需的安全级别,准入控制器会失败,阻止 pod 运行。
虽然这三种技术都作为准入控制器运行,但它们以非常不同的方式实现各自的功能。PSP 通过首先定义一个 PodSecurityPolicy 对象,然后定义 RBAC Role 和 RoleBinding 对象来允许 ServiceAccount 以某个策略运行。PSP 准入控制器会根据创建 Pod 的“用户”或 Pod 运行所在的 ServiceAccount 是否经过授权(根据 RBAC 绑定)来做出决定。这导致了在设计和调试策略应用时的困难。如果用户可以提交一个 Pod 也很难授权,因为用户通常不再创建 Pod 对象。用户会创建 Deployments、StatefulSets 或 Jobs。然后,有一些控制器会使用它们自己的 ServiceAccounts 运行,这些控制器再创建 Pod。PSP 准入控制器永远不知道是谁提交了原始对象。在上一章中,我们介绍了 Gatekeeper 如何通过命名空间和标签匹配绑定策略;节点安全策略不会改变这一点。稍后,我们将深入探讨如何分配策略。
PSA 是在命名空间级别实现的,而不是在单个 Pod 级别实现的。假设命名空间是集群的安全边界,那么在一个命名空间中运行的任何 Pod 都应该共享相同的安全上下文。这通常是可行的,但也存在一些限制。例如,如果你需要一个 init 容器来更改挂载点上的文件权限,那么你可能会遇到 PSA 带来的问题。
除了以不同方式分配策略外,Gatekeeper、PSP 和 PSA 在处理重叠策略时也有所不同。PSP 会尝试根据请求的帐户和权限选择最佳策略。这使得你可以定义一个高级的普遍性策略,拒绝所有权限,然后为具体的使用案例创建特定的策略,例如允许 NGINX Ingress Controller 在端口 443 上运行。相反,Gatekeeper 要求所有策略都必须通过。没有所谓的最佳策略;所有策略必须都通过。这意味着你不能应用一个普遍的策略然后做出例外。你必须为每个使用案例明确地定义你的策略。PSA 是跨命名空间通用的,因此在 API 层面没有例外,也没有变化。你可以为用户、运行时类或命名空间设置特定的豁免,但这些是全局且静态的。
这三种方法的另一个区别在于策略的定义方式。PSP 规范是一个 Kubernetes 对象,主要基于 Linux 内建的安全模型。该对象本身根据需要加入了新属性,但方式不一致。这导致了一个混乱的对象,无法很好地支持 Windows 容器的添加。相反,Gatekeeper 有一系列预构建的策略,可以从其 GitHub 仓库获取:github.com/open-policy-agent/gatekeeper-library/tree/master/library/pod-security-policy。与其只有一个策略,每个策略需要单独应用。PSA 定义了基于常见安全模式的配置文件,实际上没有太多需要定义的内容。
最后,PSP 访问控制器有一些内建的变异。例如,如果你的策略不允许 root 用户,并且你的 pod 没有定义要运行的用户,PSP 访问控制器会将用户 ID 设置为 1。Gatekeeper 具有变异能力(我们在 第十一章,使用 Open Policy Agent 扩展安全性 中讲解过),但该能力需要显式配置来设置默认值。PSA 没有变异能力。
在了解了 PSP、PSA 和 Gatekeeper 之间的区别后,接下来让我们深入探讨如何在集群中授权节点安全策略。
授权节点安全策略
在前一节中,我们讨论了 Gatekeeper、PSP 和 PSA 之间授权策略的区别。现在,我们将看看如何为策略定义你的授权模型。在我们进一步讨论之前,应该先解释一下我们所说的“授权策略”是什么意思。
当你创建一个 pod,通常通过 Deployment 或 StatefulSet,你可以选择你需要的节点级别功能,这些设置位于 pod 内的 securityContext 部分。你可以请求特定的能力或主机挂载。Gatekeeper 会检查你的 pod 定义,并决定或授权你的 pod 定义是否满足策略要求,通过其约束的 match 部分匹配一个适用的 ConstraintTemplate。Gatekeeper 的 match 部分让你可以根据命名空间、对象类型和对象上的标签进行匹配。至少,你需要包括命名空间和对象类型。标签可能会更加复杂。
决定标签是否是授权策略的适当方式的一个重要因素是:谁可以设置标签以及为什么。在单租户集群中,标签是创建受限部署的好方法。你可以定义可以通过标签直接应用的特定约束。例如,你可能在一个命名空间中有一个操作员不希望访问主机挂载,而某个 pod 需要访问。创建具有特定标签的策略将允许你对操作员应用比 pod 更严格的策略。
这种方法的风险在于多租户集群,在这种集群中,作为集群所有者,你无法限制可以应用于 Pod 的标签。Kubernetes 的 RBAC 实现并未提供授权特定标签的机制。你可以通过 Gatekeeper 实现一些功能,但那将是 100% 的定制。由于你无法阻止命名空间管理员给 Pod 打标签,受损的命名空间管理员账户可以被用来启动一个特权 Pod,而 Gatekeeper 无法对此进行检查。
当然,你可以使用 Gatekeeper 限制标签。问题在于,类似于 PSP 的问题,Gatekeeper 并不知道哪个创建者在 Pod 级别创建了标签,因为 Pod 通常是由控制器创建的。你可以在 Deployment 或 StatefulSet 级别进行强制执行,但这将意味着其他控制器类型不受支持。这就是为什么 PSA 使用命名空间作为标签的边界点。命名空间是集群的安全边界。如果需要,你还可以为 init 容器预留特定的例外情况。
在第十一章,使用 Open Policy Agent 扩展安全性 中,我们学习了如何在 Rego 中构建策略并使用 Gatekeeper 部署它们。在本章中,我们讨论了如何安全地构建镜像、PSP 和 Gatekeeper 在节点安全中的差异,以及如何在集群中授权策略。接下来,我们将加固我们的测试集群。
部署和调试节点安全策略
在深入学习了 Gatekeeper 中构建节点安全策略的理论后,让我们开始加固我们的测试集群。第一步是从一个干净的集群开始并部署 Gatekeeper:
$ kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/master/deploy/gatekeeper.yaml
接下来,我们需要部署节点的 ConstraintTemplate 对象。Gatekeeper 项目构建并维护了一套模板库,复制现有的 PodSecurityPolicy 对象,位于 github.com/open-policy-agent/gatekeeper-library/tree/master/library/pod-security-policy。对于我们的集群,我们将部署所有策略,除了只读文件系统 seccomp、selinux、apparmor、flexvolume 和主机卷策略。我选择不部署只读文件系统,因为写入容器的文件系统仍然非常常见,尽管数据是临时的,强制执行这一点可能会造成更多的麻烦而非好处。seccomp、apparmor 和 selinux 策略未包括在内,因为我们运行的是 KinD 集群。最后,我们忽略了卷,因为这不是我们计划关注的特性。然而,查看所有这些策略,看看它们是否应应用于您的集群,是个好主意。chapter12 文件夹中有一个脚本可以为我们部署所有的模板。运行 chapter12/deploy_gatekeeper_psp_policies.sh。完成后,我们的 ConstraintTemplate 对象已经部署,但它们并未被强制执行,因为我们还没有设置任何策略实现对象。在我们进行此操作之前,我们应该先设置一些理智的默认值。
生成安全上下文默认值
在 第十一章,使用 Open Policy Agent 扩展安全性 中,我们讨论了使用变更 webhook 生成理智默认值与显式配置之间的权衡。
我喜欢理智的默认设置,因为它们能带来更好的开发者体验,并且使保持安全变得更加容易。Gatekeeper 项目提供了一些示例变更集,用于此目的,位于 github.com/open-policy-agent/gatekeeper-library/tree/master/mutation/pod-security-policy。在这一章中,我对它们进行了些微调整。让我们部署它们,然后重新创建所有的 pods,以便它们能够应用我们设定的“理智默认”,然后再推出我们的约束实现:
$ kubectl create -f chapter12/default_mutations.yaml
assign.mutations.gatekeeper.sh/k8spspdefaultallowprivilegeescalation created
assign.mutations.gatekeeper.sh/k8spspfsgroup created
assign.mutations.gatekeeper.sh/k8spsprunasnonroot created
assign.mutations.gatekeeper.sh/k8spsprunasgroup created
assign.mutations.gatekeeper.sh/k8spsprunasuser created
assign.mutations.gatekeeper.sh/k8spspsupplementalgroups created
assign.mutations.gatekeeper.sh/k8spspcapabilities created
$ sh chapter12/delete_all_pods_except_gatekeeper.sh
calico-system
pod "calico-kube-controllers-7f58dbcbbd-ckshb" deleted
pod "calico-node-g5cwp" deleted
现在,我们可以部署一个 NGINX pod 并查看它现在是否具有默认的安全上下文:
$ kubectl create ns test-mutations
$ kubectl create deployment test-nginx --image=ghcr.io/openunison/openunison-k8s-html:latest -n test-mutations
$ kubectl get pods -l app=test-nginx -o jsonpath='{.items[0].spec.securityContext}' -n test-mutations
{"fsGroup":3000,"supplementalGroups":[3000]}
我们的 NGINX pod 现在有一个 securityContext,它决定容器应以哪个用户身份运行,是否具有特权,并且是否需要任何特殊能力。如果将来由于某些原因,我们希望容器以不同的进程运行,而不是更改每个清单,我们现在可以更改我们的变更配置。现在我们的默认设置已经到位并应用,下一步是实现我们的 ConstraintTemplates 实例来强制执行我们的策略。
强制执行集群策略
部署好我们的变更后,现在可以部署约束实现了。与ConstraintTemplate对象一样,Gatekeeper 项目为每个模板提供了示例模板实现。我为本章编写了一个简化版,位于chapter12/minimal_gatekeeper_constraints.yaml,设计上是为了在集群中拥有最小的权限集,忽略kube-system和calico-system。部署这个 YAML 文件并等待几分钟:
$ kubectl apply -f chapter12/minimal_gatekeeper_constraints.yaml
k8spspallowprivilegeescalationcontainer.constraints.gatekeeper.sh/privilege-escalation-deny-all created
k8spspcapabilities.constraints.gatekeeper.sh/capabilities-drop-all created
k8spspforbiddensysctls.constraints.gatekeeper.sh/psp-forbid-all-sysctls created
k8spsphostfilesystem.constraints.gatekeeper.sh/psp-deny-host-filesystem created
k8spsphostnamespace.constraints.gatekeeper.sh/psp-bloack-all-host-namespace created
k8spsphostnetworkingports.constraints.gatekeeper.sh/psp-deny-all-host-network-ports created
k8spspprivilegedcontainer.constraints.gatekeeper.sh/psp-deny-all-privileged-container created
k8spspprocmount.constraints.gatekeeper.sh/psp-proc-mount-default created
k8spspallowedusers.constraints.gatekeeper.sh/psp-pods-allowed-user-ranges created
记得在第十一章,使用 Open Policy Agent 扩展安全性中提到,Gatekeeper 相比于通用的 OPA,一个关键特性就是它不仅能作为验证 webhook,还能根据策略审计现有对象。我们正在等待,让 Gatekeeper 有机会对我们的集群执行审计。审计违规会列在每个ConstraintTemplate的每个实现的状态中。
为了更容易查看我们的集群符合规范的程度,我写了一个小脚本,列出了每个ConstraintTemplate的违规数量:
$ sh chapter12/show_constraint_violations.sh
k8spspallowedusers.constraints.gatekeeper.sh 16
k8spspallowprivilegeescalationcontainer.constraints.gatekeeper.sh 2
k8spspcapabilities.constraints.gatekeeper.sh 2
k8spspforbiddensysctls.constraints.gatekeeper.sh 0
k8spsphostfilesystem.constraints.gatekeeper.sh 1
k8spsphostnamespace.constraints.gatekeeper.sh 0
k8spsphostnetworkingports.constraints.gatekeeper.sh 1
k8spspprivilegedcontainer.constraints.gatekeeper.sh 0
k8spspprocmount.constraints.gatekeeper.sh 0
k8spspreadonlyrootfilesystem.constraints.gatekeeper.sh null
我们有几个违规。如果你没有确切的数字也没关系,下一步是调试并纠正它们。
调试约束违规
在我们实施约束后,有几个违规需要修正。让我们看看权限提升策略违规:
$ kubectl get k8spspallowprivilegeescalationcontainer.constraints.gatekeeper.sh -o jsonpath='{$.items[0].status.violations}' | jq -r
[
{
"enforcementAction": "deny",
"kind": "Pod",
"message": "Privilege escalation container is not allowed: controller",
"name": "ingress-nginx-controller-744f97c4f-msmkz",
"namespace": "ingress-nginx"
}
]
Gatekeeper 告诉我们,位于ingress-nginx命名空间中的ingress-nginx-controller-744f97c4f-msmkz Pod 正在尝试提升其权限。查看其SecurityContext,我们看到以下内容:
$ kubectl get pod ingress-nginx-controller-744f97c4f-msmkz -n ingress-nginx -o jsonpath='{$.spec.containers[0].securityContext}' | jq -r
{
"allowPrivilegeEscalation": true,
"capabilities": {
"add": [
"NET_BIND_SERVICE"
],
"drop": [
"all"
]
},
"runAsGroup": 2000,
"runAsNonRoot": true,
"runAsUser": 101
}
Nginx 请求能够提升它的权限,添加NET_BIND_SERVICE权限,使其能够在不以 root 用户身份运行的情况下,在端口443上运行。回到我们的约束违规列表中,除了有一个权限提升违规外,还有一个能力违规。让我们查看这个违规:
$ kubectl get k8spspcapabilities.constraints.gatekeeper.sh -o jsonpath='{$.items[0].status.violations}' | jq -r
[
{
"enforcementAction": "deny",
"kind": "Pod",
"message": "container <controller> has a disallowed capability. Allowed
capabilities are []",
"name": "ingress-nginx-controller-744f97c4f-msmkz",
"namespace": "ingress-nginx"
}
]
是同一个容器违反了这两个约束。在确定了哪些 Pod 不符合规范之后,我们接下来将修复它们的配置。
在本章早些时候,我们讨论了 PSP 和 Gatekeeper 的区别,其中一个关键区别是,尽管 PSP 尝试应用“最佳”策略,但 Gatekeeper 会根据所有适用的约束进行评估。这意味着,在 PSP 中,你可以创建一个“通用”策略(通常称为“默认限制”策略),然后为特定的 Pod 创建更宽松的策略,而 Gatekeeper 不会让你这样做。为了防止这些违规阻止 Nginx 运行约束实现,它们必须更新以忽略我们的 Nginx Pods。最简单的做法是将ingress-nginx添加到我们的excludednamespaces列表中。
我在chapter12/make_cluster_work_policies.yaml中的所有约束实现都做了这个操作。使用apply命令部署:
kubectl apply -f chapter12/make_cluster_work_policies.yaml
k8spsphostnetworkingports.constraints.gatekeeper.sh/psp-deny-all-host-network-ports configured
k8spsphostfilesystem.constraints.gatekeeper.sh/psp-deny-host-filesystem configured
k8spspcapabilities.constraints.gatekeeper.sh/capabilities-drop-all configured
k8spspallowprivilegeescalationcontainer.constraints.gatekeeper.sh/privilege-escalation-deny-all configured
几分钟后,让我们运行违规检查:
sh ./chapter12/show_constraint_violations.sh
k8spspallowedusers.constraints.gatekeeper.sh 12
k8spspallowprivilegeescalationcontainer.constraints.gatekeeper.sh 0
k8spspcapabilities.constraints.gatekeeper.sh 0
k8spspforbiddensysctls.constraints.gatekeeper.sh 0
k8spsphostfilesystem.constraints.gatekeeper.sh 0
k8spsphostnamespace.constraints.gatekeeper.sh 0
k8spsphostnetworkingports.constraints.gatekeeper.sh 0
k8spspprivilegedcontainer.constraints.gatekeeper.sh 0
k8spspprocmount.constraints.gatekeeper.sh 0
k8spspreadonlyrootfilesystem.constraints.gatekeeper.sh null
剩下的唯一违规项是针对我们允许用户的约束。这些违规都来自gatekeeper-system,因为 Gatekeeper 的 Pod 在其SecurityContext中没有指定用户。这些 Pod 没有接收到我们合理的默认配置,因为在 Gatekeeper 的Deployment中,gatekeeper-system命名空间被忽略。尽管被忽略,它仍然被列为违规项,尽管它不会被强制执行。
现在我们已经消除了违规项,任务就完成了吗?并不完全是。尽管 Nginx 没有产生任何错误,但我们并没有确保它是以最小权限运行的。如果有人在ingress-nginx命名空间中启动 Pod,它可能会请求权限和额外的功能,而不会被 Gatekeeper 阻止。我们需要确保在ingress-nginx命名空间中启动的任何 Pod 都无法提升其权限,超过所需权限。除了将ingress-nginx命名空间从集群范围的策略中移除外,我们还需要创建一个新的约束实现,限制ingress-nginx命名空间中的 Pod 能够请求哪些能力。
我们知道 Nginx 需要提升权限的能力,并请求NET_BIND_SERVICE,因此我们可以创建一个约束实现:
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sPSPCapabilities
metadata:
name: capabilities-ingress-nginx
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
namespaces: ["ingress-nginx"]
parameters:
requiredDropCapabilities: ["all"]
allowedCapabilities: ["NET_BIND_SERVICE"]
我们创建了一个与Deployment所需的securityContext部分相匹配的约束实现。我们没有为权限升级创建单独的约束实现,因为该ConstraintTemplate没有参数。它要么被强制执行,要么不被强制执行。对于ingress-nginx命名空间中的这个约束,一旦该命名空间被移出集群范围策略,就不需要做额外的工作。
我为其他违规项重复了这个调试过程,并将它们添加到chapter12/enforce_node_policies.yaml中。你可以部署它们以完成该过程。
你可能会想,为什么我们是在命名空间级别进行强制执行,而不是使用特定标签来隔离单个 Pod?我们在本章前面讨论过授权策略,继续延续这里的主题,我认为基于标签的额外强制执行并没有太大价值。任何能够在该命名空间中创建 Pod 的人都可以设置标签。进一步限制范围对安全性贡献不大。
部署和调试策略的过程非常注重细节。在单租户集群中,这可能是一次性操作或是罕见操作,但在多租户集群中,过程无法扩展。接下来,我们将讨论在多租户集群中应用节点安全性的策略。
在多租户集群中扩展策略部署
在前面的示例中,我们采用了“批量小批量”方式处理节点安全性。我们创建了一个单一的集群范围策略,并根据需要添加了例外。由于一些原因,这种方法在多租户环境中无法扩展:
-
限制条件中
match部分的excludedNamespaces属性是一个列表,且很难通过自动化方式进行修补。列表需要被修补,包括原始列表,因此它不仅仅是一个简单的“应用此 JSON”操作。 -
你不希望在多租户系统中更改全局对象。添加新对象并将其链接到真实数据源更为简单。使用标签追踪为何创建新的约束实现比追踪为何更改全局对象要容易。
-
你需要尽量减少全局对象更改可能影响其他租户的可能性。专门为每个租户添加新对象可以最小化这种风险。
在理想情况下,我们会创建一个全局策略,然后为需要提升权限的单个命名空间创建更具体的对象。

图 12.1:多租户集群的理想策略设计
上图展示了我所说的“通用策略”的含义。大而虚线框且圆角的框表示全局限制性策略,最小化 Pod 的能力。然后,较小的虚线圆角框表示特定例外的特例。例如,ingress-nginx 命名空间将以限制性权限创建,并且会添加一个新的策略,专门作用于 ingress-nginx 命名空间,这将允许 Nginx 以 NET_BIND_SERVICES 权限运行。通过为集群范围内的限制性策略添加特定需求的例外,你可以减少新的命名空间如果没有添加新策略而暴露整个集群漏洞的可能性。系统设计为“闭环失败”。
上述场景并非 Gatekeeper 的工作方式。每个匹配的策略必须成功;无法有全局策略。为了有效管理多租户环境,我们需要:
-
为集群管理员可以拥有的系统级命名空间创建策略
-
为每个命名空间创建可以根据需要调整的策略
-
在创建 Pod 之前,确保命名空间已有策略

图 12.2:多租户集群的 Gatekeeper 策略设计
我在图 12.2中对这些目标进行了可视化。我们已经创建的策略需要调整为“系统级”策略。我们不再说它们需要全球应用然后再做例外,而是将其具体应用到我们的系统级命名空间。授予 NGINX 绑定端口 443 的策略是系统级策略的一部分,因为 ingress 是一种系统级能力。
对于单个租户,目标#2 要求每个租户都有自己的一组约束实现。这些单独的约束模板实现对象由围绕每个租户的圆角虚线框表示。看起来似乎有些重复,因为它确实如此。你可能会有非常重复的对象,为每个命名空间授予相同的能力。你可以采取多种策略来简化管理:
-
定义一个基础的限制性约束模板实现集,并将每个新命名空间添加到
match部分的namespaces列表中。这样可以减少杂乱,但也因为需要处理列表中的补丁而使自动化变得更加困难。由于无法像处理对象那样为单个属性添加任何元数据,跟踪也变得更加困难。 -
在命名空间创建时自动化约束模板实现的创建。这是我们在第十九章《平台配置》中采用的方法。在这一章中,我们将从自服务门户自动化命名空间的创建。工作流程将配置命名空间、RBAC 绑定、管道、密钥等。它还将提供所需的约束模板,以确保限制访问。
-
创建一个控制器,根据标签复制约束模板。这与 Fairwinds RBAC Manager(
github.com/FairwindsOps/rbac-manager)生成 RBAC 绑定的方式类似,使用自定义资源定义。我还没有看到直接用于 Gatekeeper 约束实现的工具,但相同的原则在这里也可以适用。
在管理这个自动化时,以上三种选择并不是互相排斥的。在 KubeCon EU 2021 上,我做了一场名为“我能做 RBAC,你也能!”的演讲(www.youtube.com/watch?v=k6J9_P-gnro),在演讲中我展示了如何将选项#2 和#3 结合起来,创建多个命名空间的“团队”,从而减少每个命名空间需要创建的 RBAC 绑定数量。
最后,我们需要确保每个非系统级命名空间都有创建约束实现。即使我们自动化命名空间的创建,我们也不希望出现没有节点安全约束的恶意命名空间。所有租户周围的大圆角虚线框就是这个表示。现在我们已经探索了为多租户集群构建节点安全策略的理论,接下来我们来构建我们的策略。
第一步是清除我们旧的策略:
$ kubectl delete -f chapter12/enforce_node_policies.yaml
$ kubectl delete -f chapter12/make_cluster_work_policies.yaml
$ kubectl delete -f chapter12/minimal_gatekeeper_constraints.yaml
这将使我们回到一个没有策略的状态。下一步是创建我们的系统级策略:
$ kubectl create -f chapter12/multi-tenant/yaml/minimal_gatekeeper_constraints.yaml
k8spspallowprivilegeescalationcontainer.constraints.gatekeeper.sh/system-privilege-escalation-deny-all created
k8spspcapabilities.constraints.gatekeeper.sh/system-capabilities-drop-all created
k8spspforbiddensysctls.constraints.gatekeeper.sh/system-psp-forbid-all-sysctls created
k8spsphostfilesystem.constraints.gatekeeper.sh/system-psp-deny-host-filesystem created
k8spsphostnamespace.constraints.gatekeeper.sh/system-psp-bloack-all-host-namespace created
k8spsphostnetworkingports.constraints.gatekeeper.sh/system-psp-deny-all-host-network-ports created
k8spspprivilegedcontainer.constraints.gatekeeper.sh/system-psp-deny-all-privileged-container created
k8spspprocmount.constraints.gatekeeper.sh/system-psp-proc-mount-default created
k8spspallowedusers.constraints.gatekeeper.sh/system-psp-pods-allowed-user-ranges created
k8spsphostfilesystem.constraints.gatekeeper.sh/psp-tigera-operator-allow-host-filesystem created
k8spspcapabilities.constraints.gatekeeper.sh/capabilities-ingress-nginx created
查看chapter12/multi-tenant/yaml/minimal_gatekeeper_constraints.yaml中的策略,你会看到我们不是在match部分排除命名空间,而是明确地列出了它们:
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sPSPAllowPrivilegeEscalationContainer
metadata:
name: system-privilege-escalation-deny-all
spec:
**match:**
kinds:
- apiGroups: [""]
kinds: ["Pod"]
**namespaces:**
**-****default**
**-****kube-node-lease**
**-****kube-public**
**-****kubernetes-dashboard**
**-****local-path-storage**
**-****tigera-operator**
**-****openunison**
**-****activedirectory**
在我们实施了系统约束后,接下来我们要确保在创建任何 Pod 之前,所有租户命名空间都已经设置了节点安全策略。目前没有现成的ConstraintTemplates来实现这一策略,因此我们需要自己构建一个。我们的ConstraintTemplate的 Rego 需要确保在某个命名空间中创建 Pod 之前,所有必需的ConstraintTemplate实现(换句话说,特权提升、能力等)至少有一个实例。Rego 的完整代码和测试用例位于chapter12/multi-tenant/opa。下面是一个代码片段:
# capabilities
violation[{"msg": msg, "details": {}}] {
checkForCapabilitiesPolicy
msg := "No applicable K8sPSPCapabilities for this namespace"
}
checkForCapabilitiesPolicy {
policies_for_namespace = [policy_for_namespace |
data.inventory.cluster["constraints.gatekeeper.sh/v1beta1"].K8sPSPCapabilities[j].spec.match.namespaces[_] == input.review.object.metadata.namespace ;
policy_for_namespace = data.inventory.cluster["constraints.gatekeeper.sh/v1beta1"].K8sPSPCapabilities[j] ]
count(policies_for_namespace) == 0
}
# sysctls
violation[{"msg": msg, "details": {}}] {
checkForSysCtlsPolicy
msg := "No applicable K8sPSPForbiddenSysctls for this namespace"
}
checkForSysCtlsPolicy {
policies_for_namespace = [policy_for_namespace |
data.inventory.cluster["constraints.gatekeeper.sh/v1beta1"].K8sPSPForbiddenSysctls[j].spec.match.namespaces[_] == input.review.object.metadata.namespace ;
policy_for_namespace = data.inventory.cluster["constraints.gatekeeper.sh/v1beta1"].K8sPSPForbiddenSysctls[j]
]
count(policies_for_namespace) == 0
}
首先需要注意的是,每个约束模板检查都在自己的规则中,并且有自己的违规项。将所有这些规则放入一个ConstraintTemplate中,意味着它们必须全部通过,才能使整个ConstraintTemplate通过。
接下来,让我们看看checkForCapabilitiesPolicy。这个规则会创建一个包含所有K8sPSPCapabilities的列表,该列表列出了我们 Pod 中 match.namespaces 属性中的命名空间。如果这个列表为空,规则将继续执行违规项,Pod 将无法创建。为了创建这个模板,我们首先需要将我们的约束模板同步到 Gatekeeper 中。然后,我们创建我们的约束模板和实现:
$ kubectl apply -f chapter12/multi-tenant/yaml/gatekeeper-config.yaml
$ kubectl create -f chapter12/multi-tenant/yaml/require-psp-for-namespace-constrainttemplate.yaml
$ kubectl create -f chapter12/multi-tenant/yaml/require-psp-for-namespace-constraint.yaml
在新策略生效后,让我们尝试创建一个命名空间并启动一个 Pod:
$ kubectl create ns check-new-pods
namespace/check-new-pods created
$ kubectl run echo-test -ti -n check-new-pods --image busybox --restart=Never --command -- echo "hello world"
Error from server ([k8srequirepspfornamespace] No applicable K8sPSPAllowPrivilegeEscalationContainer for this namespace
[k8srequirepspfornamespace] No applicable K8sPSPCapabilities for this namespace
[k8srequirepspfornamespace] No applicable K8sPSPForbiddenSysctls for this namespace
.
.
我们要求命名空间设置节点安全策略的要求阻止了 Pod 的创建!让我们通过应用来自chapter12/multi-tenant/yaml/check-new-pods-psp.yaml的限制性节点安全策略来解决这个问题:
$ kubectl create -f chapter12/multi-tenant/yaml/check-new-pods-psp.yaml
$ kubectl run echo-test -ti -n check-new-pods --image busybox --restart=Never --command -- echo "hello world"
hello world
现在,每当在我们的集群上创建一个新的命名空间时,必须先设置好节点安全策略,才能启动任何 Pod。
在本节中,我们探讨了使用 Gatekeeper 设计节点安全策略背后的理论,并将这一理论应用于单租户和多租户集群的实践中。我们还利用 Gatekeeper 内建的变异功能,为我们的securityContexts构建了合理的默认值。有了这些信息,您就可以开始使用 Gatekeeper 将节点安全策略部署到集群中了。
使用 Pod 安全标准来执行节点安全
Pod 安全标准是 Pod 安全策略(PSP)的“替代品”。我将“替代品”一词加上引号,因为 PSA 不是一个与 PSP 可比的功能替代品,但它与 Pod 安全标准指南中定义的新策略保持一致(kubernetes.io/docs/concepts/security/pod-security-standards/)。PSA 的基本原则是,既然命名空间是 Kubernetes 中的安全边界,那么应该在命名空间中决定 Pods 是否应该以特权或受限模式运行。
乍一看,这似乎很有道理。当我们谈论多租户和 RBAC 时,一切都是在命名空间级别定义的。PSP 的许多难点来自于如何授权策略,所以这解决了那个问题。
但问题在于,有些场景下你需要一个特权容器,但又不希望它是主要容器。例如,如果你需要一个卷在 init 容器中更改权限,但又希望你的主要容器受到限制,你就不能使用 PSA。
如果这些限制对你来说不是问题,那么 PSA 默认为开启,你只需要启用它。例如,要确保根容器不能在命名空间中运行:
$ kubectl create namespace nopriv
$ kubectl label namespace nopriv pod-security.kubernetes.io/enforce=restricted
$ kubectl run echo-test -ti -n nopriv --image busybox --restart=Never --command -- id
Error from server (Forbidden): pods "echo-test" is forbidden: violates PodSecurity "restricted:latest": allowPrivilegeEscalation != false (container "echo-test" must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container "echo-test" must set securityContext.capabilities.drop=["ALL"]), runAsNonRoot != true (pod or container "echo-test" must set securityContext.runAsNonRoot=true), seccompProfile (pod or container "echo-test" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")
由于我们没有设置任何限制容器以特权方式运行的措施,我们的小模块启动失败了。PSA 简单但有效。如果你不需要像 Gatekeeper 或 Kvyrno 这样的复杂入场控制器的灵活性,PSA 是一个很好的选择!
总结
在这一章中,我们首先探讨了保护节点的重要性,从安全角度分析了容器和虚拟机的区别,并了解了当节点没有保护时,攻击者是多么容易就能利用集群。我们还讨论了安全容器设计,使用 Gatekeeper 实现并调试节点安全策略,最后,使用新的 Pod Security Admission 功能来限制 pod 能力。
限制集群节点可以减少攻击者的攻击面。将策略封装起来,可以更容易地向开发人员解释如何设计容器,同时也能更容易地构建安全的解决方案。
到目前为止,我们的所有安全措施都是为了防止工作负载受到恶意攻击。那么,当这些措施失败时会发生什么呢?你如何知道你的小模块内部发生了什么?在下一章中,我们将找到答案!
问题
-
判断对错 – 容器是“轻量级虚拟机”吗?
-
正确
-
错误
-
-
容器可以访问主机资源吗?
-
不,它是隔离的。
-
如果标记为特权容器,则可以。
-
只有在策略明确授权的情况下。
-
有时是的。
-
-
攻击者如何通过容器访问集群?
-
容器应用中的漏洞可能导致远程代码执行,这可以被用来突破脆弱的容器,然后用来获取 kubelet 的凭证。
-
受损的凭证具有在一个命名空间中创建容器的能力,可以被用来创建一个容器,将节点的文件系统挂载以获取 kubelet 的凭证。
-
上述两者。
-
-
什么机制强制执行
ConstraintTemplates?-
一个在创建和更新时检查所有 pod 的入场控制器
-
PodSecurityPolicyAPI -
OPA
-
Gatekeeper
-
-
判断对错 – 容器应该一般以 root 用户运行吗?
-
正确
-
错误
-
答案
-
b – 错误;容器是进程。
-
b – 一个特权容器可以获得对主机资源的访问权限,如进程 ID、文件系统和网络。
-
c - 上述两者。
-
d - Gatekeeper
-
b - 错误
第十三章:KubeArmor 保护你的运行时
随着 Kubernetes 的普及,保护工作负载的强大安全措施的需求也在增加。我们学习了如何使用 RBAC 来保护集群安全,RBAC 允许我们控制用户对资源的访问权限。通过 RBAC,我们可以控制用户在集群上能执行什么操作,控制某人是否能够创建或删除 Pod、查看日志、查看 Secrets 等。我们还研究了使用 Gatekeeper 策略来保护集群,这可以通过拒绝创建违反安全策略的对象(例如尝试允许特权升级)来保护节点。
虽然这些措施大大增强了集群的安全性,但许多组织经常忽视某些操作。最重要的例子之一就是保护容器运行时。
Kubernetes 在审计或保护容器内执行的操作方面的能力有限。尽管 Kubernetes 可以处理某些安全要求,例如阻止容器内的特权提升尝试,但它并没有为操作员提供限制容器中执行的大部分操作的方式。它不能允许或拒绝用户在执行进入运行中的容器时能够执行的任何操作,比如查看文件、删除文件、添加文件等。更糟糕的是,大多数在容器内执行的操作不会被 Kubernetes API 服务器审计,这也是这些操作常常被忽视的原因。
在第八章,管理机密中,我们学习了如何使用 Vault 来存储和检索机密。许多人认为,如果使用像 Vault 这样的系统,他们就能确保机密数据不被任何人查看。的确,机密数据不会存储在基本的 K8s 秘密资源中,在那里,只要有适当的命名空间权限,任何人都可以查看并解码机密。然而,由于 Vault 机密将存储在你的 Pod 中作为环境变量或文件,无法阻止拥有 exec 权限的人查看容器的环境变量或存储 Vault 机密的文件。
我们还需要一种方式来阻止某些进程在容器中运行。你的组织可能有一个策略,即容器永远不应运行 SSH 守护进程。如果没有附加工具,你在运行中的容器中保护二进制文件到这种级别的选项非常有限。
当然,你可以在镜像创建时创建管道和安全检查,并拒绝不符合文档安全标准的镜像,但一旦镜像通过并部署了,如何阻止某人执行 exec 进入容器并添加像 SSH 守护进程这样的二进制文件,甚至更糟的恶意软件或加密矿工工具?
幸运的是,一家公司名为 AccuKnox 向 CNCF 捐赠了一个名为KubeArmor的项目,它为您提供了保护容器运行时的能力。KubeArmor 不仅限于运行时,它还具有一些与保护工作负载相关的其他有用功能,包括限制进程执行、文件访问等。
在本章中,我们将解释如何部署 KubeArmor,以及如何使用其众多功能来增强集群的安全性。以下是我们将在本章中介绍的几个主题:
-
什么是运行时安全?
-
介绍 KubeArmor
-
部署 KubeArmor
-
启用 KubeArmor 日志记录
-
KubeArmor 和 LSM 策略
-
创建 KubeArmorSecurityPolicy
-
使用 karmor 与 KubeArmor 互动
技术要求
本章具有以下技术要求:
-
一台运行 Docker 的 Ubuntu 22.04+ 服务器,内存至少为 8 GB
-
一个 KinD 集群,最好是一个新的集群,并且集成了 Vault
-
可以通过访问本书的 GitHub 仓库:
github.com/PacktPublishing/Kubernetes-An-Enterprise-Guide-Third-Edition来获取chapter13文件夹中的脚本。
什么是运行时安全?
运行时安全是系统、应用程序和数据在活跃执行期间最容易受到攻击时的关键安全部分,尤其是在它们运行在您的网络上时。运行时通常未被监控,有时缺乏任何形式的日志记录或审计,这带来了重大的安全挑战。当然,运行时安全不仅限于容器,它对于应用程序、容器、物理服务器、虚拟机等都是必要的。基础设施中的每个组件都需要持续监控所有潜在的安全风险,以便快速检测到潜在攻击者带来的威胁和漏洞。
面对日益复杂和动态的安全威胁,单纯依赖静态的安全措施已经不再足够。这就是运行时安全的作用所在,它提供动态的、实时的保护,正是在最关键的时候:在系统运行时。通过不断监控运行时环境,这些系统能够发现异常、可疑活动和未经授权的进程,并根据一组策略允许或阻止操作。
为了保护工作负载,您需要遵循一些关键实践,例如只允许授权的进程在容器中运行、实施措施防止并警告任何未经授权的资源访问,或检查网络流量以检测任何恶意活动。例如,您可以限制哪些进程可以访问容器中的文件或目录,拒绝任何非 MySQL 进程访问数据库文件。
运行时安全需要考虑多个方面:使用 KubeArmor 只是帮助保护您的工作负载和集群的工具之一。图 13.1展示了来自 CNCF 安全 V2 白皮书的运行时环境组件的图片。您可以在 CNCF 网站上找到完整的白皮书,网址是www.cncf.io/wp-content/uploads/2022/06/CNCF_cloud-native-security-whitepaper-May2022-v2.pdf。
我们在前几章中已经介绍了许多用于保护运行时的工具,包括网络策略、身份和访问管理、秘密管理以及使用Gatekeeper的策略安全。将这些选项与 KubeArmor 提供的附加安全性结合起来,您可以保护集群免受恶意活动的侵害。

图 13.1:CNCF 运行时安全景观
总的来说,KubeArmor 是一个运行时安全工具,为您的系统提供动态、实时的保护,防止日益扩展的威胁和漏洞。它的目的是保护您基础设施的安全性和稳定性,在面对无数网络安全威胁时维护您操作的完整性。
介绍 KubeArmor
在我们深入了解 KubeArmor 之前,我们需要定义一些基础概念,您需要了解这些内容。如果您是 Linux 新手,可能对这些概念不太熟悉,即使您是 Linux 老手,这些概念也可能对您来说是新的。
Linux 安全简介
在本章中,您将主要看到两个需要理解的术语,以了解 KubeArmor 如何保护集群。第一个术语是eBPF,它代表扩展贝尔克利数据包过滤器,第二个术语是LSM,代表Linux 安全模块。在图 13.2中,您可以看到一个 pod 的访问如何在到达主机内核之前经过 KubeArmor。这就是 KubeArmor 能够保护您运行时的方式:它位于 pod 运行时和内核之间,在请求执行之前采取行动。

图 13.2:KubeArmor 的高级设计
现在,我们需要从一个高层次的角度解释什么是 eBPF 和 LSM,以及它们如何帮助保护集群。
您是否曾想过 Linux 是如何处理进出系统的持续数据流的?它如何监控性能,以及如何保护自己免受安全风险?那么,eBPF 就发挥了作用:它处理所有这些任务,甚至更多!
想象 eBPF 像一个数字交通警察。您的计算机就像一个繁忙的交叉口,数据在不断流动。eBPF 充当交通指挥员,能够控制数据流,检查数据是否存在问题,并跟踪正在进行的活动。
eBPF 的一个优势是它使用“虚拟机”,而不是直接编辑内核来添加监控网络流量等功能。eBPF 主要使用以 C 语言的受限子集编写的程序,这些程序在内核内执行。虽然 C 语言是创建 eBPF 程序时最常用的语言,但你也可以使用其他语言来创建它们,包括:
-
Go
-
Lua
-
Python
-
Rust
使用除 C 语言以外的语言涉及到转译成 C 语言或添加所需的库来抽象 C 语言编程。选择语言的最终决定取决于你的使用案例、标准和专业技能。
总之,eBPF 提供了许多强大的功能,而无需直接修改内核。它具有高度的安全性和隔离性,通过使用虚拟机提供了一个安全边界,类似于运行完整操作系统的标准虚拟机。
我们提到的另一个术语是LSM,即Linux 安全模块。当前最常见的两个 LSM 是 SELinux,主要用于 Red Hat 系统,以及AppArmor,它被多个系统使用,包括 Ubuntu、SUSE 和 Debian。
与之前的 eBPF 部分类似,我们将提供一个关于 LSM 的高层概述,重点介绍 AppArmor,因为我们使用 Ubuntu 作为我们的服务器操作系统。
LSM 用于将内核与安全策略和模块连接起来,提供对 Linux 系统中强制访问控制(MACs)和额外安全策略的执行。它们提供了一个安全框架,通过内核中的钩子提供对外部模块的支持,使得外部模块能够拦截和保护系统调用、文件操作和其他各种内核活动。LSM 设计为非常灵活和可扩展,允许你选择和创建符合特定需求的模块,而不是供应商认为你应该实施的一套政策。
鉴于 eBPF 和 LSM 都提供安全功能,你可能会想知道它们是否有区别,或者它们是如何不同的。
尽管它们在高层次上看似相似,但它们有显著的不同。eBPF 利用内核嵌入的虚拟机进行执行,使得能够创建能够执行低级任务的程序,如数据包过滤、追踪和性能监控。eBPF 通常用于与网络相关的任务、性能优化或开发自定义内核级功能。
LSM 是由内核执行的组件,外部运行于内核本身之外。LSM 的核心目的是通过执行策略(包括 MACs)以及其他旨在保护系统资源的措施来增强系统安全性。这些模块能够通过限制对各种元素的访问(从文件和进程到网络流量的流动)来提高集群安全性。
如果你足够了解特定的 LSM(如 AppArmor),你可以在没有像 KubeArmor 这样的工具的情况下创建策略。试想一下,如果你使用多个 Linux 发行版,你需要了解每个发行版所兼容的 LSM。这使得创建策略变得具有挑战性,而这正是 AppArmorKubeArmor 可以提供帮助的地方。
KubeArmor 简化了创建 LSM 策略的任务,免去了你需要了解不同 LSM 之间语法的麻烦。当你使用 KubeArmor 创建策略时,它会自动在主机系统上生成相应的 LSM 策略。这保证了无论底层 LSM 是什么,你都可以创建一组统一的策略,确保跨多个 Linux 发行版和 LSM 提供一致的安全标准。
正如你所想象的,KubeArmor 同时使用 eBPF 和 LSM 来帮助你保护环境。现在我们已经了解了 eBPF 和 LSM 提供的功能,接下来我们可以介绍 KubeArmor。
欢迎使用 KubeArmor
保护任何环境都可能是一项艰巨的任务。保护集群时,这不是你可以事后简单处理的问题;它应该是最初设计和讨论的一部分。许多组织倾向于推迟环境的安全性问题,因为他们认为实施安全解决方案需要技能、精力和时间。然而,在集群投入生产之前建立安全基础是至关重要的。这对许多组织来说是一个挑战,这正是 KubeArmor 可以提供帮助的地方。
通过部署 KubeArmor,你可以提高容器化应用程序的安全性和合规性。KubeArmor 作为一个运行时安全解决方案,旨在通过强制执行安全协议并及时识别和允许或拒绝任何活动来保护容器化工作负载。
KubeArmor 的功能在不断发展:等你读完这本书时,KubeArmor 可能会增加我们在本章中没有涉及的额外功能。
那么,KubeArmor 提供了哪些增强我们安全性的功能呢?
容器安全
容器是现代应用程序的基石,使得容器安全成为首要目标。这并不是说我们认为非容器化应用程序不需要安全性:当然需要,但非容器化应用程序有很多操作系统和第三方供应商提供的安全选项。我们今天所知道的容器是相对较新的,许多工具集仍在追赶。
KubeArmor 通过实时监控容器行为来提供安全性,缓解诸如容器逃逸、二进制执行和权限升级等风险。
内联缓解与事后攻击缓解
市场上有许多非常擅长检测异常的产品,但它们没有在实际执行之前阻止或允许请求的能力。这是一个攻击后缓解过程,意味着该动作会被允许或拒绝,异常也会被记录下来。
这就像是有一扇没有锁的门,当有人进入建筑物时,您得到的只是来自监控摄像头的警报。由于门没有锁,这个人仍然可以进入建筑物。
许多仅检测事件的产品可以与其他系统集成,以防止动作的执行。例如,一个系统检测到可能有人将加密货币挖矿程序注入正在运行的容器中。该事件将被异常引擎检测到,基于该事件,您可以触发一个自定义编写的程序来创建一个网络策略,拒绝所有进出流量。这将阻止应用程序进行网络活动,停止 Pod 挖矿,并且我们没有销毁 Pod,只是停止了所有进出 Pod 的网络流量。

图 13.3:攻击后缓解
在图 13.3中,您可以看到攻击后缓解的流程。缓解的流程如下:
-
攻击后缓解通过对可疑活动采取行动,响应表示恶意意图的警报。
-
攻击者被允许执行二进制文件或其他操作。由于他们有访问权限,他们可能能够禁用安全控制、日志记录等,以避免被检测到。
-
假设该动作已经被检测到,我们将其发送到事件处理程序,后者可以根据事件执行相应的动作。然而,重要的是要指出,在恶意进程被执行时,敏感内容可能已经被删除、加密或传输。
-
根据事件,处理程序将执行一个动作,如删除 Pod,或者执行其他动作,如创建网络策略以阻止通信,而不删除 Pod。
KubeArmor 的一个关键区别是它不仅能检测运行时事件,还能对事件采取行动,根据各种参数来阻止或允许该事件。与攻击后缓解类似,您仍然会看到尝试的动作被记录下来,这些记录可能是记录恶意活动所需的证据。然而,不像之前那个没有锁的门的例子,这扇门将配有摄像头和锁。当有人尝试打开门时,摄像机会记录下这一尝试:但这一次,由于门被锁住,开门的动作将被拒绝。

图 13.4:内联缓解
在 图 13.4 中,你可以看到该过程是如何简化的:我们无需外部事件处理程序或任何自定义组件来处理事件。由于 KubeArmor 在实时内联处理事件,我们可以在攻击者执行任何恶意操作之前立即停止该行为,所有这些都在一个单一产品中完成。
如你所见,内联缓解是缓解运行时事件的更好方法。威胁在今天的环境中发展迅速,我们需要同样迅速地进行缓解。如果我们只是在事件发生后再进行反应,损害已经发生,你也只能在日志中看到有人做了恶意操作。
零日漏洞
零日漏洞需要时间来修复,不仅是厂商方面的问题,组织方面也一样。如果你能够在等待官方补丁的同时修复任何漏洞,你应该这么做:每一秒钟都至关重要。KubeArmor 会监控容器活动中的任何可疑行为,能够在没有事先了解具体漏洞或攻击模式的情况下停止这些活动。
CI/CD 管道集成
KubeArmor 可以轻松集成到持续集成和持续部署(CI/CD)管道中。将 KubeArmor 集成到你的管道中可以通过整个开发和部署生命周期自动化安全检查,从而提供一个安全可靠的镜像。
强大的审计和日志记录
日志记录非常重要,KubeArmor 包含容器活动的全面日志和审计跟踪。这些日志可用于报告合规性、提供故障排除帮助,并协助进行取证检查。
增强的容器可视化
对容器行为的可视化简化了识别和应对安全事件或异常的过程。KubeArmor 可以找到容器中正在运行的进程及其访问和连接的内容。
最小权限原则遵循
KubeArmor 基于最小权限原则,这是一个基本的安全原则。它确保容器仅拥有其指定功能所需的必要权限和访问级别,从而减少攻击面并限制因容器被攻破而可能带来的损害。
策略执行
策略是 KubeArmor 的核心。它们使管理员能够为容器创建详细的安全策略,精细调整每个不同、独特工作负载的要求。想要阻止任何容器执行 SSH 守护进程?只需使用 KubeArmor 创建一个简单的策略,任何容器都无法执行 SSH 守护进程。
保持合规性
为了帮助你保持符合 CIS、NIST-800-53 和 MITRE 等标准,KubeArmor 提供了一些策略,基于定义的最佳标准自动保护你的集群,开箱即用。
策略影响测试
在强制执行任何设置之前,可以先测试任何策略。这将帮助您创建一个不会因可能影响运行中的应用程序的设置而导致工作负载停机的策略。
多租户支持
企业通常会运行多租户集群。在多个团队或应用程序共享同一个 Kubernetes 集群的情况下,您需要为所有用户提供一个安全的环境,防止一个命名空间中的工作负载对另一个命名空间造成攻击或影响。KubeArmor 通过在容器级别实施独特的策略,为租户之间提供隔离和安全性。它是确保容器化应用程序安全的关键工具,能帮助满足合规要求,并防御各种安全威胁。
现在,让我们讨论一下如何在集群中部署 KubeArmor,以及如何使用它来保护我们的工作负载。
练习的集群要求
正如您在 第二章 中学到的,KinD 是一个在容器中运行组件的 Kubernetes 集群。这种嵌套意味着某些附加组件如 KubeArmor 需要额外的步骤才能正常工作。
本章建议使用新的集群。如果您已经有一个安装了 Vault 的旧集群,您应该删除该集群并从新集群开始。如果确实需要删除现有集群,您可以执行 kind delete cluster --name cluster01 来删除它,然后使用脚本部署一个包含 Vault 集成的新集群。
为了简化部署,我们在 chapter13/cluster 目录中包含了所有必需的脚本。要部署一个新集群,请在 cluster 目录中执行 create-cluster.sh。
我们还需要 Vault 来运行其中一个示例。如果您希望运行该示例,您需要将 Vault 添加到您的集群中。我们在 chapter13/vault 目录中提供了一个自动化的 Vault 部署脚本,名为 deploy-vault.sh。
一旦两个操作都执行完成,您将拥有一个全新集成了 Vault 的集群。Vault 完全部署需要一些时间,请等待所有 Pod 创建完成后再继续在集群中部署 KubeArmor。
部署 KubeArmor
在我们可以在 KinD 集群中使用 KubeArmor 之前,我们需要对 Calico 和 kubearmor-relay 部署进行补丁,以便它们能与 KinD 一起工作。AppArmor 需要对某些工作负载进行一些修改,才能在 KinD 集群中正确部署和运行。在标准集群中,这些补丁是不需要的:一旦部署完成,KubeArmor 就能像在标准 Kubernetes 集群中一样工作。
KubeArmor 可以通过一个名为 karmor 的单一二进制文件或通过 Helm charts 轻松部署。对于本书中的练习,我们将使用 karmor 工具来安装 KubeArmor。两种部署方法提供相同的保护和配置选项,一旦部署完成,您与 KubeArmor 的交互方式是相同的,无论采用哪种部署方式。
我们在chapter13文件夹中包含了一个名为kubearmor-patch.sh的脚本,它将下载 karmor,修补 Calico 和 kubearmor-relay 部署,并部署 KubeArmor。
KubeArmor 能够在大多数 Kubernetes 集群中顺利安装。由于我们使用的是基于 KinD 构建的集群,因此我们需要做一些调整,以确保 AppArmor 能够按预期工作。这些调整由脚本自动完成。大部分修复工作是为一些部署(如 Calico Typha 控制器)添加注解,以便它们进入未受限模式。我们将在本节中讨论这些修补部署以及未受限模式的功能。
脚本下载 karmor 并将其移动到主机的/usr/local/bin目录。这个工具将用于安装 KubeArmor 并在它部署到集群后与其交互。
由于 KubeArmor 依赖 LSM(Linux 安全模块),所有节点都需要安装 LSM,如 AppArmor,以使 KubeArmor 正常运行。在大多数 Ubuntu 部署中,AppArmor 已经部署,但由于我们的 Kubernetes 集群是容器化运行的,镜像中并未包含 AppArmor。为了解决这个问题,我们需要将 AppArmor 添加到我们的节点中:脚本通过在每个容器中执行docker exec来更新 apt 源、安装 AppArmor 并重启 containerd,完成这一过程。
脚本的下一步将使用一个未受限的AppArmor策略来修补calico-typha部署。以未受限模式运行策略意味着这些策略没有分配 AppArmor 配置文件,或者分配了一个不会施加任何重大限制的配置文件。这允许进程在标准 Linux 自由访问控制下运行,而不会受到 AppArmor 的附加限制。
正如我们之前提到的,标准的 Kubernetes 集群中不需要这些修补部署,但由于我们使用的是 KinD,我们需要修补calico-typha以便它能与在 KinD 集群中运行的 KubeArmor 正常配合使用。
在部署所有要求和变更后,脚本将继续使用 karmor install 安装 KubeArmor。这个过程需要几分钟来部署所有组件,并且在部署过程中,您将看到 karmor 正在执行的每一步操作:
namespace/kubearmor created
 Installed helm release : kubearmor-operator
 KubeArmorConfig created
 This may take a couple of minutes
 KubeArmor Snitch Deployed!
 KubeArmor Daemonset Deployed!
 Done Checking , ALL Services are running!
 Execution Time : 2m1.206181193s
 Verifying KubeArmor functionality (this may take upto a minute) —.
 Your Cluster is Armored Up!
您将看到安装程序创建了许多 Kubernetes 资源,包括 CRD、ServiceAccount、RBAC、服务和部署。所有资源创建完成后,它将通过告诉您“Your Cluster is Armored Up!”来验证部署是否成功。
成功部署后,您将在kubearmor命名空间中运行额外的 Pods,包括控制器、转发器以及每个节点一个kubearmor Pod:
kubearmor-controller-7cb5467b99-wmlz5 2/2 Running 0 6m
kubearmor-gvs5f 1/1 Running 0 6m6s
kubearmor-lpkj6 1/1 Running 0 6m6s
kubearmor-relay-5ccb6b6ffb-c4dlm 1/1 Running 0 6m6s
您也可以使用 Helm charts 部署 KubeArmor。如果您想了解更多关于如何使用 Helm 部署 KubeArmor 的内容,可以在 KubeArmor 的 Git 仓库中阅读github.com/kubearmor/KubeArmor/blob/main/getting-started/deployment_guide.md。
每个 Pod 都有一个特定的功能,具体说明如下:
-
kubearmor:一个 DaemonSet,在集群的每个节点上部署
kubearmorPod。它是一个非特权的 DaemonSet,具有能够监控 Pod、容器以及主机的能力。 -
kubearmor-relay:KubeArmor 的中继服务器收集 KubeArmor 在每个节点生成的所有消息、警报和系统日志,然后它允许其他日志系统通过中继服务器的服务轻松收集这些数据。中继服务器在确保 Kubernetes 环境中高效且集中化的安全监控与数据收集中发挥着关键作用,使得组织能够在其容器化基础设施中保持强大的安全态势。
-
kubearmor-controller:KubeArmor 策略管理的准入控制器,包括策略管理、分发、同步和日志记录。
本章我们选择了 karmor 二进制安装,因为它易于使用,成为快速部署 KubeArmor 的便捷选择。此外,接下来的练习中,我们也需要使用相同的 karmor 二进制文件。这个方法不仅简化了学习过程,还突出了 karmor 工具在管理 KubeArmor 部署和操作中的多功能性和实用性。
既然我们已经部署了 KubeArmor,接下来我们将在开始创建安全集群策略之前讨论如何配置日志记录。
启用 KubeArmor 日志记录
默认情况下,KubeArmor 不启用将事件或警报记录到 STDOUT。在本章的后续部分,我们将介绍如何在控制台中交互式地查看日志事件,这对于实时故障排除策略问题非常有用,但它并不是一种高效的历史事件日志记录方式。
大多数为 Kubernetes 设计的日志解决方案会从 STDOUT 和 STDERROR 中获取日志事件。通过启用 KubeArmor 的日志选项,你将能够在标准日志解决方案中拥有事件历史记录。使用这些事件,你可以创建警报并在进行安全审计时生成变更和事件的历史记录。
KubeArmor 提供三种可以记录的事件:
-
警报:当策略被违反时,将记录一条事件,包含的内容有操作、策略名称、Pod 名称、命名空间等信息。
-
日志:当 Pod 执行系统调用、文件访问、进程创建、网络套接字事件等时,会生成一条日志事件。
-
消息:由 KubeArmor 守护进程生成的日志条目。
启用日志记录的过程在 KubeArmor 的不同部署中有所不同。我们使用了 karmor 可执行文件进行部署,因此我们需要编辑部署,添加两个环境变量:一个用于标准日志记录,ENABLE_STDOUT_LOGS,另一个用于警报,ENABLE_STDOUT_ALERTS。这两个变量都需要设置为 true 才能启用。为了启用日志记录,我们需要编辑或修补 relay server 的部署。这已通过我们包含的部署 KubeArmor 的脚本完成。该脚本将使用标准的 YAML 文件来修补部署。下面显示了修补文件:
spec:
template:
spec:
containers:
- name: kubearmor-relay-server
env:
- name: ENABLE_STDOUT_LOGS
value: "true"
- name: ENABLE_STDOUT_ALERTS
value: "true"
接下来,使用补丁文件,script 执行 kubectl patch 命令:
kubectl patch deploy kubearmor-relay -n kubearmor --patch-file patch-relay.yaml
一旦修补完毕,所有启用的日志将显示在 relay-server pod 的日志中。下面显示了一个事件的示例:
{"Timestamp":1701200947,"UpdatedTime":"2023-11-28T19:49:07.625696Z","ClusterName":"default","HostName":"cluster01-worker","NamespaceName":"my-ext-secret","Owner":{"Ref":"Pod","Name":"nginx-secrets","Namespace":"my-ext-secret"},"PodName":"nginx-secrets","Labels":"app=nginx-web","ContainerID":"88f324db1f6ffa01f42b2811288b6f8b0e66001f41c5101ce578f69c bd932e5e","ContainerName":"nginx-web","ContainerImage":"docker.io/library/nginx :latest@sha256:10d1f5b58f74683ad34eb29287e07dab1e90f10af243f151bb50aa5 dbb4d62ee","HostPPID":1441261,"HostPID":1441503,"PPID":43,"PID":50,"ProcessName":"/usr/bin/cat","PolicyName":"nginx-secret","Severity":"1","Type":"MatchedPolicy","Source":"/usr/bin/cat /etc/secrets/","Operation":"File","Resource":"/etc/secrets/","Data":"syscall=SYS_OPENAT fd=-100 flags=O_RDONLY","Enforcer":"AppArmor","Action":"Block","Result":"Permission denied"}
从示例日志条目中,你可以看到事件的信息包含了你需要了解的所有活动内容。它包括对活动的回顾,包括:
-
源命名空间
-
Kubernetes 主机
-
Pod 名称
-
进程名称
-
被违反的策略名称
-
操作
-
被操作的资源
-
操作的结果,是允许还是拒绝
单独来看,这可能没有包含你需要了解的完整活动信息。比如,它没有包括初始活动的用户。像 Kubernetes 中的许多事件一样,你需要关联来自多个日志文件的事件,以创建活动的完整过程。在此示例中,你需要将审核初始 kubectl exec 命令的事件与 KubeArmor 记录的运行时违反事件的 Pod 和时间进行关联。
此时,我们已配置 KubeArmor,并且可以开始创建和测试策略。
KubeArmor 和 LSM 策略
正如我们所提到的,KubeArmor 是一个帮助你为 Linux LSM 创建策略的工具。由于它创建标准的 LSM,任何你创建并部署的策略都会存储在节点上,存储位置与操作系统存储 LSM 策略的地方一致。由于我们使用的是 KinD,节点运行的是 Ubuntu 操作系统,而 Ubuntu 使用 AppArmor 作为 LSM。AppArmor 策略存储在主机的 /etc/apparmor.d 目录下。
以下输出显示了一个节点的示例目录,该节点已创建了一些 KubeArmor 策略:
kubearmor-local-path-storage-local-path-provisioner-local-path-provisioner
kubearmor-my-ext-secret-nginx-secrets-nginx-web
kubearmor-calico-apiserver-calico-apiserver-calico-apiserver kubearmor-tigera-operator-tigera-operator-tigera-operator
kubearmor-calico-system-calico-kube-controllers-calico-kube-controllers kubearmor-vault-vault-agent-injector-sidecar-injector
kubearmor-calico-system-calico-node-calico-node kubearmor-vault-vault-vault
kubearmor-calico-system-csi-node-driver-calico-csi
kubearmor-calico-system-csi-node-driver-csi-node-driver-registrar lsb_release
kubearmor-cert-manager-cert-manager-cainjector-cert-manager-cainjector nvidia_modprobe
kubearmor-cert-manager-cert-manager-cert-manager-controller tunables
如果你查看过任何策略,你会看到一个标准的 AppArmor 格式的策略。我们不会详细介绍如何创建 AppArmor 策略,但下面的输出展示了 KubeArmor 创建的一个策略示例:
## == Managed by KubeArmor == ##
#include <tunables/global>profile kubearmor-vault-vault-vault flags=(attach_disconnected,mediate_deleted) {
## == PRE START == ##
#include <abstractions/base>
umount,
file,
network,
capability,
## == PRE END == ##
## == POLICY START == ##
## == POLICY END == ##
## == POST START == ##
/lib/x86_64-linux-gnu/{*,**} rm,
deny @{PROC}/{*,**^[0-9*],sys/kernel/shm*} wkx,
deny @{PROC}/sysrq-trigger rwklx,
deny @{PROC}/mem rwklx,
deny @{PROC}/kmem rwklx,
deny @{PROC}/kcore rwklx,
deny mount,
deny /sys/[^f]*/** wklx,
deny /sys/f[^s]*/** wklx,
deny /sys/fs/[^c]*/** wklx,
deny /sys/fs/c[^g]*/** wklx,
deny /sys/fs/cg[^r]*/** wklx,
deny /sys/firmware/efi/efivars/** rwklx,
deny /sys/kernel/security/** rwklx,
## == POST END == ##
}
你的节点可能有些策略并非通过 KubeArmor 创建。为了知道哪些策略是由 KubeArmor 创建和管理的,哪些不是,你需要查看策略的第一行。如果策略是由 KubeArmor 创建的,它将以 ## == Managed by KubeArmor == ## 开头,而没有以此行开头的策略不是由 KubeArmor 创建的。
现在让我们进入下一部分,创建 KubeArmorSecurityPolicy。
创建一个 KubeArmorSecurityPolicy
现在是时候创建一些策略了!当 KubeArmor 部署时,它会创建三个自定义资源定义,其中之一是 kubearmorpolicies.security.kubearmor.com,用于创建新的策略资源。
让我们直接进入一个示例策略。您无需将其部署到集群中;它仅用于展示一个示例策略。
如果我们想要阻止任何尝试在 demo 命名空间的容器中的 /bin 目录下创建文件的操作,下面是该策略的格式:
apiVersion: security.kubearmor.com/v1
kind: KubeArmorPolicy
metadata:
name: block-write-bin
namespace: demo
spec:
action: Block
file:
matchDirectories:
- dir: /bin/
readOnly: true
recursive: true
message: Alert! An attempt to write to the /bin directory denied.
分析这个策略,我们可以看到它使用了 security.kubearmor.com/v1 API,并且是一个 KubeArmorPolicy 类型。元数据部分包含常见选项,将对象命名为 block-write-bin,并位于 demo 命名空间中。
spec 部分是我们实际开始创建新策略的地方。您可以使用许多选项来创建策略。有关所有选项的详细信息,您可以访问 KubeArmor 网站:docs.kubearmor.io/kubearmor/documentation/security_policy_specification。
spec 部分允许您定义策略所执行的操作。选项包括 Block、Allow 和 Audit,每个选项的描述见下表。
| 可用操作 | 描述 |
|---|---|
Block |
告诉 KubeArmor 阻止策略中包含的操作(如果未提供操作,默认为此) |
Allow |
告诉 KubeArmor 允许策略中包含的操作 |
Audit |
告诉 KubeArmor 只审计策略的操作。策略中的操作将被允许,但在我们的示例中,当有人在 /bin 目录下创建文件时,我们会收到一个已记录的事件。这对于测试策略如何影响集群中的工作负载非常有用。 |
表格 13.1:可用的策略操作
KubeArmor 的操作原则是实施最小权限访问。当您在策略中指定允许操作时,它会生成一个允许列表,只允许访问策略中指定的对象。例如,如果您为名为 demo/allowed-file 的文件建立一个 allow 策略,容器内的任何进程都将获得访问该文件的权限。容器内访问的所有其他文件都会触发审计事件,因为它们不属于允许列表。
您可能会对示例中的情况提出疑问:如果您设置了一个允许策略,而某人尝试读取另一个文件,它不会拒绝请求,而是会记录访问日志以供审计。允许策略中的默认安全态度与它如何管理未列在允许条目中的访问尝试有关。默认情况下,KubeArmor 的安全态度设置为审计模式。
需要特别注意的是,当你建立一个允许策略时,任何通常会被拒绝的访问请求将不会被拒绝;相反,它们只会触发审计警报。因此,如果你配置了一个允许规则来限制对特定文件的访问,所有其他文件仍然可以访问。
默认的姿态行为可以在全局级别或每个命名空间级别进行更改。要使全局默认姿态为阻止(block),而非审计(audit),你需要编辑 KubeArmor 配置,该配置存储在 kubearmor 命名空间中的名为 kubearmor-config 的 ConfigMap 中。在该配置中,你可以为每个选项、文件、网络和能力设置默认的安全姿态:
defaultFilePosture: block #Can be block or audit
defaultNetworkPosture: block #Can be block or audit
defaultCapabilitiesPosture: block #Can be block or audit
根据你的集群配置和集群的逻辑设计,你可能希望在特定命名空间中更改默认姿态。要在命名空间上设置策略,你需要添加一个注释 kubearmor-file-posture=<value>。如果我们想为现有的 demo 命名空间添加策略,我们只需要运行 kubectl annotate,如下所示:
kubectl annotate ns default kubearmor-file-posture=block --overwrite
如果你是通过清单创建新的命名空间,只需在应用文件创建命名空间之前,将注释添加到清单中即可。
在定义完策略行为后,我们需要添加希望阻止、允许或审计的对象。
我们可以为四个对象创建策略。它们是:
-
进程
-
文件
-
网络
-
能力
KubeArmor 网站提供了有关策略和选项的文档,位于
docs.kubearmor.io/kubearmor/documentation/security_policy_specification。
在我们的示例中,我们的目标是阻止对 /bin 目录的任何写入访问。为此,我们将使用 file 对象。在声明对象后,你需要指定一个 match 条件,该条件会触发策略行为。在此实例中,我们已将 matchDirectories 行为专门配置为 /bin 目录,这表示只有当操作发生在该目录下时,KubeArmor 才会评估该策略。
接下来,还有可选的 readOnly 和 recursive 设置。在我们的场景中,我们已启用这两个选项。当 readOnly 设置为 true 时,它允许读取位于 /bin 下的任何文件,但其他任何操作都会被拒绝。启用 recursive 选项会指示 KubeArmor 评估 /bin 目录及其所有子目录。
最后,你可以定义消息选项,这将在策略触发时在 KubeArmor 日志中添加自定义消息。在我们的示例中,我们添加了消息选项,当尝试进行除读取 /bin 目录下的文件外的任何操作时,会显示“警告!尝试写入 /bin 目录被拒绝。”的消息。
你可能会对允许操作感到好奇,我们之前说它会创建一个允许列表,只允许访问政策中的对象,拒绝访问容器中的其他文件。单个文件的示例在实际应用中并不是一个很好的例子,但它确实解释了你授予了什么访问权限,以及哪些权限被允许政策拒绝。当允许政策正确使用时,它会紧密地锁定容器。而如果使用不当,你的应用可能会崩溃,因为被拒绝访问不在允许列表中的文件。你可以想象,为一个应用创建一个允许列表可能需要大量对象,而很多对象你可能很难自己找到。
让我们用一个实际的示例政策来结束这一节。
Foowidgets 希望保护他们的机密信息。他们已经制定了一项政策,要求所有机密信息必须存储在外部机密管理器中,如 Vault。正如我们在机密章节中讨论的那样,你可以从 Vault 中读取机密信息,而不需要在命名空间中存储 base64 编码的机密。很多人认为这可以保护机密信息,但他们忽略了一个问题,那就是任何人都可以进入容器并读取大多数文件,包括存储机密信息的文件。
我们如何增强机密信息的安全性,即使使用像 Vault 这样的外部机密管理器?答案就是 KubeArmor!
我们可以通过制定一项政策来解决 Foowidgets 的需求,允许只有必要的运行进程访问包含机密信息的文件,而其他进程则被拒绝访问。
在 chapter13/nginx-secrets 目录中,有一个名为 create-nginx-vault.sh 的脚本,它将创建一个 NGNIX web 服务器,当你打开网页路径 /secrets/myenv 时,会显示一个机密文件及其内容。页面上显示的机密信息是从 Vault 拉取的,并通过 /etc/secrets/myenv 这个卷挂载到 pod 中。
当你执行脚本时,最后一行会显示 nip.io 的 URL 用于 web 服务器。你可以在任意浏览器中打开该 URL,或者使用 curl 请求 http://secret.<nip.io>/secrets/myenv 来验证机密信息是否出现在输出中。你应该会看到类似于以下的输出:

图 13.5:NGINX 显示机密文件内容
为了验证容器是否按预期工作,我们可以使用 exec 进入容器并尝试使用 cat 阅读机密文件:
kubectl exec -it nginx-secrets -n my-ext-secret -- bash
机密信息已在 /etc/secrets 目录下的 myenv 文件中挂载到 pod 中:
cat /etc/secrets/myenv
这将输出文件的内容:
some-password: mysupersecretp@ssw0rd
等等!我以为使用外部机密管理器就能确保 Kubernetes 的机密信息安全。尽管它可能不会将数据存储在命名空间中容易发现的 Secret 中,但拥有容器执行访问权限的人仍然可以获取机密信息。
这个问题是像 Vault 这样的系统的一个缺点;仅仅使用 Vault 并不一定能确保机密信息的安全。
为了通过实际场景说明这一点,我们来考虑一下我们公司 Foowidgets 的需求。他们希望将秘密访问限制为仅限那些需要访问秘密的进程。这可以通过创建一个新的 KubeArmor 策略来实现,只允许应用程序访问包含秘密的文件。在我们的示例容器中,我们打算授予 NGINX 进程读取秘密文件的权限,同时阻止其他进程访问该文件。
为了实现这一目标,我们创建了一个名为nginx-secrets-block.yaml的示例策略文件。该文件将部署到my-ext-secret命名空间:
apiVersion: security.kubearmor.com/v1
kind: KubeArmorPolicy
metadata:
name: nginx-secret
namespace: my-ext-secret
spec:
selector:
matchLabels:
app: nginx-web
file:
matchDirectories:
- dir: /
recursive: true
- dir: /etc/nginx/
recursive: true
fromSource:
- path: /usr/sbin/nginx
- dir: /etc/secrets/
recursive: true
fromSource:
- path: /usr/sbin/nginx
- dir: /etc/nginx/
recursive: true
action: Block
- dir: /etc/secrets/
recursive: true
action: Block
process:
matchPaths:
- path: /usr/sbin/nginx
action:
Allow
为了展示策略的实际效果,我们在chapter13/nginx-secrets目录中包含了一个名为redeploy-nginx-vault.sh的脚本,该脚本将删除先前的 NGINX 部署,然后创建一个带有 KubeArmor 策略的新部署,以保护 NGINX 使用的 Vault 秘密。
执行该脚本并等待新的部署和策略创建完成。我们需要确认最终结果符合我们对新策略的预期。为了验证策略,我们将通过执行kubectl exec -it nginx-secrets -n my-ext-secret -- bash进入 pod,尝试访问秘密。
进入 pod 后,我们可以尝试使用cat查看秘密:
cat /etc/secrets/myenv
您会注意到,访问该文件已经被阻止。KubeArmor 会拦截请求,并根据策略拒绝访问/etc/secrets/myenv文件:
root@nginx-secrets:/etc/secrets# cat /etc/secrets/myenv
cat: /etc/secrets/myenv: Permission denied
请注意,尽管您在容器内拥有 root 权限,但无法访问/etc/secrets目录中的myenv文件。该策略阻止了任何未明确允许的对该目录或文件的访问。
到目前为止,一切似乎都进展顺利。然而,我们现在必须验证网站,确保秘密信息仍然存在。如果网站显示与实施我们策略之前相同的内容,说明 NGINX 二进制文件允许读取该秘密。为了验证这一点,导航到您之前用来测试该网站的相同 URL,无论是通过浏览器还是使用curl命令。如果您仍然保持同一个浏览器窗口打开,只需刷新它即可。
下面的截图验证了网站正常运行,并继续显示/etc/secrets目录下myenv文件中存储的值。这确认了 NGINX 二进制文件具有访问秘密文件所需的权限:

图 13.6:NGINX 仍然可以读取秘密
KubeArmor 简化了开发人员和运维人员创建 LSM 策略的过程。其潜在应用几乎是无限的,使您能够增强工作负载的安全性,精确到单个文件或进程的粒度。现在我们已经介绍了策略创建的过程,接下来让我们深入探讨您将用来与 KubeArmor 交互的主要工具。
使用 karmor 与 KubeArmor 交互
我们使用 karmor 实用程序安装了 KubeArmor。除了在集群中安装和卸载 KubeArmor 外,它还用于执行许多其他操作。下表是您应该了解的主要选项概述。每个选项将在其各自的章节中详细解释。
| 选项 | 描述 |
|---|---|
安装 |
在集群中安装 KubeArmorKubeArmor |
日志 |
提供交互查看日志的方法,或将日志发送到文件 |
探针 |
列出集群支持的功能特性 |
配置文件 |
运行一个交互式实用程序,显示 KubeArmor 观察到的进程、文件、网络和系统调用 |
推荐 |
创建一个包含推荐策略的目录,可以在集群中部署。这将下载额外的容器来创建推荐内容。根据正在运行的容器数量和大小,可能需要一些时间。 |
自更新 |
更新 karmor 命令行工具 |
摘要 |
显示来自发现引擎的观察结果 |
系统转储 |
用于收集系统转储以帮助故障排除 |
卸载 |
从集群中卸载 KubeArmor |
版本 |
显示 karmor 二进制文件的版本 KubeArmor |
Vm |
用于对运行 Kubevirt 和 kvmservices 的虚拟机执行命令 |
表 13.2:karmor 命令选项
这个列表可能会让 KubeArmor 看起来选择不多,但大多数选项非常强大,并且在较大的集群中运行某些选项可能需要时间。在接下来的章节中,我们将解释 karmor 的选项及其提供的安全功能。
karmor 安装和卸载
如您所料,karmor install 命令将使用当前的 kubeconfig 文件将 KubeArmor 部署到集群中,而 karmor uninstall 命令将从集群中移除 KubeArmor。
需要注意的是,默认情况下 karmor uninstall 会从集群中移除 KubeArmor,但会使在主机上创建的所有 LSM 策略处于非活动状态。要完全从集群中移除 KubeArmor,包括所有创建的策略,您需要在 uninstall 命令中加入 --force 标志。
karmor 探针
探针选项将列出当前集群中的 KubeArmor 功能。
当您检查支持的探针时,karmor 将输出信息,包括每个节点及其活动的 LSM 和每个命名空间和 Pod 的默认姿态。
karmor 配置文件
KubeArmor 的配置文件为您提供了一个交互式控制台,用于查看正在使用的进程、文件、网络连接和系统调用。下面的屏幕显示了从选择了 文件 选项的探针中的简略输出。

图 13.7:karmor 配置文件输出
默认情况下,探针将输出所有命名空间的信息。如果您有具有大量命名空间和 Pod 的集群,可以将输出限制为单个命名空间或特定的 Pod。
要将输出限制为单个命名空间,可以添加选项 -n 或 --namespace <namespace to prove>,如果只想限制输出到某个 pod,可以使用 -p 或 --pod <pod name to probe>。
如果你想看到实际效果,假设你想监视一个名为 demo 的新命名空间的活动。你可以执行 recommend 命令并添加 -n demo。
在主机上,执行下面显示的探针命令:
karmor profile -n demo
你可能会在某些标签页下看不到任何活动。profile 命令需要有活动才能显示,如果没有任何被监视事件的活动,它们将不会显示任何数据。在 KubeArmor 看到活动并创建新条目之前,你会看到一个空白列表,如 图 13.8 所示。

图 13.8:KubeArmor 的 profile 控制台
打开另一个连接到主机,以便创建一个新的 NGINX 部署。在 chapter13/nginx 目录下有一个名为 ngnix-ingress.sh 的脚本,它将创建一个名为 demo 的命名空间,并部署一个 NGINX 和一个 ingress 规则。执行脚本后,最后它将显示你需要使用的 ingress URL。
现在我们已经创建了一个部署,你的另一个终端应该会在 Process 标签中显示活动,如 图 13.9 所示。

图 13.9:KubeArmor 的 profile 控制台
随着 NGINX pod 启动并且进程开始运行,这将会在 Process 标签中填充事件。在你的另一个窗口中,你将实时看到 profile 更新,并显示在 demo 命名空间中启动的进程。
KubeArmor 的探针是一个强大的工具,它提供了通常很难收集的信息。
karmor recommend
recommend 命令用于根据已建立的行业合规标准和攻击框架(如 CIS、MITRE、NIST、STIG 等)提供安全策略推荐。所有在 recommend 命令中指定的工作负载将会针对 KubeArmor 包含的任何策略模板进行测试。

图 13.10:KubeArmor recommend 策略
由于每个 pod 和容器都会被评估,你可以选择通过命名空间、容器镜像或 pod 来过滤执行,不仅仅是针对集群。以下是运行 karmor recommend 命令的示例输出,如 图 13.11 所示:

图 13.11:karmor recommend 输出
从输出中,你可以看到 karmor 将为每个容器拉取镜像,以便测试策略。所有由 karmor 创建的策略默认保存在当前工作目录下的一个目录中。你可以通过向 recommend 命令添加 -o 或 --output 选项来更改策略的创建位置。由于推荐内容按每个操作分解,你可能会生成大量的文件。举个例子,我们对 KinD 集群的 kube-system 命名空间运行 recommend 命令,它生成了如下所示的目录结构:
./out
├── kube-system-coredns
├── kube-system-kubearmor-controller
└── kube-system-kubearmor-relay
除了每个部署的目录,你还会看到一个report.txt文件,里面包含了各种标准下所有推荐的策略,包括 NIST、MITRE、PCI-DSS、CIS 等。我们将在后面的章节讨论报告及其选项。目前,我们希望关注创建的策略。
让我们更仔细地看一下列表中的第一个目录,它包含了kube-system命名空间中core-dns部署的策略。正如你从输出中看到的,recommend命令创建了 16 个策略:
kube-system-coredns/
├── registry-k8s-io-coredns-coredns-v1-10-1-cronjob-cfg.yaml
├── registry-k8s-io-coredns-coredns-v1-10-1-file-integrity-monitoring.yaml
├── registry-k8s-io-coredns-coredns-v1-10-1-impair-defense.yaml
├── registry-k8s-io-coredns-coredns-v1-10-1-k8s-client-tool-exec.yaml
├── registry-k8s-io-coredns-coredns-v1-10-1-maint-tools-access.yaml
├── registry-k8s-io-coredns-coredns-v1-10-1-network-service-scanning.yaml
├── registry-k8s-io-coredns-coredns-v1-10-1-pkg-mngr-exec.yaml
├── registry-k8s-io-coredns-coredns-v1-10-1-remote-file-copy.yaml
├── registry-k8s-io-coredns-coredns-v1-10-1-remote-services.yaml
├── registry-k8s-io-coredns-coredns-v1-10-1-shell-history-mod.yaml
├── registry-k8s-io-coredns-coredns-v1-10-1-system-owner-discovery.yaml
├── registry-k8s-io-coredns-coredns-v1-10-1-trusted-cert-mod.yaml
├── registry-k8s-io-coredns-coredns-v1-10-1-write-etc-dir.yaml
├── registry-k8s-io-coredns-coredns-v1-10-1-write-in-shm-dir.yaml
├── registry-k8s-io-coredns-coredns-v1-10-1-write-under-bin-dir.yaml
└── registry-k8s-io-coredns-coredns-v1-10-1-write-under-dev-dir.yaml
如果你查看文件名,你可以判断每个策略使用的是什么类型的操作和过程。例如,让我们看一下registry-k8s-io-coredns-coredns-v1-10-1-write-etc-dir.yaml 策略。从文件名中,我们可以看出这个策略是为了在/etc目录中添加写操作而创建的。深入查看文件,我们会看到这个策略包含了对/etc目录的阻止操作,并且它还将/etc锁定为只读状态,适用于任何与标签k8s-app=kube-dns匹配的内容。
apiVersion: security.kubearmor.com/v1
kind: KubeArmorPolicy
metadata:
name: coredns-registry-k8s-io-coredns-coredns-v1-10-1-write-etc-dir
namespace: kube-system
spec:
action: Block
file:
matchDirectories:
- dir: /etc/
readOnly: true
recursive: true
message: Alert! File creation under /etc/ directory detected.
selector:
matchLabels:
k8s-app: kube-dns
severity: 5
tags:
- NIST_800-53_SI-7
- NIST
- NIST_800-53_SI-4
- NIST_800-53
- MITRE_T1562.001_disable_or_modify_tools
- MITRE_T1036.005_match_legitimate_name_or_location
- MITRE_TA0003_persistence
- MITRE
- MITRE_T1036_masquerading
- MITRE_TA0005_defense_evasion
在这个策略中有几个我们之前没有讨论过的字段,分别是严重性和标签。与其他可能将严重性添加到触发事件的工具不同,KubeArmor 允许你为策略设置自己的严重性。当你创建策略时,可以为其分配一个从 1 到 10 的严重性评分,允许你根据组织需求创建自己的评分标准。
标签部分是由推荐命令生成的。默认情况下,当你运行推荐时,它会根据所有包含的加固策略(包括 MITRE TTP、STIG、NIST 和 CIS)对所有对象进行测试。创建的策略是基于在推荐收集过程中提供的标准。如果你没有指定任何要检查的策略,Karmor 将为所有标准创建策略,包括你可能需要或不需要的策略。
根据你的组织和安全要求,你可以将加固策略限制为仅包含你希望包括的策略。这可以通过在推荐命令中添加-t或--tag标志来完成,后面跟上标准或标准集。例如,如果我们希望对kube-system命名空间执行推荐,并且只包括 CIS 和 PCI-DSS 标准,我们可以执行:
karmor recommend -n kube-system -t PCI-DSS,CIS
像所有其他推荐命令一样,这个命令会在工作目录中创建一个out目录,里面包含策略和一个report.txt文件。如果你查看报告,你会看到针对每个 Pod 在 PCI-DSS 和 CIS 标准下的推荐操作列表。下面的图是我们对kube-system运行推荐命令时生成的report.txt的简化示例。

图 13.12:NIST 和 CIS 的推荐示例
由于我们在命令中添加了标签,karmor 只会创建满足 NIST 和 CIS 标准所需的策略。这将比没有标签的运行生成更少的策略,因为它将仅基于指定的标签生成策略,而不是所有标准(如果您未提供标签)。
我们将讨论的最后一个示例是使用推荐命令为在集群中未运行的镜像创建策略和报告。到目前为止,我们已对集群中的对象运行了推荐命令,但 KubeArmor 提供了基于任何容器镜像创建策略的功能。要对一个镜像运行推荐命令,您需要在命令中添加 -i 或 --image 参数。例如,我们想对 bitnami/nginx 镜像运行 karmor:
karmor recommend -i bitnami/nginx
这将拉取镜像并根据所有已包含的 KubeArmor 策略进行运行。策略将与之前的示例一样,创建在 out 目录中:
out
└── bitnami-nginx-latest
Notice that the directory name does not contain a namespace; it only has the image name and tag that we tested against, bitnami/nginx:latest. This example run created a number of policies since we ran it against all included policies:
├── access-ctrl-permission-mod.yaml
├── cis-commandline-warning-banner.yaml
├── cronjob-cfg.yaml
├── file-integrity-monitoring.yaml
├── file-system-mounts.yaml
├── impair-defense.yaml
├── k8s-client-tool-exec.yaml
├── maint-tools-access.yaml
├── network-service-scanning.yaml
├── pkg-mngr-exec.yaml
├── remote-file-copy.yaml
├── remote-services.yaml
├── shell-history-mod.yaml
├── system-network-env-mod.yaml
├── system-owner-discovery.yaml
├── trusted-cert-mod.yaml
├── write-etc-dir.yaml
├── write-in-shm-dir.yaml
├── write-under-bin-dir.yaml
└── write-under-dev-dir.yaml
如果我们运行相同的测试并仅包括 CIS 策略的标签,我们将生成更少的策略:
├── access-ctrl-permission-mod.yaml
├── cis-commandline-warning-banner.yaml
├── cronjob-cfg.yaml
├── file-system-mounts.yaml
└── system-network-env-mod.yaml
如示范所示,recommend 命令使您能够根据组织、政府法规或其他相关标准提高工作负载的安全性。
karmor logs
日志选项提供了 KubeArmor 活动的实时日志,这对于希望查看事件而不看到成百上千其他已记录活动的用户非常有用。当您执行 karmor log 时,日志记录器将启动并监视 KubeArmor 活动。由于这是实时日志,它将在您的 shell 中以交互方式运行,等待活动发生:
local port to be used for port forwarding kubearmor-relay-7676f9684f-652ll: 32879
Created a gRPC client (localhost:32879)
Checked the liveness of the gRPC server
Started to watch alerts
当 KubeArmor 观察到事件时,它们将显示在输出中。例如,我们创建了一个策略,阻止在演示命名空间中所有容器对 /bin 目录的任何写入尝试。我们进入容器并尝试在该目录下创建一个名为 test 的文件。如下面的输出所示,尝试被拒绝:
I have no name!@nginx-web-57794669f5-gd4r4:/app$ cd /bin
I have no name!@nginx-web-57794669f5-gd4r4:/bin$ touch test
touch: cannot touch 'test': Permission denied
由于这是一个 KubeArmor 已经有策略的操作,它也会在运行 karmor 日志的会话中记录活动。日志包含了大量信息。以下是一个记录事件的示例:
== Alert / 2023-10-09 14:14:32.192243 ==
ClusterName: default
HostName: cluster01-worker
NamespaceName: demo
PodName: nginx-web-57794669f5-gd4r4
Labels: app=nginx-web
ContainerName: nginx
ContainerID: 8048c5fb3fd2425e401505cb4c12d147fddd71e6587ba3f3c488e609b28819a8
ContainerImage: docker.io/bitnami/nginx:latest@sha256:4ce786ce4a547b796cf23efe62b54a910de6fd41245012f10e5f75e85ed3563c
Type: MatchedPolicy
PolicyName: DefaultPosture
Source: /usr/bin/touch test
Resource: test
Operation: File
Action: Block
Data: syscall=SYS_OPENAT fd=-100 flags=O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK
Enforcer: AppArmor
Result: Permission denied
HostPID: 2.731789e+06
HostPPID: 2.731618e+06
Owner: map[Name:nginx-web Namespace:demo Ref:Deployment]
PID: 59
PPID: 53
ParentProcessName: /bin/bash
ProcessName: /bin/touch
UID: 1001
默认情况下,日志输出设置为文本格式,这在日志事件较多时可能难以筛选。如果您希望日志以 JSON 格式显示,可以在 logs 命令中添加 --json 标志。最适合您需求的格式通常取决于您用于存储日志事件的系统。在大多数情况下,JSON 是大多数日志系统首选的格式。
为了展示差异,我们执行与前一条日志相同的测试,尝试在 /bin 下创建一个文件。这将把输出从文本格式转换为 JSON,如下所示:
{"Timestamp":1696861352,"UpdatedTime":"2023-10-09T14:22:32.445655Z","ClusterName":"default","HostName":"cluster01-worker","NamespaceName":"demo","Owner":{"Ref":"Deployment","Name":"nginx-web","Namespace":"demo"},"PodName":"nginx-web-57794669f5-gd4r4","Labels":"app=nginx-web","ContainerID":"8048c5fb3fd2425e401505cb4c12d147fddd71e6587ba3f3c488e6 09b28819a8","ContainerName":"nginx","ContainerImage":"docker.io/bitnami /nginx:latest@sha256:4ce786ce4a547b796cf23efe62b54a910de6fd41245012f 10e5f75e85ed3563c","HostPPID":2731618,"HostPID":2739719,"PPID":53,"PID":60,"UID":1001,"ProcessName":"/bin/touch","PolicyName":"DefaultPosture","Type":"MatchedPolicy","Source":"/usr/bin/touch test","Operation":"File","Resource":"test","Data":"syscall=SYS_OPENAT fd=-100 flags=O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK","Enforcer":"AppArmor","Action":"Block","Result":"Permission denied"}
使用文本或 JSON 的决定通常取决于您计划用来解析数据的工具。JSON 是一种流行的日志格式,因为它比文本格式更容易解析数据。
你可能正在查看使用 karmor 日志功能,想知道仅将输出定向到控制台有多大用处。
默认情况下,日志被定向到stdout。虽然实时日志查看对监控事件的发生非常有价值,但持续观察事件并不总是可行的。通常的做法是将日志发送到文件中,然后查看该文件或将其发送到外部系统进行处理。KubeArmor 提供了通过使用--logPath标志来修改日志输出偏好的灵活性。这个标志允许你指定日志文件的目标位置,从而将日志数据重定向到指定的文件路径。
当你指定日志文件的路径时,必须包含你希望使用的完整路径和文件名。logPath选项可以与其他选项一起使用,比如将日志格式设置为 JSON。下面的示例命令将把日志发送到当前用户的主目录,并使用文件名karmor.logs以 JSON 格式保存:
karmor logs --json --logPath ~/karmor.logs
如果你在logPath中指定了一个新的目录,它必须在发送任何日志之前创建;KubeArmor 不会为你创建该目录。如果你在记录日志之前没有创建目录,你将收到日志输出屏幕上的错误,并且不会记录任何事件。
当 KubeArmor 捕获到第一个事件时,它会创建文件并继续收集数据,直到你停止记录:
-rw-rw-r-- 1 surovich surovich 2602 Oct 9 14:49 karmor.logs
和其他 karmor 选项一样,日志默认会监视整个集群,这可能会生成大量事件,使你难以找到真正需要查看的事件。你可以通过向logs选项添加标志来过滤日志,而不是记录整个集群。可用的标志包括:
| 标志 | 描述 |
|---|---|
--labels |
按标签过滤日志 |
--namespace |
按命名空间过滤日志 |
--pod |
按 pod 过滤日志 |
表 13.3:限制日志到特定对象
限制被记录对象的范围有一个明显的优势,那就是可以根据需要详细信息的特定对象定制日志数据。通过实施这种方法,你可以显著提高日志数据的精确性和相关性,确保它与你的特定监控、分析和故障排除需求直接对齐。限制被记录对象可以使你简化监控工作,使其更加高效,并有效提供对目标对象的洞察,同时减少因不必要的日志条目导致的噪音和冗余。
karmor vm
你知道通过 Kubernetes 可以运行虚拟机吗?这些虚拟机的部署和管理方式与使用 VMware 和 Microsoft 的虚拟化技术时有所不同。与其直接在虚拟化程序上运行操作系统,KubeVirt 的虚拟机实际上是在容器内运行的。它们看起来像是运行其他 Docker 镜像的标准 Pod,但它们不是微服务,而是一个完整的操作系统,支持 Windows 和 Linux。
KubeVirt 是一个复杂的话题,我们无法仅在本节中涵盖。你可以在他们的官网了解更多关于 KubeVirt 的信息,kubevirt.io/。
KubeArmor 团队意识到需要将运行时安全性扩展到支持使用 KubeVirt 运行的虚拟机。这是一个强大的功能,适用于在 Kubernetes 中运行虚拟机的组织,将 KubeArmor 为容器提供的安全性扩展到虚拟机。
总结
在本章中,我们探讨了如何加强我们运行时环境的安全性,提升整体安全态势。很多人误以为组织的集群已经安全,但许多集群往往忽视了容器中运行的内容,或忽视了用户通过kubectl exec连接到运行中的 Pod 的潜在风险。
本章还详细描述了容器安全性最有效的方法之一,涉及严格控制容器的进程,专门允许仅执行必要的进程,同时拒绝访问其他所有文件。通过利用像 KubeArmor 这样的工具,你可以为受限的二进制文件集合提供文件访问权限,阻止对其他所有进程的访问并进行保护。
问题
-
以下哪些是 LSM?
-
Accuknox
-
AppArmor
-
SELinux
-
LSMLinux
-
-
LSM 和 eBPF 提供相同的功能。
-
正确
-
错误
-
-
哪个 karmor 选项提供易于查看的实时信息?
-
监控
-
跟踪
-
配置文件
-
探测
-
-
以下哪项不是 KubeArmor 为增强 Kubernetes 集群安全性而提供的功能?
-
限制进程执行
-
文件访问控制
-
网络流量加密
-
创建安全策略
-
答案
-
b - AppArmor
-
b - 错误
-
c - 配置文件
-
c - 网络流量加密
加入我们书籍的 Discord 空间
加入本书的 Discord 工作区,参加每月的问我任何问题(Ask Me Anything)环节,与作者互动:

第十四章:备份工作负载
Kubernetes 的备份产品是我们不断进化的容器编排和云原生计算世界中的重要组成部分。在本章中,我们将探讨如何使用 Velero 的功能,以及它如何帮助你确保工作负载的韧性和可靠性。Velero 在意大利语中的意思是“安全”或“保护”,这个名字非常贴切,因为它为你的应用程序提供了安全网,使你能够在动态且不断变化的环境中自信地运行它们。
随着你深入了解 Kubernetes 和微服务,你会迅速意识到备份、恢复和迁移应用程序的优势。虽然 Kubernetes 是一个出色的容器化应用程序部署和管理系统,但它本身并没有提供数据保护和灾难恢复的工具。这一空白由Velero填补,它为保护你的 Kubernetes 工作负载及其相关数据提供了完整的解决方案。
Velero 最初名为 Heptio Ark。Heptio 是由 Kubernetes 的两位原始创建者 Joe Beda 和 Craig McLuckie 共同创办的公司。从那时起,它成为了 VMware Tanzu 产品组合的一部分,展示了它在 Kubernetes 生态系统中的重要性。
在本章中,我们将探索 Kubernetes 和 Velero 的关键功能和使用场景,从基本的备份和恢复操作到更高级的场景,如跨集群迁移。无论你是刚开始接触 Kubernetes,还是一位经验丰富的 Kubernetes 运维人员,Velero 都是一个值得学习的工具。
本章将涵盖以下主题:
-
理解 Kubernetes 备份
-
执行
etcd备份 -
介绍和设置 VMware 的 Velero
-
使用 Velero 备份工作负载和 PVC
-
使用 CLI 管理 Velero
-
从备份恢复
技术要求
为了进行本章的实践实验,你需要以下内容:
-
运行 Docker 的 Ubuntu 22.04+ 服务器,至少 8 GB 内存。
-
根据第二章中的规格构建的 KinD 集群。
-
从仓库中的
chapter14文件夹获取脚本,你可以通过访问本书的 GitHub 仓库来找到:github.com/PacktPublishing/Kubernetes-An-Enterprise-Guide-Third-Edition
理解 Kubernetes 备份
备份 Kubernetes 集群不仅仅是备份集群上运行的工作负载。你需要考虑任何持久化数据以及集群本身。记住,集群状态是保存在 etcd 数据库中的,因此它是一个非常重要的组件,必须进行备份,以便在灾难发生时进行恢复。
创建集群和运行工作负载的备份使你能够执行以下操作:
-
迁移集群。
-
从生产集群创建开发集群。
-
从灾难中恢复集群。
-
从持久卷中恢复数据。
-
命名空间和部署恢复。
在本章中,我们将提供详细信息和工具来备份您的 etcd 数据库、命名空间、其中的对象以及您附加到工作负载的任何持久数据。
在企业中从完全灾难中恢复集群通常涉及备份各种组件的自定义 SSL 证书,如 Ingress 控制器、负载均衡器和 API 服务器。由于在许多环境中备份所有自定义组件的过程不同,我们将专注于大多数 Kubernetes 发行版中通用的流程。
如您所知,集群状态由 etcd 维护,如果您丢失所有 etcd 实例,您将丢失您的集群。在多节点控制平面中,您应至少拥有三个 etcd 实例,为集群提供冗余。如果丢失一个节点,集群将继续运行,并且您可以用新节点替换失败的节点。一旦添加新实例,它将接收 etcd 数据库的副本,您的集群将恢复到完全冗余状态。
如果您丢失所有 etcd 服务器并且没有数据库备份,您将丢失集群,包括集群状态和所有工作负载。由于 etcd 非常重要,etcdctl 实用程序包含内置备份功能。在接下来的部分,我们将展示如何使用 etcdctl 实用程序进行 etcd 备份。
执行 etcd 备份
由于我们在 Kubernetes 集群中使用 KinD,我们可以创建 etcd 数据库的备份,但无法恢复它。
我们的 etcd 服务器在名为 etcd-cluster01-control-plane 的 Pod 中运行,位于 kube-system 命名空间。在创建 KinD 集群时,我们为控制平面节点添加了额外的端口映射,暴露了端口 2379,用于访问 etcd。在您自己的生产环境中,可能不会将 etcd 端口暴露给外部请求,但备份数据库的过程仍将与本节中解释的步骤类似。
备份所需的证书
大多数 Kubernetes 安装将证书存储在 /etc/kubernetes/pki 中。在这方面,KinD 也不例外,因此我们可以使用 docker cp 命令备份我们的证书。
我们已经说了几次:etcd 非常重要!因此,直接访问数据库可能会有一些安全措施。确实如此,要访问它,您需要在执行针对数据库的命令时提供正确的证书。在企业中,您应将这些密钥存储在安全位置。在我们的示例中,我们将从 KinD 节点提取证书。
我们在 chapter14/etcd 目录中包含了一个名为 install-etcd-tools.sh 的脚本,该脚本将执行下载和执行 etcd 数据库备份的步骤。要执行该脚本,请切换到 chapter14/etcd 目录并执行安装脚本。
运行脚本将下载etcd工具,解压后将其移动到usr/bin目录,以便我们能够轻松地执行它们。然后它会为证书创建一个目录,并将证书复制到新创建的目录/etcd/certs中。我们用于备份etcd的证书如下:
-
ca.crt -
healthcheck-client.crt -
healthcheck-client.key
当您使用etcdctl工具执行命令时,您需要提供密钥,否则您的操作将被拒绝。
现在我们已经拥有了访问etcd所需的证书,下一步是创建数据库的备份。
备份etcd数据库
etcd的创建者开发了一个用于备份和恢复etcd数据库的工具,叫做etcdctl。对于我们的用途,我们只会使用备份操作;然而,由于etcd不仅限于 Kubernetes,因此该工具有许多选项,作为 Kubernetes 操作员或开发人员,您不会使用这些选项。如果您想了解更多关于此工具的信息,可以访问etcd-io的 Git 仓库:github.com/etcd-io/etcd。
要备份数据库,您需要etcdctl工具以及访问数据库所需的证书,这些证书我们已经从控制平面服务器复制过来了。
我们在上一节中执行的脚本下载了etcdctl,并将其移入了usr/bin目录。要创建数据库的备份,请确保您位于chapter14/etcd目录,并且certs目录已存在,并且证书已下载到该目录中。
为了备份etcd,我们执行etcd snapshot save命令:
etcdctl snapshot save etcd-snapshot.db --endpoints=https://127.0.0.1:2379 --cacert=./certs/ca.crt --cert=./certs/healthcheck-client.crt --key=./certs/healthcheck-client.key
旧版本的etcdctl需要使用ETCDCTL_API=3来设置 API 版本为 3,因为它们默认使用的是版本 2 的 API。etcd 3.4更改了默认 API 为 3,因此我们在使用etcdctl命令之前无需设置该变量。
数据库复制过来不应该花费太长时间。如果超过几秒钟仍未完成,您应该尝试在命令中添加--debug=true标志。添加调试标志会在执行快照时提供更多输出。快照失败的最常见原因是证书错误。下面是一个证书不正确导致快照命令输出的详细信息示例:
2023/11/17 21:39:19 INFO: [core] Creating new client transport to "{Addr: \"127.0.0.1:2379\", ServerName: \"127.0.0.1:2379\", }": connection error: desc = "transport: authentication handshake failed: tls: failed to verify certificate: x509: certificate signed by unknown authority (possibly because of \"crypto/rsa: verification error\" while trying to verify candidate authority certificate \"etcd-ca\")"
请注意x509错误。这很可能是由于etcdctl命令中的证书不正确所导致的。请检查您是否使用了正确的证书,然后重新运行命令。
如果命令成功,您将收到类似以下的输出:
{"level":"info","ts":"2023-11-17T21:44:38.316265Z","caller":"snapshot/v3_snapshot.go:65","msg":"created temporary db file","path":"etcd-snapshot.db.part"}
{"level":"info","ts":"2023-11-17T21:44:38.329699Z","logger":"client","caller":"v3@v3.5.10/maintenance.go:212","msg":"opened snapshot stream; downloading"}
{"level":"info","ts":"2023-11-17T21:44:38.329756Z","caller":"snapshot/v3_snapshot.go:73","msg":"fetching snapshot","endpoint":"https://127.0.0.1:2379"}
{"level":"info","ts":"2023-11-17T21:44:38.45673Z","logger":"client","caller":"v3@v3.5.10/maintenance.go:220","msg":"completed snapshot read; closing"}
{"level":"info","ts":"2023-11-17T21:44:38.461743Z","caller":"snapshot/v3_snapshot.go:88","msg":"fetched snapshot","endpoint":"https://127.0.0.1:2379","size":"6.6 MB","took":"now"}
{"level":"info","ts":"2023-11-17T21:44:38.46276Z","caller":"snapshot/v3_snapshot.go:97","msg":"saved","path":"etcd-snapshot.db"}
Snapshot saved at etcd-snapshot.db
接下来,我们可以通过尝试一个简单的etcdctl命令来验证数据库是否成功复制,这个命令会提供备份的概述:
etcdctl --write-out=table snapshot status etcd-snapshot.db
这将输出备份概述:
+----------+----------+------------+------------+
| HASH | REVISION | TOTAL KEYS | TOTAL SIZE |
+----------+----------+------------+------------+
| 224e9348 | 6222 | 1560 | 6.6 MB |
+----------+----------+------------+------------+
在这个示例中,我们只备份了一次etcd数据库。在实际场景中,您应该创建一个定时任务,定期执行etcd的快照,并将备份文件存储在安全的位置。
由于 KinD 运行控制平面的方式,我们无法使用本节中的恢复程序。本节仅提供备份步骤,以便您了解如何在企业环境中备份 etcd 数据库。
到目前为止,您已经了解了在 Kubernetes 中备份工作负载和持久化数据(包括 etcd 数据库)的关键重要性。拥有一个良好的备份策略可以帮助您便捷地迁移集群,从生产集群创建新的开发集群,并从灾难中恢复。通过了解这些策略,您可以确保更好的灾难恢复准备、更高的操作效率以及数据安全。掌握这些技巧将帮助您更有效地管理和恢复 Kubernetes 集群,确保环境的弹性和可靠性。
现在,让我们继续介绍我们将用于演示 Kubernetes 备份的工具:Velero。
介绍并设置 VMware 的 Velero
Velero 是一个开源的 Kubernetes 备份解决方案,最初由一家名为 Heptio 的公司开发。随着 VMware 增强对 Kubernetes 的支持,它收购了多家公司,其中 Heptio 是其收购之一,将 Velero 纳入 VMware 旗下。
VMware 已将其大多数 Kubernetes 相关产品纳入 Tanzu 体系下。对于一些人来说,这可能有些令人困惑,因为 Tanzu 的最初版本是将多个组件部署到 vSphere 集群上以增加对 Kubernetes 的支持。自从 Tanzu 初期版本以来,它已经包含了如 Velero、Harbor 和 Tanzu 应用平台(TAP)等组件,这些组件都不需要 vSphere 才能运行;它们可以在任何标准的 Kubernetes 集群中本地运行。
即便经历了所有的所有权和品牌变更,Velero 的基本功能依然保留。它提供了许多仅在商业产品中才有的功能,包括调度、备份钩子和细粒度的备份控制——而且这些功能都是免费的。
虽然 Velero 是免费的,但它有一定的学习曲线,因为它不像大多数商业产品那样包括一个易于使用的图形用户界面。Velero 中的所有操作都通过命令行工具执行,这个工具是一个名为 velero 的可执行文件。这个单一的可执行文件允许您安装 Velero 服务器、创建备份、检查备份状态、恢复备份等。由于管理的每个操作都可以通过一个文件完成,因此恢复集群的工作负载是一个非常简单的过程。在本章中,我们将创建一个第二个 KinD 集群,并使用现有集群的备份来填充它。
但是在此之前,我们需要处理一些要求。
Velero 的要求
Velero 由几个组件组成,您可以用这些组件创建备份系统。
-
Velero CLI:提供 Velero 组件的安装。用于所有备份和恢复功能。
-
Velero 服务器:负责执行备份和恢复操作。
-
存储提供商插件:用于将备份和恢复到特定的存储系统。
除了基础的 Velero 组件,你还需要提供一个对象存储位置,用于存储你的备份。如果你没有对象存储解决方案,可以部署 MinIO,这是一个开源项目,提供兼容 S3 的对象存储。我们将在 KinD 集群中部署 MinIO,演示 Velero 提供的备份和恢复功能。
安装 Velero CLI
部署 Velero 的第一步是下载最新的 Velero CLI 二进制文件。我们在 chapter14 目录中提供了一个名为 install-velero-binary.sh 的脚本,它将下载 Velero 二进制文件,移动到 /usr/bin 目录,并输出 Velero 的版本以验证二进制文件是否正确安装。在本章编写时,Velero 的最新版本是 1.12.1。
你可以安全地忽略最后一行,它显示的是找不到 Velero 服务器的错误。目前,我们仅安装了 Velero 可执行文件,因此它无法找到服务器。接下来的部分,我们将安装服务器以完成安装。
安装 Velero
Velero 的系统要求非常少,大部分都很容易满足:
-
一个运行版本 1.16 或更高的 Kubernetes 集群
-
Velero 可执行文件
-
系统组件的图像
-
一个兼容的存储位置
-
一个卷快照插件(可选)
根据你的基础设施,可能没有适合的备份位置或快照卷。幸运的是,如果没有兼容的存储系统,你可以添加开源选项到集群中以满足要求。
在下一部分,我们将解释本地支持的存储选项,由于我们的示例将使用 KinD 集群,我们将安装开源选项以添加兼容存储作为备份位置。
备份存储位置
Velero 需要一个兼容 S3 的存储桶来存储备份。有许多官方支持的系统,包括 AWS、Azure 和 Google 的所有对象存储服务。
在下表中,支持 列表示该插件提供一个兼容的存储位置用于存储 Velero 备份。卷快照支持 列表示该插件支持使用快照备份持久卷。如果使用的 CSI 不提供快照支持,数据将通过 Restic 或 Kopia 使用标准文件系统备份进行备份。快照有几个优势,其中最重要的是它们能够保持应用的一致性。Velero 确保快照以一种能够保持应用状态的方式进行捕捉,从而最小化数据损坏的可能性。
除了官方支持的提供商外,还有许多来自社区和供应商的提供商,来自如 DigitalOcean、惠普(Hewlett-Packard)和 Portworx 等公司。下表列出了所有当前的提供商:
| 供应商 | 对象存储 | 卷快照支持 | 支持 |
|---|---|---|---|
| Amazon | AWS S3 | AWS EBS | 官方 |
| Google Cloud Storage | GCE 磁盘 | 官方 | |
| Microsoft | Azure Blob 存储 | Azure 管理磁盘 | 官方 |
| VMware | 不支持 | vSphere 卷 | 官方 |
| Kubernetes CSI | 不支持 | CSI 卷 | 官方 |
| Alibaba Cloud | Alibaba Cloud OSS | Alibaba Cloud | 社区 |
| DigitalOcean | DigitalOcean 对象存储 | DigitalOcean 卷块存储 | 社区 |
| HP | 不支持 | HPE 存储 | 社区 |
| OpenEBS | 不支持 | OpenEBS cStor 卷 | 社区 |
| Portworx | 不支持 | Portworx 卷 | 社区 |
| Storj | Storj 对象存储 | 不支持 | 社区 |
表 14.1: Velero 存储选项
如果你没有对象存储解决方案,可以部署开源的 S3 提供商 MinIO,这就是我们在本章中用于 S3 目标的解决方案。
现在,Velero 可执行文件已安装,并且我们的 KinD 集群有了持久存储,得益于 Rancher 的自动供应器,我们可以继续进行第一个要求——为 Velero 添加一个 S3 兼容的备份位置。
部署 MinIO
MinIO 是一个开源的对象存储解决方案,兼容 Amazon 的 S3 云服务 API。你可以在其 GitHub 仓库中阅读更多关于 MinIO 的信息:github.com/minio/minio。
如果你使用来自互联网的清单安装 MinIO,请务必在尝试将其用作备份位置之前,验证部署中声明的卷。互联网上许多示例使用了 emptyDir: {},这不是持久存储。
我们在 chapter14 文件夹中包含了来自 Velero GitHub 仓库的修改版 MinIO 部署。由于我们的集群具有持久存储,我们编辑了部署中的卷,使用了 持久卷声明 (PVCs),这些 PVC 将使用自动供应器来为 Velero 的数据和配置提供支持。
要部署 MinIO 服务器,切换到 chapter14 目录并执行 kubectl create。该部署将在你的 KinD 集群上创建 Velero 命名空间、PVC 和 MinIO。部署可能需要一些时间完成。根据主机系统,部署的时间可能从一分钟到几分钟不等:
kubectl create -f minio-deployment.yaml
这将部署 MinIO 服务器,并将其公开为 minio,监听端口 9000/TCP,控制台监听端口 9001/TCP,如下所示:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
console ClusterIP 10.102.216.91 <none> 9001/TCP 42h
minio ClusterIP 10.110.216.37 <none> 9000/TCP 42h
MinIO 服务器可以通过集群中的任何 Pod,使用正确的访问密钥,通过 minio.velero.svc 和端口 9000 进行访问。
部署 MinIO 后,我们需要通过 Ingress 规则公开控制台,这样我们就可以登录查看桶并验证备份是否按预期工作。
暴露 MinIO 和控制台
默认情况下,你的 MinIO 存储只能在部署它的集群内使用。由于我们将在本章最后演示如何恢复到不同的集群,因此我们需要通过 Ingress 规则暴露 MinIO。MinIO 还包括一个仪表盘,允许你浏览服务器上 S3 桶的内容。为了访问仪表盘,你可以部署一个暴露 MinIO 控制台的 Ingress 规则。
我们在 chapter14 文件夹中包含了一个名为 create-minio-ingress.sh 的脚本,它将使用 nip.io 语法(minio-console.w.x.y.z.nip.ip 和 minio.w.x.y.z.nip.ip)并将你的主机 IP 创建一个 Ingress 规则。
当你在集群中安装 Velero 时,你将需要 console Ingress 规则。
部署完成后,你可以在任何机器上使用浏览器,打开你为 Ingress 规则设置的 URL。在我们的集群中,主机 IP 是 10.2.1.161,所以我们的 URL 是 minio-console.10.2.1.161.nip.io:

图 14.1:MinIO 仪表盘
要访问仪表盘,请提供 MinIO 部署中的访问密钥和秘密密钥。如果你使用了 GitHub 仓库中的 MinIO 安装程序,用户名和密码已经在清单中定义,它们是 packt/packt123。
登录后,你将看到一个桶列表以及存储在其中的任何项目。你应该看到一个名为 velero 的桶,这是我们用来备份集群的桶。这个桶是在初始 MinIO 部署期间创建的——我们在部署中添加了一行,创建了 velero 桶并为 packt 用户设置了必要的权限。

图 14.2:MinIO 浏览器
如果你是对象存储的新手,重要的是要注意,虽然这会在你的集群中部署一个存储解决方案,但它不会创建 StorageClass,也不会与 Kubernetes 进行任何集成。所有对 S3 桶的访问将使用我们将在下一部分提供的 URL。
既然你已经运行了一个与 S3 兼容的对象存储,你需要创建一个配置文件,Velero 将使用这个文件来定位你的 MinIO 服务器。
安装 Velero
要在你的集群中部署 Velero,你可以使用 Velero 二进制文件或 Helm 图表。我们选择使用二进制文件安装 Velero。
在我们开始安装之前,我们需要创建一个凭证文件,包含 MinIO 上 S3 目标的 access_key 和 secret_access_key。
在 chapter14 文件夹中创建一个新的凭证文件,命名为 credentials-velero,并包含以下内容:
[default]aws_access_key_id = packt
aws_secret_access_key = packt123
接下来,我们可以使用 Velero 可执行文件和 install 选项来部署 Velero,并选择备份持久化卷的选项。
使用以下命令在chapter14文件夹内执行 Velero 安装,以部署 Velero。请注意,您需要为 MinIO 提供nip.io的入口名称。当我们之前创建 Ingress 规则时,我们暴露了 MinIO 和控制台。请务必使用包含minio.w.x.y.z.nip.io的入口名称;不要使用minio-console入口,否则 Velero 将无法找到 S3 存储桶。 |
velero install --provider aws --plugins velero/velero-plugin-for-aws:v1.2.0 --bucket velero --secret-file ./credentials-velero --use-volume-snapshots=false --backup-location-config region=minio,s3ForcePathStyle="true",s3Url=http://minio.velero.svc:9000 --use-node-agent --default-volumes-to-fs-backup
让我们解释一下安装选项及其含义:
| 选项 | 描述 |
|---|---|
--provider |
配置 Velero 使用存储提供商。由于我们使用的是兼容 S3 的 MinIO,我们将aws作为我们的提供商。 |
--plugins |
告诉 Velero 使用哪个备份插件。对于我们的集群,由于我们使用 MinIO 作为对象存储,我们选择了 AWS 插件。 |
--bucket |
您希望目标的 S3 存储桶名称。 |
--secret-file |
指向包含用于与 S3 存储桶进行身份验证的凭据文件。 |
--use-volume-snapshots |
启用或禁用支持快照的提供商的卷快照。目前,Velero 只支持带有快照的对象存储;如果不支持快照,则应将此项设置为false。由于我们在示例中不使用快照,因此将此项设置为false。 |
--backup-location-config |
Velero 将备份存储到的 S3 目标位置。由于 MinIO 与 Velero 运行在同一个集群中,我们可以使用名称minio.velero.svc:9000来定位 S3。在生产环境中,您可能会将 MinIO 与 Velero 放在同一个集群中,并使用外部的 S3 目标来存储备份。使用 Kubernetes 服务名称将导致 Velero 的describe命令出现一些错误,因为它尝试使用提供的名称查询集群,而您无法从集群外部访问minio.velero.svc。 |
--use-node-agent |
如果您希望使用 Velero 的节点代理备份持久卷,请添加此标志。 |
--default-volumes-to-fs-backup |
配置 Velero 支持选择不备份持久卷。如果在部署过程中未添加此选项,您仍然可以在 Velero 备份时选择备份卷。本节的备份 PVCs部分将详细说明。 |
表 14.2:Velero 安装选项
在执行安装时,您将看到一系列对象被创建,包括多个CustomResourceDefinitions(CRDs)和 Velero 用于处理备份与恢复操作的其他对象。 |
如果在启动 Velero 服务器时遇到问题,您可以查看一些 CRD 和 Secrets,它们可能包含错误信息。以下表格解释了您在使用 Velero 时可能需要交互的一些常见对象:
| CustomResourceDefinition | 名称 | 描述 |
|---|---|---|
backups.velero.io |
Backup |
每个创建的备份将生成一个名为 backup 的对象,其中包括每个备份任务的设置。 |
backupstoragelocations.velero.io |
BackupStorageLocation |
每个备份存储位置会创建一个 BackupStorageLocation 对象,其中包含连接存储提供商的配置。 |
schedules.velero.io |
Schedule |
每个定期备份都会创建一个 Schedule 对象,其中包含备份的计划。 |
volumesnapshotlocations.velero.io |
VolumeSnapshotLocation |
如果启用,VolumeSnapshotLocation 对象包含用于卷快照的存储信息。 |
| 密钥名称 | 描述 | |
cloud-credentials |
包含以 Base64 格式连接存储提供商所需的凭据。如果你的 Velero pod 无法启动,可能是 data.cloud 配置中的值不正确。 |
|
velero-repo-credentials |
如果你使用 Restic 插件,它将包含你的存储库密码,类似于 cloud-credentials。如果遇到连接卷快照提供者的问题,请验证存储库密码是否正确。 |
表 14.3:Velero 的 CRD 和密钥 |
尽管你与这些对象的交互大多通过 Velero 可执行文件进行,但理解这些工具如何与 API 服务器交互总是一个好习惯。如果你没有访问 Velero 可执行文件的权限,但需要查看或可能更改某个对象的值以快速解决问题,理解对象及其功能将非常有帮助。 |
现在我们已经安装了 Velero,并对 Velero 对象有了高层次的理解,接下来可以开始为集群创建不同的备份任务了。 |
使用 Velero 备份工作负载和 PVCs |
Velero 支持通过单个命令执行一次性备份或按照计划定期执行备份。无论你选择执行单次备份还是定期备份,你都可以使用 include 和 exclude 标志来备份所有对象或仅备份某些对象。 |
备份 PVCs |
由于数据在 Kubernetes 集群中变得越来越常见,我们将备份所有集群工作负载,包括集群中的任何 PVCs。当我们安装 Velero 时,我们添加了 --use-node-agent 选项,这创建了一个 DaemonSet,它在每个集群节点上创建一个节点代理。DaemonSet 部署了一个包含能够执行文件系统备份的模块的 pod,包括每个节点上的数据迁移工具,这些工具可能是 Restic 或 Kopia(默认),并且在 velero 命名空间中创建了一个新的密钥,名为 velero-repo-credentials。这个密钥包含一个 repository-password,用于你的备份。这是一个生成的密码,你可以将其更改为任何你想要的密码——但是,如果你打算更改密码,请在创建任何备份之前进行更改。如果在创建备份后更改此密码,Velero 将无法读取旧的备份。 |
默认的 ServiceAccount 令牌、Secrets 和 ConfigMaps 可以映射到卷上。这些不是包含数据的卷,不会通过节点代理进行备份。像其他基础 Kubernetes 对象一样,它们将在 Velero 备份其他命名空间对象时一起被备份。
数据迁移器负责从卷中复制数据。早期的 Velero 版本使用 Restic 作为数据迁移器,但现已增强,包括在节点 DaemonSet 中同时使用 Restic 和 Kopia。默认情况下,Kopia 将作为数据迁移器使用,但如果你希望使用 Restic,可以通过在 Velero 备份创建命令中添加 --data-mover restic 选项来更改默认设置。关于使用哪个数据迁移器存在一些争论,Kopia 已成为领先者,因此它已经成为默认选项。
Velero 可以通过两种不同的方法进行 PVC 备份:
-
选择退出:Velero 会备份所有 PVC,除非某个工作负载带有要忽略的卷名注释。
-
选择加入:只有带有卷名注释的工作负载才会被备份。这是 Velero 的默认配置。
让我们更详细地了解这两种方法。
使用选择退出的方法
这是我们将在练习中使用的方法。使用此方法时,除非你在 Pod 中指定注释,添加 backup.velero.io/backup-volumes-excludes,否则所有 PVC 都会被备份。例如,如果你在某个命名空间中有 3 个 PVC,分别名为 volume1、volume2 和 volume3,并且你希望排除 volume2 和 volume3 不被备份,你需要在部署中的 Pod 规格中添加以下注释:
backup.velero.io/backup-volumes-excludes=volume2,volume3
由于我们只将 volume2 和 volume3 添加到了排除列表中,Velero 将忽略这些卷的备份,但会备份名为 volume1 的 PVC,因为它未包含在排除列表中。
在安装 Velero 时,我们设置了 --default-volumes-to-fs-backup,这告诉 Velero 备份所有持久数据,除非某个卷有注释排除它不被备份。如果你在 Velero 部署时没有设置该选项,可以通过在 backup 命令中添加相同的选项 --default-volumes-to-fs-backup 来告诉 Velero 对单次备份使用选择退出方法。
velero backup create BACKUP_NAME --default-volumes-to-fs-backup OTHER_OPTIONS
当使用此选项创建备份时,Velero 会备份所有附加到 Pods 的持久卷,除非它在 excludes 注释中被排除。
使用选择加入的方法
如果你在部署 Velero 时没有使用 --default-volumes-to-fs-backup 选项,持久卷将不会被备份,除非你添加注释告诉 Velero 备份所需的卷。
类似于你在之前的示例中选择退出的方式,你可以在部署中添加注释,指示 Velero 备份你的卷。你需要添加的注释是 backup.velero.io/backup-volumes,以下示例告诉 Velero 备份两个卷,一个叫做 volume1,另一个叫做 volume2:
kubectl -n demo annotate deploy/demo backup.velero.io/backup-volumes=volume1,volume2
当你运行下一个备份时,Velero 会看到注解,并将这两个持久化卷添加到备份任务中。
备份数据的限制
Velero 无法备份使用 hostPath 作为持久化数据的卷。local-path-provisioner 默认将持久化磁盘映射到 hostPath,这意味着 Velero 将无法备份或恢复数据。幸运的是,有一个选项可以将类型从 hostPath 更改为 local,这样就能与 Velero 一起使用。当你创建新的 PVC 时,可以添加注解 volumeType: local。下面的示例显示了一个将作为本地类型而非 hostPath 创建的 PVC 清单:
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: test-claim
namespace: demo
annotations:
volumeType: local
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
在许多情况下,这个变化并不必要,但由于在使用 local-path-provisioner 时需要这样做,我们将需要向任何我们希望与 Velero 测试的 PVC 添加注解。
使用 Velero 并部署了能够备份我们持久化数据的功能后,接下来我们开始创建一个一次性的集群备份。
执行一次性集群备份
要创建初始备份,你可以运行一个单独的 Velero 命令,它将备份集群中的所有命名空间,如果有任何 PVC 没有被注解为忽略,它们也将被备份。
执行没有任何标志的备份命令,将会备份每个命名空间以及命名空间中的所有对象。
我们将在本节所学的基础上执行恢复操作,以展示 Velero 的实际应用。在我们的备份中,我们将备份整个集群,包括 PVC。
在我们开始备份之前,我们将添加一个带有 PVC 的部署,其中我们将添加一些空文件,以验证数据恢复是否按预期工作。
在 chapter14/pvc-example 目录中,有一个名为 busybox-pvc.yaml 的清单文件。要部署示例,你应该从 chapter14/pvc-example 目录中执行命令:
kubectl create -f busybox-pvc.yaml
该脚本将创建一个名为 demo 的新命名空间,并部署一个使用名为 test-claim 的 PVC 的 busybox-pvc Pod,该 PVC 挂载在容器的 /mnt 目录中。
目前,PVC 中有一个文件,original-data。我们需要再添加几个文件,稍后测试恢复操作。首先,让我们使用 kubectl exec 来验证 PVC 的当前内容,列出目录内容。如果你在自己的集群中进行操作,需要将 busybox-pvc Pod 名称更改为你集群中正在使用的名称。你可以通过 kubectl get pods -n demo 获取 Pod 名称:
kubectl get pods -n demo
这将列出 busybox Pod 的信息。你将需要 Pod 的名称来执行 exec 命令,在 PVC 中创建文件:
NAME READY STATUS RESTARTS AGE
busybox-pvc-6cb895b675-grnxq 1/1 Running 0 4m29s
kubectl exec -it busybox-pvc-f7bfbcc44-vfsnf -n demo -- ls /mnt -la
total 8
drwxrwxrwx 2 root root 4096 Dec 7 15:41 .
drwxr-xr-x 1 root root 4096 Dec 7 15:41 ..
-rw-r--r-- 1 root root 0 Dec 7 15:41 original-data
现在,让我们向 Pod 中添加两个名为 newdata1 和 newdata2 的额外文件。为此,我们将使用另一个 kubectl exec 命令,在 /mnt 目录中创建这些文件。
kubectl exec -it busybox-pvc-f7bfbcc44-vfsnf -n demo -- touch /mnt/newfile1
kubectl exec -it busybox-pvc-f7bfbcc44-vfsnf -n demo -- touch /mnt/newfile2
同样,为了验证数据是否已成功写入,我们可以使用 kubectl exec 列出目录内容:
kubectl exec -it busybox-pvc-f7bfbcc44-vfsnf -n demo -- ls /mnt -la total 8
drwxrwxrwx 2 root root 4096 Dec 7 15:48 .
drwxr-xr-x 1 root root 4096 Dec 7 15:46 ..
-rw-r--r-- 1 root root 0 Dec 7 15:48 newfile1
-rw-r--r-- 1 root root 0 Dec 7 15:48 newfile2
-rw-r--r-- 1 root root 0 Dec 7 15:41 original-data
很好!我们可以看到两个新文件已经创建。现在,我们在 Pod 中有了新数据,可以继续进行集群备份。
要创建一次性备份,请使用velero命令并添加backup create <备份名称>选项。在我们的示例中,我们将备份命名为initial-backup:
velero backup create initial-backup
您从中获得的唯一确认信息是备份请求已被提交:
Backup request "initial-backup" submitted successfully.
Run `velero backup describe initial-backup` or `velero backup logs initial-backup` for more details.
幸运的是,Velero 还会告诉您检查备份状态和日志的命令。输出的最后一行告诉我们,可以使用velero命令与backup选项并结合describe或logs来检查备份操作的状态。
describe选项将显示任务的所有详细信息。以下是一个示例:
velero backup describe initial-backup
Name: initial-backup
Namespace: velero
Labels: velero.io/storage-location=default
Annotations: velero.io/resource-timeout=10m0s
velero.io/source-cluster-k8s-gitversion=v1.28.0
velero.io/source-cluster-k8s-major-version=1
velero.io/source-cluster-k8s-minor-version=28
Phase: Completed
Namespaces:
Included: *
Excluded: <none>
Resources:
Included: *
Excluded: <none>
Cluster-scoped: auto
Label selector: <none>
Or label selector: <none>
Storage Location: default
Velero-Native Snapshot PVs: auto
Snapshot Move Data: false
Data Mover: velero
TTL: 720h0m0s
CSISnapshotTimeout: 10m0s
ItemOperationTimeout: 4h0m0s
Hooks: <none>
Backup Format Version: 1.1.0
Started: 2023-12-06 18:46:57 +0000 UTC
Completed: 2023-12-06 18:48:15 +0000 UTC
Expiration: 2024-01-05 18:46:57 +0000 UTC
Total items to be backed up: 641
Items backed up: 641
Velero-Native Snapshots: <none included>
kopia Backups (specify --details for more information):
Completed: 4
请注意最后一部分。该部分告诉我们 Velero 使用 Kopia 数据移动器备份了 4 个 PVC。在我们执行备份时,会进一步展示这一点。
为了进一步强调前面提到的 Velero 使用的某些 CRD,我们还想解释 Velero 工具从哪里获取这些信息。
每个创建的备份都会在 Velero 命名空间中创建一个备份对象。对于我们的初始备份,创建了一个名为initial-backup的新备份对象。使用kubectl,我们可以描述该对象,以查看 Velero 可执行文件提供的类似信息。
如前面的输出所示,describe选项会显示备份任务的所有设置。由于我们没有为备份请求传递任何选项,因此该任务包含所有命名空间和对象。一些需要验证的重要细节包括阶段、备份的总项数以及已备份的项目。
如果阶段的状态不是success,您可能没有备份所有需要的项目。检查已备份项目也是一个好主意;如果备份的项目数少于要备份的项目数,则说明备份没有包含所有项目。
您可能需要检查备份的状态,但可能没有安装 Velero 可执行文件。由于这些信息在 CR 中,我们可以描述 CR 以获取备份的详细信息。运行kubectl describe命令查看备份对象将显示备份的状态:
kubectl describe backups initial-backup -n velero
如果我们跳到describe命令输出的底部,您将看到以下内容:
Name: initial-backup
Namespace: velero
Labels: velero.io/storage-location=default
Annotations: velero.io/resource-timeout: 10m0s
velero.io/source-cluster-k8s-gitversion: v1.28.0
velero.io/source-cluster-k8s-major-version: 1
velero.io/source-cluster-k8s-minor-version: 28
API Version: velero.io/v1
Kind: Backup
Metadata:
Creation Timestamp: 2023-12-06T18:46:57Z
Generation: 15
Resource Version: 35606
UID: 005da7ae-f270-470e-8907-c122815af365
Spec:
Csi Snapshot Timeout: 10m0s
Default Volumes To Fs Backup: true
Hooks:
Included Namespaces:
*
Item Operation Timeout: 4h0m0s
Metadata:
Snapshot Move Data: false
Storage Location: default
Ttl: 720h0m0s
Volume Snapshot Locations:
default
Status:
Completion Timestamp: 2023-12-06T18:48:15Z
Expiration: 2024-01-05T18:46:57Z
Format Version: 1.1.0
Phase: Completed
Progress:
Items Backed Up: 641
Total Items: 641
Start Timestamp: 2023-12-06T18:46:57Z
Version: 1
Warnings: 3
Events: <none>
在输出中,您可以看到阶段已完成、开始和完成时间,以及已备份并包含在备份中的对象数量。
使用一个集群附加组件来基于日志文件中的信息或对象状态生成警报是一种良好的做法,例如AlertManager。
您始终希望备份成功,如果备份失败,应该立即查看失败原因。
为了验证备份是否正确存储在我们的 S3 目标中,返回到 MinIO 控制台,如果你尚未进入Bucket视图,点击左侧的Buckets。如果你已经在Bucket界面,按 F5 刷新浏览器以更新视图。视图刷新后,你应该能看到velero存储桶中有对象。如果点击存储桶,你将看到另一个界面,其中会显示两个文件夹,一个是备份文件夹,一个是 Kopia 文件夹。Velero 会通过将数据存储在kopia(或者如果使用 restic 作为数据迁移工具,则为 restic)文件夹中,来将 Kubernetes 对象的数据分开。所有 Kubernetes 对象将存储在backups文件夹中。

图 14.3:存储桶详情
由于velero存储桶的概览显示了存储使用情况和对象数量,并且我们看到了backups和kopia文件夹,因此我们可以安全地假设初始备份是成功的。我们将使用此备份来恢复本章中从备份恢复部分删除的命名空间。
一次性备份并不是你会经常运行的操作。你应该定期、按计划备份你的集群。在接下来的部分,我们将解释如何创建定期备份。
调度集群备份
创建一次性备份非常有用,如果你有一个计划中的集群操作或命名空间中的重大软件升级。由于这些事件是罕见的,你会希望定期安排备份集群,而不是随机的一次性备份。
要创建一个定期备份,你可以使用 schedule 选项并创建一个带有 Velero 可执行文件的标签。除了 schedule 和创建标签外,你还需要为任务提供一个名称和 schedule 标志,该标志接受基于 cron 的表达式。以下调度告诉 Velero 每天凌晨 1 点进行备份:

图 14.4:Cron 调度表达式
使用图 14.4中的信息,我们可以创建一个将在凌晨 1 点执行的备份,使用以下velero schedule create命令:
velero schedule create cluster-daily-1 --schedule="0 1 * * *"
Velero 将回复说调度已成功创建:
Schedule "cluster-daily-1" created successfully.
如果你不熟悉 cron 及其可用选项,应该阅读 cron 包文档,网址是 godoc.org/github.com/robfig/cron。
cron 还接受一些简写表达式,使用这些简写可能比使用标准的 cron 表达式更为简便。以下表格包含了预定义调度的简写:
| 简写值 | 描述 |
|---|---|
@yearly |
每年执行一次,在 1 月 1 日午夜时分 |
@monthly |
每月执行一次,在每个月的第一天午夜时分 |
@weekly |
每周执行一次,在周日午夜时分 |
@daily |
每天午夜时分执行 |
@hourly |
每小时的开始时执行 |
表 14.4:Cron 简写调度
使用简写表中的值来调度一个每天午夜执行的备份任务,我们使用以下 Velero 命令:
velero schedule create cluster-daily-2 --schedule="@daily"
这将创建一个备份任务,每晚午夜时分备份集群。你可以通过查看 schedules 对象,使用 kubectl get schedules -n velero 来验证任务是否已创建以及上次运行的时间:
NAMESPACE NAME STATUS SCHEDULE LASTBACKUP AGE PAUSED
velero cluster-daily Enabled @daily 63s
定期任务将在任务执行时创建一个备份对象。备份名称将包含调度的名称,中间用破折号和备份的日期时间。备份名称遵循标准命名格式 YYYYMMDDhhmmss。使用前面示例中的名称,我们的初始备份名称为 cluster-daily-20231206200028。这里的 20231206200028 是备份运行的日期,而 200028 是备份运行的时间(UTC 时间)。这相当于 2021-09-30 20:00:28 +0000 UTC。
到目前为止,我们的所有示例都配置为备份集群中的所有命名空间和对象。你可能需要根据你的具体集群创建不同的调度,或者排除/包含某些对象。
在下一节中,我们将解释如何创建自定义备份,允许你使用特定的标签来包含和排除命名空间和对象。
创建自定义备份
当你创建任何备份任务时,你可以提供标志来定制哪些对象将被包含或排除在备份任务中。这里详细介绍了一些最常用的标志:
| 标志 | 描述 |
|---|---|
--exclude-namespaces |
要排除在备份任务之外的命名空间的逗号分隔列表。示例:--exclude-namespaces web-dev1,web-dev2。 |
--exclude-resources |
要排除的资源的逗号分隔列表,格式为 resource.group。示例:--exclude-resources storageclasses.storage.k8s.io。 |
--include-namespaces |
要包含在备份任务中的命名空间的逗号分隔列表。示例:--include-namespaces web-dev1,web-dev2。 |
--selector |
配置备份仅包含匹配标签选择器的对象。只接受单一值。示例:--selector app.kubernetes.io/name=ingress-nginx。 |
--ttl |
配置备份保留的时间,单位为小时、分钟和秒。默认值设置为 30 天或 720h0m0s。示例:--ttl 24h0m0s。这将在 24 小时后删除备份。 |
表 14.5:Velero 备份标志
要创建一个每天运行并仅包含 Kubernetes 系统命名空间的定期备份,我们将使用 --include-namespaces 标志创建定期任务:
velero schedule create cluster-ns-daily --schedule="@daily" --include-namespaces ingress-nginx,kube-node-lease,kube-public,kube-system,local-path-storage,velero
由于 Velero 命令使用 CLI 进行所有操作,我们应当从解释你将用来管理备份和恢复操作的常见命令开始。
使用 CLI 管理 Velero
所有 Velero 操作都必须使用 Velero 可执行文件进行。Velero 不提供用于管理备份和恢复的 UI。没有 GUI 来管理备份系统最初可能会有些挑战,但一旦你熟悉了 Velero 管理命令,执行操作就变得非常容易。
Velero 可执行文件接受两个选项:
-
命令
-
标志
命令 是像 backup、restore、install 和 get 这样的操作。大多数初始命令需要第二个命令来完成一个完整的操作。例如,backup 命令需要另一个命令,如 create 或 delete,来形成一个完整的操作。
有两种类型的标志——命令标志和全局标志。全局标志 是可以为任何命令设置的标志,而 命令标志 是特定于执行的命令的标志。
像许多 CLI 工具一样,Velero 为每个命令都提供内置帮助。如果你忘记了一些语法或者想知道某个命令可以使用哪些标志,可以使用 -h 标志来获取帮助:
velero backup create -h
以下是 backup create 命令的简略帮助输出:
Create a backup
Usage:
velero backup create NAME [flags]
Examples:
# Create a backup containing all resources.
velero backup create backup1
# Create a backup including only the nginx namespace.
velero backup create nginx-backup --include-namespaces nginx
# Create a backup excluding the velero and default namespaces.
velero backup create backup2 --exclude-namespaces velero,default
# Create a backup based on a schedule named daily-backup.
velero backup create --from-schedule daily-backup
# View the YAML for a backup that doesn't snapshot volumes, without sending it to the server.
velero backup create backup3 --snapshot-volumes=false -o yaml
# Wait for a backup to complete before returning from the command.
velero backup create backup4 --wait
我们发现 Velero 的帮助系统非常有用;一旦你熟悉了 Velero 的基础知识,你会发现内置帮助提供的信息足以应付大多数命令。
使用常见的 Velero 命令
由于许多读者可能是 Velero 新手,我们希望提供一个快速概述,介绍最常用的命令,让你能更轻松地使用 Velero。
列出 Velero 对象
正如我们提到的,Velero 管理是通过使用 CLI 来驱动的。你可以想象,随着你创建更多的备份任务,记住已经创建了哪些内容可能变得困难。这时,get 命令就非常有用。
CLI 可以检索或获取以下 Velero 对象的列表:
-
备份位置
-
备份
-
插件
-
恢复
-
调度
-
快照位置
正如你所料,执行 velero get <object> 会返回 Velero 管理的对象列表:
velero get backups
这是输出结果:
NAME STATUS ERRORS WARNINGS
initial-backup Completed 0 0
每个 get 命令都会生成类似的输出,包含每个对象的名称以及对象的任何唯一值。这个命令对于快速查看现有的对象非常有用,但通常在执行下一个命令 describe 之前使用。
获取 Velero 对象的详细信息
获取你想查看详细信息的对象名称后,你可以使用 describe 命令来获取该对象的详细信息。使用上一节中的 get 命令的输出,我们想要查看 initial-backup 任务的详细信息:
velero describe backup initial-backup
命令的输出提供了请求对象的所有详细信息。你会发现自己使用 describe 命令来排查问题,例如备份失败。
创建和删除对象
由于我们已经使用了几次 create 命令,我们将在本节中重点介绍 delete 命令。
总结一下,create 命令允许你创建将由 Velero 管理的对象,包括备份、调度、恢复以及备份和快照的位置。我们已经创建了一个备份和一个调度,接下来的部分我们将创建一个恢复。
一旦创建了一个对象,你可能会发现需要删除它。要删除 Velero 中的对象,你可以使用 delete 命令,并指定你要删除的对象和名称。
由于我们在 KinD 集群中没有名为 sales 的备份,因此示例命令将无法找到名为 sales 的备份。
在我们的 get backups 输出示例中,我们有一个名为 sales 的备份。要删除该备份,我们将执行以下 delete 命令:
velero delete backup sales
由于删除是单向操作,你需要确认是否要删除该对象。确认删除后,Velero 可能需要几分钟才能将对象从系统中移除,因为它会等待所有相关数据被删除:
Are you sure you want to continue (Y/N)? y
Request to delete backup "sales" submitted successfully.
The backup will be fully deleted after all associated data (disk snapshots, backup files, restores) are removed.
正如你在输出中看到的,当我们删除一个备份时,Velero 会删除该备份的所有对象,包括快照的备份文件和恢复文件。
还有其他可以使用的命令,但本节中涵盖的命令是你真正需要熟悉 Velero 的命令。供参考,以下是常见的 Velero 命令及其简要描述:
安装和卸载 Velero:
velero install:将 Velero 服务器组件安装到 Kubernetes 集群中。
管理备份:
-
velero backup create <NAME>:使用指定的名称创建备份。 -
velero backup describe <NAME>:描述特定备份的详细信息。 -
velero backup delete <NAME>:删除指定的备份。 -
velero backup logs <NAME>:显示特定备份的日志。 -
velero backup download <NAME>:下载备份日志以供故障排除使用。 -
velero backup get:列出所有备份。
管理恢复:
-
velero restore create --from-backup <BACKUP_NAME>:从指定的备份创建恢复。 -
velero restore describe <NAME>:描述特定恢复的详细信息。 -
velero restore delete <NAME>:删除指定的恢复。 -
velero restore logs <NAME>:显示特定恢复的日志。 -
velero restore get:列出所有恢复。
调度备份:
-
velero schedule create <NAME> --schedule <CRON_SCHEDULE>:使用cron语法创建一个定时备份。 -
velero schedule describe <NAME>:描述特定调度的详细信息。 -
velero schedule delete <NAME>:删除指定的调度。 -
velero schedule get:列出所有调度。
管理插件:
-
velero plugin add <PLUGIN_IMAGE>:将插件添加到 Velero 服务器。 -
velero plugin get:列出所有插件。
快照位置:
-
velero snapshot-location create <NAME>:使用指定的名称创建一个新的快照位置。 -
velero snapshot-location get:列出所有快照位置。 -
velero snapshot-location describe <NAME>: 描述指定快照位置的详细信息。 -
velero snapshot-location delete <NAME>: 删除指定的快照位置。
备份位置:
-
velero backup-location create <NAME>: 创建一个指定名称的新备份位置。 -
velero backup-location get: 列出所有备份位置。 -
velero backup-location describe <NAME>: 描述指定备份位置的详细信息。 -
velero backup-location delete <NAME>: 删除指定的备份位置。
管理 restic 仓库:
-
velero restic repo get: 列出所有restic仓库。 -
velero restic repo describe <NAME>: 描述指定 restic 仓库的详细信息。 -
velero restic repo forget <NAME>: 手动删除 restic 备份快照。 -
velero restic repo prune <NAME>: 从 restic 仓库中移除未使用的数据,以释放空间。 -
velero restic repo garbage-collect <NAME>: 对指定的仓库执行垃圾回收操作。
工具命令:
-
velero version: 显示当前 Velero 版本。 -
velero client config set: 配置 Velero 客户端的默认设置。 -
velero client config get: 显示当前的客户端配置。 -
velero completion <SHELL>: 为指定的 shell 生成 shell 自动完成脚本,增强 CLI 的可用性。
现在你可以创建和安排备份,并且知道如何在 Velero 中使用帮助系统,我们可以继续使用备份来恢复对象。
从备份中恢复
在本节中,我们将解释如何使用 Velero 从备份中恢复数据。拥有备份就像拥有汽车保险或房主保险一样——它很重要,尽管你希望永远不需要用到它。当意外发生时,你会感激它的存在。在数据备份领域,发现自己没有备份却需要恢复数据,常常是我们所说的“简历加分事件”。要从备份中恢复数据,你需要使用带有 --from-backup <备份名称> 标签的 create restore 命令。
在本章早些时候,我们创建了一个名为 initial-backup 的单次备份,其中包含集群中的所有命名空间和对象。如果我们决定需要恢复该备份,我们将使用 Velero CLI 执行恢复操作:
velero restore create --from-backup initial-backup
restore 命令的输出可能看起来有些奇怪:
Restore request "initial-backup-20231207163306" submitted successfully.
Run `velero restore describe initial-backup-20231207163306` or `velero restore logs initial-backup-20231207163306` for more details.
起初,可能看起来像是提交了一个备份请求,因为 Velero 回复 "initial-backup-20231207163306" 提交成功,但你可能会疑惑为什么恢复任务不是叫做 initial-backup。Velero 使用备份名称来创建恢复请求,既然我们将备份命名为 initial-backup,恢复任务的名称会使用该名称并附加恢复请求的日期和时间。
你可以使用 describe 命令查看恢复状态:
velero restore describe initial-backup-20211001002927
根据还原的大小,恢复整个备份可能需要一些时间。在还原阶段,备份的状态将是InProgress。一旦还原完成,状态将更改为Completed。
还原示范
了解了所有理论后,让我们通过两个示例来看 Velero 还原的实际操作。对于这些示例,我们将从一个简单的部署开始,该部署具有一个持久卷,我们将在同一集群中删除并还原它。第二个示例会更复杂一些;我们将在主 KinD 集群中备份几个命名空间,并将它们还原到新的 KinD 集群中。
从备份中还原部署
在本章的备份部分,我们在创建了一个附加了 PVC 的busybox部署后进行了集群备份。在备份之前,我们向 PVC 添加了数据,现在我们想确保备份完整,并且还原命名空间成功。
为了测试还原,我们将通过删除demo命名空间来模拟故障,然后使用我们的备份还原整个命名空间,包括 PVC 数据。
模拟故障
为了模拟需要备份命名空间的事件,我们将使用kubectl删除整个命名空间:
kubectl delete ns demo
删除命名空间中的对象可能需要一分钟时间。一旦返回到提示符,删除应该已经完成。
在继续之前,请确认命名空间已被删除。运行kubectl get ns并确认demo命名空间不再列出。
确认demo命名空间已被删除后,我们将演示如何从备份中恢复整个命名空间及其对象。
还原命名空间
想象一下这是一个现实场景。你接到一个电话,开发人员不小心删除了他们命名空间中的所有对象,而且他们没有源文件。
当然,你已经为这种事件做好了准备。你的集群中正在运行多个备份作业,你告诉开发人员,你可以从备份中将其恢复到昨晚的状态。
我们只想还原demo命名空间,而不是整个集群。我们知道备份的名称是initial-backup,因此在执行还原时需要使用它作为备份文件。为了将还原限制在一个命名空间,我们将在命令中添加--include-namespaces demo标志。
velero restore create --from-backup initial-backup --include-namespaces demo
Restore request "initial-backup-20231207164723" submitted successfully.
Run `velero restore describe initial-backup-20231207164723` or `velero restore logs initial-backup-20231207164723` for more details.
这将从initial-backup开始还原。因为它是单个命名空间,且 PVC 只有几个空文件需要还原,所以还原应该不会花太多时间。
首先,检查命名空间是否已被重新创建。如果你执行kubectl get ns demo,你应该能在列表中看到demo命名空间:
kubectl get ns demo
NAME STATUS AGE
demo Active 2m47s
很好!这是第一步。现在,让我们确保 Pods 已经被还原。我们需要名称来查看 PVC 的内容:
kubectl get pods -n demo
NAME READY STATUS RESTARTS AGE
busybox-pvc-f7bfbcc44-vfsnf 1/1 Running 0 4m
到目前为止看起来不错。最后,让我们使用kubectl exec查看 Pod 中的/mnt目录。我们希望看到在备份之前创建的新文件:
kubectl exec -it busybox-pvc-f7bfbcc44-vfsnf -n demo -- ls /mnt -la
Defaulted container "busybox-pvc" out of: busybox-pvc, restore-wait (init)
total 12
drwxrwxrwx 3 root root 4096 Dec 7 16:47 .
drwxr-xr-x 1 root root 4096 Dec 7 16:47 ..
drwxr-xr-x 2 root root 4096 Dec 7 16:47 .velero
-rw-r--r-- 1 root root 0 Dec 7 15:48 newfile1
-rw-r--r-- 1 root root 0 Dec 7 15:48 newfile2
-rw-r--r-- 1 root root 0 Dec 7 16:47 original-data
从ls命令的输出中,我们可以看到我们在执行备份之前添加的两个文件,newfile1和newfile2,已经存在于 pod 中,证明备份可以恢复命名空间,包括任何持久化数据。
恭喜你!你刚刚为开发人员节省了大量工作,因为你有一个命名空间的备份!
像之前的示例一样恢复对象是一个常见的操作,备份和恢复同一集群中的数据是某些操作员可能认为备份唯一用途的事情。虽然这可能是备份的最常见使用场景,但备份也可以用于许多其他活动,比如将一个集群的备份用到另一个不同的集群中。
在下一部分,我们将使用来自一个集群的备份,并将数据恢复到另一个集群中。这对于一些场景是有益的,包括将应用程序从一个集群迁移到另一个集群,或将应用程序和数据恢复到开发集群中进行升级测试。
使用备份在新集群中创建工作负载
恢复集群中的对象只是 Velero 的一个使用案例。虽然这是大多数用户的主要使用场景,但你也可以使用备份文件在另一个集群上恢复工作负载或所有工作负载。如果你需要创建一个新的开发或灾难恢复集群,这是一个有用的选项。
请记住,Velero 备份作业仅包括命名空间及命名空间中的对象。要将备份恢复到新集群中,你必须先有一个正在运行 Velero 的集群,才能恢复任何工作负载。
备份集群
到本章这一部分时,我们假设你已经多次看到过这个过程,并且知道如何使用 Velero CLI。如果你需要复习,可以返回本章的前几页进行参考,或者使用 CLI 的帮助功能。
在这个示例中,我们将不处理任何数据。相反,我们只是想演示如何将一个集群的备份恢复到另一个集群。
首先,我们应该创建一些命名空间,并为每个命名空间添加一些部署,以使其更加有趣。我们在chapter14文件夹中包含了一个名为create-backup-objects.yaml的脚本,它将为你创建命名空间和对象。运行该脚本以创建命名空间和部署。
一旦命名空间和部署创建完成,我们来创建一个新的备份,命名为namespace-demo,它将仅备份我们通过脚本创建的四个新命名空间:
velero backup create namespace-demo --include-namespaces=demo1,demo2,demo3,demo4
在继续之前,请确认备份已成功完成。你可以通过执行describe命令来验证namespace-demo备份:
velero backup describe namespace-demo
在输出中,你会看到备份包含了四个命名空间,并且备份中有 40 个对象。下面展示了一个简化的输出。
Phase: Completed
Namespaces:
Included: demo1, demo2, demo3, demo4
Excluded: <none>
Resources:
Included: *
Excluded: <none>
Cluster-scoped: auto
Label selector: <none>
Or label selector: <none>
Storage Location: default
Started: 2023-12-07 16:58:02 +0000 UTC
Completed: 2023-12-07 16:58:11 +0000 UTC
Expiration: 2024-01-06 16:58:02 +0000 UTC
Total items to be backed up: 36
Items backed up: 36
Velero-Native Snapshots: <none included>
Included: demo1, demo2, demo3, demo4
Excluded: <none>
Started: 2021-10-01 00:44:30 +0000 UTC
Completed: 2021-10-01 00:44:42 +0000 UTC
Expiration: 2021-10-31 00:44:30 +0000 UTC
Total items to be backed up: 40
Items backed up: 40
你现在有了一个新的备份,包含四个新命名空间及其对象。现在,利用这个备份,我们将把四个命名空间恢复到一个新集群中。
首先,我们需要部署一个新的 KinD 集群,它将用于恢复我们的demo1、demo2、demo3和demo4命名空间。
创建一个新集群
由于我们只是演示如何使用 Velero 从备份创建新集群中的工作负载,因此我们将创建一个简单的单节点 KinD 集群作为恢复点。
这一部分有点复杂,因为你的kubeconfig文件中将包含两个集群。对于切换配置上下文的新手,请小心遵循步骤。
一旦完成这个操作,我们将删除第二个集群,因为我们不需要两个集群。这个操作将是互动式的,你需要执行每个步骤:
-
创建一个新的 KinD 集群,命名为
velero-restore:kind create cluster --name velero-restore
这将创建一个新的单节点集群,该集群包含控制平面和工作节点,并将你的集群上下文设置为新集群。
-
一旦集群部署完成,验证你的上下文是否已切换到
velero-restore集群:kubectl config get-contexts
输出如下:
CURRENT NAME CLUSTER AUTHINFO
kind-cluster01 kind-cluster01 kind-cluster01
* kind-velero-restore kind-velero-restore kind-velero-restore
-
验证当前上下文是否设置为
kind-velero-restore集群。你将在当前使用的集群字段中看到一个*标记。 -
最后,使用
kubectl验证集群中的命名空间。你应该只看到新集群中包含的默认命名空间:NAME STATUS AGE default Active 4m51s kube-node-lease Active 4m54s kube-public Active 4m54s kube-system Active 4m54s local-path-storage Active 4m43s
现在我们已经创建了一个新集群,可以开始恢复工作负载的过程。第一步是安装 Velero 到新集群,并指向现有的 S3 桶作为备份位置。
将备份恢复到新集群
在我们的新 KinD 集群启动并运行后,我们需要安装 Velero 以恢复我们的备份。我们可以使用大多数与原始集群相同的清单和设置,但由于我们处于不同的集群中,因此需要将 S3 目标更改为我们用于暴露 MinIO 的外部 URL。
在新集群中安装 Velero
我们已经在chapter14文件夹中有了credentials-velero文件,因此我们可以直接使用velero install命令来安装 Velero。按照这些步骤,你应该在chapter14目录中:
-
确保将
s3Url更改为你之前在本章中创建的原始 KinD 集群的 MinIO Ingress 规则。如果你忘记了 ingress 名称,请将上下文切换到kind-cluster01,并使用kubectl查看velero命名空间中的规则,命令是kubectl get ingress -n velero。这将显示 MinIO 的完整nip.io地址(记得不要使用minio-console规则):velero install --provider aws --plugins velero/velero-plugin-for-aws:v1.2.0 --bucket velero --secret-file ./credentials-velero --use-volume-snapshots=false --backup-location-config region=minio,s3ForcePathStyle="true",s3Url=http://minio.10.2.1.161.nip.io --use-node-agent --default-volumes-to-fs-backup -
安装过程会花费几分钟时间,但一旦 pod 启动并运行,查看日志文件以验证 Velero 服务器是否已启动并运行,并且已连接到 S3 目标:
kubectl logs deployment/velero -n velero -
如果所有设置正确,Velero 的日志将有一条记录,显示它已经在备份位置找到需要与新 Velero 服务器同步的备份(你 KinD 集群中的备份数量可能不同):
time="2021-10-01T23:53:30Z" level=info msg="Found 2 backups in the backup location that do not exist in the cluster and need to be synced" backupLocation=default controller=backup-sync logSource="pkg/controller/backup_sync_controller.go:204" -
确认安装后,使用
velero get backups命令验证 Velero 是否可以看到现有的备份文件:NAME STATUS ERRORS WARNINGS initial-backup Completed 0 0 namespace-demo Completed 0 0
你的备份列表可能与我们的不同,但你应该能够看到与原始集群中相同的列表。
此时,我们可以使用任何备份文件在新集群中创建恢复作业。
在新集群中恢复备份
在本节中,我们将使用上一节中创建的备份,将工作负载恢复到一个全新的 KinD 集群,以模拟工作负载迁移。
我们在添加命名空间和部署后创建的原始集群备份被称为namespace-demo:
-
使用该备份名称,我们可以通过运行
velero create restore命令来恢复命名空间和对象:velero create restore --from-backup=namespace-demo -
在继续进行下一步之前,请等待恢复完成。要验证恢复是否成功,请使用
velero describe restore命令,并指定在执行create restore命令时创建的恢复作业名称。在我们的集群中,恢复作业被命名为namespace-demo-20211001235926:velero restore describe namespace-demo-20211001235926 -
一旦阶段从
InProgress变更为Completed,使用kubectl get ns验证新集群中是否包含额外的演示命名空间:NAME STATUS AGE calico-apiserver Active 23m calico-system Active 24m default Active 24m demo1 Active 15s demo2 Active 15s demo3 Active 15s demo4 Active 15s ingress-nginx Active 24m kube-node-lease Active 24m kube-public Active 24m kube-system Active 24m local-path-storage Active 24m tigera-operator Active 24m velero Active 5m18s -
你将看到新的命名空间已创建,如果你查看每个命名空间中的 Pods,你会看到每个命名空间中都有一个名为
nginx的 Pod。你可以通过kubectl get pods验证 Pods 是否已创建。例如,要验证demo1命名空间中的 Pods,输入以下命令:kubectl get pods -n demo1。
输出如下:
NAME READY STATUS RESTARTS AGE
nginx 1/1 Running 0 3m30s
恭喜!你已经成功地将对象从一个集群恢复到新集群。
删除新集群
由于我们不需要两个集群,接下来我们将删除我们恢复了备份的新 KinD 集群:
-
要删除集群,执行
kind delete cluster命令:kind delete cluster --name velero-restore -
将当前上下文设置为原始 KinD 集群
kind-cluster01:kubectl config use-context kind-cluster01
现在我们已经清理了临时的第二个集群,章节已完成。
总结
备份集群和工作负载是任何企业集群的要求。拥有备份解决方案可以帮助你从灾难或人为错误中恢复。典型的备份解决方案允许你恢复任何 Kubernetes 对象,包括命名空间、持久卷、RBAC、服务和服务账户。你还可以将一个集群中的所有工作负载恢复到完全不同的集群中,以进行测试或故障排除。
在本章中,我们回顾了如何使用etcdctl和快照功能备份etcd集群数据库。我们还详细介绍了如何在集群中安装 Velero 来备份和恢复工作负载。我们通过在新集群中恢复现有备份来完成工作负载的迁移。
下一章,我们将介绍如何监控你的集群和工作负载。
问题
-
对错问题 – Velero 只能使用 S3 目标来存储备份任务。
-
对
-
错
-
-
如果你没有对象存储解决方案,如何使用像 NFS 这样的后端存储解决方案提供 S3 目标?
-
你不能这么做——没有方法将任何东西放在 NFS 前面以呈现 S3。
-
Kubernetes 可以通过原生的 CSI 特性来实现这一点。
-
安装 MinIO 并在部署中使用 NFS 卷作为持久磁盘。
-
你不需要使用对象存储;你可以直接使用 NFS 与 Velero。
-
-
对错问题 – Velero 备份只能在创建备份的同一个集群中恢复。
-
对
-
错
-
-
你可以使用什么工具来创建
etcd备份?-
Velero。
-
MinIO。
-
没有理由备份
etcd数据库。 -
etcdctl。
-
-
哪个命令将创建一个每天凌晨 3 点运行的定期备份?
-
velero create backup daily-backup -
velero create @daily backup daily-backup -
velero create backup daily-backup –schedule="@daily3am" -
velero create schedule daily-backup --schedule="0 3 * * *"
-
答案
-
a
-
a
-
b
-
d
-
d
第十五章:集群与工作负载的监控
到目前为止,在本书中,我们花了相当多的时间来搭建企业 Kubernetes 基础设施的不同方面。搭建完成后,如何知道它是否健康?如何知道它是否在运行?你是否在用户之前就能发现问题,还是直到某人无法访问关键系统时才知道?监控是任何良好运作的基础设施中的一个关键环节,在 Kubernetes 和云原生环境中具有其独特的挑战。本章中,我们将重点关注监控的两个具体方面。首先,我们将使用 Prometheus 项目及其与 Kubernetes 的集成,了解如何检查我们的集群以及需要关注的内容。接下来,我们将使用流行的 ELK 堆栈集中管理日志。在此过程中,我们还将涉及有关安全性和合规性的典型企业讨论,以确保我们符合企业的要求。
本章将涵盖以下主要内容:
-
Kubernetes 中的指标管理
-
Kubernetes 中的日志管理
接下来,让我们回顾一下技术要求。
技术要求
本章的工作量将比前几章更大,因此需要一个更强大的集群。本章有以下技术要求:
-
一台运行 Docker 的 Ubuntu 22.04+ 服务器,至少 8 GB 内存,推荐 16 GB 内存
-
从 GitHub 仓库的
chapter15文件夹中的脚本,你可以通过访问本书的 GitHub 仓库来获取:github.com/PacktPublishing/Kubernetes-An-Enterprise-Guide-Third-Edition
获取帮助
我们尽力测试所有内容,但有时我们的集成实验室中可能有六个以上的系统。由于技术的快速发展,有时在我们的环境中正常运行的东西,在你们的环境中可能无法运行。别担心,我们会提供帮助!在我们的 GitHub 仓库上提交一个问题,github.com/PacktPublishing/Kubernetes-An-Enterprise-Guide-Third-Edition/issues,我们会很乐意帮助你!
Kubernetes 中的指标管理
曾几何时,监控和指标是行业中一个复杂且非常专有的领域。虽然有一些开源项目进行监控,但大多数“企业”系统都庞大、笨重且专有。虽然存在一些标准,如 SNMP,但大多数情况下,每个供应商都有自己的代理、配置,甚至是…一切。如果你想编写一个生成指标或警报的应用程序,那么你需要使用他们的 SDK。这导致了监控成为集中式服务之一,像数据库一样,但需要对被监控内容有更深入的理解。变更困难,最终许多系统采取了你只活一次(YOLO)监控或非常基础的高层次监控,只是“勾选合规框”,但并没有提供太多价值。
随后出现了 Prometheus 项目,它对监控过程进行了两项关键改进,真正改变了我们对监控的处理方式。第一个变化是通过简单的 HTTP 请求来实现所有操作。如果你想监控某个东西,它需要暴露一个提供指标数据的 URL。无论是网站还是数据库都无关紧要。第二个重大影响是,这些指标端点提供了文本格式的数据,使得无论监控系统是什么,都可以轻松动态生成响应。我们稍后会深入探讨这些细节,但这种格式非常强大且灵活,除了 Prometheus 外,还被 SaaS 监控系统所采用。Datadog、AWS CloudWatch 等都支持 Prometheus 的端点和格式,这使得从 Prometheus 开始并转向其他提供的解决方案变得更加容易,而无需更改你的应用程序。
除了开启了监控不同系统的能力,Prometheus 还通过提供 API,使得操作员能够更轻松地与数据交互。现在,常见的可视化工具,如 Grafana,可以在没有供应商专有 UI 的情况下访问这些数据。这些工具基于 Prometheus 的基础功能,扩展了你的监控和警报能力。
现在我们已经解释了为什么 Prometheus 对监控世界产生了如此大的影响,接下来我们将逐步介绍 Kubernetes 集群如何提供指标数据以及如何利用它。
如何 Kubernetes 提供指标
Kubernetes 在 API 服务器上提供了一个 /metrics URI。此 API 需要授权令牌才能访问。要访问这个端点,我们需要创建一个 ServiceAccount、ClusterRole 和 ClusterRoleBinding:
$ kubectl create sa getmetrics
$ kubectl create clusterrole get-metrics --non-resource-url=/metrics --verb=get
$ kubectl create clusterrolebinding get-metrics --clusterrole=get-metrics --serviceaccount=default:getmetrics
$ export TOKEN=$(kubectl create token getmetrics -n default)
$ curl -v --insecure -H "Authorization: Bearer $TOKEN" https://0.0.0.0:6443/metrics
# HELP aggregator_discovery_aggregation_count_total [ALPHA] Counter of number of times discovery was aggregated
.
.
.
这需要一些时间,因为收集的度量指标太多,无法在这里一一列举。我们将在稍后的部分讨论一些具体的度量指标,先了解一下 Prometheus 如何创建和消费这些度量指标。现在要理解的主要观点是,来自集群的所有度量指标都来自一个 URL,并且这些度量指标需要认证。你可以通过让 /metrics 端点对系统开放(未经认证的用户,即所有未认证的请求都被分配给该用户)来禁用此要求,但这会使集群暴露在潜在的提升攻击面前。最好将该资源保持保护。
如果你稍微查看一下这些数据,你会看到数据量是惊人的。我们将首先介绍如何部署 Prometheus,以便你可以更方便地与这些数据进行交互。幸运的是,在 Kubernetes 上部署完整的监控堆栈是相当简单的!
部署 Prometheus 堆栈
到目前为止,我们已经找到了如何访问 Kubernetes 度量指标端点;接下来,我们将使用 Prometheus 社区的 kube-prometheus-stack 项目 (github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack) 来部署 Prometheus。这个图表将 Prometheus 项目与 Grafana 和 Alertmanager 结合,创建了一个几乎完整的监控解决方案。我们将在本章稍后部分讨论一些缺口。首先,让我们部署 Prometheus:
$ cd chapter15/simple
$ ./deploy-prometheus-charts.sh
这个脚本创建了监控命名空间,部署了 Helm 图表,并创建了一个 Ingress 对象,主机为 prometheus.apps.X-X-X-X.nip.io,其中 X-X-X-X 是你的 API 服务器的 IP 地址,但使用破折号代替了点。对于我来说,我的 API 服务器运行在 192.168.2.82,所以,要在浏览器中访问 Prometheus 的 UI,我访问 https://prometheus.apps.192-168-2-82.nip.io/。
现在 Prometheus 已经在运行并且我们可以访问它,接下来的步骤是介绍一些 Prometheus 的功能。
Prometheus 介绍
访问 Prometheus 时你首先会注意到的是没有登录界面。Prometheus 没有安全性的概念。在当前设置下,任何能够访问你的 URL 的人都可以访问你的 Prometheus。我们将在本章稍后处理这个问题,看看如何将 Prometheus 正常化运营。
看到 Prometheus 没有安全性后,接下来要注意的是,主屏幕,称为 Graph 视图,给你提供了一个 Expression 框。在这里,你可以使用 Prometheus 的查询语言 PromQL 查找任何可用的表达式。例如,使用 sum by (namespace) (kube_pod_info) 查询可以列出每个命名空间中的所有 pod:

图 15.1:Prometheus 查询
这个屏幕有一个菜单栏,包括 Alerts(警报)和 Status(状态)菜单选项。如果你点击 Alerts,你会看到几个警报处于红色并触发。这是因为我们正在 KinD 环境中运行,它有一些独特的网络特性。根据 Kubernetes 发行版的不同,你会发现一些警报会定期触发,并且可以忽略。
虽然 Prometheus 已配置为生成警报,但它没有任何通知机制。相反,它依赖于外部系统。通常使用的开源工具是 Alertmanager,但我们将在本章后面讨论它。现在,重要的是要知道,警报在 Prometheus 中定义,你可以从 Alerts(警报)视图查看它们的状态。

图 15.2:警报视图
最后是 Status(状态)菜单,它提供了几个视图。我最常用的是 Targets(目标)视图,它可以告诉你是否有目标不可用。

图 15.3:目标视图
在深入了解度量的收集或查询细节之前,首先让 Prometheus 启动并运行是非常重要的。正如我们第一次查看 Kubernetes 的度量时所见,数据量非常庞大,且其格式不易通过命令行工具进行分析。通过图形界面,我们可以开始查看 Prometheus 如何收集和存储度量数据。
Prometheus 如何收集度量?
到目前为止,我们已经轮询了 Kubernetes 的度量端点,并使 Prometheus 堆栈启动并运行,以查询和分析这些数据。之前,我们创建了一个简单的查询来查找按 Namespace 分类的 pod 数量:sum by (namespace) (kube_pod_info)。查看我们 API 服务器的度量数据输出时,我们可以 grep 搜索 kube_pod_info,但什么也找不到!这是因为这个特定的度量并不是直接来自 API 服务器,而是来自 kube-state-metrics 项目(github.com/kubernetes/kube-state-metrics),该项目与 Prometheus 堆栈图表一起部署。这个工具生成有关 API 服务器的数据,以便集成到 Prometheus 中。如果我们查看它的 /metrics 输出,我们会发现:
# HELP kube_pod_deletion_timestamp Unix deletion timestamp
# TYPE kube_pod_deletion_timestamp gauge
# HELP kube_pod_info [STABLE] Information about pod.
# TYPE kube_pod_info gauge
kube_pod_info{namespace="calico-system",pod="calico-typha-699dc7b758-bgr5f",uid="33ec61b1-bb56-4a4c-853c-6a0ee56023c2",host_ip="172.18.0.2",pod_ip="172.18.0.2",node="cluster01-worker",created_by_kind="ReplicaSet",created_by_name="calico-typha-699dc7b758",priority_class="system-cluster-critical",host_network="true"} 1
带有井号或井号标记 # 的行提供了即将出现的度量的元数据。每个度量的形式为:
metric_name{annotation1="value1",annotation2="value2"} value
每个度量的注释使得 Prometheus 能够索引大量信息并便于查询。查看 kube_pod_info 度量时,我们看到一个 namespace 注释。这意味着我们可以请求 Prometheus 提供所有 kube_pod_info 度量的实例,并按 namespace 注释进行划分。我们也可以请求查看特定 host_ip、node 或任何其他注释下的 pod。
kube_pod_info 指标的类型是 Gauge。Prometheus 中有四种类型的指标:
-
计数器:计数器只能随着时间的推移增加,或者归零。计数器的一个例子是应用程序在其生命周期内响应的请求数量。请求数量只会增加,直到 Pod 终止,此时计数器会归零。
-
仪表:这些指标可以随时间波动。例如,一个 Pod 的开放会话数量就是一个仪表,因为它会随时间变化。
-
直方图:这种类型更复杂。它设计用来跟踪请求类型的范围或桶。例如,如果你想跟踪请求的响应时间,可以为可能的时间创建不同的桶,并增加每个桶的计数。这比为每个请求生成新的指标实例要高效得多。如果我们为每个请求都生成一个指标实例,那么每秒可能会有成千上万的数据点需要被索引和存储,而这些数据对我们来说并不有用。与其使用直方图,我们可以将范围进行分类并跟踪,这样可以节省处理和数据存储的开销。
-
摘要:摘要指标类似于直方图,但由客户端管理。一般来说,你会想使用直方图。
当 Prometheus 收集这些指标时,它们会存储在一个内部数据库中。你会看到,Prometheus 和 Alertmanager 的 Pod 都属于 StatefulSets,而不是 Deployments。这是因为它们会本地存储数据。对于 Prometheus,数据会存储起来,这样你不仅可以看到该指标的最新版本,还能查看过去的指标实例。在 Prometheus 的主 图表 页面,你可以点击任何结果旁边的 图表,查看该结果的时间变化。我们的集群很小,运行的负载也不多,但如果我们突然增加大量新的 pods 呢?这可能会触发警报。另一个保持指标历史记录重要的领域是告警。当我们定义告警规则时,会看到我们可以指定只在某个时间段内触发或清除告警。事情总是会发生的;你不希望因每一个丢包就收到警报。追踪这些信息对于数据的价值和准确的告警至关重要。
在这一节中,我们介绍了 Prometheus 如何收集和存储指标。接下来,我们将深入探讨一些你需要关注的 Kubernetes 常见指标。
常见的 Kubernetes 指标
到目前为止,我们讨论了如何使用 Kubernetes 部署 Prometheus 以及 Prometheus 如何拉取指标,但哪些指标才是重要的呢?说 Kubernetes 中有大量的指标可供选择,这绝对是轻描淡写。API 服务器就有 212 个独立的指标,kube-state-metrics 项目有 194 个指标,还有来自 kubelet 和 etcd 的指标。与其专注于具体的指标(这会根据你的项目而有所不同),我更建议你关注我们部署的图表中自带的 Grafana。
要访问 Grafana,请前往https://grafana.apps.X-X-X-X.nip.io/,其中X-X-X-X是你服务器的 IP 地址,只不过用短横线代替了点。例如,如果我的集群位于192.168.2.82,那么我访问的是https://grafana.apps.192-168-2-82.nip.io/。用户名是admin,密码是prom-operator。你可以浏览任何可用的仪表板,并点击编辑它们,查看它们是如何获取数据的。例如,在图 15.4中,我进入了集群中的计算资源,显示按命名空间划分的 CPU 使用情况。
这些仪表板都是我们部署的 Helm 图表的一部分。我们将在本章后面讨论如何创建你自己的仪表板。
在这里,我可以点击菜单并选择编辑选项:

图 15.4:Grafana 计算资源展示集群
打开图形编辑器后,你现在可以查看用于生成数据的 PromQL 表达式:

图 15.5:Grafana 中的编辑屏幕
如果你将这个表达式复制到 Prometheus 的图形界面中,就能看到用于生成图表的原始数据:

图 15.6:带有 Grafana 查询的 Prometheus
如果你仔细查看查询,会发现 Prometheus 版本没有引用集群。这是因为 Grafana 仪表板是基于管理多个集群的思路构建的,而 Prometheus 只配置为检查单个集群。数据没有被标注cluster属性,因此 Prometheus 无法基于此进行查询。我们将在后面的 Grafana 部分深入讨论这个问题。
现在我们知道了在哪里可以找到集群的重要指标示例以及如何测试它们,我们应该花一些时间来学习 Grafana 的查询语言——PromQL。
使用 PromQL 查询 Prometheus
到目前为止,本章的大部分内容集中在部署 Prometheus 和收集数据上。我们已经开始探讨如何查询数据,但尚未深入讨论 Prometheus 查询语言(PromQL)的细节。如果你熟悉其他查询语言,这应该不会看起来太陌生。
从高层次来看,查询语言与数据很相似。你从一个度量指标开始,选择你想应用的注解。例如,查看按命名空间的计算查询,首先,让我们看看当我们从node_namespace_pod_container:container_cpu_usage_seconds_total:sum_irate开始时会发生什么:

图 15.7:CPU 度量
由于我们在查询中没有包含任何注解,所以我们得到了集群中每个 Pod 的 CPU 使用情况。如果我们想查看特定命名空间的 CPU 使用情况,我们可以像在度量数据中指定的那样,添加{annotation="value"}到我们的度量中。要查看monitoring命名空间中所有容器的 CPU 使用情况,只需在查询中添加{namespace="monitoring"}:

图 15.8:监控命名空间中的 CPU 使用情况
一旦你限定了想要的数据,你可能希望对数据进行聚合。目前的细节显示的是monitoring命名空间中所有运行中的容器,但这并不能让你清楚地了解总共使用了多少 CPU。你可以添加一些聚合函数,比如sum函数:

图 15.9:监控命名空间中所有 CPU 使用情况的总和
最后,你可能希望根据特定的注解来进行sum,比如 Pod,因为大多数 Pod 中有多个容器。你可以使用by关键字进行分组:

图 15.10:监控命名空间中 Pod 的 CPU 使用情况
除了函数之外,你还可以执行数学运算。假设你想知道集群中总共使用了多少 CPU 百分比。你需要知道在任何时刻的 CPU 利用率,以及可用的总 CPU 数量。我们已经知道如何获取集群中所有容器使用的总 CPU。接下来,我们需要知道整个集群中可用的总 CPU。然后,我们需要做一些数学运算来得到百分比。在使用 PromQL 进行数学运算时,你会使用大多数其他编程和查询语言中的典型中缀表示法。例如,查询(sum(node_namespace_pod_container:container_cpu_usage_seconds_total:sum_irate) / max(count without(cpu,mode,pod) (node_cpu_seconds_total{mode="idle"}))) * 100将多个度量和计算结合起来,以获取整个集群的 CPU 利用率:

图 15.11:整个集群的 CPU 使用百分比
现在,我们已经知道了集群中使用的 CPU 的数量。我们可以通过创建一个警报,将此信息纳入我们的容量规划中,当集群达到某个容量水平时,这个警报就会告诉我们。这引出了我们下一节的内容,专注于这个问题。
使用 Alertmanager 进行警报
到目前为止,我们已经部署了 Prometheus,并将其与我们的 Kubernetes 集群集成,学会了如何查询数据库以获取关于集群的有用信息。我们已经看到 Prometheus 在 UI 的 警报 屏幕上跟踪警报,但集群操作员如何知道有问题呢?
Alertmanager 项目(prometheus.io/docs/alerting/latest/alertmanager/)是一个通用工具,它知道如何查询警报并将其发送给正确的人。它不仅仅是一个通知通道;它还帮助去重和分组。最后,它提供了一个界面,用于对不需要继续触发的警报进行静音处理。
我们之前部署的 Helm 图表中包括了一个 Alertmanager 实例以及一个用于它的 Ingress。与其他项目一样,你可以通过 https://alertmanager.apps.X-X-X-X.nip.io/ 访问它,其中的 X-X-X-X 需要替换为你集群的 IP 地址。由于我的集群在 192.168.2.82 上,所以我的 URL 是 https://alertmanager.apps.192-168-2-82.nip.io/。

图 15.12:Alertmanager 用户界面
与 Prometheus 类似,你会注意到这里没有身份验证,因为就像 Prometheus 一样,这里没有安全模型。关于这一点,章节后面会有更多信息。你会看到的是,已经有警报!这是因为在 KinD 上运行 Kubernetes 会导致一些意料之外的网络问题。如果你在云托管的 Kubernetes 上运行 Prometheus 堆栈,你会发现类似的结果。
你会注意到有多个警报组。Alertmanager 提供了警报标签功能,以便你更好地组织它们。例如,你可能只想发送关键问题的警报,或者根据警报来源来路由警报。
你也可以从这个用户界面静音一个警报。这在你处理问题时,或者你知道问题存在且是另一个团队需要处理的问题时非常有用,这样你就不需要在他们解决问题时持续接收警报。由于缺乏安全性,许多团队无法直接访问此用户界面,但我们将在本章后面讲解如何解决这个问题。
虽然用户界面让你能够查看警报并将其静音,但它不能让你配置警报或配置警报的发送目标。那部分是在自定义资源对象中完成的,我们将在下一节中讲解。
你怎么知道某些东西坏了?
到目前为止,我们已经了解了如何访问 Alertmanager UI,并且看到警报是在 Prometheus 中配置的,但我们还没有配置警报。配置警报的过程有两个步骤:
-
创建一个
PrometheusRule实例,用来定义在什么条件下应该生成警报。这涉及到创建一个 PromQL 表达式来定义数据,定义希望条件满足的时间长度,最后,如何标记警报。 -
创建一个
AlertmanagerConfig对象,用于将警报分组并路由到接收器。
我们已经拥有了大量的PrometheusRule对象,这得益于我们部署的图表中包含的丰富预配置规则。接下来的问题是如何构建AlertmanagerConfig。这一部分的难点在于,我们需要某个东西来接收警报。我们有很多选择,包括电子邮件、Slack 以及各种通知 SaaS 服务。为了简化操作,让我们部署一个 NGINX 服务器,作为一个 webhook,它可以让我们查看 JSON 负载。我们的提醒设备不会响起,但至少它能让我们大致了解我们看到的内容。在源代码库内部:
$ kubectl apply -f chapter15/alertmanager-webhook/alertmanager-webhook.yaml
这将启动一个 NGINX Pod,在alert-manager-webhook命名空间中运行。现在,让我们配置一个AlertmanagerConfig,将所有关键警报发送到我们的 webhook:
apiVersion: monitoring.coreos.com/v1alpha1
kind: AlertmanagerConfig
metadata:
name: critical-alerts
namespace: kube-system
labels:
alertmanagerConfig: critical
spec:
receivers:
- name: nginx-webhook
webhookConfigs:
- sendResolved: true
url: http://nginx-alerts.alert-manager-webhook.svc/webhook
route:
repeatInterval: 30s
receiver: 'nginx-webhook'
matchers:
- name: severity
matchType: "="
value: critical
groupBy: ['namespace']
groupWait: 30s
groupInterval: 5m
receivers部分告诉 Alertmanager 将所有事件发送到我们的 Web 服务器。route.matchers部分则告诉 Alertmanager 要发送哪些警报。在我们的示例中,我们将发送任何来自kube-system命名空间、severity为critical的警报。当处理AlertmanagerConfig对象时,对象创建所在的命名空间会自动添加到你的 matchers 中。你可以从chapter15/alertmanager-webhook/critical-alerts.yaml创建这个对象。创建后,稍等几分钟。最终会有一个来自 Prometheus 的警报被触发,并会产生如下日志条目:
0.240.189.139 - - [12/Jan/2024:22:15:22 +0000] "POST /webhook HTTP/1.1" body:"{\x22receiver\x22:\x22kube-system/critical-alerts/nginx-we… " 200 2 "-" "Alertmanager/0.26.0" "-"
日志消息中的 JSON 内容过大,无法在此提供,但它包含了 Alertmanager 可以访问的所有信息。在大多数情况下,你不需要自己编写接收器。已经有很多现成的接收器,几乎不需要自己去构建。
现在我们知道了如何配置 Alertmanager 发送警报,接下来我们将介绍如何设计基于指标的警报。
根据指标向你的团队发送警报
在前面的章节中,我们介绍了如何使用 Alertmanager 将警报发送到接收器。接下来,我们将讲解如何生成一个警报。警报并不是在 Alertmanager 中配置的,而是在 Prometheus 中配置的。Alertmanager 的唯一任务是将生成的警报转发到接收器。是否触发警报的判断由 Prometheus 来决定。
PrometheusRule 对象用于配置 Prometheus 触发警报。此对象定义了规则的元数据、规则触发的条件,以及触发规则的频率,以便将警报发送到 Alertmanager。我们部署的 kube-prometheus 项目包含约四十个预构建的规则。这些规则会根据经验不断更新,你不应该自行更新它们。不过,你可以为自己的基础设施构建自定义规则。
为了演示这个过程,让我们将 OpenUnison 部署到我们的集群中:
$ cd chapter15/user-auth
$ ./deploy_openunison_imp_impersonation.sh
我们将在稍后的 监控应用 部分详细介绍这个脚本的作用。现在,你只需要知道这个脚本部署了 OpenUnison,并将其与我们的 kube-prometheus 图表集成,既为我们的应用程序添加了登录功能,又提供了监控内容。
既然我们使用 OpenUnison 为我们的集群提供身份验证服务,如果它出现故障,你肯定希望在用户打电话之前就知道。我们之前作为部署脚本的一部分,部署了以下的 PrometheusRule:
---
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
creationTimestamp: null
labels:
release: prometheus
name: openunison-has-activesessions
spec:
groups:
- name: openunison.rules
rules:
- alert: no-sessions
annotations:
description: Fires when there are no OpenUnison sessions
expr: absent(active_sessions)
for: 1m
labels:
severity: openunison-critical
source: openunison
在我们的 PrometheusRule 中,我们创建了一个包含单个 rule 的 group。该规则创建了一个名为 no-sessions 的 alert,检查是否缺少 active_sessions 指标。这个指标由 OpenUnison 提供,用于跟踪当前打开的会话数。如果我们仅仅使用像 active_sessions < 1 这样的条件,那么该规则不会触发,因为没有 active_sessions 指标。指定 expr 的语言与我们在 Prometheus 中查询数据时使用的 PromQL 语言相同。这意味着你可以在创建 PrometheusRule 对象之前,在 Prometheus Web 应用中测试你的表达式。
让我们通过删除 OpenUnison 中的 metrics Application 对象来触发这个规则:
$ kubectl delete application metrics -n openunison
大约三十秒后,如果我们登录到 Prometheus 并点击 Alerts 链接,我们会看到:

图 15.13:Prometheus 中的待处理警报
截图显示有一个待处理的警报。这是因为在我们的规则中,我们指定了规则条件必须满足至少一分钟。这是一个重要的调整选项,有助于防止误报。根据你监控的内容,你可能会发现有些警报会在短时间内自动被清除。大约过了三十秒后,你会看到警报从待处理状态变为触发状态:

图 15.14:Prometheus 警报触发
现在我们的规则正在触发,我们可以在 Alertmanager 应用中查看到一个警报正在触发:

图 15.15:Alertmanager 中的警报触发
我们目前没有任何方式来收集警报,但如果有的话,我们现在应该会收到 OpenUnison 出现故障的警报!让我们通过重新添加监控应用来解决这个问题:
$ helm upgrade orchestra-login-portal tremolo/orchestra-login-portal -n openunison -f /tmp/openunison-values.yaml
一旦该命令完成,相同的过程将反向进行。第一次当 OpenUnison 响应active_sessions指标时,警报将进入待处理状态。如果一分钟后没有问题,警报将被清除。
你可能会问,为什么我们直接删除了度量应用程序,而不是停止 OpenUnison。部署脚本为我们的基础设施增加了安全性,这使得没有 OpenUnison 的情况下更难访问 Prometheus 和 Alertmanager 应用程序。虽然你可以使用端口转发来访问 Prometheus 和 Alertmanager,但根据集群的部署方式,这可能会比较复杂,所以我们选择了一个更简单的方法。
现在我们知道如何生成警报,如果我们想忽略它会发生什么呢?我们将在下一节中讲解。
静默警报
现在我们知道如何生成警报,如何静默它呢?你可能有很多理由想要静默警报:
-
已知故障:你已经被告知正在进行的工作将导致故障,因此没有必要对警报作出反应。
-
超出你控制范围的故障:你的故障是由你无法控制的系统引起的。例如,如果你的 Active Directory 出现问题,而你无法控制,且由于此问题 OpenUnison 无法进行身份验证,你就不应该收到警报。
-
持续故障:你知道存在问题;警报无需继续触发。
你可以根据警报提供的标签启用静默。当你看到想要静默的警报时,可以点击 Alertmanager 应用程序中的静默按钮:

图 15.16:在 Alertmanager 中从警报创建静默
现在你可以自定义警报,指定谁创建了它,以及它应持续多长时间。这个静默不会作为对象保存在 API 服务器中,所以你无法通过 Kubernetes API 扫描它(虽然这将是一个很棒的功能)。
安全意识强的读者可能会想,攻击者能否创建一个静默来掩盖他们的痕迹?当然可以!比如,你可以在运行比特币挖矿程序时静默 CPU 警告。关于 Prometheus 的安全性,我们将在本章最后讨论将 SSO 添加到监控堆栈时详细介绍。
我们已经完成了监控堆栈的大部分操作部分。下一步是可视化所有收集的数据。接下来,我们将通过 Grafana 来讲解这一部分。
使用 Grafana 可视化数据
到目前为止,我们已经以操作的方式处理了 Prometheus 收集的数据。我们专注于如何根据数据的变化采取行动,这些变化会影响到我们的集群和用户。虽然能够对这些数据做出反应是很好的,但数据量太大,单靠自己无法处理。此时,Grafana 就派上用场了;它为我们提供了基于 Prometheus(以及其他来源)数据构建仪表盘的方法。我们在本章前面已经看过一些开箱即用的图表。接下来,我们将创建自己的图表,并将这些图表集成到我们部署的 kube-prometheus 堆栈中。
创建你自己的图表
图表是数据集和一组可视化规则的组合。图表本身由 JSON 定义。这意味着它可以作为 Kubernetes 对象持久化,并作为我们堆栈的一部分加载,而不是存储在持久化数据库中。该方法的缺点是,你需要先生成那个 JSON。幸运的是,Grafana 的 Web UI 使这一过程变得容易:
-
登录到 Grafana。
-
创建一个新仪表盘:我们为 OpenUnison 的
active_sessions度量指标创建了一个简单的仪表盘。 -
创建仪表盘后,将其导出为 JSON 格式。
-
创建一个
ConfigMap,并使用labelgrafana_dashboard="1"。 -
以下是
chapter15/user-auth/grafana-custom-dashboard.yaml的重要部分:apiVersion: v1 kind: ConfigMap metadata: labels: grafana_dashboard: "1" name: openunison-activesessions-dashboard-configmap namespace: monitoring data: openunison-activesessions-dashboard.json: |- { "annotations": { .
一旦 ConfigMap 创建完成,Grafana 会几乎立即识别到它!
创建了仪表盘后,你可能注意到 Grafana 还有其他功能,比如告警。Grafana 可以用于这一过程,但这超出了 kube-prometheus 项目和本书的范围。
现在你已经熟悉了 kube-prometheus 堆栈的各个组件,下一步是看看你如何使用它来监控运行在集群中的应用程序和系统。
监控应用程序
在本章的前几节中,我们专注于使用 kube-prometheus 堆栈进行监控和告警的操作方面。我们将 OpenUnison 集成到集群中,创建了监控和告警,但并没有详细讲解它是如何工作的。我们将以 OpenUnison 为模型,讲解如何将其他系统集成到你的监控堆栈中。
为什么你应该在应用程序中添加度量指标
在继续讲解如何将度量指标和监控添加到 OpenUnison 之前,我们需要先回答一个问题:为什么要这样做?你的集群不仅仅由 Kubernetes 实现组成。如今大多数集群都有自动化框架、身份验证系统、外部集成、GitOps 框架等。如果这些组件中的任何一个发生故障,对于你的用户来说,你的集群就不可用。从客户管理的角度来看,你希望在他们开始打开告警之前就知道问题所在。
除了你的系统,你还可能依赖外部系统。当这些系统发生故障,并且它们影响到你和你的客户时,你的客户会首先找到你。
这在身份验证领域是非常真实的,如果登录过程没有“完成”,通常会认为是身份验证过程出了问题。我有很多例子可以证明这一现实,但我将重点介绍几个例子,其中下游监控帮助我识别了根本原因,并提前处理了客户的工单。首先,我的许多客户使用 OpenUnison 通过 LDAP 集成 Active Directory。尽管 Active Directory 是一个非常稳定的系统,但网络访问易受问题影响。一个错误的防火墙规则就可能切断访问,添加对 OpenUnison 下游 Active Directory 的监控提供了快速证据,证明登录过程的中断并非 OpenUnison 的问题。
Prometheus 的度量标准格式已经成为云原生世界的事实标准。即使是没有基于 Prometheus 构建的系统,也内建了对它的支持,比如像 Datadog 和 Amazon CloudWatch 这样的商业系统。这意味着大多数你部署的监控系统都支持 Prometheus 度量端点,即使你内部没有使用 Prometheus。对于那些不是基于 Web 的系统,通常也有通过 Prometheus 进行监控的“附加”解决方案,比如数据库。
在讨论了为什么你应该监控你的集群系统,而不仅仅是 Kubernetes 后,让我们一步一步看看我们如何监控 OpenUnison。
向 OpenUnison 添加度量标准
在本章之前,我们重新部署了包含 OpenUnison 实例的监控栈。现在,是时候走一遍这种集成的过程了。如果你还没有这样做,重新部署你的监控栈和 OpenUnison:
$ cd chapter15/user-auth
$ ./deploy_openunison_imp_impersonation.sh
Prometheus 的操作员会查找各种对象来进行监控;我们将重点关注 ServiceMonitor。如果你查看 monitoring 命名空间,你会注意到大约有十几个预定义的 ServiceMonitor 对象。ServiceMonitor 的目的是告诉 Prometheus 根据 Service 对象查找要监控的 Pod。这作为云原生模式是很有意义的,你不希望将你的度量端点硬编码进去。Pod 会重新调度、扩展等等。依赖于 Service 对象可以帮助 Prometheus 以云原生的方式进行扩展。对于 OpenUnison,以下是我们的 ServiceMonitor 对象:
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
labels:
release: prometheus
name: orchestra
namespace: monitoring
spec:
endpoints:
- bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
interval: 30s
port: openunison-secure-orchestra
scheme: https
targetPort: 8443
tlsConfig:
insecureSkipVerify: true
namespaceSelector:
matchNames:
- openunison
selector:
matchLabels:
app: openunison-orchestra
首先需要指出的是,那里有一个名为 release="prometheus" 的 label。这个 label 是 kube-prometheus 用来识别我们的监控的必要条件。Prometheus 不是一个多租户系统,所以可以合理地期望会有多个实例用于不同的用例。要求这个标签确保 ServiceMonitor 对象被正确的 Prometheus 操作员部署所识别。
接下来,我们将指出该端点与 openunison-orchestra 服务在 openunison 命名空间中的匹配情况。我们没有直接命名它,但通过标签将其识别出来。确保不要因标签过于宽泛而导致集成多个 Service 对象是非常重要的。最后,我们包括了 bearerTokenFile 选项,以告诉 Prometheus 在访问 OpenUnison 的指标端点时使用它自己的身份。我们将在下一节中更详细地介绍这一点。
如果我们仅部署了这个对象,Prometheus 操作员会抱怨它无法加载正确的Service对象,因为它没有 RBAC 权限。接下来的步骤是为操作员创建一个 RBAC Role 和 RoleBinding,使其能够查找Services:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: monitoring-list-services
namespace: openunison
rules:
- apiGroups:
- ""
resources:
- endpoints
- pods
- services
verbs:
- get
- list
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: monitoring-list-services
namespace: openunison
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: monitoring-list-services
subjects:
- kind: ServiceAccount
name: prometheus-kube-prometheus-prometheus
namespace: openunison
如果你已经阅读了我们关于 Kubernetes RBAC 的章节,那么这应该显得很直接。我们包括了服务、端点和 pod,因为一旦你检索到一个 Service,你就可以使用 Endpoint 对象找到具有正确 IP 的 pod。每个 /metrics 端点接着是基于 Pod 的 IP 地址而不是 Service 主机进行访问的。这意味着如果你的系统使用主机名进行路由,你需要接受所有主机名上的 /metrics。
一旦你配置了 Prometheus,几分钟后你就会开始看到你的指标。如果它们没有显示出来,有三个地方需要检查:
-
Prometheus 操作员:操作员将显示加载
Service/Endpoint/Pod时是否有任何问题。 -
Prometheus Pod,配置重载容器:Prometheus pod 包含一个侧车容器,用于重新加载配置。接下来请检查此处,看看加载配置时是否出现问题。
-
Prometheus Pod,Prometheus 容器:最后,检查 Prometheus pod 中的 Prometheus 容器,看看是否有 Prometheus 加载指标时出现问题。
在了解如何在 Prometheus 中设置监控后,接下来的问题是为什么以及如何保护你的指标端点。
保护指标端点的访问
在本章中,我们只稍微提到了安全性。这是因为大多数情况下,Prometheus 遵循 SNMP 方法:安全性不是我的问题。这样做有一些合理的原因。如果你正在使用 Prometheus 栈来调试停机问题,你不希望安全性中断这个过程。同时,将所有可以从指标中提取的、可能被攻击者获取的数据公开是危险的。在 2019 年 KubeCon 北美大会的主题演讲中,Ian Coldwater 说过:“攻击者思维是基于图表的”(Ian 引用了 John Lambert:github.com/JohnLaTwC/Shared/blob/master/Defenders%20think%20in%20lists.%20Attackers%20think%20in%20graphs.%20As%20long%20as%20this%20is%20true%2C%20attackers%20win.md),这让我想到了,因为你可以根据指标端点绘制出一个环境的图谱!想一想关于工作负载和分布的所有数据,节点何时何地工作最辛苦,等等。以我们在本章早些时候讨论的 active_sessions 指标为例。只需将该数字随时间映射出来,就可以告诉你什么时候使用量的激增可能不会触发警报,因为它仍在正常范围内。
好消息是,因为 Prometheus 在集群中运行,它拥有自己的身份。这就是为什么我们的 ServiceMonitor 向 Pod 内建的 Kubernetes 身份添加了 bearerTokenFile 选项。OpenUnison 使用 SubjectAccessReview 将此身份与 API 服务器进行验证。这就是为什么,当你查看 OpenUnison 的日志时,你会看到类似下面的内容:
[2024-01-16 03:18:22,254][XNIO-1 task-4] INFO AccessLog - [AuSuccess] - metrics - https://10.240.189.139:8443/metrics - username=system:serviceaccount:monitoring:prometheus-kube-prometheus-prometheus,ou=oauth2,o=Tremolo - 20 / oauth2k8s [10.240.189.165] - [f763bbd1a1c474929d91bfe89a2fd8e5f5b49a1d5]
[2024-01-16 03:18:22,254][XNIO-1 task-4] INFO AccessLog - [AzSuccess] - metrics - https://10.240.189.139:8443/metrics - username=system:serviceaccount:monitoring:prometheus-kube-prometheus-prometheus,ou=oauth2,o=Tremolo - NONE [10.240.189.165] - [f763bbd1a1c474929d91bfe89a2fd8e5f5b49a1d5]
每当 Prometheus 尝试从 OpenUnison 抓取指标时,我们知道它是使用一个绑定到运行中的 Pod 且仍然有效的令牌。当评估提供指标的系统时,请检查它们是否支持某种令牌验证。使用 NetworkPolicies 限制访问也不是一个坏主意,但正如我们之前多次讨论的那样,你会根据 Pod 的身份获得最佳保护。
在回顾如何保护应用程序指标之后,关于 Prometheus 的最后一节将专注于为 kube-prometheus 堆栈添加安全性。
保护你的监控栈访问
kube-prometheus 堆栈是一个由 Prometheus、Alertmanager 和 Grafana 组成的组合,结合了运维工具来自动化堆栈的部署和管理。当我们逐一讨论堆栈中的每个应用程序时,我们指出 Prometheus 和 Alertmanager 都无法识别用户的身份。Grafana 拥有自己的用户模型,但 kube-prometheus 带有硬编码凭证。假定你会通过 kubectl port-forward 指令访问这些工具。这与我们在本书早些时候保护的 Kubernetes 仪表盘情景相似。虽然这些应用程序都没有使用用户的身份与 API 服务器通信,但它们可能被滥用以提供有关环境的广泛知识,因此应该跟踪使用情况。
对于 Prometheus 和 Alertmanager,最简单的方法是在它们前面放置一个身份验证的反向代理,例如一个OAuth2代理。对于本章,我们使用了 OpenUnison,因为它是内建功能,部署时所需的东西较少。
Grafana 更复杂,因为它确实有多个身份验证选项。它还拥有基于团队和角色的用户授权模型。与 kube-prometheus 图表一起发布的 Grafana 是社区版,只支持两种角色:管理员和查看者。虽然 Grafana 开箱即用支持 OpenID Connect,但这会涉及更复杂的 helm 配置。由于我们已经在使用 OpenUnison 的反向代理来对 Prometheus 和 Alertmanager 进行身份验证,因此我们也使用相同的方法来处理 Grafana。用户的身份通过 OpenUnison 向 Grafana 发出的请求中的 HTTP 头注入,所有用户都被视为管理员。然后,在 helm 图表中使用代理身份验证方法配置 Grafana。所以,原本指向我们应用程序的 Ingress 对象,现在只有一个指向 OpenUnison 的 Ingress,它负责对这些应用程序进行身份验证和授权访问:

图 15.17:为 kube-prometheus 添加 SSO
将 kube-prometheus 堆栈集成到 OpenUnison 中的一个好处是,你不需要记住 URLs,因为它们已经作为徽章与仪表盘和令牌一起包含在内:

图 15.18:带有 kube-prometheus “徽章”的 OpenUnison
如果 OpenUnison 出现故障会发生什么?始终拥有一个“紧急情况时打破玻璃”的计划非常重要!你仍然可以通过端口转发访问所有三个应用程序。
本节内容到此结束,介绍了如何使用 Prometheus 监控 Kubernetes。接下来,我们将探讨 Kubernetes 中日志的工作原理及其管理方法。
Kubernetes 中的日志管理
在本书中,进行完一个练习后,我们通常会要求你通过运行类似下面的命令来查看容器的日志:
kubectl logs mypod -n myns
这让我们能够查看日志,但获取日志的过程是怎样的呢?日志存储在哪里,如何管理?如何管理日志的归档?事实证明,这是一个复杂的话题,在刚开始使用 Kubernetes 时,往往会被忽视。本章的其余部分将致力于回答这些问题。首先,让我们讨论 Kubernetes 如何存储日志,然后我们将介绍如何将这些日志拉入集中式系统。
理解容器日志
在运行容器之前,日志记录相对简单。你的应用程序通常有一个库,负责将数据发送到日志中。该库会轮换日志,并且通常会清理旧的日志。多个日志文件用于不同的目的也并不罕见。例如,大多数 web 服务器至少有两个日志文件,一个是访问日志,用于记录谁向 web 服务器发出了请求;另一个是错误日志,用于跟踪任何错误或调试信息。在 2000 年代初期,像 Splunk 这样的公司推出了系统,将你的日志导入时间序列数据库,以便你可以在多个系统中实时查询它们,使得日志管理变得更加轻松。
然后是 Docker 容器的出现,打破了这种模式。容器是自包含的,不用于生成数据。容器鼓励将所有日志数据管道化到标准输出,而不是将日志数据写入某个存储卷,这样可以通过 Docker API 来观看日志,而不需要直接访问存储日志的卷。这个标准在 Kubernetes 中得到了延续,所以作为操作员,我不需要访问日志存储的文件,只需要访问 Kubernetes API。虽然这种方式大大简化了直接访问日志的过程,但也使得日志的管理变得更加复杂。首先,应用程序不再按功能划分日志,因此作为操作员,我需要筛选出我需要的日志部分。此外,日志不再按照应用程序拥有者可以配置的标准进行轮换。最后,如何以满足合规要求的方式归档日志?答案是将日志管道化到一个中央日志管理系统。接下来,我们将介绍 OpenSearch 项目,这是我们选择的日志管理系统,用来说明容器日志管理是如何工作的。
介绍 OpenSearch
有多个日志管理系统。Splunk 通常是最为人知的,但围绕日志管理构建了一个完整的行业。也有多个开源的日志管理系统。最著名的可能是 “ELK” 堆栈,它是由以下组件组成:
-
Elasticsearch:一个用于存储和排序日志的时间序列数据库和索引系统
-
Logstash:一个将日志导入 Elasticsearch 的项目
-
Kibana:一个 Elasticsearch 的仪表盘和 UI
ELK 堆栈并不是唯一的开源日志管理系统。另一个名为 Graylog 的项目也非常受欢迎。不幸的是,这两个项目都将它们对 OpenID Connect 的 SSO 支持隐藏在商业版本中。2021 年,亚马逊将 Elasticsearch 7.0 的 ELK 堆栈分叉到 OpenSearch 项目中。此后,两个项目已经分道扬镳。我们决定在本章中聚焦于 OpenSearch,因为它是完全开源的,我们可以展示它如何通过 OpenID Connect 集成到集群的企业需求中。

图 15.19:OpenSearch 架构
在上述图中,我们可以看到 OpenSearch 部署的主要组件:
-
Masters:这是 OpenSearch 的引擎,负责索引日志数据。
-
Node:节点是与 OpenSearch 交互的服务的集成点。它托管用于查询索引和将日志推送到集群的 API。
-
Kibana:OpenSearch 附带了一个 Kibana 仪表板,用于通过 Web 应用程序与 OpenSearch API 交互。
-
Logstash/Fluent Bit/Fluentd:一个
DaemonSet,用于尾随集群中的日志并将其发送到 OpenSearch。
我们不会深入探讨 OpenSearch 是如何工作的,因为它是一个复杂的系统,值得专门撰写一本书(实际上已经有几本)。我们将深入到足够的程度,了解它如何与我们的企业 Kubernetes 需求相关,如何通过我们的集中式 Active Directory 聚合日志,并通过目录组来管理访问。现在我们已经对 OpenSearch 集群的不同组件有了概述,接下来我们将进行部署。
部署 OpenSearch
我们已经通过脚本自动化了 OpenSearch 的部署。OpenSearch 依赖 Prometheus 操作符的 CRDs,因此我们也需要部署它。我们将从一个全新的集群开始:
$ cd chapter2
$ kind delete cluster -n cluster01
$ ./create-cluster.sh
.
.
.
$ cd ../chapter15/simple
$ ./deploy-prometheus-charts.sh
.
.
.
$ cd ../user-auth/
$ ./deploy_openunison_imp_impersonation.sh
这些脚本:
-
部署一个新的 KinD 集群,并使用 NGINX Ingress 控制器。
-
部署 kube-prometheus 项目,用于 Prometheus、Alertmanager 和 Grafana。
-
部署“Active Directory”和 OpenUnison,并将 SSO 集成到 Prometheus 堆栈应用中。
这是你如果按照本章内容操作时应该到达的地方。接下来,我们将部署 OpenSearch:
$ cd ../opensearch/
$ ./deploy_opensearch.sh
这个脚本执行了几个操作:
-
增加文件限制,以支持 OpenSearch 和 FluentBit 同时打开每个日志并进行尾随处理
-
通过 Helm 部署 OpenSearch 操作符
-
创建 OpenUnison 配置对象,以集成 OpenSearch
-
部署一个 OpenSearch 集群,配置为通过 OpenUnison 使用 OpenID Connect 进行 SSO 认证
-
通过 Helm 部署 Fluent Bit
我们不会花太多时间深入探讨个别配置。鉴于事物变化如此之快,最好直接从 OpenSearch 项目获取个别指令。我们将重点介绍这些组件如何相互关联,如何与集群以及企业安全需求相匹配。既然一切都已经部署好了,我们将演示日志如何从容器进入 OpenSearch,以及如何访问它。
从容器到控制台的追踪日志
在集成到集群中的 OpenSearch 已就绪后,让我们跟踪日志从ingress-nginx容器到控制台的过程。首先要查看的是fluentbit命名空间,您将在其中找到一个名为fluent-bit的DaemonSet。回想一下,DaemonSet是一个会部署到集群中每个节点的 pod。由于我们在 KinD 集群中只有一个节点,因此fluent-bit DaemonSet只有一个 pod。这个 pod 负责扫描节点上的所有日志,并对其进行尾随,类似于您在本地文件系统上查看日志的方式。Fluent Bit 的重要之处在于,除了将日志数据发送到 OpenSearch,它还会添加元数据,这使我们能够轻松地在 OpenSearch 中搜索日志数据。
您可能会问,为什么我们不使用 Logstash,毕竟它是 ELK 栈中的一个重要组件。Logstash 并不是唯一的日志聚合工具,FluentD 和 FluentBit 也是非常流行的从集群中提取日志的工具。Fluentd 比 FluentBit 更重,且具有更多在发送日志到 OpenSearch 之前对日志数据进行转换和解析的能力。FluentBit 则更简洁,体积也小。考虑到我们已经在集群中使用了其他工具,我们选择了 FluentBit,因为它的简便和轻便。
让我们查看Pod的日志,寻找ingress-nginx。
$ k logs fluent-bit-grhvw -n fluentbit | grep nginx
[2024/01/26 01:26:23] [ info] [input:tail:tail.0] inotify_fs_add(): inode=1606570 watch_fd=19 name=/var/log/containers/ingress-nginx-admission-create-k8fxz_ingress-nginx_create-6….log
[2024/01/26 01:26:23] [ info] [input:tail:tail.0] inotify_fs_add(): inode=1606592 watch_fd=20 name=/var/log/containers/ingress-nginx-admission-patch-fhwpx_ingress-nginx_patch-0….log
**[2024/01/26 01:26:23] [ info] [input:tail:tail.0] inotify_fs_add(): inode=1610601 watch_fd=21 name=/var/log/containers/ingress-nginx-controller-977d987f8-4xxvr_ingress-nginx_controller-8….log**
如您所见,FluentBit 找到了节点上的日志。您可能会问 FluentBit pod 是否需要特殊权限才能访问节点上的日志,答案是肯定的!如果我们查看fluent-bit DaemonSet,我们会发现securityContext为空,意味着该 pod 没有任何约束,而volumes则包含hostMount指令,指向在标准 kubeadm 部署中存储日志的位置。这些Pods是特权的,应该通过限制对fluentbit命名空间的访问以及通过使用 GateKeeper 等策略,限制哪些容器可以在fluentbit命名空间中运行,从而受到保护。
一旦将监视器放置在ingress-nginx日志上,这些日志和附加的元数据将被发送到 OpenSearch 节点。正如我们之前所讨论的,OpenSearch 节点托管着 API,并作为主节点的通道,主节点负责管理索引。fluent-bit 的DaemonSet使用 Logstash 协议与 OpenSearch 通信,并通过基本身份验证进行简单的认证。
对于我们的 FluentBit 部署,使用其 ServiceAccount 令牌与 OpenSearch 安全地通信,就像我们配置 Pods 与 Vault 通信一样,将会非常理想,但不幸的是,这个功能在节点或 FluentBit 中都不存在。相反,你应该确保为 Logstash 账户设置一个非常长的密码,并确保按照企业政策定期更换密码。你甚至可以利用一个秘密管理器……
当 OpenSearch 节点拉取数据时,它会将数据发送给主节点进行索引。这是 OpenSearch 魔力的所在,因为所有数据会存储在索引中,并提供给你和你的集群管理员。
既然数据已经存储在 OpenSearch 中,接下来你打算如何访问它?OpenSearch 包含一个 Kibana 仪表盘,用于访问和可视化日志数据。默认实现使用一个管理员用户名和密码,但这对我们来说行不通!日志数据极为敏感,我们希望在访问它时遵循企业安全要求!也就是说,我们需要将 OpenSearch 与 OpenUnison 集成,就像我们集成其余集群管理应用程序一样。幸运的是,OpenSearch 支持 OpenID Connect,这使得与 OpenUnison 的集成变得非常直接!
除了 OpenID Connect,OpenSearch 还支持多种身份验证系统,包括 LDAP。我们可以使用这个 LDAP 功能与我们与 OpenUnison 部署的“Active Directory”进行集成。然而,这种集成有一些重大限制。如果我们的企业决定将身份管理平台从 Active Directory 切换到身份即服务平台,比如 Entra(前身为 Azure AD)或 Okta,那么这个解决方案将不再有效。另外,如果添加了多因素身份验证方案,这种方法也将失效。使用 OpenID Connect 结合像 OpenUnison、Dex 或 KeyCloak 这样的集成工具,将使你的部署更加可管理。
OpenSearch OpenID Connect 实现的有趣之处在于,它与 Kubernetes 仪表盘的工作方式非常相似。捆绑在 OpenSearch 中的 Kibana 可以使用 OpenID Connect 将用户重定向到 OpenUnison 进行身份验证,并且知道如何刷新用户的 id_token 以保持会话开放。身份验证通过后,Kibana 使用用户的令牌与 OpenSearch 节点交互,保持其身份。这意味着,除了配置 Kibana,我们还需要配置 OpenSearch 节点以信任 OpenUnison 的令牌。
要完成此操作,有两个配置点。在 chapter15/opensearch/opensearch-sso.yaml 文件中,你将找到一个 OpenSearch 集群对象,其中包含一个 spec.dashboard.additionalConfig,该配置包含了仪表盘(Kibana)的配置。如果我们仅仅部署了这个,我们可以进行 Kibana 身份验证,但我们无法与 OpenSearch 进行交互,因为 API 调用会失败。
接下来,有一个名为Secret的秘密,叫做opensearch-security-config,它包含一个名为config.yml的键,用于存储 OpenSearch 节点的主要安全配置。在这里,我们告诉 OpenSearch 从哪里获取 OpenUnison 的 OpenID Connect 发现文档,以便 Kibana 发送的id_token可以得到验证。与 Kubernetes 仪表板类似,使用 OpenID Connect 时,API 无法刷新或管理用户的会话。节点仅验证用户的id_token。
我们已经追踪了来自容器日志的数据,并将其存储到 OpenSearch 中,接下来我们将展示如何访问这些数据。接下来,让我们登录 Kibana 查看我们的日志数据!
在 Kibana 中查看日志数据
我们已经花费了相当多的时间描述 OpenSearch 是如何部署的,以及如何从 Kubernetes 将日志数据摄取到 OpenSearch 集群中。接下来,我们将登录到 Kibana,并查看来自ingress-nginx部署的日志。
首先,打开一个 Web 浏览器,输入你的 OpenUnison 部署的 URL。就像之前章节所示,它将是https://k8sou.apps.X-X-X-X.nip.io/,其中X-X-X-X是你的集群 IP 地址,使用破折号代替点。由于我的集群运行在192.168.2.93,因此我将导航到https://k8sou.apps.192-168-2-93.nip.io/。使用用户名mmosley和密码start123登录。现在你应该会看到一个 OpenSearch 徽章:
图 15.20:OpenUnison 与 OpenSearch 的“徽章”
点击 OpenSearch 徽章。你可能会被要求添加数据,但跳过此步骤,我们直接进入查看数据。接下来,点击左上角的三条水平线以打开菜单,滚动到管理,然后点击仪表板管理:

图 15.21:OpenSearch 仪表板管理菜单
接下来,点击左侧的索引模式,然后点击右侧的创建索引模式:

图 15.22:索引模式
在下一个屏幕上,使用logstash-*作为索引模式名称,以加载来自 FluentBit 的所有索引,然后点击下一步。

图 15.23:创建索引模式
在下一个屏幕上,选择@timestamp作为时间字段,然后点击创建索引模式:

图 15.24:索引时间字段
下一屏幕将显示可以搜索的所有字段列表。这些字段由 FluentBit 创建,并提供更便捷的日志搜索。它们包含来自 Kubernetes 的各种元数据,包括命名空间、标签、注解、Pod 名称等。有了我们创建的索引模式,接下来我们将查询日志数据,找出哪些日志来自ingress-nginx。接下来,再次点击左上角的三个横线,显示菜单,在可观察性下点击日志:

图 15.25:日志菜单项
我们还没有创建任何可视化,因此暂时没有内容可见!点击事件浏览器:

图 15.26:日志屏幕
在下一个屏幕上,将索引模式设置为logstash-*,并将时间范围设置为过去 15 小时。最后,点击刷新。

图 15.27:日志浏览器
这将加载大量数据,其中大部分对我们来说没有意义。我们只想要来自ingress-nginx命名空间的数据。所以,我们需要将结果限制为ingress-nginx命名空间。接下来,在PPL旁边粘贴以下内容并点击刷新:
source = logstash-*
| where kubernetes.namespace_name="ingress-nginx"
| fields log
现在,你将看到来自 NGINX 的访问日志:

图 15.28:搜索 NGINX 日志
虽然我们现在能够从一个集中位置搜索日志,但这仅仅是 OpenSearch 能力的冰山一角。正如我们在本章之前所说,仅此主题就有书籍专门介绍,因此我们无法在本章中深入掌握 OpenSearch,但我们已经涵盖了足够的内容,展示了日志如何从容器移动到集中式系统。无论你是部署本地集群(如我们的 KinD 集群),还是基于云的集群,相同的概念都会存在。
总结
日志记录和监控对于能够跟踪集群健康、规划持续维护和容量,并确保维持合规性至关重要。在本章中,我们从监控开始,讲解了 Prometheus 栈,并探索了每个组件及其交互方式。在查看栈之后,我们通过将 OpenUnison 集成到 Prometheus 中,研究了如何监控在我们集群上运行的系统。我们探讨的最后一个 Prometheus 话题是如何将栈集成到我们的企业认证系统中,使用 OpenUnison。
在研究完 Prometheus 后,我们通过部署 OpenSearch 集群来集中处理日志聚合,从而探索了 Kubernetes 中的日志记录。部署后,我们跟踪生成日志的容器,将日志存储到 OpenSearch 的索引中,然后学习如何通过 OpenSearch 的 Kibana 仪表板安全地访问这些日志。
在下一章中,我们将学习服务网格的工作原理,并部署 Istio。
问题
-
Prometheus 的度量标准使用 JSON 进行传输。
-
正确
-
错误
-
-
Alertmanager 可以将警报发送到哪里?
-
Webhook
-
Slack
-
邮件
-
以上所有
-
-
存储 Grafana 仪表板的 ConfigMap 需要什么标签?
-
grafana_dashboard: 1 -
dashboard_type: grafana -
无需任何配置
-
-
OpenSearch 与 Elasticsearch 兼容。
-
正确
-
错误
-
-
Logstash 是日志管理所必需的。
-
正确
-
错误
-
答案
-
b: 错误:Prometheus 有其自己的度量标准格式。
-
d: Alertmanager 可以将通知发送到所有这些系统以及更多系统。
-
a: Grafana 会查找集群中所有带有
grafana_dashboard: 1 的 ConfigMap,以加载仪表板。 -
b: 错误:OpenSearch 是从 Elasticsearch 7.0 分支出来的;这两个系统此后已经发生了分歧。
-
b: 错误:Logstash 不是必需的;像 FluentD 和 FluentBit 这样的系统也与 OpenSearch 和 Elasticsearch 兼容。
加入我们书籍的 Discord 空间
加入书籍的 Discord 工作区,参加每月的 Ask Me Anything 会话,与作者互动:

第十六章:Istio 简介
“如果前端用户使用起来很容易,那可能意味着后端很复杂。”
本章将介绍 Istio,它是 Kubernetes 的一个服务网格插件。服务网格是一个工具,用于提升 Kubernetes 集群中微服务的管理、安全性和可视化。它通过处理服务间通信、负载均衡和流量路由,简化了复杂的网络任务,而无需在应用代码中实现这些功能。Istio 还通过加密、认证和授权等功能增强了安全性。它提供了详细的指标和监控,帮助开发者了解他们的服务性能。
Istio 是一个庞大而复杂的系统,它通过提供增强的安全性、发现、可观察性、流量管理等功能,给你的工作负载带来好处——而无需应用开发者为每个任务编写模块或应用。
虽然 Istio 有陡峭的学习曲线,但掌握它能够为开发者提供高级功能,使得复杂的服务网格部署和广泛的功能成为可能,包括以下能力:
-
根据不同需求路由流量
-
安全的服务间通信
-
流量整形
-
电路断路
-
服务可观察性
-
未来:环境网格
开发者可以在最少或无需修改代码的情况下使用这些工具。当某些功能对用户来说很容易使用时,往往意味着背后有很多复杂的工作,Istio 就是一个很好的例子。本章将展示如何设置 Istio 和 Kiali,一个用于监控的工具。我们还将讨论 Istio 的关键功能,这些功能有助于流量管理、增强安全性和揭示工作负载。
要全面解释 Istio,我们需要一本专门介绍其自定义资源及使用方法的书籍。本章及下一章的目标是让你掌握使用 Istio 的基本知识,以便你可以自信地开始使用它。我们不能涵盖每个组件的所有细节,因此建议你访问 Istio 官网 istio.io,以获得更多信息,进一步扩展你在本章所学的内容。
本章将涵盖以下主题:
-
理解控制平面和数据平面
-
为什么你应该关心服务网格?
-
Istio 概念介绍
-
理解 Istio 组件
-
安装 Istio
-
介绍 Istio 资源
-
部署附加组件以提供可观察性
-
将应用部署到服务网格中
-
未来:环境网格
在我们深入本章内容之前,先为你即将学习的内容定个基调。本章旨在介绍如何部署 Istio 以及它提供的主要功能。它提供了了解 Istio 功能和各部分的关键细节。在本章以及下一章中(你将把应用程序添加到服务网格中),你应该能够很好地掌握如何设置和使用基本的 Istio 服务网格。
我们将在本章结束时,展望服务网格的未来,即所谓的环境网格(ambient mesh)。截至本书发布时,环境网格仍处于测试阶段,由于从测试版到正式版的过程中可能会有许多变化,我们将仅概述环境网格发布时将带来的新特性。
作为本章介绍的结尾,这里有一个关于 Kubernetes 的趣味事实:像 Kubernetes 中的许多东西一样,Istio 的名字来源于与海洋相关的事物。在希腊语中,“Istio”意味着“帆”。
技术要求
本章有以下技术要求:
-
按照第一章《Docker 和容器基础》中的步骤安装的 Docker 主机,至少需要 8 GB 的 RAM,推荐 16 GB。
-
按照第二章《使用 KinD 部署 Kubernetes》中的初始脚本配置的 KinD 集群
-
本书 GitHub 仓库中的安装脚本
你可以通过访问本书的 GitHub 仓库来获取本章的代码:github.com/PacktPublishing/Kubernetes-An-Enterprise-Guide-Third-Edition/tree/main/chapter16。
为了使用 Istio 来暴露工作负载,我们将从 KinD 集群中移除 NGINX,这样可以让 Istio 使用主机上的端口 80 和 443。
理解控制平面和数据平面
服务网格框架增强了微服务之间的通信,使这些交互更加安全、快速和可靠。它分为两个主要组件:控制平面和数据平面,每个组件在提供 Kubernetes 中的服务间通信中扮演着特定角色。这两个层次构成了服务网格的整体,了解每个层次的基本概念对于理解 Istio 非常关键。
控制平面
让我们从 Istio 控制平面开始。
Istio 中的控制平面是中央权威,控制并指挥服务网格中各服务之间的通信方式。一个常见的类比是将它比作城市的交通管理,管理道路和交通信号灯,确保交通流畅和有序。它在服务间通信、服务安全以及整个网格的可观察性方面起着重要作用。我们将在理解 Istio 组件部分讨论控制平面中的主要守护进程 istiod 时,详细介绍控制平面所管理的不同功能。
数据平面
第二个组件是数据平面,它是包含一组代理(称为 Envoy 代理)的工作层,这些代理与您的服务一起部署。它们拦截并管理微服务之间的网络流量,而无需您或您的开发人员做任何额外的工作或重新编码。
为了建立我们之前提到的控制平面的类比,可以将数据平面比作城市中的道路。网格中的每个服务都有一条专用的道路,流量通过交通控制器指向该服务——在我们的例子中,交通控制器就是 Istio 控制平面(istiod)。
到目前为止,您可能一直在使用 Kubernetes,而没有服务网格。那么,您可能会想,为什么要关心 Istio 为您的集群带来的功能?在接下来的部分中,我们将介绍 Istio 的特点,以便您能够向开发人员和企业解释为什么服务网格是集群的重要附加组件。
为什么要关心服务网格?
服务网格(如 Istio)提供了多个功能,通常开发人员需要自己开发这些功能,这会迫使他们修改现有的代码。如果没有 Istio,当开发人员需要特定功能时,比如在多种编程语言(如 Java、Python 或 Node.js)编写的服务之间实现安全通信,他们需要为每种语言单独实现必要的代码或库。这会增加代码的复杂性,通常还会导致效率低下,进而引发应用性能问题。
添加安全性,比如加密,仅仅是开发人员在创建应用时可能需要的一个例子。那么其他功能呢,比如控制数据流、测试应用如何应对故障,或者网络延迟如何导致应用行为异常?这些以及许多其他功能,Istio 都包含其中——它允许开发人员专注于他们自己的业务代码,而不是编写额外的代码来控制流量或模拟错误或延迟。
让我们来看一下 Istio 提供的一些优势。
工作负载可观察性
应用的停机时间或变慢可能会影响您组织的声誉,并可能导致收入损失。
您是否曾经在一个有许多活跃服务的应用中,难以找到根本问题?想象一下,通过实时监控服务之间的交互和状态,或者回放过去的事件,快速识别和解决问题,发现几小时或几天前出了什么问题。
管理复杂的应用程序和多个服务可能会让人感到不知所措。Istio 提供的功能使这项任务变得不那么令人生畏。如果开发者能有一种更简单的方法该多好!幸好有了 Istio 及其生态系统,这不仅仅是空想。凭借 Prometheus 用于存储度量数据、Kiali 用于深入洞察以及 Jaeger 用于详细追踪等强大功能,你可以拥有一套强大的故障排除工具。
在本章中,我们将设置所有三个插件,重点使用 Kiali,它让你以前所未有的方式观察服务之间的通信。
流量管理
Istio 为你的工作负载提供了强大的流量管理能力,提供了灵活性,可以采用任何你需要的部署模型,而无需修改网络基础设施。这种控制完全掌握在你和开发者手中。Istio 还包括一些工具,使你能够模拟应用程序可能遇到的各种不可预见的情况,如 HTTP 错误、延迟、超时和重试。
我们认识到,一些读者可能对部署模型的概念还不熟悉。掌握可用的不同类型对于理解和欣赏 Istio 带来的好处至关重要。通过 Istio,开发者可以有效地利用蓝绿部署和金丝雀部署等部署策略。
蓝绿部署
在这种模型中,你将两个版本部署到生产环境,按比例将流量分配到每个版本的应用程序,通常会将少量流量引导到“新”的(绿色)版本。当你验证新部署按预期工作时,可以将所有流量切换到绿色部署,或者将蓝绿部署与金丝雀部署结合使用,直到最终将 100% 的流量切换到新部署。
金丝雀部署
这个术语源自矿业时代,当时矿工会将金丝雀放入矿井中,以验证工作环境是否安全。在部署的情况下,它允许你在将发布版本升级为新版本之前,先部署一个早期的测试版本。实质上,这类似于蓝绿部署,但在金丝雀部署中,你会将非常小比例的流量引导到金丝雀版本的应用程序上。使用少量的流量将最大限度地减少金丝雀部署可能引入的影响。当你越来越确信“金丝雀”版本的应用程序是稳定的时,你会逐步增加流量,直到所有流量都切换到新版本。
在问题发生之前发现问题
我们可以进一步深化部署模型;Istio 还为你提供了开发弹性和测试工具,在部署工作负载之前,帮助你发现问题,而不是等客户或终端用户反馈。
你曾经担心过应用程序如何应对某些未知事件吗?
开发者需要担心他们几乎无法控制的事件,包括:
-
应用程序超时
-
通信延迟
-
HTTP 错误代码
-
重试
Istio 提供了对象来帮助处理这些问题,允许你在迁移到生产环境之前与工作负载创建问题。这使得开发者能够在将应用发布到生产环境之前捕捉并解决应用中的问题,从而提供更好的用户体验。
安全性
在今天的世界中,安全性是我们每个人都应该关注的问题。许多保护工作负载的方法都很复杂,并可能需要很多开发者没有的技能。这正是 Istio 大显身手的地方,它提供了工具,使得安全部署变得简单,并且尽可能减少对开发的影响。
Istio 中第一个也是最受欢迎的安全功能是能够在工作负载之间提供相互传输层安全性(mTLS)。通过使用 mTLS,Istio 不仅为通信提供加密,还提供工作负载身份。当你访问一个证书过期或自签名证书的网站时,浏览器会警告你该站点无法被信任。这是因为浏览器在建立 TLS 连接时会进行服务器认证,通过验证服务器提供的证书是否被浏览器信任。mTLS 不仅验证客户端到服务器的信任,还验证服务器到客户端的信任。这就是“相互”的部分。服务器验证客户端提供的证书是否被信任,客户端也验证服务器的证书。当你首次启动集群并使用为你创建的初始证书时,你就在使用 mTLS。Istio 让这一过程变得更加简单,因为它会使用内置的 sidecar 为你创建所有证书和身份。
你可以将 mTLS 配置为整个网格或单个命名空间的要求(STRICT),或选项(PERMISSIVE)。如果你将选项设置为 STRICT,任何与服务的通信都需要 mTLS,如果请求未能提供身份,则连接将被拒绝。然而,如果你设置 PERMISSIVE 选项,具有身份并请求 mTLS 的流量将会被加密,而任何未提供身份或加密请求的请求仍然会被允许进行通信。
另一个提供的功能将使你能够控制允许哪些通信访问工作负载,类似于防火墙,但实现方式更简单。通过 Istio,你可以决定只允许 HTTP GET 请求,或只允许 HTTP POST 请求,或者两者都允许——且仅限于特定来源。
最后,你可以使用JSON Web 令牌(JWTs)进行初始用户认证,以限制哪些人有权限与工作负载通信。这使得你能够通过只接受来自批准的令牌提供者的 JWT 来保护初始通信尝试。
现在我们已经讨论了一些你可能想要部署 Istio 的原因,让我们来介绍一些 Istio 的概念。
Istio 概念介绍
Istio 的原则可以分为四个主要领域:流量管理、安全性、可观察性和可扩展性。对于这些领域,我们将介绍开发人员可以利用的组件和自定义资源,以便充分利用 Istio 带来的好处。
了解 Istio 组件
类似于标准 Kubernetes 集群,Istio 指代两个独立的平面:控制平面和数据平面。历史上,数据平面包括四个不同的服务,Pilot、Galley、Citadel 和 Mixer——所有这些服务都采用真正的微服务设计。这种设计有多种原因,包括将职责分配给多个团队的灵活性、使用不同编程语言的能力,以及独立扩展每个服务的能力。
自 Istio 首次发布以来,Istio 发展迅速。团队决定,拆分核心服务几乎没有好处,反而让 Istio 变得更复杂。这促使团队重新设计 Istio,并且从 Istio 1.5 开始,Istio 包含了我们将在本节中讨论的组件。
使用 istiod 简化控制平面
就像 Kubernetes 将多个控制器打包成一个可执行文件 kube-controller-manager 一样,Istio 团队决定将控制平面组件打包成一个名为 istiod 的单一守护进程。这个单一守护进程将所有控制平面组件组合成一个单一的 Pod,可以根据需要轻松扩展性能。
单一守护进程的主要优势列在 Istio 的博客中,地址为istio.io/latest/blog/2020/istiod/。总结团队的理由,单一进程提供了:
-
更简便快捷的控制平面安装
-
更简单的配置
-
更容易将虚拟机集成到服务网格中,只需要一个代理和 Istio 的证书
-
更容易进行扩展
-
减少控制平面的启动时间
-
减少所需的整体资源量
控制平面负责控制你的服务网格。它具有创建和管理 Istio 组件所需的多个重要特性,下一节我们将解释 istiod 所提供的特性。
拆解 istiod Pod
移动到单一二进制文件并没有减少 Istio 的功能或特性;它仍然提供了各个独立组件所提供的所有功能,只不过现在它们都集中在一个单一的二进制文件中。每个部分都为服务网格提供了一个关键特性,接下来我们将解释这些特性:
- 服务发现:确保与服务一起部署在同一 Pod 中的 Envoy 代理,能够获取最新的网络位置信息,包括服务网格中服务的 IP 地址和端口。服务发现提供了跨服务网格的高效服务间通信。
服务可能频繁地进行扩展或缩减,pod 可能作为滚动更新或自动扩展活动的一部分被终止或启动。每次变化可能会改变端点。服务发现通过监控服务及其相关 pod 的更新来自动跟踪这些变化。当发生变化时,服务发现组件会更新内部服务端点注册表,并将这些更新推送到 Envoy sidecar。
服务发现对于保持服务网格的响应性和效率至关重要,能够实时动态适应容器化应用环境不断变化的格局。
-
配置分发:处理数据平面 sidecar 代理的流量路由、安全协议和策略执行的配置。配置分发集中管理以前由名为 Galley 的组件执行的功能,包括在服务网格中授权配置变更。
-
证书生命周期管理:管理数字证书的颁发、续期和撤销,这些证书用于通过相互传输层安全(mTLS)保证的安全服务间通信,之前由一个名为 Citadel 的组件处理,该组件提供身份验证和证书管理,确保网格内的所有服务都能信任连接,无需任何额外的组件或配置。mTLS 通过加密服务之间的数据传输,减少了安全威胁,提供了服务网格内通信的机密性和完整性。
-
自动化 Envoy 代理部署:通过在 Kubernetes pod 内自动部署 Envoy sidecar 代理,简化了部署过程。这种无缝集成优化了通过 pod 管理
Egress和Ingress流量的方式,充当了一个无形的中介,监督网络流量。
这个自动化过程确保服务网格中的每个 pod 都接收到自己的 Envoy 代理,提供先进的流量能力,包括路由、负载分配和安全措施。Envoy 代理部署的自动化消除了建立和维护服务网格时的复杂性,使开发者和运维人员能够将精力集中在主要职责上。
- 流量路由与控制:负责创建并共享管理流量到 Envoy 代理的规则,在执行高级网络操作中扮演关键角色,并提供精确控制通常复杂的网络流量流向的方式。
流量路由和控制提供的功能包括:
-
确定流量的路径
-
战略性重试机制
-
故障转移机制以确保持续性
-
为了创建现实的测试环境,引入故障
流量路由和控制提供了多项优势,包括简化网络流量管理、测试网络在特定条件下的稳定性和响应,以及通过模拟中断和应用程序反应来提高工作负载的弹性,以便在生产环境中发生之前解决潜在问题。
-
安全策略执行:使用安全规则确保只有授权用户或服务能够安全地访问并在网络中进行交互。
-
可观察性数据收集:在服务网格中,跟踪系统运行情况并快速识别和解决问题至关重要。这就是可观察性数据收集的作用,通过收集和分析遥测信息(包括指标、日志和来自数据平面的跟踪),增强网格的监控和运营洞察能力。
现在我们已经讨论了 istiod 提供的功能,接下来我们将讨论如何使用 istio-ingressgateway 组件在服务网格中管理传入流量。
了解 istio-ingressgateway
从基础的 istiod pod 转到 Istio 的一个重要组成部分——istio-ingressgateway。这个网关使外部客户端和服务能够访问服务网格,充当进入 Kubernetes 集群的入口点。每个启用 Istio 的集群通常至少配备一个 istio-ingressgateway 实例。然而,Istio 的设计并不限于此;根据具体需求,可以部署多个 ingress 网关来服务不同的目的或处理不同的流量模式。
istio-ingressgateway 提供了两种方式来访问应用程序:
-
标准 Kubernetes Ingress 对象支持
-
Istio 网关和虚拟服务对象
由于我们已经讨论并部署了 NGINX 作为 Ingress 控制器,因此不会再讨论使用 Envoy 作为标准 Ingress 控制器;相反,我们将重点介绍使用网关和虚拟服务来处理传入请求的第二种方法。
使用网关暴露我们的服务比标准的 Ingress 对象提供了更多的灵活性、定制性和安全性。
了解 istio-egressgateway
istio-egressgateway 旨在将流量从 sidecar 定向到单个 pod 或一组 pod,从而集中管理服务网格中的外发(egress)流量。通常,Istio sidecar 同时管理网格内服务的传入和传出流量。虽然 istio-ingressgateway 用于管理传入流量,但实现 istio-egressgateway 也可以对传出流量进行管理。ingressgateway 和 egressgateway 的功能和细节将在 介绍 Istio 资源 部分中进行详细探讨。
现在,让我们深入了解如何在集群中安装 Istio。
安装 Istio
部署 Istio 有多种方法。目前最常见的方法是使用 istioctl 或 Helm,但根据你的组织需求,还可以选择其他选项。你可以选择通过 istioctl 或 Helm 创建清单来使用替代的安装方法。
每种方法的优缺点简要列在表 16.1中:
| 部署方法 | 优点 | 缺点 |
|---|---|---|
istioctl |
配置验证与健康检查不需要特权 Pod,增强了集群安全性多个配置选项 | 每个 Istio 版本都需要新的二进制文件 |
| Istio 操作员 | 配置验证和健康检查不需要为每个 Istio 版本准备多个二进制文件多个配置选项 | 需要在集群中运行特权 Pod |
Manifests(通过 istioctl) |
生成可定制的清单,在使用 kubectl 部署前可以进行调整多个配置选项 |
并未执行所有检查,这可能导致部署错误与使用 istioctl 或 Istio 操作员相比,错误检查和报告功能受限 |
| Helm | Helm 和 Helm Charts 对大多数 Kubernetes 用户都很熟悉利用 Helm 标准,简化了部署管理 | 提供的验证检查是所有部署选项中最少的,执行大多数任务时需要额外工作和复杂度 |
表 16.1:Istio 部署方法
在本章中,我们将重点介绍使用 istioctl 二进制文件进行安装,在接下来的章节中,我们将使用 istioctl 部署 Istio。
下载 Istio
我们提供了一个脚本来部署 Istio,输出安装验证,移除 NGINX Ingress,并将 istio-ingressgateway 暴露为我们 KinD 集群的 Ingress。如果你更喜欢手动使用 istioctl 安装,也提供了手动过程。脚本 install-istio.sh 已经包括在内,供读者在自动化测试时使用,位于 chapter16 目录中。
我们首先需要做的是定义我们想要部署的 Istio 版本。我们可以通过设置环境变量来完成,在我们的示例中,我们希望部署 Istio 1.20.3。首先,确保你在克隆仓库后的 chapter16 目录中,然后执行以下命令:
curl -L https://istio.io/downloadIstio | ISTIO_VERSION=1.20.3 TARGET_ARCH=x86_64 sh -
这将下载安装脚本,并使用我们之前定义的 ISTIO_VERSION 执行该脚本。执行后,你的当前工作目录中会有一个 istio-1.20.3 目录。
最后,由于我们将使用 istio-1.12.3 目录中的可执行文件,因此应将其添加到 path 环境变量中。为了简化操作,建议在设置 path 变量之前,先进入书本仓库中的 chapter16 目录:
export PATH="$PATH:$PWD/istio-1.20.3/bin"
使用配置文件安装 Istio
为了简化 Istio 的部署,团队包含了多个预定义的配置文件。每个配置文件定义了哪些组件被部署以及默认配置。共包含七个配置文件,但大多数部署只使用五个配置文件。
| 配置文件 | 已安装组件 |
|---|---|
| 默认 | istio-ingressgateway 和 istiod |
| 演示 | istio-egressgateway、istio-ingressgateway 和 istiod |
| 精简 | istiod |
| 预览 | istio-ingressgateway 和 istiod |
| 环境 | istiod、CNI 和 Ztunnel 注:在 Istio 1.20.3 版本中,环境网格是一个 Alpha 特性 |
表 16.2:Istio 配置文件
如果没有任何包含的配置文件适合你的部署需求,你可以创建一个定制的部署。由于我们将使用包含的演示配置文件,因此这超出了本章的范围——但是,你可以在 Istio 的网站上阅读更多有关定制配置的信息:istio.io/latest/docs/setup/additional-setup/customize-installation/。
要使用 istioctl 部署使用演示配置文件的 Istio,我们只需执行一条命令:
istioctl manifest install --set profile=demo
安装程序将询问你是否确认使用默认配置文件部署 Istio,该配置文件将部署所有 Istio 组件:
This will install the Istio 1.20.3 "demo" profile (with components: Istio core, Istiod, Ingress gateways, and Egress gateways) into the cluster. Proceed? (y/N)
按 y 键表示同意继续部署。如果你想跳过确认,可以在 istioctl 命令行中添加一个选项 --skip-confirmation,该选项告诉 istioctl 跳过确认。
如果一切顺利,你应该会看到每个组件已安装的确认信息,并显示感谢你安装 Istio 的完成信息。
 Istio core installed
 Istiod installed
 Egress gateways installed
 Ingress gateways installed
 Installation complete Made this installation the default for injection and validation.
istioctl 可执行文件可用于验证安装。要验证安装,你需要一个清单。由于我们使用 istioctl 直接部署 Istio,因此没有清单,因此需要创建一个来检查我们的安装。
istioctl manifest generate --set profile=demo > istio-kind.yaml
然后运行 istioctl verify-install 命令。
istioctl verify-install -f istio-kind.yaml
这将验证每个组件,并在验证完成后,提供类似于下面输出的摘要:
Checked 15 custom resource definitions
Checked 3 Istio Deployments
✔ Istio is installed and verified successfully
现在我们已经验证了安装,让我们看看 istioctl 创建了什么:
-
一个名为
istio-system的新命名空间。 -
创建了三个部署,并为每个部署创建了相应的服务:
-
istio-ingressgateway -
istio-egressgateway -
istiod
-
-
15 个 自定义资源定义 (CRDs) 用于提供 Istio 资源,包括:
-
destinationrules.networking.istio.io -
envoyfilters.networking.istio.io -
gateways.networking.istio.io -
istiooperators.install.istio.io -
peerauthentications.security.istio.io -
proxyconfigs.networking.istio.io -
requestauthentications.security.istio.io -
serviceentries.networking.istio.io -
sidecars.networking.istio.io -
telemetries.telemetry.istio.io -
virtualservices.networking.istio.io -
wasmplugins.extensions.istio.io -
workloadentries.networking.istio.io -
workloadgroups.networking.istio.io
-
在这一阶段,您无需关心自定义资源(CR)的细节。随着我们在本章中的进展,我们将深入探讨最常用资源的具体细节。接下来,在下一章中,我们将讲解如何将应用程序部署到网格中,这将涉及几个已部署的 CR。
对于本章或下一章中未涉及的任何 CR,您可以参考 istio.io 网站上的文档,地址如下:istio.io/latest/docs
在 KinD 集群中暴露 Istio
在部署 Istio 后,我们的下一步是将其暴露给我们的网络,以便我们可以访问我们将构建的应用程序。由于我们是在 KinD 上运行,这可能有些棘手。Docker 会将所有来自端口80(HTTP)和443(HTTPS)的流量转发到我们的 KinD 服务器上的工作节点。工作节点则在端口443和80上运行 NGINX Ingress 控制器来接收这些流量。在实际场景中,我们会使用外部负载均衡器,例如 MetalLB,通过 LoadBalancer 来暴露各个服务。然而,针对我们的实验环境,我们将重点关注简化操作。
当您执行先前的脚本来安装 Istio 时,最后一步运行了一个名为expose_istio.sh的独立脚本,做了两件事。首先,它会删除ingress-nginx命名空间,移除 NGINX 并释放 Docker 主机上的端口80和443。其次,它会修补istio-system命名空间中的istio-ingressgateway部署,使其在工作节点上的端口80和443上运行。
由于该脚本作为安装的一部分执行,因此您无需再次执行它。
现在我们已经在集群中完全部署了 Istio,并了解了 Istio 包含的自定义资源,接下来我们将进入下一部分,解释每个资源及其用例。
介绍 Istio 资源
Istio 的自定义资源为您的集群提供了强大的功能,每个资源都可能成为一个章节的内容。
在本节中,我们将提供足够的细节,以帮助您充分了解每个对象。概述过后,我们将部署一个基础应用程序,该应用程序将在真实的应用示例中演示许多对象。
授权策略
授权策略是可选的;然而,如果您没有创建任何策略,所有请求都将允许访问您的集群工作负载。这可能是一些组织期望的默认行为,但大多数企业应该根据最小所需权限来部署工作负载。这意味着您应仅允许访问应用程序所需的权限——没有更多,也没有更少。最小权限访问常常被组织忽视,因为它增加了访问的复杂性,如果配置不当,可能会拒绝有效请求的访问。虽然这种情况确实存在,但这并不是让系统对所有访问请求敞开大门的合理理由。
Istio 的授权策略提供了针对网格内服务的详细访问管理,允许你根据调用者的身份和权限定义访问权限。它们为开发人员提供了根据拒绝、允许和自定义操作控制工作负载访问的能力。
在深入解释策略之前,我们需要先了解一个名为隐式启用的概念。这意味着当任何授权策略与请求匹配时,Istio 会将默认的“允许所有”策略转换为对任何不匹配该策略的请求的拒绝。
让我们通过一个例子来详细解释这个问题。我们在一个启用了 Istio 的命名空间中运行着一个 NGINX 服务器,并且我们有一个标准,要求拒绝访问端口80。
乍一看,这似乎是一个简单的策略,我们只需创建一个拒绝策略,包含端口80。于是,我们创建了策略并将其部署到集群中——为了验证该策略,我们尝试访问端口80上的网站。我们打开浏览器,正如预期的那样,无法访问该站点。太好了!现在让我们验证是否能访问端口443上的网站。我们更改 URL 以使用端口443访问该站点,结果令我们惊讶的是,它也被拒绝了。
等等,什么!?!? 拒绝策略仅仅拒绝端口80——为什么端口443也被拒绝了?
这就是隐式启用的体现,对于任何刚接触 Istio 的人来说,可能会感到困惑。正如本节开头所讨论的,当创建一个策略并且请求匹配该策略时,Istio 会从允许所有的安全策略转换为拒绝所有的安全策略。即使我们只打算拒绝访问端口80,如果没有为端口443创建允许策略,访问也会被拒绝。
为了满足要求并允许访问端口443上的 NGINX 站点,我们需要创建一个允许策略,允许所有传入端口443的流量。稍后我们将详细解释这一点。
理解策略如何评估操作非常重要,因为配置错误的策略可能无法提供预期的结果。策略评估的高级流程如图 16.1所示。

图 16.1:Istio 策略评估流程
-
如果CUSTOM操作的评估定义了对请求的DENY,则访问将被拒绝,评估过程将停止。
-
接下来,如果DENY策略匹配请求,则该请求被拒绝访问该资源,评估过程停止。
-
如果没有与请求匹配的ALLOW策略,则该请求将被拒绝访问。
-
如果ALLOW策略匹配请求,则授予访问该资源的权限。
除了理解策略的流程,你还需要了解冲突的策略如何执行。如果一个策略有任何冲突的规则,比如既拒绝又允许同一个请求,则会优先评估拒绝策略,请求将被拒绝,因为拒绝策略的优先级高于允许策略。还需要特别注意的是,如果你允许某个特定操作,比如 HTTP GET,那么 GET 请求会被允许,但任何其他操作都会被拒绝,因为它没有被策略允许。
授权策略可能会变得非常复杂。Istio 团队在 Istio 网站上创建了一个页面,提供了多个示例,地址是istio.io/latest/docs/reference/config/security/authorization-policy/。
策略可以被拆解为范围、操作和规则:
-
范围:范围定义了哪些对象会受到策略的强制执行。你可以将策略作用范围限定到整个网格、一个命名空间,或任何 Kubernetes 对象标签,如 Pod。
-
操作:可以定义三种操作之一:CUSTOM、ALLOW 或 DENY——每种操作基于定义的规则拒绝或允许请求。ALLOW 和 DENY 是最常用的操作,但 CUSTOM 操作在需要复杂逻辑的场景中非常有用,当 ALLOW 或 DENY 无法处理时,可以使用外部系统进行附加决策。
-
规则:定义哪些操作会被请求允许或拒绝。规则可以变得非常复杂,允许你根据源和目标、不同的操作、密钥等定义动作。
为了帮助理解流程,让我们看几个示例授权策略,以及当策略被评估时将应用哪些访问控制。
稍后我们将在本章中部署一个更大的应用。如果你想查看本节中的示例策略如何工作,可以使用chapter16/testapp目录中的deploy-testapp.sh脚本来部署一个 NGINX Web 服务器。此脚本将创建所有必要的 Kubernetes 和 Istio 对象,以便在真实集群中测试这些策略。
执行脚本并创建了对象后,通过 curl 测试 NGINX 是否正常工作,访问刚刚创建的nip.io虚拟服务。在我们的服务器上,它创建了testapp.10.3.1.248.nip.io。
curl -v testapp.10.3.1.248.nip.io
这应该会显示 NGINX 欢迎页面。现在,我们可以创建一些示例策略,展示授权策略是如何工作的。每个示例都位于chapter16/testapp目录下,文件名为exampleX-policy.yaml,其中X可以是1、2或3——每个示例都可以使用命令kubectl apply <policyname> -n testapp进行部署。
示例 1:拒绝和允许所有访问
在我们的第一个示例中,我们将创建一个策略,拒绝对命名空间testapp中的资源的所有请求:
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: testapp-policy-deny
namespace: testapp
spec:
{}
部署策略后,再次尝试curl访问nip.io地址。你会发现这次会被拒绝访问,并且 Istio 会返回一个 RBAC 错误:
RBAC: access denied
Istio 启用新策略可能需要几秒钟。如果你没有收到 RBAC: access denied 错误,请等待几秒钟再试。
这是一个非常简单的策略,它没有包含 selector,并且在 spec 部分没有定义任何内容。通过省略 selector 部分,Istio 会将策略应用于命名空间中的所有工作负载,而由于 spec 部分没有任何内容,Istio 将拒绝所有流量。如果我们回顾 图 16.1 中的策略流图,它将沿底部流动,并在圆圈 #3 处进行评估——存在一个 selector 匹配,即命名空间中的 所有 工作负载,并且没有定义 ALLOW 策略。这将导致请求被拒绝。
我们不会部署下一个示例;我们展示它是为了加强上面提供的示例。通过向策略添加一个条目,我们可以将其从拒绝所有请求更改为允许所有请求。
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: testapp-policy-deny
namespace: testapp
spec:
rules:
- {}
当我们向策略定义中添加 rules 部分(使用 {})时,我们创建了一个规则,意味着所有流量。与前面的示例类似,我们没有添加 selector,这意味着策略将应用于命名空间中的所有部署。由于此规则适用于所有工作负载,并且该规则包含所有流量,因此访问将被允许。
你可能已经开始理解为什么我们提到不了解策略在流程中如何评估可能会导致意外的访问结果。这是一个典型的例子,展示了如何通过添加一个条目 rules,将策略从拒绝所有请求更改为允许所有请求。
在继续之前,通过执行以下命令删除策略:
kubectl delete -f example1-policy.yaml -n testapp
示例 2:只允许 GET 方法访问工作负载
策略可以非常细化,只允许某些操作,例如允许 HTTP 请求中的 GET 方法。这个例子将允许 GET 请求,同时拒绝所有其他请求类型,应用于在 marketing 命名空间中标记为 app=nginx-web 的 pod。在这个示例中,我们将使用第一个示例中的相同 NGINX 部署。使用位于 chapter16/testapp 目录下的 exampe2-policy.yaml 清单,通过 kubectl 创建策略:
kubectl create -f example2-policy.yaml -n testapp
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: nginx-get-allow
namespace: marketing
spec:
selector:
matchLabels:
app: nginx-web
action: ALLOW
rules:
- to:
- operation:
methods: ["GET"]
如果你尝试 curl 访问与示例 1 中相同的 nip.io 地址,你将看到 NGINX 欢迎页面。这是使用 HTTP GET 命令进行的。为了证明 HTTP PUT 命令会被阻止,我们可以使用 curl 命令向 NGINX 发送请求:
curl -X PUT -d argument=value -d value1=dummy-data http://testapp.10.3.1.248.nip.io/
这将导致 Istio 拒绝请求,并出现 RBAC 错误:
RBAC: access denied
在这个示例中,策略接受来自任何来源的 GET 请求,因为我们只定义了一个操作而没有指定具体的 from 对象。由于我们没有在策略中添加 PUT 操作,任何尝试发送 HTTP PUT 请求的行为将被策略拒绝。
策略甚至可以更细化,根据请求的来源来接受(或拒绝)请求。在下一个示例中,我们将展示另一个策略示例,但我们将把来源限制为单一的 IP 地址。
在继续之前,使用kubectl删除示例策略:
kubectl delete -f example2-policy.yaml -n testapp
示例 3:允许来自特定来源的请求
在我们最后的策略示例中,我们将限制哪些来源可以使用 GET 或 POST 方法访问工作负载。
通过拒绝任何来自不在策略源列表中的来源的请求,这将增强安全性。我们不会创建这个策略,因为许多读者可能在测试时受限于可用的机器数量。
metadata:
name: nginx-get-allow-source
namespace: marketing
spec:
selector:
matchLabels:
app: nginx
action: ALLOW
rules:
- from:
- source:
ipBlocks:
- 192.168.10.100
与之前的示例不同,这个策略包含一个source:部分,允许你根据不同的来源(如 IP 地址)限制访问。这个策略将允许源 IP 192.168.10.100访问 NGINX 服务器上的所有操作,其他所有来源的访问将被拒绝。
从授权策略转向,我们将介绍下一个自定义资源——目标网关。
网关
之前,我们提到过流量将进入一个中央点istio-ingressgateway。我们没有解释流量如何从ingressgateway流向命名空间和工作负载——这就是网关的作用。
网关可以在命名空间级别进行配置,因此你可以将创建和配置的任务委托给一个团队。它是一个负载均衡器,接收传入和传出的流量,可以通过诸如接受的加密套件、TLS 版本、证书处理等选项进行自定义。
网关与虚拟服务一起工作,我们将在下一节中讨论虚拟服务,但在此之前,以下图示展示了Gateway和VirtualService对象之间的交互。

图 16.2:网关到虚拟服务的通信流
以下列表更详细地解释了图 16.2中所示的通信:
-
一个传入请求被发送到 Istio 的
ingress-gateway控制器,该控制器位于istio-system命名空间中。 -
sales命名空间有一个配置了ingressgateway的网关,主机名为entry.foowidgets.com。这告诉ingressgateway将请求发送到sales命名空间中的网关对象。 -
最终,流量通过一个已使用网关在
sales命名空间中创建的虚拟服务对象路由到服务。
为了展示一个Gateway配置示例,我们有一个启用了 Istio 的名为sales的命名空间,运行着一个可以通过 URLentry.foowidgets.com访问的应用程序,我们需要将其暴露以供外部访问。为此,我们将创建一个网关,使用以下示例清单。(下面的示例仅供讨论;你不需要在你的 KinD 集群上部署它。)
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: sales-gateway
namespace: sales
spec:
selector:
istio: ingressgateway
servers:
- port:
number: 443
name: http
protocol: HTTP
hosts:
- sales.foowidgets.com
tls:
mode: SIMPLE
serverCertificate: /etc/certs/servercert.pem
privateKey: /etc/certs/privatekey.pem
这个网关配置将告诉入口网关在端口443上监听来自sales.foowidgets.com的传入请求。它还定义了将用于保护传入 Web 请求的通信的证书。
你可能会想:“它怎么知道使用我们在集群中运行的入口网关?”如果你查看spec部分,然后查看选择器,我们已经配置了selector,使用带有istio=ingressgateway标签的入口网关。这个selector和标签告诉网关对象哪个入口网关将为我们的新网关创建传入连接。当我们之前部署 Istio 时,入口网关被标记为默认标签istio=ingressgateway,如下所示,来自kubectl get pods --show-labels -n istio-system的输出。
app=istio-ingressgateway,chart=gateways,heritage=Tiller,install.operator.istio.io/owning-resource=unknown,istio.io/rev=default,istio=ingressgateway,operator.istio.io/component=IngressGateways,pod-template-hash=78c9969f6b,release=istio,service.istio.io/canonical-name=istio-ingressgateway,service.istio.io/canonical-revision=latest,sidecar.istio.io/inject=false
你可能会想,既然网关中没有配置选项告诉它将流量引导到哪里,那么网关将如何被用来指引流量到特定的工作负载呢?这是因为网关只是配置了入口网关来接受指向目标 URL 的流量以及所需的端口——它并不控制流量如何流向服务;这是下一个对象,虚拟服务对象的工作。
虚拟服务
网关和虚拟服务结合在一起,为服务或服务提供正确的流量路由。一旦您部署了网关,您需要创建一个虚拟服务对象来告诉网关如何将流量路由到您的服务。
基于网关示例,我们需要告诉网关如何将流量路由到运行在端口443上的 Web 服务器。该服务器已使用 NGINX 在marketing命名空间中部署,并具有app-nginx标签和一个名为frontend的服务。为了将流量路由到 NGINX 服务,我们将部署以下清单。(下面的示例仅供讨论;您无需在 KinD 集群上部署它。)
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: sales-entry-web-vs
namespace: sales
spec:
hosts:
- entry.foowidgets.com
gateways:
- sales-gateway
http:
- route:
- destination:
port:
number: 443
host: entry
分解清单时,我们指定了这个VirtualService对象将路由的主机;在我们的示例中,我们只有一个主机,entry.foowidgets.com。下一个字段定义了将用于流量的网关;在上一节中,我们定义了一个名为marketing-gateway的网关,并配置它监听端口443。
最后,最后一部分定义了流量将路由到哪个服务。路由、目标和端口都很容易理解,但host部分可能会让人误解。该字段实际上定义了您将路由流量到的服务。在这个示例中,我们将流量路由到一个名为entry的服务,因此我们的字段被定义为host: entry。
通过了解如何使用网关和虚拟服务在服务网格中路由流量,我们可以继续讨论下一个主题:目标规则。
目标规则
虚拟服务提供了一种基本的方法来将流量导向服务,但 Istio 提供了一个额外的对象,通过使用 Destination 规则来创建复杂的流量导向。Destination 规则在虚拟服务之后应用。流量最初使用虚拟服务路由,如果定义了 Destination 规则,可以使用该规则将请求路由到最终目的地。
这可能一开始有点混淆,但当你看到一个示例时会变得容易理解,所以让我们通过一个示例来深入了解,它可以将流量路由到不同版本的部署。
如我们所学,传入的请求最初会使用虚拟服务,然后,如果定义了目标规则,它将路由请求到目标。在这个示例中,我们已经创建了一个虚拟服务,但实际上我们有两个版本的应用程序,分别标记为 v1 和 v2,我们想要使用轮询方式在这两个版本之间进行流量导向。为了实现这一点,我们将使用下面的清单创建一个 DestinationRule。(下面的示例仅用于讨论;你不需要在你的 KinD 集群上部署它。)
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: nginx
spec:
host: nginx
trafficPolicy:
loadBalancer:
simple: ROUND_ROBIN
subsets:
- name: v1
labels:
version: nginx-v1
- name: v2
labels:
version: nginx-v2
使用这个示例,传入 NGINX 服务器的请求将平分到应用程序的两个版本中,因为我们将 loadBalancer 策略定义为 ROUND_ROBIN。但如果我们想要将流量路由到连接数最少的版本呢?目标规则对于 loadBalancer 还有其他选项,如果要将连接路由到连接数最少的版本,我们将设置 LEAST_CONN 的 loadBalancer 策略。
接下来,我们将讨论 Istio 提供的一些安全功能,首先是一个名为 Peer Authentication 的对象。
对等认证
Istio 的对等认证对象控制了服务网格如何控制工作负载的互相 TLS 设置,这些设置可以应用于整个服务网格或仅仅是某个命名空间。每个策略都可以配置一个值,这个值要么允许 pod 之间进行加密和非加密通信,要么要求 pod 之间进行加密通信。
| mTLS 模式 | Pod 通信 | 描述 |
|---|---|---|
STRICT |
mTLS 必须 | 任何发送到 Pod 的非加密流量将被拒绝 |
PERMISSIVE |
mTLS 可选 | Pod 会接受加密和非加密流量 |
表 16.3:PeerAuthentication 选项
如果你想为整个服务网格设置 PeerAuthentication,你需要在 istio-system 命名空间中创建一个 PeerAuthentication。例如,要要求所有 pod 之间使用 mTLS,你需要创建如下所示的策略:
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: mtls-policy
namespace: istio-system
spec:
mtls:
mode: STRICT
要允许加密和非加密流量,策略模式只需设置为 PERMISSIVE,通过将模式更改为 mode: PERMISSIVE 即可。
与其为整个服务网格设置模式,许多企业只为需要额外安全性的命名空间将模式设置为 STRICT。在下面的示例中,我们为 sales 命名空间将模式设置为 STRICT。
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: mtls-policy
namespace: sales
spec:
mtls:
mode: STRICT
由于此策略是为 sales 命名空间配置的,而不是为 istio-system 命名空间配置的,Istio 将仅对该命名空间强制执行严格的 mTLS 策略,而不是对整个服务网格执行。
这是网格提供的一个很好的安全功能,但加密并不会阻止请求访问我们的工作负载;它只是对请求进行加密。接下来我们将讨论的对象将通过要求认证才能允许访问,进一步增强工作负载的安全性。
请求认证和授权策略
安全性需要两部分。首先是认证部分,告诉我们“你是谁”。第二部分是授权,即提供认证后,允许的操作或“你可以做什么”。
RequestAuthentication 对象只是确保工作负载安全的一个部分。要完全保障工作负载的安全,你需要创建 RequestAuthentication 对象和 AuthorizationPolicy。RequestAuthentication 策略将决定哪些身份被允许访问工作负载,而 AuthorizationPolicy 策略将决定哪些权限被允许。
如果没有 AuthorizationPolicy,RequestAuthorization 策略可能会无意中允许访问资源。如果你只创建了一个 RequestAuthorization 策略,表 16.4 显示了哪些人会被允许访问。
| 令牌操作 | 提供的访问权限 |
|---|---|
| 提供无效令牌 | 访问将被拒绝 |
| 未提供令牌 | 将允许访问 |
| 提供有效令牌 | 将允许访问 |
表 16.4:RequestAuthentication 访问
正如你所看到的,一旦我们创建了策略,任何无效的 JWT 都会被拒绝访问工作负载,而任何有效的令牌将被允许访问工作负载。然而,当没有提供令牌时,许多人认为访问会被拒绝,但实际上,访问将被允许。RequestAuthentication 策略仅验证令牌,如果没有令牌,RequestAuthentication 规则将不会拒绝请求。
以下是一个示例清单。我们将在本章的示例部分使用这个清单,但在本节中先展示它以解释各个字段。
apiVersion: security.istio.io/v1beta1
kind: RequestAuthentication
metadata:
name: demo-requestauth
namespace: demo
spec:
selector:
matchLabels:
app: frontend
jwtRules:
- issuer: testing@secure.istio.io
jwksUri: https://raw.githubusercontent.com/istio/istio/release-1.11/security/tools/jwt/samples/jwks.json
这个清单将创建一个策略,该策略配置位于 demo 命名空间中、标签为 matching app=frontend 的工作负载,接受来自 testing@secure.istio.io 的 JWT,并且使用 raw.githubusercontent.com/istio/istio/release-1.11/security/tools/jwt/samples/jwks.json 这个 URL 来确认令牌。
此 URL 包含用于验证令牌的密钥:
{ "keys":[ {"e":"AQAB","kid":"DHFbpoIUqrY8t2zpA2qXfCmr5VO5ZEr4RzHU_-envvQ","kty":"RSA","n":"xAE7eB6qugXyCAG3yhh7pkDkT65pHymX-P7KfIupjf59vsdo91bSP9C8H07pSAGQO1MV_xFj9VswgsCg4R6otmg5PV2He95lZdHtOcU5DXIg_pbhLdKXbi66GlVeK6ABZOUW3WYtnNHD-91gVuoeJT_DwtGGcp4ignkgXfkiEm4sw-4sfb4qdt5oLbyVpmW6x9cfa7vs2WTfURiCrBoUqgBo_-4WTiULmmHSGZHOjzwa8WtrtOQGsAFjIbno85jp6MnGGGZPYZbDAa_b3y5u-YpW7ypZrvD8BgtKVjgtQgZhLAGezMt0ua3DRrWnKqTZ0BJ_EyxOGuHJrLsn00fnMQ"}]}
当令牌被提供时,将验证它是否来自 RequestAuthentication 对象中 jwtRules 部分定义的颁发者。
我们将在下一章详细介绍令牌身份验证是如何工作的。
服务条目
一旦工作负载成为服务网格的一部分,它的边车代理将处理所有与网格内服务的出站通信。如果工作负载试图与网格外的外部服务进行通信,而未正确配置,则这种通信可能会失败。幸运的是,Istio 提供了定义和管理外部服务的机制,使工作负载能够与网格外的服务进行通信。其中一种机制是ServiceEntry对象,它允许你定义外部网格的服务,并配置这些服务的访问方式。
如果你有一个要求,要求你的工作负载在服务网格内与服务网格外的服务通信,那么你需要为外部资源在网格中创建一个条目。这可以通过两种方式来完成,第一种方法引出了我们下一个自定义资源——ServiceEntry对象,它允许你将外部条目添加到服务网格中。当你为外部服务创建ServiceEntry时,它将表现得像是实际服务网格的一部分。这允许流量从服务网格内路由到手动指定的服务。如果没有ServiceEntry,任何尝试与外部资源通信的行为都会失败,因为 Istio 会试图在服务网格条目中查找该服务,但无法找到该资源(因为它不是网格的一部分)。
要创建一个ServiceEntry,你需要创建一个新对象,其中包含外部服务的主机和端口。下面的示例将创建一个新条目,将主机api.foowidgets.com(端口80)通过 HTTP 协议添加到服务网格中。
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
name: api-server
namespace: sales
spec:
hosts:
- api.foowidgets.com
ports:
- number: 80
name: http
protocol: HTTP
ServiceEntry是将外部资源显式添加到服务网格中的绝佳资源。我们提到过有两种方法可以将外部资源添加到服务网格中,其中一种是服务条目,另一种是Sidecars对象。选择使用哪种对象非常具体,取决于你自己的使用案例和组织标准。服务条目是非常具体的,你必须为每个需要通信的外部资源创建一个条目。边车则不同,它不是定义什么是服务网格外的资源,而是定义什么在服务网格内。
边车
首先,我们知道这可能会让人感到困惑——这个对象不是边车本身;它是一个允许你定义边车认为“在网格中”的项目的对象。根据集群的大小,你的网格中可能有成千上万的服务,如果你没有创建边车对象,Envoy 边车将假定你的服务需要与所有其他服务通信。
通常,你可能只需要你的命名空间与同一命名空间内的服务或少数其他命名空间的服务进行通信。由于追踪网格中的每个服务需要资源,因此最好创建一个边车对象,以减少每个 Envoy 边车所需的内存。
要创建一个限制 Envoy 代理中服务的边车对象,你需要部署下面显示的清单:
apiVersion: networking.istio.io/v1beta1
kind: Sidecar
metadata:
name: sales-sidecar
namespace: sales
spec:
egress:
- hosts:
- "./*"
- "istio-system/*"
这个清单中的 spec 包含了网格中主机的列表,./* 引用的是创建该对象的命名空间,所有的边车应该包含部署 Istio 的命名空间,默认情况下为 istio-system。
如果我们有三个命名空间需要在网格中进行通信,我们只需将额外的命名空间添加到主机的条目中:
apiVersion: networking.istio.io/v1beta1
kind: Sidecar
metadata:
name: sales-sidecar
namespace: sales
spec:
egress:
- hosts:
- ./*
- istio-system/*
- sales2
- sales3
未能限制网格对象可能导致你的 Envoy 边车发生崩溃循环,这可能是由于资源不足引起的。你可能会遇到 内存不足(OOM)事件,或者只是出现不显示任何根本原因的崩溃循环。如果你遇到这些情况,部署一个边车对象可能会解决问题。
Envoy 过滤器
Envoy 过滤器使你能够创建由 Istio 生成的自定义配置。记住,Pilot(Istiod 的一部分)负责边车的管理。当任何配置发送到 Istio 时,Pilot 会将配置转换为 Envoy 可用的格式。由于你受限于 Istio 自定义资源中的选项,你可能无法拥有某些工作负载所需的所有潜在配置选项,这时候 Envoy 过滤器就派上用场了。
过滤器是非常强大且潜在危险的配置对象。它们允许你自定义标准 Istio 对象无法自定义的值,允许你添加过滤器、监听器、字段等。这让我想起了已故斯坦·李在《蜘蛛侠》中的一句话:“能力越大,责任越大。” Envoy 过滤器为你提供了扩展的配置选项,但如果过滤器被滥用,可能会导致整个服务网格崩溃。
Envoy 过滤器很复杂,对于本书的目的来说,并不是理解 Istio 所需深入了解的主题。你可以在 Istio 网站上阅读更多关于 Envoy 过滤器的内容,网址是 istio.io/latest/docs/reference/config/networking/envoy-filter/。
WASM 插件
与 Envoy 过滤器类似,WasmPlugins 对象用于扩展 Envoy 边车的功能。虽然它们在提供的功能上类似,但它们提供的自定义程度各不相同。
通常,WasmPlugins 被认为更易于开发和实现,使它们比 Envoy 过滤器更简单,也更不危险。然而,这种简化带来了功能的减少,相对于 Envoy 过滤器提供的功能,WasmPlugins 功能较少。
Envoy 过滤器提供了对代理设置的详细、精细控制,使得能够创建比 WasmPlugins 更复杂的操作。这种控制程度增加了它们的复杂性,如果配置或使用不当,可能会带来风险,从而导致服务网格中的中断。
选择 EnvoyFilters 还是 WasmPlugins 最终取决于你的具体需求和偏好。决定采用哪种方案时,重要的是考虑各种因素,权衡易用性、功能性以及对服务网格的潜在影响。
部署插件组件以提供可观察性
到现在为止,你已经知道如何部署 Istio,并理解了一些最常用的对象,但你还没有看到其中一个最有用的功能——可观察性。在本章开始时,我们提到过可观察性是 Istio 提供的我们最喜欢的功能之一,本章将解释如何部署一个叫做 Kiali 的流行 Istio 插件。
安装 Istio 插件
当你部署 Istio 时,你为开发人员提供了一个服务网格及其所有功能。虽然这本身很强大,但你需要添加一些额外的组件才能真正提供一个完整的解决方案。你应该为你的服务网格添加四个插件——虽然有些解决方案有替代方案,但我们使用的是最常用的插件,具体如下:
-
Prometheus
-
Grafana
-
Jaeger
-
Kiali(我们将在下一节中介绍)
我们在前几章中讨论了 Prometheus 和 Grafana,但 Jaeger 是一个我们之前没有提到的新组件。
Jaeger 是一个开源工具,提供 Istio 中服务之间的追踪。追踪对一些读者来说可能是一个新词。从高层次看,追踪是对服务执行路径的表示。它们让我们能够查看服务之间实际的通信路径,提供一个易于理解的视图,并提供关于性能和延迟的度量,帮助你快速解决问题。
为了部署所有插件,我们在 chapter16/add-ons 目录下包含了一个脚本,名为 deploy-add-ons.sh,用来在集群中部署插件。执行该脚本即可部署插件。
许多插件示例部署不维持状态,因此我们在部署中添加了持久性,利用 KinD 内置的配置器,通过为每个部署添加持久磁盘来实现。
脚本执行以下步骤:
-
使用标准 Kubernetes 清单在
istio-system命名空间中部署每个插件。- 每个部署都会创建一个 PVC,并将其挂载为数据位置,以保持重启后的持久性。
-
查找主机的 IP 地址,为每个插件创建新的 Gateway 和 VirtualService 条目。
-
创建一个共享的 Istio Gateway,供每个插件使用。
-
创建包含
nip.ioURL 的 VirtualServices。将创建的三个 URL 是:-
prom.<Host IP>.nip.io -
grafana.<Host IP>.nip.io -
kiali.<Host IP>.nip.io
-
脚本的最终输出将包含为你创建的 URL。
插件部署完成后,我们可以继续进入下一节,这一节将介绍主要的可观察性工具,Kiali。
安装 Kiali
Kiali 提供了一个强大的管理控制台,用于管理我们的服务网格。它提供了服务、Pod、流量安全等的图形化视图。由于它是开发人员和运维人员都非常有用的工具,本章剩余的内容将集中于部署和使用 Kiali。
有几种方式可以部署 Kiali,但我们将使用最常见的安装方法,即使用 Helm chart。为了部署该 chart 并创建访问 Kiali UI 所需的对象,我们在 chapter16/kiali 目录中提供了一个名为 deploy-kiali.sh 的脚本。执行该脚本来部署 Kiali。
该脚本将把 Kiali 部署到你的集群中的 istio-system 命名空间,并预配置与我们在上一部分部署的附加组件进行集成。它还将使用 nip.io URL 暴露 Kiali 的 UI,URL 会在脚本执行结束时提供。
这会部署一个匿名访问的仪表盘;然而,Kiali 可以接受其他身份验证机制来保护仪表盘。在下一章,我们将修改 Kiali 部署以接受 JWT,并使用 OpenUnison 作为提供者。
将应用程序部署到服务网格中
我们可以整天定义 Istio 的组件和对象,但如果你像我们一样,你会发现通过示例和用例来理解像 Istio 提供的功能等高级概念更为有益。在这一部分,我们将详细解释许多自定义资源,并提供你可以在 KinD 集群中部署的示例。
将你的第一个应用程序部署到网格中
最终!我们已经安装了 Istio 和附加组件,现在可以继续在服务网格中安装一个真实的应用程序,以验证一切是否正常工作。
在本节中,我们将部署一个来自 Google 的示例应用程序——Boutique 应用。在下一章,我们将部署另一个应用程序,并解释所有的细节和服务之间的通信,但在我们深入到那个层次的信息之前,Boutique 应用是一个很好的测试网格的应用程序。
在 chaper16/example-app 目录中,有一个名为 deploy-example.sh 的安装脚本,它将应用程序部署到集群中。该脚本会安装基础应用程序和所需的 Istio 对象,以便将应用程序暴露给外部世界。脚本执行的详细信息如下:
-
将创建一个名为
demo的新命名空间,并为其添加istio-injection=enabled标签。 -
使用
kubernetes-objects.yaml清单,将部署基础应用程序。 -
Istio 对象将使用模板创建,以便在
nip.io域中创建名称,从而方便访问应用程序。创建的 Istio 对象包括Gateway和VirtualService对象。 -
创建的
nip.io域名将在屏幕上输出。在我们的服务器上,它是kiali.10.3.1.248.nip.io。
执行后,您将在demo命名空间中拥有一个正在运行的示例应用程序。我们将使用这个应用程序来演示 Istio 和 Kiali 的可观察性功能。
通过使用浏览器打开nip.io URL,快速验证应用程序和 Istio 对象是否已通过脚本正确部署。您应该能够看到 Kiali 首页,我们将在下一节中讨论它。
使用 Kiali 观察网格工作负载
Kiali 提供了服务网格中的可观察性。它为您和您的开发人员提供了许多优势,包括对象之间流量流动的可视化地图、验证服务之间的 mTLS、日志和详细的度量数据。
Kiali 概览屏幕
如果您使用执行create-ingress脚本时提供的 URL 导航到 Kiali 首页,这将打开 Kiali 概览页面,在这里您将看到集群中所有命名空间的列表。

图 16.3:Kiali 主页
Kiali 会显示集群中的所有命名空间,即使它们没有启用 Istio。在我们当前的部署中,它会显示所有命名空间,不受任何已实施的 RBAC 限制,因为它是在没有身份验证的情况下运行的。如安装 Kiali章节中所述,我们将在下一章使用 JWTs 来保护 Kiali。
使用图表视图
我们将访问的仪表板的第一部分是图表视图,它提供了应用程序的图形视图。最初,它看起来可能像是工作负载构成对象的一个简单静态图形表示,但这只是打开图表视图时的默认视图;它不仅限于一个简单的静态视图,正如您在本节中将看到的那样。
由于我们将示例应用程序部署到demo命名空间,稍微向下滚动并查找包含demo命名空间的块,点击瓷砖上的三个点,然后选择图表:

图 16.4:使用 Kiali 显示命名空间的图表
这将带您到一个新的仪表板视图,显示示例应用程序对象:

图 16.5:Kiali 图表示例
图表上有很多对象,如果您是 Kiali 新手,可能会想知道每个图标代表什么。Kiali 提供了一个图例,帮助您识别每个图标的角色。
如果点击图表面板左下角的图标,您将看到图例图标。点击它即可查看每个图标的解释——下面显示了一个简略的图例列表:

图 16.6:Kiali 图例示例
默认情况下,这个视图只显示应用程序对象之间的静态路径视图。然而,您不仅限于静态视图——这正是 Kiali 的亮点所在。我们实际上可以启用实时流量视图,使我们能够观察所有请求的流量流动。
要启用此选项,点击图表视图上方的显示选项,在选项列表中勾选流量动画框,如图 16.7所示。

图 16.7:启用流量动画
这在静态图像中很难展示,但一旦启用了流量动画选项,你将实时看到所有请求的流量。
你不仅限于流量动画;你可以使用显示选项在图表视图中启用许多其他选项,包括响应时间、吞吐量、流量速率和安全性等项目。
在图 16.8中,我们启用了吞吐量、流量分布、流量速率和安全性:

图 16.8:Kiali 图表显示选项
如你所见,图像中对象之间的线条现在包含了额外的信息,包括:
-
一个锁,表示通信通过 sidecar 和 mTLS 加密
-
RPS,即每秒请求数
如你所见,Kiali 的图表视图是一个强大的工具,可以观察工作负载的端到端通信。这只是使用服务网格的额外好处之一。服务网格提供的可观察性是一个极其宝贵的工具,用于发现过去很难揭示的问题。
我们不仅限于图表视图;我们还有三个额外的视图,提供更多关于应用程序的见解。在 Kiali 仪表板的左侧,你会看到另外三个视图,应用、工作负载和服务。你还会注意到有一个其他选项,Istio 配置,允许你查看控制命名空间中 Istio 功能的对象。
使用应用视图
应用视图显示了具有相同标签的工作负载的详细信息,让你可以将视图细分为更小的部分。
使用我们在 Kiali 中打开的精品应用视图,点击左侧选项中的应用链接。这将带你进入按标签划分的应用概览页面。

图 16.9:Kiali 应用视图
每个应用程序都可以通过点击服务名称提供更多信息。如果我们点击adservice应用程序,Kiali 将打开一个页面,提供有关adservice应用程序交互对象的概览。对于每个应用程序,你还可以查看概览、流量、进出流量指标和跟踪。
概览页面为你提供了一个专门展示与adservice通信的对象的视图。我们在图表视图中看到了类似的通信视图,但我们也看到了其他所有对象——包括与adservice无关的对象。
应用视图将简化我们所看到的内容,使我们能够更轻松地浏览应用程序。

图 16.10:使用应用程序视图的简化通信视图
如您所见,应用程序视图包含来自图形视图的组件。涉及adservice的通信路径从前端 pod 开始,目标是adservice服务,最终将流量路由到adservice pod。
我们可以通过点击应用程序视图顶部的一个标签来查看应用程序的更多细节。概览旁边的第一个标签是流量标签,它为您提供应用程序的流量视图。

图 16.11:查看应用程序流量
流量标签将显示应用程序的入站和出站流量。在精品店的adservice示例中,我们可以看到adservice接收到了来自前端的入站请求。在入站流量下方,我们可以看到出站流量,在我们的示例中,Kiali 告诉我们没有出站流量。如在图 16.10的概览中所示,adservice pod 没有连接到任何对象,因此我们无法查看任何流量。要获取流量的更多细节,您可以点击操作下的查看指标链接——此操作与点击入站指标标签相同。
入站指标标签将为您提供有关传入流量的更多细节。图 16.12显示了adservice流量的简化示例。

图 16.12:查看入站指标
入站指标将显示许多不同的指标,包括请求量、请求持续时间、请求和响应大小、请求和响应吞吐量、gRPC 接收和发送、TCP 连接的打开和关闭、以及 TCP 的接收和发送。此页面将实时更新,使您能够查看实时捕获的指标。
最后,最后一个标签将允许您查看adservice应用程序的追踪。这就是我们在安装 Istio 时在集群中部署 Jaeger 的原因。追踪是一个相对复杂的话题,超出了本章的范围。如需了解更多有关使用 Jaeger 进行追踪的信息,请访问 Jaeger 官网 www.jaegertracing.io/。
使用工作负载视图
接下来我们将讨论工作负载视图,该视图将展示按工作负载类型(如部署)分类的视图。如果您点击 Kiali 中的工作负载链接,您将进入精品店工作负载的详细信息。

图 16.13:工作负载视图
您可能会注意到,在Details栏下有一个警告,告知我们缺少部署的版本。这是该视图的一个特点。它会提供诸如工作负载未分配版本的信息,这对网格中的标准功能没有影响,但会限制某些功能的使用,例如路由和一些遥测。最佳实践是始终为您的应用程序指定版本,但在本示例中,Google 的 Boutique 应用程序没有在部署中包含版本。
Workloads视图提供了一些与Applications视图相同的详细信息,包括流量、入站指标、出站指标和追踪——然而,除了这些细节,我们现在还可以查看日志和关于 Envoy 的详细信息。
如果点击Logs标签,您将看到adservice容器的日志。

图 16.14:查看容器日志
这是adservice容器生成的日志的实时视图。在此视图中,您可以创建一个过滤器来显示或隐藏特定的关键字,回滚到先前的事件,将默认缓冲区大小从 100 行更改,复制日志到剪贴板,或者进入全屏日志视图。许多用户发现这个标签非常有用,因为它不需要他们使用kubectl查看日志;他们只需在浏览器中打开 Kiali,便可快速在 GUI 中查看日志。
我们将讨论的最后一个标签是Envoy标签,它提供有关 Envoy sidecar 的附加详细信息。此标签中的详细信息非常广泛——它包含了您在命名空间中包含的所有网格对象(请记住,我们创建了一个 sidecar 对象,将对象限制为仅命名空间和istio-system命名空间),所有监听器、路由、引导配置、配置和指标。
到本章这一部分,您大概可以看到,Istio 确实需要一本独立的书来涵盖所有基本组件。Envoy标签中的所有标签都提供了丰富的信息,但非常详细,我们无法在本章中涵盖所有内容,因此在本章中,我们将只讨论Metrics标签。
点击Metrics标签,您将看到与 Envoy 的正常运行时间、分配的内存、堆大小、活动的上游连接、上游总请求、下游活动连接和下游 HTTP 请求相关的指标。

图 16.15:Envoy 指标
与大多数指标一样,如果您遇到 Envoy 代理容器的问题,这些指标将非常有帮助。正常运行时间将告诉您 Pod 已经运行了多长时间,分配的内存告诉您分配给 Pod 的内存量,这可能有助于确定为什么发生 OOM 情况,而活动连接则会告知如果连接数低于预期或为零时,服务是否存在问题。
使用服务视图
最后,我们将讨论应用程序的最后一个视图——服务视图。顾名思义,这将提供工作负载中包含的服务视图。你可以通过点击 Kiali 中的Services选项来打开服务视图。

图 16.16:服务视图
与其他视图类似,这将提供服务的名称和每个服务的健康状况。如果你点击任何单个服务,你将进入该服务的详细信息。如果你点击adservice,你将进入该服务的概览页面。

图 16.17:服务概览
Overview页面应该包含一些你熟悉的对象。与其他视图一样,它仅显示与adservice通信的对象,并且有流量、入站指标和跟踪的标签页——然而,除了这些,它还显示了服务的网络信息。在我们的示例中,服务已配置为使用ClusterIP类型,分配的服务 IP 是10.110.47.79,它有一个端点10.240.189.149,并且在端口9555上暴露了 gRPC TCP 端口。
这些信息你可以通过kubectl获取,但对于很多人来说,从 Kiali 仪表盘获取这些细节更为快捷。
Istio 配置视图
我们讨论的最后一个视图与工作负载并没有直接关系。相反,它是一个用于命名空间的 Istio 配置视图。这个视图将包含你所创建的 Istio 对象。在我们的示例中,我们有两个对象:网关和虚拟服务。

图 16.18:Istio 配置视图
你可以通过点击每个对象的名称来查看该对象的 YAML。这允许你直接在 Kiali 仪表盘中编辑对象。任何保存的更改将会编辑集群中的对象,因此如果你使用这种方法修改对象时,请小心。
这个视图提供了其他视图没有的一个附加功能——使用向导创建新的 Istio 对象。要创建新对象,点击 Istio 配置视图右上角的Actions下拉菜单。这样会弹出一个你可以创建的对象列表,如图 16.19所示。

图 16.19:Istio 对象创建向导
如图所示,Kiali 提供了一个向导,用于创建 6 个 Istio 对象,包括AuthorizationPolicies、Gateways、PeerAuthentication、RequestAuthentication、ServiceEntries和Sidecars。
每个选项都有一个向导,引导你完成该对象的具体要求。例如,我们可以使用向导创建一个 sidecar,如图 16.20所示。

图 16.20:使用 Istio 对象向导
一旦所有字段都正确输入,你可以点击Preview,这将带你到下一个屏幕,你将在那里看到对象的 YAML 源代码,如图 16.21所示。

图 16.21:向导源 YAML
如果看起来不错,点击创建来创建新对象。
向导是新接触 Istio 的用户一个很好的工具,但要小心不要过度依赖它们。你应该始终理解如何为你的所有对象创建清单。像这样的向导创建对象可能会导致问题,因为你不懂对象是如何工作的或如何创建的。
在接下来的部分,我们将介绍 Istio 未来的发展方向。尽管 sidecar 非常强大,但它也有其局限性,并且每个网格中的 pod 都需要额外的资源。在 2023 年,Istio 推出了一个名为环境网格(ambient mesh)的新概念,作为一个早期访问功能,移除了对 Istio sidecar 的需求。
未来:环境网格
今天,像 Istio 这样的服务网格依赖于连接到每个服务实例的 sidecar 代理来处理流量、安全措施和度量数据收集。虽然这种方法效果良好,但它会导致额外的资源使用和复杂性,特别是在较大集群中的部署。
在本章中,我们提到过很多次 sidecar——它们是网格的核心,提供了一个层次,去除了使用网格功能的所有复杂性,而不需要对我们的应用程序进行代码更改。
环境网格标志着服务网格设计的一个重要变化,试图在不需要每个服务都附带 sidecar 代理的情况下,使将服务网格功能添加到已有复杂系统中变得更加容易。其目标是减少额外的工作和复杂性,同时保持服务网格的主要优势,包括监控、安全性和流量管理。
从 Istio 1.20 开始,环境网格处于Alpha阶段。我们尚未为环境网格单独添加章节的主要原因是因为从当前的 Alpha 阶段到它正式进入正式发布(GA)阶段期间,可能会发生一些变化。然而,由于它是一次巨大的飞跃和设计上的重大改变,我们希望引起你的注意。你可以在 Istio 官网上阅读更多关于如何开始使用环境网格的内容:istio.io/latest/docs/ops/ambient/getting-started/。
阅读 Istio 官网的文档将提供一些很好的示例,展示环境网格是如何部署和工作的。由于许多读者对 Istio 是新手,因此直接跳入文档进行概览可能会有些困难,所以我们想要提供我们对环境网格对我们意义的关键点的看法。
正如我们所提到的,环境网格通过去除通常由 sidecar 代理管理的任务,解决了许多问题。与将代理附加到每个服务实例不同,环境网格会将这些功能直接集成到网络中,或者集成到一个公共的代理层中。这一设计旨在简化流程,并减少与传统 sidecar 方法相比所需的资源。这带来了许多优势,包括:
-
保留基本特性:尽管其架构独特,环境网格仍将继续提供当前服务网格中所包含的基本功能,包括服务之间的安全通信、流量控制和监控组件的能力。
-
简化、高效的部署:环境网格消除了对单独 sidecar 代理的需求,从而简化了服务网格的设置和管理。这将促进更易于采纳和维护,特别是对于具有复杂微服务结构的组织。
-
改进的资源利用率:通过减少服务间交互对 CPU 和内存的需求,环境网格能够高效地利用资源。
-
性能增强:环境网格通过优化服务通信所用的路由,减少延迟并提高效率,从而提升系统性能。
想象一下在一个大型集群中,可能有成千上万,甚至几十万个服务正在运行时,资源和复杂性的节省。目前,这将需要相应数量的代理实例来运行,每个代理都增加了通信中的额外层,并使用自己的资源——使用额外的 CPU 和 RAM,这些资源对于基础应用程序来说并非“必需”。环境网格将通过减少所需资源来为您节省成本,并通过简化架构,当应用程序行为异常时,问题的排查将变得更加容易。
我们希望本章提供了关于 Istio 以及 Istio 未来发展方向的有用介绍。在下一章中,我们将深入探讨如何在企业环境中使用 Istio 运行应用程序。
总结
本章介绍了使用流行的开源项目 Istio 进入服务网格的世界。在本章的第一部分,我们解释了使用服务网格的一些优势,其中包括为网格服务提供的安全性和可观测性。
本章的第二部分详细介绍了 Istio 的安装以及可用的不同安装配置文件。我们将 Istio 部署到 KinD 集群中,并移除了 NGNIX,以腾出端口80和443,供 Istio 的入口网关使用。本节还介绍了部署 Istio 后,集群中添加的对象。我们通过使用示例清单,覆盖了最常见的对象,帮助加深理解如何在自己的部署中使用每个对象。
在本章的最后,我们详细介绍了如何安装 Kiali、Prometheus 和 Jaeger,以便在我们的服务网格中提供强大的可观测性。我们还解释了如何使用 Kiali 查看网格中应用程序的度量和日志。
在下一章,我们将部署一个新应用,并将其绑定到服务网格,基于本章介绍的许多概念进行构建。
问题
-
哪个 Istio 对象用于在多个版本的应用程序之间路由流量?
-
Ingress 规则
-
VirtualService
-
DestinationRule
-
你不能在多个版本之间路由流量,只能在单个实例之间路由。
-
-
提供服务网格可观察性需要哪些工具?
-
Prometheus
-
Jaeger
-
Kiali
-
Kubernetes Dashboard
-
-
对错:Istio 的特性要求开发人员修改代码以利用诸如互相 TLS 和授权等功能。
-
对
-
错
-
-
Istio 通过将多个组件合并成一个可执行文件,简化了控制平面的部署和配置,称为:
-
Istio
-
IstioC
-
istiod
-
Pilot
-
答案
-
b - VirtualService 和 c - DestinationRule
-
a Prometheus 和 c - Kiali
-
b - 错
-
c - Pilot
第十七章:在 Istio 上构建和部署应用程序
在上一章中,我们将 Istio 和 Kiali 部署到了我们的集群中。我们还部署了一个示例应用程序,以查看各个部分如何组合。在本章中,我们将探讨构建能够在 Istio 上运行的应用程序所需要的内容。我们将从比较微服务和单体应用程序的区别开始。然后,我们将在 Istio 上部署一个单体应用程序,接着构建在 Istio 上运行的微服务。本章将涵盖以下主要内容:
-
微服务与单体架构的比较
-
部署单体应用
-
构建微服务
-
我是否需要 API 网关?
完成本章后,你将实际理解单体应用与微服务之间的区别,并掌握决定选择哪种架构的相关信息,你还将成功地在 Istio 中部署一个安全的微服务。
技术要求
要运行本章中的示例,你需要:
-
一个已部署 Istio 的运行集群,如第十六章《Istio 简介》中所述。
-
来自本书 GitHub 仓库的脚本。
你可以通过访问本书的 GitHub 仓库来获取本章的代码:github.com/PacktPublishing/Kubernetes-An-Enterprise-Guide-Third-Edition/tree/main/chapter17。
微服务与单体架构的比较
在深入代码之前,我们应花一些时间讨论微服务和单体架构的区别。微服务与单体架构的争论几乎与计算机本身的历史一样久远(而且这一理论可能更久远)。理解这两种方法如何相互关联以及与问题集的关系,将帮助你决定使用哪种方法。
我与微服务与单体架构的历史
在我们深入讨论微服务与单体架构之前,我想分享一下我自己的历史。我怀疑这并不独特,但它确实为我对这一讨论的看法提供了框架,并为本章中的建议提供了一些背景。
我第一次接触这个讨论是当我还是一名计算机科学专业的大学生,并开始使用 Linux 和开源软件时。我最喜欢的书之一,《开放源代码:开源革命的声音》,书中附录讨论了 Andrew Tanenbaum 与 Linus Torvalds 关于微内核与单体内核的争论。Tanenbaum 是 Minix 的发明者,他支持简约内核,大多数功能在用户空间中完成。而 Linux 则使用单体内核设计,更多的功能是在内核中完成的。如果你曾经运行过modprobe来加载驱动程序,那么你就是在与内核交互!完整的讨论可以在www.oreilly.com/openbook/opensources/book/appa.xhtml查看。
Linus 的核心论点是,管理良好的单体架构比微内核更容易维护。
Tanenbaum 则指出,微内核更容易移植,而且大多数“现代”内核都是微内核。Windows(当时是 Windows NT)今天可能是最普遍的微内核。作为软件开发者,我一直在努力找到我可以构建的最小单元。微内核架构真的很吸引我这一方面的才能。
与此同时,我开始了我的 IT 职业生涯,主要作为数据管理和分析领域的 Windows 开发人员。我大部分时间都在使用ASP(Active Server Pages,微软版的 PHP)、Visual Basic 和 SQL Server。我试图说服我的老板,我们应该从单体应用设计转向使用MTS(Microsoft Transaction Server)。MTS 是我第一次接触到今天我们所称的分布式应用。我的老板和导师们都指出,如果我们为了更干净的代码库而注入额外的基础设施,那我们的成本,进而客户的成本,将会飙升。没有什么我们在做的事情是不能用我们紧密结合的 ASP、Visual Basic 和 SQL Server 三者组合以更低成本完成的。
后来,我从数据管理转到了身份管理。我也从微软技术转向了 Java。我的第一个项目之一是部署一个基于分布式架构构建的身份管理厂商的产品。当时,我认为这很好,直到我开始尝试调试问题并在数十个日志文件中追踪问题。我很快开始使用另一家厂商的产品,它是一个单体架构。尽管部署速度较慢,因为需要完全重新编译,但除此之外,管理要容易得多,而且它的扩展性也同样很好。我们发现,分布式架构并没有带来帮助,因为身份管理是由一个高度集中的团队完成的,采用单体架构并不会影响生产力或管理。将实现分布的好处并不足以抵消额外的复杂性。
快进到 Tremolo Security 的成立。那是在 2010 年,也就是 Kubernetes 和 Istio 出现之前。那时,虚拟设备正风靡一时!我们决定 OpenUnison 采用单体架构,因为我们希望简化部署和升级。在第六章,将身份认证集成到集群中,我们使用一些 Helm 图表部署了 OpenUnison,以便在不同配置上叠加。假如有一个身份认证服务需要安装,一个目录服务,一个即时配置服务等等,难度会增加多少呢?有一个系统来部署,简化了部署过程。
说到这里,并不是说我反对微服务——我并不反对!当正确使用时,它是一种非常强大的架构,许多世界上最大的公司都在使用它。多年来我学到的是,如果它不是你的系统的合适架构,它会显著影响你的交付能力。现在我已经跟你分享了我在架构方面的历程,让我们更深入地了解微服务和单体架构之间的区别。
比较应用中的架构
首先,让我们谈谈这两种架构方法在一个常见示例应用——店面中的作用。
单体应用设计
假设你有一个在线商店。你的商店可能需要一个产品查找服务、一个购物车、一个支付系统和一个运输系统。这是一个对店面应用的极度简化,但讨论的重点是如何拆分开发,而不是如何构建一个店面。你可以通过两种方式来构建这个应用。第一种是构建一个单体应用,其中每个服务的所有代码都存储并管理在同一个树结构中。你的应用架构可能看起来像这样:

图 17.1:单体应用架构
在我们的应用中,我们有一个单一的系统,包含多个模块。根据你选择的编程语言,这些模块可能是类、结构体或其他形式的代码模块。一个中央应用程序管理用户与这些代码的交互。这通常是一个网页前端,模块则是服务器端代码,编写为 Web 服务或请求/响应风格的应用。
是的,Web 服务可以在单体架构中使用!这些模块通常需要存储数据,通常是某种形式的数据库。无论是关系型数据库、文档型数据库,还是一系列数据库,其实并不重要。
这种单体架构最大的优势是它相对容易管理,且系统之间可以互相交互。如果用户想进行产品搜索,店面可能只需执行以下类似的代码:
list_of_products = products.search(search_criteria);
display(list_of_products);
应用代码只需要知道它将要调用的服务的接口。无需“验证”从应用控制器到产品目录模块的调用。也无需担心创建速率限制系统或试图弄清楚使用哪个版本的服务。一切都紧密绑定。如果你对任何系统进行了更新,你很快就会知道是否破坏了接口,因为你可能使用了一个开发工具,当模块接口出现问题时,它会告诉你。最后,部署通常也非常简单。你只需要将代码上传到部署服务(或者创建容器……毕竟这是一本 Kubernetes 书!)。
如果你需要一个开发者更新订单系统,而另一个开发者更新支付系统怎么办?他们各自有自己的代码副本,必须合并。在合并之后,两个分支的更改需要在部署之前进行对账。这对于一个小系统可能没问题,但随着你的商店前端不断扩展,这可能会变得繁琐,甚至无法管理。
另一个潜在的问题是,如果有比整个应用程序更合适的语言或系统来构建其中的某个服务怎么办?多年来,我参与了多个项目,在一些组件上 Java 是一个不错的选择,而在其他组件上 C# 提供了更好的 API。也许一个服务团队是基于 Python 构建的,另一个则是基于 Ruby 的。标准化本是好事,但为了标准化,你不会用螺丝刀的尾端来钉钉子吧?
这个论点与前端和后端无关。一个具有 JavaScript 前端和 Golang 后端的应用程序仍然可以是一个单体应用程序。Kubernetes Dashboard 和 Kiali 都是构建在跨语言服务 API 上的单体应用程序示例。它们都有 HTML 和 JavaScript 前端,而后端 API 则是用 Golang 编写的。
微服务设计
如果我们将这些模块拆分成服务会怎样?我们不再有一个单一的源代码树,而是将应用程序拆分成如下所示的独立服务:

图 17.2:简单的微服务架构
这看起来并不复杂。与其说是一个大框框,不如说是一堆线条。让我们放大看一下前端到产品查询服务的调用:

图 17.3:服务调用架构
这不再是一个简单的函数或方法调用了。现在,我们的商店控制器需要确定将服务调用发送到哪里,这可能会在每个环境中有所不同。它还需要注入某种认证令牌,因为你不希望任何人都能调用你的服务。由于远程服务不再有本地代码表示,你要么需要手动构建调用,要么使用架构语言来描述你的产品列表服务,并将其与客户端绑定结合起来。一旦调用发出,服务需要验证调用的架构,并应用认证和授权的安全规则。响应被打包并发送回商店控制器后,控制器需要验证响应的架构。如果失败,它需要决定是否重试。
将所有这些额外的复杂性与版本管理结合起来。我们的前端应该使用哪个版本的产品查找服务?其他服务是否紧密耦合在一起?正如我们之前讨论的,微服务方法在版本和部署管理方面有几个好处。这些优势伴随着额外复杂性的代价。
选择单体架构还是微服务
这两种方法哪种适合你?这实际上取决于你的团队是什么样的?你的管理需求是什么?你是否需要微服务带来的灵活性,还是单体架构更简单的设计能够让系统更容易管理?
微服务架构的一个主要优点是,你可以让多个团队在自己的代码上工作,而无需共享同一个源代码仓库。在假设将服务拆分到各自的源代码仓库对你的团队有益之前,这些服务的耦合程度如何?如果服务之间有大量的相互依赖,那么你的微服务其实就是一个分布式的单体架构,你可能无法享受到不同仓库带来的好处。管理分支并将它们合并可能会更容易。
此外,你的服务是否需要被其他系统调用?回顾我们在上一章构建的集群,Kiali 有自己的服务,但这些服务不太可能被其他应用程序使用。然而,Jaeger 和 Prometheus 确实有被 Kiali 使用的服务,即使这些系统也有自己的前端。除了这些服务,Kiali 还使用了 Kubernetes API。所有这些组件都是单独部署和管理的,需要独立升级、监控等。这可能会带来管理上的头痛,因为每个系统都是独立管理和维护的。话虽如此,Kiali 团队重新实现 Prometheus 和 Jaeger 并没有任何意义。将这些项目的整个源代码导入并强迫保持更新也没有意义。
使用 Istio 来帮助管理微服务
我们已经花了相当多的时间讨论微服务和单体架构,但却没有提到 Istio。在本章前面,图 17.3 指出了在我们调用代码之前,微服务所需要做出的决策。
这些应该很熟悉,因为我们在上一章已经讨论了 Istio 中的对象,它们可以满足大部分需求!Istio 可以消除我们编写代码来进行客户端身份验证和授权、发现服务运行位置以及管理流量路由的需求。在本章的其余部分,我们将通过构建一个小型应用程序来演示如何利用微服务,并使用 Istio 来利用这些常见服务,而无需将它们构建到我们的代码中。
到目前为止,我们已经了解了单体应用和微服务之间的区别,以及这些区别如何在概念上与 Istio 进行交互。接下来,我们将看到如何将单体应用部署到 Istio 中。
部署单体应用
本章内容是关于微服务的,那么为什么我们从在 Istio 中部署单体应用开始呢?第一个答案是,因为我们可以!在集群中使用单体应用时,完全没有理由不享受 Istio 内置功能的好处。即使它不是“微服务”,但能够追踪应用请求、管理部署等依然很有用。第二个答案是,因为我们需要这么做。我们的微服务需要知道在我们的企业中是哪个用户在调用它。为了做到这一点,Istio 需要一个 JWT 来验证。我们将首先使用 OpenUnison 生成 JWT,以便我们可以手动调用服务,然后我们可以从前端进行用户身份验证,并确保安全地调用我们的服务。
从第十六章开始,我们现在要部署 OpenUnison。进入chapter17/openunison-istio目录,并运行deploy_openunison_istio.sh:
cd chapter17/openunison-istio
./deploy_openunison_istio.sh
这将需要一段时间来运行。这个脚本做了几件事:
-
使用我们的企业 CA 部署
cert-manager。 -
部署所有 OpenUnison 组件(包括我们的测试 Active Directory)用于模拟,因此我们不需要担心更新 API 服务器以使 SSO 工作。
-
给
openunison命名空间打上istio-injection: enabled标签。这告诉 Istio 为所有 Pod 启用 sidecar 注入。你可以通过运行kubectl label ns openunison istio-injection=enabled手动完成此操作。 -
为我们创建所有的 Istio 对象(接下来我们将详细介绍这些对象)。
-
在
istio-system命名空间中创建一个ou-tls-certificate证书。再次说明,为什么要这么做我们将在下一节详细探讨。
一旦脚本运行完毕,我们现在可以登录到我们的单体应用了!就像在第六章中提到的,将身份验证集成到集群中,访问https://k8sou.apps.XX-XX-XX-XX.nip.io/进行登录,其中XX-XX-XX-XX是你主机的 IP 地址。
比如说,我的主机的 IP 地址是192.168.2.114,因此我的 URL 是https://k8sou.apps.192-168-2-114.nip.io/。同样,正如在第六章中提到的,用户名是mmosley,密码是start123。
现在我们的单体应用已经部署好了,让我们一步一步地了解与部署相关的 Istio 特定配置。
将我们的单体应用暴露到集群外部
我们的 OpenUnison 已经在运行,让我们来看看将它暴露到网络中的对象。完成这项工作的有两个主要对象:Gateway和VirtualService。这些对象是在我们安装 OpenUnison 时创建的。如何配置这些对象已经在第十六章,Istio 简介中描述过。接下来,我们将查看正在运行的实例,展示它们如何授予访问权限。首先,让我们来看一下网关的关键部分。有两个网关,第一个是openunison-gateway-orchestra,它负责访问 OpenUnison 门户和 Kubernetes 仪表板:
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
spec:
selector:
istio: ingressgateway
servers:
- hosts:
- k8sou.apps.192-168-2-114.nip.io
- k8sdb.apps.192-168-2-114.nip.io
port:
name: http
number: 80
protocol: HTTP
tls:
httpsRedirect: true
- hosts:
- k8sou.apps.192-168-2-114.nip.io
- k8sdb.apps.192-168-2-114.nip.io
port:
name: https-443
number: 443
protocol: HTTPS
tls:
credentialName: ou-tls-certificate
mode: SIMPLE
selector告诉 Istio 要与哪个ingress-ingressgateway pod 进行交互。部署到istio-system的默认网关具有标签istio: ingressgateway,它将与这个网关匹配。你可以运行多个网关,使用这个部分来确定你希望将服务暴露给哪一个。如果你有多个网络并且流量不同,或者你想要在集群中分隔应用之间的流量,这将非常有用。
servers列表中的第一个条目告诉 Istio,如果请求通过 HTTP 进入端口80,并且目标是我们的任一主机,那么我们希望 Istio 将请求重定向到 HTTPS 端口。这是一种良好的安全实践,因此大家不要试图绕过 HTTPS。servers中的第二个条目告诉 Istio 接受端口443上的 HTTPS 连接,并使用名为ou-tls-certificate的Secret中的证书。这个Secret必须是 TLS 类型的Secret,并且必须与运行入口网关的 pod 位于同一命名空间。对于我们的集群,这意味着ou-tls-certificate必须位于istio-system命名空间中。这就是为什么我们的部署脚本在istio-system命名空间中创建了通配符证书。这与使用 NGINX 的Ingress对象不同,后者将 TLS Secret保留在与Ingress对象相同的命名空间中。
如果你没有将你的Secret放在正确的命名空间中,可能会很难调试。你首先会注意到,当你尝试连接到主机时,浏览器会报告连接已被重置。这是因为 Istio 没有证书可以提供。Kiali 不会告诉你有配置问题,但通过查看istio-system中istiod pod 的日志,你会发现failed to fetch key and certificate for kubernetes://secret-name,其中secret-name是你的Secret的名称。一旦你将Secret复制到正确的命名空间中,应用程序就会开始在 HTTPS 上工作。
第二个网关是openunison-api-gateway-orchestra,用于通过 HTTPS 直接暴露 OpenUnison 的 API 服务器主机。这绕过了 Istio 的内建功能,因此除非有必要,否则我们不希望这样做。这个网关与我们的另一个网关的主要区别在于我们如何配置 TLS:
- hosts:
- k8sapi.192-168-2-114.nip.io
port:
name: https-443
number: 443
protocol: HTTPS
tls:
**mode:****PASSTHROUGH**
我们使用PASSTHROUGH作为mode,而不是SIMPLE。这告诉 Istio 不要费力去解密 HTTPS 请求,而是直接将其下游传送。我们必须为 Kubernetes API 调用这样做,因为 Envoy 不支持 kubectl 用于exec、cp和port-forward的 SPDY 协议,因此我们需要绕过它。当然,这也意味着我们失去了 Istio 的许多功能,所以如果能避免,最好不要这样做。
Gateway对象告诉 Istio 如何监听连接,而VirtualService对象则告诉 Istio 将流量发送到哪里。就像Gateway对象一样,这里有两个VirtualService对象。第一个对象处理 OpenUnison 门户和 Kubernetes 仪表盘的流量。以下是其中的重要部分:
spec:
gateways:
- openunison-gateway-orchestra
hosts:
- k8sou.apps.192-168-2-114.nip.io
- k8sdb.apps.192-168-2-114.nip.io
http:
- match:
- uri:
prefix: /
route:
- destination:
host: openunison-orchestra
port:
number: 80
gateways部分告诉 Istio 要将这个配置链接到哪些Gateway对象。理论上,你可以有多个 Gateway 作为流量的来源。hosts部分告诉 Istio 将此配置应用于哪些主机名,而match部分则告诉 Istio 要根据什么条件匹配请求。对于微服务路由,这部分可以提供很大的灵活性,但对于单体应用,通常/就足够了。
最后,route部分告诉 Istio 将流量发送到哪里。destination.host是你想要发送流量的Service名称。我们将所有流量发送到端口80(大致如此)。
这个配置的 NGINX Ingress版本将所有流量发送到 OpenUnison 的 HTTPS 端口(8443)。这意味着所有数据都会在用户浏览器和 OpenUnison Pod 之间的网络传输中被加密。我们在这里并没有这样做,因为我们将依赖 Istio 的 sidecar 来进行 mTLS。
即使我们通过 HTTP 将流量发送到端口80,从流量离开ingressgateway Pod 直到到达 OpenUnison Pod 上的 sidecar 并拦截所有 OpenUnison 的入站网络连接之前,这些流量都会被加密。无需显式配置 TLS!
现在我们已经将流量从我们的网络路由到 OpenUnison,接下来我们来处理单体应用程序中的一个常见需求:粘性会话。
配置粘性会话
大多数单体应用程序都需要粘性会话。启用粘性会话意味着每个会话中的每个请求都会发送到同一个 pod。在微服务中通常不需要这种设置,因为每个 API 调用都是独立的。用户交互的 Web 应用程序通常需要管理状态,通常是通过 cookies。但这些 cookies 通常不会存储整个会话的状态,因为它们会变得太大,并且可能包含敏感信息。相反,大多数 Web 应用程序使用指向服务器上保存的会话(通常在内存中)的 cookie。虽然有一些方法可以确保该会话对应用程序的任何实例都可用,且具有高可用性,但这样做并不常见。这些系统维护成本较高,通常不值得投入过多精力。
OpenUnison 和大多数其他 Web 应用程序一样,需要确保会话与其来源的 pod 保持粘性。为了告诉 Istio 如何管理会话,我们使用 DestinationRule。DestinationRule 对象告诉 Istio 在通过 VirtualService 路由到主机的流量中应该做什么。以下是我们规则中的重要部分:
spec:
host: openunison-orchestra
trafficPolicy:
loadBalancer:
consistentHash:
httpCookie:
name: openunison-orchestra
path: /
ttl: 0s
tls:
mode: ISTIO_MUTUAL
规则中的 host 指的是流量的目标(Service),而不是原始 URL 中的主机名。spec.trafficPolicy.loadBalancer.consistentHash 告诉 Istio 我们希望如何管理粘性。大多数单体应用程序会希望使用 cookies。ttl 设置为 0s,因此该 cookie 被视为“会话 cookie”。这意味着当浏览器关闭时,cookie 会从浏览器的 cookie 存储中消失。
应避免使用具有特定生存时间的 cookies。这些 cookies 会被浏览器持久化,并且可能被贵企业视为安全风险。
OpenUnison 启动并运行,且了解了 Istio 的集成方式后,让我们来看一下 Kiali 会如何向我们展示关于我们的单体应用的信息。
集成 Kiali 和 OpenUnison
首先,让我们整合 OpenUnison 和 Kiali。Kiali 和其他集群管理系统一样,应当配置为需要访问权限。Kiali 和 Kubernetes Dashboard 一样,可以与 impersonation 集成,这样 Kiali 将使用用户自己的权限与 API 服务器进行交互。完成这个操作相当简单。我们在 chapter17/kiali 文件夹中创建了一个名为 integrate-kiali-openunison.sh 的脚本,功能如下:
-
删除 Kiali、Prometheus、Jaeger 和 Grafana 的旧网关和虚拟服务。
-
更新 Grafana,以便接受来自 OpenUnison 的 SSO 头部信息。
-
更新 Kiali 的 Helm chart,使用
header作为auth.strategy,并重启 Kiali 以应用更改。 -
重新部署 OpenUnison,并集成 Kiali、Prometheus、Jaeger 和 Grafana 以实现单点登录(SSO)。
该集成的工作方式与仪表盘相同,但如果你对细节感兴趣,可以在 openunison.github.io/applications/kiali/ 阅读相关内容。
集成完成后,让我们看看 Kiali 能告诉我们关于我们单体应用的哪些信息。首先,登录到 OpenUnison。你会在门户屏幕上看到新的徽章:

图 17.4:带有 Kiali、Prometheus、Grafana 和 Jaeger 徽章的 OpenUnison 门户
接下来,点击Kiali徽章打开 Kiali,然后点击Graphs,选择openunison命名空间。你将看到类似于以下的图表:

图 17.5:Kiali 中的 OpenUnison 图表
现在,你可以像查看微服务一样查看 OpenUnison、apacheds和其他容器之间的连接!说到这里,既然我们已经学习了如何将单体应用集成到 Istio 中,接下来让我们构建一个微服务,并学习它如何与 Istio 集成。
构建一个微服务
我们花了不少时间讨论单体应用。首先,我们讨论了哪种方法最适合你,然后我们花了一些时间展示如何将单体应用部署到 Istio 中,以从中获得许多微服务的好处。现在,让我们深入探讨构建和部署一个微服务。我们的微服务将会很简单。目标是展示微服务是如何构建并集成到一个应用中的,而不是如何基于微服务构建一个完整的应用程序。我们的书聚焦于企业应用,因此我们将专注于一个服务:
-
需要特定用户的身份验证
-
需要基于组成员资格或属性的特定用户授权
-
做了一些非常重要的事情
-
生成一些关于发生了什么的日志数据
这是企业应用程序及其构建服务中的常见情况。大多数企业需要能够将某些操作或决策与组织中的特定人员关联起来。如果下了订单,是谁下的?如果案件被关闭,是谁关闭的?如果支票被开出,是谁开的?当然,也有很多情况下,用户并不对某个操作负责。有时是另一个自动化的服务。一个拉取数据来创建仓库的批处理服务并不与特定人员关联。这是一个交互式服务,意味着预期终端用户与其进行交互,因此我们假设用户是企业中的一个人。
一旦你知道了谁将使用这个服务,接下来你需要确认用户是否有权使用该服务。在前一段中,我们已经确定了你需要知道“谁开了支票”。另一个重要的问题是,“他们有权开这张支票吗?”你肯定不希望组织中的任何人都能随便开支票,对吧?确定谁有权执行某个操作可能是多本书的内容,因此为了简单起见,我们将基于组成员身份做出授权决策,至少在高级别上是这样。
在确定了用户并授权之后,下一步就是做一些重要的事情。这是一个充满需要完成的重要事务的企业!因为写支票是我们都能理解的,并且代表了企业服务面临的许多挑战,我们将继续以此为例。我们将编写一个支票服务,允许我们发出支票。
最后,做完一些重要的事情后,我们需要进行记录。我们需要跟踪是谁调用了我们的服务,一旦服务完成了重要部分,我们还需要确保将这些信息记录在某个地方。这些记录可以存储在数据库或其他服务中,甚至可以发送到标准输出,以便日志聚合工具(如我们在第十五章中部署的 OpenSearch)收集。
在确定了我们的服务将做什么之后,下一步是确定我们的基础设施中哪一部分将负责每个决策和行动。对于我们的服务:
| 操作 | 组件 | 描述 |
|---|---|---|
| 用户认证 | OpenUnison | 我们的 OpenUnison 实例将会验证用户的“Active Directory”身份 |
| 服务路由 | Istio | 我们将如何向外界暴露我们的服务 |
| 服务认证 | Istio | RequestAuthentication 对象将描述如何验证我们服务的用户 |
| 服务粗粒度授权 | Istio | AuthorizationPolicy 将确保用户是某个特定组的成员,从而能够调用我们的服务 |
| 细粒度授权或权利 | 服务 | 我们的服务将决定你能为哪些收款人开支票 |
| 写支票 | 服务 | 编写这个服务的目的! |
| 记录是谁写了支票以及支票发给了谁 | 服务 | 将这些数据写入标准输出 |
| 日志聚合 | Kubernetes | 在生产环境中——使用像 OpenSearch 这样的工具 |
表 17.1:服务职责
我们将在接下来的部分中逐层构建这些组件。在深入讨论服务本身之前,我们需要先向世界打个招呼。
部署 Hello World
我们的第一个服务将是一个简单的 Hello World 服务,作为我们检查写入服务的起点。我们的服务是基于 Python 和 Flask 构建的。我们使用它是因为它相对简单,易于使用和部署。请进入 chapter17/hello-world 目录并运行 deploy_helloworld.sh 脚本。这将创建我们的 Namespace、Deployment、Service 和 Istio 对象。查看 service-source ConfigMap 中的代码。这是我们代码的主体,也是我们构建检查服务的框架。代码本身并不做太多的事情:
@app.route('/')
def hello():
retVal = {
"msg":"hello world!",
"host":"%s" % socket.gethostname()
}
return json.dumps(retVal)
这段代码接受所有对 / 的请求并运行我们的 hello() 函数,返回一个简单的响应。为了简化操作,我们将代码作为 ConfigMap 嵌入其中。
如果你已经阅读了所有前面的章节,你会注意到从安全角度来看,我们的这个容器违反了一些基本规则。它是一个以 root 身份运行的 Docker Hub 容器。现在这样是可以的,我们并不想在这一章中深入探讨构建过程。在第十九章:构建开发者门户中,我们将通过使用 GitLab 工作流来构建该服务的更安全版本的容器。
一旦我们的服务部署完成,我们可以通过使用 curl 来测试它:
$ curl http://service.192-168-2-114.nip.io/
{"msg": "hello world!", "host": "run-service-785775bf98-fln49"}%
这段代码并不令人兴奋,但接下来我们将为我们的服务增加一些安全性。
将身份验证集成到我们的服务中
在 第十六章:Istio 简介 中,我们介绍了 RequestAuthentication 对象。现在,我们将使用该对象来强制执行身份验证。我们希望确保访问我们的服务时,你必须拥有一个有效的 JWT。在前面的示例中,我们直接调用了我们的服务。现在,我们希望只有在请求中嵌入有效的 JWT 时,才能收到响应。我们需要确保将我们的 RequestAuthentication 与一个 AuthorizationPolicy 配对,这样 Istio 才会强制要求 JWT;否则,Istio 将只会拒绝不符合我们 RequestAuthentication 的 JWT,而允许没有 JWT 的请求。
即使在我们配置对象之前,我们也需要从某个地方获取 JWT。我们将使用 OpenUnison。为了与我们的 API 一起使用,让我们部署我们在 第六章:将身份验证集成到集群中 中部署的管道令牌生成图表。进入 chapter6/pipelines 目录并运行 Helm 图表:
$ helm install orchestra-token-api token-login -n openunison -f /tmp/openunison-values.yaml
NAME: orchestra-token-api
LAST DEPLOYED: Tue Aug 31 19:41:30 2021
NAMESPACE: openunison
STATUS: deployed
REVISION: 1
TEST SUITE: None
这将为我们提供一个轻松从内部 Active Directory 生成 JWT 的方法。接下来,我们将部署实际的策略对象。进入 chapter17/authentication 目录并运行 deploy-auth.sh。它将如下所示:
$ ./deploy-auth.sh
secret/cacerts configured
pod "istiod-75d8d56b68-tm6b7" deleted
requestauthentication.security.istio.io/hello-world-auth created
authorizationpolicy.security.istio.io/simple-hellow-world created
首先,我们创建了一个名为 cacerts 的 Secret,用于存储我们的企业 CA 证书,并重新启动 istiod。这将允许 istiod 与 OpenUnison 通信,以拉取 jwks 签名验证密钥。接下来,创建了两个对象。第一个是 RequestAuthentication 对象,第二个是简单的 AuthorizationPolicy。首先,我们将介绍 RequestAuthentication:
apiVersion: security.istio.io/v1
kind: RequestAuthentication
metadata:
name: hello-world-auth
namespace: istio-hello-world
spec:
jwtRules:
- audiences:
- kubernetes
issuer: https://k8sou.192-168-2-119.nip.io/auth/idp/k8sIdp
jwksUri: https://k8sou.192-168-2-119.nip.io/auth/idp/k8sIdp/certs
outputPayloadToHeader: User-Info
selector:
matchLabels:
app: run-service
这个对象首先指定了 JWT 需要如何格式化才能被接受。我们这里有点作弊,直接利用我们的 Kubernetes JWT。让我们将这个对象与我们的 JWT 进行比较:
{
"iss": "https://k8sou.192-168-2-119.nip.io/auth/idp/k8sIdp",
"aud": "kubernetes",
"exp": 1630421193,
"jti": "JGnXlj0I5obI3Vcmb1MCXA",
"iat": 1630421133,
"nbf": 1630421013,
"sub": "mmosley",
"name": " Mosley",
"groups": [
"cn=group2,ou=Groups,DC=domain,DC=com",
"cn=k8s-cluster-admins,ou=Groups,DC=domain,DC=com"
],
"preferred_username": "mmosley",
"email": "mmosley@tremolo.dev"
}
我们 JWT 中的aud声明与RequestAuthentication中的受众对齐。iss声明与RequestAuthentication中的issuer对齐。如果这两个声明中的任何一个不匹配,Istio 将返回401 HTTP 错误代码,告诉你请求未经授权。
我们还指定了outputPayloadToHeader: User-Info,告诉 Istio 将用户信息作为 base64 编码的 JSON 头传递给下游服务,头部名称为User-Info。我们的服务可以使用这个头部来识别谁调用了它。当我们进入权限授权时,我们将详细介绍这一点。
此外,jwksUri部分指定了包含用于验证 JWT 的 RSA 公钥的 URL。这个 URL 可以通过首先访问发行者的 OIDC 发现 URL 并从jwks声明中获取。
请注意,RequestAuthentication对象会告诉 Istio JWT 需要采取什么形式,但不会指定需要包含哪些用户数据。我们将在接下来的授权部分进行介绍。
说到授权,我们希望确保强制要求 JWT,因此我们将创建一个非常简单的AuthorizationPolicy:
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: simple-hellow-world
namespace: istio-hello-world
spec:
action: ALLOW
rules:
- from:
- source:
requestPrincipals:
- '*'
selector:
matchLabels:
app: run-service
from部分表示必须有一个requestPrincipal。这告诉 Istio 必须有一个用户(在这种情况下,匿名用户不是用户)。requestPrincipal来自 JWT,表示用户。还有一个principal配置,但它表示调用我们 URL 的服务,在这种情况下会是ingressgateway。这告诉 Istio,用户必须通过 JWT 进行身份验证。
在我们有了策略之后,接下来可以进行测试。首先,测试没有用户的情况:
curl -v http://service.192-168-2-119.nip.io/
* Trying 192.168.2.119:80...
* TCP_NODELAY set
* Connected to service.192-168-2-119.nip.io (192.168.2.119) port 80 (#0)
> GET / HTTP/1.1
> Host: service.192-168-2-119.nip.io
> User-Agent: curl/7.68.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 403 Forbidden
< content-length: 19
< content-type: text/plain
< date: Tue, 31 Aug 2021 20:23:14 GMT
< server: istio-envoy
< x-envoy-upstream-service-time: 2
<
* Connection #0 to host service.192-168-2-119.nip.io left intact
我们可以看到请求被拒绝,并返回了403 HTTP 代码。收到403是因为 Istio 期望一个 JWT,但请求中没有 JWT。接下来,让我们按照第六章中的方式生成一个有效的令牌,《将身份验证集成到集群中》:
curl -H "Authorization: Bearer $(curl --insecure -u 'mmosley:start123' https://k8sou.apps.192-168-2-119.nip.io/k8s-api-token/token/user 2>/dev/null| jq -r '.token.id_token')" http://service.192-168-2-119.nip.io/
{"msg": "hello world!", "host": "run-service-785775bf98-6bbwt"}
现在,我们成功了!我们的 Hello World 服务现在要求进行正确的身份验证。接下来,我们将更新授权,要求来自 Active Directory 的特定组。
授权访问我们的服务
到目前为止,我们已经构建了一个服务,并确保用户在访问之前从身份提供者获得有效的 JWT。
现在,我们希望应用通常称为“粗粒度”授权的方式。这是应用层或服务级别的访问控制。它的意思是:“你通常可以使用此服务”,但并未说明你是否可以执行你希望进行的操作。对于我们的支票写作服务,你可能被授权写支票,但可能有更多的控制措施限制你可以为谁写支票。如果你负责企业中的企业资源规划(ERP)系统,你可能不应该能够为设施供应商写支票。我们将在下一部分讨论如何让服务管理这些业务级别的决策,但现在,我们将重点讨论服务级别的授权。
结果证明我们已经具备了所有需要的信息。之前,我们查看了mmosley用户的 JWT,其中包含多个声明。其中一个声明是groups声明。我们在第六章、将身份验证集成到集群中和第七章、RBAC 策略与审计中使用了该声明来管理对集群的访问。以类似的方式,我们将根据是否属于特定组来管理谁可以访问我们的服务。首先,我们将删除现有策略:
kubectl delete authorizationpolicy simple-hellow-world -n istio-hello-world
authorizationpolicy.security.istio.io "simple-hellow-world" deleted
禁用该策略后,你现在可以在没有 JWT 的情况下访问服务。接下来,我们将创建一个策略,要求你是 Active Directory 中cn=group2,ou=Groups,DC=domain,DC=com组的成员。
部署以下策略(在chapter17/coursed-grained-authorization/coursed-grained-az.yaml中):
---
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: service-level-az
namespace: istio-hello-world
spec:
action: ALLOW
selector:
matchLabels:
app: run-service
rules:
- when:
- key: request.auth.claims[groups]
values: ["cn=group2,ou=Groups,DC=domain,DC=com"]
该策略告诉 Istio,只有具有名为groups且值为cn=group2,ou=Groups,DC=domain,DC=com的声明的用户才能访问此服务。部署此策略后,你会发现你仍然可以以mmosley身份访问服务,匿名访问服务仍然失败。接下来,尝试以jjackson身份访问服务,使用相同的密码:
curl -H "Authorization: Bearer $(curl --insecure -u 'jjackson:start123' https://k8sou.192-168-2-119.nip.io/k8s-api-token/token/user 2>/dev/null| jq -r '.token.id_token')" http://service.192-168-2-119.nip.io/
RBAC: access denied
我们无法以jjackson身份访问该服务。如果我们查看jjackson的id_token,就能明白原因:
{
"iss": "https://k8sou.192-168-2-119.nip.io/auth/idp/k8sIdp",
"aud": "kubernetes",
"exp": 1630455027,
"jti": "Ae4Nv22HHYCnUNJx780l0A",
"iat": 1630454967,
"nbf": 1630454847,
"sub": "jjackson",
"name": " Jackson",
**"groups":****"cn=k8s-create-ns,ou=Groups,DC=domain,DC=com"****,**
"preferred_username": "jjackson",
"email": "jjackson@tremolo.dev"
}
查看声明后,jjackson并不是cn=group2,ou=Groups,DC=domain,DC=com组的成员。
现在我们能够告诉 Istio 如何限制服务的访问权限,仅允许有效用户访问,下一步是告诉我们的服务用户是谁。然后,我们将使用这些信息查找授权数据、记录操作,并代表用户进行操作。
告诉你的服务谁在使用它
在编写涉及用户的服务时,首先需要确定的是:“谁在尝试使用我的服务?”到目前为止,我们已经告诉 Istio 如何确定用户身份,但我们如何将该信息传递给我们的服务呢?我们的RequestAuthentication包含了配置选项outputPayloadToHeader: User-Info,该选项将用户身份验证令牌中的声明作为 base64 编码的 JSON 注入到 HTTP 请求的头部。这些信息可以从该头部提取,并由你的服务用来查找额外的授权数据。
我们可以通过我们构建的一个名为/headers的服务查看此头信息。该服务将返回传递给我们服务的所有头信息。让我们来看看:
curl -H "Authorization: Bearer $(curl --insecure -u 'mmosley:start123' https://k8sou.192-168-2-119.nip.io/k8s-api-token/token/user 2>/dev/null| jq -r '.token.id_token')" http://service.192-168-2-119.nip.io/headers 2>/dev/null | jq -r '.headers'
Host: service.192-168-2-119.nip.io
User-Agent: curl/7.75.0
Accept: */*
X-Forwarded-For: 192.168.2.112
X-Forwarded-Proto: http
X-Request-Id: 6397d068-537e-94b7-bf6b-a7c649db5b3d
X-Envoy-Attempt-Count: 1
X-Envoy-Internal: true
X-Forwarded-Client-Cert: By=spiffe://cluster.local/ns/istio-hello-world/sa/default;Hash=1a58a7d0abf62d32811c084a84f0a0f42b28616ffde7b6b840c595149d99b2eb;Subject="";URI=spiffe://cluster.local/ns/istio-system/sa/istio-ingressgateway-service-account
User-Info: eyJpc3MiOiJodHRwczovL2s4c291LjE5Mi0xNjgtMi0xMTkubmlwLmlvL2F1dGgvaWRwL2
s4c0lkcCIsImF1ZCI6Imt1YmVybmV0ZXMiLCJleHAiOjE2MzA1MTY4MjQsImp0aSI6InY0e
kpCNzdfRktpOXJoQU5jWDVwS1EiLCJpYXQiOjE2MzA1MTY3NjQsIm5iZiI6MTYzMDUxNj
Y0NCwic3ViIjoibW1vc2xleSIsIm5hbWUiOiIgTW9zbGV5IiwiZ3JvdXBzIjpbImNuPWdy
b3VwMixvdT1Hcm91cHMsREM9ZG9tYWluLERDPWNvbSIsImNuPWs4cy1jbHVzdGVyLWFkbW
lucyxvdT1Hcm91cHMsREM9ZG9tYWluLERDPWNvbSJdLCJwcmVmZXJyZWRfdXNlcm5hbWUi
OiJtbW9zbGV5IiwiZW1haWwiOiJtbW9zbGV5QHRyZW1vbG8uZGV2In0=
X-B3-Traceid: 28fb185aa113ad089cfac2d6884ce9ac
X-B3-Spanid: d40f1784a6685886
X-B3-Parentspanid: 9cfac2d6884ce9ac
X-B3-Sampled: 1
这里有几个头信息。我们关心的是User-Info。这是我们在RequestAuthentication对象中指定的头信息名称。如果我们从 base64 解码,我们会得到一些 JSON:
{
"iss": "https://k8sou.192-168-2-119.nip.io/auth/idp/k8sIdp",
"aud": "kubernetes",
"exp": 1630508679,
"jti": "5VoEAAgv1rkpf1vOJ9uo-g",
"iat": 1630508619,
"nbf": 1630508499,
"sub": "mmosley",
"name": " Mosley",
"groups": [
"cn=group2,ou=Groups,DC=domain,DC=com",
"cn=k8s-cluster-admins,ou=Groups,DC=domain,DC=com"
],
"preferred_username": "mmosley",
"email": "mmosley@tremolo.dev"
}
我们拥有的所有声明就像我们自己解码令牌时得到的一样。我们没有的是 JWT。这从安全角度来看非常重要。我们的服务不能泄露它没有的令牌。
现在我们知道如何确定用户身份了,让我们将其集成到一个简单的who-am-i服务中,该服务只会告诉我们用户是谁。首先,让我们看看我们的代码:
@app.route('/who-am-i')
def who_am_i():
user_info = request.headers["User-Info"]
user_info_json = base64.b64decode(user_info).decode("utf8")
user_info_obj = json.loads(user_info_json)
ret_val = {
"name": user_info_obj["sub"],
"groups": user_info_obj["groups"]
}
return json.dumps(ret_val)
这非常基础。我们从请求中获取头信息。接下来,我们将其从 base64 解码,最后,我们得到 JSON 并将其添加到返回中。如果这是一个更复杂的服务,这时我们可能会查询数据库来确定用户拥有的权限。
除了不要求我们的代码知道如何验证 JWT 外,这还使得我们更容易在与 Istio 隔离的情况下开发代码。打开run-service pod 中的 shell,尝试使用任何用户直接访问这个服务:
kubectl exec -ti run-service-785775bf98-g86gl -n istio-hello-world – bash
# export USERINFO=$(echo -n '{"sub":"marc","groups":["group1","group2"]}' | base64 -w 0)
# curl -H "User-Info: $USERINFO" http://localhost:8080/who-am-i
{"name": "marc", "groups": ["group1", "group2"]}
我们能够在不需要了解任何关于 Istio、JWT 或加密学的知识的情况下调用我们的服务!所有的工作都交给 Istio 处理,这样我们就可以专注于我们的服务。虽然这确实简化了开发,但如果存在一种方法可以将我们想要的任何信息注入到我们的服务中,这对安全性有什么影响呢?
让我们从一个没有 Istio sidecar 的命名空间直接尝试一下:
$ kubectl run -i --tty curl --image=alpine --rm=true – sh
/ # apk update add curl
/ # curl -H "User-Info $(echo -n '{"sub":"marc","groups":["group1","group2"]}' | base64 -w 0)" http://run-service.istio-hello-world.svc/who-am-i
RBAC: access denied
我们的RequestAuthentication和AuthorizationPolicy阻止了请求。尽管我们没有运行 sidecar,但我们的服务正在运行,它将所有流量重定向到 Istio,在那里我们的策略将被执行。那么如果我们尝试从一个有效请求中注入自己的User-Info头信息呢?
export USERINFO=$(echo -n '{"sub":"marc","groups":["group1","group2"]}' | base64 -w 0)
curl -H "Authorization: Bearer $(curl --insecure -u 'mmosley:start123' https://k8sou.192-168-2-119.nip.io/k8s-api-token/token/user 2>/dev/null| jq -r '.token.id_token')" -H "User-Info: $USERINFO" http://service.192-168-2-119.nip.io/who-am-i
{"name": "mmosley", "groups": ["cn=group2,ou=Groups,DC=domain,DC=com", "cn=k8s-cluster-admins,ou=Groups,DC=domain,DC=com"]}
再一次,我们试图覆盖用户身份的行为(在有效的 JWT 之外)被 Istio 阻止了。我们已经展示了 Istio 如何将用户身份注入到我们的服务中;现在,我们需要了解如何授权用户的权限。
授权用户权限
到目前为止,我们已经在不编写任何代码的情况下为我们的服务添加了很多功能。我们增加了基于令牌的身份验证和粗粒度的授权。我们知道用户是谁,并且已经确定在服务层面,他们被授权调用我们的服务。接下来,我们需要决定用户是否被允许执行他们尝试做的特定操作。这通常被称为细粒度授权或权限。在这一部分,我们将讨论多种方法,并讨论你应该如何选择一种方法。
服务中的授权
与粗粒度的授权和身份验证不同,权限通常不在服务网格层面进行管理。但这并不意味着不可能做到。我们会讨论在服务网格中如何做,但一般来说,这不是最好的做法。授权通常与业务数据相关,这些数据通常存储在数据库中。有时,数据库是一个通用的关系型数据库,比如 MySQL 或 SQL Server,但它也可以是任何类型的数据库。由于用于做出授权决策的数据通常由服务拥有者而非集群拥有者管理,因此在代码中直接做出权限决策通常更简单、更安全。
之前,我们讨论过在我们的开支票服务中,我们不希望负责 ERP 的人向设施供应商开支票。那决定数据在哪里呢?很可能就在你企业的 ERP 系统中。根据你的企业规模,可能是自研的应用程序,也可能是 SAP 或 Oracle。假设你希望 Istio 为我们的开支票服务做出授权决策。它如何获取这些数据?你认为负责 ERP 的人希望你,作为集群拥有者,直接访问他们的数据库吗?作为集群拥有者,你愿意承担这个责任吗?如果 ERP 出现问题,且有人指责你造成了问题怎么办?你有足够的资源证明你和你的团队并不负责吗?
事实证明,企业中那些受益于微服务设计管理方面的独立模块也会对集中式授权产生不利影响。在我们确定谁可以为特定供应商开支票的例子中,最简单的做法可能就是在我们的服务内部做出这个决定。这样,如果出现问题,Kubernetes 团队就不需要负责确定问题所在,而负责的人可以掌控自己的命运。
这并不是说更集中式的授权方式就没有优势。让各个团队实现自己的授权代码会导致使用不同的标准和方法。如果没有严格的控制,可能会导致合规性问题。让我们看看 Istio 如何提供一个更强大的授权框架。
在 Istio 中使用 OPA
使用在第十六章《Istio 简介》中讨论的 Envoy 过滤器功能,你可以将开放策略代理(OPA)集成到你的服务网格中来做出授权决策。我们在第十一章《使用开放策略代理扩展安全性》中讨论了 OPA。我们需要回顾一下关于 OPA 的几个关键点:
-
OPA 通常不会向外部数据存储请求授权决策。OPA 的许多优势要求它使用自己的内部数据库。
-
OPA 的数据库是非持久化的。当 OPA 实例终止时,必须重新填充数据。
-
OPA 的数据库没有集群功能。如果您有多个 OPA 实例,每个数据库必须独立更新。
要使用 OPA 验证我们的用户是否可以为特定供应商开具支票,OPA 要么需要能够直接从 JWT 中拉取数据,要么需要将 ERP 数据复制到其自己的数据库中。前者由于多种原因不太可能实现。首先,当您的身份提供者尝试与 ERP 通信时,您的集群与 ERP 之间的问题仍然存在。其次,运行身份提供者的团队需要知道包括正确的数据,这是一个困难的任务,而且他们不太可能有兴趣去做。最后,可能会有许多人员(从安全到 ERP 团队)不愿意将这些数据存储在一个被传递的令牌中。后者,即将数据同步到 OPA,更有可能成功。
有两种方法可以将您的授权数据从 ERP 同步到 OPA 数据库。第一种是通过推送数据。一个“机器人”可以将更新推送到每个 OPA 实例。这种方式下,ERP 所有者负责推送数据,您的集群仅作为消费者。然而,这种方式没有简单的实现方法,安全性也会成为一个问题,必须确保没有人推送虚假数据。另一种选择是编写一个拉取“机器人”,作为 OPA pod 的副车运行。这就是 GateKeeper 的工作方式。这里的优势是,您可以负责保持数据同步,而无需构建一个推送数据的安全框架。
在这两种情况下,您都需要了解存储的数据是否存在任何合规性问题。现在您已经拥有数据,数据在泄露中丢失会带来什么影响?这是您想承担的责任吗?
集中化授权服务在 Kubernetes 或甚至 RESTful API 出现之前就已经被讨论过了。它们甚至比 SOAP 和 XML 还要早!对于企业应用而言,这种方式从未真正成功,因为数据管理、所有权以及打破孤岛所带来的额外成本。如果您拥有所有数据,这种方式非常合适。而当微服务的主要目标之一是让各个孤岛能够更好地管理自己的开发时,强迫使用集中化的授权引擎很可能无法成功。
尽管如此,已经出现了集中化授权服务的趋势。这个趋势催生了许多商业公司和项目,这些项目位于 OPA 之外:
-
Cedar:亚马逊网络服务(Amazon Web Services)推出的一个开源项目,旨在创建一种新的策略语言。亚马逊还基于这种语言创建了一个服务:
github.com/cedar-policy。 -
Topaz:基于 OPA 和 Zanzibar 构建,Topaz 为 OPA 授权引擎提供了来自 Zanzibar 的基于关系的授权。此外,还有一个商业产品:
github.com/aserto-dev/topaz。 -
OpenFGA:另一个基于 Zanzibar 关系授权系统构建的引擎,由 Auth0/Okta 构建:
github.com/openfga。
我们不会深入探讨这些解决方案的细节;关键在于,已经有了一个明确的趋势,即构建授权解决方案,类似于外部身份验证已经成为一个产品和项目类别,历时数十年。
现在我们已经了解了创建集中式授权的一些问题,让我们为 Istio 使用 OPA 构建一个授权规则。
创建 OPA 授权规则
在本节前面,我们讨论了编写检查的规则。编写检查的常见规则是,编写支票的人也允许签署支票。这个规则称为“职责分离”。它旨在为潜在的有害和高成本的过程构建检查点。例如,如果允许一个员工既编写支票又签署支票,就没有机会有人询问支票是否用于有效的理由。
我们已经实现了一个验证用户组成员资格的 AuthorizationPolicy,但为了职责分离,我们需要实现一个规则来验证用户属于某个组,同时不属于另一个组。这样的复杂决策是通用 AuthorizationPolicy 无法实现的,因此我们需要构建我们自己的策略。我们可以使用 OPA 作为我们的授权引擎,同时指示 Istio 使用我们的策略。
在我们部署策略之前,让我们复查一下它。完整的策略位于 chapter17/opa/rego 中,并包含测试用例:
package istio.authz
import input.attributes.request.http as http_request
import input.parsed_path
default allow = false
contains_element(arr, elem) = true {
arr[_] = elem
} else = false { true }
verify_headers() = payload {
startswith(http_request.headers["authorization"], "Bearer ")
[header, payload, signature] := io.jwt.decode(trim_prefix(http_request.headers["authorization"], "Bearer "))
} else = false {true}
allow {
payload := verify_headers()
payload.groups
contains_element(payload.groups,"cn=k8s-cluster-admins,ou=Groups,DC=domain,DC=com")
not contains_element(payload.groups,"cn=group2,ou=Groups,DC=domain,DC=com")
}
allow {
payload := verify_headers()
payload.groups
payload.groups == "cn=k8s-cluster-admins,ou=Groups,DC=domain,DC=com"
}
我们移除了注释,以使代码更加简洁。该策略的基本内容是:
-
验证是否存在
authorization头部。 -
验证
authorization头部是否为Bearer令牌。 -
验证承载令牌是否为 JWT。
-
解析 JWT 并验证是否包含
groups声明。 -
如果
groups声明是一个列表,确保它包含k8s-cluster-admins组,但不包含group2组。 -
如果
groups声明不是列表,仅验证它是否为k8s-cluster-admins组。
我们正在验证授权头是否存在且格式正确,因为 Istio 不要求必须提供令牌才能通过身份验证。这是通过我们之前的 AuthorizationPolicy 实现的,无论是通过显式要求存在某个主体,还是通过隐式要求某个特定声明具有特定值。
一旦我们验证了授权头的格式正确,我们就会解析其负载。我们不会基于公钥、有效性或颁发者来验证 JWT,因为我们的 RequestAuthentication 对象会为我们完成这部分验证。我们只需要确保令牌存在。
最后,我们有两个潜在的 allow 策略。第一个是当 groups 声明是一个列表时,我们需要应用数组逻辑来检查是否存在正确的组,并且不存在被禁止的组。第二个 allow 策略会在 groups 声明不是列表,而只是一个单一值时触发。在这种情况下,我们只关心该组的值是否是我们的管理员组。
要部署此策略,请进入 chapter17/opa 目录并运行 deploy_opa_istio.sh。该脚本将启用授权并部署我们的策略:
-
配置 istiod:更新存储网格配置的
istioConfigMap,以启用envoyExtAuthzGrpc扩展提供程序。 -
部署 OPA 变异准入控制器:部署一个变异准入控制器,自动在 pod 上创建 OPA 实例,与服务一起运行。
-
部署我们的策略:我们之前创建的策略被作为
ConfigMap创建。 -
重新部署我们的服务:删除 pod,使其根据我们的授权策略重新创建。
-
一旦所有内容都部署完成,我们现在可以使用
curl命令验证我们的策略是否生效。如果我们现在尝试使用mmosley用户调用我们的头信息服务,它将失败,因为mmosley是k8s-cluster-admin组和group2组的成员:
$ curl -v -H "Authorization: Bearer $(curl --insecure -u 'mmosley:start123' https://k8sou.apps.192-168-2-96.nip.io/k8s-api-token/token/user 2>/dev/null| jq -r '.token.id_token')" http://service.192-168-2-96.nip.io/headers
* Trying 192.168.2.96:80...
* Connected to service.192-168-2-96.nip.io (192.168.2.96) port 80
> GET /headers HTTP/1.1
> Host: service.192-168-2-96.nip.io
> User-Agent: curl/8.4.0
> Accept: */*
> Authorization: Bearer …
>
< HTTP/1.1 403 Forbidden
< date: Tue, 27 Feb 2024 19:49:49 GMT
< server: istio-envoy
< content-length: 0
< x-envoy-upstream-service-time: 8
<
* Connection #0 to host service.192-168-2-96.nip.io left intact
然而,如果我们使用 pipeline_svc_account 用户,它会成功,因为该用户仅是 k8s-cluster-admin 组的成员:
$ curl -H "Authorization: Bearer $(curl --insecure -u 'pipeline_svc_account:start123' https://k8sou.apps.192-168-2-96.nip.io/k8s-api-token/token/user 2>/dev/null| jq -r '.token.id_token')" http://service.192-168-2-96.nip.io/headers
{"headers": "Host: service.192-168-2-96.nip.io\r\nUser-Agent: curl/8.4.0\r\nAccept: */*\r\nX-Forwarded-For: 192.168.3.6\r\nX-Forwar…
现在,我们可以构建比 Istio 的 AuthorizationPolicy 内置授权功能更复杂的策略。
确定了如何将授权集成到我们的服务中之后,我们需要回答的下一个问题是,如何安全地调用其他服务?
调用其他服务
我们编写了执行简单任务的服务,但当你的服务需要与另一个服务通信时怎么办?就像集群部署中的几乎所有其他选择一样,你有多种方式来验证对其他服务的身份。你选择哪种方式取决于你的需求。我们将首先介绍 OAuth2 标准的获取新令牌的方式及其如何与 Istio 配合使用。然后,我们将介绍一些应被视为反模式的替代方法,但你可能仍然选择使用它们。
使用 OAuth2 令牌交换
你的服务知道你的用户是谁,但需要调用另一个服务。你如何向第二个服务证明你的身份?OAuth2 规范(OpenID Connect 就是基于它)有 RFC 8693 – OAuth2 令牌交换用于此目的。基本思路是,你的服务将根据现有的用户,从身份提供商处获取新的令牌来进行服务调用。通过为你的远程服务调用获取新的令牌,你可以更容易地控制令牌的使用范围和使用者,从而更轻松地跟踪调用的身份验证和授权流程。以下图表提供了一个高层次的概述。

图 17.6:OAuth2 令牌交换序列
根据你的使用场景,有一些细节我们需要逐一说明:
-
用户向身份提供者请求
id_token。在这一部分的序列中,用户如何获得令牌并不重要。我们将在实验中使用 OpenUnison 中的工具。 -
假设你已通过身份验证并获得授权,身份提供者将为你提供一个带有
aud声明的id_token,该声明将被 Service-X 接受。 -
用户将
id_token作为持有令牌(bearer token)来调用 Service-X。不言而喻,Istio 将验证此令牌。 -
Service-X 代表用户向身份提供者请求 Service-Y 的令牌。执行此操作有两种可能的方法,一种是模拟身份,另一种是委托身份。我们将在本节稍后详细讨论这两种方法。你将向身份提供者发送原始的
id_token,以及一些用于识别服务的内容。 -
假设 Service-X 已获得授权,身份提供者将把一个新的
id_token发送给 Service-X,该id_token包含原始用户的属性,并且aud作用域限定为 Service-Y。 -
Service-X 在调用 Service-Y 时,将新的
id_token作为Authorization头部传递。再次强调,Istio 会验证id_token。
前面图示中的第 7 步和第 8 步在此并不重要。
如果你认为进行服务调用需要很多工作,那你是对的。这里有多个授权步骤在进行:
-
身份提供者授权用户生成一个作用域限定为 Service-X 的令牌。
-
Istio 验证该令牌,并确认其作用域正确,限定为 Service-X。
-
身份提供者授权 Service-X 为 Service-Y 获取令牌,并为我们的用户执行此操作。
-
Istio 验证 Service-X 为 Service-Y 使用的令牌是否具有正确的作用域。
这些授权点提供了拦截不正当令牌的机会,允许你创建短生命周期的令牌,这些令牌更难滥用且作用域更窄。例如,如果用于调用 Service-X 的令牌被泄露,它就无法单独用于调用 Service-Y。你仍然需要 Service-X 自己的令牌,才能获得用于 Service-Y 的令牌。这是攻击者需要额外采取的一步,才能控制 Service-Y。这也意味着需要突破多个服务,从而提供多层安全防护。这与我们在第十一章 使用 Open Policy Agent 扩展安全性 中讨论的深度防御相一致。了解了 OAuth2 令牌交换的高层次工作原理后,接下来的问题是,你的服务将如何向身份提供者进行身份验证?
验证你的服务
为了使令牌交换起作用,您的身份提供者需要知道原始用户是谁以及哪个服务想要代表用户交换令牌。在我们讨论的撰写支票服务示例中,您不希望提供今天午餐菜单的服务能够生成用于发放支票的令牌!通过确保您的身份提供者知道您的支票撰写服务与午餐菜单服务之间的区别,通过逐个验证每个服务来实现这一点。
Kubernetes 中运行的服务可以用三种方式进行身份验证:
-
使用 Pod 的
ServiceAccount令牌 -
使用 Istio 的 mTLS 能力
-
使用预共享的“客户端密钥”
在本节的其余部分,我们将专注于选项 #1,即使用 Pod 的内置 ServiceAccount 令牌。该令牌默认为每个运行中的 Pod 提供。您可以通过将其提交给 API 服务器的 TokenReview 服务或将其视为 JWT 并根据 API 服务器发布的公钥进行验证来验证该令牌。
在我们的示例中,我们将使用 TokenReview API 来测试传入的 ServiceAccount 令牌是否与 API 服务器匹配。这是最具向后兼容性的方法,并支持集成到您的集群中的任何类型的令牌。例如,如果您部署在具有自己 IAM 系统的托管云中,该系统挂载令牌,您也可以使用它。每次需要验证令牌时,都会将其发送到 API 服务器,这可能会对您的 API 服务器产生相当大的负载。
在 第六章,将认证集成到您的集群中中讨论的 TokenRequest API 可以用来减少这种额外的负载。而不是使用 TokenReview API,我们可以调用 API 服务器的发行者端点来获取适当的令牌验证公钥,并使用该密钥验证令牌的 JWT。虽然这很方便且扩展性更好,但它确实有一些缺点:
-
从 1.21 开始,
ServiceAccount令牌使用TokenRequestAPI 进行挂载,但生存期长达一年或更长时间。您可以手动将其更改为短至 10 分钟。 -
直接使用公钥对 JWT 进行验证不会告诉您 Pod 是否仍在运行。如果与已删除 Pod 关联了
ServiceAccount令牌,则TokenReviewAPI 将失败,从而增加了额外的安全层。 -
启用此功能需要在您的集群中启用匿名身份验证,这可以用来通过错误配置的 RBAC 或潜在的错误提升权限。
我们不会使用 Istio 的 mTLS 功能,因为它不像令牌那样灵活。mTLS 主要用于集群内部通信,因此如果我们的身份提供者位于集群外部,使用起来会更为困难。此外,由于 mTLS 需要点对点连接,任何 TLS 终止点都会破坏其使用。由于企业系统托管自己的证书是很罕见的,即使在 Kubernetes 外部,也很难实现集群服务与身份提供者之间的 mTLS。
最后,我们不会在服务和身份提供者之间使用共享密钥,因为我们不需要这样做。共享密钥只在没有其他方式为工作负载赋予身份时才需要。由于 Kubernetes 为每个 Pod 提供自己的身份,因此我们不需要使用客户端密钥来识别我们的服务。
现在我们知道我们的服务将如何向身份提供者进行身份验证,接下来让我们通过一个使用 OAuth2 令牌交换的示例,来安全地从一个服务调用另一个服务。
部署和运行支票写入服务
在讲解了使用令牌交换安全调用服务的理论之后,让我们部署一个示例的支票写入服务。当我们调用这个服务时,它将调用另外两个服务。第一个服务 check-funds 将使用 OAuth2 令牌交换的假冒配置,而第二个服务 pull-funds 将使用委托。我们将分别讲解每一个。首先,使用 Helm 部署一个身份提供者。进入 chapter17 目录并运行:
helm install openunison-service-auth openunison-service-auth -n openunison
NAME: openunison-service-auth
LAST DEPLOYED: Mon Sep 13 01:08:09 2021
NAMESPACE: openunison
STATUS: deployed
REVISION: 1
TEST SUITE: None
我们不会深入讨论 OpenUnison 的配置。可以简单地说,这将为我们的服务设置一个身份提供者,并提供获取初始令牌的方式。接下来,部署 write-checks 服务:
cd write-checks/
./deploy_write_checks.sh
getting oidc config
getting jwks
namespace/write-checks created
configmap/service-source created
deployment.apps/write-checks created
service/write-checks created
gateway.networking.istio.io/service-gateway created
virtualservice.networking.istio.io/service-vs created
requestauthentication.security.istio.io/write-checks-auth created
authorizationpolicy.security.istio.io/service-level-az created
在本章的第一个示例之后,这应该看起来非常熟悉。我们将我们的服务部署为 Python 并放入 ConfigMap,以及在之前的服务中创建的相同 Istio 对象。唯一的主要区别是在我们的 RequestAuthentication 对象中:
apiVersion: security.istio.io/v1beta1
kind: RequestAuthentication
metadata:
name: write-checks-auth
namespace: write-checks
spec:
jwtRules:
- audiences:
- users
- checkfunds
- pullfunds
forwardOriginalToken: true
issuer: https://k8sou.apps.192-168-2-119.nip.io/auth/idp/service-idp
jwksUri: https://k8sou.apps.192-168-2-119.nip.io/auth/idp/service-idp/certs
outputPayloadToHeader: User-Info
selector:
matchLabels:
app: write-checks
还有一个额外的设置,forwardOriginalToken,它告诉 Istio 将用于认证调用的原始 JWT 发送给服务。我们需要这个令牌来证明我们应该尝试执行令牌交换。如果无法提供原始令牌,你就无法请求新令牌。这可以防止拥有服务 Pod 访问权限的人仅凭服务的 ServiceAccount 请求令牌。
在本章前面,我们提到过如果我们没有令牌,就不应该泄露它,因此我们不应该访问原始令牌。如果我们不需要它来为另一个服务获取令牌,那这是正确的。遵循最小权限原则,如果不需要,我们不应该转发令牌。在这种情况下,我们需要它进行令牌交换,因此为了实现更安全的服务间调用,增加的风险是值得的。
在我们部署了示例的支票写入服务后,接下来让我们运行它并倒推。和之前的示例一样,我们将使用curl来获取令牌并调用我们的服务。在chapter17/write-checks中,运行call_service.sh:
./call_service.sh
{
"msg": "hello world!",
"host": "write-checks-84cdbfff74-tgmzh",
"user_jwt": "...",
"pod_jwt": "...",
"impersonated_jwt": "...",
"call_funds_status_code": 200,
"call_funds_text": "{\"funds_available\": true, \"user\": \"mmosley\"}",
"actor_token": "...",
"delegation_token": "...",
"pull_funds_text": "{\"funds_pulled\": true, \"user\": \"mmosley\", \"actor\": \"system:serviceaccount:write-checks:default\"}"
}
你看到的输出是调用/write-check后的结果,接着调用了/check-funds和/pull-funds。让我们逐步讲解每个调用、生成的令牌以及生成它们的代码。
使用模拟认证
我们这里说的模拟认证不同于第六章中你在将身份认证集成到集群中时使用的模拟认证。虽然概念类似,但这次是针对令牌交换的。当/write-check需要获取令牌来调用/check-funds时,它会代表我们的用户mmosley向 OpenUnison 请求一个令牌。模拟认证的关键在于,生成的令牌中没有包含请求客户端的任何引用。/check-funds服务并不知道它接收到的令牌并不是用户自己获取的。从倒推的角度看,impersonated_jwt是我们服务调用响应中返回的内容,它是/write-check用来调用/check-funds的令牌。以下是将结果粘贴到jwt.io后的载荷:
{
"iss": "https://k8sou.192-168-2-119.nip.io/auth/idp/service-idp",
"aud": "checkfunds",
"exp": 1631497059,
"jti": "C8Qh8iY9FJdFzEO3pLRQzw",
"iat": 1631496999,
"nbf": 1631496879,
"nonce": "bec42c16-5570-4bd8-9038-be30fd216016",
"sub": "mmosley",
"name": " Mosley",
"groups": [
"cn=group2,ou=Groups,DC=domain,DC=com",
"cn=k8s-cluster-admins,ou=Groups,DC=domain,DC=com"
],
"preferred_username": "mmosley",
"email": "mmosley@tremolo.dev",
"amr": [
"pwd"
]
}
这里有两个重要的字段是sub和aud。sub字段告诉/check-funds用户是谁,而aud字段则告诉 Istio 哪些服务可以使用此令牌。将其与user_jwt响应中的原始令牌载荷进行对比:
{
"iss": "https://k8sou.192-168-2-119.nip.io/auth/idp/service-idp",
**"aud"****:****"users"****,**
"exp": 1631497059,
"jti": "C8Qh8iY9FJdFzEO3pLRQzw",
"iat": 1631496999,
"nbf": 1631496879,
**"sub"****:****"mmosley"****,**
"name": " Mosley",
"groups": [
"cn=group2,ou=Groups,DC=domain,DC=com",
"cn=k8s-cluster-admins,ou=Groups,DC=domain,DC=com"
],
"preferred_username": "mmosley",
"email": "mmosley@tremolo.dev",
"amr": [
"pwd"
]
}
原始的sub是相同的,但aud不同。原始的aud是针对用户的,而模拟的aud是针对checkfunds的。这就是模拟的令牌与原始令牌之间的区别。尽管我们的 Istio 部署配置为接受同一服务的两种 audience,但在大多数生产集群中,这并不是一个保证。当我们调用/check-funds时,你会看到在输出中,我们回显了令牌的用户mmosley。
现在我们已经看到了最终产品,接下来我们来看一下如何获得它。首先,我们获取用于调用/write-check的原始 JWT:
# let's first get the original JWT. We'll
# use this as an input for impersonation
az_header = request.headers["Authorization"]
user_jwt = az_header[7:]
一旦我们拥有了原始的 JWT,我们还需要 Pod 的ServiceAccount令牌:
# next, get the pod's ServiceAccount token
# so we can identify the pod to the IdP for
# an impersonation token
pod_jwt = Path('/var/run/secrets/kubernetes.io/serviceaccount/token').read_text()
现在我们拥有了获取模拟令牌所需的一切。我们将创建一个 POST 主体和一个Authorization头部,以便向 OpenUnison 认证我们并获取令牌:
# with the subject (user) jwt and the pod
# jwt we can now request an impersonated
# token for our user from openunison
impersonation_request = {
"grant_type":"urn:ietf:params:oauth:grant-type:token-exchange",
"audience":"checkfunds",
"subject_token":user_jwt,
"subject_token_type":"urn:ietf:params:oauth:token-type:id_token",
"client_id":"sts-impersonation"
}
impersonation_headers = {
"Authorization": "Bearer %s" % pod_jwt
}
我们创建的第一个数据结构是一个 HTTP POST 的主体,它将告诉 OpenUnison 为clientfunds aud生成一个模拟令牌,使用我们现有的用户(user_jwt)。OpenUnison 将通过验证在Authorization头部作为Bearer令牌发送的 JWT 来认证我们的服务,使用的是TokenReview API。
OpenUnison 将应用其内部策略,验证我们的服务是否能够为mmosley生成clientfunds受众的令牌,然后生成access_token、id_token和refresh_token。我们将使用id_token来调用/check-funds:
resp = requests.post("https://k8sou.apps.IPADDR.nip.io/auth/idp/service-idp/token",verify=False,data=impersonation_request,headers=impersonation_headers)
response_payload = json.loads(resp.text)
impersonated_id_token = response_payload["id_token"]
# with the impersonated user's id_token, call another
# service as that user
call_funds_headers = {
"Authorization": "Bearer %s" % impersonated_id_token
}
resp = requests.get("http://write-checks.IPADDR.nip.io/check-funds",verify=False,headers=call_funds_headers)
由于最终的 JWT 并未提及模拟身份,我们如何将请求追溯到我们的服务呢?希望你已经将日志发送到一个集中式日志系统。如果我们查看模拟令牌的 jti 声明,就可以在 OpenUnison 的日志中找到模拟调用:
INFO AccessLog - [AzSuccess] - service-idp - https://k8sou.apps.192-168-2-119.nip.io/auth/idp/service-idp/token - username=system:serviceaccount:write-checks:default,ou=oauth2,o=Tremolo - client 'sts-impersonation' impersonating 'mmosley', jti : 'C8Qh8iY9FJdFzEO3pLRQzw'
因此,我们至少有一种方式将它们联系起来。我们可以看到,我们的 Pod 服务账户被授权为 mmosley 创建了模拟令牌。
在通过模拟身份的示例后,让我们接下来讲解令牌委托。
使用委托
在上一个示例中,我们使用模拟身份为用户生成了一个新令牌,但我们的下游服务并未意识到模拟身份的发生。委托不同之处在于,令牌携带了关于原始用户和请求它的服务或演员的信息。
这意味着被调用的服务既知道调用者的来源,也知道发起调用的服务。我们可以在 call_service.sh 执行结果的响应中的 pull_funds_text 值中看到这一点。它包含了我们原始的用户 mmosley 和发起调用的服务的 ServiceAccount,system:serviceaccount:write-checks:default。就像模拟身份一样,让我们看看生成的令牌:
{
"iss": "https://k8sou.apps.192-168-2-119.nip.io/auth/idp/service-idp",
"aud": "pullfunds",
"exp": 1631497059,
"jti": "xkaQhMgKgRvGBqAsOWDlXA",
"iat": 1631496999,
"nbf": 1631496879,
"nonce": "272f1900-f9d9-4161-a31c-6c6dde80fcb9",
"sub": "mmosley",
"amr": [
"pwd"
],
"name": " Mosley",
"groups": [
"cn=group2,ou=Groups,DC=domain,DC=com",
"cn=k8s-cluster-admins,ou=Groups,DC=domain,DC=com"
],
"preferred_username": "mmosley",
"email": "mmosley@tremolo.dev",
**"act"****:** **{**
**"sub"****:****"system:serviceaccount:write-checks:default"****,**
**"amr"****:** **[**
**"k8s-sa"**
**]****,**
**.**
**.**
**.**
**}**
}
除了标识用户为 mmosley 的声明外,还有一个 act 声明,用来标识 /write-checks 使用的 ServiceAccount。我们的服务可以根据这个声明做出额外的授权决策,或者仅仅记录下来,表明它接收到的令牌是委托给了另一个服务。为了生成这个令牌,我们首先获取原始主体的 JWT 和 Pod 的 ServiceAccount 令牌。
客户端在调用 OpenUnison 获取委托令牌之前,首先需要通过使用 client_credentials 授权方式获取一个演员令牌。这将为我们获取一个最终会进入 act 声明的令牌:
client_credentials_grant_request = {
"grant_type": "client_credentials",
"client_id" : "sts-delegation"
}
delegation_headers = {
"Authorization": "Bearer %s" % pod_jwt
}
resp = requests.post("https://k8sou.IPADDR.nip.io/auth/idp/service-idp/token",verify=False,data=client_credentials_grant_request,headers=delegation_headers)
response_payload = json.loads(resp.text)
actor_token = response_payload["id_token"]
我们使用 Pod 的原生身份对 OpenUnison 进行身份验证。OpenUnison 返回一个 access_token 和一个 id_token,但我们只需要 id_token。拿到我们的演员令牌后,现在可以获取我们的委托令牌:
delegation_request = {
"grant_type":"urn:ietf:params:oauth:grant-type:token-exchange",
"audience":"pullfunds",
"subject_token":user_jwt,
"subject_token_type":"urn:ietf:params:oauth:token-type:id_token",
"client_id":"sts-delegation",
"actor_token": actor_token,
"actor_token_type": "urn:ietf:params:oauth:token-type:id_token"
}
resp = requests.post("https://k8sou.IPADDR.nip.io/auth/idp/service-idp/token",verify=False,data=delegation_request)
response_payload = json.loads(resp.text)
delegation_token = response_payload["id_token"]
类似于模拟身份,在这个调用中,我们不仅发送了原始用户的令牌(user_jwt),还发送了我们刚从 OpenUnison 获得的 actor_token。我们同样不会发送 Authorization 头,因为 actor_token 已经完成了我们的身份验证。最终,我们能够使用返回的令牌来调用 /pull-funds。
现在我们已经看过了使用模拟身份和委托的最正确调用服务的方式,让我们来看一些反模式,以及为什么你不应该使用它们。
在服务之间传递令牌
在上一节中,我们使用了身份提供者生成冒充或委托令牌,而这种方法跳过了这些步骤,直接从服务传递原始令牌到另一个服务。这是一种简单的实现方法,也带来了更大的暴露范围。如果令牌泄露了(而且由于现在它被传递到多个服务,泄露的可能性大大增加),那么不仅仅是一个服务暴露了,你还暴露了所有信任该令牌的服务。
虽然使用 OAuth2 令牌交换需要更多的工作,但如果令牌泄露,它将限制你的影响范围。接下来,我们将看看如何简单地告诉下游服务是谁在调用它。
使用简单的冒充
之前的服务到服务调用示例依赖第三方为用户生成令牌,而直接冒充则是你的服务代码使用服务账户(广义上的服务账户,而非 Kubernetes 版本)来调用第二个服务,并且只是告诉该服务用户是谁,作为调用的输入。例如,/write-check 可以直接使用 Pod 的 ServiceAccount 令牌来调用 /check-funds,并带上包含用户 ID 的参数。像下面这样就能工作:
call_headers = {
"Authorization": "Bearer %s" % pod_jwt
}
resp = requests.post("https://write-checks.IPADDR.nip.io/check-funds?user=mmosley",verify=False,data=impersonation_request,headers=call_headers)
这再次是一个非常简单的问题。你可以告诉 Istio 认证一个 Kubernetes ServiceAccount。只需两行代码,就能完成之前需要 15 到 20 行代码才能实现的功能。就像服务之间传递令牌一样,这种方法在多个方面仍然存在暴露的风险。首先,如果有人获取了我们服务使用的 ServiceAccount,他们可以不受检查地冒充任何人。使用令牌服务可以确保即便服务账户被泄露,也不会被用来冒充其他人。
你可能会发现这种方法与我们在第六章《将身份认证集成到集群》中使用的冒充非常相似。你没错。虽然这使用了相同的机制,即 ServiceAccount 和一些参数来指定用户是谁,但 Kubernetes 用于 API 服务器的冒充方式通常被称为协议转换。当你从一种协议(如 OpenID Connect)转换到另一种协议(如 Kubernetes 服务账户)时,会使用这种方式。正如我们在第五章中讨论的,Kubernetes 冒充可以通过多种控制机制来实现,包括使用 NetworkPolicies、RBAC 和 TokenRequest API。这也是一个比通用服务更加孤立的用例。
我们已经走过了多种服务间相互调用和认证的方式。虽然这可能不是最简单的方式来确保服务间的访问安全,但它会限制令牌泄露的影响。现在我们知道了服务间的通信方式,最后一个我们需要讨论的话题是 Istio 和 API 网关之间的关系。
我需要 API 网关吗?
如果你正在使用 Istio,是否还需要 API 网关?过去,Istio 主要关注为服务路由流量。它将流量引入集群并决定将其路由到哪里。API 网关则通常更多关注应用级功能,如身份验证、授权、输入验证和日志记录。
例如,在本章前面,我们将模式输入验证确定为一个需要在每次调用时重复执行的过程,而且不应该需要手动执行。这一点非常重要,因为它可以防止利用意外输入进行攻击,同时也能为开发者提供更好的体验,在集成过程中更早地向开发者提供反馈。这是 API 网关中的常见功能,但在 Istio 中并不可用。
另一个 Istio 没有内建,但在 API 网关中常见的功能是记录身份验证和授权的决策和信息。在本章中,我们利用 Istio 内建的身份验证和授权来验证服务访问,但 Istio 并未记录这些决策,仅仅记录了是否做出了决策。它不会记录谁访问了特定的 URL,只会记录从哪里访问。记录谁从身份角度访问了服务,则交由每个独立的服务来处理。这是 API 网关的常见功能。
最后,API 网关能够处理更复杂的转换。网关通常会提供映射输入和输出的功能,甚至可以与遗留系统进行集成。
这些功能都可以通过 Istio 进行集成,无论是直接集成还是通过 Envoy 过滤器。我们在查看使用 OPA 来做出比AuthorizationPolicy对象提供的更复杂的授权决策时就看到过一个例子。然而,在过去的几个版本中,Istio 进一步进入了传统 API 网关的领域,而 API 网关也开始承担更多服务网格的功能。我猜想,未来这些系统之间会有相当大的重叠,但在写这篇文章时,Istio 尚未具备实现所有 API 网关功能的能力。
我们在构建 Istio 服务网格的服务过程中经历了一段不小的旅程。现在,你应该已经掌握了开始在你自己的集群中构建服务所需的工具。
总结
在本章中,我们学习了单体应用和微服务在 Istio 中的运行方式。我们探讨了何时以及为何使用这两种方法。我们部署了一个单体应用,并确保单体应用的会话管理正常工作。然后我们开始部署微服务,进行请求身份验证、授权请求,最后讨论了服务如何安全地进行通信。最后,我们讨论了在使用 Istio 时,API 网关是否仍然必要。
Istio 可能比较复杂,但如果使用得当,它可以提供相当强大的功能。本章未涉及的是如何构建容器并管理服务的部署。我们将在下一章,第十八章,提供多租户平台中讨论这个问题。
问题
-
对错:Istio 是一个 API 网关。
-
对
-
错
-
答案:b – 错。Istio 是一个服务网格,虽然它具有网关的许多功能,但并不包含所有功能(例如,模式检查)。
-
我是否应该始终将应用程序构建为微服务?
-
显然——这是正确的方式。
-
只有当微服务架构与你组织的结构和需求相匹配时,才应考虑使用微服务。
-
不是。微服务带来的麻烦远大于它们的价值。
-
什么是微服务?
-
答案:b – 微服务在你的团队能够充分利用其粒度时非常有用。
-
什么是单体应用?
-
一个看起来像由一个未知制造者用一块材料做成的大物件
-
一个自包含的应用程序
-
一个无法在 Kubernetes 上运行的系统
-
一家新创公司的产品
-
答案:b – 单体应用是一个自包含的应用程序,可以在 Kubernetes 上运行得很好。
-
如何在 Istio 中授权访问你的服务?
-
你可以在 Istio 中编写规则,通过令牌中的声明限制访问。
-
你可以将 OPA 与 Istio 集成,以做出更复杂的授权决策。
-
你可以在代码中嵌入复杂的授权决策。
-
上述所有。
-
答案:d – 从技术角度来看,这些都是有效的策略。每种情况不同,因此需要查看每个选项,判断哪个最适合你!
-
对错:代表用户调用服务而不进行令牌交换是一种安全的方法。
-
对
-
错
-
答案:b. 错 – 如果不使用令牌交换来获取新令牌以便用户使用下一个服务时,你会让自己面临各种攻击,因为你无法限制或追踪调用。
-
对错:Istio 支持粘性会话。
-
对
-
错
-
答案:a. 对 – 它们不是默认的,但支持它们。
加入我们本书的 Discord 空间
加入本书的 Discord 工作空间,参与每月一次的 提问与解答 会话,与作者互动:

第十八章:提供多租户平台
本书中的每一章,直到这一章,都集中在你的集群基础设施上。我们探讨了如何部署 Kubernetes,如何保护它,以及如何监控它。而我们没有讨论的是如何部署应用程序。
在这些最终章节中,我们将基于我们在 Kubernetes 上学到的内容,构建一个应用程序部署平台。我们将根据一些常见的企业需求来构建我们的平台。如果我们无法直接实现某个需求,因为在 Kubernetes 上构建一个平台可能会成为一本完整的书,我们会指出这一点并提供一些见解。
在本章中,我们将涵盖以下主题:
-
设计一个管道
-
设计我们的平台架构
-
使用基础设施即代码进行部署
-
自动化租户入驻
-
构建内部开发者平台的考虑因素
到本章结束时,你将拥有一个良好的概念性起点,帮助你在 Kubernetes 上构建自己的 GitOps 平台。我们将在本章中介绍的概念将推动我们在最后一章中构建内部开发者门户的方式。
技术要求
本章将全部是理论和概念。我们将在最后一章中讨论实现部分。
设计一个管道
管道这个术语在 Kubernetes 和 DevOps 领域被广泛使用。简单来说,管道是一个通常是自动化的过程,它将代码处理并使其运行。这个过程通常包括以下内容:

图 18.1:一个简单的管道
让我们快速了解这个过程中的步骤:
-
将源代码存储在一个中央代码库中,通常是 Git
-
当代码提交时,构建它并生成工件,通常是一个容器
-
告诉平台——在这个例子中是 Kubernetes——推出新的容器并关闭旧的容器
这就是管道的基本形式,在大多数部署中并没有太大用途。除了构建和部署代码外,我们还需要确保扫描容器中的已知漏洞。我们可能还需要在进入生产环境之前对容器进行一些自动化测试。在企业部署中,通常还会有合规要求,需要有人对迁移到生产环境负责。考虑到这些因素,管道开始变得更加复杂。

图 18.2:具有常见企业需求的管道
管道增加了一些额外的步骤,但它仍然是线性的,从一个起点开始,一个提交。这也是非常简化且不现实的。你应用所依赖的基础容器和库在不断更新,因为新的常见漏洞和暴露(CVE),即识别和归类安全漏洞的常见方法,被发现并修补。除了有开发人员为新的需求更新应用代码之外,你还需要一个系统来扫描代码和基础容器的可用更新。这些扫描器监视你的基础容器,并在新的基础容器准备好时触发构建。虽然扫描器可以调用 API 来触发管道,但你的管道已经在等待 Git 仓库做某些事情,因此更好的做法是直接向 Git 仓库添加一个提交或拉取请求来触发管道。

图 18.3:集成了扫描器的管道
这意味着你的应用代码和操作更新都被 Git 跟踪。Git 现在不仅是应用代码的真实来源,也是操作更新的来源。当需要进行审计时,你已经有了现成的变更日志!如果你的政策要求你将变更输入变更管理系统,只需从 Git 导出这些变更即可。
到目前为止,我们一直专注于应用代码,并仅将Rollout放在管道的最后步骤。最终的发布步骤通常意味着用我们新构建的容器来修补部署或 StatefulSet,让 Kubernetes 执行启动新 Pod 和缩减旧 Pod 的任务。这可以通过一个简单的 API 调用来完成,但我们如何跟踪和审计这个变化呢?真实的来源是什么?
我们在 Kubernetes 中的应用定义为存储在 etcd 中的一系列对象,这些对象通常通过 YAML 文件表示为代码。为什么不把这些文件也存储在 Git 仓库中呢?这让我们可以享受与存储应用代码在 Git 中相同的好处。我们有了应用源代码和应用操作的统一真实来源!现在,我们的管道涉及更多的步骤。

图 18.4:GitOps 管道
在这个图示中,我们的发布会更新 Git 仓库中的应用 Kubernetes YAML。集群中的一个控制器会监视 Git 的更新,一旦看到更新,就会将集群与 Git 中的内容保持同步。它还可以检测到集群中的漂移,并将其恢复到与真实来源一致的状态。
这种专注于 Git 的方式称为GitOps。其理念是应用的所有工作都通过代码完成,而不是直接通过 API。你对这一理念的严格程度将决定你的平台如何构建。接下来,我们将探讨意见如何塑造你的平台。
强制性平台
Google 的开发者倡导者及 Kubernetes 领域的领导者 Kelsey Hightower 曾说:“Kubernetes 是构建平台的平台。它是一个更好的起点,而非最终目标。”当你看到基于 Kubernetes 的供应商和项目的生态时,它们都有自己对如何构建系统的看法。例如,Red Hat 的 OpenShift 容器平台(OCP)希望成为一个多租户企业部署的一站式平台。它构建了我们讨论过的流水线的很大一部分。你定义一个由提交触发的流水线,构建一个容器并将其推送到内部注册表中,随后触发新容器的发布。命名空间是租户的边界。Canonical 是一个极简主义的发行版,不包含任何流水线组件。像 Amazon、Azure 和 Google 这样的托管供应商提供集群的构建块和流水线的托管构建工具,但还是将平台的构建工作留给你。
关于使用哪个平台没有正确的答案。每个平台都有其独特的观点,适合你的部署的平台将取决于你的具体需求。根据企业的规模,看到多个平台部署也不足为奇!
在了解了观点明确的平台后,让我们探讨一下构建流水线的安全影响。
保护你的流水线
根据你的起始点,这个过程可能会迅速变得复杂。你的流水线中有多少是一个集成系统,或者它是否能用一个形象的美国俚语来形容,涉及到胶带?即使在所有组件都存在的平台中,将它们连接起来通常也意味着要构建一个复杂的系统。流水线中的大多数系统都将包含可视化组件,通常这个可视化组件是一个仪表盘。用户和开发者可能需要访问该仪表盘。你不希望为所有这些系统维护单独的账户,对吧?你会希望为流水线的所有组件提供一个统一的登录入口和门户。
在确定如何验证使用这些系统的用户后,接下来的问题是如何自动化发布流程。流水线的每个组件都需要配置。它可以简单到通过 API 调用创建的一个对象,也可以复杂到将 Git 仓库和构建过程通过 SSH 密钥连接起来以自动化安全性。在这样复杂的环境中,手动创建流水线基础设施将导致安全漏洞,还会导致无法管理的系统。自动化该过程并提供一致性将帮助你既保护基础设施,又保持其可维护性。
最后,从安全角度理解 GitOps 对我们集群的影响非常重要。我们在第六章《将身份验证集成到集群》中讨论了如何验证管理员和开发者使用 Kubernetes API 并授权访问不同 API,在第七章《RBAC 策略与审计》中进一步讨论了这一点。如果有人能够提交一个 RoleBinding,将他们赋予某个命名空间的 admin ClusterRole,并且 GitOps 控制器会自动将其推送到集群中,这会产生什么影响?在设计你的平台时,考虑开发者和管理员如何与平台互动是很重要的。虽然说“让每个人都与其应用程序的 Git 注册库互动”很有诱惑力,但这意味着将大量请求的负担放在你作为集群所有者身上。正如我们在第七章《RBAC 策略与审计》中讨论的,这可能会使你的团队成为企业中的瓶颈。了解你的客户,在这种情况下,了解他们如何希望与操作系统互动,即使这不是你最初的设计意图,也是至关重要的。
在我们讨论了 GitOps 和流水线的一些安全方面之后,让我们来探索一个典型平台的需求,以及我们如何构建它。
构建我们平台的需求
Kubernetes 部署,特别是在企业环境中,通常会有以下基本需求:
-
开发和测试环境:至少需要两个集群来测试更改在集群层面上对应用程序的影响
-
开发者沙箱:开发者可以在其中构建容器并进行测试,而不必担心对共享命名空间的影响
-
源代码控制与问题跟踪:存储代码并跟踪开放任务的地方
除了这些基本需求,企业通常还会有额外的需求,比如定期的访问审查、基于策略的访问限制,以及分配责任的工作流,这些责任可能会影响共享环境。最后,你还需要确保政策到位,以保护节点安全。
对于我们的平台,我们希望尽可能涵盖这些需求。为了更好地自动化部署到我们的平台,我们将定义每个应用程序具有以下特点:
-
开发命名空间:开发者是管理员
-
生产命名空间:开发者是查看者
-
源代码控制项目:开发者可以进行分叉
-
构建过程:由 Git 更新触发
-
部署过程:由 Git 更新触发
此外,我们希望开发者拥有自己的沙箱,以便每个用户可以为开发分配自己的命名空间。
为了提供对每个应用程序的访问,我们将定义三个角色:
-
所有者:作为应用程序所有者的用户可以批准其他角色在其应用程序内的访问权限。此角色分配给应用程序请求者,并且可以由应用程序所有者分配。所有者还负责推动开发和生产中的更改。
-
开发人员:这些是能够访问应用程序源代码控制并可以管理应用程序开发命名空间的用户。他们可以查看生产命名空间中的对象,但无法编辑任何内容。任何用户都可以申请此角色,应用程序所有者会批准。
-
运维:这些用户具备与开发人员相同的能力,但还可以根据需要对生产命名空间进行更改。任何用户都可以申请此角色,应用程序所有者会批准。
我们还将创建一些跨环境的角色:
-
系统审批者:拥有此角色的用户可以批准访问任何系统范围的角色。
-
集群管理员:此角色专门用于管理我们的集群和构成我们平台的应用程序。任何人都可以申请此角色,但必须经过系统审批者角色成员的批准。
-
开发人员:任何登录的用户都会在开发集群上获得自己的开发命名空间。这些命名空间不能由其他用户申请访问。这些命名空间与任何 CI/CD 基础设施或 Git 仓库没有直接连接。
即使在我们非常简单的平台上,我们也有六个角色需要映射到构成我们流水线的应用程序。每个应用程序都有自己的身份验证和授权过程,这些角色需要与之映射。这仅仅是为什么自动化对集群安全如此重要的一个例子。基于电子邮件请求手动配置这些访问权限可能很快变得难以管理。
开发人员在使用应用程序时所需遵循的工作流程将与我们之前设计的 GitOps 流程一致:
-
应用程序所有者将申请创建一个应用程序。一旦批准,将为应用程序代码和 Kubernetes 清单创建一个 Git 仓库。将在适当的集群中创建开发和生产命名空间,并创建相应的
RoleBinding对象。将创建反映每个应用程序角色的组,并将访问这些组的批准权限委托给应用程序所有者。 -
开发人员和运维人员通过申请或由应用程序所有者直接提供访问权限来获得对应用程序的访问。一旦获得访问权限,更新预计会出现在开发人员的沙箱和开发命名空间中。更新是在用户的 Git 仓库分支中进行的,通过拉取请求将代码合并到驱动自动化的主仓库中。
-
所有构建通过应用程序源代码控制中的脚本进行控制。
-
所有工件都发布到一个集中式的容器注册表。
-
所有生产更新必须经过应用程序所有者的批准。
-
这个基础工作流并不包括工作流的典型组件,例如代码和容器扫描、定期的访问认证更新或特权访问的要求。本章的主题完全可以单独成为一本书。我们的目标不是构建一个完整的企业平台,而是为你提供构建和设计自己系统的起点。
选择我们的技术栈
在本节的前面部分,我们以通用的方式讨论了流水线和平台。现在,让我们深入探讨构建流水线所需的技术。我们之前提到,每个应用都有应用源代码和 Kubernetes 清单定义,它还需要构建容器。必须有一种方式来监控 Git 的变化并更新我们的集群。最后,我们需要一个自动化平台,确保所有这些组件能够协同工作。
根据我们平台的需求,我们希望选择具有以下特性的技术:
-
开源:我们不希望你为这本书购买任何东西!
-
API 驱动:我们需要能够以自动化的方式提供组件和访问权限。
-
支持外部认证的可视化组件:本书聚焦于企业,而企业中的每个人都喜欢图形用户界面(GUI),只是希望不同的应用不需要不同的凭证。
-
支持 Kubernetes:这是一本关于 Kubernetes 的书。
为了满足这些需求,我们将向集群部署以下组件:
-
Git 注册表 – GitLab:GitLab 是一个强大的系统,提供出色的 Git 操作体验,并支持外部认证(即 单点登录 (SSO))。它有集成的 issue 管理系统和强大的 API。它还提供了一个 Helm chart,我们已经根据本书的需要进行了定制,以便进行最小化安装。
-
自动化构建 – GitLab:GitLab 旨在成为一个开发巨擘。鉴于它拥有一个与 Kubernetes 原生兼容的集成流水线系统,我们将使用它,而不是像 Jenkins 或 TektonCD 这样的外部系统。
-
容器注册表 – Harbor:在之前的版本中,我们使用了一个简单的 Docker 注册表,但由于我们将构建一个多集群环境,因此使用一个专为生产环境设计的容器注册表非常重要。Harbor 使我们能够存储容器,并通过支持 OpenID Connect 认证的 Web 用户界面进行管理,同时提供 API 进行管理。
-
GitOps – ArgoCD:ArgoCD 是 Intuit 推出的一个功能丰富的 GitOps 平台。它原生支持 Kubernetes,拥有自己的 API,并将对象存储为 Kubernetes 自定义资源,从而简化了自动化。它的 UI 和 CLI 工具都通过 OpenID Connect 集成了 SSO。
-
访问、认证和自动化 – OpenUnison:我们将继续使用 OpenUnison 进行集群认证。我们还将整合我们的技术栈中的 UI 组件,提供一个统一的门户用于平台管理。最后,我们将使用 OpenUnison 的工作流来管理基于角色结构的每个系统的访问权限,并为所有系统提供所需的对象,以确保它们能够协同工作。访问将通过 OpenUnison 的自服务门户提供。
-
节点策略执行 – GateKeeper:第十二章中的 GateKeeper 部署,使用 Gatekeeper 进行节点安全,将强制执行每个命名空间必须有一组最小的策略。
-
租户隔离 – vCluster:我们在第九章中使用 vCluster 为每个租户提供了自己的虚拟集群。我们将在此基础上继续提供各个租户自己的虚拟集群,以便他们更好地控制自己的环境。
-
机密管理 – HashiCorp Vault:我们已经知道如何使用 Vault 部署 vCluster,因此我们将继续使用它来外部化我们的机密。
阅读这个技术栈时,你可能会问:“为什么不选择XYZ?”Kubernetes 生态系统非常多样化,集群中有许多优秀的项目和产品。这绝不是一个权威的技术栈,也不是一个“推荐”的栈。这是一个满足我们需求的应用程序集合,让我们能够专注于正在实施的过程,而不是学习特定的技术。
你也可能会发现,这个栈中的工具之间存在相当多的重叠。例如,GitLab 不仅可以用于 Git 和管道,我们想展示的是不同组件如何相互集成。尤其是在企业中,组件由不同的组织管理,通常只会用某个系统来做该组专长的事情,这是很常见的。比如,一个专注于 GitLab 的团队可能不希望你把它作为身份提供者使用,因为他们并不从事身份提供者业务,尽管 GitLab 有这个功能。他们不希望为此提供支持。
最后,你会注意到没有提到Backstage。Backstage是一个流行的开源内部开发者平台,通常与任何与“平台工程”相关的项目联系在一起。我们决定不使用 Backstage 来构建我们的平台,因为没有办法在一个章节中涵盖它!关于 Backstage 已经写了多本书,它是一个需要大量独立分析才能妥善处理的话题。接下来的两章的目标是帮助你看到我们通过本书构建的许多技术是如何结合在一起的。这是一个起点,而不是一个完整的解决方案。如果你想集成 Backstage 或任何其他内部开发者平台系统,你会发现你的方法不会与我们有太大不同。
有了我们的技术栈,下一步是看看我们将如何整合这些组件。
设计我们的平台架构
在前面的章节中,我们的所有工作都围绕着一个单一的集群展开。这使得实验更加简单,但现实中的 IT 世界并非如此运作。至少你应该将开发集群和生产集群分开,这不仅是为了隔离工作负载,还能让你在生产环境之外测试操作流程。你可能还需要根据其他风险和政策的要求来隔离集群。例如,如果你的企业跨越多个国家,你可能需要遵守每个国家的数据主权法律,并在该国的基础设施上运行工作负载。如果你所在的行业受监管,需要对不同种类的数据实施不同的安全级别,你也可能需要将集群分开。基于这些原因,本章和下一章将不再局限于单一集群,而是采用多集群设计。
为了保持简洁,我们假设可以有一个集群用于开发,一个集群用于生产。然而,这种设计有一个问题:我们该将管理栈中的所有技术部署到哪里?它们是“生产”系统,所以你可能会想将它们部署到生产集群中,但由于这些通常是特权系统,这可能会引发安全和政策问题。由于许多系统与开发相关,你可能会认为它们应该部署到开发集群中。但这也可能是一个问题,因为你不希望开发系统控制生产系统。
为了解决这些问题,我们将添加第三个集群作为我们的“控制平面”集群。这个集群将托管 OpenUnison、GitLab、Harbor、ArgoCD 和 Vault。这样,开发和生产集群中的租户就能继续运行。每个租户将在其命名空间中运行一个 vCluster,而该 vCluster 将运行自己的 OpenUnison,正如我们在 vCluster 章节中所做的那样。这样,我们的架构就变成了:

图 18.5:开发平台架构
从我们的图示中可以看到,我们创建了一个相当复杂的基础设施。不过,由于我们利用了多租户技术,这比它可能出现的样子要简单得多。如果每个租户都有自己的集群和相关基础设施,你就需要管理和更新所有这些系统。在本书中,我们一直把身份作为一个重要的安全边界。我们已经讨论了 OpenUnison 和 Kubernetes 如何与 Vault 交互,但我们的技术栈中的其他组件呢?
安全地管理远程 Kubernetes 集群
在第六章,将身份验证集成到集群中中,我们讨论了外部管道如何与 Kubernetes 集群进行安全通信。我们使用了一个例子,展示如何基于 Kubernetes 为每个 Pod 发放的身份,或者通过 Active Directory 发放的凭证,为远程集群生成身份。我们没有使用ServiceAccount令牌来为远程集群进行身份验证,因为我们认为这种做法在 Kubernetes 中是不推荐的。ServiceAccount令牌本不应该作为外部集群访问集群的凭证,而自 Kubernetes 1.24 以来,默认行为是生成具有有限有效期的令牌,但它仍然需要令牌轮换,并违反了使用ServiceAccount的初衷。
我们将通过依赖 OpenUnison 内置的身份提供者功能来避免这种反模式。当我们将一个节点或租户集群集成到我们的控制平面 OpenUnison 中时,OpenUnison 会部署一个信任 OpenUnison 的 kube-oidc-proxy 实例。然后,当 OpenUnison 需要向其中一个节点或租户集群发出 API 调用时,它可以使用一个短期令牌来完成此操作。

图 18.6:控制平面 API 与租户和节点的集成
在图 18.6中,我们的租户集群运行一个 kube-oidc-proxy 实例,该实例配置为信任控制平面集群的 OpenUnison 中配置的身份提供者。在这种情况下,我们将代理称为“管理”代理,因为它仅用于 OpenUnison 和 ArgoCD 与节点或租户的 API 服务器进行交互。当 OpenUnison 想要调用远程 API 时,它首先为该调用生成一个有效期为一分钟的令牌。这与我们使用短期令牌与远程集群进行通信的目标一致。这样,如果令牌被泄露,一旦令牌被攻击者获得,它可能会失效。
我们使用 kube-oidc-proxy 是因为 Kubernetes 在 1.29 之前仅支持一个 OpenID Connect 发行者。从 1.29 开始,Kubernetes 引入了一个 Alpha 功能,允许定义多个令牌发行者,消除了在这种用例中使用冒充代理的需求。我们决定不使用这个功能,原因如下:
-
目前它仍然是一个 Alpha 功能,可能会发生变化。
-
即使该功能正式发布,它也不会作为 API 实现,而是作为一个静态配置,必须部署到每个控制平面。这类似于如何通过 API Server 命令行标志配置集群,并将给托管集群带来类似的挑战。
因此,我们决定不在设计中包含此功能,而是依赖 kube-oidc-proxy。
现在我们知道了 OpenUnison 如何与远程集群交互,我们还需要考虑 ArgoCD 如何与远程集群交互。与 OpenUnison 类似,它也需要能够调用我们租户和节点集群的 API。正如 OpenUnison 一样,我们不希望使用静态的 ServiceAccount 令牌。幸运的是,我们已经具备了所有需要的组件来实现这一点。
由于 OpenUnison 已经能够生成受远程集群信任的短期令牌,现在我们需要做的是确保在 ArgoCD 需要令牌时能够安全地将其传递给 ArgoCD,并告诉 ArgoCD 使用它。由于 ArgoCD 使用 Kubernetes 的 client-go SDK,因此它能够使用凭证插件调用远程 API 来检索凭证。在这种情况下,我们将使用类似于 第六章 《将身份验证集成到集群中》中的模式来生成令牌。不同的是,我们将使用 ArgoCD 控制器 pod 的身份来生成所需的令牌:

图 18.7:ArgoCD 与租户和节点的集成,使用短期凭证
我们能够利用 go-sdk 的凭证插件。在第 1 步中,我们生成一个 HTTP 请求,发送到我们在 OpenUnison 部署的服务,并使用 Pod 的凭证来获取令牌:
#!/bin/bash
OPENUNISON_CP_HOST=$1
CLUSTER_NAME=$2
PATH_TO_POD_TOKEN=$3
REMOTE_TOKEN=$(curl -H "Authorization: Bearer $(<$PATH_TO_TOKEN)" https://$OPENUNISON_CP_HOST/api/get-target-token?targetName=$CLUSTER_NAME 2>/dev/null)
Pod's token. The script then makes a curl call with that token to OpenUnison to get the token, using the Pod's identity as a bearer token.
当请求到达 OpenUnison 时,它会执行第 2 步,OpenUnison 会向 API 服务器发出 TokenRequest,以确保 API 调用中提供的令牌是有效的。为了成功执行,令牌必须未过期,并且与令牌绑定的 pod 必须仍在运行。如果有人从一个过期的 pod 获取令牌,但令牌尚未过期,这个调用仍然会失败。此时,请求已经通过身份验证,第 3 步中,API 服务器会将其判断结果返回给 OpenUnison。我们不希望任何身份都能为我们的远程集群获取令牌。
接下来,OpenUnison 需要授权该请求。在我们的 API Application 配置中,我们将 azRule 定义为 (sub=system:serviceaccount:argocd:argocd-application-controller),确保只有控制器 pod 能够为我们的远程集群获取令牌。这确保了,如果 ArgoCD 的 Web 界面发生泄露,攻击者不能仅凭该 pod 的身份生成令牌。他们还需要进入应用控制器 pod。
在请求经过身份验证和授权后,第 4 步中,OpenUnison 会查找目标并返回生成的令牌。最后,在第 5 步中,我们在凭证插件中生成一些 JSON,告诉 client-go SDK 使用哪个令牌:
echo -n "{\"apiVersion\": \"client.authentication.k8s.io/v1\",\"kind\": \"ExecCredential\",\"status\": {\"token\": \"$REMOTE_TOKEN\"}}"
一旦 ArgoCD 获取到令牌,它将在与远程集群交互时使用该令牌。我们设计了一种方法,让我们的平台能够使用集中式的 ArgoCD,同时不依赖于长期凭证!
但我们还没有完成。ArgoCD 配置为使用我们的令牌进行两步操作:
-
定义一个包含集群连接配置的
Secret,并定义一个标签来标识它 -
创建一个指定目标集群的
ApplicationSet
例如,下面是一个Secret示例:
---
apiVersion: v1
kind: Secret
metadata:
name: k8s-kubernetes-satelite
namespace: argocd
labels:
argocd.argoproj.io/secret-type: cluster
tremolo.io/clustername: k8s-kubernetes-satelite
type: Opaque
stringData:
name: k8s-kubernetes-satelite
server: https://oumgmt-proxy.idp-dev.tremolo.dev
config: |
{
"execProviderConfig": {
"command": "/custom-tools/remote-token.sh",
"args": ["k8sou.idp-cp.tremolo.dev","k8s-kubernetes-satelite","/var/run/secrets/ubernetes.io/serviceaccount/token"],
"apiVersion": "client.authentication.k8s.io/v1"
},
"tlsClientConfig": {
"insecure": false,
"caData": "LS0tL…
}
}
你可以看到我们的配置中不包含任何秘密信息!标签acrocd.argoproj.io/secret-type: cluster告诉 ArgoCD,这个Secret用于配置远程集群。附加标签tremolo.io/clustername帮助我们知道应该支持哪个集群。接下来,我们定义一个ApplicationSet,ArgoCD 的操作员将使用该ApplicationSet来生成 ArgoCD 的Application对象和集群配置:
---
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: test-remote-cluster
namespace: argocd
spec:
goTemplate: true
goTemplateOptions: ["missingkey=error"]
generators:
- clusters:
selector:
matchLabels:
tremolo.io/clustername: k8s-kubernetes-satelite
template:
metadata:
name: '{{.name}}-guestbook' # 'name' field of the Secret
spec:
project: "default"
source:
repoURL: https://github.com/mlbiam/test-argocd-repo.git
targetRevision: HEAD
path: yaml
directory:
recurse: true
destination:
server: '{{.server}}' # 'server' field of the secret
namespace: myns
spec.generators[0]标识一个集群生成器,该生成器匹配标签tremolo.io/clustername: k8s-kubernetes-satelite。由于我们正在构建一个多租户平台,我们需要确保定义一个 GateKeeper 策略,以防止用户在创建ApplicationSet对象时指定他们不拥有的集群标签。
现在我们已经搞清楚了 OpenUnison 和 ArgoCD 如何与平台中的远程集群安全地通信,接下来我们需要解决我们的集群如何从镜像仓库中安全地拉取镜像。
安全地推送和拉取镜像
除了需要安全地调用远程集群的 API 之外,我们还需要能够安全地从镜像仓库中推送和拉取镜像。虽然我们希望采用与其他 API 相同的技术来与我们的镜像仓库互动,但遗憾的是,我们无法做到这一点。Kubernetes 并没有提供一种动态生成镜像拉取凭证的方法,这意味着我们需要生成一个静态令牌。该令牌也需要作为Secret存储在我们的 API 服务器中。由于我们将使用 Vault,我们计划在每个集群中使用 External Secrets Operator,以便从 Vault 同步拉取凭证。
我们已经梳理了我们的技术栈以及各个组件如何进行通信。接下来,我们将探讨如何部署这些技术。
使用基础设施即代码进行部署
在本书中,我们使用 bash 脚本来部署所有实验室。我们之所以能够这样做,是因为大多数实验室都很简单,集成度低,并且不需要重复性。这在企业环境中通常并非如此。你可能需要为开发和测试创建多个环境。你可能需要部署到多个或不同的云环境中。你可能需要在跨国边界重建环境,以遵守数据主权法规。最后,你的部署可能需要比 bash 能够轻松提供的更复杂的逻辑。
这是基础设施即代码(IaC)工具开始提供价值的地方。IaC 工具之所以流行,是因为它们在代码和部署基础设施所需的 API 之间提供了一层抽象。例如,IaC 工具可以为创建 Kubernetes 资源和云服务提供商中的资源创建提供一个通用的 API。它们不会完全相同,但如果你知道如何使用其中一个,那么这种模式通常适用于其他提供商。
IaC 工具有两种常见的使用方式:
-
命令式脚本:IaC 工具可以是简化在多个系统上重新运行命令的工具,且是可重复使用的。它提供最小的抽象,并且在每次运行之间不保持任何内部“状态”。Ansible 就是这种工具的一个很好的例子。它使得在多个主机上运行命令变得容易,但它不会处理与已知配置的“漂移”。
-
状态协调:许多 IaC 工具存储环境的预期状态,并与该状态进行协调。这与 GitOps 的理念非常相似,GitOps 将状态存储在 Git 仓库中。这种方法的主要好处是,你可以使你的基础设施与预期的状态保持一致,因此,如果你的基础设施发生“漂移”,IaC 工具知道如何将其恢复到正确的状态。这个方法的一个挑战是,你现在需要管理和维护状态。
这里没有“正确”的方法;它真正取决于你想要实现的目标。有许多开源的 IaC 工具。对于我们的平台,我们将使用 Pulumi(www.pulumi.com/)。我喜欢 Pulumi 的原因之一是它没有自己独特的领域特定语言或标记语言——它提供了 Python、Java、Go、JavaScript 等语言的 API。因此,尽管你仍然需要额外的二进制文件来运行它,但它的学习曲线更为平缓,我认为也更容易进行长期的维护。
在管理状态方面,Pulumi 提供了免费的云服务,或者你可以使用像 Amazon S3 这样的对象存储系统,或者使用本地文件系统。由于我们不希望你需要注册任何服务,我们将在所有示例中使用本地文件系统。
在使用 Pulumi 程序时,理解的一个关键点是,你并不是在操作基础设施本身,而是在处理你希望创建的状态,然后 Pulumi 会将你程序创建的状态与现有基础设施的实际情况进行协调。为了实现这一点,Pulumi 会对你的程序进行两次运行。第一次是生成预期的状态,第二次是应用该状态中的未知部分。举个例子,假设你要使用 Pulumi 部署 OpenUnison 和 Kubernetes Dashboard。OpenUnison 的 Helm chart 部分要求知道暴露仪表盘部署的 Service 的名称。默认情况下,Pulumi 控制资源的名称,所以在编写代码时你不会知道 Service 的名称,但它会通过一个变量提供给你。这个变量在第一次运行时不可用,但在代码的第二次运行时会可用。以下是通过 Pulumi 部署仪表盘的 Python 代码:
k8s_db_release = k8s.helm.v3.Release(
'kubernetes-dashboard',
k8s.helm.v3.ReleaseArgs(
chart=chart_name,
version=chart_version,
namespace='kubernetes-dashboard',
skip_await=False,
repository_opts= k8s.helm.v3.RepositoryOptsArgs(
repo=chart_url
),
),
opts=pulumi.ResourceOptions(
provider = k8s_provider,
depends_on=[dashboard_namespace],
custom_timeouts=pulumi.CustomTimeouts(
create="8m",
update="10m",
delete="10m"
)
)
)
这段代码的重要部分是,一旦 Helm chart 部署完成,它也会被提供给代码中的其他部分。接下来,当我们创建 OpenUnison 的 Helm chart 值时,我们需要获取 Service 名称:
openunison_helm_values["dashboard"]["service_name"] = k8s_db_release.name.apply(lambda name: name)
在这里,我们并不是直接从发布中获取名称作为变量,因为根据你所在的部署阶段,程序无法得知该信息。所以,你可以在 Python 中使用 lambda 注入一个函数,该函数会返回该值,以便 Pulumi 在正确的时机生成它。刚开始使用 Pulumi 时,这对我来说是一个很大的思维障碍,所以我想在这里特别指出这一点。
我们不会在这里深入讨论更多的 Pulumi 实现细节。有很多关于 Pulumi 的书籍,此外,他们的网站也提供了非常好的文档,涵盖了他们支持的所有语言。我们希望专注于简要介绍和一些关键概念,为后续的内容做铺垫。在下一章中,我们将讲解如何存储和检索配置信息,并逐步部署我们的平台。
我们已经介绍了平台的基础设施、基础设施之间的相互连接方式,以及我们计划如何部署它。接下来,我们将把注意力转向租户如何被部署。
自动化租户入驻
在之前的 vCluster 章节中,我们部署了 OpenUnison NaaS 门户,为用户提供了一种自助方式来请求租户并将其部署。该门户允许用户请求创建新的命名空间,并允许开发者通过自助界面请求访问这些命名空间。我们在此基础上扩展了功能,除了相应的 RoleBinding 对象外,还包括在我们的命名空间中创建 vCluster。尽管该实现是一个良好的起点,但它将所有操作都运行在一个单一的集群上,并且仅与运行 vCluster 所需的组件进行了集成。
我们要做的是构建一个集成平台的工作流程,创建所有我们需要的对象,以满足我们所有项目的需求。目标是,我们能够将一个新应用程序部署到我们的环境中,而不必运行kubectl命令(或者至少尽量减少它的使用)。
这将需要精心的规划。以下是我们的开发人员工作流程:

图 18.8:平台开发人员工作流程
让我们快速浏览一下前面图示中展示的工作流程:
-
应用程序拥有者将请求创建应用程序。
-
基础设施管理员批准创建。
-
此时,OpenUnison 将部署我们手动创建的对象。稍后我们会详细说明这些对象。
-
一旦创建,开发人员可以请求访问该应用程序。
-
应用程序拥有者批准访问该应用程序。
-
一旦获得批准,开发人员将分支应用程序源代码库并开始工作。他们可以在自己的开发工作区启动应用程序。也可以分支构建项目来创建管道,和开发环境运维项目来为应用程序创建清单。
-
一旦工作完成并在本地测试通过,开发人员将推送代码到自己的分支,并发起合并请求。
-
应用程序拥有者将批准请求并合并来自 GitLab 的代码。
一旦代码被合并,ArgoCD 将同步运维项目。GitLab 将启动一个管道,构建我们的容器,并用最新的容器标签更新开发运维项目。ArgoCD 将同步更新后的清单到应用程序的开发命名空间。一旦测试完成,应用程序拥有者将从开发运维工作区向生产运维工作区提交合并请求,触发 ArgoCD 启动到生产环境。
在这个流程中并没有一个步骤叫做“运维人员使用kubectl创建命名空间”。这是一个简单的流程,虽然并不会完全阻止运维人员使用kubectl,但它应该是一个很好的起点。所有这些自动化都需要创建一套广泛的对象:

图 18.9:应用程序入驻对象图
上面的图示展示了我们环境中需要创建的对象以及它们之间的关系。由于涉及的部分很多,自动化这个过程显得尤为重要。手动创建这些对象既耗时又容易出错。我们将在下一章中处理这些自动化的工作。
在 GitLab 中,我们为我们的应用代码和运维创建了一个项目。我们还将运维项目分叉为开发运维项目。对于每个项目,我们生成部署密钥并注册 webhook。我们还创建了与本章前面定义的角色相匹配的组。由于我们使用 GitLab 来构建镜像,我们需要注册一个密钥,以便它可以将镜像推送到 Harbor。
对于 Kubernetes,我们为开发和生产环境创建命名空间。我们在租户命名空间中定义 vClusters,并由每个集群的 MySQL 数据库提供支持。接下来,我们将 OpenUnison 部署到每个 vCluster,并使用我们的控制平面 OpenUnison 作为身份提供者。这将使我们的控制平面 OpenUnison 为每个 vCluster 生成身份,并允许 Argo CD 管理它们,而不需要使用静态密钥。
一旦 OpenUnison 部署完成,我们需要将 vClusters 添加到 Vault 以进行密钥管理。我们还将在控制平面集群中创建命名空间和 ApplicationSets,以配置 Argo CD 为我们的租户集群生成 Application 对象。由于 Argo CD 没有任何控制措施来确保 ApplicationSet 仅使用特定集群,我们需要添加 GateKeeper 策略,以确保用户不会尝试为其他租户创建 ApplicationSets。
我们还需要在 Harbor 中配置资源和凭证,以便我们的租户可以管理他们的容器。接下来,我们将每个 vCluster 加入 Vault,并为每个 vCluster 添加外部密钥操作员部署。然后,我们将通过 Vault 将拉取密钥配置到每个集群中。
最后,我们向 ArgoCD 添加 RBAC 规则,以便我们的开发人员可以查看他们的应用程序同步状态,但所有者和运维人员可以进行更新和更改。
如果这听起来像是相当多的工作,你说得对!想象一下,如果我们必须手动做所有的工作。幸运的是,我们不需要。 在进入最后一章并开始部署之前,让我们先谈谈 GitOps 是什么,以及我们将如何使用它。
设计 GitOps 策略
我们已经概述了开发工作流的步骤以及我们将如何构建这些对象。在讨论实现之前,让我们一起了解 Argo CD、OpenUnison 和 Kubernetes 如何相互作用。
到目前为止,我们已经通过在集群中运行kubectl命令,手动部署了本书 Git 仓库中清单文件中的所有内容。这其实不是最理想的方式。如果你需要重建集群呢?与其手动重新创建所有内容,不如让 Argo CD 从 Git 中自动部署一切?你能把更多内容保存在 Git 中,就越好。
话虽如此,OpenUnison 在为我们执行所有这些自动化操作时,如何与 API 服务器进行通信呢?最“简单”的方式就是 OpenUnison 直接调用 API 服务器。

图 18.10:直接写入对象到 API 服务器
这样是可行的。我们最终将实现通过 GitOps 的开发者工作流,但我们的集群管理工作流该如何处理呢?我们希望作为集群操作员,能够像开发人员一样从 GitOps 中获得尽可能多的好处!为此,一个更好的策略是将对象写入 Git 仓库。这样,当 OpenUnison 创建这些对象时,它们会被 Git 追踪,如果需要在 OpenUnison 之外进行更改,这些更改也会被追踪。

图 18.11:将对象写入 Git
当 OpenUnison 需要在 Kubernetes 中创建对象时,它将不会直接将这些对象写入 API 服务器,而是将它们写入 GitLab 中的一个管理项目。Argo CD 将同步这些清单到 API 服务器。
在这里,我们将写入任何不希望用户访问的对象。这包括集群级别的对象,如Namespaces,也包括我们不希望用户具有写访问权限的命名空间对象,如RoleBindings。通过这种方式,我们可以将操作对象管理与应用程序对象管理分开。
这是一个重要的安全问题需要回答:如果 Argo CD 为我们写入这些对象,那么是什么阻止开发人员将RoleBinding或ResourceQuota提交到他们的仓库并让 Argo CD 将其同步到 API 服务器呢?在发布时,唯一的限制方法是告诉 Argo CD 哪些对象可以在AppProject对象中同步。这种方法不如依赖 RBAC 那样有用。我们可以通过使用 vCluster 来解决这个限制,以实现租户隔离。是的,Argo CD 将作为集群管理员访问租户的远程集群,但用户将无法提交与其他集群交互的ApplicationSets。
最后,看看图 18.11,你会注意到我们仍然在将Secret对象写入 API 服务器。我们不希望将敏感信息写入 Git。无论数据是否加密,都不重要;反正这样做都会带来麻烦。Git 的设计初衷是让代码能够以去中心化的方式更容易地共享,而你的敏感数据应该通过集中式仓库进行小心追踪。这是两个对立的需求。
作为敏感数据丢失的一个例子,假设你的工作站上有一个包含密钥的仓库。执行简单的git archive HEAD命令将删除所有 Git 元数据,并提供无法再追踪的干净文件。误将仓库推送到公共仓库是多么容易的事?失去对代码库的控制实在是太容易了。
另一个为什么 Git 不适合存储机密信息的例子是,Git 没有任何内建的身份验证。当你使用 SSH 或 HTTPS 访问 Git 仓库时,GitHub 或 GitLab 会对你进行身份验证,但 Git 本身没有任何身份验证机制。如果你按照本章的练习进行操作,去看看你的 Git 提交。它们是显示“root”还是显示你的名字?Git 只是从你的 Git 配置中获取数据。没有任何东西将这些数据与你绑定起来。这对于你的组织的机密数据来说,能作为有效的审计追踪吗?可能不能。
一些项目尝试通过加密代码库中的敏感数据来解决这个问题。这样,即使代码库被泄露,你仍然需要密钥来解密数据。那么,加密用的Secret存储在哪里呢?开发者在使用吗?需要特别的工具吗?有几个地方可能出问题。最好根本不要在 Git 中存储敏感数据,比如机密信息。
在生产环境中,你希望像管理其他清单一样,将机密外部化。我们将把我们的机密数据写入 HashiCorp 的 Vault,并让集群决定如何提取这些信息,可以使用外部机密操作器或 Vault sidecar。

图 18.12:将机密写入 Vault
设计好我们的开发者工作流并准备好示例项目后,接下来,我们将更新 OpenUnison、GitLab 和 ArgoCD,以让所有这些自动化工作起来!
构建内部开发者平台的注意事项
在开发内部开发者平台(IDP)时,重要的是要牢记一些事项,以避免常见的反模式。当基础设施团队构建应用支持平台时,通常希望尽可能多地内建功能,以最小化应用团队在运行应用程序时需要做的工作量。
比如,你可以将其推向极端,简单地为你的代码提供一个存放的地方,然后自动化其余部分。我们在本章中提到的许多事情都可以通过模板来完成,对吧?为什么要麻烦开发者去暴露这些东西呢?只要让他们提交代码,其余的我们来做!这通常被称为“无服务器”或“功能即服务”。在适当的时候,这非常好,因为你的开发者无需了解太多基础设施的内容。
上段中的关键短语是“在适当的时候”。在本书中,我们强调了技术对 Kubernetes 的影响,也谈到了企业中构建的孤岛。虽然在 DevOps 中,我们经常提到“打破孤岛”,但在企业中,这些孤岛是管理结构的结果。正如我们在本书中不同情境下所讨论的,隐藏在多层抽象之下的部署可能会与这些孤岛发生冲突,并让你的团队处于成为瓶颈的位置。
当基础设施团队成为瓶颈时,通常是因为他们尝试承担过多的应用发布责任,这往往会引发反弹,导致开发人员被给予一个空的“云”来部署自己的基础设施。这也没有帮助,因为这会导致各团队之间大量重复的工作和专业知识。
除了将团队从基础设施中隔离外,过度抽象化还可能失去让应用团队实现所需逻辑的能力。没有任何情况是应用基础设施团队能够预见到应用团队所需的每个边缘案例的,这会导致你的应用团队寻找其他实现方式。这通常是“影子 IT”概念的来源。应用开发人员有需求,而基础设施团队无法满足这些需求,所以他们转向了基于云的选项。
Brian Gracely,Red Hat 的高级总监兼《Cloud Cast》播客的联合主持人,经常说(转述):“护栏,而不是轨道。” 这意味着基础设施团队需要提供护栏,以便最佳地维护共享基础设施,而不是过于严格,以至于应用团队无法完成他们需要做的工作。
在设计你的内部开发平台时,避免走向极端。如果你想提供像无服务器/功能即服务这样的低基础设施选项,那是很棒的。但要确保它只是一个选项,而不是唯一的解决方案。
本章我们已经讲了不少理论。在下一章中,我们将把这些理论付诸实践,构建我们的平台!
总结
本书的前几章重点是构建清单和基础设施。虽然我们开始使用 Istio 查看应用程序,但我们并没有涉及如何管理它们的发布。在本章中,我们将重点从构建基础设施转移到使用管道、GitOps 和自动化发布应用程序。我们了解了管道中包含的组件,如何将应用程序从代码推送到部署。我们深入探讨了什么是 GitOps,它是如何工作的。最后,我们设计了一个自助式多租户平台,我们将在最后一章实施它!
使用本章中的信息应该能为你如何构建自己的平台提供一些方向。通过使用本章中的实际示例,你可以将你组织的需求映射到自动化基础设施所需的技术。我们在本章构建的平台远未完成,但它应该为你规划一个符合你需求的自有平台提供地图。
问题
-
正确或错误:必须实现一个流水线才能让 Kubernetes 工作。
-
正确
-
错误
-
-
一个流水线的最少步骤是什么?
-
构建、扫描、测试和部署
-
构建和部署
-
扫描、测试、部署和构建
-
以上都不是
-
-
什么是 GitOps?
-
在 Kubernetes 上运行 GitLab
-
使用 Git 作为操作配置的权威来源
-
一个愚蠢的营销术语
-
来自新兴初创公司的产品
-
-
编写流水线的标准是什么?
-
所有流水线都应该用 YAML 编写。
-
没有标准;每个项目和供应商都有自己的实现方式。
-
JSON 和 Go 结合使用。
-
Rust。
-
-
如何在 GitOps 模型中部署一个新的容器实例?
-
使用
kubectl更新命名空间中的Deployment或StatefulSet。 -
更新 Git 中的
Deployment或StatefulSet清单,允许 GitOps 控制器更新 Kubernetes 中的对象。 -
提交一个工单,要求运维人员采取行动。
-
以上都不是。
-
-
正确或错误:GitOps 中的所有对象都需要存储在你的 Git 仓库中。
-
正确
-
错误
-
-
正确或错误:你可以用任何你想要的方式自动化流程。
-
正确
-
错误
-
答案
-
a – 正确。虽然这不是强制要求,但它确实能让生活更轻松!
-
d – 没有最少步骤数。如何实现流水线将取决于你的需求。
-
b – 你不与 Kubernetes API 交互,而是将对象存储在 Git 仓库中,允许控制器保持它们的同步。
-
b – 每个流水线工具都有自己的一套方法。
-
b – Git 中的清单才是你真实的依据!
-
b – 在操作符模型中,操作符将根据你提交的清单创建对象。它们应该根据注释或标签被 GitOps 工具忽略。
-
a – Kubernetes 是一个构建平台的框架。根据你的需求来构建它!
第十九章:构建开发者门户
近年来 DevOps 和自动化领域最受欢迎的概念之一就是提供内部开发者门户(IDP)。该门户的目的是为开发者和基础设施团队提供一个单一的服务点,使他们能够访问架构服务,而无需向 IT 部门的“某个人”发送电子邮件。这通常是基于云服务的承诺,尽管实现这一目标需要大量的定制开发。它还为创建开发可管理架构所需的护栏提供了基础。
本章将结合我们在第十八章《提供多租户平台》中讨论的理论,以及我们在本书中学到的大部分概念和技术,来构建一个 IDP。完成本章后,你将对如何为你的基础设施构建 IDP 有一个概念,并理解我们通过本书构建并集成到 Kubernetes 中的各种技术如何结合在一起。
本章将涵盖以下主题:
-
技术要求
-
部署我们的 IDP
-
租户入驻
-
部署应用程序
-
扩展我们的平台
最后,在深入本章之前,我们想说一声谢谢! 本书对我们来说是一段精彩的旅程。自从我们编写第二版以来,看到行业发生了如此大的变化,我们也取得了很多进步,真是令人惊叹。感谢你加入我们,一起探索如何构建企业级 Kubernetes,以及各种技术如何与企业世界的需求相结合。
技术要求
本章比之前的章节有更多的技术要求。你需要三个 Kubernetes 集群,具有以下要求:
-
计算:16 GB 内存和 8 核。你将运行 GitLab、Vault、OpenUnison、Argo CD 等。这将需要一些强劲的计算能力。
-
访问:确保你可以更新并访问本地节点。你需要这样做才能将我们的 CA 证书添加到节点中,以便在拉取容器镜像时能够信任它。
-
网络:你不需要公共 IP 地址,但你需要能够从工作站访问所有三个集群。如果为每个集群使用负载均衡器,会使实现过程更为简便,但这并不是强制要求。
-
Pulumi 和 Python 3:我们将使用 Pulumi 部署我们的平台,运行在 Python 3 上。你使用的工作站需要能够运行这些工具。我们在 macOS 上构建并编写了本章,使用 Homebrew(
brew.sh/)安装 Python。
在开始构建之前,我们将花一些时间讨论本章的技术要求以及它们为什么是必需的,并将这些要求与常见的企业场景联系起来。首先,让我们来看一下我们的计算要求。
满足计算要求
在本书的其余部分,我们的目标是将所有实验运行在一台虚拟机上。我们这样做有几个原因:
-
成本:我们知道在学习技术的过程中成本会迅速上升,所以我们希望确保不会让你为了这个目的而额外花费。
-
简易性:Kubernetes 已经足够复杂了,更不用说在企业环境中如何设置计算和网络!我们也不想担心存储问题,因为它带来了许多复杂性。
-
实现和支持的简易性:我们希望确保能够帮助你完成实验,所以限制部署方式使我们更加轻松地提供支持。
总的来说,本章内容有所不同。你可以使用运行 KinD 的三台虚拟机,但那样可能会引发更多问题,远不值得。我们将讨论两种主要选项:使用云和建立家庭实验室。
使用云托管的 Kubernetes
使用托管 Kubernetes 很受欢迎,尤其是在没有要部署的内容时。每个大型云服务商都有自己的 Kubernetes 托管服务,大多数小型云服务商也是如此。如果你愿意花一些钱,专注于 Kubernetes 而不是支撑它的基础设施,这些服务非常适合你。不过,要确保在设置集群时,你能够通过 SSH 或其他方式直接访问你的工作节点。
许多基于云的托管集群默认情况下不允许访问你的节点,从安全角度看,这是个好事!你无法攻击你无法访问的东西!但缺点是,你也无法进行自定义配置。我们将在下一部分讨论这一点,但大多数企业即使使用云托管 Kubernetes,也要求对节点进行某种程度的自定义。
此外,确保你能承担费用。三集群的配置不太可能在你获得的免费积分内长时间保持。你需要确保自己能支付这笔费用。也就是说,如果你不愿意将钱投入云计算,那么也许家庭实验室是你最好的选择。
建立家庭实验室
云计算可能变得非常昂贵,而且那笔钱几乎是浪费掉的。你根本没有什么可展示的成果!另一种选择是建立一个家庭实验室来运行你的集群。根据写这本书时的情况,现在在自己的家或公寓里运行企业级基础设施比以往任何时候都要容易。它不需要大额投资,而且通常比仅使用云管理集群一个月或两个月还要便宜得多。
你可以非常简单地从 eBay 等拍卖网站上以不到$500 的价格,购买一台翻新的或自建的服务器。拿到服务器后,安装 Linux 和虚拟化管理程序,你就可以开始了。当然,这需要花费更多时间来处理底层基础设施,远超我们目前的操作,但从个人和经济层面来看,这将是非常有价值的。如果你是带着进入企业级 Kubernetes 领域的目标在读这本书,了解你的基础设施如何运作将使你在其他候选人中脱颖而出。
如果你有条件为你的家庭实验室投入更多资金,有一些项目能让你更容易地搭建实验室。以下是两个这样的项目:
-
Metal as a Service (MaaS):这个来自 Canonical 的项目(
maas.io/)通过提供资源管理、DNS、网络启动等功能,使得快速将基础设施接入实验室变得更加容易。Canonical 是创建 Ubuntu Linux 发行版的公司。虽然它最初是为了快速接入硬件而开发的项目,但它也支持通过virsh协议管理 KVM,这样就可以通过 SSH 管理虚拟机。现在,我正在使用这个项目运行我的家庭实验室,设备是在几台运行 Ubuntu 的家庭组装的 PC 上。 -
Container Craft Kargo:这是一个相对较新的平台(
github.com/ContainerCraft/Kargo),它将多个“企业级”系统结合起来,构建一个基于 Kubernetes 的家庭实验室。这个项目的亮点是它从 Talos 开始,Talos 是一个操作系统与 Kubernetes 发行版的结合体,并使用 KubeVirt 利用 Kubernetes API 来部署虚拟机。这是一个很棒的项目,我已经开始使用并投入其中,也将把我的家庭实验室迁移到这个平台。
在研究了如何构建家庭实验室以及在哪里部署你的 IDP 之后,我们接下来将探讨为什么你需要直接访问你的节点。
自定义节点
在使用如 Amazon 或 Azure 这样的托管 Kubernetes 时,节点是由服务提供商提供的。你也可以选择禁用外部访问。从安全的角度来看,这非常好,因为你不需要保护那些无法访问的内容!
正如我们之前所说,安全性与合规性是有区别的。虽然一个完全托管的节点可能更安全,但你的合规性规则可能要求你作为集群管理员,必须有流程来管理节点访问。简单地移除所有访问权限可能无法解决这一合规问题。
然而,这种方法有一个功能上的缺点;你现在无法自定义节点。这个缺点可以通过几种方式表现出来:
-
自定义证书:我们在本书中多次提到,企业通常会维护内部的证书颁发机构(CAs)。我们通过使用自己的内部 CA 来颁发用于 Ingress 和其他组件的证书,来模仿这一过程。这包括我们的 Harbor 实例,这意味着为了让我们的集群能够拉取镜像,运行它的节点必须信任我们的 CA。为了完成这项工作,节点必须配置为信任我们的 CA。不幸的是,Kubernetes 没有 API 来信任私有 CA。
-
驱动程序:虽然在云管理的 Kubernetes 中不那么重要,但企业通常会使用经过认证的特定硬件堆栈,以确保与特定硬件兼容。例如,你的存储区域网络(SAN)可能有特定的内核驱动程序。如果你没有访问节点的权限,就无法安装这些驱动程序。
-
支持的操作系统:许多企业,特别是在高度监管行业的企业,想确保他们运行的是受支持的操作系统和配置。例如,如果你正在运行 Azure Kubernetes,但你的企业已经标准化使用 Red Hat Enterprise Linux(RHEL),你将需要创建一个自定义的节点镜像。
虽然需要访问你的节点会在多个方面复杂化部署,例如需要管理和保护访问权限,但它通常是部署和管理 Kubernetes 的一个必要“坏事”。
虽然你可能最熟悉在 Ubuntu、RHEL 或 RHEL 克隆系统上构建节点,但 Sidero 的 Talos Linux (www.talos.dev/) 提供了一种创新的方式,通过将操作系统精简到启动 Kubernetes 所需的最小功能。意味着所有与操作系统的交互都是通过 API 完成的,来自 Kubernetes 或 Talos。这种管理方式非常有趣,因为你不再需要担心修补操作系统;所有升级都是通过 API 完成的。不再需要保护 SSH,但你仍然需要锁定 API。无法访问操作系统意味着你也不能直接部署驱动程序。我们之前提到的 Kargo 项目使用 Talos 作为其操作系统。当我想要集成我的 Synology 网络附加存储(NAS)时,我不得不为此创建一个 DaemonSet,以便它能与 iSCSI 一起工作 (github.com/ContainerCraft/Kargo/blob/main/ISCSI.md)。
由于我们使用的是自己的内部证书颁发机构(CA),因此你的节点至少需要能够包括自定义证书,以便进行定制。
你可能认为避免这种情况的一个简单方法是使用 Let’s Encrypt (letsencrypt.org/) 来生成证书,从而避免使用自定义证书颁发机构。这个方法的问题在于,它避免了企业常见的需求——使用自定义证书,而 Let’s Encrypt 并没有提供一个标准的内部证书颁发方式。它的自动颁发 API 基于公共验证技术,例如通过公开的 URL 或 DNS。这两种方式都不被大多数企业接受,因此 Let’s Encrypt 通常不允许用于内部系统。由于 Let’s Encrypt 通常不用于企业的内部系统,我们在这里也不使用它。
现在我们知道了为什么需要访问我们的节点,接下来我们将讨论网络管理。
访问节点上的服务
在本书中,我们假设一切都在单个虚拟机上运行。即使我们在 KinD 中运行多个节点,我们也通过端口转发来访问这些节点上运行的容器。由于这些集群更大,你可能需要采取不同的方法。我们在第四章《服务、负载均衡和网络策略》中介绍了 MetalLB,作为一个负载均衡器,可能是多个节点集群的一个很好的选择。你也可以将你的 Ingress 部署为一个 DaemonSet,让 Pods 使用主机端口来监听所有节点,然后使用 DNS 来解析所有节点。
无论你使用哪种方法,我们假设所有服务都会通过你的 Ingress 控制器进行访问。这包括文本或二进制协议:
-
控制平面:
-
80/443: http/https
-
22: ssh
-
3306: MySQL
-
-
开发节点:
-
80/443: http/https
-
3306: MySQL
-
-
生产节点:
-
80/443: http/https
-
3306: MySQL
-
当我们开始部署时,我们会看到 NGINX 可以用来转发 Web 协议和二进制协议,这样你就可以为每个集群使用一个 LoadBalancer。需要注意的是,控制平面集群需要能够访问开发节点和生产节点上的 HTTPS 和 MySQL。同时需要注意,端口 22 将被控制平面集群使用,因此如果你计划直接为节点支持 SSH,如果没有使用像 MetalLB 这样的外部 LoadBalancer,你需要为其配置其他端口。
我们知道如何运行我们的集群,如何自定义工作节点,以及如何访问它们的服务。我们的最后一步是准备好 Pulumi。
部署 Pulumi
在上一章中,我们介绍了基础设施即代码(IaC)的概念,并表示我们将使用 Pulumi 的 IaC 工具来部署我们的 IDP。为了使用 Pulumi,你需要一个工作站来托管和运行客户端。
在深入讨论如何部署 Pulumi 之前,理解一些与 IaC 相关的关键概念非常重要,这些概念经常被忽视。所有 IaC 工具至少由三个组件组成:
-
控制器:控制器通常是运行 IaC 工具的工作站或服务。在本书中,我们假设一个工作站将运行我们的 Pulumi 程序。对于大规模或生产环境的实现,通常更好的做法是部署一个控制器服务,代替你运行 IaC 工具。对于 Pulumi,这可以是他们自己的 SaaS 服务或 Kubernetes 操作员。
-
工具:这是 IaC 的核心组成部分。它是你为构建基础设施而创建的部分,且特定于每个 IaC 工具。
-
远程 API:每个 IaC 工具通过 API 与远程系统进行交互。最初的 IaC 工具通过 SSH 与 Linux 服务器交互,然后通过它们自己的 API 与云服务进行交互。如今,IaC 工具通过自己的提供程序与单个系统进行交互,这些提供程序封装了目标系统的 API。这个过程的一个难点是如何保护这些 API。我们在本书中花了大量篇幅强调短生命周期令牌的重要性,这也可以应用于我们的 IaC 实现。
除了上述三个组件外,许多 IaC 工具还包括某种状态管理文件。在上一章中,我们描述了像 Pulumi 和 Terraform 这样的 IaC 工具如何基于你的 IaC 工具生成一个预期状态,并将其应用到下游系统中。这个状态将包含与基础设施相同的特权信息,应该视为“机密”。例如,如果你通过 IaC 配置一个数据库密码,你的状态文件就包含了该密码的副本。
对于我们的部署,我们将使用本地状态文件。Pulumi 提供了将状态存储在远程服务(如 S3 存储桶)或使用其自己的云服务中的选项。虽然这些选项比本地文件更适合管理,但我们不希望你为了阅读这本书和进行练习而需要注册任何服务,所以我们将使用本地文件来管理所有 Pulumi 状态。
如果你关注 IaC 行业新闻,可能看到过 HashiCorp(Terraform 和 Vault 的创建公司)在 2023 年夏季将开源许可证更改为“商业源代码许可证”。此举是为了应对大量提供“Terraform as a Service”而且没有向 HashiCorp 付费的 SaaS 提供商。这一许可证的变化导致许多这些 SaaS 提供商创建了 OpenTofu,这是 Terraform 的一个分支,遵循原始的 Apache 2 许可证。我们对这个情况不做任何评判或建议,仅仅指出,围绕状态和控制器的托管服务是 IaC 公司收入的主要来源。
由于 Pulumi 是一个商业化的开源软件包,你需要按照他们的说明将命令行 Pulumi 工具安装到你希望作为控制器的工作站上:www.pulumi.com/docs/install/。
最后,你需要获取本章的 GitHub 代码库。你可以在以下 GitHub 仓库访问本章的代码:github.com/PacktPublishing/Kubernetes-An-Enterprise-Guide-Third-Edition/tree/main/chapter19。
既然我们已经了解了 IDP 构建的环境和要求,接下来可以深入到 IDP 的部署过程。
部署我们的 IDP
在我们技术要求处理完毕后,让我们来部署我们的门户!首先,我假设你有三个运行中的集群。如果每个集群都有一个LoadBalancer解决方案,那么下一步就是部署 NGINX。我们没有在 Pulumi 工具中包括 NGINX,因为根据集群的部署方式,这会影响 NGINX 的部署方式。例如,我没有使用典型的集群和LoadBalancer;我只是使用了单节点集群,并通过主机端口修补了 NGINX 以支持80和443端口。
我们还假设你已经附加了一种本地存储,并设置了默认的StorageClass。
我们将运行 NGINX,假设它将作为 HTTP(S)、MySQL 和 SSH 的 Ingress。这可以通过 NGINX 的 Helm chart 轻松实现。在所有三个集群上运行以下命令:
helm upgrade --install ingress-nginx ingress-nginx \
--repo https://kubernetes.github.io/ingress-nginx \
--namespace ingress-nginx --create-namespace \
--set tcp.3306=mysql/mysql:3306
这将部署 NGINX 作为 Ingress 控制器,并启动一个LoadBalancer。
如果你使用的是单节点集群,现在可以用类似这样的命令来修补你的Deployment:kubectl patch deployments ingress-nginx-controller -n ingress-nginx -p '{"spec":{"template":{"spec":{"containers":[{"name":"controller","ports":[{"containerPort":80,"hostPort":80,"protocol":"TCP"},{"containerPort":443,"hostPort":443,"protocol":"TCP"},{"containerPort":22,"hostPort":22,"protocol":"TCP"},{"containerPort":3306,"hostPort":3306,"protocol":"TCP"}]}]}}}}。
这将强制通过你集群上部署的 NGINX 来转发端口。命令位于chapter19/scripts/patch-nginx.txt。
一旦 NGINX 部署完成,你将需要为所有三个LoadBalancer IP 配置 DNS 通配符。如果你没有 DNS 访问权限,可能会很想直接使用 IP 地址,但千万不要这样做!IP 地址在证书管理中可能会以奇怪的方式处理。如果你没有可用的域名,可以像本书中一样使用nip.io。我使用了三个域名,在所有示例中都会用到:
-
控制平面 – *
.idp-cp.tremolo.dev -
开发集群 – *
.idp-dev.tremolo.dev -
生产集群 – *
.idp-prod.tremolo.dev
现在我们的环境已经准备好进行部署,接下来让我们通过创建一个 Pulumi 虚拟环境开始。
设置 Pulumi
在我们开始运行 Pulumi 程序启动部署之前,我们首先需要创建一个 Python 虚拟环境。Python 会动态链接到库,这种方式可能会与其他基于 Python 构建的系统发生冲突。为了避免这些冲突,在我们的 Windows 编程时代,这种问题被称为“DLL 地狱”,你需要创建一个仅用于你的 Pulumi 程序的虚拟环境。在一个新目录下,运行以下命令:
$ python3 -m venv .
这将创建一个虚拟环境,你现在可以使用它来运行 Pulumi,而不会干扰其他系统。接下来,我们需要“激活”这个环境,使得我们的执行使用它:
$ . ./bin/activate
最后一步是下载你的依赖项,前提是你已经检出了本书的最新 Git 仓库:
$ pip3 install -r /path/to/Kubernetes-An-Enterprise-Guide-Third-Edition/chapter19/pulumi/requirements.txt
pip3命令会读取requirements.txt中列出的所有包并将它们安装到我们的虚拟环境中。此时,Python 已经准备好,我们需要初始化我们的堆栈。
准备 Pulumi 的第一步是“登录”以存储你的状态文件。有多种选项,从使用 Pulumi 的云平台到 S3 桶再到你的本地计算机。你可以在其官网上查看各种选项:www.pulumi.com/docs/concepts/state/。我们将使用我们的本地目录:
$ pulumi login file://.
这将在当前目录下创建一个名为./pulumi的目录,其中将包含你的后端。接下来,我们需要初始化一个 Pulumi“堆栈”以跟踪部署的状态:
$ cd chapter19
$ git archive --format=tar HEAD > /path/to/venv/chapter19/chapter19.tar
$ cd /path/to/venv/chapter19/
$ tar -xvf chapter19.tar
$ rm chapter19.tar
接下来,编辑/path/to/venv/chapter19/pulumi/Pulumi.yaml,将runtime.options.virtualenv更改为指向/path/to/venv。最后,我们可以初始化我们的堆栈:
$ cd /path/to/venv/chapter19/pulumi
$ pulumi stack init bookv3-platform
系统会要求你提供密码以加密你的机密信息。确保将其写在一个安全的地方!现在你将有一个名为/path/to/venv/chapter19/pulumi/Pulumi.bookv3-platform.yaml的文件,用于跟踪你的状态。
我们的环境现在已经准备好!接下来,我们将配置变量并开始部署。
初始部署
最终一致性是一个谎言——古老的云原生西斯格言
在 Kubernetes 世界中,我们经常假设“最终一致性”的理念,即创建一个控制循环,等待我们期望的条件变为现实。这通常是对系统的过于简化的看法,尤其是在处理企业系统时。所有这些的意思是,尽管我们几乎所有的部署都在一个 Pulumi 程序中管理,但它需要多次运行才能完全部署环境。在我们逐步讲解的过程中,我们将解释为什么需要单独运行每个步骤。
解决了这个问题后,我们需要配置我们的变量。为了最小化配置的数量,你需要设置以下内容:
| 选项 | 描述 | 示例 |
|---|---|---|
openunison.cp.dns_suffix |
控制平面集群的 DNS 域名 | idp-cp.tremolo.dev |
kube.cp.context |
控制平面在控制平面 kubectl 配置中的 Kubernetes 上下文 | kubernetes-admin@kubernetes |
harbor:url |
部署后 Harbor 的 URL | https://harbor.idp-cp.tremolo.dev |
kube.cp.path |
控制平面集群的 kubectl 配置文件路径 | /path/to/idp-cp |
harbor:username |
Harbor 的管理员用户名 | Always admin |
openunison.dev.dns_suffix |
开发集群的 DNS 后缀 | idp-dev.tremolo.dev |
openunison.prod.dns_suffix |
生产集群的 DNS 后缀 | idp-prod.tremolo.dev |
表 19.1:Kubernetes 集群中的配置选项
为了简化操作,您可以自定义chapter19/scripts/pulumi-initialize.sh并运行它。您也可以通过运行以下命令手动设置这些选项:
$ pulumi config set option value
其中option是您想设置的选项,value是它的value。最后,我们可以运行部署:
$ cd /path/to/venv/chapter19/pulumi
$ pulumi up -y
您将被要求提供密码以解密您的密钥。一旦完成,初始部署将需要一段时间。根据您的网络连接速度和控制平面集群的性能,可能需要 10 到 15 分钟。
一旦所有内容部署完成,您将看到类似这样的消息:
.
.
.
Resources:
+ 53 created
Duration: 7m6s
如果您查看命令的输出,您将看到所有已创建的资源!这与我们在上一章设计的内容一致。
我们不会逐行讲解所有的代码。代码超过了 45,000 行!部署完成后,我们将讲解其中的重点内容。
到目前为止,我们还有一些差距:
-
Vault:Vault 已部署,但尚未配置。我们不能配置 Vault,直到它被解封。
-
GitLab:GitLab 的基础部署已经完成,但我们还没有办法运行工作流。我们还需要生成一个访问令牌,以便 OpenUnison 可以与其交互。
-
Harbor:Harbor 正在运行,但没有 Harbor 管理员密码,我们无法完成 SSO 集成。我们还需要这个密码来与 OpenUnison 进行集成。
-
OpenUnison:基础的 OpenUnison 命名空间即服务门户已经部署,但我们还没有部署任何额外的配置来支持我们的 IDP。
接下来,让我们解封 Vault 并准备好部署。
解封 Vault
还记得在第八章,管理密钥中,我们需要通过提取随机生成的密钥并在 Pod 中运行脚本来解封 Vault 吗?在 Pulumi 中没有简单的方法来实现这一点,因此我们需要使用 Bash 脚本。我们还希望能够将解封的密钥存储在安全的地方,因为一旦获取,它们就不能再次获取。幸运的是,Pulumi 的密钥管理使得将密钥与其余配置存储在同一位置变得容易。首先,让我们解封我们的 Vault:
$ cd /path/to/venv/chapter19/pulumi
$ export KUBECONFIG=/path/to/cp/kubectl.conf
$ export PULUMI_CONFIG_PASSPHRASE=mysecretpassword
$ ../vault/unseal.sh
在运行unseal.sh之前,重要的是设置PULUMI_CONFIG_PASSPHRASE或PULUMI_CONFIG_PASSPHRASE_FILE。设置完成后,如果运行pulumi config,你会看到多了两个密钥:
$ pulumi pulumi config --show-secrets
.
.
.
vault.key hvs.I...
vault.tokens {
"unseal_keys_b64":
.
.
.
现在你的配置已存储在 Pulumi 配置中。如果你使用的是集中式配置,比如通过 Pulumi Cloud 或 S3 存储桶,这可能会更加有用!如果你因为某些原因需要重启 pod,你可以通过运行以下命令再次解锁:
$ cd /path/to/venv/chapter19/pulumi
$ export KUBECONFIG=/path/to/cp/kubectl.conf
$ export PULUMI_CONFIG_PASSPHRASE=mysecretpassword
$ ../vault/unseal_after_init.sh
随着 Vault 准备好配置,接下来,我们将准备 Harbor 的配置。
完成 Harbor 配置
配置 Harbor 实际上非常简单:
$ cd /path/to/venv/chapter19/pulumi
$ export KUBECONFIG=/path/to/cp/kubectl.conf
$ export PULUMI_CONFIG_PASSPHRASE=mysecretpassword
$ ../scripts/harbor-get-root-password.sh
这个脚本完成了两件事:
-
从
harbor-admin密钥获取随机生成的密码,并将其存储在 Pulumi 配置中 -
设置一个标志,让我们的 Pulumi 程序知道要完成 SSO 配置
我们必须执行此步骤,因为 Harbor 提供程序使用了特定于harbor命名空间的配置选项。这与我们其他的配置选项不同。我们来看看在你的 Pulumi 程序中类似这样的一段代码:
my_config = pulumi.config("someconfig")
你的代码并不是在说“获取名为someconfig的配置”;它是在说“获取我栈命名空间中名为someconfig的配置”。命名空间之间的分隔意味着我们的代码无法从另一个命名空间获取配置。实际上,这意味着我们在harbor:url、harbor:password以及harbor:username之间多次定义相同的信息。
在我们的场景中,这种做法看起来效率低下且容易出错,但在大规模部署中,这是一种确保分隔孤岛的好方法。在许多部署中,拥有 Harbor 的人和可能拥有自动化的人并不是同一个人。通过不允许我们的代码访问harbor命名空间,但能够调用依赖于它的库,我们能够使用这些密钥数据而不实际了解它!当然,由于我们使用的是单一的密钥集,并且我们有权限访问它,这个安全性优势被抵消了。不过,如果你使用的是集中管理的 Pulumi 控制器服务,它允许开发人员编写从不直接知道所依赖的密钥数据的代码。
现在 Harbor 已准备好进行最终配置,我们需要在 GitLab 中执行一些手动步骤。我们将在下一节中进行。
完成 GitLab 配置
在 GitLab 中我们缺少两个关键组件。首先,我们需要生成一个令牌,供 OpenUnison 在自动化 GitLab 时使用。另一个是我们需要手动配置一个 runner。稍后,我们将使用 GitLab 集成的工作流构建一个容器,并将其推送到 Harbor。GitLab 通过启动一个 Pod 来完成此操作,这需要一些自动化。启动这些 Pod 的服务需要在 GitLab 中注册。这是有道理的,因为你可能希望在本地 Kubernetes 集群或远程云中运行服务。为了处理这种情况,我们需要告诉 GitLab 生成一个 runner 并给我们一个注册令牌。首先,我们将生成 runner 注册令牌。
生成 GitLab Runner
第一步是登录到 GitLab。我们还没有配置单点登录(SSO),所以你需要使用 root 凭证登录。这些凭证作为一个秘密存储在 GitLab 命名空间下,文件名以 gitlab-initial-root-password 结尾。一旦获得密码,使用用户名 root 登录到 GitLab。URL 将是 https://gitlab.controlplane.dns.suffix,其中 controlplane.dns.suffix 是你的控制平面集群的 DNS 后缀。对我来说,URL 是 https://gitlab.idp-cp.tremolo.dev/。
登录后,点击左下角的 Admin Area:

图 19.2:GitLab 管理区域
屏幕加载完成后,点击 新实例 runner:

图 19.3:GitLab Runners
当新实例 runner屏幕加载时,勾选运行未标记的作业,因为我们只在自己的集群上运行作业。你可以使用这些标签来管理跨多个平台运行的作业,类似于如何使用节点标签在 Kubernetes 中管理工作负载的运行位置。接下来,点击创建 runner:

图 19.4:GitLab 新实例 runner
最后,你将获得一个令牌,我们可以在我们的 Pulumi 配置中进行配置:

图 19.5:新 runner 令牌
复制此令牌并在 Pulumi 中进行配置:
$ cd /path/to/venv/chapter19/pulumi
$ export PULUMI_CONFIG_PASSPHRASE=mysecretpassword
$ pulumi config set gitlab.runner.token \
'glrt-Y-fSdvy_6_xgXcyFW_PW' --secret
接下来,我们将配置一个令牌来自动化 GitLab。
生成 GitLab 个人访问令牌
在上一节中,我们配置了一个 runner。接下来,我们需要一个令牌,以便 OpenUnison 可以自动化将 GitLab 配置到系统中。不幸的是,GitLab 不提供令牌的替代方式。你可以定期使令牌过期,但需要替换它。也就是说,当你以 root 用户身份登录到 GitLab 时,点击左上角的彩色图标,然后点击偏好设置:

图 19.6:GitLab 偏好设置
一旦偏好设置页面加载完毕,点击访问令牌,然后点击添加新令牌:

图 19.7:GitLab 访问令牌
当新的令牌页面加载时,你需要为它命名并设置过期时间,并点击api选项以授予完全访问权限。完成后,点击页面底部的创建个人访问令牌按钮:

图 19.8:创建新的 GitLab 个人访问令牌
令牌生成后,最后一步是复制令牌,并将其配置为 Pulumi 配置机密:

图 19.9:GitLab 个人访问令牌
复制令牌并将其设置到 Pulumi 配置中:
$ cd /path/to/venv/chapter19/pulumi
$ export PULUMI_CONFIG_PASSPHRASE=mysecretpassword
$ pulumi config set gitlab.root.token \
'glpat-suHtqupeNetAsYfGwVyz' --secret
我们现在已经完成了完成控制平面配置所需的额外步骤。接下来,我们将在 Pulumi 程序中完成控制平面的部署。
完成控制平面部署
下一步是重新运行我们的 Pulumi 程序以完成集成:
$ cd /path/to/venv/chapter19/pulumi
$ export PULUMI_CONFIG_PASSPHRASE=mysecretpassword
$ pulumi up -y
这应该比初始运行所需的时间更短,但仍然需要几分钟。OpenUnison 需要再次部署,并应用新的配置选项,这将是最耗时的部分。
部署完成后,我们还有一项任务需要在控制平面上完成。我们需要更新 NGINX,将 22 端口的 SSH 转发到 GitLab shell 服务。我们可以通过获取 GitLab 命名空间中的 shell 服务 名称,然后更新我们的控制平面 NGINX 来实现:
$ cd chapter19/scripts
$ ./patch_nginx_ssh.sh
一旦 NGINX 恢复运行,你应该可以通过 SSH 登录到 GitLab。此时,你可以访问 https://k8sou.idp-cp.tremolo.dev(将 idp-cp.tremolo.dev 替换为你的控制平面后缀),并使用用户名 mmosley 和密码 start123 登录到 OpenUnison:

图 19.10:OpenUnison 主页面
我们还没有完成开发或生产系统的集成,但你应该通过 OpenUnison 使用 SSO 登录 Vault、GitLab、Harbor、ArgoCD 和控制平面 Kubernetes。由于你是第一个登录的人,你将自动成为控制平面集群管理员和顶级审批人。
配置好控制平面后,Pulumi 的最后一步是将开发和生产集群加入到系统中,接下来我们将介绍这个步骤。
集成开发和生产
到目前为止,我们的所有工作都集中在控制平面上。然而,这里不会有任何用户工作负载。我们的租户将位于开发和生产集群中。为了确保我们的自动化计划正常运行,我们需要将这些集群集成到 OpenUnison 中,以便为所有自动化 API 调用使用短生命周期的令牌。幸运的是,OpenUnison 已经具备我们需要的所有功能,并且已经集成到 OpenUnison 的 Helm 图表中。
第一步是将 NGINX 部署到每个集群。我们还将为 MySQL 转发3306端口,以便控制平面上的 OpenUnison 可以与每个集群上的 MySQL 通信。虽然我们本可以使用控制平面上的 MySQL 来支持 vClusters,但我们不希望在控制平面出现问题时影响到开发或生产集群的运行。通过在每个集群上运行 MySQL,一个集群的故障不会影响到其他集群的操作。运行以下命令:
$ export KUBECONFIG=/path/to/idp-dev.conf
$ helm upgrade --install ingress-nginx ingress-nginx \
--repo https://kubernetes.github.io/ingress-nginx \
--namespace ingress-nginx --create-namespace \
--set tcp.3306=mysql/mysql:3306
$ export KUBECONFIG=/path/to/idp-prod.conf
$ helm upgrade --install ingress-nginx ingress-nginx \
--repo https://kubernetes.github.io/ingress-nginx \
--namespace ingress-nginx --create-namespace \
--set tcp.3306=mysql/mysql:3306
启动后,您将能够更新 Pulumi 的配置。
| 选项 | 描述 | 示例 |
|---|---|---|
kube.dev.path |
开发集群的 kubectl 配置文件路径 | /path/to/idp-dev |
kube.dev.context |
开发集群的 Kubernetes 上下文,在其 kubectl 配置文件中 | kubernetes-admin@kubernetes |
kube.prod.path |
生产集群的 kubectl 配置文件路径 | /path/to/idp-prod |
kube.prod.context |
生产集群的 Kubernetes 上下文,在其 kubectl 配置文件中 | kubernetes-admin@kubernetes |
表 19.2:Kubernetes 配置选项以及开发和生产集群中的路径
配置添加完毕后,我们可以完成 Pulumi 的部署:
$ cd /path/to/venv/chapter19/pulumi
$ export PULUMI_CONFIG_PASSPHRASE=mysecretpassword
$ pulumi up -y
这将需要几分钟时间,但完成后,我们将在 OpenUnison 中进行最后一步以完成部署。如果一切顺利,您应该会看到类似的内容:
Resources:
+ 46 created
~ 2 updated
+-1 replaced
49 changes. 68 unchanged
Duration: 4m59s
恭喜,我们的基础设施已经部署完成!三个集群、十多个系统,全部集成完毕!接下来,我们将使用 OpenUnison 的工作流完成最后的集成步骤。
使用 OpenUnison 引导 GitOps
我们已经部署了几个系统来支持我们的 GitOps 工作流,并通过 SSO 将它们集成,用户可以登录,但我们还没有启动 GitOps 引导过程。在这个上下文中,“引导”是指在 GitLab 中设置一些初始的仓库,将它们集成到 Argo CD,并使它们同步到控制平面、开发集群和生产集群。这样,当我们添加新租户时,我们将通过在 Git 中创建清单而不是直接写入集群的 API 服务器来进行。我们仍然会向 API 服务器写入短暂对象,比如用于部署 vClusters 并将其与控制平面集成的Jobs,但除此之外,我们希望所有内容都写入 Git 中。
我们将把最后一步放在 OpenUnison 中,而不是 Pulumi。您可能会想,为什么我们在这部分使用 OpenUnison,而不是使用 Pulumi?最初的计划是使用 Pulumi,但不幸的是,Pulumi 的 GitLab 提供程序中存在一个已知的 bug,导致我们无法在 GitLab 社区版中创建组。由于 OpenUnison 的工作流引擎已经具备此功能,并且这是一个只需要执行一次的步骤,我们决定直接在 OpenUnison 的工作流引擎中完成它。
说完这些后,使用上一节的说明登录到 OpenUnison。接下来,点击左侧的请求访问,选择Kubernetes 管理,然后通过将Kubernetes-prod 集群管理员和Kubernetes-dev 集群管理员添加到购物车来添加开发和生产集群:

图 19.11:OpenUnison 请求访问开发和生产集群管理
添加到购物车后,点击左侧的新结账菜单选项,添加请求的理由,然后点击提交您的请求。

图 19.12:提交集群管理访问请求
刷新屏幕后,您将看到有两个待处理的请求。像在第九章中一样处理它们,使用 vClusters 构建多租户集群,然后退出。当您重新登录时,您将访问开发和生产集群以及控制平面。
登录后,返回到请求访问,点击OpenUnison 内部管理工作流,然后将初始化 OpenUnison添加到购物车。像之前一样,以某个理由将其从购物车结账。这次不会有审批步骤。虽然需要几分钟,但一旦完成,您可以登录 Argo CD,您将看到三个新项目:

图 19.13:OpenUnison 初始化后的 Argo CD
状态为未知,因为我们还没有信任 GitLab 的 ssh 服务的密钥。使用您喜欢的方法下载 Argo CD 命令行工具并运行以下命令:
$ argocd login --grpc-web \
--sso argocd.idp-cp.tremolo.dev
$ ssh-keyscan gitlab-ssh.idp-cp.tremolo.dev \
| argocd cert add-ssh --batch
这将把正确的密钥添加到 Argo CD。几分钟后,你会看到我们的应用程序在 Argo CD 中正在同步!现在是个好时机来浏览 Argo CD 和 GitLab。你将看到我们的 GitOps 基础设施的基本框架是怎样的。你还可以查看chapter19/pulumi/src/helm/kube-enterprise-guide-openunison-idp/templates/workflows/initialization/init-openunison.yaml中的入职流程。我认为你最重要的发现是,我们如何通过身份将所有内容联系在一起。这在 DevOps 世界中是常常被忽视的,尤其是在 Argo CD 中,我们创建了一个AppProject,限制可以添加哪些仓库和集群。然后我们为每个集群创建一个Secret,但该Secret不包含任何秘密数据。最后,我们生成ApplicationSets来生成Application对象。在部署我们的租户时,我们还会遵循这个模式。
现在,你已经拥有了一个可工作的多租户 IDP!这大约花费了 30 页的解释,可能几个小时的集群设计和设置,以及几千行的自动化代码,但你已经拥有它了!接下来,是时候部署一个租户了!
租户入职
到目前为止,我们已经花费了所有的时间来搭建我们的基础设施。我们有一个 Git 仓库,控制平面的集群,以及开发和生产环境的集群,一个 GitOps 控制器,一个秘密管理器,和一个 SSO 和自动化系统。现在我们可以构建我们的第一个租户了!好消息是,这部分非常简单,不需要任何命令行工具。就像我们在第九章中介绍的那样,使用 vClusters 构建多租户集群,我们将使用 OpenUnison 作为请求和批准新租户的门户。如果你已经登录到 OpenUnison,请退出并重新登录,这次使用用户名jjackson和密码start123。你会注意到,在主页上,你的徽章数量大大减少,因为你还没有任何权限!我们将通过创建一个新租户来解决这个问题。点击新 Kubernetes 命名空间徽章:

图 19.14:使用用户名 jjackson 登录 OpenUnison
打开后,使用名称myapp,理由为new application,然后点击保存:

图 19.15:新项目屏幕
保存后,退出并重新登录,但这次使用用户名mmosley和密码start123。你会看到有一个待处理的审批。点击打开审批,处理请求,提供一个理由,然后点击批准请求。当按钮变为青绿色时,点击确认审批。这个过程会花一些时间。并不是说步骤本身计算量很大,而是大部分时间是等待系统同步。这是因为我们并没有直接向集群的 API 服务器写入数据,而是通过 GitLab 部署创建清单,然后通过 Argo CD 将这些清单同步到集群中。我们还在部署多个 vCluster 和 OpenUnison,这也需要时间。如果你想查看进度,可以在控制平面的openunison-orchestra pod 中查看日志。这可能需要 10 到 15 分钟才能完全部署。由于我们没有一个工作的邮件服务器,因此你可以通过登录 Argo CD 来了解工作流是否完成,并且你将看到两个新应用程序:一个是我们的开发 vCluster,另一个是我们的生产 vCluster:

图 19.16:新租户部署后的 Argo CD
如果你点击myapp/k8s-myapp-prod应用程序,你会看到我们并没有同步太多内容:

图 19.17:生产租户 Argo CD 应用程序
到目前为止,我们创建了一个可以与 Vault 通信的ServiceAccount,以及一个同步我们为 Harbor 生成的拉取秘钥的ExternalSecret,将其同步到默认命名空间。接下来,我们要查看我们的新租户!确保退出所有内容,或者直接在不同的浏览器中打开一个隐身/私密窗口,再次以jjackson身份登录。

图 19.18:租户部署后作为 jjackson 的 OpenUnison
你会看到现在我们的集群管理应用程序有了徽章。当你登录到这些应用程序时,你会发现你的视图是有限制的。Argo CD 仅允许你与租户的Application对象进行交互。GitLab 仅允许你查看与租户相关的项目。Harbor 仅允许你查看租户的镜像,最后,Vault 仅允许你与租户的秘密进行交互。如果你查看屏幕顶部树形结构中的myapp-dev和myapp-prod部分,你将看到现在你有了两个 vCluster 的令牌和仪表板。身份管理真是太神奇了!
到目前为止,我们已经构建了大量的基础设施,并使用共同的身份创建了一个租户,允许每个系统的基于策略的系统来决定如何划定边界。接下来,我们将向我们的租户部署一个应用程序。
部署应用程序
我们已经建立了相当多的基础设施来支持我们的多租户平台,并且现在已经有一个租户可以部署我们的应用程序了。让我们继续部署我们的应用程序吧!
如果我们以 jjackson 登录 GitLab,我们会看到有三个项目:
-
myapp-prod/myapp-application:此代码库将存储我们的应用程序代码和生成容器的构建文件。
-
myapp-dev/myapp-ops:存储开发集群清单文件的代码库。
-
myapp-prod/myapp-ops:存储生产集群清单文件的地方。
从开发项目到生产项目没有直接的分支。这是我们最初的设计思路,但从开发到生产的严格路径效果不好。开发环境和生产环境通常不同,而且基础设施的所有者也往往不同。例如,我维护一个公共安全身份提供者,而我们的开发环境并未与生产环境所涉及的所有司法管辖区建立信任。为了解决这个问题,我们设置了一个额外的系统来替代这些身份提供者。这些差异使得直接自动将开发中的更改合并到生产环境中变得困难。
话虽如此,让我们构建我们的应用程序。第一步是生成一个 SSH 密钥并将其添加到 jjackson 在 GitLab 上的个人资料中,以便我们可以将代码提交进去。更新 SSH 密钥后,克隆 myapp-dev/myapp-ops 项目。该项目包含了你的开发集群的清单文件。你会看到已经有清单文件支持将 Harbor 的拉取密钥同步到默认命名空间。创建一个名为 yaml/namespaces/default/deployments 的文件夹,并将 chapter19/examples/ops/python-hello.yaml 添加进去。提交并推送到 GitLab。几分钟后,Argo CD 将尝试同步,但会失败,因为镜像指向的是不存在的内容。这没关系,接下来我们会处理这个问题。

图 19.19:Argo CD 部署失败
我们的部署中的镜像标签将在应用程序构建成功后更新。这为我们提供了所需的自动化,并且如果需要的话,可以轻松回滚。我们的应用程序是一个简单的 Python Web 服务。克隆 myapp-prod/myapp-application 项目并将其复制到 chapter19/examples/myapp 文件夹中。需要注意两点:
-
source:此文件夹包含我们的 Python 源代码。这部分不太重要,我们不会花太多时间在它上面。
-
.gitlab-ci.yml:这是 GitLab 构建脚本,负责生成 Docker 容器,将其推送到 Harbor,然后在 Git 中修补我们的 Deployment。
如果你查看 .gitlab-ci.yml 文件,你可能会注意到它看起来与我们在之前版本中构建的 Tekton 任务类似。GitLab 的管道的相似之处在于,每个阶段都作为集群中的 pod 运行。我们有两个阶段。第一个阶段构建我们的容器,第二个阶段则部署它。
构建阶段使用了 Google 提供的一款名为Kaniko的工具(github.com/GoogleContainerTools/kaniko),它无需与 Docker 守护进程交互即可构建和推送 Docker 镜像。这意味着我们的构建容器不需要特权容器,从而使得构建环境更容易确保安全。Kaniko 使用 Docker 配置来管理凭据。如果你使用 AWS 或其他主要云服务提供商,可以直接与其 IAM 解决方案集成。在这个实例中,我们使用 Harbor,因此我们的 OpenUnison 工作流将 Docker config.json 作为变量配置到 GitLab 项目中。构建过程使用 Git SHA 哈希的简短版本作为标签。这样,我们可以将每个容器与生成它的构建和提交进行关联。
构建的第二阶段是部署。此阶段首先检出我们的myapp-dev/myapp-ops项目,使用正确的镜像 URL 修补我们的Deployment,然后提交并推送回 GitLab。不幸的是,GitLab 和 GitHub 都没有提供便捷的方式使用工作流的身份检出代码。为了解决这个问题,我们的 OpenUnison 入职工作流为我们的 Ops 项目创建了一个“部署密钥”,将其标记为可写,然后将私钥添加到应用项目中。这样,应用项目就可以配置 SSH,允许克隆 DevOps 仓库,生成补丁,然后提交并推送。完成后,Argo CD 会在几分钟内检测到变化,并将其同步到你的开发 vCluster 中。

图 19.20:具有工作同步部署的 Argo CD
假设一切顺利,你现在拥有了一个自动更新的开发环境!你现在可以在推送到生产环境之前运行任何自动化测试。由于我们目前没有自动化的流程来完成这项工作,让我们探讨一下如何可以实现它。
推送到生产环境
到目前为止,我们已经将应用程序部署到开发 vCluster。那么我们的生产 vCluster 呢?myapp-dev/myapp-ops 项目和 myapp-prod/myapp-ops 项目之间没有直接关系。如果你读过这本书的前两版,你可能还记得开发项目是生产项目的一个分支。你可以通过提交合并请求(GitLab 的版本是拉取请求)将容器从开发环境推广到生产环境,一旦通过,开发中的更改就会合并到生产环境中,允许 Argo CD 将它们同步到我们的集群中。这个方法的问题在于它假设开发和生产是完全相同的,而这从来都不是事实。即使是数据库的主机名不同,也会导致这个方法失效。我们需要打破这种模式,才能使我们的集群可用。
对于我们的实验室,最简单的方法是创建 myapp-prod/myapp-ops 项目中的 yaml/namespaces/default/deployments 目录,将 chapter19/examples/ops/python-hello.yaml 文件添加到其中,并将镜像更新为指向我们在 Harbor 中的当前镜像。这并不是一个高度自动化的过程,但它是可行的。最终,Argo CD 会完成清单的同步,我们的服务将运行在生产集群中。
如果你想更进一步地自动化,以下是多个选项:
-
创建 GitLab 工作流:工作流可以用于自动化更新,方式类似于生产环境。你需要某种触发方式,但这个解决方案效果不错,因为你可以利用 GitOps 来跟踪发布过程。
-
创建自定义任务或脚本:我们正在运行 Kubernetes,因此有多种方法可以创建批处理任务来执行升级过程。根据你选择在 Kubernetes 中运行批处理任务的方式,这也可以通过 GitOps 来实现。
-
Akuity Kargo:一个新的项目,专注于以一致的方式解决这个问题。
前两个选项是同一主题的变种:定制化的发布脚本。并没有说这是一种反模式,只是看起来可能有更好的方法,或者至少有更一致的方法来实现这一点。于是出现了 Akuity Kargo 项目(github.com/akuity/kargo),不要与 Container Craft Kargo 项目混淆!Akuity 是由许多 Argo CD 的原始开发者创立的。Kargo 项目不属于 Argo 生态系统,它是完全独立的。它采用了一种有趣的方法来自动化跨多个仓库同步的过程,以推动系统在不同环境中的推广。我们最初考虑将这个工具直接集成到我们的集群中,但它仍然处于版本 1 之前,因此我们决定暂时提及它。毫无疑问,这是一个我们会持续关注的项目!
现在我们已经将应用程序发布到生产环境,接下来可以做什么呢?当然是添加更多用户!我们接下来将探讨如何扩展我们的平台。
向租户添加用户
现在我们已经创建了租户并部署了应用程序,也许是时候看看如何添加新用户了。好消息是我们可以添加更多用户!OpenUnison 使得团队成员可以轻松请求访问权限。登录 OpenUnison,点击请求访问,然后选择你想要的应用程序和角色。应用程序所有者将负责批准该访问权限。这个方法的一个优点是集群所有者无需参与。完全由应用程序所有者决定谁可以访问他们的系统。然后,权限将在平台的所有组件中按需进行配置。

图 19.21:在 OpenUnison 中为应用程序角色添加用户
我们已经部署了平台,并且知道如何部署租户以及如何向租户中添加新成员。我们可以做些什么来改善我们的新平台?有哪些空白需要填补?接下来我们将逐一讲解。
扩展我们的平台
在过去的两章中,我们已经涵盖了构建多租户平台的相当多内容。我们讲解了 GitOps 的工作原理、不同的策略以及像 Pulumi 这样的 IaC 工具如何简化自动化。最后,我们在三个集群上构建了我们的多租户平台。我们的平台包括使用 GitLab 的 Git 和构建、使用 Vault 的机密管理、使用 Argo CD 的 GitOps、Harbor 中的 Docker 注册表,最后,所有这些都通过 OpenUnison 的身份集成在一起。就这样,对吧?不,遗憾的是还没有。这一节将涵盖一些空白或我们的平台可以扩展的地方。首先,我们从身份管理开始。
不同的身份来源
本书中我们特别关注的一个领域是用户身份如何跨越构成集群的各个系统边界。在这个平台中,我们使用 Active Directory 进行用户身份验证,并使用 OpenUnison 的内部组进行授权。类似于第九章,我们也可以将企业的组集成到授权中。我们还可以扩展至 Active Directory 以外,使用 Okta、Entra ID(前身为 Azure AD)、GitHub 等。一个很好的补充是集成多因素身份验证。我们在本书中多次提到过这一点,但值得再强调一遍:多因素身份验证是帮助锁定环境最简单的方法之一!
在了解了其他用户识别方式之后,让我们看看监控和日志记录。
集成监控和日志记录
在第十五章,《监控集群和工作负载》中,我们学习了如何使用 Prometheus 监控 Kubernetes,并使用 OpenSearch 聚合日志。我们没有将这些系统集成到我们的平台中。我们这么做有几个原因:
-
简化:这两章已经足够复杂了,不需要更多集成的内容!
-
缺乏多租户:Prometheus 没有身份的概念,Grafana Community 版只允许两个角色。看来 OpenSearch 支持多租户,但这需要相当大的工程工作量。
-
vCluster 的复杂性:这和多租户问题类似;我们会为每个 vCluster 配置一个 Prometheus 吗?虽然有方法可以做到,但它们可能需要一本独立的书来详细讨论。
鉴于该解决方案是为生产环境设计的,你需要将某种监控和日志记录工具集成到你的主集群和 vCluster 中。这可能会很复杂,但绝对值得。想象一下,应用程序的拥有者能够从一个地方查看所有日志,而无需登录到集群。工具都已经具备,关键在于如何集成!
我们知道监控集群很重要,但接下来我们来谈谈政策管理。
集成政策管理
本书涵盖了两章关于政策管理的内容,并在我们关于 Istio 的章节中包含了授权管理。这是 Kubernetes 的一个重要方面,但我们没有在我们的平台中包括它。我们还花了一章讨论了运行时安全性。我们决定不包括政策管理,因为虽然它在生产环境中是必需的,但对实验室本身并没有提供额外的好处。不过,对于任何生产部署,它绝对是需要包括的!
现在我们已经讨论了在平台中未包括的技术,让我们来谈谈你可以替换的部分。
替换组件
我们之所以选择这些组件,是因为我们希望展示如何基于书中学到的内容构建平台。我们有意识地选择了仅使用开源项目,避免使用任何需要注册或试用的服务。话虽如此,你也可以用像 AKeyLess 这样的服务替换 Vault,或者用 GitHub 替换 GitLab 等。这是你的环境,选择最适合你的工具吧!
平台的故事远未结束,但这也标志着我们书籍的结束!
概述
本章涉及了相当多的内容。我们从构建三个 Kubernetes 集群来支持我们的平台开始。一旦集群搭建好,我们通过 Pulumi 部署了 cert-manager、Argo CD、MySQL、GitLab、Harbor、Vault 和 OpenUnison,将它们集成在一起。有了平台,我们部署了一个租户,看看如何使用自动化和 GitOps 简化管理,最后讨论了更新和调整多租户平台的不同方式。要在一个章节中覆盖这么多内容,实属不易!
我要感谢 Kat Morgan 为本章提供了 Pulumi 代码,作为我开始该章的基础,并在我遇到问题时给予了帮助。
最后,感谢您! 通过 19 章和数十种不同的技术、实验和系统,您和我们一起踏上了企业云原生领域的精彩旅程。我们希望您在阅读和使用本书的过程中,能像我们编写它时一样感到愉快。如果您遇到问题或只是想打个招呼,请在我们的 GitHub 仓库中提一个问题。如果您有任何想法或建议,我们也非常欢迎!我们不能再强调这一点了,再次感谢您与我们一起同行!
问题
-
Kubernetes 有一个用于信任证书的 API:
-
正确
-
错误
-
-
Pulumi 可以在哪里存储状态?
-
本地文件
-
S3 存储桶
-
Pulumi 的 SaaS 服务
-
上述所有
-
-
OpenUnison 可以使用哪些身份源?
-
Active Directory
-
Entra ID(前身为 Azure AD)
-
Okta
-
GitHub
-
上述所有
-
-
GitLab 让你使用工作流的令牌从另一个代码库中检出代码:
-
正确
-
错误
-
-
Argo CD 可以检查代码库的变动:
-
正确
-
错误
-
答案
-
b: 错误:节点的操作系统必须信任远程证书。
-
d: 所有这些选项都是有效的。
-
e: OpenUnison 支持所有这些选项。
-
b: 错误:GitLab 和 GitHub 都不支持使用工作流令牌来检出其他代码库。
-
a: 正确:让 Argo CD 检查代码库的更新,而不是使用 webhook,可以简化部署过程。
加入我们书籍的 Discord 空间
加入本书的 Discord 工作区,与作者进行每月一次的 问我任何问题 会话:


订阅我们的在线数字图书馆,全面访问超过 7,000 本书籍和视频,并使用行业领先的工具帮助您规划个人发展并推进职业生涯。欲了解更多信息,请访问我们的网站。
第二十章:为什么订阅?
-
通过来自 4,000 多名行业专业人士的实用电子书和视频,减少学习时间,增加编程时间
-
通过特别为您打造的技能计划提升学习效果
-
每月免费获取电子书或视频
-
完全可搜索,方便访问重要信息
-
复制并粘贴、打印和书签内容
在 www.packt.com,您还可以阅读一系列免费的技术文章,订阅各种免费的新闻通讯,并享受 Packt 书籍和电子书的独家折扣和优惠。
您可能喜欢的其他书籍
如果您喜欢本书,您可能对 Packt 出版的其他书籍感兴趣:
Kubernetes 深入学习
吉吉·赛凡
ISBN: 9781804611395
-
学习如何使用策略引擎治理 Kubernetes
-
学习在生产环境中如何运行 Kubernetes 并实现规模化
-
构建并运行有状态应用和复杂微服务
-
精通 Kubernetes 网络,掌握服务、Ingress 对象、负载均衡器和服务网格
-
实现 Kubernetes 集群的高可用性
-
使用 Prometheus、Grafana 和 Jaeger 等工具提高 Kubernetes 的可观察性
-
通过 Kubernetes API、插件和 Webhooks 扩展 Kubernetes
解决方案架构师手册
索拉布·施里瓦斯塔瓦,尼兰贾利·施里瓦斯塔夫
ISBN: 9781835084236
-
探索解决方案架构师在企业中的各种角色
-
应用设计原则,实现高性能、成本效益的解决方案
-
选择最佳策略来确保您的架构安全并提升可用性
-
发展 DevOps 和 CloudOps 思维方式,促进协作、提高运营效率并优化生产流程
-
应用机器学习、数据工程、大型语言模型(LLM)和生成性 AI 提升安全性和性能
-
将遗留系统现代化为云原生架构,采用经过验证的实际策略
-
掌握关键的解决方案架构师软技能
Spring Boot 3 和 Spring Cloud 微服务(第三版)
马格努斯·拉尔森
ISBN: 9781805128694
-
使用 Spring Boot 构建响应式微服务
-
使用 Spring Cloud 开发具备韧性和可扩展性的微服务
-
使用 OAuth 2.1/OIDC 和 Spring Security 保护公共 API
-
实施 Docker,弥合开发、测试和生产之间的差距
-
使用 Kubernetes 部署和管理微服务
-
应用 Istio 提升安全性、可观察性和流量管理
-
使用 JUnit、测试容器、Gradle 和 bash 编写并运行自动化的微服务测试
-
使用 Spring AOT 和 GraalVM 将微服务编译为本地代码
-
使用 Micrometer Tracing 进行分布式追踪
Packt 正在寻找像您这样的作者
如果您有兴趣成为 Packt 的作者,请访问authors.packtpub.com并立即申请。我们已经与成千上万的开发者和技术专业人士合作,帮助他们将见解分享给全球技术社区。您可以提交一般申请,申请我们正在招募作者的热门话题,或提交您自己的想法。
分享您的想法
现在您已经完成了《Kubernetes - 企业指南》第三版,我们很想听听您的想法!如果您是在 Amazon 上购买的这本书,请点击这里直接访问 Amazon 评论页面,分享您的反馈或在您购买的站点上留下评论。
您的评论对我们和技术社区都非常重要,它将帮助我们确保提供优质的内容。



浙公网安备 33010602011771号