Kubernetes Operator 知多少(開發(fā)篇)

在 Kubernetes 的監(jiān)控方案中我們經(jīng)常會使用到一個Promethues Operator的項目撬即,該項目可以更加方便的去使用 Prometheus,而不需要直接去使用最原始的一些資源對象问词,比如 Pod拴念、Deployment柜蜈,隨著 Prometheus Operator 項目的成功磨澡,CoreOS 公司開源了一個比較厲害的工具:Operator Framework,該工具可以讓開發(fā)人員更加容易的開發(fā) Operator 應(yīng)用笔宿。

在本篇文章中介紹一個簡單示例來演示如何使用 Operator Framework 框架來開發(fā)一個 Operator 應(yīng)用犁钟。

Kubernetes Operator

Operator 是由 CoreOS 開發(fā)的棱诱,用來擴展 Kubernetes API,特定的應(yīng)用程序控制器涝动,它用來創(chuàng)建迈勋、配置和管理復(fù)雜的有狀態(tài)應(yīng)用,如數(shù)據(jù)庫醋粟、緩存和監(jiān)控系統(tǒng)靡菇。Operator 基于 Kubernetes 的資源和控制器概念之上構(gòu)建,但同時又包含了應(yīng)用程序特定的領(lǐng)域知識米愿。創(chuàng)建Operator 的關(guān)鍵是CRD(自定義資源)的設(shè)計厦凤。

Kubernetes 1.7 版本以來就引入了自定義控制器的概念,該功能可以讓開發(fā)人員擴展添加新功能育苟,更新現(xiàn)有的功能较鼓,并且可以自動執(zhí)行一些管理任務(wù),這些自定義的控制器就像 Kubernetes 原生的組件一樣违柏,Operator 直接使用 Kubernetes API進行開發(fā)博烂,也就是說他們可以根據(jù)這些控制器內(nèi)部編寫的自定義規(guī)則來監(jiān)控集群、更改 Pods/Services漱竖、對正在運行的應(yīng)用進行擴縮容脖母。

Operator Framework

Operator Framework 同樣也是 CoreOS 開源的一個用于快速開發(fā) Operator 的工具包,該框架包含兩個主要的部分:

Operator SDK: 無需了解復(fù)雜的 Kubernetes API 特性闲孤,即可讓你根據(jù)你自己的專業(yè)知識構(gòu)建一個 Operator 應(yīng)用谆级。
Operator Lifecycle Manager OLM: 幫助你安裝、更新和管理跨集群的運行中的所有 Operator(以及他們的相關(guān)服務(wù))


image

Workflow

Operator SDK 提供以下工作流來開發(fā)一個新的 Operator:

    1. 使用 SDK 創(chuàng)建一個新的 Operator 項目
    1. 通過添加自定義資源(CRD)定義新的資源 API
    1. 指定使用 SDK API 來 watch 的資源
    1. 定義 Operator 的協(xié)調(diào)(reconcile)邏輯
    1. 使用 Operator SDK 構(gòu)建并生成 Operator 部署清單文件

Demo實戰(zhàn)

我們平時在部署一個簡單的 Webserver 到 Kubernetes 集群中的時候讼积,都需要先編寫一個 Deployment 的控制器肥照,然后創(chuàng)建一個 Service 對象,通過 Pod 的 label 標簽進行關(guān)聯(lián)勤众,最后通過 Ingress 或者 type=NodePort 類型的 Service 來暴露服務(wù)舆绎,每次都需要這樣操作,是不是略顯麻煩们颜,我們就可以創(chuàng)建一個自定義的資源對象吕朵,通過我們的 CRD 來描述我們要部署的應(yīng)用信息,比如鏡像窥突、服務(wù)端口努溃、環(huán)境變量等等,然后創(chuàng)建我們的自定義類型的資源對象的時候阻问,通過控制器去創(chuàng)建對應(yīng)的 Deployment 和 Service梧税,是不是就方便很多了,相當(dāng)于我們用一個資源清單去描述了 Deployment 和 Service 要做的兩件事情。

這里我們將創(chuàng)建一個名為 AppService 的 CRD 資源對象第队,然后定義如下的資源清單進行應(yīng)用部署:

apiVersion: app.example.com/v1
kind: AppService
metadata:
  name: nginx-app
spec:
  size: 2
  image: nginx:1.7.9
  ports:
    - port: 80
      targetPort: 80
      nodePort: 30002

通過這里的自定義的 AppService 資源對象去創(chuàng)建副本數(shù)為2的 Pod哮塞,然后通過 nodePort=30002 的端口去暴露服務(wù),接下來我們就來一步一步的實現(xiàn)我們這里的這個簡單的 Operator 應(yīng)用凳谦。

開發(fā)環(huán)境

環(huán)境需求

要開發(fā) Operator 自然 Kubernetes 集群是少不了的忆畅,還需要 Golang 的環(huán)境,這里的安裝就不多說了尸执,然后還需要一個 Go 語言的依賴管理工具包:dep邻眷,由于 Operator SDK 是使用的 dep 該工具包,所以需要我們提前安裝好剔交,可以查看資料:https://github.com/golang/dep肆饶,另外一個需要說明的是,由于 dep 去安裝的時候需要去谷歌的網(wǎng)站拉取很多代碼岖常,所以正常情況下的話是會失敗的驯镊,需要做什么工作大家應(yīng)該清楚吧?要科學(xué)竭鞍。

安裝 operator-sdk

operator sdk 安裝方法非常多板惑,我們可以直接在 github 上面下載需要使用的版本,然后放置到 PATH 環(huán)境下面即可偎快,當(dāng)然也可以將源碼 clone 到本地手動編譯安裝即可冯乘,如果你是 Mac,當(dāng)然還可以使用常用的 brew 工具進行安裝

$ brew install operator-sdk
......
$ operator-sdk version
operator-sdk version: v0.7.0
$ go version
go version go1.11.4 darwin/amd64

我們這里使用的 sdk 版本是v0.7.0晒夹,其他安裝方法可以參考文檔:https://github.com/operator-framework/operator-sdk/blob/master/doc/user/install-operator-sdk.md

實戰(zhàn)演示

創(chuàng)建新項目

環(huán)境準備好了裆馒,接下來就可以使用 operator-sdk 直接創(chuàng)建一個新的項目了,命令格式為: operator-sdk new

按照上面我們預(yù)先定義的 CRD 資源清單丐怯,我們這里可以這樣創(chuàng)建:

# 創(chuàng)建項目目錄
$ mkdir -p operator-learning  
# 設(shè)置項目目錄為 GOPATH 路徑
$ cd operator-learning && export GOPATH=$PWD  
$ mkdir -p $GOPATH/src/github.com/cnych 
$ cd $GOPATH/src/github.com/cnych
# 使用 sdk 創(chuàng)建一個名為 opdemo 的 operator 項目
$ operator-sdk new opdemo
......
# 該過程需要國外網(wǎng)絡(luò)喷好,需要花費很長時間,請耐心等待
......
$ cd opdemo && tree -L 2
.
├── Gopkg.lock
├── Gopkg.toml
├── build
│   ├── Dockerfile
│   ├── _output
│   └── bin
├── cmd
│   └── manager
├── deploy
│   ├── crds
│   ├── operator.yaml
│   ├── role.yaml
│   ├── role_binding.yaml
│   └── service_account.yaml
├── pkg
│   ├── apis
│   └── controller
├── vendor
│   ├── cloud.google.com
│   ├── contrib.go.opencensus.io
│   ├── github.com
│   ├── go.opencensus.io
│   ├── go.uber.org
│   ├── golang.org
│   ├── google.golang.org
│   ├── gopkg.in
│   ├── k8s.io
│   └── sigs.k8s.io
└── version
    └── version.go

23 directories, 8 files

到這里一個全新的 Operator 項目就新建完成了读跷。

項目結(jié)構(gòu)

