kind: APIService
metadata:
labels:
k8s-app: metrics-server
name: v1beta1.metrics.k8s.io
spec:
# k8s會(huì)轉(zhuǎn)發(fā)所有發(fā)送到 /apis/metrics.k8s.io/v1beta1/ 的請(qǐng)求
group: metrics.k8s.io
version: v1beta1
# 發(fā)送到 /apis/metrics.k8s.io/v1beta1/ 的請(qǐng)求會(huì)被轉(zhuǎn)發(fā)給這個(gè) service
service:
name: metrics-server
namespace: kube-system

而GVK(metrics.k8s.io/v1beta1)下的資源定義是在service背后的服務(wù)中定義,代碼地址:https://github.com/kubernetes/metrics/blob/master/pkg/apis/metrics/types.go#L31,你甚至可以暫時(shí)理解為和k8s的內(nèi)建類型(Pod、Deployment)的定義模式一致,沒有CRD實(shí)體。

參閱 https://kubernetes.io/docs/tasks/debug/debug-cluster/resource-metrics-pipeline/ 官方文檔的例子,發(fā)送到 /apis/metrics.k8s.io/v1beta1 的請(qǐng)求被轉(zhuǎn)發(fā)給以AA方式注冊(cè)到K8S的MetricsServer服務(wù)

$ kubectl get --raw "/apis/metrics.k8s.io/v1beta1/nodes/minikube" | jq '.'
{
"kind": "NodeMetrics",
"apiVersion": "metrics.k8s.io/v1beta1",
"metadata": {
"name": "minikube",
"selfLink": "/apis/metrics.k8s.io/v1beta1/nodes/minikube",
"creationTimestamp": "2022-01-27T18:48:43Z"
},
"timestamp": "2022-01-27T18:48:33Z",
"window": "30s",
"usage": {
"cpu": "487558164n",
"memory": "732212Ki"
}
}

其中對(duì) metrics.k8s.io/v1beta1 下的node資源的CURD請(qǐng)求將會(huì)交由對(duì)象 nodeMetrics 處理,核心代碼在 https://github.com/kubernetes-sigs/metrics-server/blob/master/pkg/api/node.go ,從下面的代碼可以看到,nodeMetrics 只要實(shí)現(xiàn) k8s.io/apiserver/pkg/registry/rest 包下的幾個(gè)關(guān)鍵接口,就可以為資源對(duì)象 node 賦予GET(實(shí)現(xiàn)rest.Getter接口)、LIST(實(shí)現(xiàn)rest.Lister接口),類似的,實(shí)現(xiàn)了 rest.Creater 接口就可以為資源對(duì)象實(shí)現(xiàn)CREAT功能,具體細(xì)節(jié)在下篇文章解釋,大家有個(gè)大概的概念就行。

var _ rest.KindProvider = &nodeMetrics{}
var _ rest.Storage = &nodeMetrics{}
var _ rest.Getter = &nodeMetrics{}
var _ rest.Lister = &nodeMetrics{}
var _ rest.Scoper = &nodeMetrics{}
var _ rest.TableConvertor = &nodeMetrics{}
var _ rest.SingularNameProvider = &nodeMetrics{}

既然K8S把請(qǐng)求轉(zhuǎn)發(fā)到 APIService 指定的 service 中,那么這個(gè)service背后的服務(wù)需要以K8S的規(guī)范來提供API服務(wù),這時(shí)候我們的 k8s.io/apiserver登場(chǎng)了,你可以基于 k8s.io/apiserver 包啟動(dòng)一個(gè)基于K8S規(guī)范的APIServer服務(wù),對(duì)外提供的API對(duì)象只要實(shí)現(xiàn) k8s.io/apiserver 中提供的接口,就能創(chuàng)建一個(gè)符合K8S規(guī)范的資源對(duì)象,本文不會(huì)告訴你如何使用 k8s.io/apiserver開發(fā)一個(gè) APIServer,我會(huì)在下一篇文章(文章已經(jīng)寫完了,排版好之后就會(huì)發(fā)布到公眾號(hào)上)中從源碼層面告訴你如何開發(fā)一個(gè)符合K8S規(guī)范的APIServer

下面我們對(duì)照著k8s源碼來學(xué)習(xí)k8s是如何實(shí)現(xiàn)把請(qǐng)求 代理 到APIService后的服務(wù)這個(gè)動(dòng)作,請(qǐng)認(rèn)真閱讀這段代碼,在后面的例子中會(huì)頻繁出現(xiàn)類似的處理邏輯

代碼地址:https://github.com/kubernetes/kube-aggregator/blob/5544326a401d11ff3c58aac83b0c60c30e129ae9/pkg/apiserver/handler_proxy.go#L100-L171

