带你入门operator-framework


【编者的话】让你快速了解如何基于operator-framework开发自己的operator。

Banzai Cloud,我们一直在寻找新的创新技术,以支持我们的用户使用Pipeline过渡将微服务部署到Kubernetes。在最近几个月中,我们与CoreOSRedHat合作共同开发operators,并且它现已在GitHub上开源。通过阅读这篇文章,您将了解到什么是operator,如何使用这个operator sdk开发一个我们在Banzai Cloud开发和使用的具体operator示例。我们的GitHub上也有其他一些使用这个新的operators SDK开发的operator

tl;dr:

  • 本博客中讨论的operator可以为任何基于JVM的应用程序提供无缝监控,而无需应用程序其提供监控数据抓取接口


使用Kubernetes提供的对象接口在Kubernetes上部署和管理由多个相互依赖的组件/服务组成的复杂应用程序并不总是琐碎的。举一个简单的例子,如果应用程序需要提供最少数量的实例,这可以通过Kubernetes的部署来解决。但是如果这些实例当数量发生变化(升级/降级)且必须在运行时重新配置或重新初始化,那我们需要对这些事件做出反应并执行必要的重新配置步骤。尝试通过使用Kubernetes命令行工具实现的脚本来解决这些问题可能会非常麻烦,尤其是当我们必须处理弹性,日志收集和监控等更接近真实用例的问题的情况下。

CoreOS引入了operators来自动化处理这些复杂的操作场景。简而言之operators通过TPR/CRD扩展Kubernetes API,提供良好的粒度访问并控制集群内正在发生的事情。

在进一步讨论之前,了解一些关于Kubernetes custom resources(以下简写为CR)的内容以便我们更好地理解operator到底是什么。Kubernetes中的resourceKubernetes API中某存储一定的Kubernetes对象(例如Pod对象)类型的端点。一个CR本质上是一种资源,它可以被添加到Kubernetes中去扩展基本的Kubernetes API。当custom resources definition创建之后,用户可以像管理类似pods这种内置资源一样使用kubectl来管理这些对象资源。同时必须有一个控制器来处理通过kubectl创建的CR。自定义控制器是CR的控制器。总而言之,operator是一个可以处理某种类型的自定义资源的自定义控制器。

CoreOS还开发了用于开发这样的operators的SDK。SDK提供了高级API来编写操作逻辑,并为其生成框架以帮助开发人员避免编写样板代码,大大简化了operator的实现。

我们来看看如何使用这个Operator SDK

首先,我们需要将Operator SDK安装到我们的开发机器上。如果您想获取最新最好的体验,请从master分支安装CLI。当CLI安装完成之后,开发流程如下:

创建一个新的operator项目

使用CLI工具创建一个新的operator项目。
$ cd $GOPATH/src/github.com/<your-github-repo>/
$ operator-sdk new <operator-project-name> --api-version=<your-api-group>/<version> --kind=<custom-resource-kind>
$ cd <operator-project-name>

  • operator-project-name - CLI在该目录下生成项目框架
  • your-api-group - 这是我们的operator处理的自定义资源的Kubernetes API组(例如mycompany.com)
  • version - 这是operator处理的自定义资源的Kubernetes API版本(例如v1alpha,beta等,请参阅Kubernetes API版本控制)
  • custom-resource-kind - 自定义资源类型的名称


定义要WatchList的Kubernetes资源(持续监听事件)

cmd/&lt;operator-project-name>目录下的main.go为启动和初始化operator的主入口。这个文件是配置operator想要从Kubernetes中获取关于某类资源类型列表的通知的地方。

在指定的方法函数中定义operator的处理逻辑

从Kubernetes获取到与被观察资源相关的事件将被引导到定义在pkg/stub/handler.go中的func (h *Handler) Handle(ctx types.Context, event types.Event) error函数。在这个函数里实现您的operator的处理逻辑,它负责对由Kubernetes发布的各种事件做出反应。

每个自定义资源都需要定义结构体,我们创建的operator处理的自定义资源的结构必须定义在pkg/apis/&lt;api-group>/&lt;version>目录下的types.go文件中。我们可以在Spec字段来定义规范自定义资源的结构属性,还有一个Status字段用于补充描述自定义资源对象状态的信息。

Operator SDK提供了对Kubernetes资源执行CRUD操作函数:
  • 查询 - 定义用于检索群集中可用的Kubernetes资源的函数
  • 操作 - 定义用于创建,更新和删除Kubernetes资源的函数


有关如何使用这些函数的更多详细信息,请参阅下面的具体operator示例。

更新并生成自定义资源的代码

每当types.go文件有变更时,因为有一些代码文件依赖于types.go中定义的类型,所以它们需要重新生成。
$ operator-sdk generate k8s


