起因

看到 k8s 生态中越来越多的 operator,是不是也想自己写一个?

在使用 k8s 部署业务应用时,是不是每一次都要 编写 Deployment,Service,Ingress?能不能只写一个文件就把这三种资源都部署起来呢?

大家可能想到使用 helm,helm 是个不错的东西。但我们不使用 helm,而是使用go编写类似于 k8s 内置资源的控制器。采取 crd 与 controller 来控制部署自定义资源。如果从头开始写控制器,则非常繁琐复杂。 coreOS 团队开源了一个很好的脚手架工具 operator-sdk,我们只要按照要求编写 crd 结构体和一致性逻辑即可。极大地降低了编写 controller 的难度。

operator-sdk 介绍

在了解 operator-sdk 之前,先了解 crd 和 operator

CustomResourceDefinition

在 Kubernetes 中一切都可视为资源,Kubernetes 1.7 之后增加了对 CRD 自定义资源二次开发能力来扩展 Kubernetes API,通过 CRD 我们可以向 Kubernetes API 中增加新资源类型,而不需要修改 Kubernetes 源码来创建自定义的 API server,该功能大大提高了 Kubernetes 的扩展能力。
当你创建一个新的 CustomResourceDefinition (CRD)时,Kubernetes API 服务器将为你指定的每个版本创建一个新的 RESTful 资源路径,我们可以根据该 api 路径来创建一些我们自己定义的类型资源。CRD 可以是命名空间的,也可以是集群范围的,由 CRD 的作用域(scpoe)字段中所指定的,与现有的内置对象一样,删除名称空间将删除该名称空间中的所有自定义对象。customresourcedefinition 本身没有名称空间,所有名称空间都可以使用。

Operator

Operator 模式旨在捕获(正在管理一个或一组服务的)运维人员的关键目标。 负责特定应用和 service 的运维人员,在系统应该如何运行、如何部署以及出现问题时如何处理等方面有深入的了解。在 Kubernetes 上运行工作负载的人们都喜欢通过自动化来处理重复的任务。Operator 模式会封装您编写的(Kubernetes 本身提供功能以外的)任务自动化代码。Kubernetes 控制器 使您无需修改 Kubernetes 自身的代码,即可以扩展集群的行为。 Operator 是 Kubernetes API 的客户端,充当 自定义资源的控制器。

operator 可以由任何能够实现 http 的语言实现,所以是语言无关的,但是使用 golang 则更加接近 k8s 原生。

operator-sdk

什么是 Opearator SDK,为什么要使用它?

该项目是 Operator Framework 的组成部分,Operator Framework 是一个开放源代码工具包,用于以有效,自动化和可扩展的方式管理称为 Kubernetes 的本机应用程序。在简介博客文章中了解更多信息。

Operator 可以轻松地在 Kubernetes 上管理复杂的有状态应用程序。但是,由于诸如使用低级 API,编写样板以及缺少导致重复的模块化等挑战,今天编写 Operator 可能会很困难。

Operator SDK 是一个框架,该框架使用控制器运行时库通过提供以下功能使编写操作员更加容易:

  • 高级 API 和抽象,可以更直观地编写操作逻辑
  • 脚手架和代码生成工具,可快速引导新项目
  • 扩展以涵盖常见的 Operator 用例

阅读前提

  1. 熟悉 k8s 相关概念,编写 k8s 各类 yaml 文件
  2. 本地已有一个 k8s 集群
  3. git
  4. mercurial version 3.9+
  5. bazaar version 2.7.0+
  6. go version v1.13+.

安装

git clone https://github.com/operator-framework/operator-sdk
cd operator-sdk
git checkout master
make tidy
make install
cp $GOPATH/bin/operator-sdk /usr/local/bin/

使用

初始化 project

cd $GOPATH/src
operator-sdk init --domain=bryce-huang.club
# 表示初始化一个operator --domain 表示api的域名, --repo表示拉取代码
cd app-operator

创建 api,生成自定义文件接口

