如何实现一个简单的K8s apiserver

原创 吴就业 184 0 2024-04-03

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/c9f6946877044771b0686eb3b4470e51

作者:吴就业
链接:https://wujiuye.com/article/c9f6946877044771b0686eb3b4470e51
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

apiserver是k8s的另一种扩展机制,相比CRD,它更为灵活。 Metrics API就是基于apiserver扩展的,关于Metrics API和apiserver的理解可以看这篇文章:Kubernetes可观测之Metrics API,什么是Metrics API?

本篇以实战为主,介绍如何实现一个简单的apiserver。

需求假设

先定义我们这个apiserver提供什么服务。

假设,我需要实现一个预测算法,预测Pod需要的cpu和内存。自定义应用部署服务(自动生成deployment资源部署应用)可以调用这个apiserver获取预测的pod的cpu和内存的requests值。自定义应用部署服务在生成deployment资源的时候为pod的容器指定cpu和内存的requests就使用这个apiserver预测的值。

一、实现接口

相当于我们平时写业务api一样,写http接口,但是会有一些要求。

首先是路径的要求,必须是“/apis/{your group}/{your version}”开头。假设我们定义的group是:k8s.wujiuye.com,然后version是:v1beta1。那么我们的apiserver的路径必须是以”/apis/k8s.wujiuye.com/v1beta1”开头。

其次,我们必须要支持https请求,所以要生成ca证书。

先看代码:

func main() {
  // (1)
	r := gin.New()
	group := r.Group("/apis/k8s.wujiuye.com/v1beta1")
  
	// kubectl get --raw "/apis/k8s.wujiuye.com/v1beta1/namespaces/default/requests/hello-word"
	group.GET("/namespaces/:namespaces/requests/:name", func(context *gin.Context) {
		paths := strings.Split(r.URL.Path, "/")
		namespace := paths[5]
		name := paths[7]
		data := &v1.DeploymentRequests{
			Kind:       "Requests",
			ApiVersion: "k8s.wujiuye.com/v1beta1",
			Metadata: v1.Metadata{
				Name:      name,
				Namespace: namespace,
			},
			Resources: v1.Resources{
				Cpu:    "100m",
				Memory: "100Mi",
			},
		}
		jsonData, _ := json.Marshal(data)
		w.WriteHeader(200)
		w.Write(jsonData)
	})
  
  // (2)
	s := &http.Server{
    Addr:    ":443",
		Handler: r.Handler(),
		TLSConfig: &tls.Config{
			MinVersion:       tls.VersionTLS12,
			CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
		},
	}
  
	curPath, _ := os.Getwd()
	if err := s.ListenAndServeTLS(curPath+"/apiservice.pem",curPath+"/apiservice-key.pem"); 
         err != nil && !errors.Is(err, http.ErrServerClosed) {
		//....
	}
}

解释下这段代码:

DeploymentRequests是我们自定义的Requests资源,不是严格意义上的CRD,只要求包含ApiVersion、Kind、Metadata即可。

type DeploymentRequests struct {
	Kind       string    `json:"kind"`
	ApiVersion string    `json:"apiVersion"`
	Metadata   Metadata  `json:"metadata"`
  
	Resources  Resources `json:"resources"`
}

type Metadata struct {
	Name      string `json:"name"`
	Namespace string `json:"namespace"`
}

type Resources struct {
	Cpu    string `json:"cpu"`
	Memory string `json:"memory"`
}

二、构建docker镜像

怎么构建docker镜像就不举例了。

只是要注意,我们需要生成ca证书,将证书同编译后的二进制执行文件一同放到镜像中。

怎么生成证书呢?

可以参考这篇文章:Operator实战2:实现webhook修改Job的最大重试次数

我们使用cfssl工具生成ca证书。

1.安装cfssl工具,mac下使用brew安装。

brew install cfssl

2.创建一个目录,用于作为自签证书的workspace。

mkdir /tmp/ca

3.在workspace下添加以下几个配置文件。

