多廠商容器平臺開發系統性總結

總述

自2021年6月21號由玩雲原生的運維轉玩雲原生的開發至今,已有5月有餘,除去中間的一些其他工作任務,實際參與(實際是一個人負責開發)多廠商容器平臺開發應有3月有餘。個人開發並規劃的多廠商容器平臺是根據此張由我個人設計的規劃圖進行的(ps:部門沒有架構師級別的,能提供可行的架構圖或者一點指導,所以只能根據4年的kubernetes使用經驗和逛各大網站和大廠的相關文檔)。

也算是我心目中的多廠商容器平臺的開發思路吧,下面將重點圍繞上圖展開。

注:此篇爲我心目中的"多廠商容器管理平臺"。

依賴項

主要

  • golang 1.16.6
  • goland 2021.2.3
  • gin 1.7.4
  • gorm 1.9.16
  • kubernetes 1.16 -- 1.19
  • client-go 0.19.0

規劃圖淺解

採取模塊分層開發:以適配層爲分界線

1、上層爲適配層,根據不同的廠商進行API封裝,統一返回字段,需要根據廠商的字段進行不同的方法開發。

2、下層爲核心層,直接調kubernetes接口,統一返回字段。

各容器廠商會根據實際需求,對容器平臺進行適當的整改,如:

集羣的創建方案[1、在創建集羣的同時按照參數創建雲廠商節點資源 2、導入已有的節點資源創建集羣 3、等等等]

節點特性[1、根據地域就近調度 2、控制節點是否可見 3、等等等]

kubernetes附件組建[1、網絡插件的按需選擇或二開 2、節點的pod最大數 3、等等等]

導入第三方kubernetes集羣的方案[1、導入config 2、導入secret 3、導入agent 4、等等等]

......

根據以上的考慮:

1、集羣和節點的增刪改採取適配器的方案開發

2、集羣和節點的查詢走kubernetes,其一查詢是個頻繁的操作而廠家平臺是有APi調用次數限制,其二集羣和節點的狀態應以kubernetes原生爲依據而不應該是以廠家的容器平臺爲依據,其三減少代碼的無用重複便於維護

3、每個廠家的倉庫(helm倉庫和image倉庫)都有各自的特性且不屬於kubernetes核心資源且無法像kubernetes那樣獲取到最底層的API文檔,所以增刪改查都走廠家似乎沒有不妥之處。

4、數據庫需要維護的有:集羣信息、節點信息、倉庫的有關權控信息。儘可能的減少此服務的維護複雜度,能交給廠家和kubernetes的etcd維護的最好。
4.1 如沒有"集羣沒創建成功即可查看集羣的個別信息和集羣沒創建成功既可查看節點的有關信息"的需求,那其實集羣和節點也沒有數據庫維護的必要性,我認爲。

5、kubernetes的核心資源,即可以被kubectl api-resources 查到的資源,通過kubernetes的api即可。

6、因爲服務在可預見的時期是部署與虛機上,所以暫沒有準備進行kubernetes 聚合api開發,但api最好爲聲明式的,留下可行性。

7、requests和response採取常規的json格式,沒有選擇採用yaml的json格式,主要是爲了減輕前端的不便,且當下大廠也是各有格式,最主要的是目前還沒有進行api聚合到kubernetes。

8、認爲開發的重點應該在中期規劃部分,這才所有創新,有所不同於衆多廠家。

部分核心代碼

目錄結構:

.
├── Makefile
├── README.md
├── cmd
│   └── container
│       └── container.go
├── config
│   └── container.yaml
├── deploy
│   ├── docker
│   │   └── Dockerfile
│   └── vmware
├── docs
├── go.mod
├── go.sum
├── internal
│   └── container
│       ├── bootstrap
│       ├── controller
│       ├── dao
│       ├── dto
│       ├── ecode
│       ├── initialize
│       ├── job
│       ├── middleware
│       ├── models
│       ├── pkg
│       │   ├── kubernetes
│       │   │   ├── dto
│       │   │   └── service
│       │   ├── registry
│       │   │   ├── registry.go
│       │   │   └── tke
│       │   │   └── acr
│       │   │   └── harbor
│       │   └── rancher
│       │       ├── dto
│       │       └── service
│       │   └── ack
│       │       ├── dto
│       │       └── service
│       │   └── tke
│       │       ├── dto
│       │       └── service
│       ├── routers
│       └── service
├── pkg
├── scripts
│   ├── db.sh
│   └── deploy.sh
└── test