整體的流程大概是:

  1. 根據(jù)APIService的配置解析出用戶自定義服務(wù)的地址
  2. 構(gòu)造新的請(qǐng)求,把發(fā)送給Kube-APIServer請(qǐng)求的目標(biāo)地址修改為AA的SVC地址
  3. 基于APIService中的服務(wù)信息,構(gòu)造代理Handler

??重點(diǎn),第三步中 proxy.NewUpgradeAwareHandler ?創(chuàng)建一個(gè)協(xié)議升級(jí)感知的Handler,因?yàn)閗8s中的請(qǐng)求中有很多協(xié)議升級(jí)的場(chǎng)景(例如 pod exec 用到的spdy),NewUpgradeAwareHandler 方法會(huì)幫你處理好協(xié)議升級(jí)的場(chǎng)景,后面會(huì)經(jīng)??吹絅ewUpgradeAwareHandler這個(gè)方法的使用

func (r *proxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
value := r.handlingInfo.Load()
if value == nil {
r.localDelegate.ServeHTTP(w, req)
return
}
handlingInfo := value.(proxyHandlingInfo)
...
// write a new location based on the existing request pointed at the target service
location := &url.URL{}
location.Scheme = "https"
// 根據(jù)APIService的配置解析出用戶自定義服務(wù)的地址
rloc, err := r.serviceResolver.ResolveEndpoint(handlingInfo.serviceNamespace, handlingInfo.serviceName, handlingInfo.servicePort)
if err != nil {
klog.Errorf("error resolving %s/%s: %v", handlingInfo.serviceNamespace, handlingInfo.serviceName, err)
proxyError(w, req, "service unavailable", http.StatusServiceUnavailable)
return
}
// 構(gòu)造新的請(qǐng)求,把發(fā)送給Kube-APIServer請(qǐng)求的目標(biāo)地址修改為AA的SVC地址
location.Host = rloc.Host
location.Path = req.URL.Path
location.RawQuery = req.URL.Query().Encode()

// 對(duì)原始請(qǐng)求深拷貝構(gòu)建新的轉(zhuǎn)發(fā)請(qǐng)求
newReq, cancelFn := apiserverproxyutil.NewRequestForProxy(location, req)
defer cancelFn()

if handlingInfo.proxyRoundTripper == nil {
proxyError(w, req, "", http.StatusNotFound)
return
}

proxyRoundTripper := handlingInfo.proxyRoundTripper
upgrade := httpstream.IsUpgradeRequest(req)

proxyRoundTripper = transport.NewAuthProxyRoundTripper(user.GetName(), user.GetGroups(), user.GetExtra(), proxyRoundTripper)

// If we are upgrading, then the upgrade path tries to use this request with the TLS config we provide, but it does
// NOT use the proxyRoundTripper. It's a direct dial that bypasses the proxyRoundTripper. This means that we have to
// attach the "correct" user headers to the request ahead of time.
if upgrade {
transport.SetAuthProxyHeaders(newReq, user.GetName(), user.GetGroups(), user.GetExtra())
}
// ??!重點(diǎn),創(chuàng)建一個(gè)協(xié)議升級(jí)感知的Handler,因?yàn)閗8s中的請(qǐng)求中有很多協(xié)議升級(jí)的場(chǎng)景(例如 pod exec 用到的spdy),
// NewUpgradeAwareHandler 方法會(huì)幫你處理好協(xié)議升級(jí)的場(chǎng)景,后面會(huì)經(jīng)??吹絅ewUpgradeAwareHandler這個(gè)方法
// 的使用
// 基于APIService中的服務(wù)信息,構(gòu)造代理Handler
handler := proxy.NewUpgradeAwareHandler(location, proxyRoundTripper, true, upgrade, &responder{w: w})
if r.rejectForwardingRedirects {
handler.RejectForwardingRedirects = true
}
utilflowcontrol.RequestDelegated(req.Context())
// 這里請(qǐng)求就轉(zhuǎn)發(fā)到了用戶啟動(dòng)的服務(wù)上,
handler.ServeHTTP(w, newReq)
}

關(guān)于AA的原理介紹就結(jié)束了,讀者可以基于AA的原理繼續(xù)看下面的開源項(xiàng)目是如何做定制化開發(fā)

代理網(wǎng)關(guān)

Cluster-Gateway

一切的源頭都要從 https://github.com/oam-dev/cluster-gateway 這個(gè)項(xiàng)目開始,該項(xiàng)目作為 KubeVela 多集群功能的核心模塊,管控組件對(duì)不同集群的請(qǐng)求交由 cluster-gateway 組件代理,我們先通過這個(gè)組件來了解市面上的一些實(shí)現(xiàn)K8S-API代理的開源組件的實(shí)現(xiàn)原理

