<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          利用 CRD 實現(xiàn)一個 mini-k8s-proxy

          共 26133字,需瀏覽 53分鐘

           ·

          2021-09-10 12:19

          前言

          進入正文前,需要熟悉以下幾個概念定義:

          • Custom Resources: 資源(Resource) 是 Kubernetes API 中的一個端點, 其中存儲的是某個類別的 API 對象(Objects)的一個集合。例如內(nèi)置的 pods 資源包含一組 Pod 對象。定制資源(Custom Resources)是對 Kubernetes API 的擴展,可以動態(tài)的在集群內(nèi)安裝刪除,對用戶而言,如同使用 pods 這種內(nèi)置資源方式一樣,可以使用 kubectl 來創(chuàng)建和訪問定制資源里面的定制對象(Custom Objects)。

          • CustomResourceDefinition(CRD): CRD 定制資源定義是 Kubernetes 提供的向集群中添加定制資源(Custom Resources)的一種方式,允許用戶定義定制資源。

          • Custom Controllers: 定制資源只能存取結(jié)構(gòu)化的數(shù)據(jù),而定制控制器(Custom Controllers)可以通過觀測分析將結(jié)構(gòu)化的數(shù)據(jù)解釋為用戶所期望狀態(tài)的記錄,并持續(xù)地維護該狀態(tài)。它可以用于任何類別的資源,若定制資源與定制控制器相結(jié)合,定制資源就能夠提供真正的聲明式 API(Declarative API)。例如,Traefik 的 IngressRoute,apache apisix 的 apisix-ingress-controller ,以及本文接下來的示例等,都是通過編寫 CRD 創(chuàng)建定制資源 + 使用 k8s.io/code-generator 實現(xiàn)定制控制器完成的。

          • Operator: Operator 是 Kubernetes 的擴展軟件,可以理解為一種特殊的 Controller ,旨在捕獲(正在管理一個或一組服務的)運維人員的關(guān)鍵目標。原理和 Controller 一樣,都是通過編寫 CRD 創(chuàng)建定制資源然后監(jiān)聽 CRD 相關(guān)變化并作出響應。區(qū)別就在于 Operator 可能會結(jié)合原生的 Controller(Deployment/Replicas),或者 Kubernetes 的網(wǎng)絡,存儲等來控制應用程序的狀態(tài)。通俗點講就是,Operator 是一個功能更強大,封裝了對于特定應用程序的運維經(jīng)驗的 Controller 。例如,etcd operator,prometheus operator 等。

          如果覺得上面的定義太枯燥了,那就由我來簡單的總結(jié)一遍吧:

          • Kubernetes 里面呢,有很多內(nèi)置的資源,資源中包括對象,例如 pods 資源包含著一組 Pod 對象

          • Kubernetes 為了方便用戶,還允許用戶創(chuàng)建自定義資源,用戶創(chuàng)建出來的資源就取了個高大上的名字叫定制資源,同理,定制資源里有定制對象,例如 Traefik 里面的 ingressroutes 定制資源中包含著一組 IngressRoute 對象

          • 用戶想要創(chuàng)建定制資源,可以通過編寫 CustomResourceDefinition(CRD,定制資源定義)來實現(xiàn)

          • 用戶創(chuàng)建完定制資源后,還可以編寫代碼來監(jiān)聽資源的創(chuàng)建、刪除、更新動作,這就是一個 Controller 控制器了

          • 用戶編寫的控制器如果是專注于某個特定的應用程序,并賦予了特定的領(lǐng)域知識,就形成了一個 Operator

          目標

          實現(xiàn)一個可以通過配置 host 攔截到匹配的請求域名,將流量代理轉(zhuǎn)發(fā)到具體的 service 中(通過配置 serviceName,namespace,port,scheme)的極簡網(wǎng)絡代理工具。其中,配置通過 CRD 創(chuàng)建,代理程序可以通過控制器監(jiān)聽配置變化,動態(tài)更新,無需重啟。(PS:其實就是簡單模擬了 Traefik IngressRoute 的實現(xiàn))

          創(chuàng)建 CRD

          將下面的 CustomResourceDefinition 保存為 crd.yaml 文件

          apiVersion: apiextensions.k8s.io/v1
          kind: CustomResourceDefinition
          metadata:
            # 名字必需與下面的 spec 字段匹配,并且格式為 '<名稱的復數(shù)形式>.<組名>'
            name: proxyroutes.miniproxy.togettoyou.com
          spec:
            # 組名
            group: miniproxy.togettoyou.com
            names:
              # kind 通常是單數(shù)形式的駝峰編碼(CamelCased)形式。你的資源清單會使用這一形式。
              kind: ProxyRoute
              # shortNames 允許你在命令行使用較短的字符串來匹配資源
              shortNames:
                - pr
              # 名稱的復數(shù)形式,用于 URL:/apis/<組>/<版本>/<名稱的復數(shù)形式>
              plural: proxyroutes
              # 名稱的單數(shù)形式,作為命令行使用時和顯示時的別名
              singular: proxyroute
            # 可以是 Namespaced 或 Cluster
            scope: Namespaced
            # 列舉此 CustomResourceDefinition 所支持的版本
            versions:
              - name: v1alpha1
                # 每個版本都可以通過 served 標志來獨立啟用或禁止
                served: true
                # 其中一個且只有一個版本必需被標記為存儲版本
                storage: true
                # schema 是必需字段
                schema:
                  # openAPIV3Schema 是用來檢查定制對象的模式定義
                  openAPIV3Schema:
                    type: object
                    properties:
                      spec:
                        type: object
                        properties:
                          # 定義我們需要的幾個配置項
                          host:
                            type: string
                          serviceName:
                            type: string
                          namespace:
                            type: string
                          port:
                            type: integer
                          scheme:
                            type: boolean

          之后創(chuàng)建它:

          kubectl apply -f crd.yaml

          現(xiàn)在,就可以創(chuàng)建定制對象啦,將下面的 YAML 保存為 example.yaml

          apiVersion: miniproxy.togettoyou.com/v1alpha1
          kind: ProxyRoute
          metadata:
            name: example-proxyroute
          spec:
            # 監(jiān)聽域名
            host: whoami.togettoyou.com
            # 假設(shè)你有一個 whomai 的 service,位于 default 命名空間,容器內(nèi)部端口為 80 ,http 協(xié)議
            serviceName: whoami
            namespace: default
            port: 80
            scheme: false

          并執(zhí)行創(chuàng)建命令:

          kubectl apply -f example.yaml

          結(jié)合上文的定義介紹,復習一遍,在這里 proxyroutes 就是我們通過 CRD 創(chuàng)建的定制資源,其中包含著一組 ProxyRoute 對象。

          現(xiàn)在可以使用 kubectl 來查看我們剛才創(chuàng)建的定制對象:

          $ kubectl get proxyroute
          NAME                 AGE
          example-proxyroute   49s

          $ kubectl get pr
          NAME                 AGE
          example-proxyroute   50s

          實現(xiàn)控制器

          創(chuàng)建項目目錄如下:

          ├─pkg
          │  └─apis
          │      └─miniproxy
          │          └─v1alpha1
          │              └─doc.go
          │              └─register.go
          │              └─types.go
          │          └─register.go
          ├─script
          │  └─boilerplate.go.txt
          │  └─code-gen.sh
          │  └─codegen.Dockerfile

          doc.go 代碼:

          // +k8s:deepcopy-gen=package
          // +groupName=miniproxy.togettoyou.com

          // Package v1alpha1 is the v1alpha1 version of the API.
          package v1alpha1

          register.go 代碼:

          package v1alpha1

          import (
           "mini-k8s-proxy/pkg/apis/miniproxy"

           metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
           "k8s.io/apimachinery/pkg/runtime"
           "k8s.io/apimachinery/pkg/runtime/schema"
          )

          // SchemeGroupVersion is group version used to register these objects
          var SchemeGroupVersion = schema.GroupVersion{Group: miniproxy.GroupName, Version: "v1alpha1"}

          var (
           // SchemeBuilder initializes a scheme builder
           SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
           // AddToScheme is a global function that registers this API group & version to a scheme
           AddToScheme = SchemeBuilder.AddToScheme
          )

          // Kind takes an unqualified kind and returns back a Group qualified GroupKind
          func Kind(kind string) schema.GroupKind {
           return SchemeGroupVersion.WithKind(kind).GroupKind()
          }

          // Resource takes an unqualified resource and returns a Group qualified GroupResource
          func Resource(resource string) schema.GroupResource {
           return SchemeGroupVersion.WithResource(resource).GroupResource()
          }

          // Adds the list of known types to Scheme.
          func addKnownTypes(scheme *runtime.Scheme) error {
           scheme.AddKnownTypes(SchemeGroupVersion,
            // 主要在這里導入我們的定制資源對象
            &ProxyRoute{},
            &ProxyRouteList{},
           )
           metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
           return nil
          }

          types.go 代碼:

          package v1alpha1

          import (
           metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
          )

          // +genclient
          // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

          // ProxyRoute is a specification for a ProxyRoute resource
          type ProxyRoute struct {
           metav1.TypeMeta   `json:",inline"`
           metav1.ObjectMeta `json:"metadata,omitempty"`

           Spec ProxyRouteSpec `json:"spec"`
          }

          // ProxyRouteSpec is the spec for a ProxyRoute resource
          type ProxyRouteSpec struct {
           Host        string `json:"host"`
           ServiceName string `json:"serviceName"`
           Namespace   string `json:"namespace,omitempty"`
           Port        int32  `json:"port,omitempty"`
           Scheme      bool   `json:"scheme,omitempty"`
          }

          // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

          // ProxyRouteList is a list of ProxyRoute resources
          type ProxyRouteList struct {
           metav1.TypeMeta `json:",inline"`
           metav1.ListMeta `json:"metadata"`

           Items []ProxyRoute `json:"items"`
          }

          register.go 代碼:

          package miniproxy

          // GroupName is the group name used in this package
          const (
           GroupName = "miniproxy.togettoyou.com"
          )

          代碼編寫完成后,就可以使用 k8s.io/code-generator 來生成控制器相關(guān)代碼了,腳本定義在 script 文件夾下,其中 boilerplate.go.txt 為生成的代碼頭部協(xié)議注釋,codegen.Dockerfile 內(nèi)容為:

          FROM golang:1.16

          ARG KUBE_VERSION

          ENV GO111MODULE=on
          ENV GOPROXY https://goproxy.cn,direct

          RUN go get k8s.io/code-generator@$KUBE_VERSIONexit 0
          RUN go get k8s.io/apimachinery@$KUBE_VERSIONexit 0

          RUN mkdir -p $GOPATH/src/k8s.io/{code-generator,apimachinery}
          RUN cp -R $GOPATH/pkg/mod/k8s.io/code-generator@$KUBE_VERSION $GOPATH/src/k8s.io/code-generator
          RUN cp -R $GOPATH/pkg/mod/k8s.io/apimachinery@$KUBE_VERSION $GOPATH/src/k8s.io/apimachinery
          RUN chmod +x $GOPATH/src/k8s.io/code-generator/generate-groups.sh

          WORKDIR $GOPATH/src/k8s.io/code-generator

          code-gen.sh 腳本內(nèi)容如下:

          #!/bin/bash -e

          set -e -o pipefail

          PROJECT_MODULE="mini-k8s-proxy"
          IMAGE_NAME="kubernetes-codegen:latest"

          echo "Building codegen Docker image..."
          docker build --build-arg KUBE_VERSION=v0.20.2 -f "./script/codegen.Dockerfile" \
                      -t "${IMAGE_NAME}" \
                      "."

          cmd="/go/src/k8s.io/code-generator/generate-groups.sh all \
              ${PROJECT_MODULE}/pkg/generated \
              ${PROJECT_MODULE}/pkg/apis \
              miniproxy:v1alpha1 \
              --go-header-file=/go/src/${PROJECT_MODULE}/script/boilerplate.go.txt"


          echo "Generating clientSet code ..."
          docker run --rm \
                     -v "$(pwd):/go/src/${PROJECT_MODULE}" \
                     -w "/go/src/${PROJECT_MODULE}" \
                     "${IMAGE_NAME}" $cmd

          執(zhí)行腳本生成相關(guān)代碼:

          $ ./script/code-gen.sh

          ......
          Generating clientSet code ...
          Generating deepcopy funcs
          Generating clientset for miniproxy:v1alpha1 at mini-k8s-proxy/pkg/generated/clientset
          Generating listers for miniproxy:v1alpha1 at mini-k8s-proxy/pkg/generated/listers
          Generating informers for miniproxy:v1alpha1 at mini-k8s-proxy/pkg/generated/informers

          實現(xiàn)業(yè)務邏輯

          由于業(yè)務較簡單,我們直接在 main.go 完成業(yè)務邏輯,貼上代碼:

          type ProxyRouteSpec struct {
           V map[string]v1alpha1.ProxyRouteSpec
           sync.RWMutex
          }

          var prs = &ProxyRouteSpec{
           V: make(map[string]v1alpha1.ProxyRouteSpec, 0),
          }

          type resourceEventHandler struct {
           Ev chan<- interface{}
          }

          func (reh *resourceEventHandler) OnAdd(obj interface{}) {
           eventHandlerFunc(reh.Ev, obj)
          }

          func (reh *resourceEventHandler) OnUpdate(oldObj, newObj interface{}) {
           eventHandlerFunc(reh.Ev, newObj)
          }

          func (reh *resourceEventHandler) OnDelete(obj interface{}) {
           eventHandlerFunc(reh.Ev, obj)
          }

          func eventHandlerFunc(events chan<- interface{}, obj interface{}) {
           select {
           case events <- obj:
           default:
           }
          }

          func main() {
           ctx := context.Background()

           eventCh := make(chan interface{}, 1)
           // 采用緩沖大小為 1 的通道方式來處理 CRD 事件
           eventHandler := &resourceEventHandler{Ev: eventCh}

           // 作為測試,可以直接使用 kubeconfig 連接 k8s,實際部署使用 InClusterConfig 模式
           //cfg, err := clientcmd.BuildConfigFromFlags("", "tmp/config")
           cfg, err := rest.InClusterConfig()
           if err != nil {
            panic(err)
           }
           client, err := clientset.NewForConfig(cfg)
           if err != nil {
            panic(err)
           }
           // 構(gòu)建 k8s Crd Informer 實例
           factoryCrd := externalversions.NewSharedInformerFactoryWithOptions(
            client,
            10*time.Minute,
           )
           // 注冊 Informer 事件處理
           factoryCrd.Miniproxy().V1alpha1().ProxyRoutes().Informer().AddEventHandler(eventHandler)
           // 啟動 Informer
           factoryCrd.Start(ctx.Done())
           // 等待首次緩存同步
           for t, ok := range factoryCrd.WaitForCacheSync(ctx.Done()) {
            if !ok {
             panic(fmt.Errorf("timed out waiting for controller caches to sync %s", t.String()))
            }
           }
           go startServer()

           for {
            select {
            case _, ok := <-eventCh:
             if !ok {
              continue
             }
             // 從 Lister 緩存獲取 CRD 資源對象
             proxyRoutes, err := factoryCrd.Miniproxy().V1alpha1().ProxyRoutes().Lister().List(labels.Everything())
             if err != nil {
              log.Println(err.Error())
              continue
             }
             // 清空本地緩存并重新放入
             prs.Lock()
             prs.V = make(map[string]v1alpha1.ProxyRouteSpec, 0)
             for _, proxyRoute := range proxyRoutes {
              fmt.Printf("%+v\n", proxyRoute)
              prs.V[proxyRoute.Spec.Host] = proxyRoute.Spec
             }
             prs.Unlock()
            }
           }
          }

          原理比較粗暴,通過 Informer — Lister 機制監(jiān)聽 CRD 資源的變化,并將資源對象存入本地 map 緩存中。

          繼續(xù)添加代理轉(zhuǎn)發(fā)邏輯:

          func startServer() {
           gin.SetMode(gin.ReleaseMode)
           r := gin.Default()
           r.Any("/*any", handler)
           log.Fatalln(r.Run(":80"))
          }

          func handler(c *gin.Context) {
           prs.RLock()
           defer prs.RUnlock()
           if proxyRouteSpec, ok := prs.V[c.Request.Host]; ok {
            u := ""
            if proxyRouteSpec.Scheme {
             u += "https://"
            } else {
             u += "http://"
            }
            if proxyRouteSpec.Namespace != "" {
             u += proxyRouteSpec.ServiceName + "." + proxyRouteSpec.Namespace
            } else {
             u += proxyRouteSpec.ServiceName
            }
            if proxyRouteSpec.Port != 0 {
             u += fmt.Sprintf(":%d", proxyRouteSpec.Port)
            }
            log.Println("代理地址: ", u)
            proxyUrl, err := url.Parse(u)
            if err != nil {
             c.AbortWithStatus(http.StatusInternalServerError)
            }
            proxyServer(c, proxyUrl)
           } else {
            c.String(http.StatusNotFound, "404")
           }
          }

          // 代理轉(zhuǎn)發(fā)
          func proxyServer(c *gin.Context, proxyUrl *url.URL) {
           proxy := &httputil.ReverseProxy{
            Director: func(outReq *http.Request) {
             u := outReq.URL
             outReq.URL = proxyUrl
             if outReq.RequestURI != "" {
              parsedURL, err := url.ParseRequestURI(outReq.RequestURI)
              if err == nil {
               u = parsedURL
              }
             }

             outReq.URL.Path = u.Path
             outReq.URL.RawPath = u.RawPath
             outReq.URL.RawQuery = u.RawQuery
             outReq.RequestURI = "" // Outgoing request should not have RequestURI

             outReq.Proto = "HTTP/1.1"
             outReq.ProtoMajor = 1
             outReq.ProtoMinor = 1

             if _, ok := outReq.Header["User-Agent"]; !ok {
              outReq.Header.Set("User-Agent""")
             }

             // Even if the websocket RFC says that headers should be case-insensitive,
             // some servers need Sec-WebSocket-Key, Sec-WebSocket-Extensions, Sec-WebSocket-Accept,
             // Sec-WebSocket-Protocol and Sec-WebSocket-Version to be case-sensitive.
             // https://tools.ietf.org/html/rfc6455#page-20
             outReq.Header["Sec-WebSocket-Key"] = outReq.Header["Sec-Websocket-Key"]
             outReq.Header["Sec-WebSocket-Extensions"] = outReq.Header["Sec-Websocket-Extensions"]
             outReq.Header["Sec-WebSocket-Accept"] = outReq.Header["Sec-Websocket-Accept"]
             outReq.Header["Sec-WebSocket-Protocol"] = outReq.Header["Sec-Websocket-Protocol"]
             outReq.Header["Sec-WebSocket-Version"] = outReq.Header["Sec-Websocket-Version"]
             delete(outReq.Header, "Sec-Websocket-Key")
             delete(outReq.Header, "Sec-Websocket-Extensions")
             delete(outReq.Header, "Sec-Websocket-Accept")
             delete(outReq.Header, "Sec-Websocket-Protocol")
             delete(outReq.Header, "Sec-Websocket-Version")
            },
            Transport: &http.Transport{
             TLSClientConfig: &tls.Config{
              InsecureSkipVerify: true,
             },
            },
            ErrorHandler: func(w http.ResponseWriter, request *http.Request, err error) {
             statusCode := http.StatusInternalServerError
             w.WriteHeader(statusCode)
             w.Write([]byte(http.StatusText(statusCode)))
            },
           }
           proxy.ServeHTTP(c.Writer, c.Request)
          }

          每次請求連接會從本地緩存讀取配置,判斷是否匹配,若匹配則轉(zhuǎn)發(fā)代理到配置的服務中去。

          部署

          為了方便測試,我已經(jīng)編譯好鏡像上傳到 Docker Hub 上,所以大家可以直接使用下面的 yaml 部署:

          apiVersion: apiextensions.k8s.io/v1
          kind: CustomResourceDefinition
          metadata:
            name: proxyroutes.miniproxy.togettoyou.com
          spec:
            group: miniproxy.togettoyou.com
            names:
              kind: ProxyRoute
              shortNames:
                - pr
              plural: proxyroutes
              singular: proxyroute
            scope: Namespaced
            versions:
              - name: v1alpha1
                served: true
                storage: true
                schema:
                  openAPIV3Schema:
                    type: object
                    properties:
                      spec:
                        type: object
                        properties:
                          host:
                            type: string
                          serviceName:
                            type: string
                          namespace:
                            type: string
                          port:
                            type: integer
                          scheme:
                            type: boolean
          ---
          apiVersion: apps/v1
          kind: Deployment
          metadata:
            name: mini-k8s-proxy
          spec:
            selector:
              matchLabels:
                app: mini-k8s-proxy
            replicas: 1
            template:
              metadata:
                labels:
                  app: mini-k8s-proxy
              spec:
                containers:
                  - name: mini-k8s-proxy
                    image: togettoyou/mini-k8s-proxy:latest
                    ports:
                      - containerPort: 80

          ---
          apiVersion: v1
          kind: Service
          metadata:
            name: mini-k8s-proxy-service
          spec:
            ports:
              - port: 80
                targetPort: 80
            selector:
              app: mini-k8s-proxy
            type: NodePort

          部署:

          $ kubectl apply -f mini-k8s-proxy.yaml
          customresourcedefinition.apiextensions.k8s.io/proxyroutes.miniproxy.togettoyou.com created
          deployment.apps/mini-k8s-proxy created
          service/mini-k8s-proxy-service created

          $ kubectl get svc
          NAME                     TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
          mini-k8s-proxy-service   NodePort    10.111.139.14   <none>        80:32112/TCP   32s

          訪問集群域名:32112 ,看到 404 的話,恭喜部署成功。

          驗證使用 ProxyRoute

          現(xiàn)在我有一個名稱為 test-service 的 service 處于 testns 命名空間下,容器內(nèi)部端口為 80(是一個 nginx 服務)

          $ kubectl get svc -n testns
          NAME           TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
          test-service   ClusterIP   10.97.89.250   <none>        80/TCP    2s

          我想要當請求 host 為 test.togettoyou.com:32112 的流量請求過來時,可以代理轉(zhuǎn)發(fā)到 test-service 上,怎么做呢

          按照心里預期,創(chuàng)建一個 ProxyRoute 資源對象:

          apiVersion: miniproxy.togettoyou.com/v1alpha1
          kind: ProxyRoute
          metadata:
            name: test-proxyroute
          spec:
            host: test.togettoyou.com:32112
            serviceName: test-service
            namespace: testns
            port: 80
            scheme: false

          瀏覽器訪問 test.togettoyou.com:32112 ,神奇的事情就會發(fā)生了


          總結(jié)

          本文通過 CRD + Controller 實現(xiàn)了一個簡易的 K8S 代理轉(zhuǎn)發(fā)工具,相關(guān)代碼均上傳到了 Github(https://github.com/togettoyou/mini-k8s-proxy)

          實現(xiàn)思路來自 Traefik,強烈推薦

          最后,感謝您的閱讀!


          瀏覽 60
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲 欧美 综合 | 撸一撸天天日 | 国产日本精品视频 | 欧美熟女视频 | 亚洲第一在线视频 |