Bootstrap

如何基于 OAM 编写一个扩展 Trait?

此文中些许部分已重构,更新版本:

1. 背景

OAM 是阿里云与微软云在 2019 年末联合推出的标准化云原生应用管理模型。相比于传统 PaaS 封闭、不能同“以 Operator 为基础的云原生生态”衔接的现状,基于 OAM 和 Kubernetes 构建的现代云原生应用管理平台,本质上是一个“以应用为中心”的 Kubernetes ,保证了这个应用平台在能够无缝接入整个云原生生态。同时,OAM 可以进一步屏蔽掉容器基础设施的复杂性和差异性,为平台的使用者带来低心智负担的、标准化的、一致的应用管理与交付体验。

来源:

在 OAM 中,一个应用程序包含三个核心理念。

  • 第一个核心理念是组成应用程序的组件(Component),它可能包含微服务集合、数据库和云负载均衡器;

  • 第二个核心理念是描述应用程序运维特征(Trait)的集合,例如,弹性伸缩和 Ingress 等功能。它们对应用程序的运行至关重要,但在不同环境中其实现方式各不相同;

  • 最后,为了将这些描述转化为具体的应用程序,运维人员使用应用配置(Application Configuration)来组合组件和相应的特征,以构建应部署的应用程序的具体】实例。

来源:

1.1 Workload

Workload 并不是一个实例,而是定义了应用程序能够使用的 Component 类型:如何运行 Component,以及它的运行内容。

Workload 既可以是根据 OAM 规范定义的类型,如 OAM core workloads:ContainerizedWorkload,参考 ;也可以复用 K8S 原生的资源,如直接使用 StatefulSet,参考

1.2 Trait

Trait 所代表的是运维特征,可以将多种 Trait 自由组合并绑定在 Component 上,为应用程序扩展运维能力。

Trait 对 Workload 资源的操作方式主要分为两类,一类是直接操作 Workload 或者其生成的下层资源的字段,如修改资源的 ,参考 中的 ManualScalerTrait;另一类是创建一个独立的资源,如为资源创建一个 K8s Service,参考

1.3 Workload 与 Trait 交互

在 OAM 中,Workload 和 Trait 都以 CR(custom resource)的方式独立存在,非常方便扩展,那么 Trait 是如何知道与之绑定的 Workload 的呢?

Application Configuration 是组合组件和相应运维特征的地方,Workload 与 Trait 的交互就在其控制器逻辑中。Application Configuration 中储存了 ComponentName、Workload、Traits 的信息,它会将 Workload 的信息依次添加到各个 Trait 中,通过在 Trait 中指定 字段来绑定。由此 Trait 便知晓了与之绑定的 Workload 信息。

2. 使用 kubebuilder 构建 OAM 扩展 Trait

上文中简单介绍了 OAM 的两种主要资源类型:Workload 和 Trait ,并简单介绍了 Workload 与 Trait 之间的交互逻辑。

众所周知,掌握 CRD 是成为 Kubernetes 高级玩家的必备技能,而编写 OAM 扩展 Trait 的主要方式同样也是编写 CRD controller。所以接下来将介绍如何使用 CRD 编写框架 kubebuilder 来实现自定义 CRD 和 Controller,并重点讲解 Trait 的内部逻辑编写。

首先,你需要安装 kubebuilder,参照网址:

2.1 构建项目

创建一个目录,并用 命令初始化一个新项目。

mkdir $GOPATH/src/cronjob
cd $GOPATH/src/cronjob
kubebuilder init --domain tutorial.kubebuilder.io

2.2 创建API

使用 命令创建一个新的 API,注意指定 GVK(group/version/kind)。

kubebuilder create api --group batch --version v1 --kind CronJob

由此两步便已成功构建 CRD 和 Controller 的模板:

2.3 编码

kubebuilder 已经为我们生成了较为完整的框架,我们主要编辑 和 两个文件,来自定义 CRD 和 Controller 逻辑。

具体 CRD 定义和逻辑编写以及注意点参考下文3。

2.4 安装并运行

编写好逻辑后,使用以下命令安装并运行 CRD 和 Controller:

make install
make run

运行成功后,可编写一个 example 用于测试:

