Go 中的 Kubernetes GraphQL 查詢

搭建 GraphQL 服務器,用於在集羣中搜索 Kubernetes 資源

在共享集羣治理中,我們的部分責任是允許用戶訪問他們的資源,我們通過 RBAC 來分配合理的權限並通過 kubectl 啓用用戶的查詢。對於後端開發人員來說 “沒有障礙”,而對於不熟悉命令行操作或 kubectlWeb 和數據開發人員等用戶來說則不是這樣。

那麼我們該如何爲用戶掃清障礙呢?

我們通過 UI 可查詢界面實現了這一點,提供查詢服務和 RESTGraphQLAPI,以確保更輕鬆的訪問、更高的平臺可見性和更好的用戶體驗。在實現方面,我們使用 Go 進行了 GraphQL 服務。

爲什麼選擇 GraphQL

RESTGraphQL 是實現前後端交互時的兩個流行選擇。前者多年來一直是 Internet 標準,後者是 Facebook 開源 API 查詢語言,您可以在graphql.org上找到其基礎知識。並且在這兩者之間進行了許多比較,但在這裏我將只發布來自https://graphcms.com/blog/graphql-vs-rest-apis的一個,它最能直觀地顯示差異。

因此,不難得出爲什麼我們選擇 GraphQL 而不是 REST

GraphQL 更加靈活

它的對手 REST 是剛性的。以Pod查詢(名稱,命名空間,標籤,容器名稱,維護者)爲例,我們可以找出原因: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

SchemaGraphQL 模式語言)是 GraphQL 的核心,描述了我們要查詢的數據模型,其中最基本也是最關鍵的就是抽象和定義對象類型。

Go 中開發 GraphQL 時,我們使用框架graphql-go,在此基礎上我們完成了 GraphQL 模式的定義。這 4 個元素是

Type Schema

定義字段

type Resource {
  name: String!
  labels: [Label!]
  status: Status!
}

Type就像ClassJavastructGo 中一樣,由一組 Fields 組成,每個 Fields 都有一個對應的類型。如示例所示,類型有不同的類別,其中Scalar Type是最常見的一種,如String示例中和以下許多。

https://graphql.org/learn/schema/#scalar-types

GraphQL還支持定製的標量類型。這個例子中的Labeland Status是對象類型,它允許我們定義GraphQL類型,就像定義類圖一樣,並將它們鏈接在一起。

[], !, []!是類型修飾符,用於將Field標記爲數組或不爲空,如[Label!]表示Label是一個由Label類型組成的數組,數組可以爲空,但Label不能爲空。

對於GraphQL支持的其他接口類型、聯合類型和輸入類型,如果您感興趣,可以參考文檔中的示例。

在圖形執行方面,還有一對一對應的類型,比如graphql。字符串,每個字段默認爲空,您可以使用graphqlNewNonNull用於在需要非null時包裝類型。

Resolver

下一步是分配或解析字段。

解析器以您定義的任何方式填充返回數據。要查詢Kubernetes集羣中的資源,這裏使用的是client-go

Subscriber

通過註冊Subscriber(這與List/Watch非常相似),我們可以隨後更新GraphQL數據。如果你熟悉Kubernetes informer模式,這對你來說並不新鮮。

由於我們使用Kubernetes查詢,clientinformer是一個完美的匹配。但是當查詢數據庫或像Kafka這樣的消息存儲時,必須有其他方法來支持它。

Type Schema, ResolverSubscriber之後,可以爲Pod查詢定義一個簡單的GraphQL模式。

import (
 "github.com/graphql-go/graphql"
)

func main() {
 clientset := getK8sClient()
 queryFields := graphql.Field{
  Name:      "Pod",
  Type:      buildQueryType(),
  Args:      buildQueryArgs(),
  Resolve:   buildResolver(clientset),
  Subscribe: buildSubscriber(clientset),
 }
 
 query := graphql.ObjectConfig{Name: "Query", Fields: queryFields}
 rootObject := graphql.NewObject(query)
 schemaConfig := graphql.SchemaConfig{Query: rootObject, Subscription: rootObject}
 schema, err := graphql.NewSchema(schemaConfig)
}

func buildQueryType() *graphql.Object {
 return graphql.NewObject(graphql.ObjectConfig{
  Name: "Pod",
  Fields: graphql.Fields{
   "name"&graphql.Field{
    Type: graphql.String,
   },
   "namespace"&graphql.Field{
    Type: graphql.String,
   },
   "creationTimestamp"&graphql.Field{
    Type: graphql.String,
   },
   "status"&graphql.Field{
    Type: graphql.String,
   },
  },
  Description: "Query Pod",
 })
}

func buildQueryArgs() graphql.FieldConfigArgument {
 return graphql.FieldConfigArgument{
  "name"&graphql.ArgumentConfig{
   Description: "The metadata.name of the Pod",
   Type:        graphql.NewNonNull(graphql.String),
  },
  "namespace"&graphql.ArgumentConfig{
   Description: "The metadata.namespace of the Pod",
   Type:        graphql.NewNonNull(graphql.String),
  },
  "label"&graphql.ArgumentConfig{
   Description: "The metadata.labels of the Pod",
   Type:        graphql.NewNonNull(graphql.String),
  },
  "annotation"&graphql.ArgumentConfig{
   Description: "The metadata.annotations of the Pod",
   Type:        graphql.NewNonNull(graphql.String),
  },
 }
}