ca-config.json:

{
  "signing": {
    "default": {
      "expiry": "87600h"
    },
    "profiles": {
      "server": {
        "usages": [
          "signing",
          "key encipherment",
          "server auth",
          "client auth"
        ],
        "expiry": "87600h"
      }
    }
  }
}

ca-csr.json:

{
  "CN": "Kubernetes",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "US",
      "L": "Portland",
      "O": "Kubernetes",
      "OU": "CA",
      "ST": "Oregon"
    }
  ]
}

server-csr.json:

{
  "CN": "admission",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "US",
      "L": "Portland",
      "O": "Kubernetes",
      "OU": "Kubernetes",
      "ST": "Oregon"
    }
  ]
}

4.编写脚本(create_cert.sh)

cfssl gencert -initca ca-csr.json | cfssljson -bare ca

cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -hostname=requests-apiserver-service.default.svc \
  -profile=server \
  server-csr.json | cfssljson -bare apiservice

其中hostname值为“{service-name}.{service-namespace}.svc”,就是此apiserver的Service资源的名称和部署的namespace。

5.执行create_cert.sh脚本,我们将在workspace得到apiservice.pem、apiservice-key.pem,然后将这两个文件拷贝到项目中,然后在Dockerfile中把这两个文件拷贝到镜像中即可。

三、编写部署此apiserver的yaml

现在我们开始写Deployment来部署这个apiserver。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: requests-apiserver
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: requests-apiserver
  template:
    metadata:
      name: requests-apiserver
      namespace: default
      labels:
        app: requests-apiserver
    spec:
      containers:
        - name: requests-apiserver
          image: requests-apiserver:v1.0.0
          imagePullPolicy: Always
          ports:
            - name: web-https
              containerPort: 443
              protocol: TCP
          resources:
            limits:
              cpu: 200m
              memory: 100Mi
            requests:
              cpu: 200m
              memory: 100Mi
      restartPolicy: Always
      terminationGracePeriodSeconds: 30

其中镜像替换为我们自己打包的镜像即可。

四、编写Service

前面第二步生成ca证书时,我们指定的hostname(requests-apiserver-service.default.svc)就是这一步定义的Service资源。

apiVersion: v1
kind: Service
metadata:
  name: requests-apiserver-service
  namespace: default
spec:
  type: ClusterIP
  selector:
    app: requests-apiserver
  ports:
    - name: api-service
      port: 443
      targetPort: 443
      protocol: TCP

五、编写APIService

当前面四个步骤做完后,我们的apiserver就可以部署起来了。

但是还需要通过APIService资源将我们的apiserver注册到kube-apiserver。

apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
  name: v1beta1.k8s.wujiuye.com
spec:
  group: k8s.wujiuye.com
  groupPriorityMinimum: 20000
  version: v1beta1
  versionPriority: 1000
  insecureSkipTLSVerify: true
  service:
    name: requests-apiserver-service
    namespace: default
    port: 443

意思是,将group为k8s.wujiuye.com,version为v1beta1的请求,转发给namespace为default、name为requests-apiserver-service的Service,并且端口号为443。

六、实现check接口

当我们将第五步生成的APIService资源apply到k8s之后,我们就可以通过kubectl命令访问接口了。

例如:

kubectl get --raw "/apis/k8s.wujiuye.com/v1beta1/namespaces/default/requests/hello-pod"

不出意外,我们会看到这个错误:

Error from server (ServiceUnavailable): the server is currently unable to handle the request

说是服务不可用。

接着我们可以通过kubectl get apiservice v1beta1.k8s.wujiuye.com -o yaml命令查看具体原因。

status:
  conditions:
  - lastTransitionTime: "2024-04-03T03:44:10Z"
    message: 'failing or missing response from https://10.112.1.120:443/apis/k8s.wujiuye.com/v1beta1:
      bad status from https://10.112.1.120:443/apis/k8s.wujiuye.com/v1beta1:
      404'
    reason: FailedDiscoveryCheck
    status: "False"
    type: Available

