Skip to main content

Walkthrough: Bring Your Own Helm Chart

The Installable and Installation CRDs are two halves of one flow:

  • Installablewhat to deploy: a Helm chart, its version, and a values template.
  • Installationwhere to deploy it: which cluster(s), plus any per-cluster overrides.

This walkthrough takes you end-to-end using a real public chart — Bitnami Redis — so the focus stays on the CRDs, not the application. By the end you'll have authored both resources, stored them in your org's .platform repo, and watched ArgoCD deploy Redis to a cluster.

What You'll Learn

  • How to author an Installable for a public Helm chart, including a values template
  • How to author an Installation that targets one cluster, then several
  • How to apply a per-destination override (a different value on one cluster)
  • How the resources flow through .platform → ArgoCD → your cluster
  • How deletion works (auto-prune is intentionally disabled)

Prerequisites

  • Access to your GitHub org's .platform repository.
  • The names of your provisioned clusters ({solution}-{env}-{region} — see Discovering Available Cluster Names).
  • ArgoCD access for your target environment.

The Big Picture

  Installable (what)          Installation (where)
┌────────────────┐ ┌───────────────────────┐
│ chart: redis │◄─────────│ installableRef: redis │
│ repo, version │ refers │ destinations: │
│ values template│ to │ - acme-corp-dev-... │
└────────────────┘ │ - acme-corp-prd-... │
└───────────┬───────────┘
│ installation-operator
▼ renders one per destination
┌───────────────────────┐
│ ArgoCD Application │──► Redis runs on
│ redis-<cluster> │ each target cluster
└───────────────────────┘

You author the two CRDs; the installation-operator (running on the orchestration cluster) renders one ArgoCD Application per destination and hands off the actual sync to ArgoCD.


Step 1 — Author the Installable (what to deploy)

