前言
到目前为止,我们讨论的大部分内容都集中在构建适用于任何多租户SaaS架构的核心概念基础。这些信息应已为您提供了多租户架构的清晰视图。现在是时候转变思路,从最佳实践策略转向探讨这些概念如何受到不同技术栈现实影响的具体实现。更具体地说,本章将重点探讨多租户原则在Kubernetes环境中的具体实现。为了便于讨论,我们将通过EKS的视角来分析Kubernetes。
虽然本章内容大多适用于任何Kubernetes环境,但EKS的托管特性会在某些方面影响我们的选择。本章将首先回顾EKS堆栈与SaaS架构原则在关键领域中的良好契合点。目标是更清晰地阐述团队选择EKS作为构建和开发SaaS解决方案首选技术的一些核心因素。在此基础上,我们将转向探讨SaaS环境中用于解决分层、噪声邻居和隔离需求的不同EKS部署模式。
我们将探讨多种可能性,重点介绍可在EKS集群中定义租户环境足迹的不同构造方式。随后,我们将介绍一些关键工具和机制,用于在架构中添加租户上下文路由,从而支持不同的部署模型、身份验证策略等。
接下来,我将深入探讨EKS的入门和部署自动化。在此过程中,我们将看到EKS如何直接影响您对租户环境的预配置、配置和更新的策略。我将探讨如何将Helm、Argo Workflows和Flux等工具结合使用,以描述并自动化环境预配置和部署生命周期的各个环节。这一DevOps工具链的示例将让您初步了解通过构建单一自动化体验,如何应对SaaS环境中可能需要的独特分层和部署模型。
随后,我将转向探讨EKS架构中租户隔离的实现方式。首先,我们将分析不同的EKS部署策略如何影响防止跨租户访问的策略。在此过程中,我们将探索用于实现部署和运行时隔离策略的各种EKS构造。最后,我将总结本章内容,探讨如何优化底层EKS集群中运行的不同计算节点,并介绍可用于将计算实例类型配置与多租户工作负载需求相匹配的新技术。
对多租户和EKS的探讨应能帮助您更好地理解技术选择如何直接影响整体架构设计。它还应突出使用EKS构建SaaS解决方案所带来的可能性范围。当前和新兴的工具、策略和模式列表可能令人望而生畏。同时,这些工具也为您提供了以新颖和创造性的方式解决多租户架构问题的方法。
EKS与SaaS的适配性
我们或许可以从探讨EKS与SaaS为何如此契合开始。我认为,多种因素共同作用,使EKS成为迁移或构建SaaS解决方案的团队极具吸引力的模型。对部分团队而言,吸引力源于编程模型。尽管EKS引入了大量新概念和构造,但应用程序服务的设计与实现方式基本保持不变。在大多数情况下,您使用的语言、工具和库都可以随迁移而保留。这看似是一个非常基本的优势,但对许多团队而言可能具有重大意义。对于正在将解决方案迁移到SaaS交付模型的组织而言,这一点尤为宝贵。
它允许他们将系统的一部分直接迁移到EKS,从而将更多精力聚焦于如何实现多租户的核心要素。EKS的扩展模型也体现了SaaS与EKS之间的良好契合。在本书中,我多次强调了支持租户不可预测需求所带来的复杂性,以及在某些环境中这可能导致计算资源过度配置的问题。在某些情况下,这种过度配置是必要的,以抵消租户活动突然激增的可能性。借助EKS,可以快速扩展计算资源以支持突发性工作负载。
它还可以高效地缩减规模。这可以帮助一些团队更接近其目标,即实现租户活动与计算资源消耗之间的更好对齐。除了EKS的基本扩展功能外,您还将发现一套丰富且不断演进的机制和工具,这些工具可帮助您优化租户工作负载在集群内不同实例类型之间的映射。可调整的参数和设置为实现更多可能性打开了广阔空间。值得注意的是,由于EKS集群仍需根据多租户工作负载需求动态添加或移除计算节点,因此在EKS环境中仍可能出现一些合理的过度配置情况。
不过总体而言,我认为EKS在可扩展性方面确实具备其他计算架构难以比拟的优势。在SaaS架构中,我们还看到了支持多种部署模型的重要性,这使得资源可以在隔离和共享的模型中运行。这代表了EKS与SaaS环境需求高度契合的另一个领域。如后续章节所示,EKS包含分组构造,可让您确定解决方案的计算资源在SaaS环境中如何被管理、部署和落地。这些分组构造与我们在多租户架构中对不同计算资源进行选择性分组的需求高度契合。
EKS部署工具也非常吸引SaaS开发者,其活跃且不断扩充的工具列表能够满足SaaS环境中复杂的部署自动化需求。这些工具中许多由社区驱动,提供了强大且高度可配置的机制,能够很好地适配多租户环境中资源预配置和更新等繁重任务。这些工具和库能够更自然地支持基于层级的入驻和部署的复杂组合,从而减少了创建自定义一次性解决方案的需要。
这对于许多SaaS团队而言是一个巨大优势,使他们能够将SaaS部署自动化需求与一套更适合应对SaaS环境中资源预配置和部署挑战的工具集相连接。隔离是另一个领域,EKS引入了另一层机制,使您能够定义隔离策略。这为构建者提供了新的工具和策略,用于防止跨租户访问。Sidecars、服务网格和其他EKS构造为构建者提供了多种新选项,用于考虑如何以及在哪里注入和强制执行隔离策略。
这也是我们经常看到将隔离推向更集中机制的更大机会。这可以简化服务开发人员的工作,将许多隔离细节从他们的视图中移除。它还可以为整个系统带来更强的隔离配置文件。EKS的许多SaaS优势根植于其社区的强大与深度。您所发现的许多支持多租户的机制,都是社区驱动的解决方案的副产品,这些解决方案正在不断丰富EKS中的多租户可能性。
AWS也在添加新特性,为EKS故事增添新维度。这意味着您需要密切关注社区动态,以了解哪些新选项正在涌现。每次我谈论SaaS和EKS时,我的多租户工具箱中都会新增一些工具或机制。从某种意义上说,这既是采用EKS的福气,也是其挑战;您选择的架构策略可能被Kubernetes工具或构造的引入所取代,而这些工具或构造在您开始时并不存在。
部署模式
每次审视技术栈时,我首先关注的领域是部署模式。对我而言,一旦业务目标定义了部署模型,我便可开始规划如何在架构中实现该模型。这也会直接影响我选择的技术栈中,用于实现该部署模型的不同策略和构造。在本次案例中,我们聚焦于EKS,需要理解如何实现SaaS应用计算资源的隔离部署与池化部署。
通常,这类选项列表较为有限。然而,借助EKS,我们拥有更丰富的策略组合,可灵活决定计算资源的部署方式。在开始探索这些模型之前,让我们先为一些核心EKS构造体进行标注,以便更好地理解整个系统架构的各个组成部分。图10-1展示了部分关键EKS概念的抽象视图,这些概念将是我们后续更广泛部署模式讨论的基础。
图10-1
在图10-1的外边缘,您会注意到一个集群,它用于分组和扩展EKS环境中的所有底层计算资源。在图的底部,我还包含了运行在此集群中的节点引用。每个节点对应一个EC2实例。这些节点将根据EKS环境中的负载在集群中弹性扩展。您在此处看到的pod是EKS的计算单元,将运行我们解决方案的服务。
EKS将负责调度这些pod如何映射到集群中的节点。这些pod代表EKS环境中最小的工作执行单元。最后,您会看到我使用命名空间创建了这些Pod的集合。命名空间使EKS能够隔离和分组集群中的资源。有些人可能将命名空间视为EKS集群中的子集群。对于我们的用途,我们将重点关注命名空间分组结构如何支持SaaS环境的部署需求。
这是对一些基本EKS构造的高级概述。如您所想,这里涉及的细节远超本书的范围。然而,对于我们的目的而言,这已足够让我们开始探索这些概念如何与部署模式相连接。后续章节将详细说明这些不同的EKS机制如何用于分组、隔离和扩展多租户环境的计算层。
池化部署
池化是EKSSaaS部署模型中最简单直接的模式。在此模式下,您本质上采用了一种所有租户共享EKS集群内所有计算资源的架构,并依赖EKS的集群级扩展能力来支持不同租户的计算负载需求。图10-2展示了通过EKS实现的完全池化多租户计算资源布局。
图10-2
在图10-2的顶部,我展示了一组正在使用单个EKS集群的租户,这表明所有计算资源均由所有租户共享。该集群中的Pod运行在单个共享命名空间中。在此环境中,没有其他分组结构用于分离正在运行的工作负载。
在图的底部,您可以看到节点的作用。如前所述,我们的集群运行在动态扩展的节点集上。EKS负责在这些节点上创建将要执行的Pod。随着系统负载增加,将添加更多节点以支持不断变化的需求。从许多方面来看,集群本质上是通过经典的云弹性机制来调整集群规模,以使资源消耗与租户活动保持一致。
在此处,您可能会看到对节点进行超额配置,以确保在租户活动激增时拥有足够的容量。优化EKS集群的消耗和规模可能需要一些工作来完善您的缩放策略。在EKS中,您确实拥有大量配置选项,可影响该共享环境如何扩展以满足系统及其服务的独特需求。例如,您可以根据pod的规模和可用性配置其副本数量。
您可以将pod的内存设置调整为满足特定服务的需求。我的观点是,在池化模型中,充分利用不同的EKS配置选项尤为重要,以确保您的环境能够有效应对支持完全池化计算模型所带来的不断变化的需求。
孤岛部署
EKS为开发者提供了日益丰富的选项,用于隔离其计算资源。其中许多策略可通过应用EKS分组构造实现,利用EKS内置的天然机制在计算资源之间划定边界,并使其以按租户模式运行。其他隔离策略则更贴近传统模型,其中租户被分配专用基础设施(例如每个租户一个集群)。
让我们来看看一些常用的EKS技术,这些技术用于实现隔离部署模型。如您所料,我之前提到的命名空间分组模型是其中一种更常见的隔离构造。图10-3展示了基于命名空间的租户隔离模型概念视图。
图10-3
在此示例中,我使用了命名空间提供的基本分组功能,将应用程序的Pod和计算服务与特定租户负载关联。这意味着我们将应用程序的微服务副本部署到每个租户命名空间中。同时,这些租户命名空间中的Pod仅处理分配给其租户的请求。将租户计算资源放置在命名空间中具有多个优势。
首先,它提供了一种机制,允许您集中管理、配置和操作单个租户的计算环境。它还提供了一种结构,用于附加策略以控制和限制命名空间之间的访问。您可以想象这些策略在定义整体租户隔离模型中发挥的作用。对于此部署策略,您会注意到集群的节点(如图10-3底部所示)会根据命名空间上的负载进行扩展或缩减。
命名空间与集群中的节点之间没有实际关联。节点会集体扩展以满足命名空间上的负载需求。有一种替代方法可以更紧密地将命名空间pod与底层计算节点绑定。图10-4展示了节点按租户分配模型的概念视图。
图10-4
在图10-4中,我们有一系列与特定租户关联的独立隔离pod集合。每个租户都配置为在集群中运行的特定计算节点上运行。现在,隔离租户边界包括命名空间、其pod以及运行这些pod的节点。这为隔离部署故事添加了另一层,确保租户不会共享任何计算资源。对于某些场景,这可能有助于解决与合规性以及计算资源边界性质相关的特定问题。
现在,还有另一种更激进的隔离EKS计算资源的方法。与其尝试通过共同集群隔离资源,您还可以考虑为每个租户单独部署EKS集群。虽然这种方案在某些环境中可能具有吸引力且适用,但我通常建议避免此路径。在我看来,这种高度分布式的架构可能引入运营和成本效率方面的挑战。
如果您预计需要支持大量租户,这种方法还可能带来扩展挑战。同样,这并非不可行,只是相较于其他选项,它显得有些过度设计。这种方法的一个变体是采用基于pod的部署,即将租户分组并分布在多个集群中(此处“pod”一词的含义有所扩展)。例如,你可以为少数几个高级别租户设置一个独立的集群,并将剩余的基本级别租户放置在共享集群中。你可以想象这种模式的多种变体。
另一种我关注的隔离变体是虚拟集群的概念。其核心思想是在无需为每个租户创建物理集群的情况下,实现集群级别的隔离。在此模型中,集群内部存在隔离结构,但工作负载仍运行在共享节点上。这可能对部分团队具有吸引力。
混合池化和孤岛部署
如本书多次提及,选择部署模型并无万能方案。因此,在考虑如何将这些不同部署模型映射到EKS时,我们还需思考在单一EKS架构中支持隔离与池化混合模型的意义。图10-5展示了混合模式部署模型的实现示例。
图10-5
这里没有太多意外。您会发现,我基本上使用了相同的命名空间模型来为每种部署类型创建独立的分组。孤岛式租户(租户1和2)部署到各自的命名空间中,而其余租户则部署到一个独立的“池化”命名空间中。如您所想,同时支持这些模型并不会为整体架构增添过多复杂性。
当然,部署、接入、隔离等其他因素会影响这些命名空间的占用空间,但总体而言,利用命名空间结构创建不同部署模式的能力最终显得相对简单。描述一个命名空间所用的许多概念可以复用到下一个命名空间(需注意某些限制)。
控制平面
到目前为止,关于多租户EKS部署模式的讨论主要集中在如何将应用程序平面中的各种服务分组并部署到EKS集群中,以满足工作负载的隔离和池化要求。还有一个关键组件。在我们的EKS环境中,我们还必须放置SaaS架构的控制平面(假设整个环境基于EKS)。
关于控制平面应如何部署,几乎没有绝对的规则。然而,我们知道其服务必须与应用程序平面分离进行管理、版本控制和部署——即使它们运行在与应用程序平面相同的EKS集群中。我们基本上需要从不同的EKS分组机制中进行选择,识别出最符合SaaS环境需求的构造。图10-6展示了在EKS架构中部署控制平面时可能采用的两种不同方法。
图10-6
在图10-6的右侧,您可以看到一个用于托管应用程序平面的集群。应用程序平面支持隔离部署和池化部署,使用命名空间来分组每个租户配置所需的计算资源。现在的问题是:我们应该将控制平面部署在哪里?第一个示例显示在最左侧,其中控制平面上的所有服务都部署在完全独立的集群中。
在此模型中,控制平面与应用程序平面之间的交互需要允许跨集群边界进行。有些人可能更喜欢将这些服务完全部署在独立的集群中。这确实允许控制平面的配置、部署、扩展配置文件及其他属性仅根据控制平面服务的需求进行管理和运维。相比之下,我还在最右侧展示了一个示例,其中控制平面与应用程序平面命名空间位于同一集群内。
在此,我引入了另一个命名空间,用于聚合所有与控制平面相关的服务。将控制平面部署在同一集群内确实能简化部分流程,复用集群内其他命名空间部署和配置所使用的机制与自动化策略。部分用户可能更倾向于让控制平面资源在同一集群内进行扩展,从而利用单个共享集群的规模优势,最大化运营效率和成本效益。
另一些用户则可能更倾向于将控制平面的需求与其他部分分离。这里没有绝对的答案。您需要权衡利弊,找到与架构目标和需求相匹配的平衡点。
路由考虑
随着您在部署中引入不同的按租户资源,您还需要考虑租户计算资源的分布如何影响多租户环境中的路由体验。我们在第6章中对此概念进行了详细探讨,分析了不同技术栈可能采用的工具和策略,以将租户特定流量路由到适当的计算资源。这恰好是EKS提供多种机制来影响租户流量在架构中流动方式的领域。
有大量供应商和开源工具能够代理EKS计算服务,并添加自定义处理功能,这在多租户环境中非常有用。例如,入口控制器(NGINX、Contour、Kong)可作为入站负载均衡器,将流量路由至租户专用资源。服务网格(Istio、Linkerd、AWSAppMesh)也可用于引入高度可配置的路由控制。
**核心概念是您在环境前端添加了一层租户上下文处理层,该层可应用于路由、身份验证以及其他多种可能性。**您如何应用这些路由技术将取决于解决方案的性质。在我看来,这是EKS体验中凸显多租户优势的领域之一。这里存在大量现有和新兴工具,要确定哪种工具最适合您的环境需求可能颇具挑战。图10-7展示了不同构造如何塑造SaaS架构中租户流量的流动。
图10-7
在此特定示例中,我展示了两个与EKS环境中的资源交互的租户。这两个租户均通过亚马逊Route53服务访问环境时,在URL中使用子域来传递租户上下文。当它们进入环境时,这就是您引入我们之前讨论过的租户感知路由构造的地方。这些工具会提取并应用每个租户的上下文。
该图示例了如何应用这些路由机制的两个具体场景。首先,我将租户上下文路由作为身份验证模型的一部分。其核心思想是提取子域名,并根据子域名确定用于验证特定租户的身份提供商。这种方式在混合使用孤立和共享身份资源的场景下尤为强大。例如,假设每个孤立的premium级租户都有一个专用的Amazon Cognito用户池。
同时,所有基础级租户共享一个通用用户池。在此场景下,身份验证流程需要将传入请求映射到相应的身份提供商。为了避免在应用服务中处理这些路由策略,我将其卸载到其中一个代理工具中。
若进一步追踪入站请求的流向,您会发现租户感知路由在此处应用,以将流量导向对应的命名空间。图10-8展示了通过NGINX入站控制器配置此功能的示例。
图10-8
在图10-8的顶部,您会看到两个不同租户通过各自的子域名进入我们的SaaS环境。当请求从这些租户流入时,它们将通过NGINX入站控制器路由,并根据入站资源配置将请求发送给相应的租户微服务。此路由过程实际上将请求发送到每个命名空间中运行的特定租户微服务。
例如,在左下角,您会看到一个命名空间,其中包含Tenant1的微服务。每个微服务都配置了一个入口资源,其中包含将给定微服务与相应入站请求连接的路径。例如,您会看到左侧图表中有一个/tenant1/order路径,该路径展示了Tenant1命名空间中订单请求的转发路径。这些示例仅代表路由可能性的一小部分。
您使用的每种工具都会带来自己独特的考虑因素和机制,这些因素和机制可能会影响您的架构占用空间。可用的配置选项和模式远超本章的范围。尽管如此,我认为作为一名SaaS架构师,您需要深入研究工具,并确定哪些技术最适合支持您的多租户架构的特定需求。
入驻和部署自动化
多租户资源的预配置、配置和部署会受到所使用技术的影响。分层要求、部署模型以及其他众多因素将直接决定您如何构建SaaS解决方案的入驻和部署组件。这再次是一个领域,EKS提供了大量工具,可解决多租户环境中通常伴随的复杂自动化需求。为了更好地理解这一挑战,让我们先看看入驻和部署流程中可能涉及的各个组成部分。图10-9突出了整体自动化策略中可能包含的一些关键概念要素。
图10-9
在图的右侧,您可以看到一个多租户环境的应用程序层示例。它包含两个命名空间,一个用于隔离的premium级租户,另一个用于共享的基本级租户。我为这些租户配置添加了更多细节,展示了不同的微服务及其关联存储。例如,订单服务依赖于DynamoDB和AmazonS3。
产品服务将其数据存储在RDS实例中。对于隔离环境,假设这些存储构造也是隔离的(按租户分配)资源——尽管实际上不必如此。然后,对于共享命名空间中,订单服务为所有租户共享存储,而产品服务则隔离其存储,为每个租户提供独立的RDS实例。
现在,我们需要分析应用程序平面将如何影响环境的部署和入驻流程。在左上角(步骤1)中,我展示了通过控制平面进行租户入驻的过程。虽然这里未详细展开,但您可以想象在后台需要进行的自动化流程和工具,以实现对每个新租户环境的上下文感知配置和资源分配。您的租户接入代码需要根据租户的层级确定所需的新资源。
例如,隔离式接入需要创建命名空间、部署服务并配置关联存储。对于资源池,我们主要配置环境,仅在必要时创建一次性基础设施。在此情况下,由于基础层Product服务需要为每个租户单独创建RDS实例,您的自动化流程需为每个新接入的租户创建并配置该实例。另一部分是开发CI/CD管道,该管道需要将服务更新部署到应用程序层(步骤2)。
在此阶段,重点在于开发人员的日常体验,即构建人员更新并发布需要经过构建和部署管道的新服务版本。这种方法的不同之处在于,我们的CI/CD管道必须能够将更新后的微服务部署到每个租户命名空间。为此,您需要构建一个能够跨这些环境自动化部署的流程,并支持各种孤立和共享的租户配置。
使用Helm配置入门流程
在此背景下,我们可以将注意力转向确定哪种工具组合最能满足该特定环境的需求。我们当然可以选择直接使用AWS Code Pipeline、Terraform、AWS Cloud DevelopmentKit(CDK)以及其他经典的DevOps工具来自动化这些流程。这种做法完全可行。
与此同时,针对EKS和Kubernetes环境,还存在一系列专为构建和部署设计的工具,这些工具能够以更高效的方式满足需求,并将更多复杂性转移至工具层。为了支持我们环境所需的各种配置选项,我们需要首先确定如何最好地捕获和描述不同入门配置的细微差别。
一个适合此场景的工具是Helm,它允许您创建“图表”,这些图表概述了Kubernetes环境的所有不同属性。这些图表为我们提供了一种机制,自然地解决了我们定义不同分层配置的需求。图10-10展示了如何使用这些图表来描述应用程序层的入驻配置特性。
图10-10
我在此确定的设计方案是创建一个基础Helm模板,用于描述任何租户环境中包含的核心组件。在此模板中,您将找到描述所有服务及其默认设置的配置项,且不包含任何与隔离层或资源池相关的概念。在基线模板的基础上,我们生成针对不同层级的Helm图表,这些图表会应用与各层级关联的参数。
在本示例中,我已包含基本层和高级层的参数,用于生成对应的Helm图表。这种模型的优势在于,我们通过工具化方式表达了分层和部署模型的属性,从而能够对每个环境的属性进行特征化描述。我们还从将所有通用设置捕获在一个基线模板中获得了好处。这使我们能够在单一位置维护、管理和版本控制所有通用设置。
另一个优势是,我们采用了可无缝集成到入驻流程中的工具,通过内置构造能够自动完成这些预配置和配置步骤,从而塑造和调整多租户EKS环境的资源占用。在制定策略时,您需要考虑一个关键点。虽然Helm非常擅长描述我们的EKS环境,但环境中通常还包含一些非EKS构造或服务,这些无法通过Helm进行配置。
例如,如图10-10所示的入门流程需要验证和配置S3、DynamoDB及RDS资源。此时,您可能需要结合使用多种工具,将CDK、Terraform或其他基础设施自动化工具来管理这些非Kubernetes资产。最终,我们环境的完整自动化和特征化将被打包为Helm加上支持基础设施配置和预配置模型其他元素所需的其他资产。
使用Argo Workflows和Flux进行自动化
到目前为止,我主要讨论了如何打包和描述我们基础版和高级版租户的入驻配置文件。现在,我们还需要考虑如何自动化入驻流程中的所有环节。这就是我们可以引入更多DevOps工具的地方,将Argo Workflows和Flux整合到入驻流程中,以协调和同步入驻配置的部署。
正是这些工具的应用,帮助我们串联起整个流程中的各个环节,并处理将部署自动化到这些租户环境时的细节问题。让我们通过一个具体示例来看看如何将Helm、Argo Workflows和Flux结合使用。图10-11展示了这些工具如何处理支持分层入驻体验所带来的复杂性。
图10-11
在图的左下角,您可以看到入驻体验的启动(步骤1)。在此处,一个租户(或某个内部流程)触发入驻流程,并提供租户的层级和其他属性。此请求由控制平面中的入驻服务处理。在入驻流程的某个阶段,该服务将调用租户配置服务,该服务负责创建和配置新租户所需的任何基础设施。
在此示例中,我们有基本和高级层级的租户,每个租户都需要不同的基础设施配置。这就是Helm和Argo Workflow发挥作用的地方。我们的租户预配置服务将触发一个工作流,该工作流负责执行所有步骤以完成租户资源的分层预配置(步骤2)。该工作流必须使用两个不同的工具链来创建租户环境。
在流程的一半阶段,它必须使用经典的基础设施自动化工具(如Terraform、CDK、Cloud Formation)来验证非EKS基础设施资源。在这种特定情况下,这意味着验证用于微服务所需的各种存储资源。为此,工作流会调用一个基础设施自动化工具(例如Terraform)来执行此部分流程(步骤3)。在此处使用的自动化脚本和代码将调用所有与层级相关的操作,以创建任何新高级层租户的隔离存储,或为任何新基础层租户验证仅一个隔离的RDS实例(步骤4)。如您所知,基础层租户大多是共享的,但为产品服务提供隔离存储(如右下角所示)。
到目前为止,我们主要执行了一个经典的基础设施自动化流程,该流程恰好作为Argo Workflow的部分被触发。然而,在该流程的后半部分,我们更关注如何配置EKS集群和租户结构。在此阶段,我们将主要依赖Helm图表来驱动租户EKS资源的配置和创建。首先,我们将克隆用于所有租户的基线模板(步骤5)。然后,Argo工作流将生成一个租户特定的Helm发布配置,合并每个层级的配置设置(步骤6)。
最终结果是一个层级特定的Helm发布配置,其中包含为给定层级接入租户所需的所有信息/设置。此时,我们已拥有代表租户配置的Helm图表,但尚未将其应用到EKS集群。此时Flux发挥作用。当Helm图表准备就绪后,我们可以将其提交到仓库(步骤7)。Flux将监听该仓库以检测任何更新。
当它检测到新的Helm图表时,将使用此配置创建新租户所需的EKS资源(步骤8)。此时您将看到命名空间、微服务及其他EKS特定构造的创建与配置。在回顾这一方法时,您应能看出,这种做法将租户预配置过程中的大部分复杂性转移到了与分层式入驻体验需求相匹配的一组工具中。
租户感知服务部署
租户的入驻只是故事的一半。一旦我们拥有了这些租户专属的环境和资源,还必须考虑开发人员管道如何集成对这些不同部署配置的支持。例如,发布一个微服务的更新时,必须包含将该新微服务部署到EKS集群内多个命名空间的能力。在此阶段,您需要找到一种方式,在不增加开发人员负担的情况下满足多租户部署需求。开发人员应能够直接构建并发布新微服务,无需了解这些部署复杂性。借助EKS工具,您确实可以访问一些构造,这些构造可以帮助自动化这些部署。图10-12提供了一个概念视图,说明Helm和Flux如何支持您部署服务。
图10-12
让我们先看看该图的右侧,这里有一个包含多个租户的集群。有两个高级租户分别在独立的命名空间中运行(租户1和2)。还有一个共享命名空间,其中运行所有基础级租户。每个命名空间都运行相同的订单和Product微服务,且这些微服务具有特定版本(在每个微服务上方右侧显示)。
现在,假设你是负责发布订单服务新版本的开发人员,需要将服务从v3.1升级到v4.0。作为开发者,我只需构建、测试并发布新版本。然而,需要将这个更新后的服务部署到右侧的三个命名空间中。这就是Flux可以处理1对多映射并解决订单服务新版本发布的问题。要实现这一功能,首先需要使用左上角展示的Helm图表。
这些图表中包含了需要部署的微服务引用。我的构建流程可以克隆当前图表,更新产品微服务版本,并更新Helm图表版本以指示存在需要应用的更改。当更新后的Helm图表(v1.6)打包并提交到仓库后,Flux流程将检测到新版本的存在。它将确定如何将此更新后的图表应用到所有运行旧版本的环境中,并将它们全部迁移到v1.6配置。
最终结果是,我的新v4.0订单服务将部署到所有命名空间。在此示例中,我重点更新了微服务。然而,Helm图表还可以配置其他设置。相同的机制可以更改EKS环境中任意数量的不同设置。
租户隔离
在多租户EKS环境中,我们引入了大量新的构造和机制来描述计算资源的部署方式。现在,我们需要思考如何在这些构造之上实现租户隔离。如何在EKS集群中插入策略,以确保一个租户无法访问另一个租户的资源?
如您所料,EKS隔离机制涉及多个维度。为了更好地理解EKSSaaS隔离策略的基础原理,让我们先从一个更概念化的视角审视环境的基本隔离边界(如图10-13所示)。
图10-13
在此图中,本质上存在三种隔离方式。在顶层,不同隔离的租户命名空间之间需要某种隔离机制,以确保一个租户命名空间中运行的微服务无法访问另一个租户命名空间中的微服务。在此级别实现隔离可通过Kubernetes的原生构造较为轻松地实现。
当为新租户预配置命名空间时,入驻流程会为该命名空间配置一个网络策略,限制其访问其他命名空间的能力。以下是一个在预配置租户命名空间时可应用的示例策略:
#tenant-service-policy.yaml
kind:NetworkPolicy
apiVersion:networking.k8s.io/v1
metadata:
namespace:TENANT_NAME
name:TENANT_NAME-policy-deny-other-namespace
spec:
podSelector:
matchLabels:
ingress:
-ports:
-port:
protocol:
这里需要重点关注的是元数据部分,其中包含了租户命名空间名称和策略名称的占位符。该策略本质上阻止了其他租户命名空间中的任何租户访问我们为新租户创建的命名空间。
在图10-13中,我们还引入了策略来隔离微服务访问的存储。在此场景中,您会看到我引入了两个微服务,一个使用共享存储(产品),另一个使用隔离存储(订单)。这些隔离和共享存储模型需要不同的隔离机制来防止跨租户访问。让我们先看看如何隔离订单表。
对于这些数据,我们知道每个租户将拥有自己的专用表。这意味着我们可以使用部署时隔离方法,在每个租户命名空间预配置时配置隔离。为了更好地理解这种隔离机制,我们需要从强制订单表隔离的策略开始。以下策略是模板,会在为新租户验证订单表时填充租户上下文并应用:
{
"Version":"2012-10-17",
"Statement":[
{
"Sid":"TENANT_NAME",
"Effect":"Allow",
"Action":"dynamodb:*",
"Resource":"arn:aws:dynamodb:us-east-1:ACCOUNT_ID:table/Order-TENANT_NAME"
}
]
}
这代表了一个AWS身份与访问管理(IAM)策略,用于声明对资源的访问权限,本例中为DynamoDB表。在此,我们基本上允许租户的订单微服务访问其对应的订单表。同样,此文件为模板。TENANT_NAME占位符将在租户预配置过程中创建租户命名空间时被替换为具体租户值。
此策略通过标记为“IAM服务账户角色(IRSA)”的方式应用于租户命名空间。其工作原理是,填充后的IAM策略将附加到与目标租户命名空间关联的服务账户(如图10-14所示)。
图10-14
在该图的顶部,您可以看到Tenant1和Tenant2命名空间,每个命名空间都运行着订单微服务。在每个命名空间中,都有对服务账户的引用。这些服务账户配置了之前定义的租户特定订单IAM策略。这些策略的配置在租户入驻过程中创建命名空间时应用。现在,当订单微服务尝试访问订单表时,命名空间服务账户的租户范围将自动应用与该命名空间关联的策略。这意味着租户1只能访问与租户1关联的订单表。
任何尝试访问租户2的订单表的操作都将被阻止。在此,我们充分利用了部署时隔离的优势,即隔离策略完全在订单微服务构建器视图之外应用。该策略对我们的孤立订单表效果良好。然而,对于我们的共享产品表,我们需要考虑如何实现项级隔离,因为租户数据在同一DynamoDB表中混合存储。
在此情况下,我们必须采用一种运行时强制隔离模型,该模型会检查每个请求的租户上下文,并根据该上下文确定需应用的策略,以防止跨租户访问产品表中的租户项。图10-15展示了产品表隔离的各个组成部分,这些组件可作为多租户EKS架构的一部分进行实施。
图10-15
在该图的左上角,我展示了在独立、隔离的命名空间中运行的Product微服务。虽然计算资源是隔离的,但该图还显示了一个共享的产品表,用于存储所有租户的数据。由于这些数据是共享的,我们无法使用用于隔离订单表的IRSA机制。因此,产品微服务中的代码必须检查每个请求的租户上下文,并获取该租户的租户范围凭据,以限制其视图仅包含当前租户在表中的项目。
获取这些凭据是通过基于右侧图表中显示的策略来假设一个角色实现的。在此处,我们为DynamoDB表定义了另一个IAM策略,但该版本新增了“条件”部分,用于根据前缀键限制对项的访问权限。该前缀键会根据当前租户(本示例中为Tenant1)的上下文动态填充。如您所想,实现运行时隔离的方式多种多样。
例如,您可以使用共享库来捕获租户上下文、映射到相应的策略,并为当前租户假定一个有效的角色。通常,对于任何运行时隔离模型,我们都希望将隔离策略的解析逻辑移出开发人员视图。
EKS提供另一种可用于运行时隔离策略的构造:sidecars,即可在EKS网络中与服务并行运行的独立进程。其概念类似于摩托车搭载侧车。在我们的讨论中,sidecar的价值在于它作为代理位于微服务之间,可用于收集遥测数据、应用策略等。图10-16展示了将sidecar应用于租户隔离问题的示例。
图10-16
在此图中,我们有相同的Product微服务(针对租户1),该微服务消费存储所有租户产品数据的同一共享表。不同之处在于,我引入了一个sidecar组件。它位于产品服务旁边,并拦截所有从产品服务流出的流量。现在,当产品服务请求数据时,sidecar可以通过适当的IAM策略假冒角色,仅访问表中属于租户1的数据项。
这种方法的优势在于,它位于所有与微服务交互的中间位置,充当完美的流量隔离守护者。对于许多人来说,将隔离置于此层级非常具有吸引力。然而,侧车与存储层的交互方式并不明显。在我的模型中,对产品表的调用必须从侧车发起,因为它需要在请求中应用范围限定的凭证。
这意味着你需要在侧车中放置一个数据访问库来实现这一功能。这种职责划分可能并非最佳选择,也不符合你对微服务代码分布的预期。另一种方法是让侧车获取范围凭据并将其返回给微服务。然后,微服务使用这些凭据。不过,采用这种方法,侧车更像是一个高级库。
因此,虽然我认为侧车在这里有其作用,但还需要进一步探索如何最好地将其应用于这个隔离问题。我们在此审查的示例展示了EKS中可用的各种隔离技术。您可能会发现自己需要结合这些策略来满足解决方案的各种需求。
节点类型选择
在我们的EKS集群中,Pod始终运行在可扩展的计算节点上,以满足多租户工作负载的需求。这些节点的性质和配置是您在配置EKS集群时需要定义的内容。您需要CPU密集型节点还是内存密集型节点?是否需要GPU?
选择节点类型这一概念代表了我们在整体多租户计算策略中需要考虑的又一变量。如果您正在部署EKS环境,您需要确定哪种节点类型组合最适合租户的工作负载需求。一种解决此问题的方法是为应用程序中的不同服务手动选择不同的节点。
您可以查看应用程序中的各项服务,并决定某些服务可能更适合映射到特定的节点类型。如果采用此方法,您需要配置集群以启动多种节点类型,然后将这些服务与目标节点类型关联(如图10-17所示)。
图10-17
在此示例中,我创建了三个独立的托管节点组。分组结构允许我配置集群中一组节点的配置文件,包括为每个组定义关联的EC2实例类型。为了展示极端情况,我为每个节点组选择了不同类型的AWS实例类型。一个组使用优化内存的R5实例类型。第二个组使用优化计算的C5节点。
最后一个组使用GPU实例类型(G5)。此处的假设是,运行在这些节点组中pod上的工作负载与各实例类型的能力高度匹配。虽然我认为这种策略有一定价值,但建议确保您有明确的需求来证明采用此模型带来的额外复杂性是合理的。可能仅有少数关键服务恰好需要特定实例类型。其余服务则可使用一种通用实例类型,该类型能有效支持剩余工作负载。
然而,还有其他更具创意的方法来确定支持系统工作负载所需的实例类型。在理想情况下,节点类型选择可以是一个更动态的过程,实时分析活动并动态调整节点类型。这就是Karpenter发挥作用的地方。使用Karpenter,我可以配置一组可供集群使用的节点类型,而无需将它们与任何特定节点或受管节点组关联。
图10-18展示了Karpenter如何优化集群与租户活动之间的匹配。在图10-18中,我有两个正在运行的节点,它们都是使用C5实例类型启动的。在设置环境时,我还配置了Karpenter,并为其提供了一份我认为适用于集群的候选节点类型列表(如右上角所示)。这意味着Karpenter可以将这些实例类型中的任何一种分配给节点;具体分配哪些实例类型则由Karpenter决定。它会评估和分析系统中的当前活动,并决定应将哪些实例类型分配给各个节点。
图10-18
这是一个非常强大的构造,尤其在多租户环境中,当集群活动波动剧烈时,它能发挥显著作用。将节点选择交由Karpenter处理,可免去您自行定义将工作负载与实例类型关联的策略。此举还可能带来优化,从而提升SaaS环境的整体效率。
节点选择和优化策略虽然强大,但这种方法仍要求您定义决定集群如何扩展这些节点的策略。这为EKS架构增添了一层复杂性,在某些情况下可能导致团队在集群中过度验证节点。这会影响环境的成本效率并削弱规模经济效益。
无服务器计算与EKS的混合使用
我们在此讨论的大多数策略都围绕着这样一个模型:集群中的节点完全处于您的控制之下。例如,节点选择主要专注于优化节点与工作负载的匹配。在所有这些模型中,您仍需制定策略来确定这些节点如何扩展以满足环境的整体需求。然而,您还可以为EKS集群选择另一种计算策略。
EKS允许您将AWS Fargate作为EKS环境的计算模型。Fargate允许您采用无服务器计算策略,消除对集群中节点的任何感知。这简化了EKS环境的计算扩展模型,使您能够依赖Fargate的托管计算模型来提供环境所需的计算资源。它还可以限制过度配置的担忧,使您只需为实际使用的计算资源付费。
真正的挑战在于确定哪种计算策略最能满足您的环境需求。对于部分用户而言,Fargate可能是理想选择。而对于其他用户,对节点和实例类型拥有更多控制权可能更具吸引力。实际上,您需要综合考虑成本及其他因素,以确定哪种方案最适合您的架构和运营模式。