基于 kubebuilder 的 operators 的設計 & 開發(fā)

前文提要

本篇開始開發(fā)一個有實際作用的operator戚哎,該operator名為elasticweb,既彈性web服務嫂用,本篇參考網(wǎng)絡的文章進行匯總實踐型凳;
這將是一次完整的operator開發(fā)實戰(zhàn),設計嘱函、編碼甘畅、部署等環(huán)節(jié)都會涉及到每一個產(chǎn)品的細節(jié)和具體實現(xiàn),elasticweb從CRD設計再到controller功能都有明確的業(yè)務含義,能執(zhí)行業(yè)務邏輯疏唾。

需求背景

設計架構(gòu)

背景:做過網(wǎng)站開發(fā)的同學對橫向擴容應該都了解蓄氧,簡單的說,假設一個tomcat的QPS上限為500槐脏,如果外部訪問的QPS達到了600喉童,為了保障整個網(wǎng)站服務質(zhì)量,必須再啟動一個同樣的tomcat來共同分攤請求顿天,如下圖所示(簡單起見堂氯,假設咱們的后臺服務是無狀態(tài)的,也就是說不依賴宿主機的IP牌废、本地磁盤之類):

QPS:Queries-per-second咽白,既每秒查詢率,就是說服務器在一秒的時間內(nèi)處理了多少個請求鸟缕;


image

以上是橫向擴容常規(guī)做法晶框,在kubernetes環(huán)境,如果外部請求超過了單個pod的處理極限懂从,我們可以增加pod數(shù)量來達到橫向擴容的目的授段,如下圖:

image

需求說明

場景描述:
1 開發(fā)人員需要將 springboot 應用部署到 kubernetes 上,目前的現(xiàn)狀和面臨的問題如下:
2 springboot應用已做成docker鏡像莫绣;
3 通過壓測得出單個pod的QPS為500;
4 估算得出上線后的總QPS會在800左右悠鞍;
5 隨著運營策略變化对室,QPS還會有調(diào)整;

總的來說咖祭,開發(fā)人員目前只有三個數(shù)據(jù):docker鏡像掩宜、單個pod的QPS、總QPS么翰,需要有個方案將服務部署好牺汤,并且在運行期間能支撐外部的高并發(fā)訪問;

需求匯總:

1 需要運維開發(fā)同學設計一個operator(名為elasticweb)浩嫌,將從用戶那里獲取的三個參數(shù)(docker鏡像檐迟、單個pod的QPS、總QPS)告訴elasticweb就完事兒了码耐,從而實現(xiàn)調(diào)度追迟;
2 elasticweb在kubernetes創(chuàng)建pod,至于pod數(shù)量當然是自動算出來的骚腥,要確保能滿足QPS要求敦间,以前面的情況為例,需要兩個pod才能滿足800的QPS;
3 單個pod的QPS和總QPS都隨時可能變化廓块,一旦有變厢绝,elasticweb也要自動調(diào)整pod數(shù)量,以確保服務質(zhì)量带猴;
4 為了確保服務可以被外部調(diào)用昔汉,同時創(chuàng)建好service;

需求設計

CRD設計之Spec部分

Spec是用來保存用戶的期望值的浓利,也就是產(chǎn)品方提供的三個參數(shù)(docker鏡像挤庇、單個pod的QPS、總QPS)贷掖,再加上端口號:

1 image:業(yè)務服務對應的鏡像
2 port:service占用的宿主機端口嫡秕,外部請求通過此端口訪問pod的服務
3 singlePodQPS:單個pod的QPS上限
4 totalQPS:當前整個業(yè)務的總QPS

對應用開發(fā)人員來說,輸入這四個參數(shù)就可以了苹威;

CRD設計之Status部分

Status用來保存實際值昆咽,這里設計成只有一個字段realQPS,表示當前整個operator實際能支持的QPS牙甫,這樣無論何時掷酗,只要開發(fā)人員用kubectl describe命令就能知道當前系統(tǒng)實際上能支持多少Q(mào)PS;

CRD源碼分析

吃透如下代碼:

package v1

import (
    "fmt"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "strconv"
)