下面展示了Cluster-Gateway的設(shè)計(jì)圖,我們把項(xiàng)目中關(guān)于 Cluster-Gateway 的關(guān)鍵設(shè)計(jì)提煉為4點(diǎn):

  1. 組件是基于 apiserver-aggregation[1] 的方式提供擴(kuò)展API
  2. 基于AA模式抽象出 ClusterGateway 資源用來代表不同集群
  3. 受到 “service/proxy”, “pod/proxy” 這類子資源的設(shè)計(jì)影響,也為 ClusterGateway 設(shè)計(jì)了proxy子資源實(shí)現(xiàn)請(qǐng)求代理的核心邏輯
  4. hub集群存儲(chǔ)能夠訪問被納管集群的訪問憑證,你可以理解為kubeconfig,cluster-gateway使用這些kubeconfig完成請(qǐng)求的代理轉(zhuǎn)發(fā)

首先我們通過介紹 KubeVela 中的多集群應(yīng)用管理功能的設(shè)計(jì)和需求背景來了解 Cluster-Gateway 在多集群管理中發(fā)揮的作用。

下圖展示了KubeVela的一個(gè)多集群應(yīng)用是如何完成資源多集群的分發(fā),其中多集群應(yīng)用的配置是存放在Hub集群中,并被被Hub集群的 KubeVela 控制器管理,根據(jù)應(yīng)用的配置中的多集群調(diào)度策略,選擇不同的集群下下發(fā)資源配置。

講到這里,請(qǐng)大家思考一個(gè)這樣的問題:KubeVela是如何在控制器內(nèi)完成把應(yīng)用配置下發(fā)到不同的集群內(nèi)?

聰明的你可能會(huì)想到在控制器內(nèi)管理多個(gè)不同的集群的 client,需要向特定集群下發(fā)配置的時(shí)候切換到指定的集群client,這種方法會(huì)帶來很多問題:

  1. 1. 首先維護(hù)多個(gè)集群的client的成本就不必多說,
  2. 2. 當(dāng)有新集群納管到多集群的時(shí)候,又如何自動(dòng)化的注冊(cè)新的client,
  3. 3. 假設(shè)被納管集群因?yàn)榫W(wǎng)絡(luò)環(huán)境的特殊性不能直接通過管控集群直接訪問,如何處理

KubeVela給出了一個(gè)相對(duì)優(yōu)雅的解決方案,在管控集群中安裝以AA模式部署的Cluster-Gateway,所有發(fā)往納管集群的請(qǐng)求都會(huì)被轉(zhuǎn)為請(qǐng)求管控集群 clustergateway/proxy 子資源的請(qǐng)求,所有的請(qǐng)求鏈路都在管控集群內(nèi)完成,KubeVela控制器只要持有管控集群的KubeConfig和Client即可,具體如何實(shí)現(xiàn)請(qǐng)求的轉(zhuǎn)換,涉及到client-go的一個(gè)黑魔法,一會(huì)會(huì)詳細(xì)介紹。

請(qǐng)讀者注意下圖中標(biāo)紅部分的請(qǐng)求路徑的轉(zhuǎn)換,

  1. 首先KubeVela控制器請(qǐng)求cluster0集群創(chuàng)建deploy,該請(qǐng)求是直接發(fā)往管控集群的APIServer,經(jīng)過一些客戶端的黑魔法,該請(qǐng)求會(huì)被封裝為對(duì) clustergateway/proxy 子資源的請(qǐng)求,其中對(duì)deploy資源進(jìn)行操作的API路徑(/apis/apps/v1/deployments)會(huì)被附加在 clustergateway/proxy 后,所以最終請(qǐng)求被轉(zhuǎn)為 clustergateway/proxy/apis/apps/v1/deployments
  2. 因?yàn)閏lustergateway資源對(duì)象是通過AA的模式注冊(cè)到管控集群,該請(qǐng)求便會(huì)被轉(zhuǎn)到Cluster-Gateway,Cluster-Gateway提取出放在proxy資源后的正常請(qǐng)求,把封裝后的請(qǐng)求轉(zhuǎn)為對(duì)cluster0集群的Kube-APIServer的正常請(qǐng)求

大家再回去看看上面貼出的 Cluster-Gateway 的原理圖,是不是就清晰很多,下面統(tǒng)一回答遺留的幾個(gè)問題:

1.kubevela是如何在只持有管控集群的client的情況下完成請(qǐng)求路徑的轉(zhuǎn)換?

完成請(qǐng)求路徑轉(zhuǎn)換的核心代碼如下,代碼地址:https://github.com/kubevela/kubevela/blob/fdcdf659d89eb81e9b381794b2c72c5e7a6d1e85/pkg/cmd/factory.go#L97

