Helm, Kustomize and GitOps with Argo CD · Kustomize Without the Mess
Patches That Delete What You Meant to Keep
A JSON patch replacing a list or a strategic merge patch with the wrong merge key silently drops configuration; check the rendered output.
Scenario
Example company: production needs one extra environment variable, REGION=eu-west-1. A teammate adds a JSON 6902 patch that "adds" it.
What you see in production
After the deploy, the API fails to start: DATABASE_URL is not set. The patch replaced the whole env list with a list containing only REGION.
The naive code
patches:
- target: {kind: Deployment, name: api}
patch: |
- op: replace # replaces the entire list
path: /spec/template/spec/containers/0/env
value: [{name: REGION, value: eu-west-1}]
Reproduce it
The same intent written three ways, rendered with kubectl kustomize:
mkdir -p base
cat > base/deployment.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata: {name: api}
spec:
selector: {matchLabels: {app: api}}
template:
metadata: {labels: {app: api}}
spec:
containers:
- name: api
image: example/api:1.4.2
env:
- {name: DATABASE_URL, value: postgres://db/shop}
- {name: LOG_LEVEL, value: info}
EOF
printf 'resources: [deployment.yaml]\n' > base/kustomization.yaml
render() {
mkdir -p "o-$1"
printf 'resources: [../base]\npatches:\n - target: {kind: Deployment, name: api}\n patch: |\n%s\n' "$2" > "o-$1/kustomization.yaml"
echo "$1: $(kubectl kustomize o-$1 | grep -oE 'name: (DATABASE_URL|LOG_LEVEL|REGION)' | sed 's/name: //' | tr '\n' ' ')"
}
render replace ' - op: replace
path: /spec/template/spec/containers/0/env
value: [{name: REGION, value: eu-west-1}]'
render add ' - op: add
path: /spec/template/spec/containers/0/env/-
value: {name: REGION, value: eu-west-1}'
render strategic ' apiVersion: apps/v1
kind: Deployment
metadata: {name: api}
spec:
template:
spec:
containers:
- name: api
env: [{name: REGION, value: eu-west-1}]'
Output
replace: REGION add: DATABASE_URL LOG_LEVEL REGION strategic: REGION DATABASE_URL LOG_LEVEL
Run this on your computer — see “Get Started” for setup.
Root cause
JSON 6902 patches are positional: replace on a list path replaces the whole list. Strategic merge patches merge lists of known Kubernetes types by a merge key (name for containers and env), but only for built-in types; for custom resources they also replace lists. Without looking at the rendered output, both mistakes go unnoticed.
The fix
- To append to a list with JSON 6902, use
op: addwith path.../-. - Prefer strategic merge patches for built-in resources: env, containers, volumes merge by name.
- For custom resources, use JSON patches with explicit indexes or restructure the base.
- Render overlays in CI and assert critical settings exist (for example a policy that every API container has
DATABASE_URL).
Trade-offs
- JSON 6902 indexes break when the base list order changes.
- Strategic merge semantics differ per field (some lists replace); check the API reference's patch strategy.
Detect it in production
- CI assertions on rendered manifests for required environment variables and settings.
- Startup failures right after configuration-only deploys.
Key points
- JSON patch
replaceon a list replaces all of it;addwith/-appends. - Strategic merge patches merge built-in lists by key; custom resources don't get this.
- Always check rendered output and assert critical settings.
Further reading: Kustomize patches · Strategic merge patch
Practice questions
What does a JSON 6902 replace operation on an env list do?
Answer
It replaces the entire list with the given value, dropping every existing entry not in the patch.
How do you append one item to a list with a JSON patch?
Answer
Use op: add with the list path followed by /- (the end of the array).
Why are strategic merge patches safer for built-in resources?
Answer
They merge lists such as containers and env by their merge key (name), so adding one entry keeps the others; custom resources don't have merge keys, so lists are replaced.
Open in the app to mark lessons complete, take notes and track your progress.