使用operator-sdk new命令創(chuàng)建新的 Operator 項目后梗搅,項目目錄就包含了很多生成的文件夾和文件。

  • Gopkg.toml Gopkg.lock?—?Go Dep 清單效览,用來描述當(dāng)前 Operator 的依賴包无切。
  • cmd - 包含 main.go 文件,使用 operator-sdk API 初始化和啟動當(dāng)前 Operator 的入口丐枉。
  • deploy - 包含一組用于在 Kubernetes 集群上進行部署的通用的 Kubernetes 資源清單文件哆键。
  • pkg/apis - 包含定義的 API 和自定義資源(CRD)的目錄樹,這些文件允許 sdk 為 CRD 生成代碼并注冊對應(yīng)的類型矛洞,以便正確解碼自定義資源對象洼哎。
  • pkg/controller - 用于編寫所有的操作業(yè)務(wù)邏輯的地方
  • vendor - golang vendor 文件夾,其中包含滿足當(dāng)前項目的所有外部依賴包沼本,通過 go dep 管理該目錄噩峦。

我們主要需要編寫的是pkg目錄下面的 api 定義以及對應(yīng)的 controller 實現(xiàn)。

添加 API

接下來為我們的自定義資源添加一個新的 API抽兆,按照上面我們預(yù)定義的資源清單文件识补,在 Operator 相關(guān)根目錄下面執(zhí)行如下命令:

$ operator-sdk add api --api-version=app.example.com/v1 --kind=AppService

添加完成后,我們可以看到類似于下面的這樣項目結(jié)構(gòu):


image

添加控制器

上面我們添加自定義的 API辫红,接下來可以添加對應(yīng)的自定義 API 的具體實現(xiàn) Controller凭涂,同樣在項目根目錄下面執(zhí)行如下命令:

$ operator-sdk add controller --api-version=app.example.com/v1 --kind=AppService

這樣整個 Operator 項目的腳手架就已經(jīng)搭建完成了,接下來就是具體的實現(xiàn)了贴妻。

自定義 API

打開源文件pkg/apis/app/v1/appservice_types.go切油,需要我們根據(jù)我們的需求去自定義結(jié)構(gòu)體 AppServiceSpec,我們最上面預(yù)定義的資源清單中就有 size名惩、image澎胡、ports 這些屬性,所有我們需要用到的屬性都需要在這個結(jié)構(gòu)體中進行定義:

type AppServiceSpec struct {
    // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
    // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file
    // Add custom validation using kubebuilder tags: https://book.kubebuilder.io/beyond_basics/generating_crd.html
    Size      *int32                      `json:"size"`
    Image     string                      `json:"image"`
    Resources corev1.ResourceRequirements `json:"resources,omitempty"`
    Envs      []corev1.EnvVar             `json:"envs,omitempty"`
    Ports     []corev1.ServicePort        `json:"ports,omitempty"`
}

代碼中會涉及到一些包名的導(dǎo)入娩鹉,由于包名較多攻谁,所以我們會使用一些別名進行區(qū)分,主要的包含下面幾個:

import (
    appsv1 "k8s.io/api/apps/v1"
    corev1 "k8s.io/api/core/v1"
    appv1 "github.com/cnych/opdemo/pkg/apis/app/v1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

這里的 resources弯予、envs戚宦、ports 的定義都是直接引用的"k8s.io/api/core/v1"中定義的結(jié)構(gòu)體,而且需要注意的是我們這里使用的是ServicePort锈嫩,而不是像傳統(tǒng)的 Pod 中定義的 ContanerPort受楼,這是因為我們的資源清單中不僅要描述容器的 Port,還要描述 Service 的 Port呼寸。

然后一個比較重要的結(jié)構(gòu)體AppServiceStatus用來描述資源的狀態(tài)那槽,當(dāng)然我們可以根據(jù)需要去自定義狀態(tài)的描述,我這里就偷懶直接使用 Deployment 的狀態(tài)了:

type AppServiceStatus struct {
    // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
    // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file
    // Add custom validation using kubebuilder tags: https://book.kubebuilder.io/beyond_basics/generating_crd.html
    appsv1.DeploymentStatus `json:",inline"`
}

定義完成后等舔,在項目根目錄下面執(zhí)行如下命令:

$ operator-sdk generate k8s

改命令是用來根據(jù)我們自定義的 API 描述來自動生成一些代碼骚灸,目錄pkg/apis/app/v1/下面以zz_generated開頭的文件就是自動生成的代碼,里面的內(nèi)容并不需要我們?nèi)ナ謩泳帉憽?/p>

這樣我們就算完成了對自定義資源對象的 API 的聲明慌植。

實現(xiàn)業(yè)務(wù)邏輯