封裝httpClient

type HTTPClient interface {
	Do(req *http.Request) (*http.Response, error)
}

var (
	Client HTTPClient
)

func init() {
	Client = &http.Client{
		Timeout: 10 * time.Second,
		Transport: &http.Transport{
			TLSClientConfig:   &tls.Config{InsecureSkipVerify: true},
			DisableKeepAlives: true,
			Proxy:             http.ProxyFromEnvironment,
			DialContext: (&net.Dialer{
				Timeout:   30 * time.Second, // tcp連接超時時間
				KeepAlive: 60 * time.Second, // 保持長連接的時間
				DualStack: true,
			}).DialContext, // 設置連接的參數
			MaxIdleConns:          50, // 最大空閒連接
			MaxConnsPerHost:       100,
			MaxIdleConnsPerHost:   100,              // 每個host保持的空閒連接數
			ExpectContinueTimeout: 30 * time.Second, // 等待服務第一響應的超時時間
			IdleConnTimeout:       60 * time.Second, // 空閒連接的超時時間
		},
	}
}

// CheckRespStatus 狀態檢查
func CheckRespStatus(resp *http.Response) ([]byte, error) {
	bodyBytes, _ := ioutil.ReadAll(resp.Body)
	if resp.StatusCode >= 200 && resp.StatusCode < 400 {
		return bodyBytes, nil
	}
	return nil, errors.New(string(bodyBytes))
}

// Request 建立http請求
func Request(url, token, body string, headerSet map[string]string, method string) (respStatusCode int, respBytes []byte, err error) {
	request, err := http.NewRequest(method, url, strings.NewReader(body))
	if err != nil {
		return 401, nil, err
	}

	//添加token
	if token != "" {
		request.Header.Set("Authorization", "Bearer "+token)
	}

	// header 添加字段
	if headerSet != nil {
		for k, v := range headerSet {
			request.Header.Set(k, v)
		}
	}
	resp, err := Client.Do(request)
	if err != nil {
		return 401, nil, err
	}
	defer resp.Body.Close()
	// 返回的狀態碼
	respBytes, err = CheckRespStatus(resp)
	respStatusCode = resp.StatusCode
	return
}

封裝clusterManager

type ClusterManager struct {
	ClientSet     *kubernetes.Clientset
	Metrics       *metrics.Clientset
	DynamicClient dynamic.Interface
}

const (
	//DefaultQPS High enough QPS to fit all expected use cases.
	DefaultQPS = 1e6
	//DefaultBurst High enough Burst to fit all expected use cases.
	DefaultBurst = 1e6
)

func buildConfig(clusterName string) (*rest.Config, error) {
	var clientConfig *rest.Config
	var configV1 *clientcmdapiv1.Config
	var dbCluster models.Cluster
	var err error
	var host string
	rows := bootstrap.DB.Where("cluster_name = ?", clusterName).Find(&dbCluster).RowsAffected
	if rows == 0 {
		return nil, errors.New("the database does not have this information")
	}
	if dbCluster.KubeConfigSecret != "" {
		kubeConfigBytes, err := base64.StdEncoding.DecodeString(dbCluster.KubeConfigSecret)
		kubeConfigJson, err := yaml.YAMLToJSON(kubeConfigBytes)
		err = json.Unmarshal(kubeConfigJson, &configV1)
		if err != nil {
			logrus.Error(err.Error())
		}
		// 切換匹配的版本
		configObject, err := clientcmdlatest.Scheme.ConvertToVersion(configV1, clientcmdapi.SchemeGroupVersion)
		configInternal := configObject.(*clientcmdapi.Config)
		// 實例化配置信息
		clientConfig, err = clientcmd.NewDefaultClientConfig(*configInternal, &clientcmd.ConfigOverrides{}).ClientConfig()
		clientConfig.QPS = DefaultQPS
		clientConfig.Burst = DefaultBurst
	} else if dbCluster.Token != "" {
		var addresses []dto.Addresses
		err := json.Unmarshal([]byte(dbCluster.APIServer), &addresses)
		for _, address := range addresses {
			if address.Type == "Real" {
				host = fmt.Sprintf("https://") + address.Host + fmt.Sprintf(":") + strconv.Itoa(address.Port)
				break
			}
		}
		if err != nil {
			return nil, errors.New("request connection cluster failed")
		}

		clientConfig = &rest.Config{
			Host:                host,
			APIPath:             "",
			ContentConfig:       rest.ContentConfig{},
			Username:            "",
			Password:            "",
			BearerToken:         dbCluster.Token,
			BearerTokenFile:     "",
			Impersonate:         rest.ImpersonationConfig{},
			AuthProvider:        nil,
			AuthConfigPersister: nil,
			ExecProvider:        nil,
			TLSClientConfig: rest.TLSClientConfig{
				Insecure: true,
			},
			UserAgent:          "",
			DisableCompression: false,
			Transport:          nil,
			WrapTransport:      nil,
			QPS:                DefaultQPS,
			Burst:              DefaultBurst,
			RateLimiter:        nil,
			WarningHandler:     nil,
			Timeout:            0,
			Dial:               nil,
			Proxy:              nil,
		}
	} else {
		return nil, errors.New("build  client config error")
	}
	return clientConfig, nil
}

