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.