What is kro?
kro (Kube Resource Orchestrator) lets you turn a set of Kubernetes resources into a reusable API. You define the API schema, describe the resources behind it in YAML, and connect them with CEL expressions. kro turns that definition into a CRD, watches for instances of that API, and reconciles the underlying resources for each one.
A ResourceGraphDefinition (RGD) is the blueprint for a custom API: it describes the interface users work with and the resources each instance should produce. kro validates that definition before it ever reconciles an instance, catching schema errors, invalid expressions, and broken references before they become runtime failures.
How it works
A ResourceGraphDefinition has two parts: a schema that defines your API surface (the fields users fill in), and resource templates that reference those fields with CEL expressions. kro parses the expressions, infers the dependency graph, generates a CRD, and stands up a controller - all at runtime.
apiVersion: kro.run/v1alpha1 kind: WebApp metadata: name: my-app spec: image: nginx bucketName: my-app-assetsapiVersion: kro.run/v1alpha1 kind: WebApp metadata: name: my-app spec: image: nginx bucketName: my-app-assets status: bucketArn: arn:aws:s3:::my-app-assets endpoint: my-app.example.comspec: schema: kind: WebApp spec: image: string | default=nginx replicas: integer | default=1 bucketName: string | required=true status: bucketArn: ${bucket.status.arn} endpoint: ${service.status.endpoint}spec: resources: - id: config template: kind: ConfigMap # ... name: ${schema.metadata.name}-config - id: bucket template: kind: Bucket # ... name: ${schema.spec.bucketName} - id: deployment template: kind: Deployment # ... image: ${schema.spec.image} env: ${bucket.status.arn} - id: service template: kind: Service # ... name: ${deployment.metadata.name}In practice
Once the definition is installed, users work with the generated API like any other Kubernetes resource.
apiVersion: kro.run/v1alpha1
kind: WebApp
metadata:
name: my-app
spec:
image: nginx
bucketName: my-app-assets
$ kubectl get webapp my-app -o yaml
status:
bucketArn: arn:aws:s3:::my-app-assets
$ kubectl get configmap,bucket,deploy,svc
NAME AGE
configmap/my-app-config 45s
NAME AGE
bucket.s3.services.k8s.aws/my-app-assets 45s
NAME READY AGE
deployment.apps/my-app 1/1 45s
NAME TYPE PORT(S)
service/my-app ClusterIP 80/TCP
Users apply one WebApp. kro creates the ConfigMap, Bucket, Deployment, and Service, and writes useful outputs like bucketArn back onto the same object.
The definition behind it
A ResourceGraphDefinition defines the API under spec.schema (see Simple Schema) and the backing resources under spec.resources. CEL expressions (${}) are what tie those two parts together (this example uses S3 via ACK, but kro works with any Kubernetes resource - native or CRD. For example, Azure Blob Storage via ASO or GCP Cloud Storage via Config Connector).
apiVersion: kro.run/v1alpha1 kind: ResourceGraphDefinition metadata: name: webapp spec: schema: apiVersion: v1alpha1 kind: WebApp spec: image: string | default=nginx bucketName: string status: bucketArn: ${bucket.status.arn} resources: - id: config template: apiVersion: v1 kind: ConfigMap metadata: name: ${schema.metadata.name}-config data: APP_NAME: ${schema.metadata.name} - id: bucket template: apiVersion: s3.services.k8s.aws/v1alpha1 kind: Bucket metadata: name: ${schema.spec.bucketName} spec: name: ${schema.spec.bucketName} - id: deployment template: apiVersion: apps/v1 kind: Deployment metadata: name: ${schema.metadata.name} spec: selector: matchLabels: app: ${schema.metadata.name} template: metadata: labels: app: ${schema.metadata.name} spec: containers: - name: app image: ${schema.spec.image} envFrom: - configMapRef: name: ${config.metadata.name} env: - name: BUCKET_ARN value: ${bucket.status.arn} - id: service template: apiVersion: v1 kind: Service metadata: name: ${deployment.metadata.name} spec: selector: ${deployment.spec.selector.matchLabels}bucket.status.arn does not exist when you apply this definition. kro waits for it before reconciling deployment, and it knows service comes after deployment because the selector is derived from ${deployment.spec.selector.matchLabels}.
What kro handles for you
Once the graph is defined, kro takes care of the mechanics that usually end up in custom controller code.
image: string | default=nginxreplicas: integer | default=1 minimum=0bucketName: string | required=truemonitoring: boolean | default=falseSimpleSchema
Define your API schema inline — types, defaults, constraints, and validation in a single readable line. No OpenAPI boilerplate.
Schema docs →Wires data that doesn't exist yet
Reference status fields from resources that haven't been created. kro waits for the data to exist, then wires it into dependent resources.
CEL expressions →Infers ordering from expressions
You never declare resource order. kro reads your CEL expressions and builds the dependency graph automatically.
Dependency ordering →enableMonitoringConditional resources
Include or exclude entire subgraphs based on any CEL expression. When a condition is false, the resource and everything that depends on it are skipped.
Conditional resources →forEach: ${lists.range(3)}One template, many resources
forEach expands a single resource template into multiple resources from a list or range. Define once, create N.
Collections →Non-Turing complete by design
CEL always terminates, has no side effects, and is type-checked at apply time. You can prove what your definitions do.
Type checking →Get started
- Install kro to run the controller in your cluster
- Deploy your first RGD to create a new API end to end
- Browse examples to see more patterns
Need help or want to contribute? Join #kro on Kubernetes Slack, browse GitHub, or read Contributing.