Skip to main content
Version: 0.9.0

Single Resource RGD

A single-resource RGD puts a simpler API in front of one underlying resource. That resource might be a native Kubernetes object like a Deployment, or a provider CRD like an ACK Bucket or DBInstance. The point is to publish a better API than the raw resource already provides.

Why Wrap A Single Resource

Wrapping one resource is a good fit when you want to:

  • Hide provider-specific or controller-specific implementation details
  • Expose only the small set of inputs users should actually control
  • Apply platform defaults for labels, tags, naming, probes, security, or networking
  • Standardize resources that have unusual or inconsistent status shapes
  • Change the underlying implementation later without changing the user-facing API
  • Define readiness semantics that match the platform contract, not necessarily the child resource's native semantics
  • Define a clear readiness contract with readyWhen
  • Project a stable, easy-to-consume status back to users

In other words, the wrapper becomes the public contract. The wrapped resource is just the implementation behind that contract, and that implementation can change over time without forcing users to adopt a new API.

Typical Use Cases

1. Publish A Simpler API Over A Native Kubernetes Resource

Sometimes the underlying resource is standard Kubernetes, but still too low-level or annotation-heavy for your platform contract.

Example: wrap a Service of type LoadBalancer as PublicService:

apiVersion: kro.run/v1alpha1
kind: ResourceGraphDefinition
metadata:
  name: publicservice.platform
spec:
  schema:
    apiVersion: v1alpha1
    kind: PublicService
    spec:
      name: string
      app: string
      port: integer | default=8080
      scheme: string | default="internet-facing"
    status:
      published: ${service.status.?loadBalancer.?ingress.size() > 0}
      dnsName: ${service.status.?loadBalancer.?ingress[0].?hostname.orValue("")}

  resources:
    - id: service
      readyWhen:
        - ${service.status.?loadBalancer.?ingress.size() > 0}
      template:
        apiVersion: v1
        kind: Service
        metadata:
          name: ${schema.spec.name}
          annotations:
            service.beta.kubernetes.io/aws-load-balancer-scheme: ${schema.spec.scheme}
            service.beta.kubernetes.io/aws-load-balancer-type: nlb
        spec:
          type: LoadBalancer
          selector:
            app.kubernetes.io/name: ${schema.spec.app}
          ports:
            - port: ${schema.spec.port}
              targetPort: ${schema.spec.port}

Here the wrapper owns the cloud-specific annotations, defines Ready as "an external address has been assigned", and projects a simple DNS name into status.

2. Hide A Provider CRD Behind A Consumer-Facing Contract

Third-party resources often expose too many knobs and controller-specific status. A wrapper lets you expose only the fields your users should care about.

Example: wrap an ACK DBInstance as AppDatabase:

apiVersion: kro.run/v1alpha1
kind: ResourceGraphDefinition
metadata:
  name: appdatabase.platform
spec:
  schema:
    apiVersion: v1alpha1
    kind: AppDatabase
    spec:
      name: string
      storageGB: integer | default=20
      instanceClass: string | default="db.t3.micro"
    status:
      ready: ${database.status.?endpoint.?address.orValue("") != ""}
      endpoint: ${database.status.?endpoint.?address.orValue("")}

  resources:
    - id: database
      readyWhen:
        - ${database.status.?endpoint.?address.orValue("") != ""}
      template:
        apiVersion: rds.services.k8s.aws/v1alpha1
        kind: DBInstance
        metadata:
          name: ${schema.spec.name}
        spec:
          engine: postgres
          dbInstanceIdentifier: ${schema.spec.name}
          allocatedStorage: ${schema.spec.storageGB}
          dbInstanceClass: ${schema.spec.instanceClass}

Users interact with AppDatabase, not the ACK resource directly. The wrapper hides provider-specific fields, exposes a stable endpoint in status, and makes the wrapper instance Ready when the database is actually reachable by name.

3. Keep The User API Stable While The Implementation Changes

A wrapper also gives you room to change the backing implementation later without exposing that change to users.

Example: the public TeamBucket API can stay the same even if the wrapped bucket implementation changes:

# User-facing contract stays stable
schema:
  apiVersion: v1alpha1
  kind: TeamBucket
  spec:
    name: string
  status:
    bucketName: ${bucket.metadata.name}
# Today: TeamBucket wraps an internal MinIO bucket CRD (illustrative)
resources:
  - id: bucket
    template:
      apiVersion: storage.internal.example.com/v1alpha1
      kind: MinIOBucket
      metadata:
        name: ${schema.spec.name}