上面 API 描述聲明完成了甚牲,接下來就需要我們來進行具體的業(yè)務(wù)邏輯實現(xiàn)了,編寫具體的 controller 實現(xiàn)蝶柿,打開源文件pkg/controller/appservice/appservice_controller.go丈钙,需要我們?nèi)ジ牡牡胤揭膊皇呛芏啵诵牡木褪荝econcile方法交汤,該方法就是去不斷的 watch 資源的狀態(tài)雏赦,然后根據(jù)狀態(tài)的不同去實現(xiàn)各種操作邏輯劫笙,核心代碼如下:

func (r *ReconcileAppService) Reconcile(request reconcile.Request) (reconcile.Result, error) {
    reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
    reqLogger.Info("Reconciling AppService")

    // Fetch the AppService instance
    instance := &appv1.AppService{}
    err := r.client.Get(context.TODO(), request.NamespacedName, instance)
    if err != nil {
        if errors.IsNotFound(err) {
            // Request object not found, could have been deleted after reconcile request.
            // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers.
            // Return and don't requeue
            return reconcile.Result{}, nil
        }
        // Error reading the object - requeue the request.
        return reconcile.Result{}, err
    }

    if instance.DeletionTimestamp != nil {
        return reconcile.Result{}, err
    }

    // 如果不存在,則創(chuàng)建關(guān)聯(lián)資源
    // 如果存在星岗,判斷是否需要更新
    //   如果需要更新填大,則直接更新
    //   如果不需要更新,則正常返回

    deploy := &appsv1.Deployment{}
    if err := r.client.Get(context.TODO(), request.NamespacedName, deploy); err != nil && errors.IsNotFound(err) {
        // 創(chuàng)建關(guān)聯(lián)資源
        // 1. 創(chuàng)建 Deploy
        deploy := resources.NewDeploy(instance)
        if err := r.client.Create(context.TODO(), deploy); err != nil {
            return reconcile.Result{}, err
        }
        // 2. 創(chuàng)建 Service
        service := resources.NewService(instance)
        if err := r.client.Create(context.TODO(), service); err != nil {
            return reconcile.Result{}, err
        }
        // 3. 關(guān)聯(lián) Annotations
        data, _ := json.Marshal(instance.Spec)
        if instance.Annotations != nil {
            instance.Annotations["spec"] = string(data)
        } else {
            instance.Annotations = map[string]string{"spec": string(data)}
        }

        if err := r.client.Update(context.TODO(), instance); err != nil {
            return reconcile.Result{}, nil
        }
        return reconcile.Result{}, nil
    }

    oldspec := appv1.AppServiceSpec{}
    if err := json.Unmarshal([]byte(instance.Annotations["spec"]), oldspec); err != nil {
        return reconcile.Result{}, err
    }

    if !reflect.DeepEqual(instance.Spec, oldspec) {
        // 更新關(guān)聯(lián)資源
        newDeploy := resources.NewDeploy(instance)
        oldDeploy := &appsv1.Deployment{}
        if err := r.client.Get(context.TODO(), request.NamespacedName, oldDeploy); err != nil {
            return reconcile.Result{}, err
        }
        oldDeploy.Spec = newDeploy.Spec
        if err := r.client.Update(context.TODO(), oldDeploy); err != nil {
            return reconcile.Result{}, err
        }

        newService := resources.NewService(instance)
        oldService := &corev1.Service{}
        if err := r.client.Get(context.TODO(), request.NamespacedName, oldService); err != nil {
            return reconcile.Result{}, err
        }
        oldService.Spec = newService.Spec
        if err := r.client.Update(context.TODO(), oldService); err != nil {
            return reconcile.Result{}, err
        }

        return reconcile.Result{}, nil
    }

    return reconcile.Result{}, nil

}

上面就是業(yè)務(wù)邏輯實現(xiàn)的核心代碼俏橘,邏輯很簡單允华,就是去判斷資源是否存在,不存在寥掐,則直接創(chuàng)建新的資源靴寂,創(chuàng)建新的資源除了需要創(chuàng)建 Deployment 資源外,還需要創(chuàng)建 Service 資源對象召耘,因為這就是我們的需求百炬,當(dāng)然你還可以自己去擴展,比如在創(chuàng)建一個 Ingress 對象污它。更新也是一樣的收壕,去對比新舊對象的聲明是否一致,不一致則需要更新轨蛤,同樣的蜜宪,兩種資源都需要更新的。

