Kubernetesを使いこなす抽象化の技法 5

「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のように既定のComponentTraitモデルを使うのではなく、公開するAPIの語彙をプラットフォームチームが定義します。

登場するもの

誰が用意するか

位置づけ

ResourceGraphDefinition(RGD)

プラットフォームチーム

公開するAPIの形と、そこから作るリソースのテンプレート

生成されたCRD(以下、生成CRD)

Kro(RGDから自動生成)

開発者向けAPIの実体

そのCRDのインスタンス

開発者

公開されたAPIを使う

実リソース

Kro(インスタンスから展開)

DeploymentServiceなど

Kro202411月のKubeCon North Americaで公開されました。現在はKubernetes SIG Cloud Providerのサブプロジェクトで、AWSMicrosoft AzureGoogle Cloudをはじめとする各社のメンテナが開発しています(kubernetes-sigs/kro)。執筆時点の最新はv0.9.3(2026727日)、APIバージョンはv1alpha1です。

Adopt事例もかなりあります。Spotify20263月のKubeCon Europeで、kubebuilderでのoperator自作からKroと内製operatorへ移した事例を報告しました。AWS20251130日からAmazon EKS CapabilitiesKroをマネージドサービスとして提供しています。20267月のKubeCon Japanでは、SAPJakob Möller氏らが「Evolving Platform Primitives: Beautiful Platforms with kro」を発表しています。

Kroの使い方

RGDから公開APIができるまで

プラットフォームチームはRGDだけを書きます。中身はschemaresources2つに分かれます。以下では、コンテナを1つ動かすたびに書くDeploymentSecretServiceを、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行が、生成されるCRDGVK(Group / Version / Kind)になります。groupの既定はkro.runで、上の例のように自社ドメインへ置けます。GVK3つとも、作成後は変更できません。

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: 80

idは他のリソースから参照するための名前です。${}の中に書けるのは2種類だけです。schemaはインスタンス自身を指します(schema.spec.imageschema.metadata.name)。それ以外はresourcesidです。

${secret.metadata.name}は、Secretの名前だけを返します。Secret本体は同じRGDid: 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が付ける

複数形の導出と、StateReadyAgeの表示列は、Kroが付け足したものです。

同じRGDに書いた2つのブロックのうち、CRDになるのはschemaだけです。resourcesCRDに入らず、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

適用すると、SecretDeploymentServiceが揃い、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もそのまま通りました。status2つの値は、どちらもKroが作ったリソースから引いたものです。ServiceclusterIPDeploymentの実際のレプリカ数で、開発者はそのどちらも自分では書いていません。

開発者に提供するもの

開発者が読むのは、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

ComponentDefinitionCUE

KubeVelaのエンジン

Crossplane

XRD

生成CRDKube API Server

Kro

RGDschema

生成CRDKube API Server

守る仕組みがKube API Serverになるのは、Kroだけではありません。第4回で見たCrossplaneも、XRDからCRDを生成する同じ形です。分かれるのは書く場所のほうで、Crossplaneはスキーマ(XRD)とレシピ(Composition)を別のリソースに分け、Kro1つのRGDに収めています。

Kroでの答えはschemaに書いた項目で、裏返せば書かなかったものが「知らなくて済むこと」です。ここまでのWebServiceでは、開発者が書いたのはimagereplicastierapiKey4つだけです。次のものが視界から消えていました。

  • Secretの名前の付け方と、キー名(API_KEY)を環境変数へ入れる経路

  • DeploymentServiceを結ぶselectorとラベルの付け方

  • Serviceのポート番号

隠せる対象は、Kubernetesのリソースとして表現できるものに限られます。ただしその範囲は広く、組み込みのリソースだけでなく、他のcontrollerが管理するカスタムリソースもテンプレートに置けます。CrossplaneXRも、KubeVelaApplicationも、FluxHelmReleaseも、RGDから作れます。

