kubeconform: Validate Manifests in CI
kubeconform <files> validates each manifest against the matching Kubernetes JSON schema and exits non-zero if any resource is invalid.
kubeconform is the maintained, faster successor to kubeval. It validates manifests against versioned schemas, parallelizes well, and works against a self-hosted schema mirror for air-gapped CI.
What it does
kubeconform parses each YAML/JSON manifest, identifies the apiVersion/kind, fetches the matching OpenAPI-derived JSON schema, and checks the resource against it. It validates structure and types, not cluster admission, so it needs no API server.
Common usage
kubeconform deployment.yaml
kubeconform -summary -output json manifests/
cat manifest.yaml | kubeconform - # read from stdin
kubeconform -kubernetes-version 1.29.0 deploy.yamlOptions
| Flag | What it does |
|---|---|
| -summary | Print a counts summary (valid/invalid/skipped) |
| -output | Output format: text, json, tap, junit |
| -kubernetes-version | Target a specific Kubernetes version of schemas |
| -schema-location | Where to fetch schemas (URL or local path) |
| -skip | Comma-separated kinds to skip (e.g. CustomResourceDefinition) |
| -ignore-missing-schemas | Do not fail when a schema is unavailable |
In CI
kubeconform exits 0 only when every resource validates, so it gates a pipeline cleanly. Use -output junit to publish results as test reports. For air-gapped runners, point -schema-location at a mirror so it never reaches out to GitHub.
Common errors in CI
could not find schema for <Kind> means a CRD or an unknown kind; add it via -schema-location or pass -ignore-missing-schemas to skip it. failed parsing ... yaml: line N: ... is a YAML syntax error, not a schema problem. error validating ... additionalProperties ... not allowed with -strict means an unrecognized field.
Using this in CI
A runner has no kubeconfig, no cached context, and no interactive auth. Every kubectl invocation in CI needs the context supplied explicitly, and most confusing CI failures here are the command running against the wrong cluster or no cluster at all.
# never rely on the ambient context on a runner
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get pods
# confirm what you are actually connected to before mutating anything
kubectl config current-context
kubectl cluster-info
# fail fast instead of hanging on an unreachable API server
kubectl --request-timeout=30s get nodes