另外兩個核心的方法就是上面的resources.NewDeploy(instance)和resources.NewService(instance)方法祥山,這兩個方法實現(xiàn)邏輯也很簡單圃验,就是根據(jù) CRD 中的聲明去填充 Deployment 和 Service 資源對象的 Spec 對象即可。

NewDeploy 方法實現(xiàn)如下

func NewDeploy(app *appv1.AppService) *appsv1.Deployment {
    labels := map[string]string{"app": app.Name}
    selector := &metav1.LabelSelector{MatchLabels: labels}
    return &appsv1.Deployment{
        TypeMeta: metav1.TypeMeta{
            APIVersion: "apps/v1",
            Kind:       "Deployment",
        },
        ObjectMeta: metav1.ObjectMeta{
            Name:      app.Name,
            Namespace: app.Namespace,

            OwnerReferences: []metav1.OwnerReference{
                *metav1.NewControllerRef(app, schema.GroupVersionKind{
                    Group: v1.SchemeGroupVersion.Group,
                    Version: v1.SchemeGroupVersion.Version,
                    Kind: "AppService",
                }),
            },
        },
        Spec: appsv1.DeploymentSpec{
            Replicas: app.Spec.Size,
            Template: corev1.PodTemplateSpec{
                ObjectMeta: metav1.ObjectMeta{
                    Labels: labels,
                },
                Spec: corev1.PodSpec{
                    Containers: newContainers(app),
                },
            },
            Selector: selector,
        },
    }
}

func newContainers(app *v1.AppService) []corev1.Container {
    containerPorts := []corev1.ContainerPort{}
    for _, svcPort := range app.Spec.Ports {
        cport := corev1.ContainerPort{}
        cport.ContainerPort = svcPort.TargetPort.IntVal
        containerPorts = append(containerPorts, cport)
    }
    return []corev1.Container{
        {
            Name: app.Name,
            Image: app.Spec.Image,
            Resources: app.Spec.Resources,
            Ports: containerPorts,
            ImagePullPolicy: corev1.PullIfNotPresent,
            Env: app.Spec.Envs,
        },
    }
}

newService 對應(yīng)的方法實現(xiàn)如下:

func NewService(app *v1.AppService) *corev1.Service {
    return &corev1.Service {
        TypeMeta: metav1.TypeMeta {
            Kind: "Service",
            APIVersion: "v1",
        },
        ObjectMeta: metav1.ObjectMeta{
            Name: app.Name,
            Namespace: app.Namespace,
            OwnerReferences: []metav1.OwnerReference{
                *metav1.NewControllerRef(app, schema.GroupVersionKind{
                    Group: v1.SchemeGroupVersion.Group,
                    Version: v1.SchemeGroupVersion.Version,
                    Kind: "AppService",
                }),
            },
        },
        Spec: corev1.ServiceSpec{
            Type: corev1.ServiceTypeNodePort,
            Ports: app.Spec.Ports,
            Selector: map[string]string{
                "app": app.Name,
            },
        },
    }
}

這樣我們就實現(xiàn)了 AppService 這種資源對象的業(yè)務(wù)邏輯缝呕。

調(diào)試

如果我們本地有一個可以訪問的 Kubernetes 集群澳窑,我們也可以直接進行調(diào)試,在本地用戶~/.kube/config文件中配置集群訪問信息供常,下面的信息表明可以訪問 Kubernetes 集群:

$ kubectl cluster-info
Kubernetes master is running at https://ydzs-master:6443
KubeDNS is running at https://ydzs-master:6443/api/v1/namespaces/kube-system/services/kube-dns/proxy

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.

首先摊聋,在集群中安裝 CRD 對象:

$ kubectl create -f deploy/crds/app_v1_appservice_crd.yaml
customresourcedefinition "appservices.app.example.com" created
$ kubectl get crd
NAME                                   AGE
appservices.app.example.com            <invalid>
......

