Operator 就可以看成是 CRD 和 Controller 的一种组合特例,Operator 是一种思想,它结合了特定领域知识并通过 CRD 机制扩展了 Kubernetes API 资源,使用户管理 Kubernetes 的内置资源(Pod、Deployment等)一样创建、配置和管理应用程序,Operator 是一个特定的应用程序的控制器,通过扩展 Kubernetes API 资源以代表 Kubernetes 用户创建、配置和管理复杂应用程序的实例,通常包含资源模型定义和控制器,通过 Operator 通常是为了实现某种特定软件(通常是有状态服务)的自动化运维。

我们完全可以通过上面的方式编写一个 CRD 对象,然后去手动实现一个对应的 Controller 就可以实现一个 Operator,但是我们也发现从头开始去构建一个 CRD 控制器并不容易,需要对 Kubernetes 的 API 有深入了解,并且 RBAC 集成、镜像构建、持续集成和部署等都需要很大工作量。为了解决这个问题,社区就推出了对应的简单易用的 Operator 框架,比较主流的是 kubebuilder 和 Operator Framework,这两个框架的使用基本上差别不大,我们可以根据自己习惯选择一个即可,我们这里使用 Operator Framework 来给大家简要说明下 Operator 的开发。

Operator Framework

Operator Framework 是 CoreOS 开源的一个用于快速开发 Operator 的工具包,该框架包含两个主要的部分:

  • Operator SDK: 无需了解复杂的 Kubernetes API 特性,即可让你根据你自己的专业知识构建一个 Operator 应用。
  • Operator Lifecycle Manager(OLM): 帮助你安装、更新和管理跨集群的运行中的所有 Operator(以及他们的相关服务)

Operator SDK 提供以下工作流来开发一个新的 Operator:

  • 使用 SDK 创建一个新的 Operator 项目
  • 通过添加自定义资源(CRD)定义新的资源 API
  • 指定使用 SDK API 来 watch 的资源
  • 定义 Operator 的协调(reconcile)逻辑
  • 使用 Operator SDK 构建并生成 Operator 部署清单文件

示例

我们平时在部署一个简单的 Webserver 到 Kubernetes 集群中的时候,都需要先编写一个 Deployment 的控制器,然后创建一个 Service 对象,通过 Pod 的 label 标签进行关联,最后通过 Ingress 或者 type=NodePort 类型的 Service 来暴露服务,每次都需要这样操作,是不是略显麻烦,我们就可以创建一个自定义的资源对象,通过我们的 CRD 来描述我们要部署的应用信息,比如镜像、服务端口、环境变量等等,然后创建我们的自定义类型的资源对象的时候,通过控制器去创建对应的 Deployment 和 Service,是不是就方便很多了,相当于我们用一个资源清单去描述了 Deployment 和 Service 要做的两件事情。

这里我们将创建一个名为 AppService 的 CRD 资源对象,然后定义如下的资源清单进行应用部署:

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 资源对象去创建副本数为2的 Pod,然后通过 nodePort=30002 的端口去暴露服务,接下来我们就来一步一步的实现我们这里的这个简单的 Operator 应用。

开发环境

要开发 Operator 自然 Kubernetes 集群是少不了的,还需要 Golang 的环境,这里的安装就不多说了。然后需要安装 operator-sdk,operator sdk 安装方法非常多,我们可以直接在 github 上面下载需要使用的版本,然后放置到 PATH 环境下面即可,当然也可以将源码 clone 到本地手动编译安装即可,如果你是 Mac,当然还可以使用常用的 brew 工具进行安装:

$ brew install operator-sdk
......
$ operator-sdk version
operator-sdk version: "v0.12.0", commit: "2445fcda834ca4b7cf0d6c38fba6317fb219b469", go version: "go1
.13.3 darwin/amd64"
$ go version
go version go1.13.3 darwin/amd64

我们这里使用的 sdk 版本是 v0.12.0,其他安装方法可以参考文档:https://github.com/operator-framework/operator-sdk/blob/master/doc/user/install-operator-sdk.md

创建项目

环境准备好了,接下来就可以使用 operator-sdk 直接创建一个新的项目了,命令格式为:operator-sdk new。

按照上面我们预先定义的 CRD 资源清单,我们这里可以这样创建:

# 创建项目目录
$ mkdir -p operator-learning && cd operator-learning
$ export GO111MODULE=on # 使用gomodules包管理工具
$ export GOPROXY="https://goproxy.io" # 使用包代理,加速
# 使用 sdk 创建一个名为 opdemo 的 operator 项目,如果在 GOPATH 之外需要指定 repo 参数
$ operator-sdk new opdemo --repo github.com/cnych/opdemo
INFO[0000] Creating new Go operator 'opdemo'.
INFO[0000] Created go.mod
INFO[0000] Created tools.go
INFO[0000] Created cmd/manager/main.go
INFO[0000] Created build/Dockerfile
INFO[0000] Created build/bin/entrypoint
INFO[0000] Created build/bin/user_setup
INFO[0000] Created deploy/service_account.yaml
INFO[0000] Created deploy/role.yaml
INFO[0000] Created deploy/role_binding.yaml
INFO[0000] Created deploy/operator.yaml
INFO[0000] Created pkg/apis/apis.go
INFO[0000] Created pkg/controller/controller.go
INFO[0000] Created version/version.go
INFO[0000] Created .gitignore
INFO[0000] Validating project
......
INFO[0063] Project validation successful.
INFO[0063] Project creation complete.
$ cd opdemo && tree -L 2
.
├── build
│ ├── Dockerfile
│ └── bin
├── cmd
│ └── manager
├── deploy
│ ├── operator.yaml
│ ├── role.yaml
│ ├── role_binding.yaml
│ └── service_account.yaml
├── go.mod
├── go.sum
├── pkg
│ ├── apis
│ └── controller
├── tools.go
└── version
└── version.go 9 directories, 9 files

到这里一个全新的 Operator 项目就新建完成了。

项目结构

使用 operator-sdk new 命令创建新的 Operator 项目后,项目目录就包含了很多生成的文件夹和文件。

  • go.mod go.sum — Go Modules 包管理清单,用来描述当前 Operator 的依赖包。
  • cmd - 包含 main.go 文件,使用 operator-sdk API 初始化和启动当前 Operator 的入口。
  • deploy - 包含一组用于在 Kubernetes 集群上进行部署的通用的 Kubernetes 资源清单文件。
  • pkg/apis - 包含定义的 API 和自定义资源(CRD)的目录树,这些文件允许 sdk 为 CRD 生成代码并注册对应的类型,以便正确解码自定义资源对象。
  • pkg/controller - 用于编写所有的操作业务逻辑的地方
  • version - 版本定义
  • build - Dockerfile 定义目录

我们主要需要编写的是 pkg 目录下面的 api 定义以及对应的 controller 实现。

添加 API

接下来为我们的自定义资源添加一个新的 API,按照上面我们预定义的资源清单文件,在 Operator 相关根目录下面执行如下命令:

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

添加完成后,我们可以看到类似于下面的这样项目结构:

添加控制器

上面我们添加自定义的 API,接下来可以添加对应的自定义 API 的具体实现 Controller,同样在项目根目录下面执行如下命令:

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

这样整个 Operator 项目的脚手架就已经搭建完成了,接下来就是具体的实现了。

自定义 API

打开源文件 pkg/apis/app/v1/appservice_types.go,需要我们根据我们的需求去自定义结构体 AppServiceSpec,我们最上面预定义的资源清单中就有 size、image、ports 这些属性,所有我们需要用到的属性都需要在这个结构体中进行定义:

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

代码中会涉及到一些包名的导入,由于包名较多,所以我们会使用一些别名进行区分,主要的包含下面几个:

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" 中定义的结构体,而且需要注意的是我们这里使用的是 ServicePort,而不是像传统的 Pod 中定义的 ContanerPort,这是因为我们的资源清单中不仅要描述容器的 Port,还要描述 Service 的 Port。

然后一个比较重要的结构体 AppServiceStatus 用来描述资源的状态,当然我们可以根据需要去自定义状态的描述,我这里就偷懒直接使用 Deployment 的状态了:

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

定义完成后,在项目根目录下面执行如下命令:

$ operator-sdk generate k8s
INFO[0000] Running deepcopy code-generation for Custom Resource group versions: [app:[v1], ]
INFO[0011] Code-generation complete.

该命令是用来根据我们自定义的 API 描述来自动生成一些代码,目录 pkg/apis/app/v1/ 下面以 zz_generated 开头的文件就是自动生成的代码,里面的内容并不需要我们去手动编写。

这样我们就算完成了对自定义资源对象的 API 的声明。

实现业务逻辑