func NewDefaultFactory(cfg *rest.Config) Factory {
copiedCfg := *cfg
copiedCfg.RateLimiter = DefaultRateLimiter
copiedCfg.Wrap(multicluster.NewTransportWrapper())
return &defaultFactory{cfg: &copiedCfg}
}

關(guān)鍵點(diǎn)是調(diào)用了rest.Config的Wrap方法來構(gòu)造 rest.Config 對(duì)象,這里你可以把 multicluster.NewTransportWrapper() 理解為一個(gè) Middleware,所有client的請(qǐng)求都會(huì)經(jīng)過這個(gè)中間層處理發(fā)往 APIServer

 // RoundTrip is the main function for the re-write API path logic
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
cluster := t.getClusterFor(req)
If !IsLocal(cluster) {
req = req.Clone(req.Context())
req.URL.Path = formatProxyURL(cluster, req.URL.Path)
}
return t.delegate.RoundTrip(req)
}

經(jīng)常使用client-go的同學(xué)知道,在對(duì)資源進(jìn)行CURD的時(shí)候,調(diào)用的方法都會(huì)傳遞 context,kubevela在這個(gè)ctx里注入了想要請(qǐng)求的集群信息,在 Middleware 處理 http.Request 的時(shí)候,可以通過 http.Request 的 Context方法獲取到client-go傳遞的 ctx,并提取集群信息,用來構(gòu)建轉(zhuǎn)換路徑,感興趣的同學(xué)可以看下 t.getClusterFor(req) 里的實(shí)現(xiàn)。

2.cluster-gateway是如何完成請(qǐng)求的代理?

cluster-gateway 抽象出了 clustergateways/proxy 子資源實(shí)現(xiàn)了 Connecter 接口(關(guān)于這個(gè)接口的細(xì)節(jié)會(huì)在下一篇文章討論,如果不理解也沒關(guān)系),用來代理發(fā)往被納管集群的請(qǐng)求,以pod/proxy為例,這個(gè)子資源也是實(shí)現(xiàn)了 Connecter 接口用來代理發(fā)往Pod內(nèi)進(jìn)程的請(qǐng)求,這里思路做一些轉(zhuǎn)換你可以把被納管的集群當(dāng)成Pod進(jìn)程,這里也是簡(jiǎn)單做了一層代理,只是后面的服務(wù)變成了k8s。

// Connecter is a storage object that responds to a connection request.
type Connecter interface {
// Connect returns an http.Handler that will handle the request/response for a given API invocation.
// The provided responder may be used for common API responses. The responder will write both status
// code and body, so the ServeHTTP method should exit after invoking the responder. The Handler will
// be used for a single API request and then discarded. The Responder is guaranteed to write to the
// same http.ResponseWriter passed to ServeHTTP.
Connect(ctx context.Context, id string, options runtime.Object, r Responder) (http.Handler, error)

// NewConnectOptions returns an empty options object that will be used to pass
// options to the Connect method. If nil, then a nil options object is passed to
// Connect. It may return a bool and a string. If true, the value of the request
// path below the object will be included as the named string in the serialization
// of the runtime object.
NewConnectOptions() (runtime.Object, bool, string)

// ConnectMethods returns the list of HTTP methods handled by Connect
ConnectMethods() []string
}

我們來看下 clustergateways/proxy 在 Connect 方法中是如何完成的代理,代碼地址:https://github.com/oam-dev/cluster-gateway/blob/48259e095bb66d5a98b4203def038b359e408391/pkg/apis/cluster/v1alpha1/clustergateway_proxy.go#L157

return &proxyHandler{
parentName: id,
path: proxyOpts.Path,
impersonate: proxyOpts.Impersonate,
clusterGateway: clusterGateway,
responder: r,
finishFunc: func(code int) {
metrics.RecordProxiedRequestsByResource(proxyReqInfo.Resource, proxyReqInfo.Verb, code)
metrics.RecordProxiedRequestsByCluster(id, code)
metrics.RecordProxiedRequestsDuration(proxyReqInfo.Resource, proxyReqInfo.Verb, id, code, time.Since(ts))
},
}, nil

Connect方法最終返回了一個(gè)實(shí)現(xiàn)了http.Handler 接口的 proxyHandler 對(duì)象,在這個(gè)對(duì)象的ServerHTTP方法內(nèi)完成請(qǐng)求的代理 ,下面列出了ServerHTTP方法的部分實(shí)現(xiàn),請(qǐng)讀者回顧一開始我們分析 AA 模式是如何實(shí)現(xiàn)請(qǐng)求的代理,和這里的代理功能實(shí)現(xiàn)原理基本一致,正如我之前提到的,后面你還會(huì)見到類似的代碼。唯一的不同點(diǎn)是AA是通過APIService對(duì)象里的信息構(gòu)造轉(zhuǎn)發(fā)請(qǐng)求,cluster-gateway則是通過存在secret里的納管集群的kubeconfig構(gòu)造轉(zhuǎn)發(fā)請(qǐng)求。