operator-sdk create api --group k8s  --version v1 --kind App --resource=true --controller=true
#创建一个简单的api

编写结构体

在 api/v1/app_types.go 中主要编写 AppSpec 和 AppStatus 两个结构体,对应 k8s 资源中的 spec 和 status

具体代码如下:

// AppSpec defines the desired state of App
type AppSpec struct {

//容器及服务的端口

Port int32 `json:"svcPort"`
//镜像名称
Image string `json:"image"`
// 副本数量
Replicas *int32 `json:"replicas"`
//ingress 域名
Host string `json:"host"`
// ingress 访问上下文,如:/xxx
Context string `json:"context"`
}
type AppStatus struct {
 // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
 // Important: Run "make" to regenerate code after modifying this file
 DeployStatus  appsv1.DeploymentStatus `json:"deployStatus,omitempty"`
 SvcStatus     corev1.ServiceStatus    `json:"svcStatus,omitempty"`
 IngressStatus extv1.IngressStatus     `json:"ingressStatus,omitempty"`
 Pods          []Pod                   `json:"pods"`
}


// +kubebuilder:object:root=true
// +kubebuilder:subresource:status

// App is the Schema for the apps API
type App struct {
 metav1.TypeMeta   `json:",inline"`
 metav1.ObjectMeta `json:"metadata,omitempty"`
 Spec              AppSpec   `json:"spec,omitempty"`
 Status            AppStatus `json:"status,omitempty"`
}

编写循环逻辑

定义好数据结构,就可以根据数据结构来编写逻辑了
在 controllers/app_controller.go 中可以看到 Reconcile 和 SetupWithManager 两个方法,前者是定义资源一致性逻辑而后者则是定义需要监听哪些资源,当监听的资源发生变化时,则调用 Reconcile,执行逻辑。

func (r *AppReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
  // write your logic


ctx := context.Background()
log := r.Log.WithValues("app", req.NamespacedName)

//

app := &k8sv1.App{}

err := r.Client.Get(ctx, req.NamespacedName, app)

if err != nil {
  if errors.IsNotFound(err) {
    log.Info("App resource not found. Ignoring since object must be deleted")
    return ctrl.Result{}, nil
  }
  log.Error(err, "Failed to get App")
  return ctrl.Result{}, err
}

// find deployment
fd := &appsv1.Deployment{}
err = r.Get(ctx, types.NamespacedName{Name: app.Name, Namespace: app.Namespace}, fd)
if err != nil && errors.IsNotFound(err) {
  fd = r.deployment(app)
  log.Info("Creating a new Deployment", "Deployment.Namespace", fd.Namespace, "Deployment.Name", fd.Name)
  err = r.Create(ctx, fd)
  if err != nil {
    log.Error(err, "Failed to create new Deployment", "Deployment.Namespace", fd.Namespace, "Deployment.Name", fd.Name)
    return ctrl.Result{}, err
  }
  return ctrl.Result{Requeue: true}, nil
} else if err != nil {
  log.Error(err, "Failed to get Deployment")
  return ctrl.Result{}, err
}

// check images
image := app.Spec.Image
fImage := fd.Spec.Template.Spec.Containers[0].Image
if fImage != image {
  fd.Spec.Template.Spec.Containers[0].Image = image
  err = r.Update(ctx, fd)
  if err != nil {
    log.Error(err, "Failed to update Deployment image", "Deployment.Namespace", fd.Namespace, "Deployment.Name", fd.Name)
    return ctrl.Result{}, err
  }
  // Spec updated - return and requeue
  return ctrl.Result{Requeue: true}, nil
}
// check replicas

if *fd.Spec.Replicas != *app.Spec.Replicas {
  fd.Spec.Replicas = app.Spec.Replicas
  err = r.Update(ctx, fd)
  if err != nil {
    log.Error(err, "Failed to update Deployment", "Deployment.Namespace", fd.Namespace, "Deployment.Name", fd.Name)
    return ctrl.Result{}, err
  }
  // Spec updated - return and requeue
  return ctrl.Result{Requeue: true}, nil
}

// find service
fs := &corev1.Service{}

err = r.Get(ctx, types.NamespacedName{Name: app.Name, Namespace: app.Namespace}, fs)
if err != nil && errors.IsNotFound(err) {
  fs = r.service(app)
  log.Info("Creating a new Service", "Service.Namespace", fs.Namespace, "Service.Name", fs.Name)
  err = r.Create(ctx, fs)
  if err != nil {
    log.Error(err, "Failed to create new Service", "Service.Namespace", fs.Namespace, "Service.Name", fs.Name)
    return ctrl.Result{}, err
  }
  return ctrl.Result{Requeue: true}, nil
} else if err != nil {
  log.Error(err, "Failed to get Service")
  return ctrl.Result{}, err
}

  // find ingress
fi := &extv1.Ingress{}

err = r.Get(ctx, types.NamespacedName{Name: app.Name, Namespace: app.Namespace}, fi)
if err != nil && errors.IsNotFound(err) {
  fi = r.ingress(app)
  log.Info("Creating a new Ingress", "Ingress.Namespace", fi.Namespace, "Ingress.Name", fi.Name)
  err = r.Create(ctx, fi)
  if err != nil {
    log.Error(err, "Failed to create new Ingress", "Ingress.Namespace", fi.Namespace, "Ingress.Name", fi.Name)
    return ctrl.Result{}, err
  }
  return ctrl.Result{Requeue: true}, nil
} else if err != nil {
  log.Error(err, "Failed to get Ingress")
  return ctrl.Result{}, err
}

  return ctrl.Result{}, nil
}

