Problem – Validation Errors After Updating a CRD Schema
After a recent schema change to a shared Custom Resource Definition (CRD) used by multiple AI/ML services, kubectl apply and CI/CD pipelines began failing with errors such as:
error: admission webhook "v1.crdvalidation.k8s.io" denied the request: spec: Required value
Other observed messages include:
validation failed: spec.replicas: Invalid value: "two": must be an integererror converting YAML to JSON: yaml: line 12: did not find expected ':'no matches for kind "MLJob" in version "ml.example.com/v2beta1"
These errors manifest during creation or update of custom resources across several namespaces, causing model deployment pipelines to stall and production inference services to become unavailable.
Root Cause – How Schema Changes Break Existing Resources
The CRD schema is defined using an OpenAPI v3 validation block. When a new field is marked required: true or an existing field’s type is altered, the built‑in CRD validation webhook (v1.crdvalidation.k8s.io) re‑evaluates every incoming object against the updated schema.
Key mechanisms involved (see Kubernetes API conventions and the CRD versioning guide):
- Structural schema enforcement – Starting with Kubernetes 1.22,
preserveUnknownFields: falseis the default, causing any field not explicitly described in the schema to be rejected. - Version bump side effects – Adding a new
apiVersion(e.g.,v1beta2) without removing the old version can lead to “no matches for kind” errors if clients still reference the removed version. - Conversion webhook expectations – When
spec.versions[].servedis true for multiple versions, a conversion webhook must translate objects between versions. Missing conversion logic results in schema mismatches for existing resources.
In the real incident on GKE (see evidence package), the team added a required field metadata.annotations.aiPlatform to the MLJob CRD without providing a migration path. Existing MLJob objects lacked this field, so the validation webhook rejected them with “spec: Required value”.
Debug – Investigation Steps
1. Inspect the CRD definition
kubectl get crd mljobs.ml.example.com -o yaml > crd.yaml
Key sections to review:
| Section | What to Look For |
|---|---|
spec.validation.openAPIV3Schema |
Required fields, type definitions, preserveUnknownFields |
spec.versions |
Served vs storage versions, conversion webhook config |
metadata.annotations |
Any version‑specific notes about migration |
2. Reproduce the error with a minimal manifest
cat > broken.yaml <<EOF
apiVersion: ml.example.com/v1beta1
kind: MLJob
metadata:
name: sample-job
spec:
model: my-model
# missing the newly required field `metadata.annotations.aiPlatform`
EOF
kubectl apply -f broken.yaml
Expected output (matches the logs observed in the field):
error: admission webhook "v1.crdvalidation.k8s.io" denied the request: spec: Required value
3. Verify the cluster version and CRD compatibility
kubectl version --short
Managed clusters (EKS, GKE, AKS) enforce stricter validation starting with Kubernetes 1.22 (see the EKS version matrix). If the cluster was upgraded recently (e.g., from 1.22 to 1.24), the new validation rules may be the trigger.
4. Check conversion webhook logs (if applicable)
kubectl logs -n ml-platform deployment/mljob-conversion-webhook -c webhook
Look for messages like “conversion failed: missing required field”. Missing conversion logic is a common cause noted in GitHub issue #102317.
Solution – Fixing the Validation Errors
Option A: Add a Migration Job to Populate Required Fields
When a field becomes required, back‑fill existing resources before the schema is enforced.
# migration-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: mljob-migration
spec:
template:
spec:
serviceAccountName: ml-platform-admin
restartPolicy: OnFailure
containers:
- name: migrate
image: bitnami/kubectl:latest
command:
- /bin/sh
- -c
- |
kubectl get mljobs.ml.example.com -A -o json | \
jq -c '.items[] | select(.metadata.annotations.aiPlatform == null)' | \
while read -r obj; do
name=$(echo "$obj" | jq -r .metadata.name)
ns=$(echo "$obj" | jq -r .metadata.namespace)
kubectl patch mljob $name -n $ns \
--type merge -p '{"metadata":{"annotations":{"aiPlatform":"default"}}}'
done
Run the job, verify that all resources now contain the annotation, then re‑apply the updated CRD.
Option B: Relax the Schema Temporarily
If an immediate migration is not feasible, modify the CRD to make the new field optional and set preserveUnknownFields: true for backward compatibility.
# before (failing schema)
spec:
validation:
openAPIV3Schema:
type: object
required:
- spec
properties:
spec:
type: object
required:
- model
- replicas
properties:
model:
type: string
replicas:
type: integer
# New required field added
metadata:
type: object
required:
- annotations
properties:
annotations:
type: object
required:
- aiPlatform
properties:
aiPlatform:
type: string
# after (temporary relaxation)
spec:
preserveUnknownFields: true # <--- added
validation:
openAPIV3Schema:
type: object
required:
- spec
properties:
spec:
type: object
required:
- model
- replicas
properties:
model:
type: string
replicas:
type: integer
metadata:
type: object
properties: # removed required list
annotations:
type: object
properties:
aiPlatform:
type: string
Deploy the relaxed CRD, run the migration job, then revert to the strict schema once all resources are updated.
Option C: Use Versioned Conversion
Introduce a new storage version (e.g., v1beta2) that includes the required field, and implement a conversion webhook that injects a default value for existing objects.
# crd.yaml snippet with conversion
spec:
versions:
- name: v1beta1
served: true
storage: false
- name: v1beta2
served: true
storage: true
schema:
openAPIV3Schema: ... # includes required field
conversion:
strategy: Webhook
webhook:
clientConfig:
service:
name: mljob-converter
namespace: ml-platform
path: /convert
conversionReviewVersions: ["v1"]
The webhook adds metadata.annotations.aiPlatform: "default" when converting from v1beta1 to v1beta2, eliminating the validation failure without a separate migration job.
Verify – Confirming the Fix
- Re‑apply the updated CRD (or the relaxed version) and ensure
kubectl get crdshows the new schema. - Re‑run the minimal manifest used in the debug step; it should now succeed:
kubectl apply -f broken.yaml
customresource.ml.example.com/sample-job created
- Check that all existing resources contain the required field:
kubectl get mljobs.ml.example.com -A -o jsonpath='{range .items[*]}{.metadata.name}{" -> "}{.metadata.annotations.aiPlatform}{"\n"}{end}'
All entries should output a non‑empty value (e.g., default).
- Monitor the admission webhook logs for a period of 15‑30 minutes to ensure no further validation rejections appear.
Prevent – Best Practices for Evolving CRDs
- Never add a required field without a migration path. Prefer making fields optional first, then back‑fill, then mark required in a subsequent version.
- Use versioned CRDs with storage version separation. Keep the old version served for a deprecation window while a conversion webhook handles defaults.
- Pin
preserveUnknownFields: falseexplicitly. Document the impact in the CRD changelog to avoid surprises when clusters upgrade. - Automate schema compatibility checks. Run
kubectl diff -fin CI pipelines and validate that existing resources pass against the new schema usingkubectl apply --dry-run=client. - Leverage the Kubernetes API conventions. Follow the guidelines in the API conventions for field naming, requiredness, and versioning.
FAQ – Common Follow‑Up Questions
- Why does the error appear only after a cluster upgrade?
Managed clusters (EKS/GKE/AKS) enable stricter OpenAPI validation starting with Kubernetes 1.22. Fields that were previously ignored become enforced, causing “spec: Required value” failures. - Can I disable the built‑in CRD validation webhook?
No. The validation webhook is integral to the API server. The correct approach is to adjust the CRD schema or provide a conversion webhook. - How do I test that a new required field won’t break existing resources?
Export all existing resources, remove the new field, and runkubectl apply --dry-run=client -f <exported.yaml>. If the dry run fails, you need a migration strategy. - What is the difference between
preserveUnknownFields: trueand structural schemas?
Whentrue, the API server accepts fields not described in the schema, effectively bypassing validation for those fields. Setting it tofalse(the default in recent versions) forces strict structural validation. - Do I need to bump the
apiVersionwhen adding required fields?
Best practice is to introduce a new version (e.g.,v1beta2) and mark the previous version as deprecated. This allows a conversion webhook to handle defaulting without breaking existing manifests.
Related Topic Hub: Distributed Systems Troubleshooting Hub