Create an Installable describing the Redis chart. Because you're introducing this chart yourself, you author it in your own org's .platform repo — it lands in your org namespace on the orchestration cluster (leave metadata.namespace unset; it's applied for you).

note

Widely-shared installables instead live in the central p6m-run/installables catalog in the installables namespace — see the namespace note below.

apiVersion: p6m.dev/v1alpha1
kind: Installable
metadata:
name: redis
spec:
source:
type: helm
repoURL: registry-1.docker.io/bitnamicharts
chart: redis
targetRevision: 27.0.15
namespace: redis
releaseName: redis
# Rendered as a Helm values file. {{variable}} tokens are interpolated
# per-destination from that cluster's cluster-info (see Template Variables).
template: |
global:
security:
allowInsecureImages: true
image:
registry: docker.io
repository: bitnamilegacy/redis
tag: 8.2.1
architecture: standalone
auth:
enabled: false
commonLabels:
p6m.dev/cluster: "{{cluster.name}}"
p6m.dev/environment: "{{environment.name}}"

A few things worth calling out:

  • repoURL / chart / targetRevision point at any Helm chart source. Here it's Bitnami's OCI registry, pinned to chart version 27.0.15.
  • namespace is where the chart's resources land on the target cluster; the operator creates it for you (CreateNamespace=true).
  • releaseName — always set this explicitly, even when it matches the name. See Best Practice: Set a releaseName.
  • template is a Helm values file with {{variable}} tokens. Those tokens are filled in per-destination from each cluster's cluster-info — so the same Installable adapts to wherever it lands. See Cluster Info Template Variables.
Use the chart's OCI registry, not the legacy HTTP repo

Use registry-1.docker.io/bitnamicharts (Bitnami's OCI registry), not the old https://charts.bitnami.com/bitnami HTTP repo. Bitnami now publishes OCI-style entries in the legacy HTTP index that ArgoCD's chart fetcher rejects with invalid_reference: invalid tag; the OCI registry works cleanly. How an OCI chart is split across the two fields — registry path in repoURL, chart name in chart, no oci:// prefix — is standard ArgoCD Helm behavior, documented in ArgoCD's Helm source guide.

Why the bitnamilegacy image override?

Bitnami's free docker.io/bitnami/* images stopped publishing in 2025; the community snapshot now lives under docker.io/bitnamilegacy/*. Pointing the image at bitnamilegacy/redis:8.2.1 with allowInsecureImages: true is a good example of using template to make a public chart work in your environment. Full background in Common Observations below.


Step 2 — Author the Installation (where to deploy)

An Installation references the Installable and lists the destination cluster(s). Start with a single cluster:

apiVersion: p6m.dev/v1alpha1
kind: Installation
metadata:
name: redis
spec:
installableRef:
kind: Installable
name: redis
namespace: <your-github-org> # where your Installable lands (org namespace)
destinations:
- clusterRef:
name: acme-corp-dev-us-east-2

spec.destinations[].clusterRef.name must match a provisioned cluster name ({solution}-{env}-{region}). The operator looks up that cluster's cluster-info to resolve the {{variables}} in your template and to name the resulting ArgoCD project.


Step 3 — Deploy to multiple clusters

Add more entries under destinations to fan the same Installable out across clusters:

  destinations:
- clusterRef:
name: acme-corp-dev-us-east-2
- clusterRef:
name: acme-corp-stg-us-east-2
- clusterRef:
name: acme-corp-prd-us-east-2

The operator renders one ArgoCD Application per destination (redis-acme-corp-dev-us-east-2, redis-acme-corp-stg-us-east-2, …). Because the template pulls values from each cluster's cluster-info, the p6m.dev/cluster and p6m.dev/environment labels come out different on each cluster with no extra work.


Step 4 — Apply a per-destination override

Sometimes one cluster needs something different. Use overrides on a destination to change chart source fields or values for just that cluster, without touching the shared Installable. Here, production pins a different chart version and turns on persistence:

  destinations:
- clusterRef:
name: acme-corp-dev-us-east-2
- clusterRef:
name: acme-corp-prd-us-east-2
overrides:
source:
targetRevision: 27.0.14 # pin prod to a specific chart version
template: |
global:
security:
allowInsecureImages: true
image:
registry: docker.io
repository: bitnamilegacy/redis
tag: 8.2.1
architecture: standalone
auth:
enabled: false
master:
persistence:
enabled: true
size: 8Gi

overrides.source accepts the same fields as the Installable's spec.source; only the fields you set are overridden, the rest are inherited. Override template values support the same {{variable}} interpolation. See Per-Destination Overrides.


Step 5 — Store both resources in .platform and open a PR

In your org's .platform repository, add a single file installations/redis.yaml holding both resources, separated by ---:

# installations/redis.yaml
apiVersion: p6m.dev/v1alpha1
kind: Installable
metadata:
name: redis
spec:
source:
type: helm
repoURL: registry-1.docker.io/bitnamicharts
chart: redis
targetRevision: 27.0.15
namespace: redis
releaseName: redis
template: |
global:
security:
allowInsecureImages: true
image:
registry: docker.io
repository: bitnamilegacy/redis
tag: 8.2.1
architecture: standalone
auth:
enabled: false
commonLabels:
p6m.dev/cluster: "{{cluster.name}}"
p6m.dev/environment: "{{environment.name}}"
---
apiVersion: p6m.dev/v1alpha1
kind: Installation
metadata:
name: redis
spec:
installableRef:
kind: Installable
name: redis
namespace: <your-github-org> # the org namespace on the orchestration cluster
destinations:
- clusterRef:
name: acme-corp-dev-us-east-2

Everything under installations/ in the .platform repo is managed by the {github-org}-installations root ArgoCD application. Open a PR and merge to main; ArgoCD picks it up from there. (Keeping the Installable next to its Installation is the common pattern for a chart you're introducing yourself. Widely-shared installables instead live in the central p6m-run/installables catalog and are referenced by name.)

Namespaces

On the orchestration cluster, Installations live in your org namespace and Installables you author yourself land there too — so .platform installations/*.yaml files leave metadata.namespace unset and point installableRef.namespace at the org. The dedicated installables namespace (shown on the Installable concept page) is where the shared catalog (p6m-run/installables) lives; reference those with installableRef.namespace: installables.


Step 6 — Validate via ArgoCD

Once the root app syncs, the operator renders one child ArgoCD Application per destination. For the acme-corp-dev-us-east-2 destination above, the operator produces (lines with a # comment are derived per-destination from that cluster's cluster-info):

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: redis-acme-corp-dev-us-east-2 # <installation-name>-<cluster>
spec:
project: dev-aws-us-east-2 # from cluster-info: {env}-{cloud}-{region}
destination:
namespace: redis # from spec.source.namespace
server: https://<api-endpoint-of-acme-corp-dev-us-east-2> # the target cluster's API server, from cluster-info
source:
repoURL: registry-1.docker.io/bitnamicharts
chart: redis
targetRevision: 27.0.15
helm:
releaseName: redis
values: | # your template, with {{variables}} resolved
global:
security:
allowInsecureImages: true
image:
registry: docker.io
repository: bitnamilegacy/redis
tag: 8.2.1
architecture: standalone
auth:
enabled: false
commonLabels:
p6m.dev/cluster: "acme-corp-dev-us-east-2" # {{cluster.name}} -> resolved
p6m.dev/environment: "dev" # {{environment.name}} -> resolved
syncPolicy:
automated:
prune: false # deletes are never automatic (see Teardown)
syncOptions:
- CreateNamespace=true
- ServerSideApply=true

Validation happens in the ArgoCD UI — ArgoCD itself runs on an orchestration vcluster you don't have direct kubectl access to. Open your org's ArgoCD ({github-org}-argocd.o11n.p6m.run; see the ArgoCD Cheat Sheet for login and navigation) and confirm:

  • An application named redis-<cluster> (here redis-acme-corp-dev-us-east-2) exists and is Synced / Healthy.
  • Its resource tree shows redis-master-0 Running in the redis namespace, on the bitnamilegacy/redis:8.2.1 image.

If you have kubectl access to the target cluster (the one Redis was deployed to, not the ArgoCD vcluster), you can double-check the workload directly:

$ kubectl get pods -n redis
NAME READY STATUS RESTARTS AGE
redis-master-0 1/1 Running 0 68s

$ kubectl get pod redis-master-0 -n redis -o jsonpath='{.spec.containers[0].image}'
docker.io/bitnamilegacy/redis:8.2.1
Auto-prune is disabled

The operator sets syncPolicy.automated.prune: false on every Application it creates. Syncing will add and update resources, but it will never delete them automatically — deletions are always a deliberate, manual step (see below).


Teardown (Optional)

To undo everything above, reverse the steps. Because auto-prune is disabled, removing the resources from .platform does not tear down what's already deployed — you delete the child Applications by hand in the ArgoCD UI. This undoes Steps 5–6 (and, by removing the Installable, Steps 1–4):

  1. Remove installations/redis.yaml from .platform. Open a PR against your org's .platform repo deleting the file (it holds both the Installable and the Installation), and merge to main.

  2. Refresh and sync the root app. In the ArgoCD UI, find the {github-org}-installations application, click Refresh, then Sync. Once synced, the redis-<cluster> child Application shows as OutOfSync with a prune indicator — but is not removed, because auto-prune is off.

  3. Delete the per-destination Application(s). For each destination you deployed to (here just redis-acme-corp-dev-us-east-2), open the three-dot (⋮) menu on that Application and select Delete. This removes Redis from the target cluster.

Confirm in the ArgoCD UI that the {github-org}-installations root app is Synced / Healthy and no redis-* Application remains. For the full reference (including the Kargo caveat and the "verify no Installation still references this Installable" check before removing a shared one), see Deleting an Installation and Deleting an Installable.


Common Observations

Why is prune: false on the rendered Application?

The operator disables auto-prune on every Application it creates, so a sync never deletes resources on its own. This prevents an accidental removal from .platform (or a bad render) from tearing down a running install. Deletions are always the deliberate manual step in Teardown.

Why point the image at bitnamilegacy instead of bitnami?

As of 2025 Bitnami moved its maintained catalog behind Bitnami Secure, and the free docker.io/bitnami/* images (plus the chart's default latest tags) stopped being published. The community images now live under docker.io/bitnamilegacy/* — a frozen snapshot — so we override the image and pin a tag that exists there (8.2.1), with global.security.allowInsecureImages: true to allow the non-default registry.

Why the OCI registry-1.docker.io/bitnamicharts repoURL and not https://charts.bitnami.com/bitnami?

The legacy HTTP repo now serves OCI-style index entries that ArgoCD's chart fetcher rejects (invalid_reference: invalid tag). Pointing repoURL at Bitnami's OCI registry avoids that. This applies to any Bitnami chart, not just Redis.

Do I need a cd block or Kargo?

No. The base "bring your own chart" flow renders a plain ArgoCD Application and syncs it directly. A cd block (and Kargo) only comes into play when you want automated promotion between environments — see the Installation reference.

Where does the Application object itself live?

The operator creates it in the orchestration cluster's ArgoCD namespace, named <installation>-<cluster> (e.g. redis-acme-corp-dev-us-east-2) — not in your org namespace, where the Installable/Installation CRs live.


Troubleshooting

SymptomCause / Fix
ArgoCD app ComparisonError: … invalid_reference: invalid tagThe repoURL is the legacy https://charts.bitnami.com/bitnami HTTP repo. Switch to the OCI registry registry-1.docker.io/bitnamicharts (chart name stays in chart:, no oci:// prefix).
Pods stuck ImagePullBackOff / ErrImagePull on a bitnami/* imageThe free bitnami/* tag no longer exists. Override the image to bitnamilegacy/<name> with a tag present in that repo, and set global.security.allowInsecureImages: true.
App is Synced but resources you removed are still runningExpected — auto-prune is disabled. Delete the child Application(s) manually (see Teardown).
installableRef not resolving / Installation never produces an Applicationspec.installableRef.namespace must match the namespace the Installable actually lives in (your org namespace for a self-authored one; installables for the shared catalog).
{{variable}} tokens render emptyThe value isn't present in that cluster's cluster-info, or isn't available for the cluster type — see Cluster Info Template Variables.

For general deployment issues, see the Troubleshooting Guide.


Platform CRDs

Tools & external references