如何基于 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 上,为应用程序扩展运维能力。
1.3 Workload 与 Trait 交互
Application Configuration 是组合组件和相应运维特征的地方,Workload 与 Trait 的交互就在其控制器逻辑中。Application Configuration 中储存了 ComponentName、Workload、Traits 的信息,它会将 Workload 的信息依次添加到各个 Trait 中
2. 使用 kubebuilder 构建 OAM 扩展 Trait
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 作为返回值即可。
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