func (r *AppReconciler) SetupWithManager(mgr ctrl.Manager) error {
 return ctrl.NewControllerManagedBy(mgr).
        For(&k8sv1.App{}).
        Owns(&appsv1.Deployment{}).
        Owns(&corev1.Service{}).
        Owns(&extv1.Ingress{}).
        Complete(r)
}

在样例中主要定义了 Deployment、Service 和 Ingress,详细代码逻辑如下:


// 构造一个deployment
func (r *AppReconciler) deployment(app *k8sv1.App) *appsv1.Deployment {
ls := labelsForApp(app.Name, "deployment")
deploy := &appsv1.Deployment{
  ObjectMeta: metav1.ObjectMeta{
    Name:      app.Name,
    Namespace: app.Namespace,
  },
  Spec: appsv1.DeploymentSpec{
    Replicas: app.Spec.Replicas,
    Selector: &metav1.LabelSelector{
      MatchLabels: ls,
    },
    Template: corev1.PodTemplateSpec{
      ObjectMeta: metav1.ObjectMeta{
        Labels: ls,
      },
      Spec: corev1.PodSpec{
        Containers: []corev1.Container{
          {
            Image: app.Spec.Image,
            Name:  app.Name,
            Ports: []corev1.ContainerPort{
              {
                ContainerPort: app.Spec.Port,
                Name:          app.Name,
              },
            },
          },
        },
      },
    },
  },
}
deploy.SetLabels(ls)
_ = ctrl.SetControllerReference(app, deploy, r.Scheme)
return deploy
}

// 构造一个Service
func (r *AppReconciler) service(app *k8sv1.App) *corev1.Service {
ls := labelsForApp(app.Name, "service")

svc := &corev1.Service{
  ObjectMeta: metav1.ObjectMeta{
    Name:      app.Name,
    Namespace: app.Namespace,
  },
  Spec: corev1.ServiceSpec{
    Ports: []corev1.ServicePort{
      {
        Port:       app.Spec.Port,
        Protocol:   corev1.ProtocolTCP,
        TargetPort: intstr.IntOrString{Type: 0, IntVal: app.Spec.Port},
        Name:       app.Name,
      },
    },
    Selector:        map[string]string{"app": app.Name},
    SessionAffinity: corev1.ServiceAffinityNone,
    Type:            corev1.ServiceTypeClusterIP,
  },
}
svc.SetLabels(ls)
_ = ctrl.SetControllerReference(app, svc, r.Scheme)
return svc

}

