Deploy Helm Charts
This tutorial walks you through deploying a Helm chart from an OCM component version to a Kubernetes cluster, using the OCM Controllers with kro and Flux.
What You’ll Learn
- Create and publish an OCM component version that references a Helm chart
- Define a ResourceGraphDefinition to orchestrate OCM and Flux resources
- Deploy the Helm chart to your cluster using the OCM Controllers
Estimated time
~20 minutes
Scenario
You as a developer create an application, packaged as a Helm chart, and publish it as OCM component version in an OCI registry. Then, you as an operator deploy the Helm chart into a Kubernetes cluster using the OCM Controllers. You define a ResourceGraphDefinition that tells kro how to orchestrate the OCM and Flux resources to deploy the Helm chart.
Prerequisites
- Controller environment set up (OCM Controllers, kro and Flux in a Kubernetes cluster)
- Custom RBAC configured to allow the controller to manage
ResourceGraphDefinitions - OCM CLI installed
- Access to an OCI registry (e.g., ghcr.io)
envsubstinstalled (pre-installed on most Linux/macOS systems; part ofgettext)
Environment Setup
Set environment variables for your GitHub username and OCM repository:
export GITHUB_USERNAME=<your-github-username>
export OCM_REPO=ghcr.io/$GITHUB_USERNAME/ocm-tutorialCreate and Publish a Component Version
Create a working directory
mkdir /tmp/helm-deploy && cd /tmp/helm-deployDefine the component
Create a
component-constructor.yamlfile that includes a Helm chart resource:components: - name: ocm.software/ocm-k8s-toolkit/simple provider: name: ocm.software version: "1.0.0" resources: - name: helm-resource type: helmChart version: 1.0.0 access: type: ociArtifact imageReference: "ghcr.io/stefanprodan/charts/podinfo:6.11.1@sha256:a9b2804ec61795a7457b2303bf9efbc5fba51f856c3945f3bb0af68bf3b35afd"This component references the
podinfoHelm chart, a simple web application that displays pod information.Build the component version
Now add the component version to a local Common Transport Format (CTF) archive. By default, the
ocm add cvcommand looks for a file calledcomponent-constructor.yamlin the current directory, and creates a CTF archive in a folder calledtransport-archive:ocm add cvYou should see this output
time=2026-02-25T12:47:09.622+01:00 level=WARN msg="could not get credential consumer identity for component version repository" repository=transport-archive error="failed to get component version repository: cannot resolve consumer identity for ctf: credentials not supported" COMPONENT │ VERSION │ PROVIDER ────────────────────────────────────┼─────────┼────────────── ocm.software/ocm-k8s-toolkit/simple │ 1.0.0 │ ocm.softwareTransfer to your registry
Use
ocm transfer cvand specify the correct reference (<path-to-your-ctf>//<component>:<version>) and target repository:NoteIf your registry requires authentication, configure
Credentials for OCM CLI first.
ocm transfer cv transport-archive//ocm.software/ocm-k8s-toolkit/simple:1.0.0 $OCM_REPOYou should see this output
Transferring component versions... ✓ transformOcmSoftwareOcmK8sToolkitSimple100Upload [OCIAddComponentVersion] [████████████████████████████████████████] 100% 1/1To make your component public in GitHub Container Registry, go to the
packagestab in your GitHub repositoryhttps://github.com/$GITHUB_USERNAME?tab=packages, select the packagecomponent-descriptors/ocm.software/ocm-k8s-toolkit/simple, and under “Package settings” change the visibility topublic.Alternatively, if you want to keep your package private, configure credentials for the OCM Controllers:
Configure credentials for private registries
Create a docker-registry secret with your registry credentials. For GitHub Container Registry, you can use a Personal Access Token or a short-lived token from the GitHub CLI:
kubectl create secret docker-registry ghcr-secret \ --docker-username=$GITHUB_USERNAME \ --docker-password="$(gh auth token)" \ --docker-server=ghcr.ioThen update the resources to use credentials:
- OCM Controller resources: Add
ocmConfigto the Repository resource in your RGD. The credentials propagate automatically to Component, Resource, and Deployer objects that reference this Repository:
- id: repository readyWhen: - ${repository.status.conditions.exists(c, c.type == 'Ready' && c.status == 'True')} template: apiVersion: delivery.ocm.software/v1alpha1 kind: Repository metadata: name: simple-repository spec: repositorySpec: baseUrl: $OCM_REPO type: OCIRegistry interval: 1m ocmConfig: - kind: Secret name: ghcr-secretFor more details, see
- OCM Controller resources: Add
Verify the upload
ocm get cv $OCM_REPO//ocm.software/ocm-k8s-toolkit/simple:1.0.0You should see this output
COMPONENT │ VERSION │ PROVIDER ─────────────────────────────────────┼─────────┼────────────── ocm.software/ocm-k8s-toolkit/simple │ 1.0.0 │ ocm.software
Deploy Helm chart using the OCM Controllers
Create ResourceGraphDefinition
The ResourceGraphDefinition tells kro how to orchestrate the OCM and Flux resources. Create
rgd.yamlwith the following content:ResourceGraphDefinition (rgd.yaml)
apiVersion: kro.run/v1alpha1 kind: ResourceGraphDefinition metadata: name: simple spec: schema: apiVersion: v1alpha1 # The name of the CRD that is created by this ResourceGraphDefinition when applied kind: Simple spec: # This spec defines values that can be referenced in the ResourceGraphDefinition and that can be set in the # instances of this ResourceGraphDefinition. # We will use it to pass a value to the Helm chart and configure the message the application shows # (see resource HelmRelease). message: string | default="foo" resources: # Repository points to the OCM repository in which the OCM component version is stored and checks if it is # reachable by pinging it. - id: repository readyWhen: - ${repository.status.conditions.exists(c, c.type == 'Ready' && c.status == 'True')} template: apiVersion: delivery.ocm.software/v1alpha1 kind: Repository metadata: name: simple-repository spec: repositorySpec: baseUrl: $OCM_REPO type: OCIRegistry interval: 1m # ocmConfig is required, if the OCM repository requires credentials to access it. # ocmConfig: # Component refers to the Repository, downloads and verifies the OCM component version descriptor. - id: component readyWhen: - ${component.status.conditions.exists(c, c.type == 'Ready' && c.status == 'True')} template: apiVersion: delivery.ocm.software/v1alpha1 kind: Component metadata: name: simple-component spec: repositoryRef: name: ${repository.metadata.name} component: ocm.software/ocm-k8s-toolkit/simple semver: 1.0.0 interval: 1m # ocmConfig is required, if the OCM repository requires credentials to access it. # ocmConfig: # Resource points to the Component, downloads the resource passed by reference-name and verifies it. It then # publishes the location of the resource in its status. - id: resourceChart readyWhen: - ${resourceChart.status.conditions.exists(c, c.type == 'Ready' && c.status == 'True')} template: apiVersion: delivery.ocm.software/v1alpha1 kind: Resource metadata: name: simple-resource spec: componentRef: name: ${component.metadata.name} resource: byReference: resource: name: helm-resource # This must match the resource name set in the OCM component version (see above) additionalStatusFields: # toOCI() converts the resource access to an OCI reference object containing registry, repository, tag, and digest oci: resource.access.toOCI() # ocmConfig is required, if the OCM repository requires credentials to access it. # ocmConfig: # OCIRepository watches and downloads the resource from the location provided by the Resource status. # The Helm chart location (url) refers to the status of the above resource. - id: ocirepository readyWhen: - ${ocirepository.status.conditions.exists(c, c.type == 'Ready' && c.status == 'True')} template: apiVersion: source.toolkit.fluxcd.io/v1 kind: OCIRepository metadata: name: simple-ocirepository spec: interval: 1m0s layerSelector: mediaType: "application/vnd.cncf.helm.chart.content.v1.tar+gzip" operation: copy url: oci://${resourceChart.status.additional.oci.registry}/${resourceChart.status.additional.oci.repository} ref: tag: ${resourceChart.status.additional.oci.tag} # secretRef is required, if the OCI repository requires credentials to access it. # secretRef: # HelmRelease refers to the OCIRepository, lets you configure the Helm chart and deploys the Helm chart into the # Kubernetes cluster. - id: helmrelease readyWhen: - ${helmrelease.status.conditions.exists(c, c.type == 'Ready' && c.status == 'True')} template: apiVersion: helm.toolkit.fluxcd.io/v2 kind: HelmRelease metadata: name: simple-helmrelease spec: releaseName: simple interval: 1m timeout: 5m chartRef: kind: OCIRepository name: ${ocirepository.metadata.name} namespace: default values: # We configure the Helm chart using Flux's HelmRelease 'values' field. We pass the value that we set in # the instance of the CRD created by the ResourceGraphDefinition (see below). ui: message: ${schema.spec.message}Apply the ResourceGraphDefinition
envsubst < rgd.yaml | kubectl apply -f -You should see this output
resourcegraphdefinition.kro.run/simple createdVerify it’s active:
kubectl get rgdYou should see this output
NAME APIVERSION KIND STATE AGE simple v1alpha1 Simple Active 19sA new Custom Resource Definition called
Simplethat you can now instantiate has been created.Create an instance
Create the file
instance.yamlto deploy the application:apiVersion: kro.run/v1alpha1 kind: Simple metadata: name: simple spec: message: "Deployed with OCM!"Deploy the application
kubectl apply -f instance.yamlYou should see this output
simple.kro.run/simple createdWait for the deployment to complete:
kubectl get simple -wYou should see this output
NAME STATE SYNCED AGE simple ACTIVE True 2mVerify the deployment
Check that the pod is running:
kubectl get deploymentsOutput:
NAME READY UP-TO-DATE AVAILABLE AGE simple-podinfo 1/1 1 1 3mVerify the custom message you configured in the HelmRelease values is showing up in the application:
kubectl get pods -l app.kubernetes.io/name=simple-podinfo \ -o jsonpath='{.items[0].spec.containers[0].env[?(@.name=="PODINFO_UI_MESSAGE")].value}'Output:
Deployed with OCM!
Troubleshooting
Authentication Errors
If you see errors like:
failed to list versions: response status code 401: unauthorizedYour registry package is private. Either:
- Make the package public in your registry settings, or
- Configure credentials as described in the collapsible section after “Transfer to your registry”
Resource Not Found
If the component isn’t found, verify:
- The component was transferred successfully:
ocm get cv $OCM_REPO//ocm.software/ocm-k8s-toolkit/simple:1.0.0 - The
baseUrlin the ResourceGraphDefinition matches your registry
RBAC Permission Errors
If the controller logs show permission errors like forbidden or cannot create resource, the controller lacks RBAC permissions to manage ResourceGraphDefinitions. Follow the
Custom RBAC guide to grant the necessary permissions.
Cleanup
Remove the deployed resources:
kubectl delete -f instance.yaml
kubectl delete -f rgd.yamlRemove the temporary files:
rm -rf /tmp/helm-deployNext Steps
- Tutorial: Create a Multi-Component Product - Learn how to structure complex applications with multiple components and resources
- Tutorial: Deploy a Helm Chart (with Bootstrap) - Use the OCM Controllers to deploy a Helm chart without manual bootstrapping, using GitOps
Related Documentation
- Concept: OCM Controllers - Learn about the architecture and capabilities of the OCM Controllers