下面是通過restconfig構(gòu)造請(qǐng)求的核心代碼,代碼地址:https://github.com/oam-dev/cluster-gateway/blob/48259e095bb66d5a98b4203def038b359e408391/pkg/apis/cluster/v1alpha1/clustergateway_proxy.go#L222

....
// 該方法返回一個(gè)http.RoundTripper對(duì)象,該對(duì)象會(huì)根據(jù)提供的
// rest.Config定義的身份驗(yàn)證信息和k8s-apiserver交互
rt, err := restclient.TransportFor(cfg)
if err != nil {
responsewriters.InternalError(writer, request, errors.Wrapf(err, "failed creating cluster proxy client %s", cluster.Name))
return
}
// 和前面AA的構(gòu)造代理Handler所調(diào)用的NewUpgradeAwareHandler是同一方法
proxy := apiproxy.NewUpgradeAwareHandler(
&url.URL{
Scheme: urlAddr.Scheme,
Path: newReq.URL.Path,
Host: urlAddr.Host,
RawQuery: request.URL.RawQuery,
},
rt,
false,
false,
nil)
...
proxy.UpgradeTransport = apiproxy.NewUpgradeRequestRoundTripper(
upgrading,
RoundTripperFunc(func(req *http.Request) (*http.Response, error) {
newReq := utilnet.CloneRequest(req)
return upgrader.RoundTrip(newReq)
}))
proxy.Transport = rt
proxy.FlushInterval = defaultFlushInterval
proxy.Responder = ErrorResponderFunc(func(w http.ResponseWriter, req *http.Request, err error) {
p.responder.Error(err)
})
proxy.ServeHTTP(writer, newReq)

3.如何實(shí)現(xiàn)權(quán)限管理?

在cluster-gateway這種多集群管理模式下權(quán)限管理是必須要實(shí)現(xiàn)的能力,cluster-gateway在轉(zhuǎn)發(fā)請(qǐng)求的時(shí)候使用的RestConfig在管控集群是具有admin權(quán)限的,因此在轉(zhuǎn)發(fā)請(qǐng)求的時(shí)候不能無腦轉(zhuǎn)發(fā),需要在轉(zhuǎn)發(fā)時(shí)做到權(quán)限收斂。cluster-gateway權(quán)限控制的功能是基于k8s的角色扮演機(jī)制 https://kubernetes.io/docs/reference/access-authn-authz/authentication/#user-impersonation 詳細(xì)的原理請(qǐng)讀者自己深入研究。

        
// rest.Config 的 Impersonate用于填充你想扮演的角色信息,比如你是admin的用戶
// 但是你可以扮演 其他只有讀權(quán)限的用戶,這樣cluster-gateway轉(zhuǎn)發(fā)的請(qǐng)求也只有只讀權(quán)限
// Impersonate is the configuration that RESTClient will use for impersonation.
Impersonate ImpersonationConfig

大概的實(shí)現(xiàn)方式是,在通過restconfig構(gòu)造代理server的時(shí)候,配置好 Impersonate 信息,其實(shí)最終轉(zhuǎn)發(fā)請(qǐng)求的時(shí)候,是把角色扮演信息注入在請(qǐng)求的Header中,完成角色扮演信息的傳遞

Impersonate-User: songyang
Impersonate-Group: developers

當(dāng)管控集群接收到轉(zhuǎn)發(fā)的請(qǐng)求的時(shí)候,會(huì)用header中指定的角色去校驗(yàn)授權(quán)

4.當(dāng)管控面和被納管集群之間的網(wǎng)絡(luò)不通的場(chǎng)景下是如何完成代理的?

cluster-gateway使用了ANP的能力通過隧道打通Hub集群和被納管集群之間的網(wǎng)絡(luò),這部分實(shí)踐不多,便不再多說,感興趣的同學(xué)可以翻閱 https://kubernetes.io/docs/tasks/extend-kubernetes/setup-konnectivity/文檔 和開源項(xiàng)目 https://github.com/kubernetes-sigs/apiserver-network-proxy

關(guān)于cluster-gateway的介紹到此為止,本文不會(huì)太多聚焦到代碼上,感興趣的讀者建議下載代碼閱讀,總結(jié)下來cluster-gateway實(shí)現(xiàn)了很優(yōu)雅的集群請(qǐng)求轉(zhuǎn)發(fā),巧妙的通過AA的方式為Hub集群支持多集群管理能力,是一種在k8s整個(gè)框架下的api增強(qiáng)。希望大家了解到原理后,能夠舉一反三擴(kuò)寬k8s下增強(qiáng)能力開發(fā)的思路。