// 构建一个ingress
func (r *AppReconciler) ingress(app *k8sv1.App) *extv1.Ingress {
ingress := &extv1.Ingress{
  ObjectMeta: metav1.ObjectMeta{
    Namespace: app.Namespace,
    Name:      app.Name,
    Annotations: map[string]string{
      "nginx.ingress.kubernetes.io/rewrite-target": "/",
    },
  },
  Spec: extv1.IngressSpec{
    Rules: []extv1.IngressRule{
      {
        Host: app.Spec.Host,
        IngressRuleValue: extv1.IngressRuleValue{
          HTTP: &extv1.HTTPIngressRuleValue{
            Paths: []extv1.HTTPIngressPath{
              {
                Path: app.Spec.Context,
                Backend: extv1.IngressBackend{
                  ServicePort: intstr.IntOrString{Type: 0, IntVal: app.Spec.Port},
                  ServiceName: app.Name,
                },
              },
            },
          },
        },
      },
    },
  },
}
ls := labelsForApp(app.Name, "ingress")
ingress.SetLabels(ls)
_ = ctrl.SetControllerReference(app, ingress, r.Scheme)
return ingress
}

这一步主要根据数据结构,生成自己想要的 deployment、Service 和 Ingress

生成 crd

根据数据结构自动生成 crd 文件,文件在 config/crd 目录下

make generate

make manifests

本地 k8s 集群安装 crd 并启动 operator

make install
make run ENABLE_WEBHOOKS=false

编写 一个 App CR

在 config/samples 目下找到 k8s_v1_app.yaml,填写如下内容:

apiVersion: k8s.bryce-huang.club/v1
kind: App
metadata:
  name: app-sample
spec:
  image: nginx
  replicas: 3
  context: /
  host: bryce.club
  svcPort: 80

创建 App 资源

使用: kubectl apply -f config/samples/k8s_v1_app.yaml 创建自定义资源

通过kubectl get pod,svc,ingress -l app=app-sample

然后看到 operator 已经自动创建了 pod 和 service 和 ingress

NAME                              READY   STATUS    RESTARTS   AGE
pod/app-sample-559f49f8d7-hzdmg   1/1     Running   0          4m45s
pod/app-sample-559f49f8d7-qthf5   1/1     Running   0          4m45s
pod/app-sample-559f49f8d7-sv894   1/1     Running   0          4m45s

NAME                 TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
service/app-sample   ClusterIP   10.99.211.244   <none>        80/TCP    4m44s

NAME                            HOSTS        ADDRESS     PORTS   AGE
ingress.extensions/app-sample   bryce.club   localhost   80      4m44s

然后通过 echo "127.0.0.1 bryce.club" >> /etc/hosts就可以通过 bryce.club 访问 nginx 的欢迎页面了

发布 Operator

我们往往需要将 operator 提供给他人使用,可以执行下面的命令,构建并推送到 dockerhub

构建镜像

make docker-build IMG=docker.io/brycehuang/app-operator:v1

推送镜像

make docker-push IMG=docker.io/brycehuang/app-operator:v1

部署镜像

cd config/default/ && kustomize edit set namespace "default" && cd ../..
make deploy IMG=docker.io/brycehuang/app-operator:v1

创建 app crd

kubectl apply -f config/samples/k8s_v1_app.yaml

清理

kubectl delete -f config/samples/ config/samples/k8s_v1_app.yaml
kubectl delete deployments,service -l control-plane=controller-manager
kubectl delete role,rolebinding --all

参考资料

app-operator 完整代码

operator-sdk 官方文档


本博客所有文章除特别声明外,均采用: 署名-非商业性使用-禁止演绎 4.0 国际协议,转载请保留原文链接及作者。

使用Java操作k8s和registry 上一篇
mysql基础知识 下一篇

 目录


买个卤蛋,吃根冰棒