July 17, 2026
Two Tools, One Problem
Most platform teams end up using both Kustomize and Helm — Helm for vendor-distributed charts, Kustomize for everything else. The question isn’t which tool to use. It’s how to combine them when the vendor’s chart doesn’t do what you need.
This post covers four patterns for integrating Helm charts with Kustomize overlays in ArgoCD, when to use each, and the one technical distinction that determines which pattern will actually work: whether you need to add fields or remove them.
The examples come from two repos:
- cots-gitops-patterns — four integration patterns
- cots-override-demo — deep dive on JSON Patch vs. SSA
Pattern 1: Helm Primary, Kustomize Overlay (Multi-Source)
ArgoCD supports multi-source Applications — one source for the Helm chart, another for Kustomize patches:
apiVersion: argoproj.io/v1alpha1
kind: Application
spec:
sources:
- repoURL: https://vendor.example.com/charts
chart: their-app
targetRevision: 2.1.0
helm:
valuesFiles:
- $patches/values-override.yaml
- repoURL: https://github.com/my-org/platform-configs
path: overlays/their-app
ref: patches
Helm renders the chart first, then Kustomize applies strategic merge patches on top. The vendor chart is primary — you’re patching its output.
Use when: You need to add fields (extra labels, annotations, tolerations, resource limits) or modify values that the chart’s values.yaml doesn’t expose. Strategic merge works well here because you’re adding, not removing.
Pattern 2: Kustomize Primary, Helm as Input (Single Source)
Kustomize can render Helm charts directly via helmCharts in the kustomization:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
helmCharts:
- name: their-app
repo: https://vendor.example.com/charts
version: 2.1.0
releaseName: their-app
namespace: vendor-ns
valuesFile: values.yaml
patches:
- target:
kind: Deployment
name: their-app
patch: |-
apiVersion: apps/v1
kind: Deployment
metadata:
name: their-app
spec:
template:
spec:
securityContext:
runAsNonRoot: true
One ArgoCD Application, one source. Kustomize is the orchestrator — it pulls in the Helm chart as raw material and patches the output.
Use when: You want the simplest possible setup. Everything is one Application, sync status is clean. Good for single-chart vendors.
Pattern 3: App-of-Apps with Layered Overrides
Some vendors ship a parent Helm chart that creates ArgoCD Application CRs (app-of-apps pattern), each deploying a child chart. You need to override the children without breaking the parent’s orchestration.
The approach: two ArgoCD Applications coordinated by sync waves.
- Wave 0: Installs the vendor’s chart with minimal values. Uses
ignoreDifferenceson the fields that wave 1 will own. - Wave 1: Applies overrides via SSA (
ServerSideApply=true,Force=true) with retry/backoff for race conditions.
The two apps perform an “SSA handshake” — wave 0 explicitly ignores the fields that wave 1 manages, preventing sync loops where each app fights the other.
Use when: The vendor’s app-of-apps topology matters to you (separate Applications per component, independent sync/rollback) and you only need to add or update fields, not remove them.
Don’t use when: You need to remove fields. SSA can’t do that — see the next section.
Pattern 4: Kustomize with JSON Patch (RFC 6902)
Same as Pattern 2, but using JSON Patch instead of strategic merge:
patches:
- target:
kind: Deployment
name: their-app
patch: |-
- op: replace
path: /spec/template/spec/securityContext
value:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
- op: replace
path: /spec/template/spec/containers/0/securityContext
value:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
The replace operation overwrites the entire object at that path. The vendor’s runAsUser: 0 doesn’t get merged alongside your fix — it’s gone entirely.
Use when: The vendor’s chart sets fields that need to be deleted, not overridden. On OpenShift, this is almost always securityContext — vendor charts that set runAsUser: 0 or capabilities.add: [NET_BIND_SERVICE] can’t be fixed by adding fields alongside them. SCC checks the full context object.
The Decision Matrix
The core question: do you need to add fields or remove them?
| Need | Patch type | Patterns that work |
|---|---|---|
| Add fields (labels, resources, tolerations) | Strategic merge | 1, 2, 3 |
| Update values (replica count, image tag) | Strategic merge or Helm values | 1, 2, 3 |
Remove fields (runAsUser: 0, capabilities) |
JSON Patch replace |
4 (or Kyverno) |
| Replace entire objects (securityContext) | JSON Patch replace |
4 (or Kyverno) |
If you’re on OpenShift and the vendor’s chart runs as root, you almost certainly need Pattern 4 or Kyverno. Strategic merge will add runAsNonRoot: true alongside runAsUser: 0, and SCC will still reject the pod.
OpenShift-Specific Overrides
Beyond security contexts, there are a few OpenShift-specific resources that vendor charts don’t account for:
Routes instead of Ingress: Vendor charts create Kubernetes Ingress objects. On OpenShift, you typically want Routes with TLS edge termination. A strategic merge patch can add Routes, but you may also need to delete or ignore the vendor’s Ingress objects.
Resource quotas: Vendor charts often ship without resource requests/limits. OpenShift namespaces with quotas will reject pods that don’t declare resources. JSON Patch’s add operation handles this cleanly.
NetworkPolicy: If the vendor doesn’t ship NetworkPolicies, you can add them via Kustomize overlay. If they ship overly permissive ones, you need JSON Patch to replace them.
Further Reading
- cots-gitops-patterns — all four patterns with working ArgoCD Applications
- cots-override-demo — deep dive on JSON Patch, Kyverno, and why SSA fails for field removal
- Fixing Insecure COTS Helm Charts — the full story on the JSON Patch approaches and the SSA dead end