func BuildApiServerClient(clusterName string) (*ClusterManager, error) {
	clientConfig, err := buildConfig(clusterName)
	if err != nil {
		return nil, err
	}
	if clientConfig == nil {
		return nil, errors.New("err: error BuildApiServerClient")
	}
	clientSet, err := kubernetes.NewForConfig(clientConfig)
	if err != nil {
		return nil, err
	}
	// 這裏一定要調用Discovery().ServerVersion(),探測Kube apiServer是否可用,因爲kubernetes.NewForConfig(restConfig)不會去檢查服務是否可用,當服務不可用時,該方法不會返回錯誤的
	_, err = clientSet.Discovery().ServerVersion()
	if err != nil {
		return nil, err
	}
	m, err := metrics.NewForConfig(clientConfig)
	if err != nil {
		return nil, err
	}
	d, err := dynamic.NewForConfig(clientConfig)
	if err != nil {
		return nil, err
	}
	clusterManager := &ClusterManager{
		clientSet,
		m,
		d,
	}
	return clusterManager, nil
}

封裝reponseApi

type ApiResponse struct {
	Code int         `json:"code"`
	Msg  string      `json:"message"`
	Data interface{} `json:"data"`
}

// PaginateResponse 顯然這個結構體可以複用 ApiResponse, 但是 swagger 不認識!
type PaginateResponse struct {
	Code int      `json:"code"`
	Msg  string   `json:"message"`
	Data Paginate `json:"data"`
}

type Paginate struct {
	CurPage     int         `json:"cur_page"`      // 當前頁
	CurPageSize int         `json:"cur_page_size"` // 每頁展示數據量
	Total       int         `json:"total"`         // 總共數據量
	TotalPage   int         `json:"total_page"`    // 總共頁數
	Data        interface{} `json:"data"`          // 數據
}

// SuccessResponse API成功返回
func SuccessResponse(c *gin.Context, data interface{}) {
	response(c, ecode.Success, data)
}

// SuccessPaginateResponse 分頁返回
func SuccessPaginateResponse(c *gin.Context, data interface{}, total int, curPage int, curPageSize int) {
	c.JSON(http.StatusOK, PaginateResponse{
		Code: int(ecode.Success),
		Msg:  ecode.ErrMsg[ecode.Success],
		Data: Paginate{CurPage: curPage, CurPageSize: curPageSize, Total: total, TotalPage: int(math.Ceil(float64(total) / float64(curPageSize))), Data: data},
	})
	c.Abort()
}

// ErrorResponse API失敗返回
func ErrorResponse(c *gin.Context, code ecode.ErrCode, data interface{}) {
	response(c, code, data)
}

func NotFoundResponse(c *gin.Context) {
	c.JSON(http.StatusNotFound, gin.H{
		"code":    404,
		"message": "頁面未找到",
		"data":    "",
	})
}

func response(c *gin.Context, code ecode.ErrCode, data interface{}) {
	c.JSON(http.StatusOK, ApiResponse{
		Code: int(code),
		Msg:  ecode.ErrMsg[code],
		Data: data,
	})
	c.Abort()
}

幾個調kubernetes的例子【認爲比較有趣的例子】