當(dāng)我們通過kubectl get crd命令獲取到我們定義的 CRD 資源對象,就證明我們定義的 CRD 安裝成功了栈暇。其實現(xiàn)在只是 CRD 的這個聲明安裝成功了麻裁,但是我們這個 CRD 的具體業(yè)務(wù)邏輯實現(xiàn)方式還在我們本地,并沒有部署到集群之中源祈,我們可以通過下面的命令來在本地項目中啟動 Operator 的調(diào)試:

$ operator-sdk up local                                                     
INFO[0000] Running the operator locally.                
INFO[0000] Using namespace default.                     
{"level":"info","ts":1559207203.964137,"logger":"cmd","msg":"Go Version: go1.11.4"}
{"level":"info","ts":1559207203.964192,"logger":"cmd","msg":"Go OS/Arch: darwin/amd64"}
{"level":"info","ts":1559207203.9641972,"logger":"cmd","msg":"Version of operator-sdk: v0.7.0"}
{"level":"info","ts":1559207203.965905,"logger":"leader","msg":"Trying to become the leader."}
{"level":"info","ts":1559207203.965945,"logger":"leader","msg":"Skipping leader election; not running in a cluster."}
{"level":"info","ts":1559207206.928867,"logger":"cmd","msg":"Registering Components."}
{"level":"info","ts":1559207206.929077,"logger":"kubebuilder.controller","msg":"Starting EventSource","controller":"appservice-controller","source":"kind source: /, Kind="}
{"level":"info","ts":1559207206.9292521,"logger":"kubebuilder.controller","msg":"Starting EventSource","controller":"appservice-controller","source":"kind source: /, Kind="}
{"level":"info","ts":1559207209.622659,"logger":"cmd","msg":"failed to initialize service object for metrics: OPERATOR_NAME must be set"}
{"level":"info","ts":1559207209.622693,"logger":"cmd","msg":"Starting the Cmd."}
{"level":"info","ts":1559207209.7236018,"logger":"kubebuilder.controller","msg":"Starting Controller","controller":"appservice-controller"}
{"level":"info","ts":1559207209.8284118,"logger":"kubebuilder.controller","msg":"Starting workers","controller":"appservice-controller","worker count":1}

上面的命令會在本地運行 Operator 應(yīng)用煎源,通過~/.kube/config去關(guān)聯(lián)集群信息,現(xiàn)在我們?nèi)ヌ砑右粋€ AppService 類型的資源然后觀察本地 Operator 的變化情況香缺,資源清單文件就是我們上面預(yù)定義的(deploy/crds/app_v1_appservice_cr.yaml)

apiVersion: app.example.com/v1
kind: AppService
metadata:
  name: nginx-app
spec:
  size: 2
  image: nginx:1.7.9
  ports:
    - port: 80
      targetPort: 80
      nodePort: 30002

直接創(chuàng)建這個資源對象:

$ kubectl create -f deploy/crds/app_v1_appservice_cr.yaml
appservice "nginx-app" created

我們可以看到我們的應(yīng)用創(chuàng)建成功了手销,這個時候查看 Operator 的調(diào)試窗口會有如下的信息出現(xiàn):

......
{"level":"info","ts":1559207416.670523,"logger":"controller_appservice","msg":"Reconciling AppService","Request.Namespace":"default","Request.Name":"nginx-app"}
{"level":"info","ts":1559207417.004226,"logger":"controller_appservice","msg":"Reconciling AppService","Request.Namespace":"default","Request.Name":"nginx-app"}
{"level":"info","ts":1559207417.004331,"logger":"controller_appservice","msg":"Reconciling AppService","Request.Namespace":"default","Request.Name":"nginx-app"}
{"level":"info","ts":1559207418.33779,"logger":"controller_appservice","msg":"Reconciling AppService","Request.Namespace":"default","Request.Name":"nginx-app"}
{"level":"info","ts":1559207418.951193,"logger":"controller_appservice","msg":"Reconciling AppService","Request.Namespace":"default","Request.Name":"nginx-app"}
......

然后我們可以去查看集群中是否有符合我們預(yù)期的資源出現(xiàn):

