「Kro」深掘り:定義の実体をCRDにするという選択
第5回の今回は、Application ModelをCRDとして公開する「Kro」のRGDの書き方と内部構造を深掘りし、KubeVelaとの設計思想の違いや現時点の制約について解説します
6:30
はじめに
これまでの連載では、開発者に見せるAPIをプラットフォームチームがどう設計するかを追ってきました。今回扱うKroは、その答えの1つです。
Kroとは結局何なのか。そして、組織のAPIの基盤として採用してよいのか。RGDの書き方から内部の仕組み、現在地までを順に見ていきます。
Kroとは何か
Kroは、組織独自のApplication Modelを、Kubernetesのカスタムリソースとして公開するためのツールです。プラットフォームチームが型とレシピをYAMLに書くと、その型がCRDとしてクラスタに登録されます。開発者が書くのは、そのCRDのインスタンスだけです。
第2回で並べた分類では、コンポジション方式にあたります。KubeVelaのように既定のComponent/Traitモデルを使うのではなく、公開するAPIの語彙をプラットフォームチームが定義します。
登場するもの | 誰が用意するか | 位置づけ |
|---|---|---|
ResourceGraphDefinition(RGD) | プラットフォームチーム | 公開するAPIの形と、そこから作るリソースのテンプレート |
生成されたCRD(以下、生成CRD) | Kro(RGDから自動生成) | 開発者向けAPIの実体 |
そのCRDのインスタンス | 開発者 | 公開されたAPIを使う |
実リソース | Kro(インスタンスから展開) | Deployment、Serviceなど |
Kroは2024年11月のKubeCon North Americaで公開されました。現在はKubernetes SIG Cloud Providerのサブプロジェクトで、AWS・Microsoft Azure・Google Cloudをはじめとする各社のメンテナが開発しています(kubernetes-sigs/kro)。執筆時点の最新はv0.9.3(2026年7月27日)、APIバージョンはv1alpha1です。
Adopt事例もかなりあります。Spotifyは2026年3月のKubeCon Europeで、kubebuilderでのoperator自作からKroと内製operatorへ移した事例を報告しました。AWSは2025年11月30日からAmazon EKS CapabilitiesでKroをマネージドサービスとして提供しています。2026年7月のKubeCon Japanでは、SAPのJakob Möller氏らが「Evolving Platform Primitives: Beautiful Platforms with kro」を発表しています。
Kroの使い方
RGDから公開APIができるまで
プラットフォームチームはRGDだけを書きます。中身はschemaとresourcesの2つに分かれます。以下では、コンテナを1つ動かすたびに書くDeployment・Secret・Serviceを、WebServiceという1つの型にまとめた例を使います。
1. schema——公開するAPIを決める。
spec:
schema:
group: example.org
apiVersion: v1alpha1
kind: WebService
spec:
image: string | required=true
replicas: integer | default=1 minimum=1 validation="self <= 20"
tier: string | default=staging enum="staging,production"
apiKey: string | default=dummy-key
status:
endpoint: ${svc.spec.clusterIP}
readyReplicas: ${deploy.status.readyReplicas}先頭の3行が、生成されるCRDのGVK(Group / Version / Kind)になります。groupの既定はkro.runで、上の例のように自社ドメインへ置けます。GVKの3つとも、作成後は変更できません。
specは開発者が書く項目です。この記法をSimpleSchemaと呼び、型名のあとにパイプを1本置いてマーカーを空白で並べます。ここに書いたdefault=やminimum=は、そのまま生成CRDへ渡ります。
statusは開発者へ返す値です。${}はCEL(Common Expression Language)の式で、ここではresources側で作ったリソースの実際の値を参照しています。
2. resources——対応するKubernetesリソースを展開する。
resources:
- id: secret
template:
apiVersion: v1
kind: Secret
metadata:
name: ${schema.metadata.name}-secret
stringData:
API_KEY: ${schema.spec.apiKey}
- id: deploy
readyWhen:
- ${deploy.status.readyReplicas > 0}
template:
apiVersion: apps/v1
kind: Deployment
metadata:
name: ${schema.metadata.name}
spec:
replicas: ${schema.spec.replicas}
selector:
matchLabels: {app: "${schema.metadata.name}"}
template:
metadata:
labels: {app: "${schema.metadata.name}"}
spec:
containers:
- name: app
image: ${schema.spec.image}
envFrom:
- secretRef:
name: ${secret.metadata.name}
- id: svc
template:
apiVersion: v1
kind: Service
metadata:
name: ${schema.metadata.name}
spec:
selector: {app: "${schema.metadata.name}"}
ports:
- port: 80idは他のリソースから参照するための名前です。${}の中に書けるのは2種類だけです。schemaはインスタンス自身を指します(schema.spec.image、schema.metadata.name)。それ以外はresourcesのidです。
${secret.metadata.name}は、Secretの名前だけを返します。Secret本体は同じRGDのid: secretが作ります。Deployment側はその名前をenvFromで受け取ります。Secretの中のキー名(この例ではAPI_KEY)は、コンテナが読む環境変数名と一致させる必要があります。ここはKroの検査対象外です。
参照があると、Kroはリソースの順序を組み立てます。Secretが先、Deploymentが後です。YAMLに書いた順ではありません。
これを適用すると、schemaの内容からCRDが組み立てられ、クラスタに登録されます。
# 生成されたCRD抜粋——kubectl get crd webservices.example.org -o yaml
spec:
group: example.org # schema.group
names:
kind: WebService # schema.kind
plural: webservices # Kroが複数形を導出する
versions:
- name: v1alpha1 # schema.apiVersion
schema:
openAPIV3Schema:
properties:
spec:
properties:
image: {type: string} # schema.spec
replicas: {type: integer, default: 1, minimum: 1,
x-kubernetes-validations: [{rule: "self <= 20"}]}
required: [image]
additionalPrinterColumns: [State, Ready, Age] # Kroが付ける複数形の導出と、State・Ready・Ageの表示列は、Kroが付け足したものです。
同じRGDに書いた2つのブロックのうち、CRDになるのはschemaだけです。resourcesはCRDに入らず、Kroのプロセスが持ち続けます。テンプレートエンジンのように一度展開して終わりではなく、インスタンスがあるかぎり、reconcileのたびに評価されます。
開発者が書くインスタンス
公式ドキュメントがinstanceと呼ぶ単位です。schema.specに並べた4項目だけを埋めます。
apiVersion: example.org/v1alpha1
kind: WebService
metadata:
name: shop-stg
spec:
image: nginx:1.27-alpine
replicas: 2
tier: staging
apiKey: stg-key適用すると、Secret・Deployment・Serviceが揃い、statusに値が返りました。
$ kubectl get webservice shop-stg
NAME STATE READY AGE
shop-stg ACTIVE True 43s
$ kubectl get webservice shop-stg -o jsonpath='{.status}'
{"conditions":[...],"endpoint":"10.96.142.146","readyReplicas":2,"state":"ACTIVE"}kubectl wait --for=condition=Readyもそのまま通りました。statusの2つの値は、どちらもKroが作ったリソースから引いたものです。ServiceのclusterIPとDeploymentの実際のレプリカ数で、開発者はそのどちらも自分では書いていません。
開発者に提供するもの
開発者が読むのは、schemaから作られたCRDだけです。1枚のRGDの中に、見せる部分と見せない部分が同居しています。schemaは開発者への約束で、開発者が読んでよいのも、頼ってよいのもここだけです。resourcesはその約束をどう実現するかで、CRDに入らないぶん開発者からは見えず、プラットフォームチームはあとから中身を差し替えられます。
何が守られ、何が守られないか
境界を守るのはKube API Serverです。ただし、その効き目はスキーマの範囲までです。RGD自体はクラスタ上のリソースなので、RBACで隠さないかぎりkubectl get rgd -o yamlで中身が読めます。
生成されるのはふつうのCRDなので、どのチームにどのAPIを提供するかを、Kubernetesの権限管理で制御できます。チームAにはWebServiceだけ、チームBにはそれとDatabaseも、といった配り分けが、Kro固有の仕組みなしで書けます。
開発者が知らなくて済むこと
第1回で、Application Modelの核心は「開発者が何を知らなくて済むべきか」という問いだと書きました。そして、その答えは組織ごとに違うと結論づけました。ネットワークの詳細を隠すべき組織もあれば、それが開発者の本業という組織もあります。
だからツールに要るのは、答えそのものではなく、答えを書く場所と、書いた答えを守る仕組みです。
答えを書く場所 | 守る仕組み | |
|---|---|---|
OAM | 仕様が定めた役割ごとのエンティティ | 各実装に委ねられる |
KubeVela | ComponentDefinitionのCUE | KubeVelaのエンジン |
Crossplane | XRD | 生成CRDとKube API Server |
Kro | RGDのschema | 生成CRDとKube API Server |
守る仕組みがKube API Serverになるのは、Kroだけではありません。第4回で見たCrossplaneも、XRDからCRDを生成する同じ形です。分かれるのは書く場所のほうで、Crossplaneはスキーマ(XRD)とレシピ(Composition)を別のリソースに分け、Kroは1つのRGDに収めています。
Kroでの答えはschemaに書いた項目で、裏返せば書かなかったものが「知らなくて済むこと」です。ここまでのWebServiceでは、開発者が書いたのはimage・replicas・tier・apiKeyの4つだけです。次のものが視界から消えていました。
Secretの名前の付け方と、キー名(API_KEY)を環境変数へ入れる経路
DeploymentとServiceを結ぶselectorとラベルの付け方
Serviceのポート番号
隠せる対象は、Kubernetesのリソースとして表現できるものに限られます。ただしその範囲は広く、組み込みのリソースだけでなく、他のcontrollerが管理するカスタムリソースもテンプレートに置けます。CrossplaneのXRも、KubeVelaのApplicationも、FluxのHelmReleaseも、RGDから作れます。
条件は、そのCRDがクラスタに入っていることだけです。手元では、helm-controllerを入れずにHelmReleaseのCRDだけを置いたクラスタでも、KroはHelmReleaseを生成しました。CRDがないままだと、RGDはInactiveで止まります。
ユースケースごとの表現力
先ほどのWebServiceに、ConfigMap・ServiceAccount・Ingressを足します。schemaにもconfigやexposeExternallyの項目を増やしています。検証環境はkind上のKubernetes v1.34.0、Kroはv0.9.3です。ここへstagingのshop-stgとproductionのshop-prd、2つのインスタンスを適用しました。
複数のリソースを組み立て、値を注入する
インスタンス1つから、最大6種類のリソースが生成されます。名前はすべてインスタンス名から導いたものです。
生成されるもの | shop-stg | shop-prd |
|---|---|---|
ServiceAccount | shop-stg | shop-prd |
Secret | shop-stg-secret | shop-prd-secret |
ConfigMap | shop-stg-config | shop-prd-config |
Deployment | shop-stg | shop-prd |
Service | shop-stg | shop-prd |
Ingress | (生成されない) | shop-prd |
リソース同士の結線も、テンプレート側に寄せられます。
serviceAccountName: ${sa.metadata.name}
envFrom:
- secretRef:
name: ${secret.metadata.name}
volumeMounts:
- name: config
mountPath: /etc/app
volumes:
- name: config
configMap:
name: ${config.metadata.name}開発者はapiKeyとconfigだけを書きます。そこからSecretが作られ、envFromで環境変数に入り、ConfigMapが/etc/appへマウントされ、ServiceAccountが紐付きます。
条件で出し分ける
出し分けの粒度は2つあります。リソースそのものを出すかどうかはincludeWhenです。
- id: ingress
includeWhen:
- ${schema.spec.exposeExternally}配列で書き、すべてが真のときだけそのリソースが生成されます。exposeExternallyの既定はfalseなので、書かなかったshop-stgにはIngressが作られません。trueを書いたshop-prdには作られます。表の最後の行の差がこれです。
値を変えたいだけなら、テンプレート内のCEL式です。
replicas: '${schema.spec.tier == "production" ? schema.spec.replicas * 2 : schema.spec.replicas}'インスタンス | 書いたtier | 書いたreplicas | 生成されたDeployment |
|---|---|---|---|
shop-stg | staging | 2 | 2 |
shop-prd | production | 2 | 4 |
同じreplicas: 2が、本番では4になります。倍率を決めるのはプラットフォームチームで、開発者は知らずに済みます。
入力に制約と既定値を置く
SimpleSchemaのマーカーは、生成CRDのOpenAPIスキーマへ落ちます。
port: integer | default=8080 minimum=1 maximum=65535
tier: string | default=staging enum="staging,production"
replicas: integer | default=1 minimum=1 validation="self <= 20"
storageClass: string | default=standard immutable=true
image: string | required=true生成されたCRDでは、validation=がx-kubernetes-validationsのself <= 20に、immutable=trueがself == oldSelfになっていました。検証を行うのはKroではなくKube API Serverです。違反する値は、Kroに届く前に拒否されました。
入力 | Kube API Serverの応答 |
|---|---|
port: 70000 | spec.port in body should be less than or equal to 65535 |
tier: dev | Unsupported value: "dev": supported values: "staging", "production" |
replicas: 50 | validation failed |
imageを省略 | spec.image: Required value |
作成後にstorageClassを変更 | field is immutable |
既定値を書き換えるのも1行です。筆者のチームはKubeVelaのComponentDefinitionをCUEで書いて6環境ぶん運用していますが(第3回で扱ったX-Definitionの資産です)、ambientModeというフラグの既定値をtrueからfalseへ切り替えたとき、CUE側では6つの定義を書き換えることになりました。KroならambientMode: boolean | default=falseの1行です。
未指定のフィールドを、キーごと消す
同じ資産に、CronJobへtimeZoneを足した変更があります。CUEではこう書きました。
if INPUT.timeZone != _|_ {
timeZone: INPUT.timeZone
}未指定なら、CronJobのspecからtimeZoneというキーごと消えます。空文字を入れるのとは違います。Kubernetesには、キーがないことと空の値が入っていることで挙動が変わるフィールドがあるためです。
KroのincludeWhenはリソース単位なので、これには使えません。三項演算子でフォールバックを書くと、キーは空文字で残ります。この不足はIssue #928として挙がっており、omit()というCEL関数が実装されています。
timeZone: '${schema.spec.timeZone != "" ? schema.spec.timeZone : omit()}'ただしomit()はまだAlpha段階なので、既定で無効です。有効化には--feature-gates CELOmitFunction=trueが要ります。
Kroの仕組み
誰が何を呼ぶか
公式ドキュメントは、Kroの動きを8つの手順で説明しています。公式がUserとまとめている側は、実際にはプラットフォームチームと開発者に分かれます。
触るリソースは、役割ごとにはっきり分かれます。プラットフォームチームが適用するのは手順1のRGDだけで、CRDは自分では作りません。開発者が適用するのは手順5のインスタンスだけで、DeploymentにもServiceにも直接は触れません。その間をつなぐCRDと実リソースは、Kroが作ります。
この分担のおかげで、片方が止まってももう片方は進みます。RGDを直している最中でも、既存のCRDは登録されたままなので開発者はインスタンスを適用できます。
Kroのシステム構成
3つのcontrollerは、3つのコンテナではありません。Podに入っているコンテナは1つです。その1プロセスで、3つが並行して動きます。HelmチャートのreplicaCountの既定も1です。
このPodの中で、それぞれが仕事を分担しています。
RGD Controller
監視対象はResourceGraphDefinitionです。RGDが適用されると、schemaとresourcesを検証し、CEL式の参照から依存グラフを組み、結果をGraphRevisionとして発行します。コンパイルが済むのを待って、CRDを登録します。そのCRD専用のreconcile処理を作り、DynamicControllerへ差し込みます。
GraphRevision Controller
仕事はコンパイルです。GraphRevisionを見つけたらスナップショットをコンパイルし、プロセス内で共有するRegistry(図3のGraphRevision Registry)へ格納します。RGD ControllerはこのRegistryに載るまでCRDを登録しません。GraphRevisionが消されたときにRegistryから取り除くのも、このcontrollerです。
DynamicController
生成されたCRDのインスタンスを監視します。ふつうのcontrollerと違うのは、監視対象が起動時に決まっていない点です。CRDが登録されるたびにそのKind用のwatchを張り、インスタンスのイベントが来ると、そのCRD用のreconcile処理を呼びます。
上図にはあと2つ、実リソースを組み立てるInstance Controllerと、コンパイル済みグラフの置き場であるRegistryが載っています。
Instance Controller
RGDごとに1つ生えるreconcile処理です。RGD Controllerが作ってDynamicControllerへ差し込みます。呼ばれるたびにRegistryからグラフを取り出し、インスタンスの値を当てはめ、Server-Side Applyで適用します。リソースが整っていなければ、既定3秒後に再キューして待ちます。
RGDを1つ公開するたびに、CRDとwatchとInstance Controllerが1つずつ増えます。ただしwork queueは全GVRで1本のままで、そこから取り出すworkerの数も設定値で決まります。処理の並行度は、公開した定義の数とは無関係です。
Registry
コンパイル済みのグラフをプロセス内で共有する置き場です。GraphRevision Controllerが書き、Instance Controllerが読みます。RGDごとに直近5つのGraphRevisionを保持します。
定義の更新
RGDは一度書いて終わりではありません。項目を足し、既定値を変え、ときには消します。更新の手順そのものはkubectl applyだけですが、その裏で3つのcontrollerが順に動きます。
RGD Controllerが新しいRevisionを発行
RGDのspecからハッシュを取り、Registryにある最新のハッシュと比べます。違えば、新しいGraphRevisionを発行します。番号はr00001、r00002と積まれていきます。
GraphRevision Controllerがコンパイル
発行されたGraphRevisionをコンパイルし、成功すればRegistryへ載せてActiveにします。RGD Controllerが待っているのは、この完了です。この待ち合わせがあるので、コンパイルできない定義は公開されません。
RGD ControllerがCRDを更新
ここで新旧のスキーマを比較し、破壊的変更がないかを判定します。通れば、CRDにパッチを当てます。
RGD ControllerがInstance Controllerを差し替え
ここで既存インスタンスが全件キューへ戻り、それぞれが最新のグラフで組み立て直されます。開発者はインスタンスに触りません。
注意すること
足すのは通り、消すのは弾かれます。破壊的変更の判定は、CRDのスキーマ比較で行われます。Kroのソースコードが挙げている主なものです。
弾かれる | 通る |
|---|---|
プロパティの削除 | 必須でないプロパティの追加 |
型の変更 | enumの値を増やす |
必須フィールドの追加 | 説明文の変更 |
enumの値を狭める | 既定値の変更 |
パターンの変更 | requiredから外す |
検証用のRGDで実際にtierを消してみると、RGDはInactiveになり、conditionに理由が残りました。
KindReady=False cannot update CRD updprobes.example.org:
breaking changes detected: Property tier was removedInactiveはRGDの状態であって、公開済みのAPIの状態ではありません。この2つが分かれているので、更新に失敗しても影響が広がりません。
公開中のAPIは無傷です。
Inactiveのあいだも生成CRDには古いスキーマが残り、既存インスタンスはACTIVEのままでした。この状態で新しいインスタンスを作っても通ります。プラットフォームチームがRGDの更新に失敗している最中でも、開発者は止まりません。
どこで止まったかはconditionで読めます。
段階が分かれているので、コンパイルまで成功してCRD更新で止まったのか、その手前で落ちたのかが区別できます。
GraphAccepted=True resource graph and schema are valid GraphRevisionsResolved=True revision 3 compiled and active KindReady=False breaking changes detected: Property tier was removed既定値の変更は既存インスタンスへ遡及しません。
既定値はCRDのOpenAPIスキーマに載り、インスタンスを作った時点で一度だけspecへ書き込まれます。あとから既定を変えても、書き込み済みの値は動きません。tierの既定をstagingからproductionへ変えたところ、変更前に作ったインスタンスはstagingのままで、変更後に作ったものだけがproductionになりました。「入力に制約と既定値を置く」で1行の書き換えを示しましたが、それが効くのはこれから作るインスタンスです。既存を含めて変えたいなら、resources側の式に置きます。
resourcesだけ壊しても止まります。
schemaから項目を消してresourcesの参照を残すと、コンパイルの段階で落ちます。この場合もCRDは更新されず、前のGraphRevisionがactiveのまま残ります。消せない、型を変えられないという制約は、裏返せば公開したAPIが勝手に壊れないということです。Issue #883の「Current plan is to block breaking changes」は、この挙動をv1の方針として据える提案です。
ドリフトの是正
Instance Controllerは、生成した実リソースを監視し、定義と違う値になったら書き戻します。ArgoCDやKubeVelaのApplication Controllerと似た働きです。
起点は実リソースの変更イベントです。DynamicController内のWatchCoordinatorが逆引きのインデックスを持っていて、変更されたリソースから所有インスタンスを引き当てます。あとは通常のreconcileと同じ経路を通り、Server-Side Applyが定義どおりの値を書き戻します。既定3秒の再キュー間隔は待たずに動きます。
戻すのは定義に書いたフィールドだけです。RGDが定義していないキーを実リソースへ手で足すと、そのキーは残ります。Server-Side Applyのfield ownershipどおりで、テンプレートに書いたフィールドはKroが所有していますが、書いていないフィールドには所有者がいません。「定義にないものを消す」動きはしません。
Kroが止まっていると戻りません。イベントを受け取る主体がいなくなるためです。停止中に加えられた変更はそのまま残り、Kroを再開したときにまとめて是正されます。
良いRGD設計
公式ドキュメントには、RGDの作り方を3つに分けた設計指針があります。それぞれに「やるべきでない場合」も書かれています。
作り方 | 向いている場面 | 向かない場面 |
|---|---|---|
包む(1リソース) | 実装の詳細を隠したい/触ってよい入力を絞りたい/ラベル・命名・probeの既定を当てたい | 下位リソースがすでに組織の契約に合っている/結局その大半へ直接触る必要がある/名前を変えるだけで複雑さが減っていない |
束ねる(複数) | 常に一緒に動く前提のリソース群/セキュリティの束/付随するインフラ | 子リソースが独立したライフサイクルを要する/チームごとに違う組み合わせで再利用したい |
繋ぐ(RGD同士) | 部品が独立したAPIとして再利用される | 入れ子が深くなる(デバッグが多段になる) |
1つのリソースを包む
悪いほうは、名前を変えただけで開発者が書く量が減っていません。
# 悪い例: Deploymentのフィールドをそのまま横流しする
schema:
spec:
replicas: integer
image: string
strategyType: string
revisionLimit: integer
terminationGracePeriodSeconds: integer
# …以下、Deploymentの項目が続く
# 良い例: 決めたものは隠し、決めさせたいものだけ出す
schema:
spec:
image: string | required=true
replicas: integer | default=2 minimum=1 validation="self <= 20"
tier: string | default=staging enum="staging,production"下の書き方なら、更新戦略も終了猶予もresources側で決められます。開発者はそれを知らずに済み、あとで変えても開発者向けAPIは変わりません。
複数をまとめる
分ける基準はライフサイクルです。1つにまとめてよいのは、同時に生まれ、同時に消えるものだけです。
# 悪い例: 消えるタイミングが違うものを1枚に混ぜる
resources:
- id: deploy # Deployment。日に何度も入れ替わる
- id: svc # Service。同上
- id: db # データベース。他チームも接続している
- id: pvc # PersistentVolumeClaim。消えるとデータが戻らないこのRGDを公開すると、開発者がkubectl delete webservice shop-stgと打った時点で、データベースもPVCも消えます。Kroはインスタンスのfinalizerを起点に、生成したリソースを逆順で消すためです。アプリを作り直すつもりの1コマンドが、データを消すコマンドになります。
# 良い例: アプリを作り直すたびに作り直してよいものだけ
resources:
- id: sa
- id: secret
- id: config
- id: deploy
- id: svc共有リソースは別のRGDへ出し、externalRefで参照します。ドキュメントは、混ぜてはいけない場合をこう書いています。
同じチームが今たまたま両方を持っているというだけで、関係のない関心事を1つの束に混ぜているとき
組織図ではなくライフサイクルで切れ、という指針です。
RGD同士を繋ぐ
指針は3つです。
RGDを小さく保ち、入力と出力を明確にする
内側のRGDは、外側が必要とするstatusを公開する
内側のインスタンス名を外側の名前から導き、追跡できるようにする
内側と外側は、statusだけで繋ぎます。
# 内側: 作ったDeployment名をstatusに出す
kind: WebApplication
schema:
status:
deployName: ${deploy.metadata.name}
# 外側: 内側をリソースとして置き、statusを受け取る
kind: FullStackApp
resources:
- id: web
readyWhen:
- ${web.status.state == "ACTIVE"} # 内側が整うまで先へ進まない
template:
apiVersion: kro.run/v1alpha1
kind: WebApplication
metadata:
name: ${schema.spec.name}-web # 名前を外側から導く
- id: cfg
template:
data:
childDeploy: ${web.status.deployName} # 内側のstatusを参照外側のインスタンスを1つ適用すると、内側のインスタンスが生え、そのstatusが外側へ返りました。
$ kubectl get fullstackapp chain-demo
NAME STATE READY AGE
chain-demo ACTIVE True 15s
$ kubectl get cm chain-demo-cfg -o jsonpath='{.data}'
{"childDeploy":"chain-demo-web"}内側がstatusを出さなければ、外側は状態も計算値も取れません。readyWhenに書ける材料がなくなり、順序も組めなくなります。
入れ子の深さは、失敗したときに響きます。外側のconditionでどの内側が失敗したかを特定し、そのうえで内側のconditionを見るという多段の作業が要るためです。
3つに共通する重み
ドキュメントは、包むべきでない場合の最後にこう書いています。
下位のリソースが変わっていくあいだ、その包んだ側のAPIを自分たちが保守し続ける気がないなら
RGDを1つ公開すると、そのAPIを保守し続けることになります。
筆者は、使い分けの基準は価値がどこから来るかだと考えます。1つのリソースのAPIを作り直すことに価値があるなら包み、常に一緒に動くものを1つの束にすることに価値があるなら束ね、部品が独立したAPIとして再利用されるべきなら繋ぎます。
Kroの現在地
APIはまだv1alpha1です。それでもEKS Capabilitiesでのマネージド提供は始まっており、v1前のAPIがクラウドベンダの製品として出ている状態です。v1へ上がるときに破壊的変更が入る可能性は残ります。
公開から2年弱で、機能は積み上がっています。
時期 | バージョン | 何ができるようになったか |
|---|---|---|
2024年11月 | v0.1.0 | KubeCon NAでAWSが公開。RGDからCRDを生成する基本形 |
2025年1月 | v0.2.0 | Google・Microsoftが合流し、Kubernetes SIGへ移管 |
2026年1月 | v0.8.0 | 配列から複数リソースを展開できる(forEach)。スキーマの破壊的変更を検知して止める |
2026年3月 | v0.9.0 | CEL式で書けることが大幅に増える(JSON・マップ・リスト・quantity・乱数)。クラスタスコープの型を作れる。omit()でフィールドをキーごと省ける |
2026年4月 | v0.9.1 | 定義をコンパイル済みの不変スナップショットとして持つ(GraphRevision)。インスタンスの一時停止 |
2026年5月 | v0.9.2 | 生成CRDの互換チェックがminimum・maximumの変更も検知する |
2026年7月 | v0.9.3 | プラットフォームチームが独自のconditionを定義できる |
開発軸は2つあります。書ける表現を増やすことと、壊れる変更を止めることです。前者はCELの拡張とforEach、後者は破壊的変更の検知とGraphRevisionにあたります。後者の流れは2026年も続いており、その先にv1で何を保証するかという議論があります。
使っていて当たる制約も、Issueに挙がっています。
何に当たるか | Issue |
|---|---|
未インストールのCRDを参照するRGDが、includeWhenで守っていても検証で落ちる | |
フィールド単位で「キーごと省略」できない(omit()がAlphaで先行) | |
ResourcesReadyのメッセージが内部のノード名を出すため、どのリソースか分からない |
3つ目は、先ほどvalidation failedで見たものと同じ性質です。失敗は検出できても、メッセージが原因まで案内してくれません。
ロードマップは公開されていますが、v1の中身を決める作業は途中です。それが一番はっきり出ているのが、v1.0マイルストーンに置かれたIssue #883「Versioning and rollout of changes to ResourceGraphDefinitions」です。
論点は、既存インスタンスへの反映のしかた、更新を制御する主体、ロールバックの手順、入力の破壊的変更の扱いの4つです。この行方を、引き続き注視していきます。
KubeVelaとの比較
KubeVelaのComponentDefinitionは、自前のエンジンが読むCUEです。開発者に見せるパラメータはCRDになりません。KroのRGDはCRDを生成し、そのCRDが開発者向けAPIの実体になります。定義の実体の置き方が正反対なので、対比の材料として取り上げます。
両者は成熟度の段階が違います。KroはAPIがv1alpha1で、機能追加が続いている最中です。KubeVelaは第3回で見たとおり、OAM仕様の停滞という別種の不確実性を抱えています。比べるのは設計上の選択で、安全性ではありません。
定義を書くときの差
以下のKubeVela側の値は、筆者のチームが運用しているv1.8.2の実資産に基づきます。
CUEでは*が既定値、|が取りうる値の並びを表します。*containerPort | intは「整数型で、既定値はcontainerPortの値」という意味です。
containerPort: *port | int
probePort: *containerPort | int
livenessPort: *probePort | intport自体もserviceTypeで既定値が変わるため、livenessPortは4段のカスケードの先端にあります。KubeVelaはこれをparameterブロックに書けます。エンジンが自前なら、既定値の解決を何段重ねようと自由です。
Kroは同じことをCRDの生成でやります。SimpleSchemaのdefault=は、そのままJSONリテラルとして生成CRDへ書き込まれます。整数や真偽値はParseInt・ParseBoolを通るので、式を書けば弾かれます。他のフィールドを参照する既定値は、まるごとresources側の式へ移ります。
観点 | KubeVela v1.8.2(CUE) | Kro v0.9.3 |
|---|---|---|
定義の実体 | ComponentDefinition | 生成されたCRD |
定義の単位 | Component / Trait / WorkflowStep | ResourceGraphDefinition |
計算された既定値 | parameterに宣言できる | resources側の式へ移す |
定義を増やしたとき | CRDは増えない | RGDごとにCRDが1つ増える |
ただし、これでKroの公開APIに載る情報が減るわけではありません。KubeVelaのparameterはもともとCRDへ投影されないので、開発者がkubectl explainで既定値を読めない点は同じです。リテラルの既定値は、むしろKro側ではCRDに載ります。差が出るのは移行の作業量です。
運用したときの差
運用の差は一方向ではありません。検証と停止耐性はKro側に、revisionの固定と機能の後付けはKubeVela側に分があります。
観点 | KubeVela v1.8.2 | Kro v0.9.3 |
|---|---|---|
入力を検証するのは | KubeVelaのWebhookとcontroller | Kube API Server |
本体が停止したら | 検証も展開も止まる | 検証は通り続ける。 展開だけ止まる |
開発者への配り分け | KubeVela側の概念で制御 | ふつうのCRDなのでRBACがそのまま効く |
定義の更新 | DefinitionRevisionでtype: api@v3と固定できる | 全インスタンスへ即時 |
運用機能の後付け | Applicationに2行足せる | 手段がない |
本体が止まったときに何が残るか
KroのDeploymentをレプリカ0にしても、開発者はWebServiceを適用できました。CRDはクラスタに残っており、imageの必須もreplicasの上限も、Kube API Serverが変わらず検証します。止まるのは実リソースの展開だけで、APIとしての約束は生きたままです。
KubeVelaでは、この分業になりません。Application CRのpropertiesには、x-kubernetes-preserve-unknown-fieldsが付いています。つまりKube API Serverは中身を見ません。パラメータが正しいかどうかは、KubeVelaのエンジンだけが知っています。
エンジンが止まったときに何が起きるかは、admission webhookのfailurePolicyによって変わります。Failなら適用そのものが拒否され、Ignoreなら未検証のまま通ります。どちらであっても、検証の規則を持っているのはエンジンだけです。筆者はこの挙動を実機で確かめていません。
Kroを保守するチームが手を離しても、公開したAPIの入口は動き続けます。第1回の「開発者が何を知らなくて済むべきか」に答えを出したあと、その答えを守り続けるのは誰かという問題が残ります。
運用機能の後付け
筆者のチームの資産では、ComponentとTraitとWorkflowStepが役割ごとに分かれています。開発者はApplicationにこう書きます。
traits:
- type: writable-volume
この2行で、書き込み可能なvolumeという運用機能が付きます。同じ2行を、別のApplicationにも、別のチームのApplicationにも書けます。Traitは一度定義すれば、どのComponentにも後から重ねられる部品です。定義した数だけ、後付けできる運用機能が増えます。
Kroには、これに相当する仕組みがありません。RGDのresourcesが持つのは、idとテンプレート、readyWhen、includeWhen、forEach、externalRefだけです。他のRGDが管理するリソースへ差分を当てるフィールドはありません。
理由はCRDにしたことにあります。1つのKindは1つのRGDが所有し、別のRGDが同じKindを定義しようとするとCRD is owned by another ResourceGraphDefinitionで止まります。CRDが公開APIの全体である以上、外から要素を足す経路がありません。
同じことをKroでやるなら、writable-volume相当の分岐をWebServiceのRGDの中へ書き、schemaにフラグを1つ増やすことになります。別の型にも同じ機能が要るなら、その型のRGDにも書きます。Traitが「1回書いてN回使う」なのに対し、Kroは「使う型の数だけ書く」です。
これはKroの欠落というより守備範囲の違いです。Kroの守備範囲は合成までで、後付けが必要なら別のレイヤで実装することになります。
まとめ
Kroは、組織のApplication ModelをKubernetesのCRDとして公開するツールです。プラットフォームチームがRGDを定義すると、そこから型が生成され、開発者はその型のインスタンスだけ、つまりよりシンプルなマニフェストを書けます。定義の実体がKubernetes NativeのCRDなので、入力の検証はKube API Serverが行い、kubectlがそのまま使え、破壊的変更は構造的に拒否されます。Kroが止まっていても、公開したAPIは使えます。
ただ、足りないものはまだあります。定義の更新は、全インスタンスへ即時に及びます。v0.9.1で入ったGraphRevisionは過去のrevisionを保持しますが、既存のインスタンスを古いrevisionに固定する仕組みではありません。スキーマのバージョンを分けて共存させる手段もありません。RGDを書ける権限をどう分離するかも設計段階です。APIはまだv1alpha1で、現時点では本番への導入は容易ではありません。
それでも筆者はKroに期待しています。理由は機能の数ではなく、Application Model資産をどう運用し続けるかです。開発者に見せる範囲を仕様で決めようとしたOAMも、自前のエンジンで決めたKubeVelaも、その境界を守る主体を自分たちで抱えることになりました。KroはそれをKubernetes APIへ預けました。境界を守るのがKube API Serverなら、Kroを保守するチームがいなくなっても定義の資産は残ります。
次回の最終回では、この選択を軸に、この時代のPlatform EngineeringにふさわしいApplication Modelとは何かを整理します。
この記事をシェアしてください
