はじめに
以前からちょっと気になっていた kro (Kube Resource Orchestrator) というツールについて調べてみた
kro とは
kro は Kube Resource Orchestrator の略称で、Kubernetes リソースやクラウドプロバイダのリソースをまとめて Kubernetes カスタム API として定義・管理できるツールらしい
Reference: kro.run
現在の最新バージョンは 0.3.0
クラウドリソースを操作できる、かつリソース管理もできるので Helm + Crossplane みたいなイメージを持ったけど、Kubernetes カスタム API としてリソースを管理するので、使用感はちょっと違う?
(Crossplane は触ったことないのでわかりません)
Google Cloud, AWS, Microsoft Azure の共同プロジェクトとのこと
発音は crow (カラス) らしい
試してみる
Getting started をやってみる
まずは kind でクラスタを起動する
$ kind create cluster --name kro
続いて kro をインストールする
kro のインストールには Helm が必要らしい
$ export KRO_VERSION=$(curl -sL \
https://api.github.com/repos/kro-run/kro/releases/latest | \
jq -r '.tag_name | ltrimstr("v")'
)
$ echo $KRO_VERSION
0.3.0
$ helm install kro oci://ghcr.io/kro-run/kro/kro \
--namespace kro \
--create-namespace \
--version=${KRO_VERSION}
$ helm -n kro list
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
kro kro 1 2025-06-22 09:30:03.163727 +0900 JST deployed kro-0.3.0 0.3.0
Pod が作成されて、おそらくこれがコントローラーだと思われる
$ kubectl get po -n kro
NAME READY STATUS RESTARTS AGE
kro-c8fb7b586-w5qnw 1/1 Running 0 3m42s
次に Kubernetes のカスタム API を定義するための ResourceGraphDefinition を作成する
$ cat <<'EOF' | kubectl apply -f -
apiVersion: kro.run/v1alpha1
kind: ResourceGraphDefinition
metadata:
name: my-application
spec:
# kro uses this simple schema to create your CRD schema and apply it
# The schema defines what users can provide when they instantiate the RGD (create an instance).
schema:
apiVersion: v1alpha1
kind: Application
spec:
# Spec fields that users can provide.
name: string
image: string | default="nginx"
ingress:
enabled: boolean | default=false
status:
# Fields the controller will inject into instances status.
deploymentConditions: ${deployment.status.conditions}
availableReplicas: ${deployment.status.availableReplicas}
# Define the resources this API will manage.
resources:
- id: deployment
template:
apiVersion: apps/v1
kind: Deployment
metadata:
name: ${schema.spec.name} # Use the name provided by user
spec:
replicas: 3
selector:
matchLabels:
app: ${schema.spec.name}
template:
metadata:
labels:
app: ${schema.spec.name}
spec:
containers:
- name: ${schema.spec.name}
image: ${schema.spec.image} # Use the image provided by user
ports:
- containerPort: 80
- id: service
template:
apiVersion: v1
kind: Service
metadata:
name: ${schema.spec.name}-service
spec:
selector: ${deployment.spec.selector.matchLabels} # Use the deployment selector
ports:
- protocol: TCP
port: 80
targetPort: 80
- id: ingress
includeWhen:
- ${schema.spec.ingress.enabled} # Only include if the user wants to create an Ingress
template:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ${schema.spec.name}-ingress
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/healthcheck-path: /health
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}]'
alb.ingress.kubernetes.io/target-group-attributes: stickiness.enabled=true,stickiness.lb_cookie.duration_seconds=60
spec:
rules:
- http:
paths:
- path: "/"
pathType: Prefix
backend:
service:
name: ${service.metadata.name} # Use the service name
port:
number: 80
EOF
$ kubectl get resourcegraphdefinition
NAME APIVERSION KIND STATE AGE
my-application v1alpha1 Application Active 54s
最後にインスタンスと呼ばれてるものを作成する
$ cat <<'EOF' | kubectl apply -f -
apiVersion: kro.run/v1alpha1
kind: Application
metadata:
name: my-application-instance
spec:
name: my-awesome-app
ingress:
enabled: false
EOF
$ kubectl get application
NAME STATE SYNCED AGE
my-application-instance ACTIVE True 16s
すると、Deployment (Pod) と Service が作成されていることが確認できた
$ kubectl get all
NAME READY STATUS RESTARTS AGE
pod/my-awesome-app-7bd85c5d65-f6xz9 1/1 Running 0 54s
pod/my-awesome-app-7bd85c5d65-kxvbh 1/1 Running 0 54s
pod/my-awesome-app-7bd85c5d65-r4mzz 1/1 Running 0 54s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 7m
service/my-awesome-app-service ClusterIP 10.96.192.166 <none> 80/TCP 44s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/my-awesome-app 3/3 3 3 54s
NAME DESIRED CURRENT READY AGE
replicaset.apps/my-awesome-app-7bd85c5d65 3 3 3 54s
ResourceGraphDefinition とは
先ほどの my-awesome-app という Kubernetes リソースを作成するための Kubernetes カスタム API を定義しているのが ResourceGraphDefinition
実際にカスタムリソース定義を確認してみると、resourcegraphdefinitions.kro.run と applications.kro.run の 2 種類存在する
resourcegraphdefinitions.kro.run で applications.kro.run を定義し、applications.kro.run で定義される Kubernetes API を呼び出して、Kubernetes にリソースを作成している
$ kubectl get crd
NAME CREATED AT
applications.kro.run 2025-06-22T00:38:50Z
resourcegraphdefinitions.kro.run 2025-06-22T00:30:01Z
ResourceGraphDefinition で定義できるのはドキュメント によると、以下の 5 つ
- schema: ユーザがインスタンスを作成する際に指定できるフィールドを定義する
- resources: 作成されるリソースを定義する
- dependencies: リソース間の依存関係を定義する
- conditions: リソースを作成する際の条件を定義する
- status: カスタムリソースが公開するステータスフィールドを定義する
先ほど作成した ResourceGraphDefinition と照らし合わせて見ていく
まずは schema から
ここでは apiVersion, kind および spec といった、よくある Kubernetes API のスキーマを定義していて、これが複数の Kubernetes リソースをまとめて作成するための Kubernetes API になる
spec は Key/Value 形式で Key と Type を指定していて、default も指定できる
schema:
apiVersion: v1alpha1
kind: Application
spec:
name: string
image: string | default="nginx"
ingress:
enabled: boolean | default=false
status には deploymentConditions と availableReplicas が定義されていて、Deployment ステータスを参照していることがわかる
schema
status:
deploymentConditions: ${deployment.status.conditions}
availableReplicas: ${deployment.status.availableReplicas}
先ほど作成した Deployment のステータスを確認してみると、conditions と availableReplicas が含まれていることがわかる
$ kubectl get deploy my-awesome-app -o yaml | yq eval '.status' -
availableReplicas: 3
conditions:
- lastTransitionTime: "2025-06-30T01:31:12Z"
lastUpdateTime: "2025-06-30T01:31:12Z"
message: Deployment has minimum availability.
reason: MinimumReplicasAvailable
status: "True"
type: Available
- lastTransitionTime: "2025-06-30T01:31:01Z"
lastUpdateTime: "2025-06-30T01:31:12Z"
message: ReplicaSet "my-awesome-app-7bd85c5d65" has successfully progressed.
reason: NewReplicaSetAvailable
status: "True"
type: Progressing
observedGeneration: 1
readyReplicas: 3
replicas: 3
updatedReplicas: 3
インスタンスの status を確認してみると、それらがそれぞれ deploymentConditions と availableReplicas として定義されていることがわかる
$ kubectl get application my-application-instance -o yaml | yq eval '.status' -
availableReplicas: 3
conditions:
- lastTransitionTime: "2025-06-30T01:31:14Z"
message: Instance reconciled successfully
observedGeneration: 1
reason: ReconciliationSucceeded
status: "True"
type: InstanceSynced
deploymentConditions:
- lastTransitionTime: "2025-06-30T01:31:12Z"
lastUpdateTime: "2025-06-30T01:31:12Z"
message: Deployment has minimum availability.
reason: MinimumReplicasAvailable
status: "True"
type: Available
- lastTransitionTime: "2025-06-30T01:31:01Z"
lastUpdateTime: "2025-06-30T01:31:12Z"
message: ReplicaSet "my-awesome-app-7bd85c5d65" has successfully progressed.
reason: NewReplicaSetAvailable
status: "True"
type: Progressing
state: ACTIVE
status は Argo CD の Custom Health Checks と連携してリソースの状態を監視したり、additionalPrinterColumns を利用して、kubectl get application の出力をカスタマイズするなどが主な用途?
続いて resources
Kubernetes リソースの Deployment, Service, Ingress が定義されていて、インスタンスを作成すると、これら 3 つのリソースをコントローラーが作成してくれる
そして ${schema.spec.name} や ${schema.spec.image} など schema で定義したフィールドだったり、${deployment.spec.selector.matchLabels} や ${service.metadata.name} など他リソースのフィールドを参照することもできる
resources:
- id: deployment
template:
apiVersion: apps/v1
kind: Deployment
metadata:
name: ${schema.spec.name}
spec:
replicas: 3
selector:
matchLabels:
app: ${schema.spec.name}
template:
metadata:
labels:
app: ${schema.spec.name}
spec:
containers:
- name: ${schema.spec.name}
image: ${schema.spec.image}
ports:
- containerPort: 80
- id: service
template:
apiVersion: v1
kind: Service
metadata:
name: ${schema.spec.name}-service
spec:
selector: ${deployment.spec.selector.matchLabels}
ports:
- protocol: TCP
port: 80
targetPort: 80
- id: ingress
includeWhen:
- ${schema.spec.ingress.enabled}
template:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ${schema.spec.name}-ingress
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/healthcheck-path: /health
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}]'
alb.ingress.kubernetes.io/target-group-attributes: stickiness.enabled=true,stickiness.lb_cookie.duration_seconds=60
spec:
rules:
- http:
paths:
- path: "/"
pathType: Prefix
backend:
service:
name: ${service.metadata.name}
port:
number: 80
試しに spec.image に httpd を指定してインスタンスを作成してみる
$ cat <<'EOF' | kubectl apply -f -
apiVersion: kro.run/v1alpha1
kind: Application
metadata:
name: my-httpd-application-instance
spec:
name: my-httpd-app
image: httpd
ingress:
enabled: false
EOF
$ kubectl get application
NAME STATE SYNCED AGE
my-application-instance ACTIVE True 29m
my-httpd-application-instance ACTIVE True 15s
image が httpd として Deployment が作成できている
$ kubectl get deploy my-httpd-app -o yaml | yq eval '.spec.template.spec.containers[]' -
image: httpd
imagePullPolicy: Always
name: my-httpd-app
ports:
- containerPort: 80
protocol: TCP
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
さらに、includeWhen を使うことで、Ingress リソースを作成するかどうかを制御でき、これが前述の conditions に該当する
今回 ingress.enabled を false としてインスタンスを作成したので、Ingress リソースは作成されなかった
resources:
- id: ingress
includeWhen:
- ${schema.spec.ingress.enabled}
template:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ${schema.spec.name}-ingress
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/healthcheck-path: /health
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}]'
alb.ingress.kubernetes.io/target-group-attributes: stickiness.enabled=true,stickiness.lb_cookie.duration_seconds=60
spec:
rules:
- http:
paths:
- path: "/"
pathType: Prefix
backend:
service:
name: ${service.metadata.name}
port:
number: 80
最後に dependencies だが、ドキュメントを読む限り明示的に定義する必要はなく、自動的に依存関係が解決されるものと思われる
あまり自信はないが、実際に処理されているのはこのあたりだろう
アクセス制御
コントローラーの権限管理は Kubernetes の RBAC が利用でき、2 種類のモードがある
デフォルトは unrestricted モードで、クラスタ内のすべてのリソースタイプに対し完全なアクセス権を付与する ClusterRole が作成されるとのこと
$ kubectl get clusterrole kro-cluster-role -o yaml | yq eval '.rules' -
- apiGroups:
- '*'
resources:
- '*'
verbs:
- '*'
一方、aggregation モードでは、rbac.kro.run/aggregate-to-controller: "true" というラベルが付与された ClusterRole のルールを動的に取り込む、集約 ClusterRole が作成される
ちょっとややこしいが、複数の ClusterRole を組み合わせて、コントローラーが必要とする権限を提供する仕組み
aggregation モードで最初に付与される権限は以下の 2 つ
- ResourceGraphDefinition へのフルアクセス
- CustomResourceDefinition へのフルアクセス
Helm で rbac.mode に aggregation を指定することで、有効にできるとのことなので試してみる
$ helm upgrade kro oci://ghcr.io/kro-run/kro/kro \
--namespace kro \
--set rbac.mode=aggregation
2 つの ClusterRole が作成された
$ kubectl get clusterrole | (head -n 1 && grep kro)
NAME CREATED AT
kro:controller 2025-06-30T02:05:23Z
kro:controller:static 2025-06-30T02:05:23Z
ServiceAccount と紐づいているのは kro:controller で、前述の権限を持っている
$ kubectl get clusterrolebinding | (head -n 1 && grep kro)
NAME ROLE AGE
kro:controller ClusterRole/kro:controller 2m15s
$ kubectl get clusterrole kro:controller -o yaml | yq eval '.rules' -
- apiGroups:
- kro.run
resources:
- resourcegraphdefinitions
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- kro.run
resources:
- resourcegraphdefinitions/finalizers
verbs:
- update
- apiGroups:
- kro.run
resources:
- resourcegraphdefinitions/status
verbs:
- get
- patch
- update
- apiGroups:
- apiextensions.k8s.io
resources:
- customresourcedefinitions
verbs:
- get
- list
- watch
- patch
- update
- delete
kro:controller:static には rbac.kro.run/aggregate-to-controller: "true" というラベルが付与されていて
$ kubectl get clusterrole kro:controller:static -o yaml | yq eval '.metadata.labels' -
app.kubernetes.io/component: controller
app.kubernetes.io/instance: kro
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: kro
app.kubernetes.io/part-of: kro
app.kubernetes.io/version: 0.3.0
helm.sh/chart: kro-0.3.0
rbac.kro.run/aggregate-to-controller: "true"
ServiceAccount と紐づいている kro:controller の権限は kro:controller:static でコントロールしている
$ kubectl get clusterrole kro:controller:static -o yaml | yq eval '.rules' -
- apiGroups:
- kro.run
resources:
- resourcegraphdefinitions
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- kro.run
resources:
- resourcegraphdefinitions/finalizers
verbs:
- update
- apiGroups:
- kro.run
resources:
- resourcegraphdefinitions/status
verbs:
- get
- patch
- update
- apiGroups:
- apiextensions.k8s.io
resources:
- customresourcedefinitions
verbs:
- get
- list
- watch
- patch
- update
- delete
この状態でインスタンスを作成しても、コントローラーに十分な権限がなくリソースは作成されない
$ cat <<'EOF' | kubectl apply -f -
apiVersion: kro.run/v1alpha1
kind: Application
metadata:
name: my-application-instance-2
spec:
name: my-awesome-app-2
ingress:
enabled: false
EOF
$ kubectl get application
NAME STATE SYNCED AGE
my-application-instance ACTIVE True 38m
my-application-instance-2 63s
my-httpd-application-instance ACTIVE True 9m42s
ラベル rbac.kro.run/aggregate-to-controller: "true" を持つ、Deployment, Service, Ingress および ResourceGraphDefinition で定義した Kubernetes API へのフルアクセス権限をもった ClusterRole を作成する
$ cat <<'EOF' | kubectl apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: kro:custom-role
labels:
rbac.kro.run/aggregate-to-controller: "true"
rules:
- apiGroups:
- apps
resources:
- deployments
verbs:
- '*'
- apiGroups:
- ''
resources:
- services
verbs:
- '*'
- apiGroups:
- networking.k8s.io
resources:
- ingresses
verbs:
- '*'
- apiGroups:
- kro.run
resources:
- applications
- applications/status
verbs:
- '*'
EOF
想定通り ClusterRole kro:controller が更新される
$ kubectl get clusterrole kro:controller -o yaml | yq eval '.rules' -
- apiGroups:
- kro.run
resources:
- resourcegraphdefinitions
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- kro.run
resources:
- resourcegraphdefinitions/finalizers
verbs:
- update
- apiGroups:
- kro.run
resources:
- resourcegraphdefinitions/status
verbs:
- get
- patch
- update
- apiGroups:
- apiextensions.k8s.io
resources:
- customresourcedefinitions
verbs:
- get
- list
- watch
- patch
- update
- delete
- apiGroups:
- apps
resources:
- deployments
verbs:
- '*'
- apiGroups:
- ""
resources:
- services
verbs:
- '*'
- apiGroups:
- networking.k8s.io
resources:
- ingresses
verbs:
- '*'
- apiGroups:
- kro.run
resources:
- applications
- applications/status
verbs:
- '*'
権限が付与されたため、先ほど作成したインスタンスから Deployment と Service が作成された
$ kubectl get application
NAME STATE SYNCED AGE
my-application-instance ACTIVE True 43m
my-application-instance-2 ACTIVE True 5m55s
my-httpd-application-instance ACTIVE True 14m
$ kubectl get deploy
NAME READY UP-TO-DATE AVAILABLE AGE
my-awesome-app 3/3 3 3 43m
my-awesome-app-2 3/3 3 3 2m57s
my-httpd-app 3/3 3 3 14m
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 50m
my-awesome-app-2-service ClusterIP 10.96.246.77 <none> 80/TCP 3m1s
my-awesome-app-service ClusterIP 10.96.192.166 <none> 80/TCP 43m
my-httpd-app-service ClusterIP 10.96.99.242 <none> 80/TCP 14m
コントローラーの利用する RBAC はもちろん重要
ただ、実際に利用する際は ResourceGraphDefinition で作成する Kubernetes カスタム API をテンプレートのように活用するはずなので、こちらの RBAC を適切にコントロールするのも重要
もちろん ResourceGraphDefinition を自由に作成されてしまうと困るため、ResourceGraphDefinition の RBAC もきちんとコントロールする必要があると思う
クラウドプロバイダのリソース管理
冒頭でも触れたように、kro では、Kubernetes リソースだけでなく、クラウドプロバイダのリソースも管理することができるらしい (大変そうだったので検証はしてない)
公式の Examples によると、例えば Google Cloud であれば、KCC を利用して GKE Cluster などを管理できるとのこと
Reference: kro.run
まとめ
冒頭で Helm + Crossplane みたいなイメージと記載したが、触ってみた感じ Helm とは全くの別物という感想
(Crossplane は存在を知っているが、触れたことはないのでわかりません)
Helm が豊富なチャートを活用して Kubernetes のエコシステム全体の管理に強みを持つのに対し、kro は Kubernetes リソースをカスタム API としてカプセル化し、特定のサービスやプロダクトに最適化されたテンプレートを通じて、アプリケーションを管理するのに特化している印象です
Helm でも同様のことはできなくないと思うが、私自身は Go Template を書くよりも、YAML を書く方がまだいい
ただし、現状はループ処理などが扱えないなどの制約もありそうなので、複雑なことを求めると大変かもしれない
大量の ResourceGraphDefinition と戦う日がくるのか・・・
References