构建并生成operator的部署清单文件

构建operator并生成部署文件。
operator-sdk build <your-docker-image>


执行完以上命令后,一个包含operator二进制执行文件的docker镜像将被构建,我们需要将它推送到镜像仓库。
同时在deploy目录下会生成用于创建自定义资源和部署operator的部署文件。
  • operator.yml - CRD以及operator的Deployment,每当operator-sdk build &lt;your-docker-image>命令执行时,该文件的任何更改都会被覆盖
  • cr.yaml - 一个自定义资源的例子
  • rbac.yaml - 当集群启用了rbac的话需要配置权限


部署operator

$ kubectl create -f deploy/rbac.yaml
$ kubectl create -f deploy/operator.yaml


创建自定义资源

一旦operator处于Running状态,您就可以开始创建您的自定义资源对象。在deploy/cr.yamlspec部分填写您想要传递给operator的数据。spec结构必须符合types.go文件中的Spec字段结构。
$ kubectl create -f deploy/cr.yaml


查看群集中的自定义资源对象:
$ kubectl get <custom-resource-kind>


查看指定的自定义资源实例:
$ kubectl get <custom-resource-kind> <custom-resource-object-name>


Prometheus JMX Exporter

我们的PaaS Pipeline将应用程序部署到Kubernetes集群,并提供企业功能(如监控,集中式日志记录等)。

我们使用Prometheus作为监控工具从我们部署的应用程序收集指标。如果您对我们为何选择Prometheus的原因感兴趣,请阅读我们的博客中的监控系列文章

应用程序可不会自行向Prometheus发布度量指标,因此我们面临这样的问题:我们可以做些什么来启用这些应用程序向Prometheus发布指标(指开箱即用,毋需其他改造)。为Java应用程序编写的便捷组件Prometheus JMX Exporter可以通过JMX从mBeans查询数据,并以Prometheus所需的格式暴露出这些数据。

有以下几点要求:
  • 识别运行Java应用程序的pod,这些应用程序本身不会为Prometheus发布度量指标
  • 将Prometheus JMX Exporter Java代理注入到应用程序中以暴露指标
  • 为Prometheus JMX Exporter Java代理提供配置,指定哪些指标需要暴露出来
  • 使Prometheus服务器能够自动识别可从中获取指标的端点
  • 这些操作无侵入性(不应该重新启动pod)


为了达到上面列出的要求,我们将执行相当多的操作,因此我们决定为它实现一个operator,接下来将让你了解我们是如何实现它的。

只有在JVM启动时,Prometheus JMX Exporter才能被加载到Java进程中。令人高兴的是,只需要做一些小小的改变就可以将其加载到已经运行的Java进程中。你可以看看我们的jmx_exporter fork中的改动。

我们需要一个加载器将JMX exporter Java代理加载到由PID标识的正在运行的Java进程中。该加载器是一个相当小的应用程序,其源代码在这里

Prometheus JMX Exporter需要传入配置。我们将它的配置保存到Kubernetes的configmap中。

types.go中定义我们的operator:

type PrometheusJmxExporter struct {
    metav1.TypeMeta   `json:",inline"`
    metav1.ObjectMeta `json:"metadata"`
    Spec              PrometheusJmxExporterSpec   `json:"spec"`
    Status            PrometheusJmxExporterStatus `json:"status,omitempty"`
}

type PrometheusJmxExporterSpec struct {
    LabelSelector map[string]string `json:"labelSelector,required"`
    Config        struct {
        ConfigMapName string `json:"configMapName,required"`
        ConfigMapKey  string `json:"configMapKey,required"`
    } `json:"config"`
    Port int `json: port,required`
}

  • LabelSelector - 指定标签以选择pod
  • ConfigMapNameConfigMapKey - 包含Prometheus JMX Exporter配置的configmap
  • Port - 暴露给Prometheus服务器抓取指标的端口号


创建自定义资源对象的示例yaml文件:
apiVersion: "banzaicloud.com/v1alpha1"
kind: "PrometheusJmxExporter"
metadata:
  name: "example-prom-jmx-exp"
spec:
  labelSelector:
    app: dummyapp
  config:
    configMapName: prometheus-jmx-exporter-config
    configMapKey: config.yaml
  port: 9400


自定义资源配置包含了operator需要根据labelSelector来处理哪些pod,用于暴露度量指标监听的端口以及用于运行exporter所需配置存在的configmap的信息。

PrometheusJmxExporter自定义资源对象的状态应列出基于其配置创建的度量标准端点,因此Status字段的结构为:
type PrometheusJmxExporterStatus struct {
    MetricsEndpoints []*MetricsEndpoint `json: metricsEndpoints,omitempty`
}

