Skip to content

Fix: kubectl apply error validating / is invalid

FixDevs · (Updated: )

Part of:  Docker, DevOps & Infrastructure

Quick Answer

How to fix kubectl apply errors like 'error validating', 'is invalid', and 'error when creating' caused by YAML syntax issues, deprecated APIs, missing fields, and more.

The Error

You run kubectl apply -f on a manifest and get one of these errors:

error: error validating "deployment.yaml": error validating data: ValidationError(Deployment.spec):
missing required field "selector" in io.k8s.api.apps.v1.DeploymentSpec
error: error validating "service.yaml": error validating data: [ValidationError(Service.spec.ports[0]):
unknown field "port" in io.k8s.api.core.v1.ServicePort]
The Deployment "my-app" is invalid: spec.template.metadata.labels: Invalid value: map[string]string{"app":"my-app"}:
`selector` does not match template `labels`
error when creating "manifest.yaml": the server could not find the requested resource

These all mean Kubernetes rejected your manifest because something in the YAML is structurally wrong, references a deprecated or unknown API, or violates a cluster policy. The good news: every one of these has a straightforward fix once you know where to look.

Why This Happens

Kubernetes validates every manifest against its API schema before applying it. This validation happens in multiple stages:

  1. Client-side validationkubectl checks the YAML structure against the API schema it knows about.
  2. Server-side validation — The API server checks the object against the current cluster’s schema, including CRDs and admission webhooks.
  3. Admission controllers — Mutating and validating webhooks can reject objects based on custom policies.

When any stage fails, you get a validation error. The most common root causes are:

  • Bad YAML syntax — Indentation errors, tabs instead of spaces, or missing colons break parsing before validation even starts.
  • Wrong apiVersion — APIs get deprecated and removed across Kubernetes versions. A manifest that worked on 1.24 may fail on 1.26.
  • Missing required fields — Every resource type has mandatory fields. Omit one and the API server rejects the entire object.
  • Immutable fields — Some fields cannot be changed after initial creation. Trying to update them triggers an “is invalid” error.
  • Namespace mismatches — Applying a namespaced resource to a non-existent namespace, or applying a cluster-scoped resource with a namespace set.
  • Quota violations — Resource requests exceeding namespace quotas or limit ranges.
  • Unknown resource types — Applying a CR before the CRD is installed.

Let’s fix each one.

The exact error text and the validation pipeline have changed substantially across recent Kubernetes releases. A failure mode that wrote a vague client-side message in 1.20 now writes a structured server-side error in 1.31+. Lining up your kubectl, your cluster, and the docs you are reading saves a lot of guesswork.

Version History That Changes the Failure Mode

kubectl apply has been part of Kubernetes since v1.0, but several releases shifted how validation works, where errors come from, and which defaults you can rely on.

  • kubectl 1.13 (Dec 2018) introduced kubectl diff, which makes immutable-field errors much easier to predict before they happen. If you are on an older client, you have no diff and must guess from the validation message alone.
  • kubectl 1.14 (Mar 2019) bundled Kustomize directly into kubectl (kubectl apply -k). Before 1.14, you needed the standalone kustomize binary, and many CI scripts that still call it externally will simply fall back to a no-op if it is missing — producing a confusing manifest that fails validation downstream.
  • Server-Side Apply (SSA) reached GA in Kubernetes 1.22 (Aug 2021). SSA introduces field managers and conflict detection. After 1.22, an apply that “succeeded” on an old client may now fail with Apply failed with N conflicts because another controller (often a HelmRelease or operator) already owns the same field. Pass --force-conflicts or use a different field manager.
  • Kubernetes 1.25 (Aug 2022) removed PodSecurityPolicy entirely (policy/v1beta1). If your manifests still reference PSP, you get the “the server could not find the requested resource” message even though the YAML is perfectly formed.
  • kubectl 1.27 (Apr 2023) changed SSA conflict-handling defaults and improved client-side validation, surfacing many issues earlier with better messages. The --validate=warn flag stabilized here, letting CI gate on validation without breaking the apply.
  • kubectl 1.29 (Dec 2023) improved default error messaging with friendlier hints — for example, “did you mean…” suggestions when an apiVersion is close to a valid one. If you are on 1.28 or older you simply will not see those hints.
  • kubectl 1.31 (Aug 2024) added richer admission-webhook error reporting, including warnings surfaced through the standard warning header. A failure that used to be a single line now includes the rejecting webhook’s name, the rule that fired, and a remediation suggestion when the webhook publishes one.
  • Kubernetes 1.32 and 1.33 (2024–2025) continued to refine validation messages and CEL-based admission policies. CEL admission policies (ValidatingAdmissionPolicy) became stable in 1.30 and can produce errors that look like webhook denials but are evaluated entirely inside the API server.

