本文回顾了我参与 KubeAdmiral 开源项目的机缘巧合、实现方案,以及所获得的感悟。一方面,这是对我的经历的记录;另一方面,我希望这些分享能对开源新人,对 KubeAdmiral 项目感兴趣的新入门者有所帮助。
我目前在浙江大学SEL实验室攻读硕士学位,研究方向是混部集群优化,主要研究工作集中在弹性伸缩场景中如何在减少QoS违约的同时提高CPU利用率的优化问题。
我的Github:zhy76 (Haiyu Zuo) --- zhy76 (Haiyu Zuo) (github.com)
因为实验室是云原生方向,导师和学院也鼓励我们多参与开源项目,在开源方面,我参与过多个CNCF下的云原生开源项目,最开始是实验室江南学长带我了解开源,带我为阿里云Sealer社区贡献,让我打开了开源的大门,后来又参与谷歌编程之夏(GSoC),GLCC开源夏令营,LFX实习计划,先后为KubeArmor,Katalyst,Karmada项目做过开源贡献。目前是Karmada member和Sealer member。
我最开始了解KubeWharf社区是在字节云原生的公众号上,那时Katalyst刚刚开源,当时的我怎么也想不到后续我也能有幸参与到KubeWharf社区的贡献。后来在2023年的暑假,我报名了GLCC开源夏令营实习计划,并在众多开源项目中选择了和我研究方向最贴切的Katalyst项目,被选中后我有幸成为了Katalyst项目的第一个外部贡献者,并在KubeCon和KubeWharf社区的PC,贺哥,伟哥成功面基。
也是这个暑假,机缘巧合下,实验室的铁成和程哲学长带我了解了Karmada这个多云项目,为我打开了多云领域的大门。我为Karmada做过一段时间贡献后,也有幸被LFX实习计划选中,为Karmada的自动提升依赖方面做出显著贡献,并成为Karmada member。
再后来,在多云领域探索和了解了一段时间的我,关注到字节的多云项目KubeAdmiral开源,并在11月份开启了开源编程挑战活动,活动的课题二也很有挑战性,于是随即写了一份Proposal提交报名,很荣幸被社区选中,能够参与到KubeAdmiral社区支持提供代理 API 供用户访问成员集群资源这一有挑战性的课题中,并得到汉波哥的指导。
KubeAdmiral 是基于 Kubernetes Federation v2 迭代演进而来,旨在提供云原生多云多集群的管理和应用分发能力。Kubernetes Federation v2 提供了 FederatedDeployment, FederatedReplicaSet, FederatedSecret 等部分资源,在调度上支持副本数调度,良好的支持无状态的 Deployment 应用;KubeAdmiral 在其基础上做了如下增强:
- 兼容原生 Kubernetes API。
- 提供更灵活的调度框架,支持丰富的调度分发策略。
- 差异化策略。
- 依赖调度/跟随调度。
- 提供状态收集的框架,提供更灵活的状态收集。
- 大规模实践下的功能和稳定性增强。
背景
用户在使用KubeAdmiral时可能需要查看各个成员集群中应用资源的分布情况,但是频繁登录每个云提供商的网站或切换kubeconfig 上下文会降低用户的使用体验。如果我们提供代理API来访问成员集群资源,将大大提高用户使用KubeAdmiral的便利性和效率。这个提议旨在在 KubeAdmiral 中引入代理 API,使用户能够在不登录每个云提供商的网站或切换 kubeconfig 上下文的情况下访问成员集群之间的资源。
目标
- 开发一个代理api server,实现统一的 API 端点,用于访问 KubeAdmiral 中的成员集群资源,类似于
/apis/aggregated.kubeadmiral.io/v1alpha1/aggregations/{clustername}/proxy
。允许用户直接通过kubeAdmiral访问成员集群中的资源。 - 利用成员集群中现有的 RBAC 进行身份验证和授权,确保无缝和安全的访问。
方案设计
通过调研,发现现在主流的多云开源项目如Karmada,OCM,Clusternet都利用 Kubernetes 的Aggregated APIServer(AA)方法来设计和实现代理 API,我们在这里也采用同样的方法。这种方法将使 KubeAdmiral 能够充当中介,处理对成员集群的请求。
总体方案架构设计如下:
API设计
新增Aggregations API定义,Aggregations为aggregated-apiserver定义了一个虚拟API端点,用于处理统一API端点访问请求。ClusterProxyOptions是集群代理请求的查询配置,用于配置请求的URL。Path是URL的一部分,它包括集群、后缀和用于当前对集群的代理请求的参数。 例如,如果整个请求的URL为http://localhost/apis/aggregated.kubeadmiral.io/v1alpha1/aggregations/{clustername}/proxy/api/v1/nodes
,那么Path为api/v1/nodes。
// Aggregations defines a virtual API endpoint for aggregated apiserver.
type Aggregations struct {
metav1.TypeMeta `json:",inline"`
}
// +k8s:conversion-gen:explicit-from=net/url.Values
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// ClusterProxyOptions is the query options to a Cluster's proxy call.
type ClusterProxyOptions struct {
metav1.TypeMeta `json:",inline"`
// Path is the part of URLs that include clusters, suffixes,
// and parameters to use for the current proxy request to cluster.
// For example, the whole request URL is
// <http://localhost/apis/aggregated.kubeadmiral.io/v1alpha1/aggregations/{clustername}/proxy/api/v1/nodes>
// Path is api/v1/nodes
// +optional
Path string `json:"path,omitempty" protobuf:"bytes,1,opt,name=path"`
}
统一API 端点
使用API服务器聚合(AA)功能,在 KubeAdmiral 中实现 API 端点,代理对成员集群的请求,允许用户向成员集群发出请求,而无需直接与其 API 交互。可以实现集群代理(Cluster Proxy)的能力,如同service/proxy,node/proxy,pod/proxy的能力一样,对请求进行代理,这样可以代理请求而不需要对kube-apiserver进行侵入式修改。
- 开发新的aggregated-apiserver。
配置好对应的Options,生成Config后,便可以新建一个Aggregated Apiserver:
// New returns a new instance of Server from the given config.
func (c completedConfig) New() (*Server, error) {
genericServer, err := c.GenericConfig.New("aggregated-apiserver", genericapiserver.NewEmptyDelegate())
if err != nil {
return nil, err
}
s := &Server{
GenericAPIServer: genericServer,
}
apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(aggregatedapiserver.GroupName, Scheme, ParameterCodec, Codecs)
restStorage, err := storage.NewREST(
c.ExtraConfig.FederatedInformerManager,
c.ExtraConfig.RestConfig,
klog.Background().WithValues("aggregated-apiserver", "aggregations"),
)
if err != nil {
klog.ErrorS(err, "Unable to new aggregation rest")
return nil, err
}
if err != nil {
klog.Errorf("Unable to create REST cluster storage: %v", err)
return nil, err
}
v1alpha1storage := map[string]rest.Storage{}
v1alpha1storage["aggregations"] = restStorage.Cluster
v1alpha1storage["aggregations/proxy"] = restStorage.Proxy
apiGroupInfo.VersionedResourcesStorageMap[v1alpha1.SchemeGroupVersion.Version] = v1alpha1storage
if err := s.GenericAPIServer.InstallAPIGroup(&apiGroupInfo); err != nil {
return nil, err
}
return s, nil
}
该Apiserver将能够处理/aggregations/proxy路径的请求,并将请求的逻辑封装在restStorage.Proxy对象中的方法中。
- 实现proxy代理功能,使用户的请求能够被代理到member cluster。
大体流程图如下:
- ConnectCluster Function: 这是流程的开始点,代表了调用**
ConnectCluster
**函数的起始点。 - GetTlsConfigForCluster: 用于获取与集群相关的TLS配置。TLS配置是用于安全通信的必要配置。
- Construct Location & Proxy Transport: 通过对应集群对象的
APIEndpoint
字段构造目标集群的URL位置(Location)和代理传输(Proxy Transport)。这些是用于建立与目标集群的连接的重要元素。 - Location: 表示目标集群的URL位置,它将被用于路由流量到正确的集群。
- New Proxy Handler: 代理处理程序负责接受传入的HTTP请求并将其转发到目标集群。
- Handle HTTP Request: 处理传入的HTTP请求,包括设置头部信息和代理URL。
- NewUpgradeAwareHandler: 创建一个新的处理程序,该处理程序具有升级意识,可以处理升级请求,还用于处理HTTP请求。
- Serve HTTP Request: 这个步骤表示处理HTTP请求并将它们转发到目标集群。
统一认证鉴权
KubeAdmiral将多集群的访问入口集中到了控制面apiserver上,因此,统一认证在控制面便可解决。
除了要便捷的访问成员集群外,我们还需要考虑权限问题,从而保证用户请求的安全性。
在统一鉴权方面,利用Kubernetes的用户伪装(User Impersonation)功能实现这一点。
当用户把他们的集群加入到KubeAdmiral时,KubeAdmiral会在这个集群的”kube-admiral-system”命名空间下创建一个和ClusterName同名的ServiceAccount,并将其生成的token收集到KubeAdmiral的控制面板中。
具体的,KubeAdmiral会在成员集群加入控制面时,使用createAuthorizedServiceAccount方法在成员集群中创建ServiceAccount和ServiceAccount的令牌(Token)Secret,并创建一个RBAC ClusterRole和ClusterRoleBinding授予控制平面管理成员集群资源所需的权限,该角色和绑定允许由ServiceAccount Name标识的ServiceAccount访问成员集群中所有名称空间中的所有资源,这个ServiceAccount和令牌将被host集群用于访问成员集群的API Server。
用户通过KubeAdmiral聚合的API服务器(aggregated-apiserver)向成员集群发送请求时,系统会使用存储在KubeAdmiral控制面板中的相应集群之前收集的“SA Token Secret” token,并附带要伪装的用户的Header信息,来访问成员集群的kube-apiserver。之后的过程就和在单个集群内部访问一样,k8s的impersonate能力会伪装成相应的用户来进行访问请求。
统一认证鉴权流程如下:
- token获取:获取存储在 kubeadmiral 控制面板中的token。
- 通过 kubeadmiral-aggreated-apiserver 发送请求:当用户想要与成员集群交互时,他们通过 kubeadmiral-aggreerated-apiserver 发送请求。
- 使用Token:使用从对应成员集群收集的token来认证访问对应成员集群apiserver。
- 附加Header信息:除了Impersonator Token之外,请求还包括特定用户的Header信息,设置
Impersonate-User
和Impersonate-Group
来模拟对应用户或者组,该用户和组需要在目标成员集群中存在对应的角色和绑定,这样的话就可以通过模拟该用户或组的方式获取对应用户或组的权限。 - 模拟和访问:认证通过后,模拟对应用户或组来访问成员集群的 kube-apiserver,就好像该请求是由模拟用户发出的一样,该请求具有该用户访问成员集群资源的权限。
- 流程完成:请求被处理,响应通过 kubeadmiral-aggreated-apiserver 转发回原始请求者。
使用独立API
尽管使用 gin、go-restful 等 go 语言 web 框架可以轻易地构建出一个稳定的 API 接口服务,但以 Kubernetes 原生的方式构建 API 接口服务还是有很多优势,例如:
- 能利用 Kubernetes 原生的认证、授权、准入等机制,有更高的开发效率;
- 能更好的和 K8s 系统融合,借助 K8s 生态更快的推广自己的产品,方便用户上手;
- 借助于 K8s 成熟的 API 工具及规范,构建出的 API 接口更加规范整齐;
但是在很多场景下,我们还是不能确定到底使用聚合 API(Aggregated APIServer)还是独立 API 来构建我们的服务,官方为我们提供了两种选择的对比:
考虑 API 聚合的情况 | 优选独立 API 的情况 |
---|---|
在开发新的 API | 已经有一个提供 API 服务的程序并且工作良好 |
希望可以是使用 kubectl 来读写你的新资源类别 | 不要求 kubectl 支持 |
希望在 Kubernetes UI (如仪表板)中和其他内置类别一起查看你的新资源类别 | 不需要 Kubernetes UI 支持 |
希望复用 Kubernetes API 支持特性 | 不需要这类特性 |
有意愿取接受 Kubernetes 对 REST 资源路径所作的格式限制,例如 API 组和名字空间。 | 你需要使用一些特殊的 REST 路径以便与已经定义的 REST API 保持兼容 |
API 是声明式的 | API 不符合声明式模型 |
资源可以自然地界定为集群作用域或集群中某个名字空间作用域 | 集群作用域或名字空间作用域这种二分法很不合适;你需要对资源路径的细节进行控制 |
使用CRD
除了聚合 API,官方还提供了另一种方式以实现对标准 kubernetes API 接口的扩展:CRD(Custom Resource Definition ),能达到与聚合 API 基本一样的功能,而且更加易用,开发成本更小,但相较而言聚合 API 则更为灵活。针对这两种扩展方式如何选择,官方也提供了相应的参考。
通常,如果存在以下情况,CRD 可能更合适:
- 定制资源的字段不多;
- 你在组织内部使用该资源或者在一个小规模的开源项目中使用该资源,而不是在商业产品中使用;聚合 API 可提供更多的高级 API 特性,也可对其他特性进行定制;例如,对存储层进行定制、对 protobuf 协议支持、对 logs、patch 等操作支持。
两种方式的核心区别是定义 api-resource 的方式不同。在 Aggregated APIServer 方式中,api-resource 是通过代码向 API 注册资源类型,而 Custom Resource 是直接通过 yaml 文件向 API 注册资源类型。
简单来说就是 CRD 是让 kube-apiserver 认识更多的对象类别(Kind),Aggregated APIServer 是构建自己的 APIServer 服务。虽然 CRD 更简单,但是缺少更多的灵活性。
参与本次课题,遇到的挑战很多,首先是我以前没有过开发自定义apiserver的经验,通过从0到1的学习,掌握了开发aggregated apiserver的方法,其次,在开发过程中遇到更多的是认证和鉴权的问题,开发完成后,发现请求发不过去,或者遇到请求未认证,未授权的bug。总的来说,我从本次课题中收获良多,对于多云统一访问,k8s的aggregated apiserver开发流程,k8s apiserver的处理流程,认证和鉴权机制都有了更深的认识。