市面上有很多在API測(cè)做增強(qiáng)的開源項(xiàng)目,https://github.com/oam-dev/cluster-gateway 似乎是我見到最早開源的(僅表個(gè)人看法),而且開源前已經(jīng)在螞蟻內(nèi)部生產(chǎn)環(huán)境使用了很久。

KubeGateway

KubeGateway[2] 是字節(jié)跳動(dòng)在2年前開源出來的項(xiàng)目,當(dāng)時(shí)一同開源的還有 KubeZoo[3],這2個(gè)項(xiàng)目都是在 K8S-API 層面實(shí)現(xiàn)了功能增強(qiáng),后面的文章會(huì)對(duì) https://github.com/kubewharf/kubezoo 進(jìn)行深入的分析。

KubeGateway 定位是為字節(jié)內(nèi)部的 kube-apiserver 的 HTTP2 流量專門設(shè)計(jì)并定制的七層負(fù)載均衡代理,可以根據(jù)七層流量特性完成定制化的請(qǐng)求轉(zhuǎn)發(fā),比如對(duì)pod的讀請(qǐng)求轉(zhuǎn)發(fā)到某個(gè)apiserver,把寫請(qǐng)求轉(zhuǎn)發(fā)到另一個(gè)apiserver完成讀寫分離,或者限制pod的list的請(qǐng)求速率,限制某個(gè)用戶的請(qǐng)求。

在介紹原理之前,讀者可以根據(jù)上文已經(jīng)了解到的知識(shí)背景自己思考一下如何設(shè)計(jì)出這樣一個(gè)服務(wù),我們一起來思考下:

這只是一個(gè)代理,但是需要承接所有用戶的請(qǐng)求,用戶需要對(duì)這層代理無感知,你會(huì)說我直接使用常見的7層代理,但是注意這個(gè)代理是面向k8s-apiserver的代理,需要適配k8s的所有請(qǐng)求特征,并不是一個(gè)簡(jiǎn)單的7層代理轉(zhuǎn)發(fā)就能完成的,比如

  1. 1. 用戶信息是從kubeconfig的用戶憑證里獲取到,需要解析下token或者用戶證書
  2. 2. k8s api里很多verb語句,比如 watch,list動(dòng)作需要從七層的請(qǐng)求信息中分析出

所以聰明的你,可能會(huì)想到我們可以用之前AA的思路,基于k8s.io/apiserver 這個(gè)構(gòu)建一個(gè)7層代理,利用這個(gè)包天然能夠兼容k8s的api,通過使用這個(gè)包下的增強(qiáng)能力,可以輕松的實(shí)現(xiàn)提取用戶信息,解析k8s請(qǐng)求的功能,用戶對(duì)某個(gè)集群的請(qǐng)求可以使用我們前面提到的proxy的能力轉(zhuǎn)發(fā)到對(duì)應(yīng)的k8s-apiserver上。

沒錯(cuò),KubeGateway的整體設(shè)計(jì)和我們分析的思路基本差不多。這個(gè)項(xiàng)目的搭建也是基于k8s.io/apiserver 這個(gè)庫,不過字節(jié)應(yīng)該是對(duì)部分功能做了增強(qiáng),自己fork了一版,具體增加的代碼細(xì)節(jié)就不再深究。

下面的官方的設(shè)計(jì)圖,本文稍微做了一些注解,client端請(qǐng)求的域名中會(huì)帶有請(qǐng)求的集群信息,以clusterA.k8s.kubegateway.io為例,表示這次請(qǐng)求的面向后面上游的Cluster A集群,KubeGateway會(huì)通過解析域名來判斷該請(qǐng)求是轉(zhuǎn)發(fā)給ClusterA的kube-apiserver,在轉(zhuǎn)發(fā)請(qǐng)求的過程中會(huì)經(jīng)過基本的鑒權(quán)和授權(quán)認(rèn)證,其實(shí)就是把用戶信息封裝向被代理集群發(fā)起 Authentication Request (TokenReview) 和 Authorization Request (SubjectReview) 來驗(yàn)證用戶身份和授權(quán),完成鑒權(quán)和授權(quán)之后就到了我們老朋友出馬的時(shí)候,把請(qǐng)求代理到目標(biāo)集群。

關(guān)于如何基于k8s.io/apiserver這個(gè)庫搭建一個(gè)符合k8s規(guī)范的apiserver我們會(huì)放在下一篇文章分析,這里大家只要知道基于這個(gè)庫就能實(shí)現(xiàn)非常神奇的功能。

下面我們簡(jiǎn)單的講解KubeGateway的核心代碼,第一部分是KubeGateway是如何一步步完成請(qǐng)求的解析,鑒權(quán)授權(quán)和代理轉(zhuǎn)發(fā)

