no matches for kind "X" in version "Y" means kubectl asked the API server for a resource type and the server said it does not serve that type at that version. There are only two reasons:
- The version is wrong. The kind exists, at a different
apiVersion. This is a deprecation. - The kind does not exist. A CustomResourceDefinition was never installed, so nothing defines it.
The message is the same for both. One command tells them apart.
Ask the cluster what it serves
kubectl api-resources | grep -i ingress
NAME SHORTNAMES APIVERSION NAMESPACED KIND
ingresses ing networking.k8s.io/v1 true Ingress
The kind exists at networking.k8s.io/v1, and your manifest says extensions/v1beta1. That is case 1: a removed API version.
If grep finds nothing, the type is not installed at all. That is case 2.
To see every version of a single kind, including ones being served alongside each other:
kubectl api-versions | sort
kubectl explain ingress --recursive | head -5
Case 1: A removed API version
Kubernetes promotes APIs from v1beta1 to v1 and then removes the beta after a deprecation window. Manifests written against the old version stop applying the moment you upgrade the cluster. These are the removals that catch the most people:
| Kind | Removed version | Use instead | Removed in |
|---|---|---|---|
Ingress | extensions/v1beta1, networking.k8s.io/v1beta1 | networking.k8s.io/v1 | 1.22 |
CustomResourceDefinition | apiextensions.k8s.io/v1beta1 | apiextensions.k8s.io/v1 | 1.22 |
ClusterRole, RoleBinding | rbac.authorization.k8s.io/v1beta1 | rbac.authorization.k8s.io/v1 | 1.22 |
CronJob | batch/v1beta1 | batch/v1 | 1.25 |
PodDisruptionBudget | policy/v1beta1 | policy/v1 | 1.25 |
HorizontalPodAutoscaler | autoscaling/v2beta2 | autoscaling/v2 | 1.26 |
PodSecurityPolicy | policy/v1beta1 | Pod Security Admission | 1.25 |
Changing apiVersion is often not enough, because the schema usually changed too. Ingress is the clearest example:
# extensions/v1beta1, no longer served
spec:
rules:
- http:
paths:
- path: /
backend:
serviceName: web
servicePort: 80
# networking.k8s.io/v1
apiVersion: networking.k8s.io/v1
kind: Ingress
spec:
ingressClassName: nginx
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80
pathType is required in v1, and the backend is now a nested object. Editing only the apiVersion line gets you a validation error instead, which at least tells you what is missing.
PodSecurityPolicy is the hard one: it was removed with no replacement resource. You migrate to Pod Security Admission, which is namespace labels rather than an object.
Finding these before an upgrade
Do not discover them by upgrading. Check the live cluster for objects on doomed versions:
kubectl get --raw /metrics 2>/dev/null \
| grep apiserver_requested_deprecated_apis
apiserver_requested_deprecated_apis{group="batch",removed_release="1.25",resource="cronjobs",version="v1beta1"} 1
The API server tracks every deprecated API anyone requests, including the removed_release. That metric is the most reliable pre-upgrade checklist you have, because it reports what is genuinely being used rather than what is in your repository.
For manifests and charts in git, kubectl-convert rewrites them:
kubectl convert -f old-ingress.yaml --output-version networking.k8s.io/v1
It ships separately from kubectl now, so you may have to install it.
Case 2: The CRD is not installed
kubectl get crd | grep cert-manager
Nothing returned means the type genuinely does not exist. Certificate, ServiceMonitor, VirtualService, IngressRoute and similar are all defined by controllers you install:
# cert-manager, which defines Certificate and Issuer
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.16.2/cert-manager.yaml
kubectl get crd | grep cert-manager.io
Install the controller first, then your resources.
The ordering problem in CI and Helm
A single kubectl apply -f ./manifests/ that contains both a CRD and a resource using it can fail even when everything is present, because kubectl builds its type mappings once at the start. The CRD is created during the apply, and the custom resource in the same batch is rejected against the mapping from before it existed.
Apply in two passes:
kubectl apply -f manifests/crds/
kubectl wait --for=condition=Established crd/certificates.cert-manager.io --timeout=60s
kubectl apply -f manifests/
The kubectl wait matters. A CRD exists before it is Established, and applying against it in that window fails intermittently, which is the sort of flake that only shows up in CI.
Helm has the same problem and solves it with a crds/ directory, which is installed before the templates are rendered. Note that Helm deliberately never upgrades or deletes anything in crds/, so CRD upgrades stay a manual step.
A checklist
kubectl api-resources | grep -i <kind>.- Listed at a different APIVERSION → deprecation. Update the manifest, and check the schema too.
- Not listed →
kubectl get crd | grep <name>. Missing → install the controller. kubectl apply --dry-run=serverto verify before applying for real.- Before a cluster upgrade, read
apiserver_requested_deprecated_apisfrom/metrics. - In CI, apply CRDs first and
kubectl wait --for=condition=Established. - In Helm, put CRDs in
crds/, and remember Helm will not upgrade them for you.
Frequently Asked Questions
What does "no matches for kind" mean in Kubernetes?
The API server does not serve the resource type at the apiVersion your manifest specifies. Either the kind exists at a different version, which means you are using an API that has been deprecated and removed, or the kind is not installed at all because the CustomResourceDefinition that defines it is missing. kubectl api-resources | grep -i <kind> distinguishes them: if the kind appears with a different APIVERSION it is the first case, and if it does not appear at all it is the second.
Why did my Ingress stop working after upgrading Kubernetes?
extensions/v1beta1 and networking.k8s.io/v1beta1 Ingress were removed in Kubernetes 1.22. The replacement is networking.k8s.io/v1, and the schema changed as well as the version: pathType is now required on every path, and the backend moved from flat serviceName and servicePort fields to a nested service.name and service.port.number. Changing only the apiVersion line produces a validation error rather than a working Ingress, which is at least a clearer message than the original one.
How do I find deprecated APIs before upgrading a cluster?
Read apiserver_requested_deprecated_apis from the API server's metrics endpoint with kubectl get --raw /metrics. Each series names the group, resource, version and the release in which it will be removed, and it reflects what is genuinely being requested against the live cluster rather than what happens to be in your repository. That makes it far more reliable than grepping manifests, because it also catches controllers, operators and Helm charts making calls you never wrote.
Why does kubectl apply fail on a custom resource right after I created its CRD?
kubectl builds its mapping of kinds to API endpoints once, at the start of the command. If the CRD is created during the same apply, the custom resource is validated against the mapping from before that type existed. There is also a window where the CRD object exists but is not yet Established. Apply CRDs as a separate step, then kubectl wait --for=condition=Established crd/<name> before applying the resources that use them. Intermittent failures of this kind are a classic CI flake.
Does Helm handle CRDs differently?
Yes. Files in a chart's crds/ directory are installed before any template is rendered, which solves the ordering problem for a fresh install. The important caveat is that Helm deliberately never upgrades or deletes those CRDs on helm upgrade or helm uninstall, because doing so would delete every custom resource in the cluster along with them. That means CRD upgrades are a manual step you have to plan for, and a chart upgrade that needs a newer CRD schema will fail until you apply it yourself.