# 倒序輸出事件
	events, err := w.workloadKubernetes.FindEvents(clientSet, namespace, name)
	if err != nil {
		return nil, err
	}
	var eventsWorkloadReps []*dto.EventsWorkloadRep
	t := time.Time{}
	for i := len(events) - 1; i >= 0; i-- {
		if events[i].FirstTimestamp.Time == t {
			events[i].FirstTimestamp.Time = events[i].EventTime.Time
		}
		if events[i].LastTimestamp.Time == t {
			events[i].LastTimestamp.Time = events[i].EventTime.Time
		}
		eventsWorkloadReps = append(eventsWorkloadReps, &dto.EventsWorkloadRep{
			WorkloadUUID:   id,
			FirstTimestamp: events[i].FirstTimestamp.Time,
			LastTimestamp:  events[i].LastTimestamp.Time,
			Type:           events[i].Type,
			Kind:           events[i].InvolvedObject.Kind,
			Name:           events[i].Name,
			Reason:         events[i].Reason,
			Message:        events[i].Message,
			Count:          events[i].Count,
		})
	}
	if len(eventsWorkloadReps) == 0 {
		return make([]*dto.EventsWorkloadRep, 0), nil
	}
	return eventsWorkloadReps, nil

# pod log
    limit, _ := strconv.ParseInt(tailLines, 10, 64)
    req := clientSet.CoreV1().Pods(namespace).GetLogs(name, &coreV1.PodLogOptions{Container: container, Timestamps: true, TailLines: &limit})
    podLogs, err := req.Stream(context.TODO())
    if err != nil {
    return "error in opening stream"
    }
    defer podLogs.Close()
    
    buf := new(bytes.Buffer)
    _, err = io.Copy(buf, podLogs)
    if err != nil {
        return "error in copy information from podLogs to buf"
    }
    str := buf.String()
    return str

# 根據kubeconfig 獲取ApiServer\CertFile\Token
    decoded, err := base64.StdEncoding.DecodeString(kubeConfig)
    decodestr := string(decoded)
    // 認證方式爲kubeConfig
    // 通過kubeConfig獲取 api / token / certFile
    kubeConfigJson, err := syaml.YAMLToJSON([]byte(decodestr))
    var configV1 *clientcmdapiv1.Config
    err = json.Unmarshal(kubeConfigJson, &configV1)
    if err != nil {
    return nil, nil, err
    }
    c, err := clientcmd.RESTConfigFromKubeConfig(decoded)
    if err != nil {
    return nil, nil, err
    }
    clientSet, err := kubernetes.NewForConfig(c)
    if err != nil {
    return nil, nil, err
    }
    sa, err := clientSet.CoreV1().ServiceAccounts("kube-system").Get(context.TODO(), "admin-user", metaV1.GetOptions{})
    secrets, err := clientSet.CoreV1().Secrets("kube-system").Get(context.TODO(), sa.Secrets[0].Name, metaV1.GetOptions{})
    if err != nil {
			return nil, err
    }
    importClusterReq.ApiServer = configV1.Clusters[0].Cluster.Server
    encoded := base64.StdEncoding.EncodeToString(configV1.Clusters[0].Cluster.CertificateAuthorityData)
    importClusterReq.CertFile = encoded
    importClusterReq.Token = string(secrets.Data["token"])