代碼地址:https://github.com/kubewharf/kubegateway/blob/43b7830e9973f45971655a92564c88e0c9b4caed/cmd/kube-gateway/app/proxy.go#L139

buildProxyHandlerChainFunc 會(huì)把每一步驟像洋蔥一樣一層一層封裝起來,用戶請(qǐng)求會(huì)從最下面的handler一層一層的被解析,有幾個(gè)關(guān)鍵的handler:

  1. WithRequestInfo 會(huì)把用戶請(qǐng)求解析為k8s標(biāo)準(zhǔn)的請(qǐng)求,并存儲(chǔ)在ctx中,包括請(qǐng)求的資源(pods),請(qǐng)求動(dòng)作(list、watch),所在命名空間(ns)等
  2. WithAuthentication和WithFailedAuthenticationAudit完成用戶身份認(rèn)證和授權(quán)
  3. 最后WithDispatcher完成請(qǐng)求的轉(zhuǎn)發(fā)到不同的集群apiserver中
func buildProxyHandlerChainFunc(clusterManager clusters.Manager, enableAccessLog bool) func(apiHandler http.Handler, c *genericapiserver.Config) http.Handler {
return func(apiHandler http.Handler, c *genericapiserver.Config) http.Handler {
// new gateway handler chain
handler := gatewayfilters.WithDispatcher(apiHandler, proxydispatcher.NewDispatcher(clusterManager, enableAccessLog))
// without impersonation log
handler = gatewayfilters.WithNoLoggingImpersonation(handler, c.Authorization.Authorizer, c.Serializer)
// new gateway handler chain, add impersonator userInfo
handler = gatewayfilters.WithImpersonator(handler)
handler = genericapifilters.WithAudit(handler, c.AuditBackend, c.AuditPolicyChecker, c.LongRunningFunc)
failedHandler := genericapifilters.Unauthorized(c.Serializer, c.Authentication.SupportsBasicAuth)
failedHandler = genericapifilters.WithFailedAuthenticationAudit(failedHandler, c.AuditBackend, c.AuditPolicyChecker)
handler = genericapifilters.WithAuthentication(handler, c.Authentication.Authenticator, failedHandler, c.Authentication.APIAudiences)
handler = genericfilters.WithCORS(handler, c.CorsAllowedOriginList, nil, nil, nil, "true")
// disabel timeout, let upstream cluster handle it
// handler = gatewayfilters.WithTimeoutForNonLongRunningRequests(handler, c.LongRunningFunc, c.RequestTimeout)
handler = genericfilters.WithWaitGroup(handler, c.LongRunningFunc, c.HandlerChainWaitGroup)
// new gateway handler chain
handler = gatewayfilters.WithPreProcessingMetrics(handler)
handler = gatewayfilters.WithExtraRequestInfo(handler, &request.ExtraRequestInfoFactory{})
handler = gatewayfilters.WithTerminationMetrics(handler)
handler = gatewayfilters.WithRequestInfo(handler, c.RequestInfoResolver)
if c.SecureServing != nil && !c.SecureServing.DisableHTTP2 && c.GoawayChance > 0 {
handler = genericfilters.WithProbabilisticGoaway(handler, c.GoawayChance)
}
handler = genericapifilters.WithCacheControl(handler)
handler = gatewayfilters.WithNoLoggingPanicRecovery(handler)
return handler
}
}

第二部分我們看下我們的老朋友是如何完成請(qǐng)求的代理:

代碼地址:https://github.com/kubewharf/kubegateway/blob/43b7830e9973f45971655a92564c88e0c9b4caed/pkg/gateway/proxy/dispatcher/dispatcher.go#L53

  1. 1. 從 endpointPicker 選擇需要代理的kube-apiserver endpoint
  2. 2. 基于選出的endpoint構(gòu)造新的代理請(qǐng)求
  3. 3. 基于被代理的集群的訪問憑證,構(gòu)造代理Handler,再細(xì)看下 NewUpgradeAwareHandler 函數(shù)的實(shí)現(xiàn),果然見到了親切的 proxy.NewUpgradeAwareHandler 函數(shù)
// 1. 從 endpointPicker 選擇需要代理的kube-apiserver endpoint
endpoint, err := endpointPicker.Pop()
if err != nil {
    d.responseError(errors.NewServiceUnavailable(err.Error()), w, req, statusReasonNoReadyEndpoints)
    return
}

ep, err := url.Parse(endpoint.Endpoint)
if err != nil {
    d.responseError(errors.NewInternalError(err), w, req, statusReasonInvalidEndpoint)
    return
}