# Later: same TeamBucket API, backed by ACK S3 Bucket
resources:
  - id: bucket
    template:
      apiVersion: s3.services.k8s.aws/v1alpha1
      kind: Bucket
      metadata:
        name: ${schema.spec.name}
      spec:
        name: ${schema.spec.name}

The important part is that users still create TeamBucket with the same schema and consume the same status fields. Only the backing resource behind the wrapper changes.

4. Enforce Naming Conventions From Namespace And Labels

Sometimes the main value of a wrapper is not the spec shape but the naming policy. A wrapper can derive resource names from the instance namespace, labels, and a user-supplied base name so teams do not handcraft names differently.

Example: wrap a ServiceAccount as TeamServiceAccount:

apiVersion: kro.run/v1alpha1
kind: ResourceGraphDefinition
metadata:
  name: teamserviceaccount.platform
spec:
  schema:
    apiVersion: v1alpha1
    kind: TeamServiceAccount
    spec:
      name: string
    status:
      generatedName: ${serviceAccount.metadata.name}

  resources:
    - id: serviceAccount
      template:
        apiVersion: v1
        kind: ServiceAccount
        metadata:
          namespace: ${schema.metadata.namespace}
          name: ${schema.metadata.namespace + "-" + schema.metadata.?labels["team"].orValue("platform") + "-" + schema.spec.name + "-sa"}

Here the wrapper enforces a consistent prefix/suffix convention based on the instance namespace and team label, while still exposing the final generated name back in status.

5. Always Inject Provider Tags With merge()

For provider resources, wrappers can also enforce mandatory tags so every resource carries platform metadata such as tenant, namespace, cost center, or ownership.

Example: inject platform tags into an ACK Cluster while still allowing callers to pass extra tags:

schema:
  apiVersion: v1alpha1
  kind: TaggedCluster
  spec:
    name: string
    tags: 'map[string]string | default={}'

resources:
  - id: cluster
    template:
      apiVersion: eks.services.k8s.aws/v1alpha1
      kind: Cluster
      metadata:
        name: ${schema.spec.name}
      spec:
        # other required fields omitted for brevity
        tags: ${merge(schema.spec.tags, {
          "managed-by": "kro",
          "tenant": schema.metadata.?labels["tenant"].orValue("shared"),
          "namespace": schema.metadata.namespace
        })}

Putting the platform tags in the second argument to merge() makes them win on key conflicts, so the wrapper always injects the tags you require.

When Not To Wrap

Wrapping is usually the wrong fit when:

  • The underlying resource already matches the platform contract well enough
  • Users still need direct access to most of the child resource's fields and semantics
  • The wrapper is mostly a pass-through that renames fields without removing meaningful complexity
  • Different teams need materially different lifecycle, security, or policy behavior that one shared wrapper would force together
  • You are not prepared to own API evolution for the wrapper as the underlying resource changes over time

In those cases, letting users work with the native resource directly is often cleaner than publishing another API surface that adds little real value.

Limitations and Future Work

kro currently supports breaking change detection for schema fields (field removal, type changes, new required fields, enum restrictions, pattern changes) and blocks them by default. However, several areas are still evolving:

  • Versioning — multi-version CRD support and migration paths are not yet available. See KREP-009.
  • Resource lifecycle — fine-grained control over create/update/delete behavior per resource is planned. See KREP-014.
  • Deletion policy — configurable owner-reference and deletion semantics are under proposal. See KREP-004.

Keep these gaps in mind when deciding whether to publish a wrapper — you will own its API surface and any future migrations.

Single Resource RGD vs. Grouping

Single-resource abstractions and grouping are related, but they solve different problems:

  • Single resource means an RGD provides a better API in front of one lower-level resource
  • Grouping means an RGD manages several tightly-coupled resources as one unit

Use a single-resource abstraction when you want to standardize one resource behind a clean platform API. Use grouping when the value comes from packaging related resources together.

For more, see Multi Resource RGD and RGD Chaining.

Operational Notes

  • If you wrap a third-party CRD, both the CRD and its controller must already be installed in the cluster
  • In aggregation RBAC mode, kro needs permissions for the wrapped resource type. See Access Control
  • Once users depend on the wrapper, you own its upgrade story. If the wrapped resource changes required fields, status shape, or version, evolve the wrapper deliberately instead of silently breaking consumers
  • Prefer additive schema changes when possible. If you need to make a breaking change to the wrapper contract, plan an explicit migration. kro does not provide multi-version managed APIs today, so breaking changes usually mean introducing a new API surface instead of bumping versions in place
  • Treat the wrapper instance as the source of truth; direct edits to the wrapped resource are drift
  • Surface the outputs users actually need so they do not have to inspect the underlying resource during normal workflows

Brought to you with ♥ by SIG Cloud Provider