上面 API 描述声明完成了,接下来就需要我们来进行具体的业务逻辑实现了,编写具体的 controller 实现,打开源文件pkg/controller/appservice/appservice_controller.go,需要我们去更改的地方也不是很多,核心的就是Reconcile 方法,该方法就是去不断的 watch 资源的状态,然后根据状态的不同去实现各种操作逻辑,核心代码如下:

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
} // 如果不存在,则创建关联资源
// 如果存在,判断是否需要更新
// 如果需要更新,则直接更新
// 如果不需要更新,则正常返回 deploy := &appsv1.Deployment{}
if err := r.client.Get(context.TODO(), request.NamespacedName, deploy); err != nil && errors.IsNotFound(err) {
// 创建关联资源
// 1. 创建 Deploy
deploy := resources.NewDeploy(instance)
if err := r.client.Create(context.TODO(), deploy); err != nil {
return reconcile.Result{}, err
}
// 2. 创建 Service
service := resources.NewService(instance)
if err := r.client.Create(context.TODO(), service); err != nil {
return reconcile.Result{}, err
}
// 3. 关联 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) {
// 更新关联资源
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 }

上面就是业务逻辑实现的核心代码,逻辑很简单,就是去判断资源是否存在,不存在,则直接创建新的资源,创建新的资源除了需要创建 Deployment 资源外,还需要创建 Service 资源对象,因为这就是我们的需求,当然你还可以自己去扩展,比如在创建一个 Ingress 对象。更新也是一样的,去对比新旧对象的声明是否一致,不一致则需要更新,同样的,两种资源都需要更新的。

另外两个核心的方法就是上面的 resources.NewDeploy(instance) 和 resources.NewService(instance) 方法,这两个方法实现逻辑也很简单,就是根据 CRD 中的声明去填充 Deployment 和 Service 资源对象的 Spec 对象即可。

NewDeploy 方法实现如下:

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 对应的方法实现如下:

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,
},
},
}
}

这样我们就实现了 AppService 这种资源对象的业务逻辑。

调试

如果我们本地有一个可以访问的 Kubernetes 集群,我们也可以直接进行调试,在本地用户 ~/.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 apply -f deploy/crds/app.example.com_appservices_crd.yaml
customresourcedefinition.apiextensions.k8s.io/appservices.app.example.com created
$ kubectl get crd |grep appservice
appservices.app.example.com 2019-12-19T04:31:12Z

当我们通过 kubectl get crd 命令获取到我们定义的 CRD 资源对象,就证明我们定义的 CRD 安装成功了。其实现在只是 CRD 的这个声明安装成功了,但是我们这个 CRD 的具体业务逻辑实现方式还在我们本地,并没有部署到集群之中,我们可以通过下面的命令来在本地项目中启动 Operator 的调试:

$ operator-sdk up local
operator-sdk up local
INFO[0000] Running the operator locally.
INFO[0000] Using namespace default.
{"level":"info","ts":1576730082.743257,"logger":"cmd","msg":"Operator Version: 0.0.1"}
{"level":"info","ts":1576730082.743318,"logger":"cmd","msg":"Go Version: go1.13.3"}
{"level":"info","ts":1576730082.743336,"logger":"cmd","msg":"Go OS/Arch: darwin/amd64"}
{"level":"info","ts":1576730082.7433379,"logger":"cmd","msg":"Version of operator-sdk: v0.12.0"}
{"level":"info","ts":1576730082.744948,"logger":"leader","msg":"Trying to become the leader."}
{"level":"info","ts":1576730082.744969,"logger":"leader","msg":"Skipping leader election; not running in a cluster."}
......

