Categories
程式開發

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


1. 背景

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

来源:阿里云携手微软与 Crossplane 社区发布 OAM Kubernetes 标准实现与核心依赖库

在 OAM 中,一个应用程序包含三个核心理念。第一个核心理念是组成应用程序的组件(Component),它可能包含微服务集合、数据库和云负载均衡器;第二个核心理念是描述应用程序运维特征(Trait)的集合,例如,弹性伸缩和 Ingress 等功能。它们对应用程序的运行至关重要,但在不同环境中其实现方式各不相同;最后,为了将这些描述转化为具体的应用程序,运维人员使用应用配置(Application Configuration)来组合组件和相应的特征,以构建应部署的应用程序的具体】实例。

来源:深度解读!阿里统一应用管理架构升级的教训与实践

1.1 Workload

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

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

1.2 Trait

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

Trait 对 Workload 资源的操作方式主要分为两类,一类是直接操作 Workload 或者其生成的下层资源的字段,如修改资源的 spec.replicas,参考 addon-oam-kubernetes-local” 中的 ManualScalerTrait;另一类是创建一个独立的资源,如为资源创建一个 K8s Service,参考 ServiceTrait“。

1.3 Workload 与 Trait 交互

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

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

2. 使用 kubebuilder 构建 OAM 扩展 Trait

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

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

首先,你需要安装 kubebuilder,参照网址:https://book.kubebuilder.io/quick-start.html#installation“。

2.1 构建项目

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

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

2.2 创建API

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

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

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

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

2.3 编码

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

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

2.4 安装并运行

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

make install
make run

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

kubectl apply -f config/samples/batch_v1_cronjob.yaml

2.5 构建并部署

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

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

3. 编写 Workload 与 Trait

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

servicetrait_types.go

需在 ServiceTraitStatus 结构体中定义以下两个字段:

runtimev1alpha1.ConditionedStatus:此字段用于反应资源在集群中的观察状态。runtimev1alpha1.TypedReference:此字段定义资源的 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
}

需在 ServiceTraitSpec 结构体中定义:

WorkloadReference 必须设置,是储存需要扩展的 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 选项:

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

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

servicetrait_controller.go

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

获取 trait 对象

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

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

获取 workload 对象

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

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

具体 fetchworkload 函数:

声明 workload 变量。根据 trait 对象的 GetWorkloadReference 方法获取其引用的 workload 对象信息:APIVersion、Kind、Name、UID。用 client.ObjectKey 生成的 NamespacedName 去获取集群中的 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)

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

util 包地址:https://github.com/crossplane/addon-oam-kubernetes-local/tree/master/pkg/oam/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
...
}
}

执行 trait 逻辑

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

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

而 Workload 的 Controller 逻辑更为简单:

第一步:定义 workload 变量,同样通过 req.NamespacedName 获取需要调谐的 workload 对象。第二步:执行 workload 逻辑。以 containerizedworkload_controller.go” 为例,它为 workload 创建了一个 deployment 和一个 service 资源。

注意点

main.go 中增加映射

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

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

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

servicetrait_controller.go 中增加 rbac

需添加 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。详细可参考oam-dev/catalog/traits“。

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

首先,appconfig 的 spec.components 字段是 ApplicationConfigurationComponent 结构体组成的数组,而 example 中定义了一个 ApplicationConfigurationComponent,包含 componentName 和 traits 的信息。由此 appconfig 控制器便可根据 componentName 去获取对应的 Component,从而由 Component 的 spec.workload 字段获取其复用的 K8S StatefulSet。接着,appconfig 会将 componentName,workload,traits 的信息储存在 appconfig 中定义的 Workload 结构体中。最后在 Workload 结构体的 Apply 函数中,将与 traits 绑定的 workload 信息依次添加到 traits 的 spec.workloadRef 字段中,实现 workload 与 traits 的交互,并同时将 workload 和 traits 部署到集群中。

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

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

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

5. 总结

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

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

6. 作者简介

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

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