2. Kubernetes API 聚合开发
自定义资源实际上是为了扩展 kubernetes 的 API,向 kubenetes API 中增加新类型,可以使用以下三种方式:
- 修改 kubenetes 的源码,显然难度比较高,也不太合适
- 创建自定义 API server 并聚合到 API 中
- 创建自定义资源(CRD)
2.1 CRD存在的问题
- 只支持 etcd
- 只支持JSON,不支持 protobuf (一种高性能的序列化语言)
- 只支持2种子资源接口 ( /status 和 /scale)
- 不支持优雅删除
- 显著增加 api server 负担
- 只支持 CRUD 原语
- 不支持跨 API groups 共享存储
2.2 自定义API server相比CRD的优势
- 底层存储无关(像metrics server 存在内存里面)
- 支持 protobuf
- 支持任意自定义子资源
- 可以实现优雅删除
- 支持复杂验证
- 支持自定义语义
2.3 使用自定义API server前先考虑以下几点
- 你的 API 是否属于 声明式的
- 是否想使用 kubectl 命令来管理
- 是否要作为 kubenretes 中的对象类型来管理,同时显示在 kubernetes dashboard 上
- 是否可以遵守 kubernetes 的 API 规则限制,例如 URL 和 API group、namespace 限制
- 是否可以接受该 API 只能作用于集群或者 namespace 范围
- 想要复用 kubernetes API 的公共功能,比如 CRUD、watch、内置的认证和授权等
2.4 Kubernetes APIServer 和 自定义APIServer的关系
2.4.1 Kubernetes 的 Aggregated API是什么?
1.1 概述
Aggregated(聚合的)API server
是为了将原来的 API server 这个巨石(monolithic)应用给拆分开,为了方便用户开发自己的 API server 集成进来,而不用直接修改 Kubernetes 官方仓库的代码,这样一来也能将 API server 解耦,方便用户使用实验特性,简而言之,它是允许k8s的开发人员编写一个自己的服务,可以把这个服务注册到k8s的api里面,这样,就像k8s自己的api一样,自定义的服务只要运行在k8s集群里面,k8s 的Aggregate通过service名称就可以转发到我们自定义的service里面去了。这些 API server 可以跟 kube-apiserver
无缝衔接,使用 kubectl 也可以管理它们。
在 1.7+
版本及以后,聚合层apiserver和 kube-apiserver 一起运行。在扩展资源被注册前,聚合层不执行任何操,要注册其 API,用户必需添加一个 APIService
对象,该对象需在 Kubernetes API 中声明 URL 路径,聚合层将发送到该 API 路径(e.g. /apis/myextension.mycompany.io/v1/…)的所有对象代理到注册的 APIService。
通常,通过在集群中的一个 Pod 中运行一个 extension-apiserver
来实现 APIService。如果已添加的资源需要主动管理,这个 extension-apiserver 通常需要和一个或多个controller配对。
1.2 设计理念
- api的扩展性:这样k8s的开发人员就可以编写自己的API服务器来公开他们想要的API。集群管理员应该能够使用这些服务,而不需要对核心库存储库进行任何更改。
- 丰富了APIs:核心kubernetes团队阻止了很多新的API提案。通过允许开发人员将他们的API作为单独的服务器公开,并使集群管理员能够在不对核心库存储库进行任何更改的情况下使用它们,这样就无须社区繁杂的审查了。
- 开发分阶段实验性API的地方:新的API可以在单独的聚集服务器中开发,当它稳定之后,那么把它们封装起来安装到其他集群就很容易了。
- 确保新API遵循kubernetes约定:如果没有这里提出的机制,社区成员可能会被迫推出自己的东西,这可能会或可能不遵循kubernetes约定。
1.3 Kubernetes Aggregated原理解析
我们说的自定义API其实就是和Metrics Server的实现方式一样,都是通过注册API的形式来完成和Kubernetes的集成的,也就是在API Server增加原本没有的API。不过添加API还可以通过CRD的方式完成,不过我们这里直说聚合方式。看下图:
Kube-Aggregator类似于一个七层负载均衡,将来自用户的请求拦截转发给其他服务器,并且负责整个 APIServer 的 Discovery 功能。
通过APIServices对象关联到某个Service来进行请求的转发,其关联的Service类型进一步决定了请求转发形式。Aggregator包括一个GenericAPIServer和维护自身状态的Controller。其中 GenericAPIServer主要处理apiregistration.k8s.io组下的APIService资源请求。
主要controller包括:
- apiserviceRegistrationController:负责APIServices中资源的注册与删除;
- availableConditionController:维护APIServices的可用状态,包括其引用Service是否可用等;
- autoRegistrationController:用于保持API中存在的一组特定的APIServices;
- crdRegistrationController:负责将CRD GroupVersions自动注册到APIServices中;
- openAPIAggregationController:将APIServices资源的变化同步至提供的OpenAPI文档;
假设有两个路由分别访问API,实际上我们访问API的时候访问的是一个aggregator的代理层,下面橙色的都是可用的服务后端。我们访问上图中的2个URL其实是被代理到不同的后端,在这个机制下你可以添加更多的后端,比如举例说说Custome-metrics-apiserver绿色线条的路径。
注册自定义API文件
该文件的主要作用就是向Api server注册一个api,此API名称是关联到一个service名称上。
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
name: v1beta1.custom.metrics.k8s.io
labels:
api: custom-metrics-apiserver
apiserver: "true"
spec:
version: v1beta1 #API版本
group: custom.metrics.k8s.io #API所属的组
groupPriorityMinimum: 2000
service:
name: custom-metrics-apiserver #自定义API所关联的service名称,当访问这个自定义API后转发到哪个service处理,就根据这个service名称选择
namespace: default
versionPriority: 10
caBundle: "LS0tLS1CRUdJTiBDRVJUSUZJQ0"
上面定义了资源类型为APIService,service名称为custom-metrics-apiserver,空间为default的一个资源聚合接口。
下面带大家从源代码的角度来看,代码路径:
staging/src/k8s.io/kube-aggregator/pkg/apiserver/apiservice_controller.go ,和k8s其它controller一样,watch变化分发到add、update和delete方法
apiServiceInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: c.addAPIService,
UpdateFunc: c.updateAPIService,
DeleteFunc: c.deleteAPIService,
})
主要监听两种资源apiService和service,路径:
staging/src/k8s.io/kube-aggregator/pkg/apiserver/apiserver.go
func (s *APIAggregator) AddAPIService(apiService *v1.APIService) error {
// if the proxyHandler already exists, it needs to be updated. The aggregation bits do not
// since they are wired against listers because they require multiple resources to respond
if proxyHandler, exists := s.proxyHandlers[apiService.Name]; exists {
proxyHandler.updateAPIService(apiService)
if s.openAPIAggregationController != nil {
s.openAPIAggregationController.UpdateAPIService(proxyHandler, apiService)
}
return nil
}
proxyPath := "/apis/" + apiService.Spec.Group + "/" + apiService.Spec.Version
// v1. is a special case for the legacy API. It proxies to a wider set of endpoints.
if apiService.Name == legacyAPIServiceName {
proxyPath = "/api"
}
// register the proxy handler
proxyHandler := &proxyHandler{
localDelegate: s.delegateHandler,
proxyCurrentCertKeyContent: s.proxyCurrentCertKeyContent,
proxyTransport: s.proxyTransport,
serviceResolver: s.serviceResolver,
egressSelector: s.egressSelector,
}
proxyHandler.updateAPIService(apiService)
if s.openAPIAggregationController != nil {
s.openAPIAggregationController.AddAPIService(proxyHandler, apiService)
}
s.proxyHandlers[apiService.Name] = proxyHandler
s.GenericAPIServer.Handler.NonGoRestfulMux.Handle(proxyPath, proxyHandler)
s.GenericAPIServer.Handler.NonGoRestfulMux.UnlistedHandlePrefix(proxyPath+"/", proxyHandler)
// if we're dealing with the legacy group, we're done here
if apiService.Name == legacyAPIServiceName {
return nil
}
// if we've already registered the path with the handler, we don't want to do it again.
if s.handledGroups.Has(apiService.Spec.Group) {
return nil
}
// it's time to register the group aggregation endpoint
groupPath := "/apis/" + apiService.Spec.Group
groupDiscoveryHandler := &apiGroupHandler{
codecs: aggregatorscheme.Codecs,
groupName: apiService.Spec.Group,
lister: s.lister,
delegate: s.delegateHandler,
}
// aggregation is protected
s.GenericAPIServer.Handler.NonGoRestfulMux.Handle(groupPath, groupDiscoveryHandler)
s.GenericAPIServer.Handler.NonGoRestfulMux.UnlistedHandle(groupPath+"/", groupDiscoveryHandler)
s.handledGroups.Insert(apiService.Spec.Group)
return nil
}
结合上面的源码:
proxyPath := "/apis/" + apiService.Spec.Group + "/" + apiService.Spec.Version
例子就是/apis/custom-metrics.k8s.io/v1beta1,而处理方法请求的handle就是
// register the proxy handler