HelmHelm

Error: rendered manifests contain a resource that already exists

Helm will not adopt an object it did not create. How to read the ownership metadata it checks, and adopt existing resources deliberately.

medium fix5 min read

the helm error
Error: INSTALLATION FAILED: Unable to continue with install: ConfigMap "app-config" in namespace "production" exists and cannot be imported into the current release: invalid ownership metadata; label validation error: missing key "app.kubernetes.io/managed-by": must be set to "Helm"

Error: rendered manifests contain a resource that already exists. Unable to continue with install: Service "api" in namespace "production" exists and cannot be imported into the current release

Do this first3 steps

Run these in order. Each one tells you what its output means before you change anything.

  1. 1

    Read the message, it names the exact object and the missing metadata

    kubectl get configmap app-config -n production -o jsonpath='{.metadata.labels}{"\n"}{.metadata.annotations}{"\n"}'

    Helm checks one label and two annotations. Whichever the error names is the one absent or wrong, and that is all it is complaining about.

  2. 2

    Adopt the object by adding the ownership metadata Helm expects

    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

    Helm then treats the object as part of the release and will update it in place. This is the non-destructive option and works for resources holding data you cannot recreate.

  3. 3

    Or delete it and let Helm create it, when it holds nothing you need

    kubectl delete configmap app-config -n production

    Safe for generated config and Services. Not safe for a PersistentVolumeClaim or a Secret you cannot regenerate, where adopting is the right call.

All 8 sections

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:

KindKeyValue
Labelapp.kubernetes.io/managed-byHelm
Annotationmeta.helm.sh/release-namethe release name
Annotationmeta.helm.sh/release-namespacethe 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 LoadBalancer whose 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

  1. Read the object name, kind and namespace out of the error.
  2. kubectl get <kind> <name> -o jsonpath='{.metadata.labels}{.metadata.annotations}'.
  3. Holding data → adopt it by setting the label and two annotations.
  4. Regenerable → delete it and let Helm create it.
  5. Never delete a PVC, an unrecoverable Secret or a LoadBalancer Service to resolve this.
  6. Two charts rendering the same object → disable it in one.
  7. Survived an uninstall → look for helm.sh/resource-policy: keep.
  8. helm install --dry-run=server catches 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.

Reference and practice

Learn the underlying concept

Other Helm errors