基于 kubebuilder 的 operators 的设计 & 开发

前文提要

本篇开始开发一个有实际作用的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,做以下几步改动:
  1. 修改数据结构ElasticWebSpec,增加前文设计的四个字段;
  2. 修改数据结构ElasticWebStatus,增加前文设计的一个字段;
  3. 增加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 的整个开发设计流程;

posted @ 2021-02-28 22:52  流雨声  阅读(561)  评论(0编辑  收藏  举报