概述
在探讨不同多租户架构构造、模式和策略时,我多次提及租户隔离的概念。此前对该主题的探讨主要停留在高层次和概念层面,重点在于说明隔离如何确保一个租户无法访问另一个租户的资源。现在,我们需要深入探讨租户隔离的细节,并分析在SaaS架构的不同层级中应用隔离的具体机制。
在本章中,我的目标是将租户隔离的细微差别提炼成一套术语、模式和实践,为您提供一个更好的框架,用于思考在何时何地应在架构中引入租户隔离机制。我将从明确租户隔离的作用以及塑造您构建租户隔离策略的基础概念开始。
从这里开始,我们可以开始探讨租户隔离的分层特性,识别架构中需要引入隔离机制以防止跨租户访问的不同区域。这将帮助您更好地理解在多租户环境中为不同技术和基础设施组件设计隔离模型时需要考虑的细节和要点。这还将突出不同资源部署模型带来的挑战与优势,为您提供更多数据,以指导您在微服务分解、数据分区及其他我们探讨过的话题上的决策。
我们的隔离之旅将深入探讨微服务和应用程序代码在执行隔离策略中的作用。我们将研究不同运行时技术如何利用请求的租户上下文来在请求级别控制租户访问范围。这将包括强调在运行时应用隔离策略时需要考虑的一些设计要点。总体目标是确保您在SaaS解决方案中将租户隔离作为优先事项,并审查构建健壮、非侵入式多租户隔离模型时可采用的各种不同方法。
核心概念
在前面的章节中,我花了很多时间描述了可以用于构建多租户SaaS架构的不同模式和部署模型。例如,对于微服务和存储,我讨论了在设计和实现这些概念时可能使用的各种隔离或池化模型。
每次提及孤岛式和池化部署策略时,我都强调了部署模型与租户隔离策略之间的潜在关联。同时,我也特别指出,定义资源的部署方式与定义资源的隔离方式之间存在明确界限。务必确保不要将这两个概念混为一谈。是的,您可能选择一种部署模型以实现特定的隔离体验,但实际实现和强制执行这种隔离仍需通过一个完全独立的机制来完成,该机制会检查每次访问租户资源的尝试,并防止任何跨租户隔离违规行为。
为了更好地说明这一点,假设你选择将租户数据库隔离作为隔离策略的一部分。图9-1展示了这种场景的概念视图。
图9-1
在此范例中,我们有一个产品服务,该服务选择将每个租户的产品资料进行资料孤岛化。假设此资料孤岛化模型是基于某些客户或领域需求所选择的,这些需求表明此资料需要被隔离,且不能与其他租户的资料混合。这一切都符合我们在前几章中讨论的资料分区和服务设计范例。
乍一看,许多人可能会认为,通过将租户数据存储在不同的数据库中,我们已经实现了数据隔离。然而,数据存储在不同的数据库中仅仅确保了数据不会被混合。这并不一定意味着数据已经实现了隔离。考虑以下场景:租户1请求产品列表。产品服务将获取租户上下文并处理请求,将请求路由到租户1的隔离数据库。
这听起来合理。此时,您可能会觉得已经实现了租户隔离。然而,假设我正在处理来自租户1的同一请求,并在服务代码中将租户1替换为租户2。会发生什么?事实上,来自租户1的请求仍然可以访问租户2的数据库。尽管我们为每个租户创建了独立的数据库,但这里没有任何机制能阻止服务跨租户边界。
仅仅分离数据库并不能确保数据真正隔离。这让我们回到了部署与隔离之间的边界问题。此时,我们需要引入一个独立的租户隔离机制,以强制执行隔离——无论资源如何部署。核心思想是在代码与被访问资源之间添加一个中间构造。该构造作为门控机制,利用租户上下文限制对任何资源的访问范围。图9-2展示了在前例基础上添加租户隔离层的修改版本。
图9-2
唯一的变化是围绕我的产品服务添加了一个封装层,该封装层代表了租户隔离的概念视图。在此模式下,每当产品服务的代码尝试访问数据时,隔离层将确保所访问的资源基于当前租户上下文是有效的。因此,无论开发人员在代码中如何操作,隔离机制都会阻止任何尝试访问其他租户资源的行为。
当我向其他团队解释这一点时,常常会遇到反对意见。开发人员通常希望将自己的代码视为“可信”的,并假设自己的团队绝不会编写违反租户边界规则的代码。假设您的代码不会违反隔离规则并非明智之举。即使是最谨慎且善意的开发人员也可能无意中引入导致跨租户边界变更的改动。行业中充斥着因某种原因导致一个租户数据被另一个租户访问的解决方案案例。即使仅发生一次跨租户访问,也可能对SaaS业务造成重大打击。
关键点在于,无论部署模型或技术如何,多租户环境都必须隔离租户资源。从租户的角度来看,不应存在资源孤岛或资源池的概念。租户应期望系统中的每个资源都处于隔离状态,并受到跨租户访问的保护。图9-3展示了该模型在实际中的视觉呈现。
图9-3
我们有两个租户正在使用孤立资源和共享资源的组合。为了简化说明,我这里仅以数据库为例,但请想象这种模式可以扩展到系统中的任何资源类型。在该方案的底层实现中,我们的系统负责确保其隔离策略能够保护每个租户的资源。核心信息是:从租户的角度来看,他们的所有资源都是独立的——即使这些资源运行在共享基础设施上。
对于一些人来说,这一现实往往是其多租户架构中的关键矛盾点。您希望设计出最适合您运营、规模、成本、性能和分层需求的环境布局。同时,您还必须考虑该解决方案如何满足其隔离要求。在某些解决方案和领域中,找到合适的平衡点可能具有挑战性。
还应明确,隔离是作为架构中非常有意为之的一部分创建的。它被明确实现为设计的核心元素,旨在捕获任何有意或无意的跨租户边界访问尝试。
隔离模型分类
在深入探讨不同隔离策略的细节之前,我希望先退一步,定义一些用于描述在SaaS环境中可以实现的不同隔离变体的隔离概念。对于每个类别,您将看到它们如何一般映射到实现和应用隔离策略的不同模式。图9-4展示了我通常看到的三大类隔离模型(不排除其他可能性)。
图9-4
如果从左到右依次分析该图,它从更粗粒度的隔离开始,逐渐变得更细粒度。对于每种隔离类型,我绘制了一条虚线,代表该模型隔离的边界。
左侧第一个选项是我标记为“全栈隔离”的方案。这与采用全栈部署模型的多租户环境直接对应,其中每个租户都分配到独立的资源栈。在此场景下,资源隔离通常是一个较为简单的过程,可通过一组明确定义的机制实现对租户资源的隔离。
当我们移至中间部分,您会注意到这里标记为“资源级隔离”。在此示例中,服务共享一个计算层,为多个租户消耗资源。在此模型中,隔离的单位是整个“资源”。例如,租户1拥有专用的数据库,租户2拥有专用的存储桶(AmazonS3)和专用的队列。隔离的边界是一个完整的资源,而资源的定义可能根据环境中的服务而有所不同。在这种隔离模式下,您仍然可能需要某种隔离构造来控制对资源的访问。虽然在某些场景下这种映射可能更具挑战性,但这类情况较为罕见。
最后,右侧是项级隔离模型。在此模型中,我们进入资源内部,不同租户的项在资源内混合存储。最简单的示例是一个数据库(作为资源),其中包含一个共享表,该表混合了多个租户的数据(池化模型)。虽然数据库是一种简化的思考方式,但相同概念可映射到其他资源。例如,队列资源中的消息可能与多个租户相关联。根本特征是:租户数据与其他租户数据共存于某个共享基础设施资源中。
项级隔离显然是所有隔离方案中最具挑战性的。一旦进入资源内部,可用的隔离构造列表会变得非常有限。部分技术会提供工具在该级别强制隔离,而其他技术则不会。以AWS为例,其身份与访问管理(IAM)构造在某些场景下可用于实现项级隔离。然而,有些服务中IAM无法提供足够细粒度的控制来实现项级隔离。此时,您需要通过构建或引入定制工具来实现项级隔离。
因此,在设计多租户架构时,您可能会发现自己需要考虑这三种租户隔离类型中的一种。在确定如何在环境中表示租户资源时,您需要决定采用这三种模型中的哪一种。您还需考虑技术栈是否提供了实现目标隔离级别所需的工具。
应用程序强制隔离
在理想情况下,您使用的技术应与安全构造有明确的映射关系,这些安全构造可用于强制执行隔离策略。例如,大多数云环境都提供了一些内置的身份和访问管理(IAM)控制,用于配置构建者用于控制对环境中各种资源访问的策略。利用这些机制来实现租户隔离是一种自然的选择。这些安全机制直接位于您与资源之间,允许您定义策略以限制对基础设施资源的访问范围,这与您在架构中引入的隔离模型精神相契合。
然而,挑战在于这些工具并不总是提供您所需的控制级别来表达隔离策略。影响每项技术或服务IAM配置的因素多种多样。例如,由云提供商从头开始构建的原生服务通常比基于现有技术构建的服务具有更细粒度的隔离控制。
在某个阶段,您可能会遇到一种情况:您为某一资源偏好的多租户模型无法支持所需的隔离控制级别。此时,您可能需要考虑引入基于应用程序的隔离机制,以防止跨租户访问。我不会过多展开,因为此处的可能性和细节非常丰富。不过,通常您需要评估不同的策略和访问控制框架、库或工具,以在内置机制无法满足需求时,为这些场景引入自定义控制层。在此场景下,您可能会看到属性基于访问控制(ABAC)或开放策略代理(OPA)等机制作为隔离模型的一部分。具体选择哪种工具高度取决于您需要隔离的对象、使用的工具链以及实施的隔离类型。关键要点是,您的解决方案必须隔离所有资源——即使需要自行构建隔离工具。
RBAC、授权与隔离
应用程序架构中可以应用多种安全机制。例如,团队通常会使用基于角色的访问控制(RBAC)和授权机制来范围和控制应用程序中功能的访问权限。在某些情况下,我看到团队使用相同的RBAC工具来实现租户隔离策略。
让我们通过一个示例来更好地理解这如何模糊应用程序访问控制与租户隔离之间的界限。假设我们有一个场景,其中一个租户以租户管理员角色身份登录到一个SaaS应用程序。现在,在你的应用程序中,假设你正在使用一个RBAC框架来启用或禁用此用户对特定应用程序功能或能力的访问权限。RBAC还可能在其他上下文中用于授权对基础设施的访问。
对于一些人来说,将RBAC映射到隔离在这种情况下是自然的。然而,RBAC通常是基于环境中某个用户角色的概念来控制访问权限。事实上,在一个租户中可能存在多个用户,他们拥有不同的角色。RBAC然后会根据这些用户的角色的不同,为他们提供不同的体验。隔离的范围不与个人用户角色相关联——它仅基于用户的租户上下文。因此,在多租户环境中,同一租户内可能存在多个用户且拥有多个角色,但隔离策略对所有用户保持一致。隔离的唯一作用是确保当前租户仅能访问该租户的资源。任何基于角色或其他应用程序构造的访问限制均应在隔离模型之外进行处理。核心要点是,我们希望明确区分用于隔离租户资源的策略与用于控制特定应用程序功能和访问的策略。当然,可能存在一些通用工具能够覆盖这两种模式。这没有问题。然而,即使所使用的工具是通用的,隔离与控制应用程序访问的思维模式是截然不同的。
应用程序隔离与基础设施隔离
“租户隔离”这一术语带有大量历史包袱。当我与注重安全的团队讨论租户隔离时,他们通常倾向于一种更侧重基础设施的隔离概念(这有其合理性)。在他们的思维框架中,他们往往处于某种基础设施技术或服务的核心,需要防止用户或账户跨越安全模型中的基础边界。这是一种完全有效的租户隔离概念。
然而,在多租户隔离中,隔离的边界和性质实际上是由应用程序定义的构造。当您构建SaaS应用程序时,您需要自行定义这些边界的位置,并在应用程序级别引入机制以确保租户资源得到保护。我将此视为共享责任模型的一种变体。在核心基础设施层,我希望所使用的技术或服务能够强制执行其对租户隔离的定义。在此基础安全模型之上,我构建的应用程序仅知晓自身租户边界的位置。是的,部分边界可能与基础设施边界相关联。然而,在多租户环境中,您的应用程序可能还会在基础设施之上定义自己的隔离边界。
关键在于,在我们的讨论范围内,我将租户隔离视为由应用程序设计和架构定义并强制执行的概念。在某些情况下,我可以利用现有的隔离构造来实现隔离,而在其他情况下,我可能需要设计并实现自己的机制来强制执行应用程序的隔离策略。还需要注意的是,这些应用程序定义的边界会依赖于代码和应用程序库作为其租户隔离方案的一部分。这是构建系统时面临的根本现实:应用程序会以无法通过经典原生安全构造保护的方式共享资源。
隔离模型的层次结构
在掌握核心隔离概念后,我们可以将注意力转向更具体的隔离构造。让我们先看看隔离在多租户架构的各个层中是如何实现的。图9-5提供了一个概念视图,展示了分层如何融入隔离方案。
图9-5
在图9-5中,您将看到多租户架构中可能存在的各种隔离层的示例。最上层是应用服务入口,需要进行隔离。通过此API发起的每个请求都会包含系统用于应用租户隔离的租户上下文。在此API层,您可以开始应用隔离的初始部分。您的API可在每次请求中提取租户上下文,并确定当前租户可访问的下游路径,某些情况下还需确定租户的角色。这可防止租户调用针对当前租户上下文无效的资源请求。例如,访问租户1的隔离微服务时,API请求不应能发送请求至与租户2关联的其他隔离微服务。在此层级,我们并未真正阻止租户访问其数据,而是隔离了租户对计算资源的访问权限。这属于分层隔离模型的一部分,我们在最外层应用隔离机制,在任何微服务尝试访问租户数据或其他下游租户资源之前,先引入额外的保护层。
一旦进入计算层,当微服务尝试访问其他依赖资源(数据库、队列、文件系统等)时,我们将应用下一级隔离机制。此时,我们需要在该层级制定隔离策略,确保每个微服务仅能访问专属于该租户的资源。对于两个孤立的资源,这些策略相对简单。然而,您会发现我们的共享微服务需要实现项级隔离,以控制对共享表中租户行数据的访问。您的隔离策略必须确保来自共享微服务的每个请求仅限于与当前租户上下文关联的项。
这种分层模型让你更好地理解隔离如何在多租户架构的多个维度上应用。在许多方面,这借鉴了传统安全模型中“每层安全”的理念。不过,我们在此基础上进行了扩展,在环境的不同层之间添加了隔离保护措施。虽然你的架构层可能与本模型有所不同,但分层隔离的理念仍应普遍适用。
部署时隔离与运行时隔离
除了分层隔离模型外,你还需要考虑隔离何时应用于你的环境。在某些场景中,你会发现可以在资源部署和配置时应用隔离策略。在其他情况下,隔离需要在运行时进行配置和应用。这些选项的选择主要取决于资源的孤岛或池占用情况以及可用隔离机制的组合。
让我们先从概念上看看部署时隔离模型与运行时隔离模型的区别。我们将从部署时隔离模型的关键要素开始(如图9-6所示)。
图9-6
图9-6展示了在孤岛模型中部署的微服务用于两个租户的场景。这些微服务在图的右侧被表示出来。每个微服务都关联有一个孤岛数据库,这些数据库也以孤岛模型部署。
您会发现,该解决方案的微服务也以孤岛模式运行。这意味着,在这些已部署微服务的整个生命周期内,它们将与特定的租户绑定。这种绑定为简化这些微服务的隔离机制提供了机会,使我们能够为每个微服务的计算资源绑定租户范围的策略,从而防止该微服务访问属于其他租户的资源。
这就是部署时隔离概念的用武之地。在图的左侧,我有一个微服务和一个模板化的隔离策略,用于为微服务定义访问范围。现在,当我的DevOps流程为每个微服务验证计算资源时,它可以将租户上下文插入隔离策略模板,并将该策略附加到计算基础设施。此过程对每个微服务重复执行,为每个租户微服务部署将租户上下文注入策略。
这种机制的副作用如图9-6右侧所示。当微服务尝试访问其关联的隔离数据库时,其访问权限将被限制在与当前租户上下文匹配的数据库范围内。例如,租户1尝试访问租户2的数据库时,会被部署时绑定的隔离策略阻止。
这种部署时模型具有强大功能。由于策略在部署时绑定,隔离策略的实现不依赖于微服务中的任何代码。如果代码尝试跨越租户边界,将被阻止。这实际上将合规性从微服务开发人员的视图中移除,使其更多地成为DevOps和部署流程的一部分。虽然这为实现隔离提供了出色方案,但它依赖于孤岛模型才能工作,而我们显然不希望仅仅为了实现这一好处而将所有资源孤立。
现在,让我们将关注点转向那些拥有共享资源的环境。这是运行时隔离模型通常应用的场景。在运行时隔离中,我们开始关注依赖应用程序代码或其他构造动态获取并应用隔离策略的策略。图9-7展示了运行时隔离模型中各组件的概念视图。
图9-7
在此示例中,计算资源在所有租户之间共享。由于这些计算资源必须能够处理来自所有租户的请求,因此必须部署为覆盖所有租户的策略(步骤1)。这意味着,在运行时,我的微服务必须能够访问与该服务关联的任何租户数据库(如图所示)。在此模式下,微服务代码的职责是使用当前租户上下文动态范围和控制对这些租户资源的访问。
该流程的流开始于顶部,我的租户访问微服务并传递其租户上下文(步骤2)。现在,在我的微服务计算环境中的某个位置,我需要使用租户上下文来填充策略并获取用于访问下游资源的租户范围凭据。为了这个概念视图,我包含了一个隔离管理器来执行此过程(步骤3)。实际上,获取租户范围凭证的方式可能因技术栈和语言不同而差异显著。您可以为微服务添加封装层、使用sidecar、采用切面编程——可选方案非常丰富。但关键在于,必须通过某种机制从动态生成的策略中获取租户范围凭证,并将其提供给微服务(步骤4)。
一旦微服务获取了这些租户范围的凭据,它将使用这些凭据访问与其关联的租户资源。在此示例中,我采用为每个租户单独配置数据库的方案,并通过凭据以上下文方式访问各数据库(步骤5)。在此场景中,假设租户2发起了请求,且该请求将租户2插入到策略中。随后,在此作用域下,我对资源的访问将被限制在租户2的数据库中。即使我的代码尝试在数据库访问请求中插入其他租户标识符,该请求也不会返回其他租户的数据。
您可以看出,这种方法高度依赖于我的解决方案中的代码和库。这确实为微服务提供了绕过您的隔离机制进行选择的空间。然而,您在开发人员视图之外的代码中注入隔离上下文的工作越充分,您就越有可能确保整体隔离策略的合规性。
值得注意的是,这里还有其他策略可供选择,可以将运行时解析移出微服务范围。图9-8展示了一个示例场景,其中凭证从微服务外部注入。
图9-8
在示例中,您会看到我在微服务前端添加了一个API网关。该网关会预处理请求,将租户上下文传递给隔离管理代码,后者会根据隔离策略使用该上下文获取租户范围的凭证。这一过程与之前示例中描述的流程非常相似。我只是将范围凭证的解析逻辑移到了API网关。一旦网关获取到凭证,就会将其传递给微服务,微服务再利用这些凭证来限制对租户资源的访问权限。这种模型有几个优势:它将凭证解析移出微服务开发人员的视线范围。此外,它还为缓存范围凭证提供了更自然的机会,以解决潜在的延迟问题。然而,此模型的缺点是隔离策略移出了微服务。通常,策略的范围定义被视为微服务的一部分,并与其实现紧密相关。这种策略打破了这一思维模式(至少在一定程度上)。如果网关被多个微服务使用,这种情况尤为明显。
通过拦截实现隔离
可用于运行时隔离拦截策略的技术和语言构造多种多样。挑战在于可选方案的组合方式过于繁多,难以一一涵盖。不过,仍有一些值得关注的共通主题。图9-9展示了两种常见的拦截策略概念视图。
图9-9
在左侧,您将看到一种基于语言或框架的拦截方法。在此处应用的工具通常会插入到您的代码执行路径中,在微服务代码无法察觉的情况下拦截并预处理请求。方面(Aspects)、中间件(Middleware)和封装库(WrapperLibraries)是可用的选项之一。采用此方法,您本质上是在拦截入站请求,并利用租户上下文获取作用域凭证以访问租户资源。这种方法仍符合我之前描述的运行时应用模型,但对开发人员配合应用隔离规范的依赖程度较低。
另一种模型(如右侧所示)采取了略微不同的方法。在此模型中,您在资源与微服务之间放置一个机制,该机制会拦截每次访问租户资源的尝试(类似于代理)。该机制在资源被访问时解析租户上下文并应用它,这就是您在使用sidecars等概念实现此拦截方案时会看到的情况。
这是一个不断发展的领域,新的概念和机制不断涌现。这些策略的更广泛价值在于,即使在运行时强制执行的模型中,它们也能提供更健壮且更集中管理的隔离方案。然而,这些策略是否适合您的具体需求,将取决于您的解决方案的隔离要求、所使用的技术栈,以及在某些情况下,环境中包含的语言或框架。
可扩展性考虑
虽然运行时应用隔离非常有效,但它可能会在您的环境中引入扩展问题。如果您有一个处理大量请求的池化服务,且每个请求都会获取租户范围的凭据,这可能会在您的环境中引入不合理的延迟。这将影响您系统整体的扩展能力。
在这种情况下,您可能需要考虑优化或调整运行时应用隔离模型以满足系统需求。您可以引入缓存策略来存储作用域凭据。采用此方法时,部分系统会设置生存周期(TTL)参数来管理这些凭据的生命周期。图9-10展示了如何实现此缓存方案的示例。
图9-10
在图9-10中,我们看到了获取和应用缓存隔离凭据的完整生命周期。在左上角,有一系列租户通过API网关进入SaaS应用服务。在此特定场景中,假设租户3正在向服务发起首次请求。当该请求到达时,网关调用凭证管理器以获取租户3的范围凭证(步骤1)。该凭证管理器会尝试在缓存中查找凭证(步骤2)。假设租户3未被找到,此时凭证管理器将从策略管理器中获取租户范围的凭证(步骤3和4)。
凭据随后被放入缓存(步骤5),返回至API网关(步骤6),并注入到与下游服务交互的过程中(步骤7)。此时,凭据在缓存中会被分配一个TTL。当租户3再次调用时,系统会调用凭据管理器(步骤8)并从缓存中查找已存储的凭据(步骤9)。缓存中的凭据会被返回至API网关(步骤10),并注入到与下游服务的交互中(步骤11)。
在此模型中,凭证管理器和辅助组件并非独立的服务。它们均在同一进程内运行。部分团队可能会尝试将所有隔离机制集中到独立的服务中。通常,跨服务边界进行通信会为系统引入额外的延迟层,从而引发性能问题。虽然您可以将这些概念迁移到库或其他共享组件中,但我更倾向于将隔离管理请求的处理逻辑保留在同一进程内。最后,你还需要考虑所使用服务的扩展限制。例如,如果你使用IAM策略在AWS上实现隔离,就需要考虑所需策略的数量是否会超过服务限制。这就是为什么在我的示例中会大量使用策略模板,因为单个策略可以跨多个租户上下文使用。
实际案例
到目前为止,您应该已经掌握了设计隔离策略的核心原则。现在,让我们转向具体示例,展示隔离如何应用于包含隔离构造的环境。以下各节将提供跨不同隔离类型(我们在前文讨论过)的实现策略示例,将概念与实际解决方案相连接。
全栈隔离
让我们先看看用于隔离租户资源的更粗粒度的隔离构造。图9-11展示了在AWS云中实现全栈隔离部署模型时使用的几种不同隔离构造。
在图中,我展示了一系列全栈隔离环境。基本思路是,每个租户都拥有完全专用的基础设施资源。正如你所料,这种模型与用于在资源之间设置边界的现有工具和技术具有天然映射关系。出于相同原因,这也是构建者认为构建隔离方案的清晰且易于实现的领域。
图9-11
在此特定示例中,我展示了三种不同的隔离构造。在左上角,您可以看到一种基于租户的隔离模型,其中云提供商(AWS)中的每个账户用于定义隔离边界。由于账户默认阻止跨账户访问,这代表了在全栈孤岛环境中实现隔离的最简单构造之一。
在右上角,我演示了如何使用网络结构来隔离我的租户隔离区。在此示例中,我使用了亚马逊虚拟私有云(VPC)来隔离我的租户资源。与大多数网络构造类似,VPC提供了丰富的内置选项,用于配置网络进出流量。因此,我们为每个租户的资源实现了相对基本的隔离单元。
最后,在图的底部,我展示了一个使用AmazonElasticKubernetesService(EKS)实现全栈隔离模型的示例。通常,随着我们进入容器环境,你会发现实现隔离策略时有许多选项(我们在第10章中会详细探讨这些选项)。然而,在此场景中,我选择采取极端方案,为每个租户单独部署一个集群。这意味着您需要为每个租户provision一个完全独立的EKS环境,并依赖集群的天然边界来实现隔离。
资源级隔离
资源级隔离通常具有良好的构造,能够与隔离机制良好映射。当我们使用专用资源时,隔离只需找到一种能够控制对该资源访问的机制。图9-12提供了一个资源级实现的示例。
图9-12
此示例描述了一个使用AmazonRedshift(一种列式数据库)的隔离模型,其中每个租户被分配独立的集群(如图右侧所示)。在这些集群内,每个租户的数据均独立管理和访问。尽管集群是我们的隔离单元,但从概念上讲,它仍映射为一个资源。在此模型中,资源的边界可能采取多种形式。数据库、队列、分析集群——这些都是我在本场景中归类为资源的不同构造。
在图的左侧,您可以看到一个访问租户资源的微服务。该微服务通过数据访问库(DAL)管理与关联Redshift集群的交互。在处理每个请求时,DAL会使用当前租户上下文及其隔离策略,获取仅限于特定租户集群的凭据,从而限制访问范围。
此资源级隔离示例恰好使用了微服务的池化计算资源,因此依赖于运行时应用的隔离策略。作为对比,当解决方案采用隔离的计算资源(如图9-13所示)时,隔离策略会发生变化。
图9-13
在图9-13中,我们与前一个示例中大部分组件相同。不同之处在于,我们现在有专门为每个租户设计的独立微服务。这种隔离的架构使我能够调整隔离策略。与在运行时获取并解决隔离范围不同,该方案可在部署和配置运行微服务的计算资源时,直接绑定特定的隔离策略。现在,数据访问层(DAL)无需吸收任何额外开销或复杂性来应用隔离策略——这些策略已在部署时通过与计算资源绑定的策略自动应用。
项级隔离
项级隔离是隔离模型中最具挑战性的一种,主要因为它需要一种许多技术尚未广泛支持的细粒度级别。同时,在多租户环境中,随着对共享基础设施的强烈需求,你会遇到许多需要临时制定策略以实现隔离在共享基础设施中运行的单个项的场景。
好消息是,在某些场景下(尤其是在云环境中),部分服务已内置对项级隔离的支持。为了让您更好地理解这种隔离模型在实际解决方案中的应用,让我们参考图9-14所示的项级隔离模型。
图9-14
在该示意图的右侧,您可以看到我有一个DynamoDB表,其中包含几个数据项。该表还包含一个分区键,用于存储与表中每个数据项关联的租户标识符(为简化示例,我将其显示为Tenant1、Tenant2等)。现在,为了对该表应用隔离,我需要限制请求仅访问与指定租户关联的项。
实现此隔离的策略如左侧所示。该策略利用AWS的IAM机制来定义DynamoDB表的访问范围。我已突出显示该策略中的“条件”部分,因为它声明了使用此策略时将被授予的访问范围。
授予使用此策略的任何人。我加粗了“dynamodb:LeadingKeys”,在此场景中,它用于限制对Tenant1的访问。
如果我们将所有组件组合在一起,流程将如下所示:(1)您的微服务根据当前租户上下文(本例中为Tenant1)填充策略;(2)您的代码使用此策略假定一个IAM角色,并获取一组凭据;(3)您的代码使用这些凭据访问表,将其访问范围限制为仅与Tenant1相关的项。如果收到针对Tenant2的另一个请求,我们会将该请求插入策略,假定角色,并获取Tenant2的凭据集。
这是在任何项级隔离中需要应用的基本原则。然而,您可能无法使用如此简单的IAM策略来实现隔离。您使用的工具可能会为项级隔离策略引入一些新挑战。主要目标是避免构建一个完全依赖于假设代码不会跨越租户边界的模型。
隔离策略管理
一旦确定了隔离策略,您还需要考虑通常包含在该策略中的政策的生命周期。政策存储的位置、所有者和管理员、部署的时间和方式、版本控制等,都是在创建和构建应用程序基础设施及微服务部署体验时需要考虑的问题。图9-15展示了管理隔离策略的两种潜在方法。
图9-15
这种方法最符合我的思路。我倾向于将隔离策略视为与微服务绑定交付物之一。作为微服务的所有者,我的团队也负责维护和定义确保微服务符合隔离要求的策略。这意味着我会将策略配置版本化、管理并提交到微服务仓库的相应范围之内。在此模式下,策略的所有权和管理权分散在系统中的各个服务中。
另一种思路(如图右侧所示)则倾向于通过集中化机制管理和版本控制所有策略。采用此方法,团队仍拥有其策略的更新权限,但所有策略均在微服务范围之外进行版本控制和管理。它们会被部署到一个中央位置,并由所有微服务引用。这为策略提供了统一的存储位置和独立部署模型,便于管理,并为架构各部分引用服务提供了统一的方案。
两种模型均有效,您可以考虑混合使用这些策略。关键在于,您需要超越这些隔离策略的具体应用方式,评估它们在更广泛的构建、版本控制和部署生命周期中如何以及在何处发挥作用。
在考虑如何操作隔离管理和部署时,您还需要思考如何引入测试来验证隔离是否真正有效。令人惊讶的是,这实际上是隔离实现中最具挑战性的方面之一。事实证明,没有天然的机制能够模拟租户跨越边界的情景。如果您已采取一切措施防止跨租户访问,您可能会发现,制定策略以有效模拟真实世界中的跨租户访问尤其具有挑战性。
虽然测试这些条件可能困难,但不进行测试似乎并非可行选项。部署隔离测试可帮助您潜在发现策略或隔离实现中的意外泄露。它们还确认了这些原则在结合使用时能产生预期效果。考虑到跨租户事件对业务的影响,很难为不投资于此领域找到合理理由。
真正的问题是:你如何在自己的环境中模拟一个跨租户事件?这必须在应用程序服务的内部实现。在某个时刻,你的代码将根据提供的租户上下文访问租户资源,而就在此时,你需要以某种方式注入一个无效的租户上下文。这可能意味着在代码中创建特殊路径或处理特殊情况,以允许注入无效租户上下文。这也是你可能考虑使用经典混沌工程策略来生成隔离异常的领域。
一个简单的示例可能是从数据库中为租户1获取一组项。这意味着周围的隔离构造将数据视图限制为属于租户1的资源。然而,在测试中,您将请求中的租户标识符替换为租户2的ID。这应返回无数据或错误,表明已越界。您还可以添加日志和操作策略,在运维控制台中触发警报,指示有人尝试跨越租户边界。
这可能感觉有些人为,您可能会找到更具创意的方式来模拟这些跨租户条件。然而,即使这感觉有些不自然,验证环境的隔离模型仍然至关重要。