$ kubectl get AppService
NAME        AGE
nginx-app   <invalid>
$ kubectl get deploy
NAME                     DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
nginx-app                2         2         2            2           <invalid>
$ kubectl get svc
NAME         TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
kubernetes   ClusterIP   10.96.0.1      <none>        443/TCP        76d
nginx-app    NodePort    10.108.227.5   <none>        80:30002/TCP   <invalid>
$ kubectl get pods
NAME                                      READY     STATUS    RESTARTS   AGE
nginx-app-76b6449498-2j82j                1/1       Running   0          <invalid>
nginx-app-76b6449498-m4h58                1/1       Running   0          <invalid>

看到了吧,我們定義了兩個副本(size=2)图张,這里就出現(xiàn)了兩個 Pod锋拖,還有一個 NodePort=30002 的 Service 對象诈悍,我們可以通過該端口去訪問下應(yīng)用:


image

如果應(yīng)用在安裝過程中出現(xiàn)了任何問題,我們都可以通過本地的 Operator 調(diào)試窗口找到有用的信息兽埃,然后調(diào)試修改即可侥钳。

清理:

$ kubectl delete -f deploy/crds/app_v1_appservice_crd.yaml
$ kubectl delete -f deploy/crds/app_v1_appservice_cr.yaml

部署

自定義的資源對象現(xiàn)在測試通過了,但是如果我們將本地的operator-sdk up local命令終止掉讲仰,我們可以猜想到就沒辦法處理 AppService 資源對象的一些操作了慕趴,所以我們需要將我們的業(yè)務(wù)邏輯實現(xiàn)部署到集群中去痪蝇。

執(zhí)行下面的命令構(gòu)建 Operator 應(yīng)用打包成 Docker 鏡像:
$ operator-sdk build cnych/opdemo
INFO[0002] Building Docker image cnych/opdemo
Sending build context to Docker daemon 400.7MB
Step 1/7 : FROM registry.access.redhat.com/ubi7-dev-preview/ubi-minimal:7.6
......
Successfully built a8cde91be6ab
Successfully tagged cnych/opdemo:latest
INFO[0053] Operator build complete.
鏡像構(gòu)建成功后鄙陡,推送到 docker hub:

$ docker push cnych/opdemo

鏡像推送成功后,使用上面的鏡像地址更新 Operator 的資源清單:

$ sed -i 's|REPLACE_IMAGE|cnych/opdemo|g' deploy/operator.yaml
# 如果你使用的是 Mac 系統(tǒng)躏啰,使用下面的命令
$ sed -i "" 's|REPLACE_IMAGE|cnych/opdemo|g' deploy/operator.yaml

現(xiàn)在 Operator 的資源清單文件準備好了趁矾,然后創(chuàng)建對應(yīng)的 RBAC 的對象:

# Setup Service Account
$ kubectl create -f deploy/service_account.yaml
# Setup RBAC
$ kubectl create -f deploy/role.yaml
$ kubectl create -f deploy/role_binding.yaml

權(quán)限相關(guān)聲明已經(jīng)完成,接下來安裝 CRD 和 Operator:

# Setup the CRD
$ kubectl apply -f deploy/crds/app_v1_appservice_crd.yaml
$ kubectl get crd
NAME                                   CREATED AT
appservices.app.example.com            2019-05-30T17:03:32Z
......
# Deploy the Operator
$ kubectl create -f deploy/operator.yaml
deployment.apps/opdemo created
$ kubectl get pods
NAME                                      READY   STATUS    RESTARTS   AGE
opdemo-64db96d575-9vtq6                   1/1     Running   0          2m2s

到這里我們的 CRD 和 Operator 實現(xiàn)都已經(jīng)安裝成功了给僵。

現(xiàn)在我們再來部署我們的 AppService 資源清單文件毫捣,現(xiàn)在的業(yè)務(wù)邏輯就會在上面的opdemo-64db96d575-9vtq6的 Pod 中去處理了。

