kube-apiserver源码剖析与开发(九):apiservice 扩展

53 阅读6分钟

这篇文章我们来说说 kube-apiserver 扩展 apiservice。

apiservice是什么

我们先看下官方描述

APIService 是用来表示一个特定的 GroupVersion 的服务器。名称必须为 "version.group"。

首先每一个 apiservice 代表的的是一个服务器,它代表的后端可以对外提供服务。我们可以看下集群内有哪些 apiservice:

kubectl get apiservice

apiservice.png

看可以看到 NAME 列就是 apiservice,满足 "version.group"。我们可以根据 NAME 列和 SERVICE 列分类

按照 NAME 分类

  • Group 为空 从上面图中我们能看到第一个 apiservice 的 name 为 "v1.",它的 Group 为空,我们在 kube-apiserver 源码分析篇章中说过,group 为空的 api 属于 legacyAPI。由于在早期 k8s 设计 api 的时候没有 Group 的概念,后来为了向前兼容,就把 Group 设置为空。常见的 pod、node 等 api 资源都属于 "v1." 这个 apiservice。

  • Group 非空

按照 SERVICE 列分类

  • Local Local 的意思就是服务由 kube-apiserver 自身提供,例如我们执行 kubectl get po 这种命令,返回内容就是由 kube-apiserver 直接返回的,而不是代理给外部服务由外部返回的。

  • 非 Local 非 Local 的 service 表示,请求不是由 kube-apiserver 自身处理,而是由 kube-apiserver 做代理转发给外部服务,这个服务部署在 k8s 集群内,通过 service 暴露出来。上图中的 kube-system/metrics-server 就是非 Local 的 service,它是在集群部署完后部署的,非 k8s 内置 api。

我们来看下这两种 service 在 k8s 中存储的内容区别

v1.

apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
  name: v1.
spec:
  groupPriorityMinimum: 18000
  version: v1
  versionPriority: 1

v1beta1.metrics.k8s.io

apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
  name: v1beta1.metrics.k8s.io
spec:
  group: metrics.k8s.io
  groupPriorityMinimum: 100
  insecureSkipTLSVerify: true
  service:
    name: metrics-server
    namespace: kube-system
    port: 443
  version: v1beta1
  versionPriority: 100

我们能看到 Local 和 非 Local 的 apiservice 最大的区别在于 spec.service 字段,Local 的 apiservice spec.service 是空的,因为属于 Local 的 apiservice 的 api 请求都会被 kube-aiserver 自身处理然后返回结果,不需要集群内的其他 service 处理。而非 Local 的 spec.service 是非空的,表示如果 kube-apiserver 如果是收到这个 apiservice 下面的 api 请求,那么就把请求转发到 spec.service 中描述的 k8s service 中去。

那么现在有这么个问题,比如有一个服务,已经运行在集群中,也有 service 可以对外提供服务,那么为什么还需要 apiservice 呢? 主要有如下几个点:

  • 服务提供的 api 风格可以和 k8s 保持一致,如 /apis/{group}/{version/}/namespaces/{namespace}/{kind}
  • 可以使用 k8s 的认证鉴权能力
  • 可以使用 kubectl 工具对 api 进行管理

我们在前面的文章中已经说过 kube-apiserver 的工作原理,包括了一个请求的转发路径,这里我们聚焦如果一个请求的目的地是用户扩展的apiservice,这个请求将会怎么转发。

a

kube-apiserver 不是一个单独的 server 而是由三个 server 组成的: aggregatorServer,kubeApiserver,apiExtensionsServer。

aggregatorServer 作为 kube-apiserver 的入口,默认监听 6443 端口,所有请求都会先经过 aggregatorServer。我们讨论的 apiservice 就是依托于 aggregatorServer 实现的。

如下源码,当一个请求到达 aggregatorServer 后,aggregatorServer 会判断该请求对应的 api 是否是 local 的,如果是的话,那么就把请求转发给本地的 server (kubeApiserver 或 apiExtensionsServer);如果不是本地的 api,那么就从 apiservice 中解析出 namespace、serviceName、servicePort,然后使用这些信息组合出用户的 server 地址,将请求转发给用户的 server,用户 server 请求处理完后再原路返回到 aggregatorServer,最后返回给客户端。