type MetricsEndpoint struct {
    Pod  string `json:"pod,required"`
    Port int    `json:"port,required"`
}


Operator必须对与PrometheusJmxExporter自定义资源和Pod有关的事件做出相应操作,因此必须在这些类型的资源上设置监听(main.go):
func main() {
   ...
    namespace := os.Getenv("OPERATOR_NAMESPACE")
    sdk.Watch("banzaicloud.com/v1alpha1", "PrometheusJmxExporter", namespace, 0)
    sdk.Watch("v1", "Pod", namespace, 0)
    ...
}


处理与PrometheusJmxExporter自定义资源和Pod相关的事件的处理方法在handler.go以下位置定义:
func (h *Handler) Handle(ctx types.Context, event types.Event) error {
    switch o := event.Object.(type) {
    case *v1alpha1.PrometheusJmxExporter:
        prometheusJmxExporter := o
    ...
    ...
   case *v1.Pod:
        pod := o
    ...
    ...
}


PrometheusJmxExporter自定义资源对象被创建/更新时,operator将会执行以下:
  1. 查询当前命名空间中所有标签与PrometheusJmxExporter自定义资源对象规范的labelSelector相匹配的所有pods。
  2. 验证哪些pods已被处理以跳过
  3. 处理未处理的pods
  4. 使用新创建的指标端点更新当前PrometheusJmxExporter自定义资源的状态


当创建/更新/删除Pod时,operator将会执行以下:
  1. 搜索其中labelSelector与pod匹配的PrometheusJmxExporter自定义资源对象
  2. 如果找到PrometheusJmxExporter自定义资源对象,则继续处理该pod
  3. 用新创建的指标端点更新PrometheusJmxExporter自定义资源的状态


为了查询Kubernetes资源,我们使用Operator SDK中的query包,例如:
podList := v1.PodList{
    TypeMeta: metav1.TypeMeta{
        Kind:       "Pod",
        APIVersion: "v1",
    },
}

listOptions := query.WithListOptions(&metav1.ListOptions{
    LabelSelector:        labelSelector,
    IncludeUninitialized: false,
})

err := query.List(namespace, &podList, listOptions)
if err != nil {
    logrus.Errorf("Failed to query pods : %v", err)
    return nil, err
}

jmxExporterList := v1alpha1.PrometheusJmxExporterList{
   TypeMeta: metav1.TypeMeta{
        Kind:       "PrometheusJmxExporter",
        APIVersion: "banzaicloud.com/v1alpha1",
    },
}

listOptions := query.WithListOptions(&metav1.ListOptions{
    IncludeUninitialized: false,
})

if err := query.List(namespace, &jmxExporterList, listOptions); err != nil {
    logrus.Errorf("Failed to query prometheusjmxexporters : %v", err)
    return nil, err
}


为了更新Kubernetes资源,我们使用Operator SDK中的action包,例如:
// update status
newStatus := createPrometheusJmxExporterStatus(podList.Items)

if !prometheusJmxExporter.Status.Equals(newStatus) {
    prometheusJmxExporter.Status = createPrometheusJmxExporterStatus(podList.Items)

    logrus.Infof(
        "PrometheusJmxExporter: '%s/%s' : Update status",
        prometheusJmxExporter.Namespace,
        prometheusJmxExporter.Name)

   action.Update(prometheusJmxExporter)
}


处理pod过程由以下步骤组成:
  1. 在pod的容器内执行jps以获取java进程的PID
  2. Prometheus JMX Exporterjava agent loader包复制到那些匹配的存在Java进程的容器中
  3. 从configmap中读取exporter的配置,并将其作为配置文件复制到容器中
  4. 在容器内运行agent loader以将exporter加载到Java进程中
  5. 添加port到容器的暴露端口列表中,Prometheus服务器将能够抓取该端口
  6. 给这个pod添加prometheus.io/scrapeprometheus.io/port注解,Prometheus将抓取包含这些注解的pod
  7. 使用注解标记该pod已成功处理。


由于Kubernetes API不支持在容器内直接执行命令,因此我们参考了kubectl exec以及kubectl cp的方法实现。

以上Prometheus JMX Exporter operator源代码已提供在GitHub

另外一篇关于Prometheus JMX Exporter operator的实现。

如果您对我们的技术和开源项目感兴趣,请在GitHub,LinkedIn或Twitter上关注我们:

原文链接 (翻译:fengxsong)

译者介绍

fengxsong,运维工程师,Golang语言爱好者,关注Kubernetes/Serverless/CI/CD,希望通过DockOne把最新的译文贡献给大家,与读者一起共同学习交流Cloud-Native Solutions。

0 个评论

要回复文章请先登录注册