kubectl apply -f config/samples/batch_v1_cronjob.yaml

2.5 构建并部署

命令来用于测试,真正将 Controller 部署到集群中需要构建镜像并部署:

make docker-build docker-push IMG=/:tag
make deploy IMG=/:tag

3. 编写 Workload 与 Trait

由于 Workload 与 Trait 的 CRD 和 Controller 的编写有相通之处,所以此处以 为例,重点介绍如何为 OAM Trait 自定义 CRD,Controller 逻辑以及一些注意事项。

servicetrait_types.go

  • :此字段用于反应资源在集群中的观察状态。

  • :此字段定义资源的 APIVersion、Kind、Name、UID。

type ServiceTraitStatus struct {
	runtimev1alpha1.ConditionedStatus `json:",inline"`

	// Resources managed by this service trait
	Resources []runtimev1alpha1.TypedReference `json:"resources,omitempty"`
}

此外,还需要编写 Conditions 相关的方法,否则无法获取或设定资源在集群中的观察状态:

var _ oam.Trait = &ServiceTrait{}

func (tr *ServiceTrait) GetCondition(ct runtimev1alpha1.ConditionType) runtimev1alpha1.Condition {
	return tr.Status.GetCondition(ct)
}

func (tr *ServiceTrait) SetConditions(c ...runtimev1alpha1.Condition) {
	tr.Status.SetConditions(c...)
}

func (tr *ServiceTrait) GetWorkloadReference() runtimev1alpha1.TypedReference {
	return tr.Spec.WorkloadReference
}

func (tr *ServiceTrait) SetWorkloadReference(r runtimev1alpha1.TypedReference) {
	tr.Spec.WorkloadReference = r
}

  • 必须设置,是储存需要扩展的 Workload 信息的地方。

  • 根据自己的需求自定义字段,示例设置的 Template 为 K8S 原生的 ServiceSpec。

type ServiceTraitSpec struct {
	// K8S native ServiceSpec
	Template corev1.ServiceSpec `json:"template,omitempty"`

	// WorkloadReference to the workload this trait applies to.
	WorkloadReference runtimev1alpha1.TypedReference `json:"workloadRef"`
}

// +kubebuilder:resource:categories={crossplane,oam}
// +kubebuilder:subresource:status

同样在 Workload 的 type.go 文件中,需要定义 WorkloadStatus 的两个字段: 和 ;编写 和 方法;添加 kubebuilder 选项;而 WorkloadSpec 只需根据需求自定义字段即可。

servicetrait_controller.go

Trait 的控制逻辑都在 Controller 的 函数中实现即可。

声明 变量,通过 获取需要调谐的 trait 对象:

var trait corev1alpha2.ServiceTrait
if err := r.Get(ctx, req.NamespacedName, &trait); err != nil { ... }

根据获取到的 trait 对象,去获取其引用的 workload 对象:

workload, result, err := r.fetchWorkload(ctx, log, &trait)

具体 函数:

  • 声明 变量。

  • 根据 trait 对象的 方法获取其引用的 workload 对象信息:APIVersion、Kind、Name、UID。

  • 用 生成的 去获取集群中的 workload 对象并返回。

func (r *ServiceTraitReconciler) fetchWorkload(ctx context.Context, log logr.Logger,
	oamTrait oam.Trait) (*unstructured.Unstructured, ctrl.Result, error) {
	var workload unstructured.Unstructured
	workload.SetAPIVersion(oamTrait.GetWorkloadReference().APIVersion)
	workload.SetKind(oamTrait.GetWorkloadReference().Kind)
	wn := client.ObjectKey{Name: oamTrait.GetWorkloadReference().Name, Namespace: oamTrait.GetNamespace()}
  if err := r.Get(ctx, wn, &workload); err != nil { ... }
	...
}

首先需要确定 workload 对象的类型,若是自定义的 OAM workload,则其子资源才是我们需要的目标资源对象;若是 K8S CR,则 workload 所代表的资源就是我们需要的目标资源对象。

resources, err := DetermineWorkloadType(ctx, log, r, workload)

具体 函数:此处示例是根据 APIVersion 来做判断,若是自定义的 OAM workload 则用 去获取 workload 的子资源并返回;若是 K8S CR 则直接将 workload 作为返回值即可。