func buildResolver(clientset *kubernetes.Clientset) func(p graphql.ResolveParams) (interface{}, error) {
  // client-go search pods
 return nil
}

func buildSubscriber(clientset *kubernetes.Clientset) func(p graphql.ResolveParams) (interface{}, error) {
  // client-go search subscribe
 return nil
}

搜索集羣並構建 GraphQL api

現在是應用client-go來實現解析器和訂閱者方法,它被認爲是GoKubernetes集羣交互的最佳選擇。

對於Pod查詢,我們直接使用clientset API,將它們與我們在上面定義的各種參數結合起來。

type PodShort struct {
 Name              string `json:"name"`
 Namespace         string `json:"namespace"`
 CreationTimestamp string `json:"creationTimestamp"`
 Status            string `json:"status"`
}

func buildResolver(clientset *kubernetes.Clientset) func(p graphql.ResolveParams) (interface{}, error) {
 return func(resolveParams graphql.ResolveParams) (interface{}, error) {
  name, hasName := resolveParams.Args["name"]
  namespace, hasNamespace := resolveParams.Args["namespace"]
  label, hasLabel := resolveParams.Args["label"]
  annotation, hasAnnotation := resolveParams.Args["annotation"]
  // validation, we check can the existence of the param
  if !hasNamespace {
   return nil, fmt.Errorf("missing required arg 'name' and 'namespace'")
  }
  listOption := metav1.ListOptions{}
  if hasLabel {
   listOption.LabelSelector = label.(string)
  }
  pods, _ := clientset.CoreV1().Pods(namespace.(string)).List(context.Background(), listOption)
  returns := []*PodShort{}
  for _, pod := range pods.Items {
   if hasAnnotation {
    if _, ok := pod.Annotations[annotation.(string)]; !ok {
     continue
    }
   }
   if hasName {
    if pod.Name != name.(string) {
     continue
    }
   }
   returns = append(returns, &PodShort{
    Name:              pod.Name,
    Namespace:         pod.Namespace,
    CreationTimestamp: pod.CreationTimestamp.String(),
    Status:            string(pod.Status.Conditions[0].Status),
   })
  }
  return returns, nil
 }
}

對於訂閱者 (subscriber),我們使用Informer定製Add事件的更新,返回一個channel,並使用graphql-go框架處理來自subscriberchannel更新消息。

func buildSubscriber(clientset *kubernetes.Clientset) func(p graphql.ResolveParams) (interface{}, error) {
 return func(resolveParams graphql.ResolveParams) (interface{}, error) {
  name, hasName := resolveParams.Args["name"]
  namespace, hasNamespace := resolveParams.Args["namespace"]
  channel := make(chan interface{}, 1)
  informerFactory := informers.NewSharedInformerFactory(clientset, time.Hour*24)
  podInformer := informerFactory.Core().V1().Pods()
  podInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
   AddFunc: func(obj interface{}) {
    pod := obj.(v1.Pod)
    if hasName && pod.Name != name.(string) {
     return
    }
    if hasNamespace && pod.Namespace != namespace.(string) {
     return
    }
    // more checks ...
    select {
    case <-resolveParams.Context.Done():
     close(channel)
    case channel <- PodShort{
     Name:              pod.Name,
     Namespace:         pod.Namespace,
     CreationTimestamp: pod.CreationTimestamp.String(),
     Status:            string(pod.Status.Conditions[0].Status),
    }:
    }
   },
   UpdateFunc: nil,
   DeleteFunc: nil,
  })

  return channel, nil
 }
}

測試

讓我們一步一步地用Go httpserver啓動GraphQL服務器。

graphqlHandler := handler.New(&handler.Config{
   Schema:     &schema,
   Pretty:     true,
   GraphiQL:   false,
   Playground: true,
})
http.Handle("/graphql", graphqlHandler)
err = http.ListenAndServe(":8080", nil)

現在在http://localhost:8080/graphql上測試它。

輸入

query {
  Pod(namespace: "prometheus") {
    name
    status
  }
}

我們得到

{
   "data":{
      "Pod":{
         "name":"prometheus-khdf12",
         "status":"Running"
      }
   }
}

部署

在將程序封裝到docker鏡像並部署到集羣之後,我們現在就可以向用戶開放服務了。

FROM golang:latest as builder
WORKDIR /app
COPY . .
RUN go mod download
RUN CGO_ENABLED=GOOS=linux go build -o main ./http

FROM alpine:latest
COPY --from=builder /app/main .
EXPOSE 8080
EXPOSE 443
CMD ["./main"]

需要注意的是,WorkloadIdentity應該配置爲在GKE集羣中啓用client-go。請參閱集羣治理中的詳細信息—定期清理資源。

https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity

https://medium.com/codex/cluster-governance-clean-up-resources-periodically-2a8d4f0966da

進一步

我們完成的Kubernetes PodGraphQL查詢還遠遠不夠完美,我們希望返回完整的Pod信息,而不是一個簡單的podshort

然而,在手動返回Pod字段的路徑上存在一些問題。

有沒有靈活可擴展的方式?簡短的回答是應用 client-go 來獲取集羣中的CRD定義並將其解析爲一個graph.Fields集合。詳細答案請關注我的下一篇文章。

感謝你的閱讀!

參考

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Tu7YUK0VJBfXTODk_SAitw