
この記事は hacomono Advent Calendar 2025 の11日目の記事です
こんにちは! 基盤本部プラットフォームチーム所属の kaikai です。
hacomono に入社して 1 年半ほど経ちましたが、ついにジムに通い始めました!
誰にも言ってないですがチーム内のマッスル枠を密かに狙っています。
さて、今回は Crossplane と kro についてコードを交えながら解説していきたいと思います。
これらはどちらも Kubernetes 上で動作する OSS で、Kubernetes の抽象を通じて独自リソースを管理できるようにするものです。
Crossplane に関しては以前私が登壇した YAPC の資料も参考にしていただけますと幸いです。
TL;DR
- Crossplane と kro は Kubernetes の抽象を通じて独自リソースを管理可能にする OSS
- Crossplane には AWS や GCP などの Kubernetes 標準外リソースを管理する
Providerが存在し、外部リソースの管理まで Crossplane で完結可能 - kro は外部リソースの管理には関与せず、必要に応じて外部リソースを管理する仕組みを別途導入する必要がある
- Crossplane は
Functionという gRPC サーバーとのやりとりで独自リソースのリソース構成を解決する - kro は
CELでリソース構成を定義し、独自リソースのリソース構成を解決する - Crossplane は責務が大きく、柔軟性も高いがその分複雑性も高い
- kro は責務が小さくシンプルでGoのコード行数も Crossplane の 1/3 以下
そもそも Crossplane とは?kro とは?
Crossplane とは簡単にいうと、Kubernetes の抽象からあらゆるリソースを管理可能にする OSS の Kubernetes エコシステムです。
最近 CNCF の Graduated stage に昇格しました。
Crossplane は Kubernetes リソースに加え、AWS や GCP,Azure などのクラウドリソースを Kubernetes の抽象を通じて管理することができるようになります。
また、独自リソースを定義し、管理することが可能で、組織のポリシーに則ったクラウドリソースや、Ingress+ Service + Deployment + Monitoring Solution などの Kubernetes 複合リソースなどを独自リソースとして管理することができるようになります。
Crossplane によって定義された独自リソースを XR(Composite Resource) と呼びます。
プラットフォームエンジニアリングの一環として XR を開発者に提供することで、開発者は数行のmanifestで必要なリソースを作成できるようになります。
kro は Kube Resource Operator の略で kubernetes-sigs で管理されている OSS の Kubernetes エコシステムです。
最近までは開発途中との明記があったのですが、最新のドキュメントでは後方互換性に関する注意書きがあるのみで、ある程度安定してきたのかなと思います。
kro も Crossplane の XR のように独自リソースを定義可能で、独自の抽象から様々な Kubernetes リソースを管理可能にします。
独自リソースを定義して利用可能になるまで
ここからは、SuperApp という架空の独自リソースを定義し、利用可能になるまでの手順や内部動作を Crossplane と kro で比較しながら説明します。
SuperApp は Kubernetes の Service + Deployment + PodIdentity を組み合わせたリソースと仮定し、以下のように Kubernetes のマニフェストで SuperApp を作成すると、Service,Deployment,PodIdentity が自動的に作成されるようなイメージです。
apiVersion: platform.example.io/v1alpha1 kind: SuperApp metadata: name: my-superapp namespace: default spec: image: my-superapp-image:latest replicas: 3 policyDocuments: | { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::example-bucket" ] } ] } ...
Crossplane でも kro でも独自リソースの抽象は同じものを提供できます。
NOTE:
これから説明する内容で、yamlのコードはCrossplane,kroの利用者が定義するKubernetes manifestで、GoのコードはCrossplane,kro自体のコードとなっております。
Crossplane の場合
Crossplane で独自リソースを定義する場合、まず XRD を作成します。
これは Kubernetes の CRD と同じようなもので、XR のスキーマ定義などを以下のように行います。
apiVersion: apiextensions.crossplane.io/v2 kind: CompositeResourceDefinition metadata: name: superapps.platform.example.io spec: scope: Namespaced group: platform.example.io names: kind: SuperApp plural: superapps versions: - name: v1alpha1 schema: # OpenAPI形式によるスキーマ定義 openAPIV3Schema: type: object properties: spec: type: object properties: image: type: string description: "Container image with tag (e.g., 'my-app:1.0.0')" ...
XRDを Kubernetes に適用すると、Crossplane の XRD 用のコントローラーが XRD を検知し、CRD に変換して適用します。
また、XR 用のコントローラーも自動生成され、稼働が開始します。
// internal/controller/apiextensions/definition/reconciler.go func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { ... d := &v1.CompositeResourceDefinition{} if err := r.client.Get(ctx, req.NamespacedName, d); err != nil { ... } // CompositeResourceDefinition からCRDをレンダリング crd, err := r.composite.Render(d) ... // CRDを適用 if err := r.client.Applicator.Apply(ctx, crd,... ... gvk := d.GetCompositeGroupVersionKind() ... ro := []composite.ReconcilerOption{ composite.WithCompositeSchema(schema), ... } ... // XR用のControllerを生成 cr := composite.NewReconciler(r.engine.GetCached(), gvk, ro...) ... // XR用のControllerを新たに稼働 if err := r.engine.Start(controllerName, co...); err != nil { ...
XRD だけではスキーマしか定義されておらず、「どのように作成するのか」の情報が不明なため、「どのように作成するのか」を Composition で定義します。CompositionはCrossplane のカスタムリソースです。
Composition には「どの Function をどの input で呼び出すか」などの情報を記述します。
Function は Crossplane のカスタムリソースで、Crossplane のコントローラーとやり取りをする gRPC サーバーです。
Function には様々な種類があり、以下は function-go-templating という Go のテンプレートエンジンを利用したケースを記述しています。
apiVersion: apiextensions.crossplane.io/v1 kind: Composition metadata: name: superapp.platform.example.io spec: compositeTypeRef: apiVersion: platform.example.io/v1alpha1 kind: SuperApp mode: Pipeline pipeline: - step: create-resources functionRef: name: function-go-templating input: apiVersion: gotemplating.fn.crossplane.io/v1beta1 kind: GoTemplate source: Inline inline: template: | {{ $appName := $composite.metadata.name }} {{ $namespace := $composite.metadata.namespace }} ... --- # PodIdentityのリソースを作成 # このリソースが展開されると、provider-aws-eksがPodIdentityを管理するようになる apiVersion: eks.aws.m.upbound.io/v1beta1 kind: PodIdentityAssociation metadata: name: {{ $clusterName }}-{{ $namespace }} namespace: {{ $namespace }} ... --- # Deploymentのリソースを作成 apiVersion: apps/v1 kind: Deployment metadata: name: {{ $appName }} namespace: {{ $namespace }} ... --- # Serviceのリソースを作成 apiVersion: v1 kind: Service metadata: name: {{ $appName }} namespace: {{ $namespace }} ...
Composition で function-go-templating を利用しているため、function-go-templating 用の Function リソースも作成し、Kubernetes に適用します。
apiVersion: pkg.crossplane.io/v1 kind: Function metadata: name: function-go-templating spec: package: xpkg.upbound.io/crossplane-contrib/function-go-templating:v0.11.0
Kubernetes 標準リソースに関しては Kubernetes 標準のコントローラーが管理してくれますが、PodIdentity のような Kubernetes 外のリソースに関しては管理してくれる別の何かが必要です。
Crossplane では Provider というカスタムリソースがあり、これを利用して外部リソースの管理を行います。
AWS の場合はリソース毎に Provider が存在するため、今回はPodIdentityを管理してくれる provider-aws-eks を Kubernetes に適用します。
apiVersion: pkg.crossplane.io/v1 kind: Provider metadata: name: provider-aws-eks spec: package: xpkg.crossplane.io/crossplane-contrib/provider-aws-eks:v2.0.0
これで XR を作成する準備が整いました。
XR をデプロイすると、以下のように内部のコントローラーが稼働して Compose メソッドを呼び出します。
// internal/controller/apiextensions/composite/reconciler.go // XR用のController func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { xr := composite.New(composite.WithGroupVersionKind(r.gvk), composite.WithSchema(r.schema)) if err := r.client.Get(ctx, req.NamespacedName, xr); err != nil { ... } // Functionを呼び出すComposeメソッド res, err := r.resource.Compose(ctx, xr, CompositionRequest{Revision: rev}) ... // 構成リソースを利用してEvent発行やXRの状態更新などを実施 for i, cd := range res.Composed { ... }
上記コードのr.resourceの実装であるFunctionComposer の Compose メソッドでは以下のように Function にリクエストを送信し、XR の構成リソースを解決、その後解決したリソース群を順次 Apply しています。
今回であれば、 Service,Deployment,PodIdentity の情報が Function から返却され、それらを Apply することになるはずです。
// internal/controller/apiextensions/composite/composition_functions.go func (c *FunctionComposer) Compose(ctx context.Context, xr *composite.Unstructured, req CompositionRequest) (CompositionResult, error) { // 現在の状態を取得 observed, err := c.composite.ObserveComposedResources(ctx, xr) ... // dはあるべき状態を表す d := &fnv1.State{} // Compositionに定義されているPipelineを順次処理してあるべき状態dを更新し続ける for _, fn := range req.Revision.Spec.Pipeline { // Functionへ渡すRequestを構築 req := &fnv1.RunFunctionRequest{Observed: o, Desired: d, Context: fctx} ... // Functionを呼び出し rsp, err := c.pipeline.RunFunction(ctx, fn.FunctionRef.Name, req) ... } // 構成リソースのあるべき状態を構築 desired := ComposedResourceStates{} ... // Functionから取得できたあるべき状態をもとにdesiredを構築 for name, dr := range d.GetResources() { cd := composed.New() if err := xfn.FromStruct(cd, dr.GetResource()); err != nil { ... } ... desired[ResourceName(name)] = ComposedResourceState{ Resource: cd, ... } } ... // 構成リソースを順次Apply for name, cd := range desired { if err := c.client.Patch(ctx, cd.Resource, client.Apply, client.ForceOwnership, client.FieldOwner(ComposedFieldOwnerName(xr))); err != nil { ... } } ... // XR自体のStatusを更新 if err := c.client.Status().Patch(ctx, xr, client.Apply, client.ForceOwnership, client.FieldOwner(FieldOwnerXR)); err != nil { ... } ... }
Apply されたリソースは各々のコントローラーが管理を行ってくれます。
Service,Deployment であれば Kubernetes 標準のコントローラー、PodIdentity であれば provider-aws-eks と言った感じです。
他にも細かい処理が実際は行われていますが、大まかな流れはこのようになっております。
kro の場合
kro で独自リソースを定義する場合、まず RGD を作成します。
RGD では管理したいリソースのスキーマと、「どのようにリソースを生成するのか」、を以下のように定義します。
「どのようにリソースを生成するのか」、に関しては CEL で定義します。
apiVersion: kro.run/v1alpha1 kind: ResourceGraphDefinition metadata: name: superapp spec: schema: apiVersion: apps.kro.run/v1alpha1 kind: SuperApp spec: type: object properties: appName: type: string description: Name of the application image: type: string description: Container image for the deployment ... resources: # Deployment resource - id: deployment definition: apiVersion: apps/v1 kind: Deployment metadata: name: ${schema.spec.appName}-deployment namespace: ${schema.metadata.namespace} ... # Service resource - id: service definition: apiVersion: v1 kind: Service metadata: name: ${schema.spec.appName}-service namespace: ${schema.metadata.namespace} ...
RGD を適用すると kro のコントローラーが RGD を検知し、以下コードのように CRD に変換して適用します。
また、独自リソース用のコントローラも自動で生成され、稼働を開始します。
ここまでの動きは Crossplane の XRD を作成したときと似ているかと思います。
// pkg/controller/resourcegraphdefinition/controller.go func (r *ResourceGraphDefinitionReconciler) Reconcile( ctx context.Context, o *v1alpha1.ResourceGraphDefinition, ) (ctrl.Result, error) { ... // RGDの内容をもとにリソースグラフを構築し、CRDの取得および適用、RGD用コントローラーの生成および稼働を実施 topologicalOrder, resourcesInformation, reconcileErr := r.reconcileResourceGraphDefinition(ctx, o) ... } // pkg/controller/resourcegraphdefinition/controller_reconcile.go func (r *ResourceGraphDefinitionReconciler) reconcileResourceGraphDefinition( ctx context.Context, rgd *v1alpha1.ResourceGraphDefinition, ) ([]string, []v1alpha1.ResourceInformation, error) { // RGDからGraphの構築 processedRGD, resourcesInfo, err := r.reconcileResourceGraphDefinitionGraph(ctx, rgd) // GraphからCRDの取得 crd := processedRGD.Instance.GetCRD() ... // CRDの適用 if err := r.reconcileResourceGraphDefinitionCRD(ctx, crd); err != nil { ... // 独自リソース用のControllerを生成および稼働 if err := r.reconcileResourceGraphDefinitionMicroController(ctx, processedRGD, graphExecLabeler); err != nil { ... } // pkg/controller/resourcegraphdefinition/controller_reconcile.go func (r *ResourceGraphDefinitionReconciler) reconcileResourceGraphDefinitionMicroController( ctx context.Context, processedRGD *graph.Graph, graphExecLabeler metadata.Labeler, ) error { // RGDで定義されたリソース群から監視対象のGVRを取得 resourceGVRsToWatch := r.getResourceGVRsToWatchForRGD(processedRGD) // RGD用のControllerを生成 controller := r.setupMicroController(processedRGD, graphExecLabeler) ... // RGD用のControllerを新たに稼働 err := r.dynamicController.Register(ctx, gvr, controller.Reconcile, resourceGVRsToWatch...) ... }
kro の場合あとは独自リソースを作成するだけです。Crossplane と比べて kro はめちゃめちゃシンプルです。
それでは RGD で定義した SuperApp リソースを作成してみましょう。
すると、先ほど内部で生成されたコントローラーが RGD を参照し、依存関係などからグラフ構造を構築、その内容に沿ってリソースを Apply します。
結果Service,Deployment,PodIdentity などのリソースが Apply されることになるはずです。
// pkg/controller/instance/controller.go func (c *Controller) Reconcile(ctx context.Context, req ctrl.Request) error { // 独自リソースを取得 instance, err := c.clientSet.Dynamic().Resource(c.gvr).Namespace(req.Namespace).Get(ctx, req.Name, metav1.GetOptions{}) ... // Reconcile処理を実施する構造体の生成 instanceGraphReconciler := &instanceGraphReconciler{ ... } ... return instanceGraphReconciler.reconcile(ctx) } // pkg/controller/instance/controller_reconcile.go func (igr *instanceGraphReconciler) reconcile(ctx context.Context) error { ... // igr.reconcileInstance を呼び出してリソースの Apply を実施 return igr.handleReconciliation(ctx, igr.reconcileInstance) } // pkg/controller/instance/controller_reconcile.go func (igr *instanceGraphReconciler) reconcileInstance(ctx context.Context) error { ... // graph の順序に従ってリソースの状態や依存関係の解決を行い、構成リソースを整備 for _, resourceID := range igr.runtime.TopologicalOrder() { // graphからリソースを取得 resource, state := igr.runtime.GetResource(resourceID) ... // 適用するリソース構成の登録 applyable := applyset.ApplyableObject{ Unstructured: resource, ID: resourceID, } clusterObj, err := aset.Add(ctx, applyable) ... } // 構成リソースのApply result, err := aset.Apply(ctx, prune) ... // エラー処理やステータス更新など }
ただし、PodIdentity というリソースを Apply されても、Kubernetes 標準のコントローラーは管理してくれません。
つまり、このままでは PodIdentity などの外部リソースを管理するコントローラーが存在しないため、うまくいきません。
どうすれば良いかというと、Crossplane の Provider のような外部リソースを管理するコントローラーを導入する必要があります。
この外部リソースの管理については kro は関与しません。ここが Crossplane との大きな違いです。
実際に kro で AWS リソースを管理する場合は、ACK(AWS Controllers for Kubernetes) という AWS の各種リソースを管理するコントローラーか、 Crossplane の Provider などが選択肢として考えられます。
Crossplane と kro の違い
独自リソースを定義し、利用可能にするまでの流れで気付いたかも知れませんが、Crossplane と kro の違いとして
- 責務の違い
- 柔軟性
- 複雑性
があると思います。
責務の違い
上述の通り、Crossplane は外部リソースを管理する Provider というカスタムリソースを持っており、Crossplane だけで外部リソースの管理まで完結できます。
そのため、Provider リソースを適用するだけで、外部リソースを含む独自リソースを定義できるようになります。
一方で kro は外部リソースの管理については関与せず、RGD の定義に基づいてリソースを Apply するという責務のみを持っています。
そのため、外部リソースを管理するコントローラーは別途導入・管理する必要があります。
柔軟性の違い
Crossplane は Function という gRPC サーバーとのやりとりでリソース構成を解決しています。
この Function は様々な種類がすでに存在していることとFunction 自体を自作することが可能なため、柔軟にリソース構成の解決処理を拡張できます。
対して kro は CEL でリソース構成を定義しています。
CEL はシンプルで学習コストも低いため、比較的簡単にリソース構成を定義できますが、凝ったリソース構成を定義するには限界があるかも知れません。
複雑性の違い
責務の違い、柔軟性の違いだけ見ると Crossplane の方が優れているように見えますが、その分複雑性も高くなっています。
まず、Crossplane は責務が大きく、Crossplane 自体のカスタムリソースに関しても20 種類ほど提供されています。
逆にいうと、それらを適切に選択し設定し、運用する必要があります。
もちろんカスタムリソース毎にそれらを管理するコントローラーも存在するため、多くのコントローラーが稼働しておりそれらの理解や管理も必要です。(インフラ費用もかかる。。。)
また、リソース構成を解決するためには Function とのやり取りによって達成する必要があり、Function との通信が何かしらの理由で失敗した場合はリソースの作成が失敗するため、フレーキーなエラーが発生する可能性も高まります。
トラブルシュートも Crossplane と Function が分散して動作するため複雑になります。
kro に関しては責務が小さくカスタムリソースも RGD のみで大変シンプルです。
Kubernetes 標準外リソースを管理する場合は外部リソースを管理するコントローラーを別途導入する必要がありますが、kro 自体の責務(リソース構成を解決して Apply)を達成するためには kro のみでよいです。
つまり、フレーキーなエラーも起きにくいですし、運用管理などの点でも優秀です。
主要ロジックが記述されているGoのコード行数に関しても Crossplane は 9 万行以上、kro は 3 万行ほどと大きな差があります。
結局どっちが良いの?
個人的にはどちらも良いソリューションであり、ユースケースによって使い分けるのが良いと思います。
Kubernetes リソースのみに対して独自リソースを構築したい場合は kro が良いと思います。
外部リソースを管理するコントローラーを導入する必要もなく、kro 単体で完結するからです。
外部リソースも含める場合やマルチクラウドを利用する場合などは Crossplane を利用するのも良いと思います。
Crossplane という単一のインターフェイスで外部リソースの管理から独自リソースの定義/管理まで完結できるからです。
また、構成リソースの解決には kro を使い、外部リソースの管理には Crossplane Provider を使うというハイブリッドな利用方法も考えられます。
これにより、構成リソースの解決には kro のシンプルさを享受しつつ、外部リソースの管理には Crossplane Provider を利用することが可能になります。
我々は現在 Crossplane のみを利用していますが、今後は kro の利用や動向も注視していきたいと思います。
最後に
ここまで読んでいただきありがとうございます!
大変長い記事になってしまいましたが、なんとなくでも Crossplane や kro の雰囲気をわかってもらえれば幸いです。
Crossplane を使ったマルチプロダクト基盤の話や冒頭で紹介したYAPCの登壇資料などもございますので、是非是非ご覧ください!
それでは〜!