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

用户头像
钱王骞
关注
发布于: 2020 年 06 月 12 日

此文中些许部分已重构,更新版本:https://xie.infoq.cn/article/5b512225b13c7bcd525b7c35b

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 的模板:

2.3 编码



kubebuilder 已经为我们生成了较为完整的框架,我们主要编辑 cronjob_types.gocronjob_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=<some-registry>/<project-name>:tag
make deploy IMG=<some-registry>/<project-name>:tag

3. 编写 Workload 与 Trait



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

servicetrait_types.go



  1. 需在 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
}



  1. 需在 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"`
}



  1. 添加 kubebuilder 选项:

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



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

servicetrait_controller.go



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



  1. 获取 trait 对象



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

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



  1. 获取 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 { ... }
...
}



  1. 获取目标资源对象



首先需要确定 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
...
}
}



  1. 执行 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 资源。

注意点

  1. main.go 中增加映射



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

import "github.com/crossplane/oam-kubernetes-runtime/apis/core"
func init() {
...
_ = core.AddToScheme(scheme)
...
}



  1. 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 访问服务。



5. 总结



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

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

6. 作者简介

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

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

用户头像

钱王骞

关注

还未添加个人签名 2020.02.13 加入

还未添加个人简介

评论

发布
暂无评论
如何基于 OAM 编写一个扩展 Trait?