张鹏,腾讯云容器产品工程师,拥有多年云原生项目开发落地经验。目前主要负责腾讯云 TKE 云原生 AI 产品的开发工作。
谢远东,腾讯高级工程师,Kubeflow Member、Fluid(CNCF Sandbox) 核心开发者,负责腾讯云 TKE 在 AI 场景的研发和支持工作。
随着 Kubernetes 的日趋成熟,越来越多的公司、企业开始使用 K8s 来构建自己的云原生平台,基于 kubernetes 良好的扩展性以及成熟稳定的架构,你可以快速部署并管理自己的云原生应用。
目前我们也在基于 kubernetes 打造一个云原生 AI 平台(我们称它为:SKAI),该平台具备极致弹性、多云兼容性、高易用、可观测性、可复现性的特点,旨在利用云原生的思想和技术,为 AI 场景的数据处理、模型训练、模型上线推理等需求构建弹性可扩展的系统架构,从而提升资源利用率。
为了使我们的平台更加的云原生,我们没有选择常用的 web 框架来构建 API 服务,而是使用 kubernetes 扩展来构建整个平台,这样使我们的平台能更好的和 kubernetes 融合,可以无缝适配任何基于 k8s 的多云混合云环境。
尽管使用 gin、go-restful 等 go 语言 web 框架可以轻易地构建出一个稳定的 API 接口服务,但以 kubernetes 原生的方式构建 API 接口服务还是有很多优势,例如:
但是在很多场景下,我们还是不能确定到底使用聚合 API(Aggregated APIServer)还是独立 API 来构建我们的服务,官方为我们提供了两种选择的对比;如果你不能确定使用聚合 API 还是独立 API,下面的表格或许对你有帮助:
考虑 API 聚合的情况 | 优选独立 API 的情况 |
---|---|
你在开发新的 API | 你已经有一个提供 API 服务的程序并且工作良好 |
你希望可以是使用 kubectl 来读写你的新资源类别 |
不要求 kubectl 支持 |
你希望在 Kubernetes UI (如仪表板)中和其他内置类别一起查看你的新资源类别 | 不需要 Kubernetes UI 支持 |
你希望复用 Kubernetes API 支持特性 | 你不需要这类特性 |
你有意愿取接受 Kubernetes 对 REST 资源路径所作的格式限制,例如 API 组和名字空间。(参阅 API 概述) | 你需要使用一些特殊的 REST 路径以便与已经定义的 REST API 保持兼容 |
你的 API 是声明式的 | 你的 API 不符合声明式模型 |
你的资源可以自然地界定为集群作用域或集群中某个名字空间作用域 | 集群作用域或名字空间作用域这种二分法很不合适;你需要对资源路径的细节进行控制 |
首先我们希望我们的 SKAI 平台能更好的和 k8s 结合,并且它是一个声明式的 API,尽可能的复用 Kubernets API 的特性,显然聚合 API 对我们来说更加适合。
除了聚合 API,官方还提供了另一种方式以实现对标准 kubernetes API 接口的扩展:CRD(Custom Resource Definition ),能达到与聚合 API 基本一样的功能,而且更加易用,开发成本更小,但相较而言聚合 API 则更为灵活。针对这两种扩展方式如何选择,官方也提供了相应的参考。
通常,如果存在以下情况,CRD 可能更合适:
两种方式的核心区别是定义 api-resource 的方式不同。在 Aggregated APIServer 方式中,api-resource 是通过代码向 API 注册资源类型,而 Custom Resource 是直接通过 yaml 文件向 API 注册资源类型。
简单来说就是 CRD 是让 kube-apiserver 认识更多的对象类别(Kind),Aggregated APIServer 是构建自己的 APIServer 服务。虽然 CRD 更简单,但是缺少更多的灵活性,更详细的 CRDs 与 Aggregated API 的对比可参考官方文档。
对于我们而言,我们希望使用更多的高级 API 特性,例如 “logs” 或 “exec”,而不仅仅局限于 CRUD ,所以我们最终选择了 Aggregated APIServer 。
kube-apiserver 作为整个 Kubernetes 集群操作 etcd 的唯一入口,负责 Kubernetes 各资源的认证&鉴权,校验以及 CRUD 等操作,提供 RESTful APIs,供其它组件调用:
kube-apiserver 其实包含三种 APIServer:
apiregistration.k8s.io
组下的 APIService 资源请求,同时将来自用户的请求拦截转发给 Aggregated APIServer(AA);三个 APIServer 通过 delegation 的关系关联,在 kube-apiserver 初始化创建的过程中,首先创建的是 APIExtensionsServer,它的 delegationTarget 是一个空的 Delegate,即什么都不做,继而将 APIExtensionsServer 的 GenericAPIServer,作为 delegationTarget 传给了 KubeAPIServer,创建出了 KubeAPIServer,再然后,将 kubeAPIServer 的 GenericAPIServer 作为 delegationTarget 传给了 AggregatorServer,创建出了 AggregatorServer,所以他们之间 delegation 的关系为: Aggregator -> KubeAPIServer -> APIExtensions,如下图所示:
虽然官方提供了一个 sample-apiserver,我们可以参考实现自己的 Aggregated APIServer。但完全手工编写太过复杂,也不便于后期维护,我们最终选择了官方推荐的工具 apiserver-builder,apiserver-builder 可以帮助我们快速创建项目骨架,并且使用 apiserver-builder 构建的项目目录结构比较清晰,更利于后期维护。
通过 Go Get 安装
$ GO111MODULE=on go get sigs.k8s.io/apiserver-builder-alpha/cmd/apiserver-boot
通过安装包安装
export PATH=$PATH:/usr/local/apiserver-builder/bin
apiserver-boot -h
完成 apiserver-boot 安装后,可通过如下命令来初始化一个 Aggregated APIServer 项目:
$ mkdir skai-demo $ cd skai-demo $ apiserver-boot init repo --domain skai.io
执行后会生成如下目录:
. ├── BUILD.bazel ├── Dockerfile ├── Makefile ├── PROJECT ├── WORKSPACE ├── bin ├── cmd │ ├── apiserver │ │ └── main.go │ └── manager │ └── main.go -> ../../main.go ├── go.mod ├── hack │ └── boilerplate.go.txt ├── main.go └── pkg └── apis └── doc.go
$ apiserver-boot create group version resource --group animal --version v1alpha1 --kind Cat --non-namespaced=false Create Resource [y/n] y Create Controller [y/n] n
可根据自己的需求选择是否生成 Controller,我们这里暂时选择不生成, 对于需要通过 namespace 隔离的 resource 需要增加 --non-namespaced=false 的参数,默认都是 true。
执行完成后代码结构如下:
└── pkg └── apis ├── animal │ ├── doc.go │ └── v1alpha1 │ ├── cat_types.go │ ├── doc.go │ └── register.go └── doc.go
可以看到在 pkg/apis 下生成了 animal 的 group 并在 v1alpha1 版本下新增了 cat_types.go
文件,此文件包含了我们资源的基础定义,我们在 spec 中增加字段定义,并在已经实现的 Validate
方法中完成基础字段的校验。
// Cat // +k8s:openapi-gen=true type Cat struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec CatSpec `json:"spec,omitempty"` Status CatStatus `json:"status,omitempty"` } // CatSpec defines the desired state of Cat type CatSpec struct { Name string `json:"name"` } func (in *Cat) Validate(ctx context.Context) field.ErrorList { allErrs := field.ErrorList{} if len(in.Spec.Name) == 0 { allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "name"), in.Spec.Name, "must be specify")) } return allErrs }
完成以上步骤,你其实已经拥有一个完整的 Aggregated APIServer,接下来我们试着将它运行起来;apiserver-boot 本身提供了两种运行模式:in-cluster、local; local 模式下只作为单独的 API 服务部署在本地方便做调试,过于简单这里不做过多介绍,主要关注一下 in-cluster 模式;in-cluster 可以将你的 Aggregated APIServer 部署在任何 K8s 集群中,例如:minikube,腾讯 TKE,EKS 等,我们这里使用 EKS 集群作为演示。
$ apiserver-boot run in-cluster --image=xxx/skai.io/skai-demo:0.0.1 --name=skai-demo --namespace=default
在执行部署命令过程中,apiserver-boot 主要帮我们做了如下几件事情:
$ kubectl api-versions |grep animal animal.skai.io/v1alpha1
$ kubectl get apiservice v1alpha1.animal.skai.io NAME SERVICE AVAILABLE AGE v1alpha1.animal.skai.io default/skai-demo True 19h
创建
$ cat lucky.yaml apiVersion: animal.skai.io/v1alpha1 kind: Cat metadata: name: mycat namespace: default spec: name: lucky # 创建自定义 resource $ kubectl apply -f lucky.yaml
查找
# 查找自定义 resource 列表 $ kubectl get cat NAME CREATED AT mycat 2021-11-17T09:08:10Z # 查找自定义资源详情 $ kubectl get cat mycat -oyaml apiVersion: animal.skai.io/v1alpha1 kind: Cat metadata: annotations: kubectl.kubernetes.io/last-applied-configuration: | {"apiVersion":"animal.skai.io/v1alpha1","kind":"Cat","metadata":{"annotations":{},"name":"mycat"},"spec":{"name":"lucky"}} creationTimestamp: "2021-11-17T09:08:10Z" name: mycat resourceVersion: "17" uid: 98af0905-f01d-4042-bad3-71b96c0919f4 spec: name: lucky status: {}
本文从实战角度出发介绍我们开发 SKAI 平台过程中选择 Aggregated API 的原因,以及 kube-apisever 的扩展原理,最后介绍了 apiserver-builder 工具,并演示如何一步一步构建起自己的 Aggregated API,并将它部署到 EKS 集群中。希望该篇 Aggregated APIServer 最佳实践可以帮助即将使用 K8s API 扩展来构建云原生应用的开发者。