前文提要
本篇开始开发一个有实际作用的operator,该operator名为elasticweb,既弹性web服务,本篇参考网络的文章进行汇总实践;
这将是一次完整的operator开发实战,设计、编码、部署等环节都会涉及到每一个产品的细节和具体实现,elasticweb从CRD设计再到controller功能都有明确的业务含义,能执行业务逻辑。
需求背景
设计架构
背景:做过网站开发的同学对横向扩容应该都了解,简单的说,假设一个tomcat的QPS上限为500,如果外部访问的QPS达到了600,为了保障整个网站服务质量,必须再启动一个同样的tomcat来共同分摊请求,如下图所示(简单起见,假设咱们的后台服务是无状态的,也就是说不依赖宿主机的IP、本地磁盘之类):
QPS:Queries-per-second,既每秒查询率,就是说服务器在一秒的时间内处理了多少个请求;
以上是横向扩容常规做法,在kubernetes环境,如果外部请求超过了单个pod的处理极限,我们可以增加pod数量来达到横向扩容的目的,如下图:
需求说明
场景描述:
1 开发人员需要将 springboot 应用部署到 kubernetes 上,目前的现状和面临的问题如下:
2 springboot应用已做成docker镜像;
3 通过压测得出单个pod的QPS为500;
4 估算得出上线后的总QPS会在800左右;
5 随着运营策略变化,QPS还会有调整;
总的来说,开发人员目前只有三个数据:docker镜像、单个pod的QPS、总QPS,需要有个方案将服务部署好,并且在运行期间能支撑外部的高并发访问;
需求汇总:
1 需要运维开发同学设计一个operator(名为elasticweb),将从用户那里获取的三个参数(docker镜像、单个pod的QPS、总QPS)告诉elasticweb就完事儿了,从而实现调度;
2 elasticweb在kubernetes创建pod,至于pod数量当然是自动算出来的,要确保能满足QPS要求,以前面的情况为例,需要两个pod才能满足800的QPS;
3 单个pod的QPS和总QPS都随时可能变化,一旦有变,elasticweb也要自动调整pod数量,以确保服务质量;
4 为了确保服务可以被外部调用,同时创建好service;
需求设计
CRD设计之Spec部分
Spec是用来保存用户的期望值的,也就是产品方提供的三个参数(docker镜像、单个pod的QPS、总QPS),再加上端口号:
1 image:业务服务对应的镜像
2 port:service占用的宿主机端口,外部请求通过此端口访问pod的服务
3 singlePodQPS:单个pod的QPS上限
4 totalQPS:当前整个业务的总QPS
对应用开发人员来说,输入这四个参数就可以了;
CRD设计之Status部分
Status用来保存实际值,这里设计成只有一个字段realQPS,表示当前整个operator实际能支持的QPS,这样无论何时,只要开发人员用kubectl describe命令就能知道当前系统实际上能支持多少QPS;
CRD源码分析
吃透如下代码:
package v1
import (
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"strconv"
)
// 期望状态
type ElasticWebSpec struct {
// 业务服务对应的镜像,包括名称:tag
Image string `json:"image"`
// service占用的宿主机端口,外部请求通过此端口访问pod的服务
Port *int32 `json:"port"`
// 单个pod的QPS上限
SinglePodQPS *int32 `json:"singlePodQPS"`
// 当前整个业务的总QPS
TotalQPS *int32 `json:"totalQPS"`
}
// 实际状态,该数据结构中的值都是业务代码计算出来的
type ElasticWebStatus struct {
// 当前kubernetes中实际支持的总QPS
RealQPS *int32 `json:"realQPS"`
}
// +kubebuilder:object:root=true
// ElasticWeb is the Schema for the elasticwebs API
type ElasticWeb struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec ElasticWebSpec `json:"spec,omitempty"`
Status ElasticWebStatus `json:"status,omitempty"`
}
func (in *ElasticWeb) String() string {
var realQPS string
if nil == in.Status.RealQPS {
realQPS = "nil"
} else {
realQPS = strconv.Itoa(int(*(in.Status.RealQPS)))
}
return fmt.Sprintf("Image [%s], Port [%d], SinglePodQPS [%d], TotalQPS [%d], RealQPS [%s]",
in.Spec.Image,
*(in.Spec.Port),
*(in.Spec.SinglePodQPS),
*(in.Spec.TotalQPS),
realQPS)
}
// +kubebuilder:object:root=true
// ElasticWebList contains a list of ElasticWeb
type ElasticWebList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []ElasticWeb `json:"items"`
}
func init() {
SchemeBuilder.Register(&ElasticWeb{}, &ElasticWebList{})
}
业务逻辑设计
CRD的完成代表核心数据结构已经确定,接下来是业务逻辑的设计,主要是理清楚controller的Reconcile方法里面做些啥,其实核心逻辑还是非常简单的:算出需要多少个pod,然后通过更新deployment让pod数量达到要求,在此核心的基础上再把创建deployment和service、更新status这些琐碎的事情做好,就完事儿了;
业务逻辑的流程图给出来如下所示:
接下来,将会基于上述架构图完成需求任务的开发工作,并且基于代码进行部署测验工作;
operator编码
项目elasticweb
新建名为elasticweb的文件夹,在里面执行以下命令即可创建名为elasticweb的项目,domain为com.vpc123:
mkdir elasticweb
go mod init elasticweb
kubebuilder init --domain com.vpc123
创建CRD资源,执行以下命令即可创建相关资源:
kubebuilder create api \
--group elasticweb \
--version v1 \
--kind ElasticWeb
通过goland打开项目工程:
CRD编码
打开文件api/v1/elasticweb_types.go,做以下几步改动:
- 修改数据结构ElasticWebSpec,增加前文设计的四个字段;
- 修改数据结构ElasticWebStatus,增加前文设计的一个字段;
- 增加String方法,这样打印日志时方便我们查看,注意RealQPS字段是指针,因此可能为空,需要判空;
完整的elasticweb_types.go如下所示:
package v1
import (
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"strconv"
)
// 期望状态
type ElasticWebSpec struct {
// 业务服务对应的镜像,包括名称:tag
Image string `json:"image"`
// service占用的宿主机端口,外部请求通过此端口访问pod的服务
Port *int32 `json:"port"`
// 单个pod的QPS上限
SinglePodQPS *int32 `json:"singlePodQPS"`
// 当前整个业务的总QPS
TotalQPS *int32 `json:"totalQPS"`
}
// 实际状态,该数据结构中的值都是业务代码计算出来的
type ElasticWebStatus struct {
// 当前kubernetes中实际支持的总QPS
RealQPS *int32 `json:"realQPS"`
}
// +kubebuilder:object:root=true
// ElasticWeb is the Schema for the elasticwebs API
type ElasticWeb struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec ElasticWebSpec `json:"spec,omitempty"`
Status ElasticWebStatus `json:"status,omitempty"`
}
func (in *ElasticWeb) String() string {
var realQPS string
if nil == in.Status.RealQPS {
realQPS = "nil"
} else {
realQPS = strconv.Itoa(int(*(in.Status.RealQPS)))
}
return fmt.Sprintf("Image [%s], Port [%d], SinglePodQPS [%d], TotalQPS [%d], RealQPS [%s]",
in.Spec.Image,
*(in.Spec.Port),
*(in.Spec.SinglePodQPS),
*(in.Spec.TotalQPS),
realQPS)
}
// +kubebuilder:object:root=true
// ElasticWebList contains a list of ElasticWeb
type ElasticWebList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []ElasticWeb `json:"items"`
}
func init() {
SchemeBuilder.Register(&ElasticWeb{}, &ElasticWebList{})
}
在elasticweb目录下执行make install即可部署CRD到kubernetes:
cd $GOPATH/src/elasticweb
[root@master elasticweb]# make install
which: no controller-gen in (/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin:/usr/local/go/bin:/usr/local/kubebuilder/bin:/root/bin)
go: creating new go.mod: module tmp
go: found sigs.k8s.io/controller-tools/cmd/controller-gen in sigs.k8s.io/controller-tools v0.2.5
/home/gowork/bin/controller-gen "crd:trivialVersions=true" rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
kustomize build config/crd | kubectl apply -f -
customresourcedefinition.apiextensions.k8s.io/elasticwebs.elasticweb.com.vpc123 configured
部署成功后,用api-versions命令可以查到该GV:
kubectl api-versions | grep elasticweb
添加资源访问权限
elasticweb会对service、deployment这两种资源做查询、新增、修改等操作,因此需要这些资源的操作权限,增加下图红框中的两行注释,这样代码生成工具就会在RBAC配置中增加对应的权限(更改 elasticweb_controller.go 文件内容):
// +kubebuilder:rbac:groups=elasticweb.com.vpc123,resources=elasticwebs,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=elasticweb.com.vpc123,resources=elasticwebs/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=apps,resources=services,verbs=get;list;watch;create;update;patch;delete
func (r *ElasticWebReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
_ = context.Background()
_ = r.Log.WithValues("elasticweb", req.NamespacedName)
// your logic here
return ctrl.Result{}, nil
}
常量定义
先把常量准备好,可见每个pod使用的CPU和内存都是在此固定的,您也可以改成在Spec中定义,这样就可以从外部传入了,另外这里为每个pod只分配了0.1个CPU,可以酌情调整该值:
const (
// deployment中的APP标签名
APP_NAME = "elastic-app"
// tomcat容器的端口号
CONTAINER_PORT = 8080
// 单个POD的CPU资源申请
CPU_REQUEST = "100m"
// 单个POD的CPU资源上限
CPU_LIMIT = "100m"
// 单个POD的内存资源申请
MEM_REQUEST = "512Mi"
// 单个POD的内存资源上限
MEM_LIMIT = "512Mi"
)
方法getExpectReplicas
有个很重要的逻辑:根据单个pod的QPS和总QPS,计算需要多少个pod,咱们将这个逻辑封装到一个方法中以便使用:
// 根据单个QPS和总QPS计算pod数量
func getExpectReplicas(elasticWeb *elasticwebv1.ElasticWeb) int32 {
// 单个pod的QPS
singlePodQPS := *(elasticWeb.Spec.SinglePodQPS)
// 期望的总QPS
totalQPS := *(elasticWeb.Spec.TotalQPS)
// Replicas就是要创建的副本数
replicas := totalQPS / singlePodQPS
if totalQPS%singlePodQPS > 0 {
replicas++
}
return replicas
}
方法createServiceIfNotExists
- 将创建service的操作封装到一个方法中,是的主干代码的逻辑更清晰,可读性更强;
- 创建service的时候,有几处要注意:
1 先查看service是否存在,不存在才创建;
2 将service和CRD实例elasticWeb建立关联(controllerutil.SetControllerReference方法),这样当elasticWeb被删除的时候,service会被自动删除而无需我们干预;
3 创建service的时候用到了client-go工具;
创建service的完整方法如下:
// 新建service
func createServiceIfNotExists(ctx context.Context, r *ElasticWebReconciler, elasticWeb *elasticwebv1.ElasticWeb, req ctrl.Request) error {
log := r.Log.WithValues("func", "createService")
service := &corev1.Service{}
err := r.Get(ctx, req.NamespacedName, service)
// 如果查询结果没有错误,证明service正常,就不做任何操作
if err == nil {
log.Info("service exists")
return nil
}
// 如果错误不是NotFound,就返回错误
if !errors.IsNotFound(err) {
log.Error(err, "query service error")
return err
}
// 实例化一个数据结构
service = &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Namespace: elasticWeb.Namespace,
Name: elasticWeb.Name,
},
Spec: corev1.ServiceSpec{
Ports: []corev1.ServicePort{{
Name: "http",
Port: 8080,
NodePort: *elasticWeb.Spec.Port,
},
},
Selector: map[string]string{
"app": APP_NAME,
},
Type: corev1.ServiceTypeNodePort,
},
}
// 这一步非常关键!
// 建立关联后,删除elasticweb资源时就会将deployment也删除掉
log.Info("set reference")
if err := controllerutil.SetControllerReference(elasticWeb, service, r.Scheme); err != nil {
log.Error(err, "SetControllerReference error")
return err
}
// 创建service
log.Info("start create service")
if err := r.Create(ctx, service); err != nil {
log.Error(err, "create service error")
return err
}
log.Info("create service success")
return nil
}
方法createDeployment
- 将创建deployment的操作封装在一个方法中,同样是为了将主干逻辑保持简洁;
- 创建deployment的方法也有几处要注意:
1 调用getExpectReplicas方法得到要创建的pod的数量,该数量是创建deployment时的一个重要参数;
2 每个pod所需的CPU和内存资源也是deployment的参数;
3 将deployment和elasticweb建立关联,这样删除elasticweb的时候deplyment就会被自动删除了;
4 同样是使用client-go客户端工具创建deployment资源;
// 新建deployment
func createDeployment(ctx context.Context, r *ElasticWebReconciler, elasticWeb *elasticwebv1.ElasticWeb) error {
log := r.Log.WithValues("func", "createDeployment")
// 计算期望的pod数量
expectReplicas := getExpectReplicas(elasticWeb)
log.Info(fmt.Sprintf("expectReplicas [%d]", expectReplicas))
// 实例化一个数据结构
deployment := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Namespace: elasticWeb.Namespace,
Name: elasticWeb.Name,
},
Spec: appsv1.DeploymentSpec{
// 副本数是计算出来的
Replicas: pointer.Int32Ptr(expectReplicas),
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": APP_NAME,
},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"app": APP_NAME,
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: APP_NAME,
// 用指定的镜像
Image: elasticWeb.Spec.Image,
ImagePullPolicy: "IfNotPresent",
Ports: []corev1.ContainerPort{
{
Name: "http",
Protocol: corev1.ProtocolSCTP,
ContainerPort: CONTAINER_PORT,
},
},
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
"cpu": resource.MustParse(CPU_REQUEST),
"memory": resource.MustParse(MEM_REQUEST),
},
Limits: corev1.ResourceList{
"cpu": resource.MustParse(CPU_LIMIT),
"memory": resource.MustParse(MEM_LIMIT),
},
},
},
},
},
},
},
}
// 这一步非常关键!
// 建立关联后,删除elasticweb资源时就会将deployment也删除掉
log.Info("set reference")
if err := controllerutil.SetControllerReference(elasticWeb, deployment, r.Scheme); err != nil {
log.Error(err, "SetControllerReference error")
return err
}
// 创建deployment
log.Info("start create deployment")
if err := r.Create(ctx, deployment); err != nil {
log.Error(err, "create deployment error")
return err
}
log.Info("create deployment success")
return nil
}
方法updateStatus
不论是创建deployment资源对象,还是对已有的deployment的pod数量做调整,这些操作完成后都要去修改Status,既实际的状态,这样外部才能随时随地知道当前elasticweb支持多大的QPS,因此需要将修改Status的操作封装到一个方法中,给多个场景使用,Status的计算逻辑很简单:pod数量乘以每个pod的QPS就是总QPS了,代码如下:
// 完成了pod的处理后,更新最新状态
func updateStatus(ctx context.Context, r *ElasticWebReconciler, elasticWeb *elasticwebv1.ElasticWeb) error {
log := r.Log.WithValues("func", "updateStatus")
// 单个pod的QPS
singlePodQPS := *(elasticWeb.Spec.SinglePodQPS)
// pod总数
replicas := getExpectReplicas(elasticWeb)
// 当pod创建完毕后,当前系统实际的QPS:单个pod的QPS * pod总数
// 如果该字段还没有初始化,就先做初始化
if nil == elasticWeb.Status.RealQPS {
elasticWeb.Status.RealQPS = new(int32)
}
*(elasticWeb.Status.RealQPS) = singlePodQPS * replicas
log.Info(fmt.Sprintf("singlePodQPS [%d], replicas [%d], realQPS[%d]", singlePodQPS, replicas, *(elasticWeb.Status.RealQPS)))
if err := r.Update(ctx, elasticWeb); err != nil {
log.Error(err, "update instance error")
return err
}
return nil
}
主干代码
前面细枝末节都处理完毕,可以开始主流程了,有了前面的流程图的赋值,主流程的代码很容就写出来了,如下所示,已经添加了足够的注释,就不再赘述了:
func (r *ElasticWebReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
// 会用到context
ctx := context.Background()
log := r.Log.WithValues("elasticweb", req.NamespacedName)
// your logic here
log.Info("1. start reconcile logic")
// 实例化数据结构
instance := &elasticwebv1.ElasticWeb{}
// 通过客户端工具查询,查询条件是
err := r.Get(ctx, req.NamespacedName, instance)
if err != nil {
// 如果没有实例,就返回空结果,这样外部就不再立即调用Reconcile方法了
if errors.IsNotFound(err) {
log.Info("2.1. instance not found, maybe removed")
return reconcile.Result{}, nil
}
log.Error(err, "2.2 error")
// 返回错误信息给外部
return ctrl.Result{}, err
}
log.Info("3. instance : " + instance.String())
// 查找deployment
deployment := &appsv1.Deployment{}
// 用客户端工具查询
err = r.Get(ctx, req.NamespacedName, deployment)
// 查找时发生异常,以及查出来没有结果的处理逻辑
if err != nil {
// 如果没有实例就要创建了
if errors.IsNotFound(err) {
log.Info("4. deployment not exists")
// 如果对QPS没有需求,此时又没有deployment,就啥事都不做了
if *(instance.Spec.TotalQPS) < 1 {
log.Info("5.1 not need deployment")
// 返回
return ctrl.Result{}, nil
}
// 先要创建service
if err = createServiceIfNotExists(ctx, r, instance, req); err != nil {
log.Error(err, "5.2 error")
// 返回错误信息给外部
return ctrl.Result{}, err
}
// 立即创建deployment
if err = createDeployment(ctx, r, instance); err != nil {
log.Error(err, "5.3 error")
// 返回错误信息给外部
return ctrl.Result{}, err
}
// 如果创建成功就更新状态
if err = updateStatus(ctx, r, instance); err != nil {
log.Error(err, "5.4. error")
// 返回错误信息给外部
return ctrl.Result{}, err
}
// 创建成功就可以返回了
return ctrl.Result{}, nil
} else {
log.Error(err, "7. error")
// 返回错误信息给外部
return ctrl.Result{}, err
}
}
// 如果查到了deployment,并且没有返回错误,就走下面的逻辑
// 根据单QPS和总QPS计算期望的副本数
expectReplicas := getExpectReplicas(instance)
// 当前deployment的期望副本数
realReplicas := *deployment.Spec.Replicas
log.Info(fmt.Sprintf("9. expectReplicas [%d], realReplicas [%d]", expectReplicas, realReplicas))
// 如果相等,就直接返回了
if expectReplicas == realReplicas {
log.Info("10. return now")
return ctrl.Result{}, nil
}
// 如果不等,就要调整
*(deployment.Spec.Replicas) = expectReplicas
log.Info("11. update deployment's Replicas")
// 通过客户端更新deployment
if err = r.Update(ctx, deployment); err != nil {
log.Error(err, "12. update deployment replicas error")
// 返回错误信息给外部
return ctrl.Result{}, err
}
log.Info("13. update status")
// 如果更新deployment的Replicas成功,就更新状态
if err = updateStatus(ctx, r, instance); err != nil {
log.Error(err, "14. update status error")
// 返回错误信息给外部
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
至此,整个elasticweb operator编码就完成了,接下来部署、运行、镜像制作等操作;
构建部署运行
部署CRD
从控制台进入Makefile所在目录,执行命令make install,即可将CRD部署到kubernetes:
[root@master elasticweb]# make install
which: no controller-gen in (/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin:/usr/local/go/bin:/usr/local/kubebuilder/bin:/root/bin)
go: creating new go.mod: module tmp
go: found sigs.k8s.io/controller-tools/cmd/controller-gen in sigs.k8s.io/controller-tools v0.2.5
/home/gowork/bin/controller-gen "crd:trivialVersions=true" rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
kustomize build config/crd | kubectl apply -f -
customresourcedefinition.apiextensions.k8s.io/elasticwebs.elasticweb.com.vpc123 configured
从上述内容可见,实际上执行的操作是用kustomize将config/crd下的yaml资源合并后在kubernetes进行创建,
可以用命令kubectl api-versions验证CRD部署是否成功:
[root@master elasticweb]# kubectl api-versions|grep elasticweb
elasticweb.com.vpc123/v1
运行Controller
进入Makefile文件所在目录,执行命令make run即可编译运行controller:
新建elasticweb资源对象
- 负责处理elasticweb的Controller已经运行起来了,接下来就开始创建elasticweb资源对象吧,用yaml文件来创建;
- 在config/samples目录下,kubebuilder为咱们创建了demo文件elasticweb_v1_elasticweb.yaml,不过这里面spec的内容不是咱们定义的那四个字段,需要改成以下内容:
apiVersion: v1
kind: Namespace
metadata:
name: dev
labels:
name: dev
---
apiVersion: elasticweb.com.bolingcavalry/v1
kind: ElasticWeb
metadata:
namespace: dev
name: elasticweb-sample
spec:
# Add fields here
image: tomcat:8.0.18-jre8
port: 30003
singlePodQPS: 500
totalQPS: 600
对上述配置的几个参数做如下说明:
1 使用的namespace为dev
2 本次测试部署的应用为tomcat
3 service使用宿主机的30003端口暴露tomcat的服务
4 假设单个pod能支撑500QPS,外部请求的QPS为600
执行命令kubectl apply -f config/samples/elasticweb_v1_elasticweb.yaml,即可在kubernetes创建elasticweb实例:
[root@master elasticweb]# kubectl apply -f config/samples/elasticweb_v1_elasticweb.yaml
namespace/dev created
elasticweb.elasticweb.com.vpc123/elasticweb-sample created
去controller的窗口发现打印了不少日志,通过分析日志发现Reconcile方法执行了两次,第一执行时创建了deployment和service等资源:
浏览器验证业务功能
本次部署操作使用的docker镜像是tomcat,验证起来非常简单,打开默认页面能见到猫就证明tomcat启动成功了,我这kubernetes宿主机的IP地址是192.168.xx.xx,于是用浏览器访问http://192.168.xx.xx:30003,如下图,业务功能正常:
[root@master ~]# kubectl get elasticweb -n dev
NAME AGE
elasticweb-sample 2d12h
[root@master ~]#
[root@master ~]# kubectl get service -n dev
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
elasticweb-sample NodePort 10.104.224.245 <none> 8080:30003/TCP 2d12h
[root@master ~]#
[root@master ~]# kubectl get deployment -n dev
NAME READY UP-TO-DATE AVAILABLE AGE
elasticweb-sample 3/3 3 3 17h
[root@master ~]#
[root@master ~]# kubectl get pod -n dev
NAME READY STATUS RESTARTS AGE
elasticweb-sample-66947d9687-5wnbb 1/1 Running 0 4h50m
elasticweb-sample-66947d9687-v9464 1/1 Running 0 4h50m
elasticweb-sample-66947d9687-xgrl7 1/1 Running 0 4h50m
拓展阅读
本篇博客进行的编辑是 controllers 的操作和资源部署操作,实际上 kuberbilder 的二次开发将会涉及到的问题点主要集中在3方面的问题,自定义 api ;controllers 业务逻辑控制;webhook 转码校验;K8s 单元测试等模块,基本就可以完全掌握 kuberbilder 的整个开发设计流程;