意思是请求/apis/k8s.wujiuye.com/v1beta1这个接口报404,找不到接口。

原因是,当我们apply一个APIService资源的时候,kube-apiserver会校验这个APIService是否可用,通过调用/apis/{group}/{version}接口来验证,如果接口响应200,则说明apiserver可用,才是注册成功。

所以我们还需要简单的实现/apis/k8s.wujiuye.com/v1beta1这个接口。

在main方法中加入:

	group.GET("", func(context *gin.Context) {
		context.Writer.WriteHeader(200)
		context.Writer.Write([]byte("{}"))
	})

其它问题

1

自定义apiserver要求必须支持TLS,也就是实现https协议。kube-apiserver请求我们的自定义apiserver是使用https请求的。如果我们只实现http,那么就会遇到这个错误:

status:
  conditions:
  - lastTransitionTime: "2024-04-03T03:44:10Z"
    message: 'failing or missing response from https://10.112.1.119:8080/apis/k8s.wujiuye.com/v1beta1:
      Get "https://10.112.1.119:8080/apis/k8s.wujiuye.com/v1beta1":
      http: server gave HTTP response to HTTPS client'
    reason: FailedDiscoveryCheck
    status: "False"
    type: Available

2

如果我们使用lens工具执行kubectl get --raw命令,那么可能会看到这个错误:

<!doctype html><html lang="en"><head><meta charset="UTF-8"><script defer="defer" src="/build/runtime.js?18572a9df0905f6f78c2"></script><script defer="defer" src="/build/lens.js?18572a9df0905f6f78c2"></script><link href="/build/lens.css?18572a9df0905f6f78c2" rel="stylesheet"></head><body><div id="app"></div><div id="terminal-init"></div></body></html>%   

所以我们不能使用Lens工具执行kubectl get --raw命令。

20240425补充-部署线上后,其它服务请求接口403问题

如果状态码是403,那就是没有权限访问资源,需要给ServiceAccount添加此apiserver提供的自定义资源的访问权限。

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: xxx-role
rules:
- apiGroups:
    - "k8s.wujiuye.com"
  resources:
    - requests
  verbs:
    - get
    - list
    - watch
#云原生

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

kubebuilder如何Watch由Pod产生的Event的创建,触发控制器Reconcile方法的执行

自定义资源的Controller创建出来的子资源,子资源创建的子资源(子子资源),如何Watch子子资源的事件?我们以MyDeployment->创建Pod->创建Event,想要watch Pod创建的Event的Create事件为例。

Kubebuilder控制器配置Owns然而监听到事件却不触发Reconcile方法

我们在CreateFunc、DeleteFunc、UpdateFunc方法中添加日记,发现这些方法被调用了,但却没有触发控制器的Reconcile方法执行。

满足Autoscaler触发自定扩容Node的条件是什么?

我们使用自定义的调度器来调度pod,有自定义的Filter插件。Autoscaler在执行扩容之前,会调用Filter插件,尝试是不是真的没有node满足调度这个pod再去扩容。而默认情况下,Autoscaler拿的是默认的Filter插件,拿不到我们自定义的Filter插件,所以没有走我们的Filter逻辑,所以不会扩容。

Autopilot: workload autoscaling at Google论文描述的requests预测算法

本篇简单描述(Autopilot: workload autoscaling at Google)论文中描述的资源request预测算法,不需要理解论文中那复杂的数学公式。

K8s Pod可观测cpu使用率转cpu使用量

前面《如何获取Pod的CPU和内存指标,使用Grafana Agent收集指标,上传到Prometheus》这篇介绍的指标获取只拿到了cpu使用率,怎么转成cpu使用量呢?

云原生专栏第一阶段完结,第二阶段开始

由于方向的错误,我们第一阶段开发的云原生PaaS平台这个项目也迎来了终结。但我们换了个方向继续做云原生PaaS平台。