# 實現apply yaml 【......寫成了x了】
func (y *Yaml) ApplyYaml(dynamicClient dynamic.Interface, clientSet *kubernetes.Clientset, yamlBody []byte) (interface{}, error) {
    data, err := yamlutil.ToJSON(yamlBody)
    var applyYaml dto.ApplyYaml
    if err = json.Unmarshal(data, &applyYaml); err != nil {
        return nil, err
    }
    var applyYamlRep string
    decoder := yamlutil.NewYAMLOrJSONDecoder(bytes.NewReader(yamlBody), len(yamlBody))
    var rawObj runtime.RawExtension
    if err := decoder.Decode(&rawObj); err != nil {
        return nil, err
    }
    obj, gvk, err := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme).Decode(rawObj.Raw, nil, nil)
    unstructuredMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
    if err != nil {
    return nil, err
    }

	unstructuredObj := &unstructured.Unstructured{Object: unstructuredMap}
	// 獲取支持的資源類型列表
	gr, err := restmapper.GetAPIGroupResources(clientSet.Discovery())
	if err != nil {
		return nil, err
	}

	// 創建 'Discovery REST Mapper',獲取查詢的資源的類型
	mapper := restmapper.NewDiscoveryRESTMapper(gr)
	// 查找 Group/Version/Kind 的 REST 映射
	mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
	if err != nil {
		return nil, err
	}

	var dri dynamic.ResourceInterface
	// 需要爲 namespace 範圍內的資源提供不同的接口
	if mapping.Scope.Name() == meta.RESTScopeNameNamespace {
		if unstructuredObj.GetNamespace() == "" {
			unstructuredObj.SetNamespace("default")
		}
		dri = dynamicClient.Resource(mapping.Resource).Namespace(unstructuredObj.GetNamespace())
	} else {
		dri = dynamicClient.Resource(mapping.Resource)
	}

	if applyYaml.Metadata.Namespace == "" {
		applyYaml.Metadata.Namespace = "default"
	}
	// 查詢k8s是否有該資源類型
	switch applyYaml.Kind {
	case "Deployment":
		_, err = clientSet.AppsV1().Deployments(applyYaml.Metadata.Namespace).Get(context.TODO(), applyYaml.Metadata.Name, metaV1.GetOptions{})
	case "StatefulSet":
		_, err = clientSet.AppsV1().StatefulSets(applyYaml.Metadata.Namespace).Get(context.TODO(), applyYaml.Metadata.Name, metaV1.GetOptions{})
	case "DaemonSet":
		_, err = clientSet.AppsV1().DaemonSets(applyYaml.Metadata.Namespace).Get(context.TODO(), applyYaml.Metadata.Name, metaV1.GetOptions{})
	case "ReplicaSet":
		_, err = clientSet.AppsV1().ReplicaSets(applyYaml.Metadata.Namespace).Get(context.TODO(), applyYaml.Metadata.Name, metaV1.GetOptions{})
	case "CronJob":
		_, err = clientSet.BatchV1().CronJobs(applyYaml.Metadata.Namespace).Get(context.TODO(), applyYaml.Metadata.Name, metaV1.GetOptions{})
	case "Job":
		_, err = clientSet.BatchV1().Jobs(applyYaml.Metadata.Namespace).Get(context.TODO(), applyYaml.Metadata.Name, metaV1.GetOptions{})
	case "Service":
		_, err = clientSet.CoreV1().Services(applyYaml.Metadata.Namespace).Get(context.TODO(), applyYaml.Metadata.Name, metaV1.GetOptions{})
	case "ConfigMap":
		_, err = clientSet.CoreV1().ConfigMaps(applyYaml.Metadata.Namespace).Get(context.TODO(), applyYaml.Metadata.Name, metaV1.GetOptions{})
	case "Ingress":
		_, err = clientSet.ExtensionsV1beta1().Ingresses(applyYaml.Metadata.Namespace).Get(context.TODO(), applyYaml.Metadata.Name, metaV1.GetOptions{})
	case "ServiceAccount":
		_, err = clientSet.CoreV1().ServiceAccounts(applyYaml.Metadata.Namespace).Get(context.TODO(), applyYaml.Metadata.Name, metaV1.GetOptions{})
	case "ClusterRole":
		_, err = clientSet.RbacV1().ClusterRoles().Get(context.TODO(), applyYaml.Metadata.Name, metaV1.GetOptions{})
	case "RoleBinding":
		_, err = clientSet.RbacV1().RoleBindings(applyYaml.Metadata.Namespace).Get(context.TODO(), applyYaml.Metadata.Name, metaV1.GetOptions{})
	case "ClusterRoleBinding":
		_, err = clientSet.RbacV1().ClusterRoleBindings().Get(context.TODO(), applyYaml.Metadata.Name, metaV1.GetOptions{})
	case "APIService":
		_, err := dri.Create(context.Background(), unstructuredObj, metaV1.CreateOptions{})
		if err != nil {
			return nil, err
		}
	}
	if err != nil {
		if !errors.IsNotFound(err) {
			return nil, err
		}
		// 不存在則創建
		obj2, err := dri.Create(context.Background(), unstructuredObj, metaV1.CreateOptions{})
		fmt.Println("obj2", obj2)
		if err != nil {
			return nil, err
		}
		applyYamlRep = fmt.Sprintf("%s/%s/%s created", obj2.GetNamespace(), obj2.GetKind(), obj2.GetName())
	} else { // 已存在則更新
		obj2, err := dri.Update(context.Background(), unstructuredObj, metaV1.UpdateOptions{})
		if err != nil {
			return nil, err
		}
		applyYamlRep = fmt.Sprintf("%s/%s/%s update", obj2.GetNamespace(), obj2.GetKind(), obj2.GetName())
	}
	return applyYamlRep, nil
}