util 包地址:

var (
	workloadAPIVersion = v1alpha2.SchemeGroupVersion.String()
	appsAPIVersion     = appsv1.SchemeGroupVersion.String()
)

func DetermineWorkloadType(ctx context.Context, log logr.Logger, r client.Reader,
	workload *unstructured.Unstructured) ([]*unstructured.Unstructured, error) {
	apiVersion := workload.GetAPIVersion()
	switch apiVersion {
	case workloadAPIVersion:
		return util.FetchWorkloadDefinition(ctx, log, r, workload)
	case appsAPIVersion:
		log.Info("workload is K8S native resources", "APIVersion", apiVersion)
		return []*unstructured.Unstructured{workload}, nil
	...
	}
}

ServiceTrait 的逻辑是为目标资源对象创建一个 K8S 原生 Service 资源。用户可根据自己的需求,自定义 trait 的逻辑。

svc, err := r.createService(ctx, trait, resources)

而 Workload 的 Controller 逻辑更为简单:

  • 第一步:定义 workload 变量,同样通过 获取需要调谐的 workload 对象。

  • 第二步:执行 workload 逻辑。以 为例,它为 workload 创建了一个 deployment 和一个 service 资源。

注意点

需在 函数中将 OAM core API 添加到 scheme 中,因为 trait 的 Controller 逻辑中需要获取集群中 对象。

import "github.com/crossplane/oam-kubernetes-runtime/apis/core"

func init() {
	...
	_ = core.AddToScheme(scheme)
	...
}

需添加 kubebuilder 选项,以支持 trait 控制器对资源的操作权限。ServiceTrait 添加了对 containerizedworkloads、workloaddefinitions、statefulsets、deployments、services 的操作权限。

// +kubebuilder:rbac:groups=core.oam.dev,resources=containerizedworkloads,verbs=get;list;
// +kubebuilder:rbac:groups=core.oam.dev,resources=workloaddefinitions,verbs=get;list;watch
 ...

同样在 workload_controller.go 中,也需要注意使用 kubebuilder 选项,添加对需要操作的资源的权限。

4. 部署使用 Traits

我们使用 kubebuilder 生成了框架,并自定义了 CRD 和 Controller 逻辑,由此便得到了一个能为 workload 创建一个 K8S 原生 Service 资源的运维特征:ServiceTrait;根据同样的流程逻辑,我们也能得到一个能为 workload 创建一个 K8S 原生 Ingress 资源的运维特征:IngressTrait。详细可参考

在 IngressTrait 的例子中,编写了一个 example:Component 中的 workload 直接复用 K8S StatefulSet;ApplicationConfiguration(以下简称 appconfig) YAML 文件中指定了 componentName,并为其绑定 ServiceTrait 和 IngressTrait。

  • 首先,appconfig 的 字段是 ApplicationConfigurationComponent 结构体组成的数组,而 example 中定义了一个 ApplicationConfigurationComponent,包含 componentName 和 traits 的信息。

  • 由此 appconfig 控制器便可根据 componentName 去获取对应的 Component,从而由 Component 的 字段获取其复用的 K8S StatefulSet。

  • 接着,appconfig 会将 componentName,workload,traits 的信息储存在 appconfig 中定义的 Workload 结构体中。

  • 最后在 Workload 结构体的 函数中,将与 traits 绑定的 workload 信息依次添加到 traits 的 字段中,实现 workload 与 traits 的交互,并同时将 workload 和 traits 部署到集群中。

如图,成功部署资源,并成功通过 ingress 访问服务。

5. 总结

本文首先介绍了 OAM Workload 与 Trait 相关知识以及它们之间的交互。而本文的重点在于如何通过 kubebuilder 为 OAM Workload 和 Trait 生成框架,以及如何编写 Workload 和 Trait 的自定义 CRD 和 Controller 逻辑。

希望通过本文能够帮助大家快速理解掌握编写 OAM Workload 和 Trait。

6. 作者简介

钱王骞,浙江大学软件学院研究生,目前在杭州谐云科技有限公司实习,同时正在参与 OAM 社区相关工作。

OAM 项目:https://github.com/oam-dev/spec