KubernetesV · Kubernetes Objects and MetadataKubernetes objects and metadata
spec and status — the discipline of declared intent vs observed reality
What you'll learn
- Distinguish spec (intent) from status (observed) and identify who owns each
- Explain the spec-vs-status gap and what convergence means
- Use managed fields, server-side apply, and the OpenAPI schema to prevent lost writes
- Diagnose objects that are converged but unhealthy
Prerequisites
Verified against Kubernetes 1.34.x · kubeadm 1.34.x · kubectl 1.34.x · etcd 3.6.x · CoreDNS 1.11.x · containerd 1.7.x / 2.x · 2026-08-16
The spec-vs-status discipline is the contract that makes Kubernetes a declarative system: operators write spec, controllers write status, and the spec-vs-status gap is what reconciliation closes. This lesson covers the discipline in depth and the operational patterns that arise from it.
The contract
flowchart LR
O[Operator] -->|writes| S[spec<br/>intent]
C[Controller] -->|writes| T[status<br/>observed]
R[Reconciler] -->|closes gap between| S
R -->|closes gap between| T
Every Kubernetes object has a spec and status (with a few
exceptions where spec is empty). The contract is:
- Operators (humans, CI, GitOps) write
spec - Controllers write
status - The API server enforces schema and concurrency
- The reconciler closes the gap between them
When spec and status agree, the system is converged. When
they disagree, reconciliation is in progress.
spec is intent
The operator declares what should be true. The controller makes it true.
spec:
replicas: 5
selector:
matchLabels:
app: web
template:
spec:
containers:
- name: nginx
image: nginx:1.27.1
The controller reads this and acts. It does not consult other fields to decide; the spec is the source of truth.
Production principle: operators should only edit spec.
Direct edits to status are temporary and will be overwritten
by the controller on the next reconcile.
status is the system’s report
The controller writes what is currently true:
status:
replicas: 5
readyReplicas: 5
availableReplicas: 5
observedGeneration: 3
conditions:
- type: Progressing
status: "True"
reason: NewReplicaSetAvailable
lastUpdateTime: "2026-08-15T12:01:01Z"
- type: Available
status: "True"
lastTransitionTime: "2026-08-15T12:01:01Z"
The status carries:
- Counters — replicas, ready, available
- Conditions — the controller’s assessment
- Timestamps — when the state was last reported
The operator reads status to verify convergence, identify
problems, and decide on next steps.
The spec-vs-status gap
sequenceDiagram
autonumber
participant API as API server
participant Ctrl as Controller
participant W as World
Note over API: spec.replicas = 5
Note over API: status.replicas = 3 (gap=2)
Ctrl->>API: GET Deployment
Ctrl->>W: count owned Pods (3)
Ctrl->>API: create 2 more Pods
W-->>Ctrl: Pods created
Ctrl->>API: PATCH status.replicas: 5
Note over API: spec.replicas == status.replicas (converged)
The gap is what the controller closes. Different controllers have different gaps:
- Deployment: spec.replicas vs status.replicas
- Node: spec.unschedulable vs status.conditions[Ready]
- Pod: spec.containers vs status.containerStatuses
- PersistentVolume: spec.claimRef vs status.phase
Who owns which field
This is critical for server-side apply and managed fields:
- Operator owns
spec,metadata.labels,metadata.annotations - Controller owns
status,metadata.finalizers(typically) - API server owns
metadata.uid,metadata.resourceVersion, , metadata.managedFields,metadata.creationTimestamp`
A common operational mistake is editing status manually. The API server accepts the write (with the right RBAC), but the controller overwrites it on the next reconcile. The manual edit has no lasting effect.
managedFields and server-side apply
Server-side apply (SSA) is the canonical way to update objects in production. SSA tracks field ownership:
metadata:
managedFields:
- manager: kubectl
operation: Apply
apiVersion: apps/v1
fields:
f:spec:
f:replicas: 5
f:template: ...
- manager: deployment-controller
operation: Update
apiVersion: apps/v1
fields:
f:status:
f:replicas: 5
f:readyReplicas: 5
When two actors both use SSA, their fields are kept separate.
The Deployment controller can update status while the
operator’s spec fields are preserved.
kubectl apply --server-side -f manifest.yaml
Without SSA, the operator’s kubectl apply could overwrite
the controller’s status update (lost write). SSA preserves
field ownership.
The discipline:
- Operators use SSA for declarative management
- Controllers write fields they own (status, finalizers)
- The API server prevents lost writes by tracking field ownership
How to inspect managedFields
kubectl get pod web-abc -o json | jq '.metadata.managedFields'
[
{
"manager": "kubectl",
"operation": "Apply",
"apiVersion": "v1",
"fields": {
"f:metadata": {
"f:labels": {
"f:app": {}
}
},
"f:spec": {
"f:containers": {
"k:{\"name\":\"web\"}": {
".": {},
"f:image": {},
"f:name": {},
"f:resources": {}
}
}
}
}
},
{
"manager": "kubelet",
"operation": "Update",
"apiVersion": "v1",
"fields": {
"f:status": {
"f:conditions": {},
"f:containerStatuses": {},
"f:phase": {}
}
}
}
]
manager: kubectl indicates the operator’s manifest. manager: kubelet indicates the kubelet’s status updates. They own
different fields; SSA merges them safely.
Conflict resolution in SSA
When two managers apply the same field:
# kubectl's apply sets spec.replicas = 5
# HPA's apply sets spec.replicas = 8
# Conflict on spec.replicas
The API server rejects the second apply if both managers have the field. Resolution options:
--force-conflicts— take the field from the new apply--server-side --validate=false— skip validation- Configure one manager to skip the conflicting field
In production, the HPA is typically allowed to manage
spec.replicas. The operator’s manifest should set
spec.replicas to a “minimum” or a “max” that HPA respects,
or use a different field (e.g., spec.minReplicas via a
custom controller).
The apiVersion is also a contract
The API version implies a schema. When the schema changes, the contract changes:
# apps/v1beta1 (deprecated)
spec:
replicas: 5
template:
metadata:
annotations:
pod.beta.kubernetes.io/...
# apps/v1
spec:
replicas: 5
template:
metadata:
annotations:
...
Different versions may have different field names, different
required fields, and different defaults. Production manifests
should pin the apiVersion and update it deliberately, not
auto-upgrade.
Convergence is necessary but not sufficient
A system can be technically converged and unhealthy:
flowchart LR
S[spec: replicas=5] --> C{Converged?}
C -->|status.replicas == 5| OK[Technically converged]
OK --> H{Healthy?}
H -->|probes pass| HLTH[Healthy]
H -->|probes fail| UN[Converged but unhealthy]
A Deployment at 5/5 replicas is converged. If those Pods are crashlooping (liveness restarts), the Deployment is converged but unhealthy.
Production observability must cover both:
- Convergence:
spec.replicas == status.replicas - Health: probes pass; application KPIs are met
Common operational mistakes
Mistake 1: writing status manually
kubectl edit pod web-abc
# edit status.phase: Running
# (reverted by kubelet in seconds)
Fix: don’t edit status manually; fix the underlying cause.
Mistake 2: kubectl apply overwriting controller fields
kubectl apply -f deployment.yaml # without --server-side
# spec is updated, but status fields may be overwritten
Fix: use kubectl apply --server-side.
Mistake 3: trusting status as the source of truth
kubectl get deployment web -o jsonpath='{.status.replicas}'
# 5
# "Spec says 5; status says 5; converged" — but the Pods may
# be unhealthy
Fix: validate health with probes and external metrics, not just status.
Cross-course references
- The Linux course part
VI-Linux-Processescovers process state transitions; spec vs status is the cluster’s analogue. - The Observability course part
CIX-Observability-InvestigationWorkflowscovers investigation methodology for converged-but-unhealthy clusters. - The Docker course part
XXX-Docker-Lifecyclecovers the container lifecycle that drives much of the status reporting. - The Linux course part
XXIV-Linux-Timecovers chrony — timestamps in status depend on clock accuracy.
Quiz
Knowledge check · 4 questions
Q1. What does `metadata.managedFields` record?
Q2. When `spec.replicas` and `status.replicas` agree, the Deployment is converged and therefore healthy.
Q3. An operator runs `kubectl apply -f manifest.yaml` (without `--server-side`). The manifest sets `spec.replicas: 8`. The HPA has been managing `spec.replicas` (currently 5). The `kubectl apply` succeeds, overwriting the HPA's value. Diagnose and remediate.
Before apply: ``` $ kubectl get deployment web -o jsonpath='{.spec.replicas}' 5 # HPA-managed ``` Manifest: ```yaml spec: replicas: 8 # operator's intent ``` Command: ``` $ kubectl apply -f manifest.yaml deployment.apps/web configured ``` After apply: ``` $ kubectl get deployment web -o jsonpath='{.spec.replicas}' 8 # operator's value; HPA's value lost ``` The HPA is still running but cannot reconcile `spec.replicas` back to 5 because the operator's apply doesn't use server-side apply — the operator owns the field, not the HPA.
Q4. Explain the spec-vs-status contract. Who writes spec? Who writes status? What does convergence mean?
Passing score: 75%. Answers are checked in this browser.
Production discipline
- Operators write
spec; controllers writestatus. Never confuse the two. - Use
kubectl apply --server-sidefor declarative management; client-side apply can overwrite controller fields. - Inspect
managedFieldswhen investigating “who changed this?” questions. - Treat
status.conditionsas the controller’s assessment; conditions stuckFalseare the controller telling you it cannot converge. - Pair convergence (spec == status) with health (probes, KPIs) — convergence alone is not health.