条件は、そのCRDがクラスタに入っていることだけです。手元では、helm-controllerを入れずにHelmReleaseCRDだけを置いたクラスタでも、KroHelmReleaseを生成しました。CRDがないままだと、RGDInactiveで止まります。

ユースケースごとの表現力

先ほどのWebServiceに、ConfigMapServiceAccountIngressを足します。schemaにもconfigexposeExternallyの項目を増やしています。検証環境はkind上のKubernetes v1.34.0Krov0.9.3です。ここへstagingshop-stgproductionshop-prd2つのインスタンスを適用しました。

複数のリソースを組み立て、値を注入する

インスタンス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}

開発者はapiKeyconfigだけを書きます。そこから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のマーカーは、生成CRDOpenAPIスキーマへ落ちます。

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-validationsself <= 20に、immutable=trueself == 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行です。筆者のチームはKubeVelaComponentDefinitionCUEで書いて6環境ぶん運用していますが(第3回で扱ったX-Definitionの資産です)、ambientModeというフラグの既定値をtrueからfalseへ切り替えたとき、CUE側では6つの定義を書き換えることになりました。KroならambientMode: boolean | default=false1行です。

未指定のフィールドを、キーごと消す

同じ資産に、CronJobtimeZoneを足した変更があります。CUEではこう書きました。

if INPUT.timeZone != _|_ {
    timeZone: INPUT.timeZone
}

未指定なら、CronJobspecからtimeZoneというキーごと消えます。空文字を入れるのとは違います。Kubernetesには、キーがないことと空の値が入っていることで挙動が変わるフィールドがあるためです。

KroincludeWhenはリソース単位なので、これには使えません。三項演算子でフォールバックを書くと、キーは空文字で残ります。この不足はIssue #928として挙がっており、omit()というCEL関数が実装されています。

timeZone: '${schema.spec.timeZone != "" ? schema.spec.timeZone : omit()}'

ただしomit()はまだAlpha段階なので、既定で無効です。有効化には--feature-gates CELOmitFunction=trueが要ります。

Kroの仕組み

誰が何を呼ぶか

公式ドキュメントは、Kroの動きを8つの手順で説明しています。公式がUserとまとめている側は、実際にはプラットフォームチームと開発者に分かれます。

触るリソースは、役割ごとにはっきり分かれます。プラットフォームチームが適用するのは手順1RGDだけで、CRDは自分では作りません。開発者が適用するのは手順5のインスタンスだけで、DeploymentにもServiceにも直接は触れません。その間をつなぐCRDと実リソースは、Kroが作ります。

この分担のおかげで、片方が止まってももう片方は進みます。RGDを直している最中でも、既存のCRDは登録されたままなので開発者はインスタンスを適用できます。

Kroのシステム構成

3つのcontrollerは、3つのコンテナではありません。Podに入っているコンテナは1つです。その1プロセスで、3つが並行して動きます。HelmチャートのreplicaCountの既定も1です。