// vendor/k8s.io/kube-aggregator/pkg/apiserver/handler_proxy.go

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)
	// 判断是否是本地的 api
	if handlingInfo.local {
		if r.localDelegate == nil {
			http.Error(w, "", http.StatusNotFound)
			return
		}
		r.localDelegate.ServeHTTP(w, req)
		return
	}
    // 判断用户的 apiservice 是否处于正常状态
	if !handlingInfo.serviceAvailable {
		proxyError(w, req, "service unavailable", http.StatusServiceUnavailable)
		return
	}

	if handlingInfo.transportBuildingError != nil {
		proxyError(w, req, handlingInfo.transportBuildingError.Error(), http.StatusInternalServerError)
		return
	}

	user, ok := genericapirequest.UserFrom(req.Context())
	if !ok {
		proxyError(w, req, "missing user", http.StatusInternalServerError)
		return
	}

	location := &url.URL{}
	location.Scheme = "https"
	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
	}
	location.Host = rloc.Host
	location.Path = req.URL.Path
	location.RawQuery = req.URL.Query().Encode()

	newReq, cancelFn := 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 upgrade {
		transport.SetAuthProxyHeaders(newReq, user.GetName(), user.GetGroups(), user.GetExtra())
	}

	handler := proxy.NewUpgradeAwareHandler(location, proxyRoundTripper, true, upgrade, &responder{w: w})
	utilflowcontrol.RequestDelegated(req.Context())
	// 开始请求
	handler.ServeHTTP(w, newReq)
}

下面我们就实践一下,去扩展一个我们自己的 apiservice。

首先我们基于 nginx 镜像,创建一个 docker 镜像,它监听一个 443 端口的,提供 https 服务,这个服务就是 aggregation 转发的后端。这个 https 服务很简单(我们重点在于实践扩展 apiservice),它的 location 如下:

location / {
default_type application/json;
return 200 '{"status":"success","result":"nginx json"}';
}

https 证书可以使用 cfssl 工具制作,这里我们偷个懒,我们使用 k8s 集群内现成的 ssl 证书,如果使用的是 kubeadm 默认配置部署的集群,他在如下目录:

/etc/kubernetes/pki/apiserver.crt
/etc/kubernetes/pki/apiserver.key
  1. 制作 nginx 镜像
FROM nginx
RUN mkdir -p /etc/nginx/ssl
COPY server.crt /etc/nginx/ssl
COPY server.key /etc/nginx/ssl
COPY nginx.conf /etc/nginx/
CMD ["nginx", "-g", "daemon off;"]

其中,nginx.conf 文件如下

user www-data;
worker_processes auto;
pid /run/nginx.pid;

events {
  worker_connections 768;
  # multi_accept on;
}

http {

  sendfile on;
  tcp_nopush on;
  tcp_nodelay on;
  keepalive_timeout 65;
  types_hash_max_size 2048;

  include /etc/nginx/mime.types;
  default_type application/octet-stream;



  access_log /var/log/nginx/access.log;
  error_log /var/log/nginx/error.log;


   server {
      listen 443 ssl;
      server_name k8s.test.com;
      ssl_certificate  /etc/nginx/ssl/server.crt;
      ssl_certificate_key /etc/nginx/ssl/server.key;
      location / {
        default_type application/json;
        return 200 '{"status":"success","result":"nginx json"}';
      }
    }
}

2. 创建 k8s 资源:deployment、service

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-for-apiservice
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx-for-apiservice
  template:
    metadata:
      labels:
        app: nginx-for-apiservice
    spec:
      containers:
      - name: nginx
        image: harbor.shopeemobile.com/logging-platform/nginx:1.0
        ports:
        - containerPort: 443

---

apiVersion: v1
kind: Service
metadata:
  name: nginx-for-apiservice
  labels:
    app: nginx-for-apiservice
spec:
  selector:
    app: nginx-for-apiservice
  ports:
  - port: 443
    targetPort: 443

创建完后,我们可以先通过 service 访问,看是否能正常访问

kubectl get svc

NAME                   TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)         AGE
kubernetes             ClusterIP   172.21.0.1      <none>        443/TCP         268d
nginx-for-apiservice   NodePort    172.21.10.115   <none>        443/TCP         80s

正常返回

curl -k https://172.21.10.115:443

{"status":"success","result":"nginx json"}

3. 经过上面步骤,后端服务就已经正常了,现在我们开始配置 apiservice

下面是 apiservice 的 yaml 文件

apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
  name: v1beta1.test.k8s.io
spec:
  group: test.k8s.io
  groupPriorityMinimum: 100
  insecureSkipTLSVerify: true
  service:
    name: nginx-for-apiservice
    namespace: default
    port: 443
  version: v1beta1
  versionPriority: 100

我们创建的组为 test.k8s.io, api 版本为 v1beta1

我们再看下集群的 aipservices,可以看到我们已经成功创建了自定义的 apiservice

2.png

  1. 使用 curl命令访问 api 进行验证

但是此时还需要增加 kube-apiserver 校验的 token,为了获取到这个 token 你可以创建一个 serviceAccount,k8s 会自动在对应的命名空间下给你常见 secret,这个 secret中就有 token 了,你还需要给创建的 serviceAccount 进行角色绑定,赋予访问 kube-apiserver 资源的权限。 验证结果如下,可以看到现在已经可以通过 kube-apiserver 的host:port 对我们的 nginx 进行访问了,说明我们自定义的 apiservice已经生效,这个结果和我们直接访问 nginx 的 service 是一样的。

3.png

注意,在创建 apiservice 的时候有一些参数需要注意,需要参考官方文档,这里不再赘述