Go中的Kubernetes GraphQL查询

390 阅读6分钟

Go中的Kubernetes GraphQL查询

构建GraphQL服务器来搜索集群中的Kubernetes资源

在共享集群的治理中,我们的部分责任是允许用户访问他们的资源,我们通过RBAC分配合理的权限,并通过kubectl实现用户的查询。对于后端开发人员来说,"无障碍",而对于移动、网络和数据开发人员等用户来说,则不是这样,他们不熟悉命令行操作或kubectl。

那么,我们应该如何为我们的用户打通这个障碍呢?

我们做了一个UI查询界面,提供查询服务和API,如REST或GraphQL,以确保更容易的访问,更高的平台可见性,以及更好的用户体验。而在实施方面,我们用Go进行了GraphQL服务。

为什么是GraphQL

REST和GraphQL是实现前端与后端交互时的两种流行选择。前者是多年来的互联网标准,后者是Facebook开源的API查询语言,其基础知识你可以在graphql.org上找到。而这两者之间有很多比较,但在这里我只贴出来自graphcms.com/blog/graphq…它最能直观地说明两者的区别。

所以,我们不难得出结论,为什么我们选择GraphQL而不是REST。

  • GraphQL更灵活

它的对手REST是僵硬的。以Pod 查询(name, namespace, labels, container_name, maintainer)为例,我们可以找到原因。REST通常设计多查询API,如GET /pod/:name, GET /pod/:namespace 。而在返回的数据需要定制,不需要所有信息而只需要某些字段的场景下,只有两种方法。

  • 返回所有字段并与客户端一起过滤,当响应包含几十个字段时,网络开销大大增加。
  • 服务器根据需求定制返回的数据,这对后端工作负载的影响很大。

在Kubernetes中,有几十种资源类型,每种类型至少支持三种查询,因此要设计几百个REST API,更不用说将来的维护了,这是多么令人难以承受的事情。

GraphQL拯救了我们。它没有几十个API,可以让客户端独立选择数据内容,服务器准确返回目标数据。此外,它还为客户端提供了统一的格式来获取数据,无论数据类型是什么,都能以更严谨、可扩展、可维护的方式进行。

query {  Pod(namespace: $namespace, name: $name) {    metadata {      name      namespace      //labels      //annotations    }    status {      conditions {        lastTransitionTime        message        reason        status        type      }    }    spec {      //spec fields    }  }}
  • GraphQL对我们的用户来说更熟悉。

Backstage是一个在许多大公司中广泛使用的平台,它给予GraphQL很好的支持,并将许多内部实现建立在GraphQL的基础上,使我们的查询API更容易被用户接受,因为他们对它非常熟悉。

在Go中启用GraphQL

模式(GraphQL schema language)是GraphQL的核心,描述了我们要查询的数据模型,其中最基本也是最关键的是抽象和定义对象类型。

在Go中开发GraphQL时,我们使用框架graphql-go,在此基础上完成GraphQL模式的定义。这4个元素是

  • 类型模式,它定义了查询名称,查询使用的参数,以及查询返回的字段和类型。
  • Resolver,用于填充返回信息的回调方法。
  • 订阅者,用于增量更新返回信息的回调方法。
  • Mutation,修改数据的方法(在此不做详细介绍)。

类型模式

定义字段。

type Resource {  name: String!  labels: [Label!]  status: Status!}

Type 是就像Java中的 ,Go中的 ,由一组字段组成,每个字段都有一个相应的类型。如例子所示,有不同类别的类型,其中Class struct Scalar Type是最常见的一种,如例子中的 ,以及下面的许多类型。String

GraphQL也支持自定义的Scalar类型。例子中的LabelStatusObject Type ,它允许我们像定义类图一样定义GraphQL类型,并把它们联系起来。

[],!,[]!Type Modifier ,用来标记Field 为数组或不为空,如[Label!] 表示labels 是由Label 类型组成的数组,数组可以为空,但Label 不能为空。

对于GraphQL支持的其他界面类型联合类型输入类型,如果感兴趣,可以参考文档中的例子。

而在graph-go 方面,也有一对一的对应类型,如graphql.String ,每个字段默认为空,如果需要非空,你可以用graphql.NewNonNull 来包装这个类型。

解析器(Resolver

赋值或解析字段是下一个步骤。

Resolver 以你定义的任何方式为返回填充数据。而为了查询Kubernetes集群中的资源,这里使用了client-go。

订阅者

通过注册Subscriber ,它与List/Watch 非常相似,我们可以随后更新GraphQL数据。如果你熟悉Kubernetes Informer Pattern,就不会对它感到陌生。

由于我们使用Kubernetes查询,client-go informer是一个完美的匹配。但是当查询数据库或像Kafka这样的消息存储时,一定有其他的方法来支持。

Type SchemaResolverSubscriber ,可以定义一个简单的GraphQL模式,用于Pod查询。

搜索集群和建立GraphQL APIs

现在是应用client-go,这被认为是Go与Kubernetes集群交互的最佳选择,以实现resolversubscriber 方法。

至于Pod查询,我们直接使用clientset API,将它们与我们上面定义的各种参数相结合。

对于订阅者,我们用Informer定制了Add 事件的更新,返回一个通道,用graphql-go 框架处理来自订阅者的通道更新消息。

测试

让我们用Gohttpserver ,一步一步地启动GraphQL服务器。

  • 创建graphqlHandler
graphqlHandler := handler.New(&handler.Config{   Schema:     &schema,   Pretty:     true,   GraphiQL:   false,   Playground: true,})
  • 定义http handler
http.Handle("/graphql", graphqlHandler)
  • 启动server
err = http.ListenAndServe(":8080", nil)

现在在http://localhost:8080/graphql 上进行测试。

输入

query {  Pod(namespace: "prometheus") {    name    status  }}

然后我们得到

{   "data":{      "Pod":{         "name":"prometheus-khdf12",         "status":"Running"      }   }}

部署

在将程序封装成docker镜像并部署到集群上之后,我们现在就可以向用户开放服务了。

Dockerfile 👇

需要注意的是,WorkloadIdentity应该被配置为在GKE集群中启用客户端-go。详见《集群治理--定期清理资源》。

进一步的步骤

我们完成的Kubernetes Pod的GraphQL查询远非完美,我们希望返回完整的Pod信息而不是简单的PodShort

然而,在手动返回Pod字段的道路上,还存在一些问题。

  • 繁琐。可能有几十个字段,它们被逐层嵌套。
  • 不利于维护。如果Kubernetes在未来的版本中丢弃或增加一些字段,代码更新是不可避免的。
  • 不利于扩展。一个Pod类型需要繁琐的定义,如果把它扩展到集群中的几十个甚至几百个类型和CRD怎么办?这似乎是一个不可能的任务。

有没有一种灵活和可扩展的方法呢?简短的答案是应用client-go来获取集群中的CRD定义,并将其解析为一个graph.Fields 集合。关于详细的答案,请关注我的下一篇文章。

谢谢你的阅读!