Gateway API 实施者指南(Implementer's Guide)
原文:https://gateway-api.sigs.k8s.io/guides/implementers-guide/
关于构建一个 Gateway API 实现,你想知道的所有事情——只是一直没敢问。
本文档是收集各种"如何编写一个 Gateway API 实现"小贴士和技巧的地方——这些内容在底层类型的 godoc 字段里没有合适的位置可放。
它也意在记录一些指导原则,帮助本 API 的实施者避免常见的错误。
如果你打算作为最终用户使用本 API(而不是基于它构建东西),本文档可能对你帮助不大。
这是一份活文档——如果你发现漏掉了什么,欢迎提 PR!
关于 Gateway API,需要记住的重要事项
希望这些内容对大多人来说都不奇怪,但它们有一些并不明显的含义,我们会在这里一一列出。
Gateway API 是一个 kubernetes.io API
Gateway API 使用 gateway.networking.k8s.io API group。这意味着——与核心 Kubernetes 二进制中提供的 API 一样——每次发布时,这些 API 都已经过上游 Kubernetes 评审者的 review,和核心二进制中提供**的 API 一样。
Gateway API 通过 CRD 交付
Gateway API 以一组 CRD 的形式提供,版本受我们的版本策略控制。
该版本策略中最重要的部分是:看起来是同一个对象(即 group、version、kind 都相同)的资源,其模式可能略有不同。我们以兼容的方式进行变更,因此事情通常"就是会工作"。但实现需要做一些动作才能让"就是会工作"更可靠——这些见下。
这种"基于 CRD 交付"的模型也意味着:如果实现尝试在 CRD 尚未安装时使用(即 get、list、watch 等)Gateway API 对象,那么你的 Kubernetes 客户端代码很可能会返回严重的错误。处理这种情况的技巧也见下。
Gateway API 对象的 CRD 定义都包含两个特定的注解:
gateway.networking.k8s.io/bundle-version:<semver-release-version>gateway.networking.k8s.io/channel:<channel-name>
"bundle version"和"channel"("发布通道"的简写)的概念在版本化文档中有解释。
实现可以用它们来判断集群中是否安装以及安装了哪个 schema 版本。
Standard 通道 CRD 的变更保持向后兼容
Standard 通道 CRD 的合约的一部分是:同一个 API 版本内的变更必须是兼容的。注意:属于 Experimental 通道的 CRD 不提供任何向后兼容性保证。
虽然 Gateway API 版本策略总体上与上游 Kubernetes API 保持一致,但它也允许"对校验的修正"。例如,如果 API 规范声明某个值无效、但相应的校验未覆盖到该情况,那么未来的发布可能会新增校验来阻止这种无效输入。
这条合约也意味着:实现在 API 版本比它编写时所基于的版本更新时不会失败,因为由 Kubernetes 存储的较新 schema 一定能够被序列化为实现代码中使用的旧**版本。
类似地,如果实现基于一个更高的版本编写,那么它能够理解的新字段值将永远不会被使用——因为它们并不存在于较旧的版本中。
实现规则与指导
CRD 管理
关于如何管理 Gateway API CRD 的信息——包括何时把 CRD 安装与你的实现打包在一起是可接受的——请参考我们的 CRD 管理指南。
一致性与版本兼容性
一个合规的 Gateway API 实现是指通过了每个 Gateway API bundle 版本发布中随附的一致性测试的实现。
一个实现必须****没有任何跳过测试地通过一致性套件才算合规。开发过程中可以跳过测试,但你想要声明为合规的版本必须不跳过任何测试。
扩展特性可以根据 Extended 状态的合约被关闭。
Gateway API 一致性与版本强相关。一个对 N 版本通过一致性测试的实现未必对 N+1 版本通过一致性测试,除非你做相应变更。
实现应当把一致性测试套件生成的报告提交回 Gateway API 的 GitHub 仓库,详细记录他们的测试情况。
一致性套件的输出中包含所支持的 Gateway API 版本。
Union feature 一致性
部分特性只有在与其他特性组合实现时才有意义。我们把这种行为称为 union feature —— 一种辅助地作用于任何与之表面兼容的其他特性的特性。
例如,BackendTLSPolicy 只有在与会把流量转发到后端的 route 或 filter 组合时才有用——TLS 已终止的 route、带后端引用的 route、或像 RequestMirror 这样针对后端 Service 的 filter。类似地,ReferenceGrant 只有在与会进行跨命名空间引用的资源组合时才有意义(跨命名空间引用后端的 route、跨命名空间引用** Secret 的 Gateway 等)。
决定支持一个 union feature 的实现,只要该组合适用,就必须支持它与所报告的每个其他特性的组合——无论那些特性处于 Core 还是 Extended 支持级别。
下面这些例子可以参考:
- 一个报告支持
BackendTLSPolicy和GRPCRoute的实现必须支持:在一条GRPCRoute内使用BackendTLSPolicy来针对一个 Service。 - 一个报告支持
BackendTLSPolicy和TLSRouteTerminate的实现必须支持:在使用mode=Terminatelistener 的TLSRoute中,使用BackendTLSPolicy来针对一个 Service。 - 一个报告支持
BackendTLSPolicy和HTTPRouteRequestMirror的实现必须支持:当一条 route 定义了httproute.spec.rules[].filters.requestMirror.backendRef为 Service 类型时,使用BackendTLSPolicy。
依赖 Union feature 行为的特性必须在它们的 Enhancement Proposal 以及 API Reference 中明确说明。
发现 union feature 支持情况
实现通过 GatewayClass.status.supportedFeatures 字段报告它们所支持的特性。这是实施者和用户****了解实现支持哪些特性(从而支持哪些特性组合)的主要机制。
一致性测试套件通过其多特性测试门控自动校验 union feature 组合。每个一致性测试会声明它所要求的特性集。只有当实现声明支持该测试所列的全部特性时,该测试才会运行。这意味着:
- 如果实现声明同时支持
BackendTLSPolicy和GRPCRoute,那么那些要求这两个特性的测试会自动运行——如果这些测试失败,就说明该实现实际上并不支持该组合。 - 如果实现只声明支持
BackendTLSPolicy而没有GRPCRoute,那么这些组合测试会被跳过。 - 一致性报告的输出会按 profile 展示
supportedFeatures和unsupportedFeatures,让用户可以验证哪些组合被校验过。
简而言之,一致性套件让"声称支持某个 union feature 组合"但实际上"未通过其校验测试"变得不可能。
Union features 与一致性 profile
Union features 必须被列入****每个该组合适用的一致性 profile 的 ExtendedFeatures 集合中。这确保了:当一个实现对某个 profile 运行一致性测试并声明一个 union feature 时,该 profile 中的组合测试会被执行。
例如,BackendTLSPolicy 适用于任何涉及把流量通过 TLS 转发到后端的 profile。它因此****必须出现在 GATEWAY-HTTP、GATEWAY-GRPC 以及 GATEWAY-TLS profile 的 extended features 中——而不仅仅是某一个 profile。
Union features 可以处于不同的支持级别:
- Core union features:
ReferenceGrant在所有 Gateway profile 中都是 Core 特性。只要该 profile 被声明,对跨命名空间引用的支持就是强制的。因为它是 Core,所以实现在该 profile 中不能选择不支持它。 - Extended union features:
BackendTLSPolicy是一个 Extended 特性。实现可以选择不支持它,但如果它们支持了,就必须在它们报告的所有适用的特性组合中都**支持它。
版本兼容性
一旦 v1.0 发布,对于支持 Gateway 和 GatewayClass 的实现,它们必须设置一个新的 Condition:SupportedVersion。status: true 表示所安装的 CRD 版本受支持,status: false 表示不受支持。
标准 Status 字段与 Conditions
Gateway API 有很多资源,但在设计时,我们一直努力让不同对象的 status 体验尽可能保持一致——方法是使用 Condition 类型和 status.conditions 字段。
大多数资源都有 status.conditions 字段,但部分资源也有一个命名空间字段,其中包含一个 conditions 字段。
对于后者,Gateway 的 status.listeners 和 Route 的 status.parents 字段是典型的例子——slice 中每个元素都与某部分配置相关联的 Conditions。
对于 Gateway 来说,这样做是为了支持每个 Listener 一组 Conditions;对于 Route 来说,这样做是为了支持每个实现一组 Conditions(因为 Route 对象可能被多个 Gateway 使用,而这些 Gateway 又可能被不同的实现协调)。
所有这些情况中,都有一些相对通用的 Condition 类型,它们的含义也比较相似:
- Accepted:资源或其部分包含可接受的配置,将在实现控制的底层数据平面中产生一些配置。这并不意味着整个配置都是有效的,只说明有足够的部分是有效的,能产生某种**效果。
- Programmed:表示一个晚于 Accepted 的阶段——当资源或其部分****被 Accepted 并被配置到底层数据平面之后。用户应当预期配置很快就会准备好接流量。该 Condition 被设置时并不意味着数据平面已就绪,只说明一切都是有效的,它很快就会就绪**。"很快"在不同实现中可能含义不同。
- ResolvedRefs:该 Condition 表示资源或其部分****中的所有引用都****有效,且****指向的对象既存在也****允许该引用。如果该 Condition 被设置为
status: false,那么资源或其部分中至少有一个引用因某种原因无效**,且 message 字段****应当指明哪一个是无效的。
实施者应当检查每种类型的 godoc,以了解这些 Condition 在每个资源(或其部分)上的具体细节。
此外,上游的 Conditions 结构体包含一个可选的 observedGeneration 字段——实现必须使用该字段,并在生成 status 时把它设置为对象的 metadata.generation 字段。这让 API 的使用者可以判断该 status 是否对当前版本的对象仍然相关**。
TLS
TLS 在 Gateway API 中是一个大主题,相关能力还在持续扩展。这里有一份 TLS 指南——它从用户视角更深入地覆盖了这个话题;本节则试图从实施者视角补齐一些空白**。
Listener 隔离(Listener Isolation)
在 Gateway 内部,TLS 配置目前仅与 Listener 绑定。为了让这种做法可控,我们鼓励****所有实现朝"完整的 Listener 隔离"这一目标迈进(定义见下):
请求应当至多匹配一个 Listener。例如,如果为 foo.example.com 和 *.example.com 分别定义了 Listener,那么对 foo.example.com 的请求应当****只用**附加到 foo.example.com Listener 上的 Route 来路由(而****不是 *.example.com Listener 上的 Route)。
不支持 Listener 隔离的实现必须****清楚地在文档中说明这一点。未来,我们计划新增 HTTPS Listener 隔离一致性测试,以保证这一行为在声明支持该特性的实现之间保持一致**。最新进展见 #2803。
间接配置
有多种情况下,TLS 证书可能不直接由 Gateway 拥有者管理。虽然这里不打算做穷举性的罗列,但它记录了一些我们期望看到的、用于通过 Gateway API 管理 TLS 证书的做法:
1. 来自其他来源的证书
部分提供方支持把 TLS 证书配置并托管在 Kubernetes 之外。能够连接这些外部提供方的实现,可以在 Gateway Listener 的 TLS option 中暴露**该能力,例如:
listeners:
- name: https
protocol: HTTPS
port: 443
tls:
mode: Terminate
options:
vendor.example.com/certificate-name: store-example-com在本例中,store-example-com 这个名字指代了由外部 vendor.example.com TLS 证书提供方存储的一张证书的名称。
2. 自动生成的 TLS 证书(稍后填充)
许多用户更愿意让 TLS 证书自动为他们生成。一种可能的实现是:有一个控制器监听 Gateway 和 HTTPRoute,自动生成 TLS 证书并把它们挂到 Gateway 上。取决于实现细节,Gateway 拥有者****可能需要在 Gateway 或 Listener 层级做某些配置,以显式开启该特性。例如,假设有人创建了 acme-cert-generator 来按此模式生成证书。该生成器可以选择只在那些 tls.options 中设置了 acme.io/cert-generator(或为整个 Gateway 设置了类似的 annotation)的 Gateway Listener 上生成和填充证书。
注意:这与 [Cert Manager 目前的做法] (https://cert-manager.io/docs/usage/gateway/) 非常类似——但那需要 Gateway 拥有者引用一张 Kubernetes Secret,再由它填充。之所以必须这样做,是因为到 Gateway API v1.1 之前,TLS CertificateRefs 被强制要求指定。
伴随 v1.1 对 Gateway API 校验的放宽,TLS 证书在创建时可以留空,从而让使用自动生成 TLS 证书时配置更少。
3. 由其他角色指定的证书
在部分组织中,应用开发者负责管理 TLS 证书(见角色与身份了解更多关于这一点以及其他角色)。
为了支持这种用例,可以创建一个新的控制器和 CRD。该 CRD 将主机名与用户提供的证书关联起来,然后该控制器将该 CRD 中指定的证书填充到那些主机名匹配的 Gateway listener 上。这也可能得益于** Listener 或 Gateway 级别的显式开启机制。
TLS 扩展的总体指导
在 Gateway API 之上构建 TLS 扩展时,务必遵循以下指导:
- 对任何 TLS Option 或 Annotation,都使用带有域名前缀且对你的实现唯一的名称。(例如,使用
example.com/certificate-name,而不只是certificate-name)。 - 不要把证书等敏感信息编码进 option 或 annotation 的值中。改为通过简洁易懂的引用名称**。虽然这些值技术上可以长达 253 字符,但我们强烈建议把值控制在 50 字符以内**,以保持整体可读性和 UX。
- 为了支持这些扩展,Gateway API v1.1+ 将不再要求在 Gateway Listener 上指定** TLS 配置。当一个 Gateway Listener 未指定足够的 TLS 配置时,实现****必须把该 Listener 的
ProgrammedCondition 设置为False,理由为InvalidTLSConfig。 - 无论你选择支持哪些扩展,都很重要的一点是**:支持****核心的 TLS 配置——这些核心配置旨在所有实现间保持可移植性。扩展在 API 中有其位置,但****所有实现仍****必须支持** API 的****核心能力。
资源细节
对每个当前可用的一致性 profile,都有一组资源是实现应当去协调的。
下面一节会逐个过每种 Gateway API 对象,并说明期望的行为。
GatewayClass
GatewayClass 只有一个主要的 spec 字段——controllerName。每个实现被期望申领一个带域名前缀的字符串值(如 example.com/example-ingress)作为它的 controllerName。
实现必须监听所有 GatewayClass,并协调那些 controllerName 匹配的 GatewayClass。该实现必须从这些 controllerName 匹配的 GatewayClass 集合中至少选择一个兼容的 GatewayClass,并通过在该 GatewayClass 上设置 Accepted Condition 为 status: true 来表示它接受**该 GatewayClass 的处理。任何 controllerName 匹配但没有被 Accepted 的 GatewayClass 必须把 Accepted Condition 设置为 status: false。
实现可以只从那一组本可接受的 GatewayClass 中挑出一个(如果它只能协调一个),或者——如果它有能力协调多个 GatewayClass——也可以按需挑出任意数量。
如果某个 GatewayClass 有使其不兼容的内容(在本文撰写时,唯一的可能原因是:存在一个指向实现不支持的 paramsRef 对象的指针),那么该实现应当把不兼容的 GatewayClass 标记为未 Accepted。
Gateway
Gateway 对象必须****在 spec.gatewayClassName 字段中引用一个存在、且被某个实现 Accepted 的 GatewayClass,该实现才会去协调它。
已超出协调范围的 Gateway 对象(例如,因为它们引用的 GatewayClass 被删除),实现可以作为删除流程的一部分清除它们的 status,但这不是****必须**的。
Routes
所有 Route 对象都有一些共性**:
- 它们必须****被附加到一个在范围内的父级,该实现才会认为它们可协调。
- 实现必须为每个在范围内的 Route 更新 status(使用带命名空间的
parents字段)。详见各 Route 类型的具体说明,但通常包括Accepted、Programmed以及ResolvedRefsConditions。 - 已超出范围的 Route 应当****不被更新** status,因为这种更新可能会覆盖任何新的拥有者。
observedGeneration字段会表明剩余的 status 是过时的。
HTTPRoute
HTTPRoute 路由的是未加密的、可被检视的 HTTP 流量。这包括在 Gateway 处终止的 HTTPS 流量(因为已被解密),从而让 HTTPRoute 能够使用 path、method 或 headers 等 HTTP 属性来做路由决策。
TLSRoute
TLSRoute 使用 SNI 头,对加密的 TLS 流量做路由——不解密该流量流——到对应**的后端。
TCPRoute
TCPRoute 把到达 Listener 的 TCP 流路由到给定的后端之一。
UDPRoute
UDPRoute 把到达 Listener 的 UDP 包路由到给定的后端之一。
ReferenceGrant
ReferenceGrant 是一种特殊资源,它由目标资源所在命名空间的拥有者使用,用以****有选择地允许来自其他命名空间的 Gateway API 对象的**引用。
ReferenceGrant 创建在它所授权被引用资源所在的那个命名空间内,从而允许来自其他命名空间、其他 Kind 或两者的**访问。
支持跨命名空间引用的实现必须****监听 ReferenceGrant,并协调任何指向某个对象的 ReferenceGrant——该对象被某个在范围内的 Gateway API 对象所引用**。