// 期望狀態(tài)
type ElasticWebSpec struct {
    // 業(yè)務服務對應的鏡像窟哺,包括名稱:tag
    Image string `json:"image"`
    // service占用的宿主機端口泻轰,外部請求通過此端口訪問pod的服務
    Port *int32 `json:"port"`

    // 單個pod的QPS上限
    SinglePodQPS *int32 `json:"singlePodQPS"`
    // 當前整個業(yè)務的總QPS
    TotalQPS *int32 `json:"totalQPS"`
}

// 實際狀態(tài),該數(shù)據(jù)結(jié)構(gòu)中的值都是業(yè)務代碼計算出來的
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{})
}
業(yè)務邏輯設計

CRD的完成代表核心數(shù)據(jù)結(jié)構(gòu)已經(jīng)確定且轨,接下來是業(yè)務邏輯的設計浮声,主要是理清楚controller的Reconcile方法里面做些啥,其實核心邏輯還是非常簡單的:算出需要多少個pod旋奢,然后通過更新deployment讓pod數(shù)量達到要求泳挥,在此核心的基礎上再把創(chuàng)建deployment和service、更新status這些瑣碎的事情做好至朗,就完事兒了屉符;

業(yè)務邏輯的流程圖給出來如下所示:


image

接下來,將會基于上述架構(gòu)圖完成需求任務的開發(fā)工作锹引,并且基于代碼進行部署測驗工作矗钟;

operator編碼

項目elasticweb

新建名為elasticweb的文件夾,在里面執(zhí)行以下命令即可創(chuàng)建名為elasticweb的項目嫌变,domain為com.vpc123:

mkdir elasticweb
go mod init elasticweb
kubebuilder init --domain com.vpc123

創(chuàng)建CRD資源真仲,執(zhí)行以下命令即可創(chuàng)建相關資源:

kubebuilder create api \
--group elasticweb \
--version v1 \
--kind ElasticWeb

通過goland打開項目工程:


image

CRD編碼

打開文件api/v1/elasticweb_types.go,做以下幾步改動:
  1. 修改數(shù)據(jù)結(jié)構(gòu)ElasticWebSpec初澎,增加前文設計的四個字段秸应;
  2. 修改數(shù)據(jù)結(jié)構(gòu)ElasticWebStatus虑凛,增加前文設計的一個字段;
  3. 增加String方法软啼,這樣打印日志時方便我們查看桑谍,注意RealQPS字段是指針,因此可能為空祸挪,需要判空锣披;
完整的elasticweb_types.go如下所示:
package v1

import (
    "fmt"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "strconv"
)

// 期望狀態(tài)
type ElasticWebSpec struct {
    // 業(yè)務服務對應的鏡像,包括名稱:tag
    Image string `json:"image"`
    // service占用的宿主機端口贿条,外部請求通過此端口訪問pod的服務
    Port *int32 `json:"port"`

    // 單個pod的QPS上限
    SinglePodQPS *int32 `json:"singlePodQPS"`
    // 當前整個業(yè)務的總QPS
    TotalQPS *int32 `json:"totalQPS"`
}

// 實際狀態(tài)雹仿,該數(shù)據(jù)結(jié)構(gòu)中的值都是業(yè)務代碼計算出來的
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目錄下執(zhí)行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
image
添加資源訪問權限

elasticweb會對service整以、deployment這兩種資源做查詢胧辽、新增、修改等操作公黑,因此需要這些資源的操作權限邑商,增加下圖紅框中的兩行注釋,這樣代碼生成工具就會在RBAC配置中增加對應的權限(更改 elasticweb_controller.go 文件內(nèi)容):

// +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
}
image
常量定義

先把常量準備好凡蚜,可見每個pod使用的CPU和內(nèi)存都是在此固定的人断,您也可以改成在Spec中定義,這樣就可以從外部傳入了朝蜘,另外這里為每個pod只分配了0.1個CPU恶迈,可以酌情調(diào)整該值:

const (
    // deployment中的APP標簽名
    APP_NAME = "elastic-app"
    // tomcat容器的端口號
    CONTAINER_PORT = 8080
    // 單個POD的CPU資源申請
    CPU_REQUEST = "100m"
    // 單個POD的CPU資源上限
    CPU_LIMIT = "100m"
    // 單個POD的內(nèi)存資源申請
    MEM_REQUEST = "512Mi"
    // 單個POD的內(nèi)存資源上限
    MEM_LIMIT = "512Mi"
)
方法getExpectReplicas

有個很重要的邏輯:根據(jù)單個pod的QPS和總QPS,計算需要多少個pod谱醇,咱們將這個邏輯封裝到一個方法中以便使用:

// 根據(jù)單個QPS和總QPS計算pod數(shù)量
func getExpectReplicas(elasticWeb *elasticwebv1.ElasticWeb) int32 {
    // 單個pod的QPS
    singlePodQPS := *(elasticWeb.Spec.SinglePodQPS)

    // 期望的總QPS
    totalQPS := *(elasticWeb.Spec.TotalQPS)

    // Replicas就是要創(chuàng)建的副本數(shù)
    replicas := totalQPS / singlePodQPS

    if totalQPS%singlePodQPS > 0 {
        replicas++
    }

    return replicas
}
方法createServiceIfNotExists
  • 將創(chuàng)建service的操作封裝到一個方法中暇仲,是的主干代碼的邏輯更清晰,可讀性更強枣抱;
  • 創(chuàng)建service的時候熔吗,有幾處要注意:
    1 先查看service是否存在辆床,不存在才創(chuàng)建佳晶;
    2 將service和CRD實例elasticWeb建立關聯(lián)(controllerutil.SetControllerReference方法),這樣當elasticWeb被刪除的時候讼载,service會被自動刪除而無需我們干預轿秧;
    3 創(chuàng)建service的時候用到了client-go工具;

創(chuàng)建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)

    // 如果查詢結(jié)果沒有錯誤咨堤,證明service正常菇篡,就不做任何操作
    if err == nil {
        log.Info("service exists")
        return nil
    }

    // 如果錯誤不是NotFound,就返回錯誤
    if !errors.IsNotFound(err) {
        log.Error(err, "query service error")
        return err
    }

    // 實例化一個數(shù)據(jù)結(jié)構(gòu)
    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,
        },
    }

    // 這一步非常關鍵一喘!
    // 建立關聯(lián)后驱还,刪除elasticweb資源時就會將deployment也刪除掉
    log.Info("set reference")
    if err := controllerutil.SetControllerReference(elasticWeb, service, r.Scheme); err != nil {
        log.Error(err, "SetControllerReference error")
        return err
    }

    // 創(chuàng)建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
  • 將創(chuàng)建deployment的操作封裝在一個方法中嗜暴,同樣是為了將主干邏輯保持簡潔;
  • 創(chuàng)建deployment的方法也有幾處要注意:
    1 調(diào)用getExpectReplicas方法得到要創(chuàng)建的pod的數(shù)量议蟆,該數(shù)量是創(chuàng)建deployment時的一個重要參數(shù)闷沥;
    2 每個pod所需的CPU和內(nèi)存資源也是deployment的參數(shù);
    3 將deployment和elasticweb建立關聯(lián)咐容,這樣刪除elasticweb的時候deplyment就會被自動刪除了舆逃;
    4 同樣是使用client-go客戶端工具創(chuàng)建deployment資源;
// 新建deployment
func createDeployment(ctx context.Context, r *ElasticWebReconciler, elasticWeb *elasticwebv1.ElasticWeb) error {
    log := r.Log.WithValues("func", "createDeployment")

    // 計算期望的pod數(shù)量
    expectReplicas := getExpectReplicas(elasticWeb)

    log.Info(fmt.Sprintf("expectReplicas [%d]", expectReplicas))

    // 實例化一個數(shù)據(jù)結(jié)構(gòu)
    deployment := &appsv1.Deployment{
        ObjectMeta: metav1.ObjectMeta{
            Namespace: elasticWeb.Namespace,
            Name:      elasticWeb.Name,
        },
        Spec: appsv1.DeploymentSpec{
            // 副本數(shù)是計算出來的
            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),
                                },
                            },
                        },
                    },
                },
            },
        },
    }

    // 這一步非常關鍵戳粒!
    // 建立關聯(lián)后路狮,刪除elasticweb資源時就會將deployment也刪除掉
    log.Info("set reference")
    if err := controllerutil.SetControllerReference(elasticWeb, deployment, r.Scheme); err != nil {
        log.Error(err, "SetControllerReference error")
        return err
    }

    // 創(chuàng)建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

不論是創(chuàng)建deployment資源對象,還是對已有的deployment的pod數(shù)量做調(diào)整蔚约,這些操作完成后都要去修改Status奄妨,既實際的狀態(tài),這樣外部才能隨時隨地知道當前elasticweb支持多大的QPS炊琉,因此需要將修改Status的操作封裝到一個方法中展蒂,給多個場景使用,Status的計算邏輯很簡單:pod數(shù)量乘以每個pod的QPS就是總QPS了苔咪,代碼如下:

// 完成了pod的處理后锰悼,更新最新狀態(tài)
func updateStatus(ctx context.Context, r *ElasticWebReconciler, elasticWeb *elasticwebv1.ElasticWeb) error {
    log := r.Log.WithValues("func", "updateStatus")

    // 單個pod的QPS
    singlePodQPS := *(elasticWeb.Spec.SinglePodQPS)

    // pod總數(shù)
    replicas := getExpectReplicas(elasticWeb)

    // 當pod創(chuàng)建完畢后,當前系統(tǒng)實際的QPS:單個pod的QPS * pod總數(shù)
    // 如果該字段還沒有初始化团赏,就先做初始化
    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
}
主干代碼

前面細枝末節(jié)都處理完畢箕般,可以開始主流程了,有了前面的流程圖的賦值舔清,主流程的代碼很容就寫出來了丝里,如下所示,已經(jīng)添加了足夠的注釋体谒,就不再贅述了:

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")

    // 實例化數(shù)據(jù)結(jié)構(gòu)
    instance := &elasticwebv1.ElasticWeb{}

    // 通過客戶端工具查詢杯聚,查詢條件是
    err := r.Get(ctx, req.NamespacedName, instance)

    if err != nil {

        // 如果沒有實例,就返回空結(jié)果抒痒,這樣外部就不再立即調(diào)用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)

    // 查找時發(fā)生異常幌绍,以及查出來沒有結(jié)果的處理邏輯
    if err != nil {
        // 如果沒有實例就要創(chuàng)建了
        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
            }

            // 先要創(chuàng)建service
            if err = createServiceIfNotExists(ctx, r, instance, req); err != nil {
                log.Error(err, "5.2 error")
                // 返回錯誤信息給外部
                return ctrl.Result{}, err
            }

            // 立即創(chuàng)建deployment
            if err = createDeployment(ctx, r, instance); err != nil {
                log.Error(err, "5.3 error")
                // 返回錯誤信息給外部
                return ctrl.Result{}, err
            }

            // 如果創(chuàng)建成功就更新狀態(tài)
            if err = updateStatus(ctx, r, instance); err != nil {
                log.Error(err, "5.4. error")
                // 返回錯誤信息給外部
                return ctrl.Result{}, err
            }

            // 創(chuàng)建成功就可以返回了
            return ctrl.Result{}, nil
        } else {
            log.Error(err, "7. error")
            // 返回錯誤信息給外部
            return ctrl.Result{}, err
        }
    }

    // 如果查到了deployment傀广,并且沒有返回錯誤,就走下面的邏輯

    // 根據(jù)單QPS和總QPS計算期望的副本數(shù)
    expectReplicas := getExpectReplicas(instance)

    // 當前deployment的期望副本數(shù)
    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
    }

    // 如果不等伪冰,就要調(diào)整
    *(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成功,就更新狀態(tài)
    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編碼就完成了贮聂,接下來部署靠柑、運行、鏡像制作等操作吓懈;

構(gòu)建部署運行

部署CRD

從控制臺進入Makefile所在目錄病往,執(zhí)行命令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

從上述內(nèi)容可見骄瓣,實際上執(zhí)行的操作是用kustomize將config/crd下的yaml資源合并后在kubernetes進行創(chuàng)建,
可以用命令kubectl api-versions驗證CRD部署是否成功:

[root@master elasticweb]# kubectl api-versions|grep elasticweb
elasticweb.com.vpc123/v1
運行Controller
image

進入Makefile文件所在目錄停巷,執(zhí)行命令make run即可編譯運行controller:


image
新建elasticweb資源對象
  • 負責處理elasticweb的Controller已經(jīng)運行起來了,接下來就開始創(chuàng)建elasticweb資源對象吧榕栏,用yaml文件來創(chuàng)建畔勤;
  • 在config/samples目錄下,kubebuilder為咱們創(chuàng)建了demo文件elasticweb_v1_elasticweb.yaml扒磁,不過這里面spec的內(nèi)容不是咱們定義的那四個字段庆揪,需要改成以下內(nèi)容:
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

對上述配置的幾個參數(shù)做如下說明:
1 使用的namespace為dev
2 本次測試部署的應用為tomcat
3 service使用宿主機的30003端口暴露tomcat的服務
4 假設單個pod能支撐500QPS,外部請求的QPS為600
執(zhí)行命令kubectl apply -f config/samples/elasticweb_v1_elasticweb.yaml妨托,即可在kubernetes創(chuàng)建elasticweb實例:

[root@master elasticweb]# kubectl apply -f config/samples/elasticweb_v1_elasticweb.yaml
namespace/dev created
elasticweb.elasticweb.com.vpc123/elasticweb-sample created

去controller的窗口發(fā)現(xiàn)打印了不少日志缸榛,通過分析日志發(fā)現(xiàn)Reconcile方法執(zhí)行了兩次,第一執(zhí)行時創(chuàng)建了deployment和service等資源:


image
瀏覽器驗證業(yè)務功能

本次部署操作使用的docker鏡像是tomcat兰伤,驗證起來非常簡單内颗,打開默認頁面能見到貓就證明tomcat啟動成功了,我這kubernetes宿主機的IP地址是192.168.xx.xx敦腔,于是用瀏覽器訪問http://192.168.xx.xx:30003均澳,如下圖,業(yè)務功能正常:

[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
image

拓展閱讀

本篇博客進行的編輯是 controllers 的操作和資源部署操作符衔,實際上 kuberbilder 的二次開發(fā)將會涉及到的問題點主要集中在3方面的問題找前,自定義 api ;controllers 業(yè)務邏輯控制判族;webhook 轉(zhuǎn)碼校驗躺盛;K8s 單元測試等模塊,基本就可以完全掌握 kuberbilder 的整個開發(fā)設計流程形帮;

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末槽惫,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子沃缘,更是在濱河造成了極大的恐慌躯枢,老刑警劉巖则吟,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件槐臀,死亡現(xiàn)場離奇詭異,居然都是意外死亡氓仲,警方通過查閱死者的電腦和手機水慨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門得糜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人晰洒,你說我怎么就攤上這事朝抖。” “怎么了谍珊?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵治宣,是天一觀的道長。 經(jīng)常有香客問我砌滞,道長侮邀,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任贝润,我火速辦了婚禮绊茧,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘打掘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布抗悍。 她就那樣靜靜地躺著吃嘿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪横朋。 梳的紋絲不亂的頭發(fā)上况芒,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天,我揣著相機與錄音叶撒,去河邊找鬼绝骚。 笑死,一個胖子當著我的面吹牛祠够,可吹牛的內(nèi)容都是我干的压汪。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼古瓤,長吁一口氣:“原來是場噩夢啊……” “哼止剖!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起落君,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤穿香,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后绎速,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體皮获,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年纹冤,在試婚紗的時候發(fā)現(xiàn)自己被綠了洒宝。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片购公。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖雁歌,靈堂內(nèi)的尸體忽然破棺而出宏浩,到底是詐尸還是另有隱情,我是刑警寧澤靠瞎,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布比庄,位于F島的核電站,受9級特大地震影響乏盐,放射性物質(zhì)發(fā)生泄漏印蔗。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一丑勤、第九天 我趴在偏房一處隱蔽的房頂上張望华嘹。 院中可真熱鬧,春花似錦法竞、人聲如沸耙厚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽薛躬。三九已至,卻和暖如春呆细,著一層夾襖步出監(jiān)牢的瞬間型宝,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工絮爷, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留趴酣,地道東北人。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓坑夯,卻偏偏與公主長得像岖寞,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子柜蜈,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容