CltOps
Tutorial

How to extend and customize an Helm chart

#helm#chart#infra#cloud

When working with Helm, you’ll often need to go beyond the default manifests provided by a chart. There are multiple strategies to extend and customize charts depending on whether they come from a traditional Helm repository or from an OCI registry (stored as artifacts).

Traditional Helm Repository vs OCI Registry

helm repo add grafana https://grafana.github.io/helm-charts
helm template grafana/grafana
helm template oci://ghcr.io/grafana/helm-charts/grafana

The customization methods are the same in both cases.

1. Override Values

Every chart exposes default values through values.yaml. Always start here.

helm show values grafana/grafana > example.yaml

Also visible here.

Create a new file my-values.yaml:

namespaceOverride: monitoring

ingress:
  enabled: true
  hosts:
    - grafana.example.local

persistence:
  enabled: true
  size: 5Gi

extraEnv:
  - name: GF_SECURITY_ALLOW_EMBEDDING
    value: "true"

Every variable added in this file overrides the default config provided with the Helm chart.

helm template grafana grafana/grafana -f my-values.yaml

You should now be able to see the impacted changes in the output of the previous command.

2. Add Extra Manifests

If you need resources not covered by the chart (e.g., NetworkPolicy, ServiceMonitor):

a. Inline with extraObjects (if supported)

The Grafana chart includes extraObjects:

extraObjects:
  - apiVersion: networking.k8s.io/v1
    kind: NetworkPolicy
    metadata:
      name: grafana-allow-same-namespace
    spec:
      podSelector:
        matchLabels:
          app.kubernetes.io/name: grafana
      ingress:
        - from:
            - podSelector:
                matchLabels:
                  access-to-grafana: "true"
      policyTypes:
        - Ingress

b. Wrapper Chart (Subchart Pattern)

Instead of forking Grafana’s chart, you create your own chart that declares Grafana as a dependency. This lets you:

Step 1. Create a new chart

helm create my-grafana
cd my-grafana

When you create your own chart (wrapper), you’ll always end up with the same minimal structure:

my-grafana/
├── Chart.yaml
├── templates
└── values.yaml

If you run the command helm template . (here . being the current folder path), you should now see the example files from the templates folder rendered.

In our case you can empty the templates folder so we can start with a clean chart.

You can then update the Chart.yaml file to add the dependency section at the end of the file:

dependencies:
  - name: grafana
    version: 10.0.0
    repository: https://grafana.github.io/helm-charts

You can then run the command:

helm dependency update

A new folder charts/ appears alongside a Chart.lock file:

my-grafana/
├── Chart.lock
├── charts
├── Chart.yaml
├── templates
└── values.yaml

If you run again the command helm template ., you should now see the entire Grafana chart rendered.

If you create a new file templates/network-policy.yaml:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: grafana-allow-same-namespace
spec:
  podSelector:
    matchLabels:
      app.kubernetes.io/name: grafana
  ingress:
    - from:
        - podSelector:
            matchLabels:
              access-to-grafana: "true"
  policyTypes:
    - Ingress

The helm template command should now include this extra manifest in the output.

You can also override the entire values.yaml content and add the overrides we specified in step one to impact the Grafana manifests:

namespaceOverride: monitoring

ingress:
  enabled: true
  hosts:
    - grafana.example.local

persistence:
  enabled: true
  size: 5Gi

extraEnv:
  - name: GF_SECURITY_ALLOW_EMBEDDING
    value: "true"

3. Extra Templating

Helm charts use Go templating to replace variables from values.yaml and chart metadata into Kubernetes manifests. This is what makes charts reusable and configurable across environments.

As an example, let’s take the custom NetworkPolicy we added earlier. Instead of hardcoding grafana, we can update our file templates/network-policy.yaml to use Helm template variables:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: {{ .Release.Name }}-allow-same-namespace
spec:
  podSelector:
    matchLabels:
      app.kubernetes.io/name: {{ .Chart.Name }}
  ingress:
    - from:
        - podSelector:
            matchLabels:
              access-to-{{ .Chart.Name }}: "true"
  policyTypes:
    - Ingress

Any values defined in the values.yaml can be accessed with {{ .Values.example }} inside a templated file.

This is much more efficient than hardcoding values. By leveraging templating, you can make your extra manifests dynamic and environment-aware, just like the original chart’s templates.

← Back to Guidebook