{"id":13831621,"url":"https://github.com/cnych/opdemo","last_synced_at":"2025-05-07T15:22:10.000Z","repository":{"id":57482797,"uuid":"189391391","full_name":"cnych/opdemo","owner":"cnych","description":"Kubernetes Operator 快速入门教程（Kubernetes Operator 101）","archived":false,"fork":false,"pushed_at":"2020-06-12T07:27:10.000Z","size":33260,"stargazers_count":203,"open_issues_count":2,"forks_count":91,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-03-31T11:21:13.409Z","etag":null,"topics":["101","kubernetes","operator","operator-framework","operator-sdk"],"latest_commit_sha":null,"homepage":"https://www.qikqiak.com/post/k8s-operator-101/","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cnych.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-05-30T10:08:50.000Z","updated_at":"2025-03-19T10:07:57.000Z","dependencies_parsed_at":"2022-09-03T09:51:27.439Z","dependency_job_id":null,"html_url":"https://github.com/cnych/opdemo","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cnych%2Fopdemo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cnych%2Fopdemo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cnych%2Fopdemo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cnych%2Fopdemo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cnych","download_url":"https://codeload.github.com/cnych/opdemo/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252902791,"owners_count":21822311,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["101","kubernetes","operator","operator-framework","operator-sdk"],"created_at":"2024-08-04T10:01:33.759Z","updated_at":"2025-05-07T15:22:04.991Z","avatar_url":"https://github.com/cnych.png","language":"Go","funding_links":[],"categories":["Go"],"sub_categories":[],"readme":"# opdemo\nKubernetes Operator 快速入门教程（Kubernetes Operator 101）\n\n[![kubernetes operator 101](https://bxdc-static.oss-cn-beijing.aliyuncs.com/images/默认标题_公众号封面首图_2019.05.30.png)](https://www.qikqiak.com/post/k8s-operator-101/)\n\n在 Kubernetes 的监控方案中我们经常会使用到一个[Promethues Operator](https://www.qikqiak.com/tags/operator/)的项目，该项目可以让我们更加方便的去使用 Prometheus，而不需要直接去使用最原始的一些资源对象，比如 Pod、Deployment，随着 Prometheus Operator 项目的成功，CoreOS 公司开源了一个比较厉害的工具：[Operator Framework](https://github.com/operator-framework)，该工具可以让开发人员更加容易的开发 Operator 应用。\n\n在本篇文章中我们会为大家介绍一个简单示例来演示如何使用 Operator Framework 框架来开发一个 Operator 应用。\n\n## Kubernetes Operator\nOperator 是由 CoreOS 开发的，用来扩展 Kubernetes API，特定的应用程序控制器，它用来创建、配置和管理复杂的有状态应用，如数据库、缓存和监控系统。Operator 基于 Kubernetes 的资源和控制器概念之上构建，但同时又包含了应用程序特定的领域知识。创建Operator 的关键是CRD（自定义资源）的设计。\n\nKubernetes 1.7 版本以来就引入了[自定义控制器](https://kubernetes.io/docs/concepts/api-extension/custom-resources/)的概念，该功能可以让开发人员扩展添加新功能，更新现有的功能，并且可以自动执行一些管理任务，这些自定义的控制器就像 Kubernetes 原生的组件一样，Operator 直接使用 Kubernetes API进行开发，也就是说他们可以根据这些控制器内部编写的自定义规则来监控集群、更改 Pods/Services、对正在运行的应用进行扩缩容。\n\n\n## Operator Framework\nOperator Framework 同样也是 CoreOS 开源的一个用于快速开发 Operator 的工具包，该框架包含两个主要的部分：\n\n* Operator SDK: 无需了解复杂的 Kubernetes API 特性，即可让你根据你自己的专业知识构建一个 Operator 应用。\n* Operator Lifecycle Manager OLM: 帮助你安装、更新和管理跨集群的运行中的所有 Operator（以及他们的相关服务）\n\n![operator sdk](https://bxdc-static.oss-cn-beijing.aliyuncs.com/images/operator-sdk-lifecycle.png)\n\n### Workflow\nOperator SDK 提供以下工作流来开发一个新的 Operator：\n\n* 1. 使用 SDK 创建一个新的 Operator 项目\n* 2. 通过添加自定义资源（CRD）定义新的资源 API\n* 3. 指定使用 SDK API 来 watch 的资源\n* 4. 定义 Operator 的协调（reconcile）逻辑\n* 5. 使用 Operator SDK 构建并生成 Operator 部署清单文件\n\n## Demo\n我们平时在部署一个简单的 Webserver 到 Kubernetes 集群中的时候，都需要先编写一个 Deployment 的控制器，然后创建一个 Service 对象，通过 Pod 的 label 标签进行关联，最后通过 Ingress 或者 type=NodePort 类型的 Service 来暴露服务，每次都需要这样操作，是不是略显麻烦，我们就可以创建一个自定义的资源对象，通过我们的 CRD 来描述我们要部署的应用信息，比如镜像、服务端口、环境变量等等，然后创建我们的自定义类型的资源对象的时候，通过控制器去创建对应的 Deployment 和 Service，是不是就方便很多了，相当于我们用一个资源清单去描述了 Deployment 和 Service 要做的两件事情。\n\n这里我们将创建一个名为 AppService 的 CRD 资源对象，然后定义如下的资源清单进行应用部署：\n```yaml\napiVersion: app.example.com/v1\nkind: AppService\nmetadata:\n  name: nginx-app\nspec:\n  size: 2\n  image: nginx:1.7.9\n  ports:\n    - port: 80\n      targetPort: 80\n      nodePort: 30002\n```\n\n通过这里的自定义的 AppService 资源对象去创建副本数为2的 Pod，然后通过 nodePort=30002 的端口去暴露服务，接下来我们就来一步一步的实现我们这里的这个简单的 Operator 应用。\n\n### 开发环境\n\n#### 环境需求\n要开发 Operator 自然 Kubernetes 集群是少不了的，还需要 Golang 的环境，这里的安装就不多说了，然后还需要一个 Go 语言的依赖管理工具包：[dep](https://github.com/golang/dep)，由于 Operator SDK 是使用的 dep 该工具包，所以需要我们提前安装好，可以查看资料：https://github.com/golang/dep，另外一个需要说明的是，由于 dep 去安装的时候需要去谷歌的网站拉取很多代码，所以正常情况下的话是会失败的，需要做什么工作大家应该清楚吧？要科学。\n\n#### 安装 operator-sdk\noperator sdk 安装方法非常多，我们可以直接在 github 上面下载需要使用的版本，然后放置到 PATH 环境下面即可，当然也可以将源码 clone 到本地手动编译安装即可，如果你是 Mac，当然还可以使用常用的 brew 工具进行安装：\n```shell\n$ brew install operator-sdk\n......\n$ operator-sdk version\noperator-sdk version: v0.7.0\n$ go version\ngo version go1.11.4 darwin/amd64\n```\n\n我们这里使用的 sdk 版本是`v0.7.0`，其他安装方法可以参考文档：https://github.com/operator-framework/operator-sdk/blob/master/doc/user/install-operator-sdk.md\n\n\n### 演示\n#### 创建新项目\n环境准备好了，接下来就可以使用 operator-sdk 直接创建一个新的项目了，命令格式为：\n**operator-sdk new \u003cproject-name\u003e**\n\n按照上面我们预先定义的 CRD 资源清单，我们这里可以这样创建：\n```shell\n# 创建项目目录\n$ mkdir -p operator-learning  \n# 设置项目目录为 GOPATH 路径\n$ cd operator-learning \u0026\u0026 export GOPATH=$PWD  \n$ mkdir -p $GOPATH/src/github.com/cnych \n$ cd $GOPATH/src/github.com/cnych\n# 使用 sdk 创建一个名为 opdemo 的 operator 项目\n$ operator-sdk new opdemo\n......\n# 该过程需要科学上网，需要花费很长时间，请耐心等待\n......\n$ cd opdemo \u0026\u0026 tree -L 2\n.\n├── Gopkg.lock\n├── Gopkg.toml\n├── build\n│   ├── Dockerfile\n│   ├── _output\n│   └── bin\n├── cmd\n│   └── manager\n├── deploy\n│   ├── crds\n│   ├── operator.yaml\n│   ├── role.yaml\n│   ├── role_binding.yaml\n│   └── service_account.yaml\n├── pkg\n│   ├── apis\n│   └── controller\n├── vendor\n│   ├── cloud.google.com\n│   ├── contrib.go.opencensus.io\n│   ├── github.com\n│   ├── go.opencensus.io\n│   ├── go.uber.org\n│   ├── golang.org\n│   ├── google.golang.org\n│   ├── gopkg.in\n│   ├── k8s.io\n│   └── sigs.k8s.io\n└── version\n    └── version.go\n\n23 directories, 8 files\n```\n\n到这里一个全新的 Operator 项目就新建完成了。\n\n#### 项目结构\n使用`operator-sdk new`命令创建新的 Operator 项目后，项目目录就包含了很多生成的文件夹和文件。\n\n* **Gopkg.toml Gopkg.lock** — Go Dep 清单，用来描述当前 Operator 的依赖包。\n* **cmd** - 包含 main.go 文件，使用 operator-sdk API 初始化和启动当前 Operator 的入口。\n* **deploy** - 包含一组用于在 Kubernetes 集群上进行部署的通用的 Kubernetes 资源清单文件。\n* **pkg/apis** - 包含定义的 API 和自定义资源（CRD）的目录树，这些文件允许 sdk 为 CRD 生成代码并注册对应的类型，以便正确解码自定义资源对象。\n* **pkg/controller** - 用于编写所有的操作业务逻辑的地方\n* **vendor** - golang vendor 文件夹，其中包含满足当前项目的所有外部依赖包，通过 go dep 管理该目录。\n\n我们主要需要编写的是**pkg**目录下面的 api 定义以及对应的 controller 实现。\n\n#### 添加 API\n接下来为我们的自定义资源添加一个新的 API，按照上面我们预定义的资源清单文件，在 Operator 相关根目录下面执行如下命令：\n```shell\n$ operator-sdk add api --api-version=app.example.com/v1 --kind=AppService\n```\n\n添加完成后，我们可以看到类似于下面的这样项目结构：\n![operator project layout](https://bxdc-static.oss-cn-beijing.aliyuncs.com/images/operator-project-layout.png)\n\n\n#### 添加控制器\n上面我们添加自定义的 API，接下来可以添加对应的自定义 API 的具体实现 Controller，同样在项目根目录下面执行如下命令：\n```shell\n$ operator-sdk add controller --api-version=app.example.com/v1 --kind=AppService\n```\n\n这样整个 Operator 项目的脚手架就已经搭建完成了，接下来就是具体的实现了。\n\n\n#### 自定义 API\n打开源文件`pkg/apis/app/v1/appservice_types.go`，需要我们根据我们的需求去自定义结构体 AppServiceSpec，我们最上面预定义的资源清单中就有 size、image、ports 这些属性，所有我们需要用到的属性都需要在这个结构体中进行定义：\n```golang\ntype AppServiceSpec struct {\n\t// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster\n\t// Important: Run \"operator-sdk generate k8s\" to regenerate code after modifying this file\n\t// Add custom validation using kubebuilder tags: https://book.kubebuilder.io/beyond_basics/generating_crd.html\n\tSize  \t  *int32                      `json:\"size\"`\n\tImage     string                      `json:\"image\"`\n\tResources corev1.ResourceRequirements `json:\"resources,omitempty\"`\n\tEnvs      []corev1.EnvVar             `json:\"envs,omitempty\"`\n\tPorts     []corev1.ServicePort        `json:\"ports,omitempty\"`\n}\n```\n\n代码中会涉及到一些包名的导入，由于包名较多，所以我们会使用一些别名进行区分，主要的包含下面几个：\n```golang\nimport (\n    appsv1 \"k8s.io/api/apps/v1\"\n    corev1 \"k8s.io/api/core/v1\"\n    appv1 \"github.com/cnych/opdemo/pkg/apis/app/v1\"\n    metav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n```\n\n这里的 resources、envs、ports 的定义都是直接引用的`\"k8s.io/api/core/v1\"`中定义的结构体，而且需要注意的是我们这里使用的是`ServicePort`，而不是像传统的 Pod 中定义的 ContanerPort，这是因为我们的资源清单中不仅要描述容器的 Port，还要描述 Service 的 Port。\n\n然后一个比较重要的结构体`AppServiceStatus`用来描述资源的状态，当然我们可以根据需要去自定义状态的描述，我这里就偷懒直接使用 Deployment 的状态了：\n```shell\ntype AppServiceStatus struct {\n\t// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster\n\t// Important: Run \"operator-sdk generate k8s\" to regenerate code after modifying this file\n\t// Add custom validation using kubebuilder tags: https://book.kubebuilder.io/beyond_basics/generating_crd.html\n\tappsv1.DeploymentStatus `json:\",inline\"`\n}\n```\n\n定义完成后，在项目根目录下面执行如下命令：\n```shell\n$ operator-sdk generate k8s\n```\n\n改命令是用来根据我们自定义的 API 描述来自动生成一些代码，目录`pkg/apis/app/v1/`下面以`zz_generated`开头的文件就是自动生成的代码，里面的内容并不需要我们去手动编写。\n\n这样我们就算完成了对自定义资源对象的 API 的声明。\n\n#### 实现业务逻辑\n上面 API 描述声明完成了，接下来就需要我们来进行具体的业务逻辑实现了，编写具体的 controller 实现，打开源文件`pkg/controller/appservice/appservice_controller.go`，需要我们去更改的地方也不是很多，核心的就是`Reconcile`方法，该方法就是去不断的 watch 资源的状态，然后根据状态的不同去实现各种操作逻辑，核心代码如下：\n```golang\nfunc (r *ReconcileAppService) Reconcile(request reconcile.Request) (reconcile.Result, error) {\n\treqLogger := log.WithValues(\"Request.Namespace\", request.Namespace, \"Request.Name\", request.Name)\n\treqLogger.Info(\"Reconciling AppService\")\n\n\t// Fetch the AppService instance\n\tinstance := \u0026appv1.AppService{}\n\terr := r.client.Get(context.TODO(), request.NamespacedName, instance)\n\tif err != nil {\n\t\tif errors.IsNotFound(err) {\n\t\t\t// Request object not found, could have been deleted after reconcile request.\n\t\t\t// Owned objects are automatically garbage collected. For additional cleanup logic use finalizers.\n\t\t\t// Return and don't requeue\n\t\t\treturn reconcile.Result{}, nil\n\t\t}\n\t\t// Error reading the object - requeue the request.\n\t\treturn reconcile.Result{}, err\n\t}\n\n\tif instance.DeletionTimestamp != nil {\n\t\treturn reconcile.Result{}, err\n\t}\n\n\t// 如果不存在，则创建关联资源\n\t// 如果存在，判断是否需要更新\n\t//   如果需要更新，则直接更新\n\t//   如果不需要更新，则正常返回\n\n\tdeploy := \u0026appsv1.Deployment{}\n\tif err := r.client.Get(context.TODO(), request.NamespacedName, deploy); err != nil \u0026\u0026 errors.IsNotFound(err) {\n\t\t// 创建关联资源\n\t\t// 1. 创建 Deploy\n\t\tdeploy := resources.NewDeploy(instance)\n\t\tif err := r.client.Create(context.TODO(), deploy); err != nil {\n\t\t\treturn reconcile.Result{}, err\n\t\t}\n\t\t// 2. 创建 Service\n\t\tservice := resources.NewService(instance)\n\t\tif err := r.client.Create(context.TODO(), service); err != nil {\n\t\t\treturn reconcile.Result{}, err\n\t\t}\n\t\t// 3. 关联 Annotations\n\t\tdata, _ := json.Marshal(instance.Spec)\n\t\tif instance.Annotations != nil {\n\t\t\tinstance.Annotations[\"spec\"] = string(data)\n\t\t} else {\n\t\t\tinstance.Annotations = map[string]string{\"spec\": string(data)}\n\t\t}\n\n\t\tif err := r.client.Update(context.TODO(), instance); err != nil {\n\t\t\treturn reconcile.Result{}, nil\n\t\t}\n\t\treturn reconcile.Result{}, nil\n\t}\n\n\toldspec := appv1.AppServiceSpec{}\n\tif err := json.Unmarshal([]byte(instance.Annotations[\"spec\"]), oldspec); err != nil {\n\t\treturn reconcile.Result{}, err\n\t}\n\n\tif !reflect.DeepEqual(instance.Spec, oldspec) {\n\t\t// 更新关联资源\n\t\tnewDeploy := resources.NewDeploy(instance)\n\t\toldDeploy := \u0026appsv1.Deployment{}\n\t\tif err := r.client.Get(context.TODO(), request.NamespacedName, oldDeploy); err != nil {\n\t\t\treturn reconcile.Result{}, err\n\t\t}\n\t\toldDeploy.Spec = newDeploy.Spec\n\t\tif err := r.client.Update(context.TODO(), oldDeploy); err != nil {\n\t\t\treturn reconcile.Result{}, err\n\t\t}\n\n\t\tnewService := resources.NewService(instance)\n\t\toldService := \u0026corev1.Service{}\n\t\tif err := r.client.Get(context.TODO(), request.NamespacedName, oldService); err != nil {\n\t\t\treturn reconcile.Result{}, err\n\t\t}\n\t\toldService.Spec = newService.Spec\n\t\tif err := r.client.Update(context.TODO(), oldService); err != nil {\n\t\t\treturn reconcile.Result{}, err\n\t\t}\n\n\t\treturn reconcile.Result{}, nil\n\t}\n\n\treturn reconcile.Result{}, nil\n\n}\n```\n\n上面就是业务逻辑实现的核心代码，逻辑很简单，就是去判断资源是否存在，不存在，则直接创建新的资源，创建新的资源除了需要创建 Deployment 资源外，还需要创建 Service 资源对象，因为这就是我们的需求，当然你还可以自己去扩展，比如在创建一个 Ingress 对象。更新也是一样的，去对比新旧对象的声明是否一致，不一致则需要更新，同样的，两种资源都需要更新的。\n\n另外两个核心的方法就是上面的`resources.NewDeploy(instance)`和`resources.NewService(instance)`方法，这两个方法实现逻辑也很简单，就是根据 CRD 中的声明去填充 Deployment 和 Service 资源对象的 Spec 对象即可。\n\nNewDeploy 方法实现如下：\n```golang\nfunc NewDeploy(app *appv1.AppService) *appsv1.Deployment {\n\tlabels := map[string]string{\"app\": app.Name}\n\tselector := \u0026metav1.LabelSelector{MatchLabels: labels}\n\treturn \u0026appsv1.Deployment{\n\t\tTypeMeta: metav1.TypeMeta{\n\t\t\tAPIVersion: \"apps/v1\",\n\t\t\tKind:       \"Deployment\",\n\t\t},\n\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\tName:      app.Name,\n\t\t\tNamespace: app.Namespace,\n\n\t\t\tOwnerReferences: []metav1.OwnerReference{\n\t\t\t\t*metav1.NewControllerRef(app, schema.GroupVersionKind{\n\t\t\t\t\tGroup: v1.SchemeGroupVersion.Group,\n\t\t\t\t\tVersion: v1.SchemeGroupVersion.Version,\n\t\t\t\t\tKind: \"AppService\",\n\t\t\t\t}),\n\t\t\t},\n\t\t},\n\t\tSpec: appsv1.DeploymentSpec{\n\t\t\tReplicas: app.Spec.Size,\n\t\t\tTemplate: corev1.PodTemplateSpec{\n\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\tLabels: labels,\n\t\t\t\t},\n\t\t\t\tSpec: corev1.PodSpec{\n\t\t\t\t\tContainers: newContainers(app),\n\t\t\t\t},\n\t\t\t},\n\t\t\tSelector: selector,\n\t\t},\n\t}\n}\n\nfunc newContainers(app *v1.AppService) []corev1.Container {\n\tcontainerPorts := []corev1.ContainerPort{}\n\tfor _, svcPort := range app.Spec.Ports {\n\t\tcport := corev1.ContainerPort{}\n\t\tcport.ContainerPort = svcPort.TargetPort.IntVal\n\t\tcontainerPorts = append(containerPorts, cport)\n\t}\n\treturn []corev1.Container{\n\t\t{\n\t\t\tName: app.Name,\n\t\t\tImage: app.Spec.Image,\n\t\t\tResources: app.Spec.Resources,\n\t\t\tPorts: containerPorts,\n\t\t\tImagePullPolicy: corev1.PullIfNotPresent,\n\t\t\tEnv: app.Spec.Envs,\n\t\t},\n\t}\n}\n```\n\nnewService 对应的方法实现如下：\n```golang\nfunc NewService(app *v1.AppService) *corev1.Service {\n\treturn \u0026corev1.Service {\n\t\tTypeMeta: metav1.TypeMeta {\n\t\t\tKind: \"Service\",\n\t\t\tAPIVersion: \"v1\",\n\t\t},\n\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\tName: app.Name,\n\t\t\tNamespace: app.Namespace,\n\t\t\tOwnerReferences: []metav1.OwnerReference{\n\t\t\t\t*metav1.NewControllerRef(app, schema.GroupVersionKind{\n\t\t\t\t\tGroup: v1.SchemeGroupVersion.Group,\n\t\t\t\t\tVersion: v1.SchemeGroupVersion.Version,\n\t\t\t\t\tKind: \"AppService\",\n\t\t\t\t}),\n\t\t\t},\n\t\t},\n\t\tSpec: corev1.ServiceSpec{\n\t\t\tType: corev1.ServiceTypeNodePort,\n\t\t\tPorts: app.Spec.Ports,\n\t\t\tSelector: map[string]string{\n\t\t\t\t\"app\": app.Name,\n\t\t\t},\n\t\t},\n\t}\n}\n```\n\n这样我们就实现了 AppService 这种资源对象的业务逻辑。\n\n\n#### 调试\n如果我们本地有一个可以访问的 Kubernetes 集群，我们也可以直接进行调试，在本地用户`~/.kube/config`文件中配置集群访问信息，下面的信息表明可以访问 Kubernetes 集群：\n```shell\n$ kubectl cluster-info\nKubernetes master is running at https://ydzs-master:6443\nKubeDNS is running at https://ydzs-master:6443/api/v1/namespaces/kube-system/services/kube-dns/proxy\n\nTo further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.\n```\n\n首先，在集群中安装 CRD 对象：\n```shell\n$ kubectl create -f deploy/crds/app_v1_appservice_crd.yaml\ncustomresourcedefinition \"appservices.app.example.com\" created\n$ kubectl get crd\nNAME                                   AGE\nappservices.app.example.com            \u003cinvalid\u003e\n......\n```\n\n当我们通过`kubectl get crd`命令获取到我们定义的 CRD 资源对象，就证明我们定义的 CRD 安装成功了。其实现在只是 CRD 的这个声明安装成功了，但是我们这个 CRD 的具体业务逻辑实现方式还在我们本地，并没有部署到集群之中，我们可以通过下面的命令来在本地项目中启动 Operator 的调试：\n```shell\n$ operator-sdk up local                                                     \nINFO[0000] Running the operator locally.                \nINFO[0000] Using namespace default.                     \n{\"level\":\"info\",\"ts\":1559207203.964137,\"logger\":\"cmd\",\"msg\":\"Go Version: go1.11.4\"}\n{\"level\":\"info\",\"ts\":1559207203.964192,\"logger\":\"cmd\",\"msg\":\"Go OS/Arch: darwin/amd64\"}\n{\"level\":\"info\",\"ts\":1559207203.9641972,\"logger\":\"cmd\",\"msg\":\"Version of operator-sdk: v0.7.0\"}\n{\"level\":\"info\",\"ts\":1559207203.965905,\"logger\":\"leader\",\"msg\":\"Trying to become the leader.\"}\n{\"level\":\"info\",\"ts\":1559207203.965945,\"logger\":\"leader\",\"msg\":\"Skipping leader election; not running in a cluster.\"}\n{\"level\":\"info\",\"ts\":1559207206.928867,\"logger\":\"cmd\",\"msg\":\"Registering Components.\"}\n{\"level\":\"info\",\"ts\":1559207206.929077,\"logger\":\"kubebuilder.controller\",\"msg\":\"Starting EventSource\",\"controller\":\"appservice-controller\",\"source\":\"kind source: /, Kind=\"}\n{\"level\":\"info\",\"ts\":1559207206.9292521,\"logger\":\"kubebuilder.controller\",\"msg\":\"Starting EventSource\",\"controller\":\"appservice-controller\",\"source\":\"kind source: /, Kind=\"}\n{\"level\":\"info\",\"ts\":1559207209.622659,\"logger\":\"cmd\",\"msg\":\"failed to initialize service object for metrics: OPERATOR_NAME must be set\"}\n{\"level\":\"info\",\"ts\":1559207209.622693,\"logger\":\"cmd\",\"msg\":\"Starting the Cmd.\"}\n{\"level\":\"info\",\"ts\":1559207209.7236018,\"logger\":\"kubebuilder.controller\",\"msg\":\"Starting Controller\",\"controller\":\"appservice-controller\"}\n{\"level\":\"info\",\"ts\":1559207209.8284118,\"logger\":\"kubebuilder.controller\",\"msg\":\"Starting workers\",\"controller\":\"appservice-controller\",\"worker count\":1}\n```\n\n上面的命令会在本地运行 Operator 应用，通过`~/.kube/config`去关联集群信息，现在我们去添加一个 AppService 类型的资源然后观察本地 Operator 的变化情况，资源清单文件就是我们上面预定义的（deploy/crds/app_v1_appservice_cr.yaml）\n```yaml\napiVersion: app.example.com/v1\nkind: AppService\nmetadata:\n  name: nginx-app\nspec:\n  size: 2\n  image: nginx:1.7.9\n  ports:\n    - port: 80\n      targetPort: 80\n      nodePort: 30002\n```\n\n直接创建这个资源对象：\n```shell\n$ kubectl create -f deploy/crds/app_v1_appservice_cr.yaml\nappservice \"nginx-app\" created\n```\n\n我们可以看到我们的应用创建成功了，这个时候查看 Operator 的调试窗口会有如下的信息出现：\n```shell\n......\n{\"level\":\"info\",\"ts\":1559207416.670523,\"logger\":\"controller_appservice\",\"msg\":\"Reconciling AppService\",\"Request.Namespace\":\"default\",\"Request.Name\":\"nginx-app\"}\n{\"level\":\"info\",\"ts\":1559207417.004226,\"logger\":\"controller_appservice\",\"msg\":\"Reconciling AppService\",\"Request.Namespace\":\"default\",\"Request.Name\":\"nginx-app\"}\n{\"level\":\"info\",\"ts\":1559207417.004331,\"logger\":\"controller_appservice\",\"msg\":\"Reconciling AppService\",\"Request.Namespace\":\"default\",\"Request.Name\":\"nginx-app\"}\n{\"level\":\"info\",\"ts\":1559207418.33779,\"logger\":\"controller_appservice\",\"msg\":\"Reconciling AppService\",\"Request.Namespace\":\"default\",\"Request.Name\":\"nginx-app\"}\n{\"level\":\"info\",\"ts\":1559207418.951193,\"logger\":\"controller_appservice\",\"msg\":\"Reconciling AppService\",\"Request.Namespace\":\"default\",\"Request.Name\":\"nginx-app\"}\n......\n```\n\n然后我们可以去查看集群中是否有符合我们预期的资源出现：\n```shell\n$ kubectl get AppService\nNAME        AGE\nnginx-app   \u003cinvalid\u003e\n$ kubectl get deploy\nNAME                     DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE\nnginx-app                2         2         2            2           \u003cinvalid\u003e\n$ kubectl get svc\nNAME         TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE\nkubernetes   ClusterIP   10.96.0.1      \u003cnone\u003e        443/TCP        76d\nnginx-app    NodePort    10.108.227.5   \u003cnone\u003e        80:30002/TCP   \u003cinvalid\u003e\n$ kubectl get pods\nNAME                                      READY     STATUS    RESTARTS   AGE\nnginx-app-76b6449498-2j82j                1/1       Running   0          \u003cinvalid\u003e\nnginx-app-76b6449498-m4h58                1/1       Running   0          \u003cinvalid\u003e\n```\n\n看到了吧，我们定义了两个副本（size=2），这里就出现了两个 Pod，还有一个 NodePort=30002 的 Service 对象，我们可以通过该端口去访问下应用：\n![crd nginx app demo](https://bxdc-static.oss-cn-beijing.aliyuncs.com/images/crd-nginx-app-demo.png)\n\n如果应用在安装过程中出现了任何问题，我们都可以通过本地的 Operator 调试窗口找到有用的信息，然后调试修改即可。\n\n清理：\n```shell\n$ kubectl delete -f deploy/crds/app_v1_appservice_crd.yaml\n$ kubectl delete -f deploy/crds/app_v1_appservice_cr.yaml\n```\n\n#### 部署\n自定义的资源对象现在测试通过了，但是如果我们将本地的`operator-sdk up local`命令终止掉，我们可以猜想到就没办法处理 AppService 资源对象的一些操作了，所以我们需要将我们的业务逻辑实现部署到集群中去。\n\n执行下面的命令构建 Operator 应用打包成 Docker 镜像：\n```shell\n$ operator-sdk build cnych/opdemo                         \nINFO[0002] Building Docker image cnych/opdemo           \nSending build context to Docker daemon  400.7MB\nStep 1/7 : FROM registry.access.redhat.com/ubi7-dev-preview/ubi-minimal:7.6\n......\nSuccessfully built a8cde91be6ab\nSuccessfully tagged cnych/opdemo:latest\nINFO[0053] Operator build complete.              \n```\n\n镜像构建成功后，推送到 docker hub：\n```shell\n$ docker push cnych/opdemo\n```\n\n镜像推送成功后，使用上面的镜像地址更新 Operator 的资源清单：\n```shell\n$ sed -i 's|REPLACE_IMAGE|cnych/opdemo|g' deploy/operator.yaml\n# 如果你使用的是 Mac 系统，使用下面的命令\n$ sed -i \"\" 's|REPLACE_IMAGE|cnych/opdemo|g' deploy/operator.yaml\n```\n\n现在 Operator 的资源清单文件准备好了，然后创建对应的 RBAC 的对象：\n```shell\n# Setup Service Account\n$ kubectl create -f deploy/service_account.yaml\n# Setup RBAC\n$ kubectl create -f deploy/role.yaml\n$ kubectl create -f deploy/role_binding.yaml\n```\n\n权限相关声明已经完成，接下来安装 CRD 和 Operator：\n```shell\n# Setup the CRD\n$ kubectl apply -f deploy/crds/app_v1_appservice_crd.yaml\n$ kubectl get crd\nNAME                                   CREATED AT\nappservices.app.example.com            2019-05-30T17:03:32Z\n......\n# Deploy the Operator\n$ kubectl create -f deploy/operator.yaml\ndeployment.apps/opdemo created\n$ kubectl get pods\nNAME                                      READY   STATUS    RESTARTS   AGE\nopdemo-64db96d575-9vtq6                   1/1     Running   0          2m2s\n```\n\n到这里我们的 CRD 和 Operator 实现都已经安装成功了。\n\n现在我们再来部署我们的 AppService 资源清单文件，现在的业务逻辑就会在上面的`opdemo-64db96d575-9vtq6`的 Pod 中去处理了。\n```shell\n$ kubectl create -f deploy/crds/app_v1_appservice_cr.yaml\nappservice.app.example.com/nginx-app created\n$ kubectl get appservice\nNAME        AGE\nnginx-app   18s\n$  kubectl get deploy\nNAME                     READY   UP-TO-DATE   AVAILABLE   AGE\nnginx-app                2/2     2            2           24s\nopdemo                   1/1     1            1           5m51s\n$  kubectl get svc\nNAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE\nkubernetes   ClusterIP   10.96.0.1       \u003cnone\u003e        443/TCP        76d\nnginx-app    NodePort    10.106.129.82   \u003cnone\u003e        80:30002/TCP   29s\nopdemo       ClusterIP   10.100.233.51   \u003cnone\u003e        8383/TCP       4m25s\n$  kubectl get pods\nNAME                                      READY   STATUS    RESTARTS   AGE\nnginx-app-76b6449498-ffhgx                1/1     Running   0          32s\nnginx-app-76b6449498-wzjq2                1/1     Running   0          32s\nopdemo-64db96d575-9vtq6                   1/1     Running   0          5m59s\n$ kubectl describe appservice nginx-app\nName:         nginx-app\nNamespace:    default\nLabels:       \u003cnone\u003e\nAnnotations:  spec: {\"size\":2,\"image\":\"nginx:1.7.9\",\"resources\":{},\"ports\":[{\"protocol\":\"TCP\",\"port\":80,\"targetPort\":80,\"nodePort\":30002}]}\nAPI Version:  app.example.com/v1\nKind:         AppService\nMetadata:\n  Creation Timestamp:  2019-05-30T17:41:28Z\n  Generation:          2\n  Resource Version:    19666617\n  Self Link:           /apis/app.example.com/v1/namespaces/default/appservices/nginx-app\n  UID:                 2756f232-8302-11e9-80ca-525400cc3c00\nSpec:\n  Image:  nginx:1.7.9\n  Ports:\n    Node Port:    30002\n    Port:         80\n    Protocol:     TCP\n    Target Port:  80\n  Resources:\n  Size:  2\nEvents:  \u003cnone\u003e\n```\n\n然后同样的可以通过 30002 这个 NodePort 端口去访问应用，到这里应用就部署成功了。\n\n\n#### 清理\n有资源清单文件，直接删除即可：\n```shell\n$ kubectl delete -f deploy/crds/app_v1_appservice_cr.yaml\n$ kubectl delete -f deploy/operator.yaml\n$ kubectl delete -f deploy/role.yaml\n$ kubectl delete -f deploy/role_binding.yaml\n$ kubectl delete -f deploy/service_account.yaml\n$ kubectl delete -f deploy/crds/app_v1_appservice_crd.yaml\n```\n\n#### 开发\nOperator SDK 为我们创建了一个快速启动的代码和相关配置，如果我们要开始处理相关的逻辑，我们可以在项目中搜索`TODO(user)`这个注释来实现我们自己的逻辑，比如在我的 VSCode 环境中，看上去是这样的：\n![operator code todo demo](https://bxdc-static.oss-cn-beijing.aliyuncs.com/images/operator-code-todo-demo.png)\n\n## 参考资料\n* [CLI reference](https://github.com/operator-framework/operator-sdk/blob/master/doc/sdk-cli-reference.md)\n* [User Guide](https://github.com/operator-framework/operator-sdk/blob/master/doc/user-guide.md)\n* [Developing Kubernetes Operator is now easy with Operator Framework](https://devops.college/developing-kubernetes-operator-is-now-easy-with-operator-framework-d3194a7428ff)\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcnych%2Fopdemo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcnych%2Fopdemo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcnych%2Fopdemo/lists"}