...
// 2. 基于選出的endpoint構(gòu)造新的代理請(qǐng)求
location := &url.URL{}
location.Scheme = ep.Scheme
location.Host = ep.Host
location.Path = req.URL.Path
location.RawQuery = req.URL.Query().Encode()
...

rw := responsewriter.WrapForHTTP1Or2(delegate)
// 3. 基于被代理的訪問憑證構(gòu)造 proxy handler
proxyHandler := NewUpgradeAwareHandler(location, endpoint.ProxyTransport, endpoint.PorxyUpgradeTransport, false, false, d, endpoint)
proxyHandler.ServeHTTP(rw, newReq)

// NewUpgradeAwareHandler creates a new proxy handler with a default flush interval. Responder is required for returning
// errors to the caller.
func NewUpgradeAwareHandler(location *url.URL, transport http.RoundTripper, upgradeTransport proxy.UpgradeRequestRoundTripper, wrapTransport, upgradeRequired bool, responder proxy.ErrorResponder, endpoint *clusters.EndpointInfo) *UpgradeAwareHandler {
    handler := proxy.NewUpgradeAwareHandler(location, transport, wrapTransport, upgradeRequired, responder)
    handler.UpgradeTransport = upgradeTransport
    return &UpgradeAwareHandler{
        UpgradeAwareHandler: handler,
        endpoint:            endpoint,
    }
}

這里簡(jiǎn)單的介紹了 KubeGateway 的實(shí)現(xiàn)原理,不過這個(gè)組件的能力不僅如此,他很多靈活的基于7層請(qǐng)求的轉(zhuǎn)發(fā)限流等功能實(shí)現(xiàn)也很有意思,這些都依賴讀者自己研究了。

Karmada-Aggregated-APIServer

這里簡(jiǎn)單介紹一下karmada多集群方案中的 karmada-aggregated-apiserver 組件,他是karmada push多集群管理模式下,管控面集群管理被納管集群的代理服務(wù),他也是抽象了cluster和cluster/proxy的概念,和ocm的cluster-gateway采用同樣的思路來代理發(fā)往不同集群的請(qǐng)求,實(shí)現(xiàn)方式和我們上面講解的思路也大同小異,不過他加入了一些增強(qiáng)功能,可以支持http和socks5協(xié)議的代理,感興趣的同學(xué)也可以下載研究一下,代碼入口在 https://github.com/karmada-io/karmada/blob/master/cmd/aggregated-apiserver/main.go。

總結(jié)

通過對(duì)比幾個(gè)開源項(xiàng)目的實(shí)現(xiàn),大家會(huì)發(fā)現(xiàn)這些項(xiàng)目的應(yīng)用場(chǎng)景和多集群密切相關(guān),cluster-gateway和karamda-aggregate-apiserver作為多集群代理,kubegateway則是支持為多個(gè)集群做負(fù)載均衡,等介紹完后面2類開源項(xiàng)目 自定義k8s-apiserver 和 多租k8s 后,大家會(huì)有更深刻的體會(huì)。系列文章的最后,會(huì)給大家介紹一種我司已經(jīng)上線了的基于k8s.io/apiserver構(gòu)建的服務(wù),也非常有意思,另外本文關(guān)于一些實(shí)現(xiàn)細(xì)節(jié)沒有深究,這些都會(huì)放在下一篇文章內(nèi)講解

參考文檔

https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/apiserver-aggregation/

https://www.zeng.dev/post/2023-k8s-apiserver-aggregation-internals/

引用鏈接

[1] apiserver-aggregation: https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/apiserver-aggregation/
[2] KubeGateway: https://github.com/kubewharf/kubegateway/blob/main/README.zh_CN.md
[3] KubeZoo: https://github.com/kubewharf/kubezoo

文章轉(zhuǎn)自微信公眾號(hào)@云原生AI百寶箱

上一篇:

LLM實(shí)戰(zhàn)?|?使用ChatGPT?API提取文本topic

下一篇:

AnalyticDB(ADB)+LLM:構(gòu)建AIGC時(shí)代下企業(yè)專屬Chatbot
#你可能也喜歡這些API文章!

我們有何不同?

API服務(wù)商零注冊(cè)

多API并行試用

數(shù)據(jù)驅(qū)動(dòng)選型,提升決策效率

查看全部API→
??

熱門場(chǎng)景實(shí)測(cè),選對(duì)API

#AI文本生成大模型API

對(duì)比大模型API的內(nèi)容創(chuàng)意新穎性、情感共鳴力、商業(yè)轉(zhuǎn)化潛力

25個(gè)渠道
一鍵對(duì)比試用API 限時(shí)免費(fèi)

#AI深度推理大模型API

對(duì)比大模型API的邏輯推理準(zhǔn)確性、分析深度、可視化建議合理性

10個(gè)渠道
一鍵對(duì)比試用API 限時(shí)免費(fèi)