Helm refuses to take over an object it did not create. This is deliberate: silently adopting resources would let a chart claim and then delete things belonging to something else.
Helm identifies its own objects by three pieces of metadata:
| Kind | Key | Value |
|---|---|---|
| Label | app.kubernetes.io/managed-by | Helm |
| Annotation | meta.helm.sh/release-name | the release name |
| Annotation | meta.helm.sh/release-namespace | the release namespace |
All three must match. The error names whichever is missing or wrong.
Look at the object
kubectl get configmap app-config -n production \
-o jsonpath='{.metadata.labels}{"\n"}{.metadata.annotations}{"\n"}'
{"app":"api"}
{"kubectl.kubernetes.io/last-applied-configuration":"..."}
No Helm metadata at all, so it was created with kubectl apply or by another tool.
Option 1: Adopt it
Non-destructive, and the right choice for anything holding data:
kubectl label configmap app-config -n production \
app.kubernetes.io/managed-by=Helm --overwrite
kubectl annotate configmap app-config -n production \
meta.helm.sh/release-name=api \
meta.helm.sh/release-namespace=production --overwrite
Then re-run the install. Helm now recognises the object and updates it in place.
For several objects at once:
for kind in configmap service deployment; do
kubectl get $kind -n production -o name | while read -r obj; do
kubectl label "$obj" -n production app.kubernetes.io/managed-by=Helm --overwrite
kubectl annotate "$obj" -n production \
meta.helm.sh/release-name=api \
meta.helm.sh/release-namespace=production --overwrite
done
done
Be careful with a loop like that on a shared namespace: it claims everything it touches for the release, and a later helm uninstall will then delete all of it. Scope it with a label selector.
The release-namespace annotation must be the namespace the release lives in, which is not always the object's namespace for cluster-scoped resources. A ClusterRole belonging to a release in production still gets meta.helm.sh/release-namespace: production.
Option 2: Delete and let Helm create it
kubectl delete configmap app-config -n production
helm upgrade --install api ./chart -n production
Fine for generated config, Services and Deployments. Not fine for:
- A PersistentVolumeClaim, where deleting may destroy the volume depending on the reclaim policy
- A Secret you cannot regenerate
- A Service with a
LoadBalancerwhose external IP is in DNS, since recreating it usually assigns a new one
For those, adopt rather than delete.
How this happens
Migrating from kubectl to Helm. Objects were applied by hand and the chart now defines the same names. Adoption is the intended path.
Two charts defining the same object. Often a subchart and a parent both rendering a ServiceAccount or a ConfigMap. Disable one:
# values.yaml
subchart:
serviceAccount:
create: false
name: shared-sa
A previous uninstall left objects behind. Anything annotated helm.sh/resource-policy: keep survives helm uninstall by design, and then blocks a fresh install. That annotation is deliberate for PVCs and CRDs, so removing the object is a decision, not a cleanup.
kubectl get all,pvc,secret -n production -o json \
| jq -r '.items[] | select(.metadata.annotations["helm.sh/resource-policy"]=="keep") | "\(.kind)/\(.metadata.name)"'
A namespace created by --create-namespace then reused. The Namespace object itself is not owned by the release, which is usually what you want.
CRDs are different
Custom Resource Definitions in a chart's crds/ directory are installed before templates render and are never upgraded or deleted by Helm. A CRD installed by one chart blocks another chart that also ships it:
kubectl get crd certificates.cert-manager.io \
-o jsonpath='{.metadata.annotations}'
The usual answer is to install the CRDs once, out of band, and disable them in both charts:
installCRDs: false
Finding the conflict before it happens
helm install api ./chart -n production --dry-run=server
A server-side dry run validates against the live cluster and reports conflicts without changing anything. Worth running in CI before a first install into an environment that was previously managed another way.
helm template alone does not do this, because it never talks to the cluster.
A checklist
- Read the object name, kind and namespace out of the error.
kubectl get <kind> <name> -o jsonpath='{.metadata.labels}{.metadata.annotations}'.- Holding data → adopt it by setting the label and two annotations.
- Regenerable → delete it and let Helm create it.
- Never delete a PVC, an unrecoverable Secret or a LoadBalancer Service to resolve this.
- Two charts rendering the same object → disable it in one.
- Survived an uninstall → look for
helm.sh/resource-policy: keep. helm install --dry-run=servercatches these before they fail a real install.
Frequently Asked Questions
Why will Helm not just take over an existing resource?
Because adopting silently would let any chart claim objects it did not create, and a later helm uninstall would then delete them. Helm therefore requires explicit ownership metadata: the label app.kubernetes.io/managed-by: Helm plus the annotations meta.helm.sh/release-name and meta.helm.sh/release-namespace. Setting them by hand is you stating that this object really does belong to that release, which is exactly the confirmation the check is asking for.
Is it safe to delete the conflicting resource?
It depends entirely on what it is. Deleting a ConfigMap, Service or Deployment that the chart will immediately recreate is usually harmless. Deleting a PersistentVolumeClaim can destroy the underlying volume depending on the StorageClass reclaim policy, deleting a Secret you cannot regenerate loses it, and deleting a LoadBalancer Service typically means a new external IP, which breaks DNS until it propagates. For any of those, adopt the object instead.
How do I migrate resources created with kubectl into a Helm release?
Add the ownership metadata to each object, then run helm upgrade --install. Helm recognises them and manages them from then on. Two cautions: apply the metadata only to objects that genuinely belong to the release, because anything you label becomes something helm uninstall will delete; and for cluster-scoped resources, the meta.helm.sh/release-namespace annotation must name the namespace the release lives in, not the object's namespace, since it has none.
Why does this happen with CRDs specifically?
Because CRDs in a chart's crds/ directory are installed before templates are rendered, and Helm deliberately never upgrades or deletes them, since doing so would destroy every custom resource in the cluster. So a CRD installed by one chart remains, unowned by any release, and blocks a second chart that also ships it. The standard resolution is to install CRDs once out of band and set the chart's installCRDs value, or its equivalent, to false in every chart that would otherwise provide them.
How can I catch these conflicts before running a real install?
Use helm install --dry-run=server, which renders the chart and validates it against the live cluster, reporting ownership conflicts without changing anything. helm template does not help here, because it renders locally and never contacts the cluster, so it cannot know what already exists. Running the server-side dry run in CI before the first install into an environment previously managed by kubectl turns this from a failed deployment into a build-time warning.