kubectl version --output=yaml shows you both the client and server version. If they are more than two minor versions apart, expect message wording differences that older articles will not predict.

Fix 1: Validate YAML Syntax

YAML syntax errors are the most common cause of kubectl apply failures. The error messages can be cryptic because the YAML parser fails before Kubernetes even sees the object.

Check for tabs. YAML does not allow tabs for indentation. Only spaces are valid. If your editor inserted tabs, you will get errors like:

error converting YAML to JSON: yaml: line 8: found character that cannot start any token

Find and replace all tabs with spaces. In most editors, you can configure this globally. From the command line:

cat -A deployment.yaml | grep '\^I'

If you see ^I characters, those are tabs. Replace them:

sed -i 's/\t/  /g' deployment.yaml

Check indentation levels. Every nested field must be indented consistently. A common mistake is misaligning containers under spec:

# Wrong -- containers is at the wrong indentation level
spec:
  template:
    spec:
    containers:
      - name: my-app
        image: nginx
# Correct
spec:
  template:
    spec:
      containers:
        - name: my-app
          image: nginx

Check for missing colons. Every key-value pair needs a colon followed by a space:

# Wrong -- missing colon after "name"
metadata
  name: my-app

# Correct
metadata:
  name: my-app

Pro Tip: Use yamllint to catch syntax errors before they hit kubectl. Install it with pip install yamllint and run yamllint deployment.yaml. It catches tabs, indentation inconsistencies, trailing spaces, and other subtle issues that kubectl error messages make hard to diagnose.

For a deeper dive into YAML parsing errors, see YAML mapping values not allowed here.

Fix 2: Fix apiVersion Mismatch

Kubernetes deprecates and removes APIs on a regular schedule. If you copied a manifest from a blog post written in 2020, chances are the apiVersion is wrong for your cluster.

Common examples of removed APIs:

Old apiVersionRemoved inReplacement
extensions/v1beta1 (Deployment)1.16apps/v1
extensions/v1beta1 (Ingress)1.22networking.k8s.io/v1
rbac.authorization.k8s.io/v1beta11.22rbac.authorization.k8s.io/v1
batch/v1beta1 (CronJob)1.25batch/v1
policy/v1beta1 (PodSecurityPolicy)1.25Removed entirely
autoscaling/v2beta21.26autoscaling/v2
flowcontrol.apiserver.k8s.io/v1beta21.29flowcontrol.apiserver.k8s.io/v1

Check which API versions your cluster supports:

kubectl api-resources | grep deployments

This shows the current valid group and version. You can also check for a specific group:

kubectl api-versions | grep apps

If you have many manifests to update, use kubectl-convert to migrate them automatically:

kubectl convert -f old-deployment.yaml --output-version apps/v1

Note: The kubectl convert plugin is not included by default. Install it via kubectl krew install convert or download it from the Kubernetes GitHub releases.

When migrating from beta APIs to stable ones, be aware that the stable version often requires fields that the beta version made optional. For example, apps/v1 Deployments require spec.selector, while extensions/v1beta1 auto-generated it.

Fix 3: Fix Missing Required Fields

The error message usually tells you exactly which field is missing:

error validating data: ValidationError(Deployment.spec): missing required field "selector"

Here are the fields that developers forget most often:

Deployment missing spec.selector:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  selector:
    matchLabels:
      app: my-app   # Must match template labels
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        - name: my-app
          image: nginx:1.25

The selector.matchLabels must exactly match template.metadata.labels. If they don’t, you get:

Invalid value: ... `selector` does not match template `labels`

Pod missing containers:

missing required field "containers" in io.k8s.api.core.v1.PodSpec

Every Pod spec needs at least one container with a name and image:

spec:
  containers:
    - name: app
      image: nginx:1.25

Service missing ports:

A Service of type ClusterIP, NodePort, or LoadBalancer requires at least one port definition. The targetPort field is also commonly confused with port:

spec:
  selector:
    app: my-app
  ports:
    - port: 80          # Port the Service listens on
      targetPort: 8080   # Port on the container
      protocol: TCP

If your Pod keeps crashing after fixing the manifest, check out CrashLoopBackOff troubleshooting for container-level debugging.

Fix 4: Fix Immutable Field Errors on Update

Some fields cannot be changed once a resource is created. Trying to update them with kubectl apply gives:

The Deployment "my-app" is invalid: spec.selector: Invalid value: ...: field is immutable

Common immutable fields:

  • spec.selector on Deployments, StatefulSets, and DaemonSets
  • spec.clusterIP on Services
  • spec.volumeName on PersistentVolumeClaims
  • metadata.name and metadata.namespace (obviously)
  • spec.nodeName on Pods

The fix: Delete and recreate the resource:

kubectl delete deployment my-app
kubectl apply -f deployment.yaml

Or use kubectl replace --force, which does the delete-and-create in one step:

kubectl replace --force -f deployment.yaml

Warning: Both approaches cause downtime. The old Pods are terminated before new ones are created. For Deployments, this is usually acceptable because the new ReplicaSet spins up quickly. For StatefulSets with persistent storage, be extra careful — verify your PersistentVolumeClaims have the correct reclaimPolicy so data is not deleted.

If you need to change the selector without downtime, the standard approach is to create a new Deployment with the updated selector, shift traffic to it, and then delete the old one.

Fix 5: Fix Namespace Issues

Namespace-related errors come in several forms:

Error from server (NotFound): namespaces "staging" not found
Error from server (Forbidden): namespaces "kube-system" is forbidden

The namespace doesn’t exist. Create it first:

kubectl create namespace staging
kubectl apply -f deployment.yaml -n staging

Or include a Namespace manifest in your YAML and apply it before other resources:

apiVersion: v1
kind: Namespace
metadata:
  name: staging
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  namespace: staging
# ...

Namespace in the manifest vs. the command line. If your manifest has metadata.namespace: production but you run kubectl apply -f deployment.yaml -n staging, the manifest wins. The object is created in production. This catches people off guard. Either set the namespace in the manifest or on the command line, not both, to avoid confusion.

Cluster-scoped resources with a namespace set. Resources like ClusterRole, ClusterRoleBinding, PersistentVolume, and Namespace itself are cluster-scoped. Setting metadata.namespace on them causes an error:

# Wrong -- ClusterRole is cluster-scoped
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: my-role
  namespace: default   # Remove this line

Remove the namespace field from cluster-scoped resources.

Fix 6: Fix Resource Quota and Limit Range Violations

If your namespace has ResourceQuota or LimitRange objects, your Pod spec must comply with them or the API server rejects the manifest:

Error from server (Forbidden): error when creating "deployment.yaml": pods "my-app-xyz" is forbidden:
exceeded quota: compute-resources, requested: requests.cpu=500m, used: requests.cpu=1800m, limited: requests.cpu=2

Check the quota:

kubectl describe resourcequota -n my-namespace

This shows the current usage and limits. Either reduce your resource requests or ask your cluster admin to increase the quota.

LimitRange violations look like this:

Error from server (Forbidden): pods "my-app" is forbidden: minimum cpu usage per Container is 100m, but request is 50m

Check the LimitRange:

kubectl describe limitrange -n my-namespace

Then adjust your container resource requests and limits to fall within the allowed range:

resources:
  requests:
    cpu: "100m"
    memory: "128Mi"
  limits:
    cpu: "500m"
    memory: "512Mi"

Common Mistake: When a LimitRange specifies default limits but you don’t set any resources on your container, the defaults are applied automatically. This can lead to unexpected OOM kills when the default memory limit is too low. Always set explicit resource requests and limits.

Fix 7: Fix CRD Not Installed

When you apply a custom resource and its CRD is missing, you get:

error: unable to recognize "my-resource.yaml": no matches for kind "MyCustomResource" in version "example.com/v1"

Or:

error when creating "my-resource.yaml": the server could not find the requested resource

Install the CRD first. CRDs must exist before you create instances of them. If you are installing an operator or controller, apply its CRDs first:

kubectl apply -f crds/
kubectl apply -f operator/
kubectl apply -f custom-resources/

Check if the CRD exists:

kubectl get crd | grep mycustomresource

If it is there but you are still getting errors, check the API group and version match between the CRD and your custom resource manifest. The spec.group and spec.versions[].name in the CRD must match the apiVersion in your CR.

After installing a CRD, it may take a few seconds for the API server to recognize the new type. If kubectl apply fails immediately after CRD creation, wait a moment and retry:

kubectl apply -f crds/ && sleep 5 && kubectl apply -f custom-resources/

If you are using Helm, ensure the CRDs are in the crds/ directory of the chart so they are installed before templates are rendered.

Fix 8: Use —dry-run and kubectl diff for Debugging

Before applying a manifest to a live cluster, validate it locally:

Client-side dry run:

kubectl apply -f deployment.yaml --dry-run=client

This checks YAML syntax and basic schema validation against kubectl’s built-in schema. It does not contact the cluster, so it won’t catch server-side issues like quota violations or webhook rejections.

Server-side dry run:

kubectl apply -f deployment.yaml --dry-run=server

This sends the manifest to the API server for full validation, including admission webhooks and quota checks, but does not actually create or modify the resource. This is the most thorough pre-flight check available.

kubectl diff:

kubectl diff -f deployment.yaml

This shows exactly what would change if you applied the manifest. It is invaluable when updating existing resources because it highlights field-level differences. If a field shows up in the diff that you did not intend to change, you may be about to trigger an immutable field error or an unintended rollout.

Validate against a specific schema version:

kubectl apply -f deployment.yaml --validate=true

The --validate flag is enabled by default, but explicitly setting it ensures you see all validation errors. You can also use --validate=warn (Kubernetes 1.27+) to see validation warnings without rejecting the apply.

Pro Tip: Chain these together in a CI pipeline. Run kubectl apply --dry-run=server as a gate before deploying. This catches 90% of manifest issues before they hit production. Pair it with kubectl diff in a PR comment so reviewers can see exactly what changes are going to the cluster.

If your connection to the cluster is the issue rather than the manifest itself, see kubectl connection refused for troubleshooting API server connectivity.

Still Not Working?

If you have validated your YAML, confirmed the apiVersion, and checked all required fields but are still getting errors, look into these less common causes:

Admission Webhook Failures

Mutating or validating admission webhooks can reject manifests with custom error messages:

Error from server: error when creating "deployment.yaml": admission webhook "validate.example.com" denied the request: image must be from approved registry

Check which webhooks are configured:

kubectl get validatingwebhookconfigurations
kubectl get mutatingwebhookconfigurations

The webhook’s error message usually tells you what policy you are violating. Common webhook policies include requiring specific image registries, enforcing label conventions, or blocking privileged containers. Work with your cluster admin to understand and comply with the policies, or get an exemption if needed.

RBAC Permission Errors

If your user or service account lacks permission to create the resource, you get:

Error from server (Forbidden): deployments.apps "my-app" is forbidden: User "dev-user" cannot create resource "deployments" in API group "apps" in the namespace "production"

Check your permissions:

kubectl auth can-i create deployments -n production
kubectl auth can-i '*' '*' -n production

If the answer is “no,” you need a RoleBinding or ClusterRoleBinding granting the appropriate permissions to your user or service account.

Cluster Version Compatibility

If your manifests target a newer Kubernetes version than your cluster is running, features and fields may not be available:

kubectl version

Compare the server version against the API features you are using. Check the Kubernetes changelog for when specific features were introduced.

If your images are not pulling correctly after fixing the manifest, check ImagePullBackOff troubleshooting for the relevant separate failure mode.

Server-Side Apply Conflicts

On clusters 1.22+, an apply can be syntactically valid yet rejected because another field manager (a controller, an operator, or Helm) already owns one of the fields you are setting. The error mentions “Apply failed with N conflicts” and lists the conflicting manager:

Apply failed with 1 conflict: conflict with "helm" using apps/v1: .spec.replicas

The safe fix is to align: stop competing for that field by removing it from your manifest. The fast fix is --force-conflicts, which takes ownership but can clash again the next time the original manager reconciles. Pick deliberately.

CEL Validating Admission Policy Rejections

If your cluster runs ValidatingAdmissionPolicy (CEL-based, stable in 1.30), errors look like admission webhook failures but reference the policy name instead of a webhook. List active policies:

kubectl get validatingadmissionpolicy
kubectl get validatingadmissionpolicybinding

Read the policy’s CEL expression to understand which field of your manifest tripped the rule.

kubectl Plugin Version Drift

kubectl-convert, kubectl-neat, and other krew plugins ship independently. A plugin pinned to an old kubectl can rewrite manifests using deprecated apiVersions, then the same kubectl rejects them server-side. Run kubectl krew upgrade or pin every plugin in your CI image to a known-good kubectl release.

Field Name Case Mismatches in JSON-Bridged Tools

Tools that convert JSON to YAML on the fly (Terraform’s kubernetes_manifest, some CDK8s outputs) sometimes serialize field names in camelCase that Kubernetes expects in a different case, or vice versa. The validator reports “unknown field” for what looks like a perfectly normal key. Round-trip the manifest through kubectl get -o yaml on a working resource to see the canonical casing.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles