從0到1使用kubebuiler開發operator

李大鵝發表於2022-05-19

介紹

假設一個Nginx的QPS(伺服器一秒內處理的請求數)上限為500,如果外部訪問的QPS達到了600,為了保證服務質量,必須擴容一個Nginx來分攤請求。

在Kubernetes環境中,如果外部請求超過了單個Pod的處理極限,我們則可以增加Pod數量來達到橫向擴容的目的。

假設我們的服務是無狀態服務,我們來利用kubebuilder來開發一個operator,來模擬我們已上所述的場景。

專案初始化

在開發 Operator 之前我們需要先提前想好我們的 CRD 資源物件,比如我們想要通過下面的 CR 資源來建立我們的Operator :

apiVersion: elasticweb.example.com/v1
kind: ElasticWeb
metadata:
  name: elasticweb-sample
  namespace: dev
spec:
  image: nginx:1.17.1  # 映象
  port: 30003          # 外部訪問的埠
  singlePodsQPS: 800   # 單個 Pod 的 QPS
  totalQPS: 2400       # 總 QPS

首先初始化專案,這裡使用kubebuilder來構建我們的腳手架:

$ mkdir app-operator && cd app-operator
$ go mod init app-operator
$ kubebuilder init --domain example.com
kubebuilder init --domain example.com
Writing kustomize manifests for you to edit...
Writing scaffold for you to edit...
...

腳手架建立完成後,然後定義資源API:

$ kubebuilder create api --group elasticweb --version v1 --kind El
asticWeb
Create Resource [y/n]
y
Create Controller [y/n]
y
Writing kustomize manifests for you to edit...
Writing scaffold for you to edit...
...

這樣我們的專案初始化就完成了,整體的程式碼結構如下:

$ tree -L 2
.
├── Dockerfile
├── Makefile
├── PROJECT
├── api
│   └── v1
├── bin
│   └── controller-gen
├── config
│   ├── crd
│   ├── default
│   ├── manager
│   ├── prometheus
│   ├── rbac
│   └── samples
├── controllers
│   ├── elasticweb_controller.go
│   └── suite_test.go
├── go.mod
├── go.sum
├── hack
│   └── boilerplate.go.txt
└── main.go

12 directories, 10 files

然後根據我們上面設計的 ElasticWeb 這個物件來編輯 Operator 的結構體即可,修改檔案 api/v1/elasticweb_types.go 中的 ElasticWebSpec 結構體以及ElasticWebStatus結構體,ElasticWebStatus結構體主要用來記錄當前叢集實際支援的總QPS:

// api/v1/elasticweb_types.go

type ElasticWebSpec struct {
	Image string `json:"image"`
	Port  *int32 `json:"port"`
	// 單個pod的QPS上限
	SinglePodsQPS *int32 `json:"singlePodsQPS"`
	// 當前整個業務的QPS
	TotalQPS *int32 `json:"totalQPS,omitempty"`
}

type ElasticWebStatus struct {
    // 當前 Kubernetes 叢集實際支援的總QPS
    RealQPS *int32 `json:"realQPS"`
}

同樣,為了列印的日誌方便我們閱讀,我們給ElasticWeb新增一個String方法:

// api/v1/elasticweb_types.go

func (e *ElasticWeb) String() string {
	var realQPS string
	if nil == e.Status.RealQPS {
		realQPS = ""
	} else {
		realQPS = strconv.Itoa(int(*e.Status.RealQPS))
	}

	return fmt.Sprintf("Image [%s], Port [%d], SinglePodQPS [%d], TotalQPS [%d], RealQPS [%s]",
		e.Spec.Image,
		*e.Spec.Port,
		*e.Spec.SinglePodsQPS,
		*e.Spec.TotalQPS,
		realQPS)
}

要注意每次修改完成需要執行make命令重新生成程式碼:

$ make
 make
/Users/Christian/Documents/code/negan/app-operator/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
go fmt ./...
api/v1/elasticweb_types.go
go vet ./...
go build -o bin/manager main.go

接下來我們就可以去控制器的 Reconcile 函式中來實現我們自己的業務邏輯了。

業務邏輯

首先在目錄 controllers 下面建立一個 resource.go檔案,用來根據我們的ElasticWeb物件生成對應的deploymentservice以及更新狀態。

// controllers/resource.go

package controllers

import (
	v1 "app-operator/api/v1"
	"context"
	"fmt"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/pointer"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
	"sigs.k8s.io/controller-runtime/pkg/log"
)

var (
	ElasticWebCommonLabelKey = "app"
)

const (
	// APP_NAME deployment 中 App 標籤名
	APP_NAME = "elastic-app"
	// CONTAINER_PORT 容器的埠號
	CONTAINER_PORT = 8080
	// CPU_REQUEST 單個POD的CPU資源申請
	CPU_REQUEST = "100m"
	// CPU_LIMIT 單個POD的CPU資源上限
	CPU_LIMIT = "100m"
	// MEM_REQUEST 單個POD的記憶體資源申請
	MEM_REQUEST = "512Mi"
	// MEM_LIMIT 單個POD的記憶體資源上限
	MEM_LIMIT = "512Mi"
)

// 根據總QPS以及單個POD的QPS,計算需要多少個Pod
func getExpectReplicas(elasticWeb *v1.ElasticWeb) int32 {
	// 單個pod的QPS
	singlePodQPS := *elasticWeb.Spec.SinglePodsQPS
	// 期望的總QPS
	totalQPS := *elasticWeb.Spec.TotalQPS
	// 需要建立的副本數
	replicas := totalQPS / singlePodQPS

	if totalQPS%singlePodQPS != 0 {
		replicas += 1
	}
	return replicas
}

// CreateServiceIfNotExists  建立service
func CreateServiceIfNotExists(ctx context.Context, r *ElasticWebReconciler, elasticWeb *v1.ElasticWeb, req ctrl.Request) error {
	logger := log.FromContext(ctx)
	logger.WithValues("func", "createService")
	svc := &corev1.Service{}

	svc.Name = elasticWeb.Name
	svc.Namespace = elasticWeb.Namespace

	svc.Spec = corev1.ServiceSpec{
		Ports: []corev1.ServicePort{
			{
				Name:     "http",
				Port:     CONTAINER_PORT,
				NodePort: *elasticWeb.Spec.Port,
			},
		},
		Type: corev1.ServiceTypeNodePort,
		Selector: map[string]string{
			ElasticWebCommonLabelKey: APP_NAME,
		},
	}

	// 設定關聯關係
	logger.Info("set reference")
	if err := controllerutil.SetControllerReference(elasticWeb, svc, r.Scheme); err != nil {
		logger.Error(err, "SetControllerReference error")
		return err
	}

	logger.Info("start create service")
	if err := r.Create(ctx, svc); err != nil {
		logger.Error(err, "create service error")
		return err
	}

	return nil
}

// CreateDeployment 建立deployment
func CreateDeployment(ctx context.Context, r *ElasticWebReconciler, elasticWeb *v1.ElasticWeb) error {
	logger := log.FromContext(ctx)
	logger.WithValues("func", "createDeploy")

	// 計算期待pod的數量
	expectReplicas := getExpectReplicas(elasticWeb)
	logger.Info(fmt.Sprintf("expectReplicas [%d]", expectReplicas))

	deploy := &appsv1.Deployment{}

	deploy.Labels = map[string]string{
		ElasticWebCommonLabelKey: APP_NAME,
	}

	deploy.Name = elasticWeb.Name
	deploy.Namespace = elasticWeb.Namespace

	deploy.Spec = appsv1.DeploymentSpec{
		Replicas: pointer.Int32Ptr(expectReplicas),
		Selector: &metav1.LabelSelector{
			MatchLabels: map[string]string{
				ElasticWebCommonLabelKey: APP_NAME,
			},
		},
		Template: corev1.PodTemplateSpec{
			ObjectMeta: metav1.ObjectMeta{
				Labels: map[string]string{
					ElasticWebCommonLabelKey: APP_NAME,
				},
			},
			Spec: corev1.PodSpec{
				Containers: []corev1.Container{
					{
						Name:  APP_NAME,
						Image: elasticWeb.Spec.Image,
						Ports: []corev1.ContainerPort{
							{
								Name:          "http",
								ContainerPort: CONTAINER_PORT,
								Protocol:      corev1.ProtocolSCTP,
							},
						},
						Resources: corev1.ResourceRequirements{
							Limits: corev1.ResourceList{
								corev1.ResourceCPU:    resource.MustParse(CPU_LIMIT),
								corev1.ResourceMemory: resource.MustParse(MEM_LIMIT),
							},
							Requests: corev1.ResourceList{
								corev1.ResourceCPU:    resource.MustParse(CPU_REQUEST),
								corev1.ResourceMemory: resource.MustParse(MEM_REQUEST),
							},
						},
					},
				},
			},
		},
	}

	// 建立關聯,刪除web後會將deploy一起刪除
	logger.Info("set reference")
	if err := controllerutil.SetControllerReference(elasticWeb, deploy, r.Scheme); err != nil {
		logger.Error(err, "SetControllerReference error")
		return err
	}

	// 建立Deployment
	logger.Info("start create deploy")
	if err := r.Create(ctx, deploy); err != nil {
		logger.Error(err, "create deploy error")
		return err
	}

	logger.Info("create deploy success")
	return nil
}

func UpdateStatus(ctx context.Context, r *ElasticWebReconciler, elasticWeb *v1.ElasticWeb) error {
	logger := log.FromContext(ctx)
	logger.WithValues("func", "updateStatus")

	// 單個pod的QPS
	singlePodQPS := *elasticWeb.Spec.SinglePodsQPS

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

	// 當pod建立完成後,當前系統的QPS為: 單個pod的QPS * pod總數
	// 如果沒有初始化,則需要先初始化
	if nil == elasticWeb.Status.RealQPS {
		elasticWeb.Status.RealQPS = new(int32)
	}

	*elasticWeb.Status.RealQPS = singlePodQPS * replicas
	logger.Info(fmt.Sprintf("singlePodQPS [%d],replicas [%d],realQPS[%d]", singlePodQPS, replicas, *elasticWeb.Status.RealQPS))

	if err := r.Update(ctx, elasticWeb); err != nil {
		logger.Error(err, "update instance error")
		return err
	}
	return nil
}

上面的程式碼雖然很多,但邏輯很簡單,就是根據我們的 ElasticWeb 去構造 deployservice資源物件,構造完成後,當我們建立 ElasticWeb 的時候就可以在控制器的 Reconcile 函式中去進行邏輯處理了。

同時,我們需要在Reconcile函式註釋中新增 deployservice的RBAC宣告。

// controllers/elasticweb_controller.go

//+kubebuilder:rbac:groups=elasticweb.example.com,resources=elasticwebs,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=elasticweb.example.com,resources=elasticwebs/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=elasticweb.example.com,resources=elasticwebs/finalizers,verbs=update
//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch;delete

func (r *ElasticWebReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    logger := log.FromContext(ctx)

    instance := &elasticwebv1.ElasticWeb{}

    if err := r.Get(ctx, req.NamespacedName, instance); err != nil {
        return ctrl.Result{}, client.IgnoreNotFound(err)
    }

    logger.Info(fmt.Sprintf("instance:%s", instance.String()))

    // 獲取deployment
    deploy := &appsv1.Deployment{}
    if err := r.Get(ctx, req.NamespacedName, deploy); err != nil {
        if errors.IsNotFound(err) {
            // 如果沒有查詢到,則需要建立
            logger.Info("deploy not exists")
            // 判斷qps的需求,如果qps沒有需求,則啥都不做
            if *instance.Spec.TotalQPS < 1 {
                logger.Info("not need deployment")
                return ctrl.Result{}, nil
            }

            // 建立service
            if err = CreateServiceIfNotExists(ctx, r, instance, req); err != nil {
                return ctrl.Result{}, err
            }

            // 建立Deploy
            if err := CreateDeployment(ctx, r, instance); err != nil {
                return ctrl.Result{}, err
            }

            // 更新狀態
            if err := UpdateStatus(ctx, r, instance); err != nil {
                return ctrl.Result{}, err
            }

            return ctrl.Result{}, nil
        }
        logger.Error(err, "failed to get deploy")
        return ctrl.Result{}, err
    }

    // 根據單個Pod的QPS計算期望pod的副本
    expectReplicas := getExpectReplicas(instance)

    // 獲取當前deployment實際的pod副本
    realReplicas := deploy.Spec.Replicas

    if expectReplicas == *realReplicas {
        logger.Info("not need to reconcile")
        return ctrl.Result{}, nil
    }

    // 重新賦值
    deploy.Spec.Replicas = &expectReplicas
    // 更新 deploy
    if err := r.Update(ctx, deploy); err != nil {
        logger.Error(err, "update deploy replicas error")
        return ctrl.Result{}, err
    }

    // 更新狀態
    if err := UpdateStatus(ctx, r, instance); err != nil {
        logger.Error(err, "update status error")
        return ctrl.Result{}, err
    }

    return ctrl.Result{}, nil
}

除錯

接下來我們首先安裝我們的 CRD 物件,讓我們的 Kubernetes 系統識別我們的 ElasitcWeb 物件:

$ make install
/Users/Christian/Documents/code/negan/app-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
/Users/Christian/Documents/code/negan/app-operator/bin/kustomize build config/crd | kubectl apply -f -
customresourcedefinition.apiextensions.k8s.io/elasticwebs.elasticweb.example.com configured

接著執行控制器:

$ make install
/Users/Christian/Documents/code/negan/app-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
/Users/Christian/Documents/code/negan/app-operator/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
go fmt ./...
controllers/elasticweb_controller.go
go vet ./...
go run ./main.go
1.652941435373431e+09   INFO    controller-runtime.metrics      Metrics server is starting to listen    {"addr": ":8080"}
1.6529414353737469e+09  INFO    setup   starting manager
1.6529414353739378e+09  INFO    Starting server {"path": "/metrics", "kind": "metrics", "addr": "[::]:8080"}
1.652941435373951e+09   INFO    Starting server {"kind": "health probe", "addr": "[::]:8081"}
1.6529414353741682e+09  INFO    controller.elasticweb   Starting EventSource    {"reconciler group": "elasticweb.example.com", "reconciler kind": "ElasticWeb", "source": "kind source: *v1.ElasticWeb"}
1.652941435374196e+09   INFO    controller.elasticweb   Starting EventSource    {"reconciler group": "elasticweb.example.com", "reconciler kind": "ElasticWeb", "source": "kind source: *v1.Deployment"}
1.652941435374202e+09   INFO    controller.elasticweb   Starting Controller     {"reconciler group": "elasticweb.example.com", "reconciler kind": "ElasticWeb"}
1.65294143547575e+09    INFO    controller.elasticweb   Starting workers        {"reconciler group": "elasticweb.example.com", "reconciler kind": "ElasticWeb", "worker count": 1}

控制器啟動成功後我們就可以去建立我們的CR了,將示例 CR 資源清單修改成下面的 YAML:

apiVersion: elasticweb.example.com/v1
kind: ElasticWeb
metadata:
  name: elasticweb-sample
spec:
  image: nginx:1.17.1
  port: 30003
  singlePodsQPS: 800
  totalQPS: 2400

另外開啟一個終端建立上面的資源物件:

$ kubectl apply -f config/samples/elasticweb_v1_elasticweb.yaml
elasticweb.elasticweb.example.com/elasticweb-sample created

建立完成後我們可以檢視對應的 ElasticWeb物件:

$ kubectl get ElasticWeb
NAME                AGE
elasticweb-sample   40s

對應也會自動建立我們的 Deployment 和 Service 資源清單:

$ kubectl get all  
NAME                                     READY   STATUS    RESTARTS   AGE
pod/elasticweb-sample-6879bdfcf4-42jtc   1/1     Running   0          2m40s
pod/elasticweb-sample-6879bdfcf4-sdmbp   1/1     Running   0          2m40s
pod/elasticweb-sample-6879bdfcf4-w87tj   1/1     Running   0          2m40s

NAME                        TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE
service/elasticweb-sample   NodePort    10.100.200.7   <none>        8080:30003/TCP   2m40s
service/kubernetes          ClusterIP   10.96.0.1      <none>        443/TCP          14d

NAME                                READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/elasticweb-sample   3/3     3            3           2m40s

NAME                                           DESIRED   CURRENT   READY   AGE
replicaset.apps/elasticweb-sample-6879bdfcf4   3         3         3       2m40s

優化

現在我們需要對Deploy進行Watch,Service是的建立包含在建立Deploy的邏輯裡,所以Deploy出現變化,我們需要重新進行調諧。當然我們只需要Watch被ElasticWeb控制的這部分獨享即可。在elasticweb_controller.go檔案中更新SetupWithManager函式即可:

func (r *ElasticWebReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&elasticwebv1.ElasticWeb{}).
		Owns(&appsv1.Deployment{}).
		Complete(r)
}

而且我們發現在終端列印的日誌中,worker count 為1,這時候我們同樣可以更新SetupWithManager函式:

func (r *ElasticWebReconciler) SetupWithManager(mgr ctrl.Manager) error {
    return ctrl.NewControllerManagedBy(mgr).
        WithOptions(controller.Options{MaxConcurrentReconciles: 5}).
        For(&elasticwebv1.ElasticWeb{}).
        Owns(&appsv1.Deployment{}).
        Complete(r)
}

同樣我們發現輸出的日誌是時間戳格式,不夠直觀。 在 main 函式中有個zap的Options,我們可以在這裡面進行設定:

opts := zap.Options{
		Development: true,
		TimeEncoder: zapcore.ISO8601TimeEncoder,
	}

自定義輸出列

我們這裡的 Elastic 例項,我們可以使用 kubectl 命令列出這個物件:

$ kubectl get ElasticWeb
NAME                AGE
elasticweb-sample   40s

但是這個資訊太過於簡單,如果我們想要檢視這個物件使用了什麼映象,部署了多少個副本,我們可能還需要通過 kubectl describe 命令去檢視,這樣就太過於麻煩了。這個時候我們就可以在 CRD 定義的結構體型別中使用 +kubebuilder:printcolumn 這個註釋來告訴 kubebuilder 將我們所需的資訊新增到 CRD 中,比如我們想要列印使用的映象,在 +kubebuilder:object:root=true 註釋下面新增一列新的註釋,如下所示:

/+kubebuilder:object:root=true
// +kubebuilder:printcolumn:name="Image",type="string",JSONPath=".spec.image",description="The Docker Image of MyAPP"
//+kubebuilder:subresource:status

// 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"`
}

printcolumn 註釋有幾個不同的選項,在這裡我們只使用了其中一部分:

  • name:這是我們新增的列的標題,由 kubectl 列印在標題中
  • type:要列印的值的資料型別,有效型別為 integer、number、string、boolean 和 date
  • JSONPath:這是要列印資料的路徑,在我們的例子中,映象 image 屬於 spec 下面的屬性,所以我們使用 .spec.image。需要注意的是 JSONPath 屬性引用的是生成的 JSON CRD,而不是引用本地 Go 類。
  • description:描述列的可讀字串,目前暫未發現該屬性的作用...

新增了註釋後,我們需要執行 make install 命令重新生成 CRD 並安裝,然後我們再次嘗試列出 CRD。

$ kubectl get ElasticWeb                                       
NAME                IMAGE
elasticweb-sample   nginx:1.17.1

可以看到現在列出來的資料有一列 IMAGE 的資料了,不過卻沒有了之前列出來的 AGE 這一列了。這是因為當我們新增自定義列的時候,就不會再顯示其他預設的列了(NAME 除外),所以如果我們還想出現 AGE 這一列,我們還需要在 EtcdCluster 的結構體上面新增對應的註釋資訊,如下所示:

// +kubebuilder:object:root=true
// +kubebuilder:printcolumn:name="Image",type="string",JSONPath=".spec.image",description="The Docker Image of Etcd"
// +kubebuilder:printcolumn:name="Port",type="integer",JSONPath=".spec.port",description="container port"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
// +kubebuilder:subresource:status

// 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"`
}

執行 make install 命令列,再次檢視 CRD 資料:

$ kubectl get ElasticWeb
NAME                IMAGE          PORT    AGE
elasticweb-sample   nginx:1.17.1   30003   37m

如果我們還想獲取當前應用的狀態,同樣也可以通過 +kubebuilder:printcolumn 來新增對應的資訊,只是狀態的資料是通過 .status 在 JSONPath 屬性中去獲取了。

如果你覺得這裡新增了太多的資訊,如果我們想隱藏某個欄位並只在需要時顯示該欄位怎麼辦?

這個時候就需要使用 priority 這個屬性了,如果沒有配置這個屬性,預設值為0,也就是預設情況下列出顯示的資料是 priority=0 的列,如果將 priority 設定為大於1的數字,那麼則只會當我們使用 -o wide 引數的時候才會顯示,比如我們給 Port 這一列新增一個 priority=1 的屬性:

// +kubebuilder:printcolumn:name="Port",type="string",priority=1,JSONPath=".spec.image",description="The Docker Image of Etcd"

同樣重新執行make install命令後,再次檢視CRD:

$ kubectl get ElasticWeb
NAME                IMAGE          AGE
elasticweb-sample   nginx:1.17.1   41m

$ kubectl get ElasticWeb -o wide
NAME                IMAGE          PORT    AGE
elasticweb-sample   nginx:1.17.1   30003   41m

瞭解更多詳細資訊請檢視 CRD 文件上的 AdditionalPrinterColumns 欄位

部署

現在我們已經完成了開發工作,並在本地完成了測試工作,這時候我們就需要把我們的operator部署到kubernetes環境中。

首先我們需要修改Dockerfile檔案,需要新增上go mod的代理配置:

# Build the manager binary
FROM golang:1.17 as builder

WORKDIR /workspace
# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum
# cache deps before building and copying source so that we don't need to re-download as much
# and so that source changes don't invalidate our downloaded layer
ENV GOPROXY https://goproxy.cn

RUN go mod download

# Copy the go source
COPY main.go main.go
COPY api/ api/
COPY controllers/ controllers/

# Build
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o manager main.go

# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
FROM gcr.io/distroless/static:nonroot
WORKDIR /
COPY --from=builder /workspace/manager .
USER 65532:65532

ENTRYPOINT ["/manager"]

接下來就是登陸docker了,我這邊使用的docker hub,直接在命令列登陸即可。

$ docker login
Authenticating with existing credentials...
Login Succeeded

Logging in with your password grants your terminal complete access to your account. 
For better security, log in with a limited-privilege personal access token. Learn more at https://docs.docker.com/go/access-tokens/

登陸成功後,就可以構建映象了。

注意如果你用的是Mac M1的電腦,那麼需要對Makefile做一小點修改,具體可見issues

.PHONY: test
test: manifests generate fmt vet envtest ## Run tests.
	#KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test ./... -coverprofile cover.out
	KUBEBUILDER_ASSETS="$(shell $(ENVTEST) --arch=amd64 use $(ENVTEST_K8S_VERSION) -p path)" go test ./... -coverprofile cover.out

接下來就是構建並將映象推送到映象倉庫:

$ make docker-build docker-push IMG=<some-registry>/<project-name>:tag

$ make docker-build docker-push IMG=huiyichanmian/elasitcweb:v0.0.1

等待推送成功後,就可以根據IMG指定的映象將控制器部署到叢集中:

$ make deploy IMG=<some-registry>/<project-name>:tag

$ make deploy IMG=huiyichanmian/elasticweb:v0.0.1

同樣,這裡可能會遇到映象gcr.io/kubebuilder/kube-rbac-proxy:v0.8.0這個映象拉不下來的情況,這裡可以使用kubesphere/kube-rbac-proxy:v0.8.0進行替代。

可以直接修改config/default/manager_auth_proxy_patch.yaml或者使用docker tag進行改名。

部署完成後,系統會自動建立專案名- system的名稱空間,我們的控制器所有東西都在這個namespace下。

最後如果要從叢集中解除安裝operator也很簡單:

$ make undeploy

參考:
https://xinchen.blog.csdn.net/article/details/113836090

相關文章