このPodの中で、それぞれが仕事を分担しています。

  • RGD Controller

    監視対象はResourceGraphDefinitionです。RGDが適用されると、schemaresourcesを検証し、CEL式の参照から依存グラフを組み、結果をGraphRevisionとして発行します。コンパイルが済むのを待って、CRDを登録します。そのCRD専用のreconcile処理を作り、DynamicControllerへ差し込みます。

  • GraphRevision Controller

    仕事はコンパイルです。GraphRevisionを見つけたらスナップショットをコンパイルし、プロセス内で共有するRegistry(図3GraphRevision 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秒後に再キューして待ちます。

    RGD1つ公開するたびに、CRDwatchInstance Controller1つずつ増えます。ただしwork queueは全GVR1本のままで、そこから取り出すworkerの数も設定値で決まります。処理の並行度は、公開した定義の数とは無関係です。

  • Registry

    コンパイル済みのグラフをプロセス内で共有する置き場です。GraphRevision Controllerが書き、Instance Controllerが読みます。RGDごとに直近5つのGraphRevisionを保持します。

定義の更新

RGDは一度書いて終わりではありません。項目を足し、既定値を変え、ときには消します。更新の手順そのものはkubectl applyだけですが、その裏で3つのcontrollerが順に動きます。

  • RGD Controllerが新しいRevisionを発行

    RGDspecからハッシュを取り、Registryにある最新のハッシュと比べます。違えば、新しいGraphRevisionを発行します。番号はr00001r00002と積まれていきます。

  • GraphRevision Controllerがコンパイル

    発行されたGraphRevisionをコンパイルし、成功すればRegistryへ載せてActiveにします。RGD Controllerが待っているのは、この完了です。この待ち合わせがあるので、コンパイルできない定義は公開されません。

  • RGD ControllerCRDを更新

    ここで新旧のスキーマを比較し、破壊的変更がないかを判定します。通れば、CRDにパッチを当てます。

  • RGD ControllerInstance Controllerを差し替え

    ここで既存インスタンスが全件キューへ戻り、それぞれが最新のグラフで組み立て直されます。開発者はインスタンスに触りません。

注意すること

足すのは通り、消すのは弾かれます。破壊的変更の判定は、CRDのスキーマ比較で行われます。Kroのソースコードが挙げている主なものです。

弾かれる

通る

プロパティの削除

必須でないプロパティの追加

型の変更

enumの値を増やす

必須フィールドの追加

説明文の変更

enumの値を狭める

既定値の変更

パターンの変更

requiredから外す

検証用のRGDで実際にtierを消してみると、RGDInactiveになり、conditionに理由が残りました。

KindReady=False  cannot update CRD updprobes.example.org:
                 breaking changes detected: Property tier was removed

InactiveRGDの状態であって、公開済みの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
  • 既定値の変更は既存インスタンスへ遡及しません。

    既定値はCRDOpenAPIスキーマに載り、インスタンスを作った時点で一度だけspecへ書き込まれます。あとから既定を変えても、書き込み済みの値は動きません。tierの既定をstagingからproductionへ変えたところ、変更前に作ったインスタンスはstagingのままで、変更後に作ったものだけがproductionになりました。「入力に制約と既定値を置く」で1行の書き換えを示しましたが、それが効くのはこれから作るインスタンスです。既存を含めて変えたいなら、resources側の式に置きます。

  • resourcesだけ壊しても止まります。

    schemaから項目を消してresourcesの参照を残すと、コンパイルの段階で落ちます。この場合もCRDは更新されず、前のGraphRevisionactiveのまま残ります。消せない、型を変えられないという制約は、裏返せば公開したAPIが勝手に壊れないということです。Issue #883の「Current plan is to block breaking changes」は、この挙動をv1の方針として据える提案です。

ドリフトの是正

Instance Controllerは、生成した実リソースを監視し、定義と違う値になったら書き戻します。ArgoCDKubeVelaApplication Controllerと似た働きです。

起点は実リソースの変更イベントです。DynamicController内のWatchCoordinatorが逆引きのインデックスを持っていて、変更されたリソースから所有インスタンスを引き当てます。あとは通常のreconcileと同じ経路を通り、Server-Side Applyが定義どおりの値を書き戻します。既定3秒の再キュー間隔は待たずに動きます。

戻すのは定義に書いたフィールドだけです。RGDが定義していないキーを実リソースへ手で足すと、そのキーは残ります。Server-Side Applyfield 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を自分たちが保守し続ける気がないなら

RGD1つ公開すると、そのAPIを保守し続けることになります。

筆者は、使い分けの基準は価値がどこから来るかだと考えます。1つのリソースのAPIを作り直すことに価値があるなら包み、常に一緒に動くものを1つの束にすることに価値があるなら束ね、部品が独立したAPIとして再利用されるべきなら繋ぎます。

Kroの現在地

APIはまだv1alpha1です。それでもEKS Capabilitiesでのマネージド提供は始まっており、v1前のAPIがクラウドベンダの製品として出ている状態です。v1へ上がるときに破壊的変更が入る可能性は残ります。

公開から2年弱で、機能は積み上がっています。

時期

バージョン

何ができるようになったか

202411

v0.1.0

KubeCon NAAWSが公開。RGDからCRDを生成する基本形

20251

v0.2.0

GoogleMicrosoftが合流し、Kubernetes SIGへ移管

20261

v0.8.0

配列から複数リソースを展開できる(forEach)。スキーマの破壊的変更を検知して止める

20263

v0.9.0

CEL式で書けることが大幅に増える(JSON・マップ・リスト・quantity・乱数)。クラスタスコープの型を作れる。omit()でフィールドをキーごと省ける

20264

v0.9.1

定義をコンパイル済みの不変スナップショットとして持つ(GraphRevision)。インスタンスの一時停止

20265

v0.9.2

生成CRDの互換チェックがminimummaximumの変更も検知する

20267

v0.9.3

プラットフォームチームが独自のconditionを定義できる

開発軸は2つあります。書ける表現を増やすことと、壊れる変更を止めることです。前者はCELの拡張とforEach、後者は破壊的変更の検知とGraphRevisionにあたります。後者の流れは2026年も続いており、その先にv1で何を保証するかという議論があります。

使っていて当たる制約も、Issueに挙がっています。

何に当たるか

Issue

未インストールのCRDを参照するRGDが、includeWhenで守っていても検証で落ちる

#1243

フィールド単位で「キーごと省略」できない(omit()Alphaで先行)

#928

ResourcesReadyのメッセージが内部のノード名を出すため、どのリソースか分からない

#1253

3つ目は、先ほどvalidation failedで見たものと同じ性質です。失敗は検出できても、メッセージが原因まで案内してくれません。

ロードマップは公開されていますが、v1の中身を決める作業は途中です。それが一番はっきり出ているのが、v1.0マイルストーンに置かれたIssue #883Versioning and rollout of changes to ResourceGraphDefinitions」です。

論点は、既存インスタンスへの反映のしかた、更新を制御する主体、ロールバックの手順、入力の破壊的変更の扱いの4つです。この行方を、引き続き注視していきます。

KubeVelaとの比較

KubeVelaComponentDefinitionは、自前のエンジンが読むCUEです。開発者に見せるパラメータはCRDになりません。KroRGDCRDを生成し、そのCRDが開発者向けAPIの実体になります。定義の実体の置き方が正反対なので、対比の材料として取り上げます。

両者は成熟度の段階が違います。KroAPIv1alpha1で、機能追加が続いている最中です。KubeVelaは第3回で見たとおり、OAM仕様の停滞という別種の不確実性を抱えています。比べるのは設計上の選択で、安全性ではありません。

定義を書くときの差

以下のKubeVela側の値は、筆者のチームが運用しているv1.8.2の実資産に基づきます。

CUEでは*が既定値、|が取りうる値の並びを表します。*containerPort | intは「整数型で、既定値はcontainerPortの値」という意味です。

containerPort: *port          | int
probePort:     *containerPort | int
livenessPort:  *probePort     | int

port自体もserviceTypeで既定値が変わるため、livenessPort4段のカスケードの先端にあります。KubeVelaはこれをparameterブロックに書けます。エンジンが自前なら、既定値の解決を何段重ねようと自由です。

Kroは同じことをCRDの生成でやります。SimpleSchemadefault=は、そのままJSONリテラルとして生成CRDへ書き込まれます。整数や真偽値はParseIntParseBoolを通るので、式を書けば弾かれます。他のフィールドを参照する既定値は、まるごとresources側の式へ移ります。

観点

KubeVela v1.8.2(CUE)

Kro v0.9.3

定義の実体

ComponentDefinition

生成されたCRD

定義の単位

Component / Trait / WorkflowStep

ResourceGraphDefinition

計算された既定値

parameterに宣言できる

resources側の式へ移す

定義を増やしたとき

CRDは増えない

RGDごとにCRD1つ増える

ただし、これでKroの公開APIに載る情報が減るわけではありません。KubeVelaparameterはもともとCRDへ投影されないので、開発者がkubectl explainで既定値を読めない点は同じです。リテラルの既定値は、むしろKro側ではCRDに載ります。差が出るのは移行の作業量です。

運用したときの差

運用の差は一方向ではありません。検証と停止耐性はKro側に、revisionの固定と機能の後付けはKubeVela側に分があります。

観点

KubeVela v1.8.2

Kro v0.9.3

入力を検証するのは

KubeVelaWebhookcontroller

Kube API Server

本体が停止したら

検証も展開も止まる

検証は通り続ける

展開だけ止まる

開発者への配り分け

KubeVela側の概念で制御

ふつうのCRDなのでRBACがそのまま効く

定義の更新

DefinitionRevisiontype: api@v3と固定できる

全インスタンスへ即時

運用機能の後付け

Application2行足せる

手段がない

本体が止まったときに何が残るか

KroDeploymentをレプリカ0にしても、開発者はWebServiceを適用できました。CRDはクラスタに残っており、imageの必須もreplicasの上限も、Kube API Serverが変わらず検証します。止まるのは実リソースの展開だけで、APIとしての約束は生きたままです。

KubeVelaでは、この分業になりません。Application CRpropertiesには、x-kubernetes-preserve-unknown-fieldsが付いています。つまりKube API Serverは中身を見ません。パラメータが正しいかどうかは、KubeVelaのエンジンだけが知っています。

エンジンが止まったときに何が起きるかは、admission webhookfailurePolicyによって変わります。Failなら適用そのものが拒否され、Ignoreなら未検証のまま通ります。どちらであっても、検証の規則を持っているのはエンジンだけです。筆者はこの挙動を実機で確かめていません。

Kroを保守するチームが手を離しても、公開したAPIの入口は動き続けます。第1回の「開発者が何を知らなくて済むべきか」に答えを出したあと、その答えを守り続けるのは誰かという問題が残ります。

運用機能の後付け

筆者のチームの資産では、ComponentTraitWorkflowStepが役割ごとに分かれています。開発者はApplicationにこう書きます。

traits:
  - type: writable-volume

この2行で、書き込み可能なvolumeという運用機能が付きます。同じ2行を、別のApplicationにも、別のチームのApplicationにも書けます。Traitは一度定義すれば、どのComponentにも後から重ねられる部品です。定義した数だけ、後付けできる運用機能が増えます。

Kroには、これに相当する仕組みがありません。RGDresourcesが持つのは、idとテンプレート、readyWhenincludeWhenforEachexternalRefだけです。他のRGDが管理するリソースへ差分を当てるフィールドはありません。

理由はCRDにしたことにあります。1つのKind1つのRGDが所有し、別のRGDが同じKindを定義しようとするとCRD is owned by another ResourceGraphDefinitionで止まります。CRDが公開APIの全体である以上、外から要素を足す経路がありません。

同じことをKroでやるなら、writable-volume相当の分岐をWebServiceRGDの中へ書き、schemaにフラグを1つ増やすことになります。別の型にも同じ機能が要るなら、その型のRGDにも書きます。Traitが「1回書いてN回使う」なのに対し、Kroは「使う型の数だけ書く」です。

これはKroの欠落というより守備範囲の違いです。Kroの守備範囲は合成までで、後付けが必要なら別のレイヤで実装することになります。

まとめ

Kroは、組織のApplication ModelKubernetesCRDとして公開するツールです。プラットフォームチームがRGDを定義すると、そこから型が生成され、開発者はその型のインスタンスだけ、つまりよりシンプルなマニフェストを書けます。定義の実体がKubernetes NativeCRDなので、入力の検証は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とは何かを整理します。

人気記事トップ10

人気記事ランキングをもっと見る

企画広告も役立つ情報バッチリ! Sponsored