上面的命令会在本地运行 Operator 应用,通过 ~/.kube/config 去关联集群信息,现在我们去添加一个 AppService 类型的资源然后观察本地 Operator 的变化情况,资源清单文件就是我们上面预定义的(deploy/crds/app.example.com_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

直接创建这个资源对象:

$ kubectl apply -f deploy/crds/app.example.com_v1_appservice_cr.yaml
appservice.app.example.com/nginx-app created

我们可以看到我们的应用创建成功了,这个时候查看 Operator 的调试窗口会有如下的信息出现:

......
{"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"}
......

然后我们可以去查看集群中是否有符合我们预期的资源出现:

$ kubectl get AppService
NAME AGE
nginx-app 2m8s
$ kubectl get deploy
NAME READY UP-TO-DATE AVAILABLE AGE
nginx-app 2/2 2 2 2m20s
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx-app NodePort 10.111.179.0 <none> 80:30002/TCP 2m23s

看到了吧,我们定义了两个副本(size=2),这里就出现了两个 Pod,还有一个 NodePort=30002 的 Service 对象,我们可以通过该端口去访问下应用:

如果应用在安装过程中出现了任何问题,我们都可以通过本地的 Operator 调试窗口找到有用的信息,然后调试修改即可。

清理:

$ kubectl delete -f deploy/crds/app.example.com_v1_appservice_cr.yaml
$ kubectl delete -f deploy/crds/app.example.com_appservices_crd.yaml

部署

自定义的资源对象现在测试通过了,但是如果我们将本地的 operator-sdk up local 命令终止掉,我们可以猜想到就没办法处理 AppService 资源对象的一些操作了,所以我们需要将我们的业务逻辑实现部署到集群中去。

执行下面的命令构建 Operator 应用打包成 Docker 镜像:

$ operator-sdk build cnych/opdemo:0.0.1
......
Successfully built 29cd605c4ad2
Successfully tagged cnych/opdemo:0.0.1
INFO[0041] Operator build complete.

镜像构建成功后,推送到 docker hub:

$ docker push cnych/opdemo:0.0.1

镜像推送成功后,使用上面的镜像地址更新 Operator 的资源清单:

$ sed -i 's|REPLACE_IMAGE|cnych/opdemo:0.0.1|g' deploy/operator.yaml
# 如果你使用的是 Mac 系统,使用下面的命令
$ sed -i "" 's|REPLACE_IMAGE|cnych/opdemo:0.0.1|g' deploy/operator.yaml

现在 Operator 的资源清单文件准备好了,然后创建对应的 RBAC 的对象:

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

权限相关声明已经完成,接下来安装 CRD 和 Operator:

$ kubectl apply -f deploy/crds/app.example.com_appservices_crd.yaml
$ kubectl get crd |grep appservices
appservices.app.example.com 2019-12-19T05:25:55Z
......
# Deploy the Operator
$ kubectl apply -f deploy/operator.yaml
deployment.apps/opdemo created
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
opdemo-7565595bbc-p7f5c 1/1 Running 0 5s

到这里我们的 CRD 和 Operator 实现都已经安装成功了。

现在我们再来部署我们的 AppService 资源清单文件,现在的业务逻辑就会在上面的 opdemo-7565595bbc-p7f5c 的 Pod 中去处理了。

$ kubectl apply -f deploy/crds/app.example.com_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 22s
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-7565595bbc-p7f5c 1/1 Running 0 5m59s
$ kubectl describe appservice nginx-app
Name: nginx-app
Namespace: default
Labels: <none>
Annotations: kubectl.kubernetes.io/last-applied-configuration:
{"apiVersion":"app.example.com/v1","kind":"AppService","metadata":{"annotations":{},"name":"nginx-app","namespace":"default"},"spec":{"ima...
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-12-19T05:30:26Z
Generation: 2
Resource Version: 12364119
Self Link: /apis/app.example.com/v1/namespaces/default/appservices/nginx-app
UID: 1bb6d5fa-9f94-4eaf-ad55-643568690bab
Spec:
Image: nginx:1.7.9
Ports:
Node Port: 30002
Port: 80
Protocol: TCP
Target Port: 80
Resources:
Size: 2
Events: <none>

然后同样的可以通过 30002 这个 NodePort 端口去访问应用,到这里应用就部署成功了。

如果需要清理,则直接删除即可:

$ kubectl delete -f deploy/crds/app.example.com_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.example.com_appservices_crd.yaml

Kubernetes Operator: Operator的更多相关文章

  1. Spark in action on Kubernetes - Spark Operator的原理解析

    前言 在上篇文章中,向大家介绍了如何使用Spark Operator在kubernetes集群上面提交一个计算作业.今天我们会继续使用上篇文章中搭建的Playground进行调试与解析,帮助大家更深入 ...

  2. Kubernetes 实战-Operator Finalizers 实现

    原文链接:https://zdyxry.github.io/2019/09/13/Kubernetes-%E5%AE%9E%E6%88%98-Operator-Finalizers/ Finalize ...

  3. C++ new operator, delete operator, operator new, operator delete, new placement

    http://www.younfor.com/cpp-new-placement-new-operator-new.html http://www.cnblogs.com/luxiaoxun/arch ...

  4. 【转】C++易混知识点3. New Operator, Operator New, Placement New 实例分析,比较区别

    我们知道,C++中引入了New 这个内置符号,很大方便了指针的使用,程序员不必关注与这块堆上新分配的内存是如何来的,如何初始化的,然后如何转换为我们想要的类型指针的.现在,我们重点来分析下这个NEW内 ...

  5. C++内存管理(new operator/operator new/operator delete/placement new)

    new operator 我们平时使用的new是new操作符(new operator),就像sizeof一样是语言内置的,不能改变它的含义,功能也是一样的 比如: string *ps = new ...

  6. kubernetes之监控Operator部署Prometheus(三)

    第一章和第二章中我们配置Prometheus的成本非常高,而且也非常麻烦.但是我们要考虑Prometheus.AlertManager 这些组件服务本身的高可用的话,成本就更高了,当然我们也完全可以用 ...

  7. Kubernetes 监控方案之 Prometheus Operator(十九)

    目录 一.Prometheus 介绍 1.1.Prometheus 架构 1.2.Prometheus Operator 架构 二.Helm 安装部署 2.1.Helm 客户端安装 2.2.Tille ...

  8. 谈谈 Kubernetes Operator

    简介 你可能听过Kubernetes中Operator的概念,Operator可以帮助我们扩展Kubernetes功能,包括管理任何有状态应用程序.我们看到了它被用于有状态基础设施应用程序的许多可能性 ...

  9. Prometheus Operator 架构 - 每天5分钟玩转 Docker 容器技术(178)

    本节讨论 Prometheus Operator 的架构.因为 Prometheus Operator 是基于 Prometheus 的,我们需要先了解一下 Prometheus. Prometheu ...

随机推荐

  1. JavaScript知识梳理

    JS内功修炼 专业术语 类,封装,继承, 专业术语 babel 块级作用域 函数 扩展对象的功能性 解构 set和map js的类 改进的数组功能 Promise与异步编程 代理和反射 用模块封装代码 ...

  2. 【docker专栏6】详解docker容器状态转换管理命令

    docker容器有三种状态运行.停止.暂停,镜像可以创建.运行容器,镜像和容器也可以转换成tar压缩包进行存储.本文为大家介绍容器的状态转换命令及镜像创建运行容器.tar包导入导出相关的命令及使用场景 ...

  3. 0基础就可以上手的Spark脚本开发-for Java

    前言 最近由于工作需要,要分析大几百G的Nginx日志数据.之前也有过类似的需求,但那个时候数据量不多.一次只有几百兆,或者几个G.因为数据都在Hive里面,当时的做法是:把数据从Hive导到MySQ ...

  4. 简答一波 HashMap 常见八股面试题 —— 算法系列(2)

    请点赞,你的点赞对我意义重大,满足下我的虚荣心. Hi,我是小彭.本文已收录到 GitHub · Android-NoteBook 中.这里有 Android 进阶成长知识体系,有志同道合的朋友,关注 ...

  5. selenium环境配置和八大元素定位

    一.环境配置 1.selenium下载安装 安装一:pip install selenium(多数会超时安装失败) 安装二:pip install -i https://pypi.tuna.tsing ...

  6. 定时脚本删除docker容器中内容

    今天在我同步mongo数据库的时候,服务器的磁盘突然就被占满了导致同步中断,mongo容器也停止工作了.然后就想要弄一个能够定时清理同步过程中留存在docker容器中的mongo数据的脚本.话不多说, ...

  7. CSp2021/ NOIp2021 游记

    目录 Part 1. CSp2021 J S Part 2. NOIp2021 总结 Part 1. CSp2021 J 好水 挂飞 239pts,1= S 好毒瘤 rnm,写了个暴力就去搞 T2 \ ...

  8. JS for in / foreach / for of 超简单对照解释

    for in 可以遍历数组/对象/字符串/enumerable对象,得到的是索引,遍历对象时可以写这样 obj[index] 代表对象当前的属性foreach 只能遍历数组,不能遍历字符串.对象for ...

  9. 浅谈MySQL的sql_mode

    SQL mode 今天我们来分享一下MySQL的SQL mode , 这也是我们比较容易忽略的一点,我们在一开始安装数据库的时候其实就要先考虑要保留哪些SQL mode,去除哪些,合理的配置能够减少很 ...

  10. 慢SQL,压垮团队的最后一根稻草!

    一.什么是慢 SQL 什么是慢SQL?顾名思义,运行时间较长的 SQL 语句即为慢 SQL! 那问题来了,多久才算慢呢? 这个慢其实是一个相对值,不同的业务场景下,标准要求是不一样的. 我们都知道,我 ...