$ kubectl create -f deploy/crds/app_v1_appservice_cr.yaml
appservice.app.example.com/nginx-app created
$ kubectl get appservice
NAME        AGE
nginx-app   18s
$  kubectl get deploy
NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
nginx-app                2/2     2            2           24s
opdemo                   1/1     1            1           5m51s
$  kubectl get svc
NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
kubernetes   ClusterIP   10.96.0.1       <none>        443/TCP        76d
nginx-app    NodePort    10.106.129.82   <none>        80:30002/TCP   29s
opdemo       ClusterIP   10.100.233.51   <none>        8383/TCP       4m25s
$  kubectl get pods
NAME                                      READY   STATUS    RESTARTS   AGE
nginx-app-76b6449498-ffhgx                1/1     Running   0          32s
nginx-app-76b6449498-wzjq2                1/1     Running   0          32s
opdemo-64db96d575-9vtq6                   1/1     Running   0          5m59s
$ kubectl describe appservice nginx-app
Name:         nginx-app
Namespace:    default
Labels:       <none>
Annotations:  spec: {"size":2,"image":"nginx:1.7.9","resources":{},"ports":[{"protocol":"TCP","port":80,"targetPort":80,"nodePort":30002}]}
API Version:  app.example.com/v1
Kind:         AppService
Metadata:
  Creation Timestamp:  2019-05-30T17:41:28Z
  Generation:          2
  Resource Version:    19666617
  Self Link:           /apis/app.example.com/v1/namespaces/default/appservices/nginx-app
  UID:                 2756f232-8302-11e9-80ca-525400cc3c00
Spec:
  Image:  nginx:1.7.9
  Ports:
    Node Port:    30002
    Port:         80
    Protocol:     TCP
    Target Port:  80
  Resources:
  Size:  2
Events:  <none>

然后同樣的可以通過 30002 這個 NodePort 端口去訪問應(yīng)用帝际,到這里應(yīng)用就部署成功了蔓同。

清理

有資源清單文件,直接刪除即可:

$ kubectl delete -f deploy/crds/app_v1_appservice_cr.yaml
$ kubectl delete -f deploy/operator.yaml
$ kubectl delete -f deploy/role.yaml
$ kubectl delete -f deploy/role_binding.yaml
$ kubectl delete -f deploy/service_account.yaml
$ kubectl delete -f deploy/crds/app_v1_appservice_crd.yaml

開發(fā)

Operator SDK 為我們創(chuàng)建了一個快速啟動的代碼和相關(guān)配置蹲诀,如果我們要開始處理相關(guān)的邏輯斑粱,我們可以在項目中搜索TODO(user)這個注釋來實現(xiàn)我們自己的邏輯,比如在我的 VSCode 環(huán)境中脯爪,看上去是這樣的:


image

本篇文章參考代碼地址:https://github.com/cnych/opdemo

視頻教程地址: https://www.bilibili.com/video/BV1zE411j7ky?p=1

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末则北,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子痕慢,更是在濱河造成了極大的恐慌尚揣,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件掖举,死亡現(xiàn)場離奇詭異快骗,居然都是意外死亡,警方通過查閱死者的電腦和手機塔次,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進店門滨巴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人俺叭,你說我怎么就攤上這事恭取。” “怎么了熄守?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵蜈垮,是天一觀的道長耗跛。 經(jīng)常有香客問我,道長攒发,這世上最難降的妖魔是什么调塌? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮惠猿,結(jié)果婚禮上羔砾,老公的妹妹穿的比我還像新娘。我一直安慰自己偶妖,他們只是感情好姜凄,可當(dāng)我...
    茶點故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著趾访,像睡著了一般态秧。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上扼鞋,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天申鱼,我揣著相機與錄音,去河邊找鬼云头。 笑死捐友,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的溃槐。 我是一名探鬼主播脆粥,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼匣缘,長吁一口氣:“原來是場噩夢啊……” “哼豁陆!你這毒婦竟也來了表鳍?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤登澜,失蹤者是張志新(化名)和其女友劉穎竭宰,沒想到半個月后廓旬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體十气,經(jīng)...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡衅疙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了走芋。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片绩郎。...
    茶點故事閱讀 40,505評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡絮识,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出嗽上,到底是詐尸還是另有隱情次舌,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布兽愤,位于F島的核電站彼念,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏浅萧。R本人自食惡果不足惜逐沙,卻給世界環(huán)境...
    茶點故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望洼畅。 院中可真熱鬧吩案,春花似錦、人聲如沸帝簇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽丧肴。三九已至残揉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間芋浮,已是汗流浹背抱环。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留纸巷,地道東北人镇草。 一個月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像瘤旨,于是被迫代替她去往敵國和親梯啤。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,515評論 2 359

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