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类型。例子中的Label和Status 是Object 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 Schema 、Resolver 、Subscriber ,可以定义一个简单的GraphQL模式,用于Pod查询。
搜索集群和建立GraphQL APIs
现在是应用client-go,这被认为是Go与Kubernetes集群交互的最佳选择,以实现resolver 和subscriber 方法。
至于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 集合。关于详细的答案,请关注我的下一篇文章。
谢谢你的阅读!