以registry的命名空間新建爲例 來個適配器的demo 【僞代碼】

# controller層
func CreateContainerRegistryNamespace(c *gin.Context) {
    var param dto.CreateContainerRegistryNamespaceReq
    if err := c.ShouldBindJSON(&param); err != nil {
        controller.ErrorResponse(c, ecode.PARAMETER_ERR, err.Error())
        return
    }
    cred, err := service.NewCredentialService().GetPlainTextCredential(param.CredentialID)
    if err != nil {
        ErrorResponse(c, ecode.ParameterErr, err.Error())
        return
    }
    param.Token = cred.Token
    if data, err := service.NewRegistryService(param.Source,param.Token).CreateContainerRegistryNamespace(param); err != nil {
        controller.ErrorResponse(c, ecode.PARAMETER_ERR, err.Error())
    } else {
        controller.SuccessResponse(c, data)
    }
}

# service層
type registryService struct {
    cli         registry.Registry
    registryDao dao.RegistryDao
}

func NewRegistryService(source,token string) *registryService {
    return &registryService{
        cli:         registry.NewRegistryCli(source,token),
    }
}
func (r *registryService) CreateContainerRegistryNamespace(param dto.CreateContainerRegistryNamespaceReq) (interface{}, error) {
    rep, err := r.cli.CreateContainerRegistryNamespace(param.DisplayName, param.Describe, param.Visibility)
    return rep, err
}

# interface層
type Registry interface {
    CreateContainerRegistryNamespace(name, describe, visibility, uuid string) (*dto.ListContainerRegistryNamespacesRep, error)
}

func NewRegistryCli(source,token string) Registry {
    var cli Registry
    switch source {
    case "tke":
        cli = tke.NewTkeClient(token)
    }
    return cli
}

# 方法層
type tkeClient struct {
    token string 
}

func NewTkeClient(token) *tkeClient {
    return &tkeClient{
        token: token
    }
}

func (t *tkeClient) CreateContainerRegistryNamespace(name, describe, visibility, uuid string) (*dto.ListContainerRegistryNamespacesRep, error) {
    url := viper.GetString("url") + "/apis/registry.tkestack.io/v1/namespaces/"
    createJson := tDto.CreateContainerRegistryReq{
        APIVersion: "registry.tkestack.io/v1",
        Kind:       "Namespace",
        Spec: tDto.CreateContainerRegistryReqSpec{
            Name:        name,
            DisplayName: describe,
            Visibility:  visibility,
        },
    }
    jsonData, errs := json.Marshal(createJson)
    if errs != nil {
        return nil, errs
    }
    _, rep, err := pkg.Request(url, token, string(jsonData), nil, http.MethodPost)
    if err != nil {
        errRep := gojsonq.New().FromString(err.Error()).Find("message")
        return nil, errors.New(errRep.(string))
    }
    var listRep *tDto.ListRepItems
    if err = json.NewDecoder(strings.NewReader(string(rep))).Decode(&listRep); err != nil {
        errRep := gojsonq.New().FromString(err.Error()).Find("message")
        return nil, errors.New(errRep.(string))
    }
    var createRep dto.ListContainerRegistryNamespacesRep
    createRep.DisplayName = listRep.Spec.Name
    createRep.Describe = listRep.Spec.DisplayName
    createRep.Visibility = listRep.Spec.Visibility
    createRep.Name = listRep.Metadata.Name
    createRep.RepoCount = listRep.Status.RepoCount
    return &createRep, nil
}

注:由上所述 似乎並沒有牽扯到多麼高深的操作,甚至是單純的api調用、封裝,也未涉及到中間件類的應用,隨着開發的不斷深入,此服務應逐漸複雜化

參考鏈接:

kubernetes 源碼分析:https://jeffdingzone.com/category/k8s/

kubernetes api文檔:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/

圖解kubernetes中API聚合機制的實現: https://juejin.cn/post/6844904081438277640

單體倉庫與多倉庫都有哪些優勢劣勢,如何確定微服務落地的最佳實踐?:https://ssoor.github.io/2020/03/24/mono-repo-vs-multi-repo/

kubernetes Events介紹:https://www.kubernetes.org.cn/1031.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章