Appendix C — YAML and API conventions¶
A practical reference for the two things every manifest in this guide depends on: YAML (and its footguns) and the Kubernetes API conventions (GVK/GVR,
spec/status, labels vs annotations, the immutable-selectorrule, optimistic concurrency, finalizers/ownerReferences, the alpha/beta/stable + deprecation policy, Server-Side Apply, and CRDs). Current for Kubernetes v1.30+. This is reference material — it does not follow the nine-section chapter anatomy; it cross-links the chapter that teaches each topic in depth.
Part 1 — YAML essentials and footguns¶
Kubernetes manifests are YAML. YAML is convenient but has sharp edges; almost every "the file looks right but the object is wrong" bug is one of these.
Indentation and structure¶
- Spaces only — never tabs. A literal tab is a YAML syntax error. Configure your editor to insert spaces; 2 spaces per level is the Kubernetes norm.
- Indentation is the structure. Nesting is by column, not braces. A field indented one space too few/many silently belongs to a different parent — the document still parses, the object is just wrong.
- List vs map. A
-prefix is a sequence item;key: valueis a mapping.containers:is a list (- name: app);resources:is a map (requests:/limits:). Mixing them ("containers:with a map underneath") is the most common structural mistake. - Trailing whitespace / invisible characters. Trailing spaces, a BOM, or
non-breaking spaces from a copy-paste can break parsing or string values in
ways that are invisible in an editor.
kubectl apply --dry-run=client -fis the fast catch.
The boolean / type traps¶
- The Norway problem. Unquoted
yes,no,on,off,true,false,y,nare parsed as booleans by YAML 1.1 (which most tooling follows). A country-code valueNO, a keyon:, orversion: yesbecomesfalse/true. Quote any string that could be read as a boolean ("NO","on"). - Unquoted version strings and numbers. Under YAML 1.1 (which
kubectland most Kubernetes tooling follow),version: 1.10parses as the number1.1(trailing zero lost);1.20→1.2. An image tag1.0may become1. A leading-zero value likeid: 0755may be read as octal. Quote versions and any number-like string that must stay a string:tag: "1.0",version: "1.20". KubernetesapiVersion/image tags/annotations are strings — quote them when ambiguous. nulland~.null,~, and an empty value all mean null.key:with nothing after it isnull, not""— which differs from an empty map{}or empty string"". For "present but empty" use{},[], or""explicitly.- The empty-map vs null distinction matters in Kubernetes.
securityContext:(null) is unset;securityContext: {}is present and empty — and for some fields the difference changes defaulting/admission behavior. Be explicit.
Multi-line strings: | vs > and chomping¶
literal: | # block scalar: newlines PRESERVED (use for scripts, configs)
line one
line two
folded: > # folded scalar: newlines become spaces (use for prose)
this is one
long logical line
keep: |+ # chomping: keep ALL trailing newlines
strip: |- # chomping: strip the final newline (common for one-line values)
clip: | # default: clip to a single trailing newline
For a single-line secret/token value, |- (strip) avoids a stray trailing
newline that breaks the consumer (a classic "the password has a \n" bug).
Anchors, aliases, and why Helm/Kustomize limit them¶
common: &common # & defines an anchor
app.kubernetes.io/part-of: bookstore
metadata:
labels:
<<: *common # * references it; << merges the map
Anchors/aliases are valid YAML and reduce repetition in hand-written files, but:
- Kustomize does not honor them as a templating mechanism — its merging is
done on parsed objects via transformers/patches, not by YAML aliasing across
documents. Use
labels:/patches/_helpers.tpl, not anchors, to share config across resources (07-delivery/02). - Helm renders Go templates before YAML parse, so anchors inside templates
interact confusingly with templating; the chart's convention is named
templates in
_helpers.tpl, not YAML anchors (07-delivery/01). - Anchors don't cross
---document boundaries, so they can't dedupe across a multi-doc manifest anyway.
Multi-document files¶
--- separates independent YAML documents in one file; ... optionally ends
one. kubectl apply -f file.yaml applies every document in order — the
guide's raw manifests rely on this. An empty document (just ---) or a
leading/trailing --- is harmless. Helm renders one multi-doc stream; Kustomize
emits one ordered multi-doc stream.
Quick YAML self-check¶
kubectl apply --dry-run=client -f manifest.yaml # parse + local schema validate (no cluster)
kubectl apply --dry-run=server -f manifest.yaml # + full apiserver admission (incl. PSA)
Convention used throughout the guide: quote ambiguous scalars (versions, tags, booleans-as-strings, all-digit strings), use
|-for single-line secret values, 2-space indentation, one concern per document, and validate every file with--dry-runbefore it enters Git.
Part 2 — Kubernetes API conventions¶
Every object follows one shape; these conventions are enforced by the API server, not stylistic. The conceptual home for all of this is 00-foundations/06 — The declarative API model.
apiVersion, group/version, GVK and GVR¶
apiVersion: apps/v1 # <GROUP>/<VERSION> (core group = "" → just "v1")
kind: Deployment # GVK = (apps, v1, Deployment)
- Group versions an area of the API:
apps/v1,networking.k8s.io/v1,batch/v1,rbac.authorization.k8s.io/v1,autoscaling/v2,policy/v1. The empty (core) group is written as just a version:apiVersion: v1→ Pod, Service, ConfigMap, Secret, Namespace, ServiceAccount. - GVK (Group/Version/Kind) =
apiVersion+kind; it identifies the type and routes the request to the controller and storage that own it. - GVR (Group/Version/Resource) is the lowercase-plural REST form
(
apps/v1→deployments) used in API paths (/apis/apps/v1/namespaces/<NS>/deployments) and in RBAC rules (resources: ["deployments"],apiGroups: ["apps"]). - Discover the correct, version-current values for your cluster — never guess:
kubectl api-resources # KIND, its GROUP, plural NAME, NAMESPACED?
kubectl api-versions # every served group/version
kubectl explain deployment.spec --recursive # the authoritative schema for the spec
Namespaced vs cluster-scoped¶
- Namespaced kinds live in a namespace (Pod, Deployment, Service, ConfigMap, Secret, Role, RoleBinding, PVC). They are isolated and quota-able per namespace.
- Cluster-scoped kinds have no namespace (Node, PersistentVolume, StorageClass, PriorityClass, ClusterRole, ClusterRoleBinding, CustomResourceDefinition, Namespace itself, IngressClass, GatewayClass).
- The Bookstore's three
PriorityClassobjects are cluster-scoped: the Kustomizenamespace:transformer correctly leaves them namespace-free, and Helm annotates themhelm.sh/resource-policy: keepso an uninstall doesn't break other workloads (07-delivery/01, 07-delivery/02). kubectl api-resources --namespaced=true|falsetells you which a kind is.
metadata: name, labels vs annotations, the recommended set¶
metadata:
name: catalog # unique within (namespace, kind)
namespace: bookstore # namespaced kinds only
labels: # identifying → SELECTED by other objects
app: catalog
app.kubernetes.io/name: catalog
annotations: # non-identifying → tools/humans, NOT selectable
kubernetes.io/change-cause: "bump catalog to 1.4"
uid: <SERVER-ASSIGNED> # survives name reuse
resourceVersion: "<etcd revision>" # optimistic concurrency (do not set by hand)
ownerReferences: [ ... ] # parent → cascading delete / GC
- Labels are identifying key/values meant for selection (Services, ReplicaSets, NetworkPolicies, HPAs select on them). Keep them stable and intentional. Rule of thumb: if something selects on it, it's a label; otherwise it's an annotation.
- Annotations are arbitrary non-identifying metadata (build SHA, change-cause, controller config, checksums). You cannot select on annotations.
- The recommended common labels — adopt this set for consistent selection,
dashboards, cost allocation, and policy:
app.kubernetes.io/name,/instance,/version,/component,/part-of,/managed-by. The guide standardizes on these (00-foundations/06).
spec vs status¶
- You (or a controller) write
spec— desired state. The owning controller/kubelet writesstatus— observed state. You almost never writestatus. - The divide is real at the API level: many kinds expose a separate
/statussubresource with its own RBAC, so a controller can update status without being able to mutate spec (and vice-versa). - This is why
kubectl get <OBJ> -o yamlshows both, and why pasting back a dumped object'sstatus:is meaningless — drop it.
The immutable-selector rule (a real outage)¶
Deployment/StatefulSet/ReplicaSet .spec.selector and a Service's
.spec.selector are immutable after creation — the API server rejects any
change:
Deployment.apps "catalog" is invalid: spec.selector:
Invalid value: ...: field is immutable
Once that happens the rollout is wedged until the workload is deleted and recreated (downtime). The two ways the guide teaches you to trip this — and avoid it:
- Kustomize
commonLabelsfootgun. The legacycommonLabels:transformer injects its labels intometadata.labelsand intospec.selectorand the pod template. Adding one later mutates the immutable selector. Fix: use the modernlabels:transformer withincludeSelectors: false(andincludeTemplates: false); keep selector labels stable and identical base↔overlays; put audit/owner metadata incommonAnnotations:(07-delivery/02). - Helm "template soup" mutating a selector. A value toggle that conditionally
alters selector/pod-template labels causes the same failure. Fix: render
selector labels from a fixed
_helpers.tplnamed template, never from a per-environment--set(07-delivery/01).
In production: make a CI check that
commonLabels:never appears and that.spec.selectoris byte-identical across base and every overlay/values file. This is the cheapest guard against a self-inflicted outage.
resourceVersion and optimistic concurrency¶
- Every object's
metadata.resourceVersionreflects etcd's revision at its last write. An update must carry theresourceVersionit read; the API server commits only if it is still current (compare-and-swap), else returnsConflict (409)— "the object has been modified; please apply your changes to the latest version". - This is working as intended, not a bug: the loser re-reads and
re-reconciles. Never hard-code or hand-edit
resourceVersion; let the client read-modify-write loop handle it (00-foundations/06).
finalizers and ownerReferences¶
metadata.finalizers— string keys that block deletion: the object entersTerminating(adeletionTimestampis set) but is not removed from etcd until the responsible controller does cleanup and removes its finalizer. A stuckTerminatingobject is almost always a finalizer whose controller is gone — investigate (and only force-remove a finalizer as a last resort, knowingly) (08-day-2-operations/05).metadata.ownerReferences— links a child to its parent; deleting the parent garbage-collects children (ReplicaSet → Pods, Deployment → ReplicaSets).--cascade=foreground|background|orphancontrols ordering. Controllers/operators set ownerReferences so cleanup is automatic (08-day-2-operations/05).
Server-Side Apply, managedFields, and conflicts¶
- Server-Side Apply (SSA) moves merge logic to the API server. Each field
records its manager in
metadata.managedFields. The server merges by field ownership and reports a conflict when two managers set the same field with different values. - This is what makes "Git owns most of a Deployment, an HPA owns
spec.replicas" correct — the conflict surfaces instead of one silently clobbering the other.
kubectl apply -f d.yaml --server-side --field-manager=ci # claim fields as "ci"
kubectl apply -f d.yaml --server-side --force-conflicts # take ownership (use deliberately)
kubectl get deploy catalog -o yaml --show-managed-fields # inspect ownership
- Classic client-side
applyinstead does a 3-way merge of (1) your manifest, (2) the live object, and (3) the stored last-applied annotation — enough to tell "user removed a field" from "a controller added one". SSA is the modern default for GitOps/CI; resolve conflicts intentionally rather than reflexively forcing (00-foundations/06).
alpha / beta / stable and the deprecation/removal policy¶
- API maturity:
v1alpha1(off by default, may change/disappear, no guarantees),v1beta1(on by default, may still change, deprecation notice before removal),v1/ stable / GA (long-term support). - The deprecation policy (the part that bites in upgrades): a GA API
version is supported for a defined window and removed only after a
deprecation period; beta APIs likewise get notice. Removed examples you must
not copy from old tutorials:
extensions/v1beta1Deployment/Ingress,networking.k8s.io/v1beta1Ingress,policy/v1beta1PodSecurityPolicy (the whole PSP API removed in v1.25 — use PSA, 05-security/02),autoscaling/v2beta2HPA (useautoscaling/v2),batch/v1beta1CronJob (usebatch/v1),policy/v1beta1PodDisruptionBudget (usepolicy/v1). - Find and fix removed/changing APIs:
kubectl api-resources # what THIS cluster serves now
kubectl api-versions | sort # served group/versions
kubectl explain ingress --api-version=networking.k8s.io/v1 # confirm the current GVK
kubectl get --raw '/metrics' | grep apiserver_requested_deprecated_apis # who still calls deprecated APIs
kubectl convert -f old.yaml --output-version apps/v1 # `kubectl convert` plugin: migrate a manifest
In production: pin and migrate deliberately. On regulated clusters pin the PSA
-versionlabel (e.g.v1.30) so a Kubernetes upgrade can't silently change whatrestrictedmeans. Before any control-plane upgrade, scan for deprecated API usage (the apiserver metric above, or tools likepluto/kubent) and bump manifests/charts to the current GVK first (08-day-2-operations/01).
CustomResourceDefinitions: structural schema & validation¶
- A CRD registers a new kind with the API server so custom objects are
stored/served/RBAC'd like built-ins. A CRD must carry a structural
OpenAPI v3 schema (
spec.versions[].schema.openAPIV3Schema): the apiserver uses it to validate and to prune unknown fields, so a malformed CR is rejected at admission just like a built-in. - CRDs can serve multiple versions with a storage version and an optional conversion webhook between them; the same alpha/beta/stable + deprecation discipline applies to your CRDs too.
kubectl explain <YOURCRD>.spec --recursiveworks for CRDs because the schema is published to the API server — the same authoritative-reference habit as for built-ins (08-day-2-operations/05).
The universal object skeleton¶
Every kind you will ever write fits this; everything above is a refinement of it:
apiVersion: <GROUP>/<VERSION> # GVK — core group is just a version, e.g. v1
kind: <Kind>
metadata:
name: <NAME> # identity within (namespace, kind)
namespace: <NS> # namespaced kinds only
labels: { app: <NAME> } # identifying → what other objects select on
annotations: { } # non-identifying metadata (NOT selectable)
spec: { } # DESIRED state — you author this
# status: OBSERVED — written by the owning controller/kubelet, NEVER by you
Which chapter teaches this¶
| Topic | Chapter |
|---|---|
spec/status, GVK, labels/selectors, apply 3-way merge, SSA, explain |
00-foundations/06 — The declarative API model |
| The admission pipeline that validates these objects | 00-foundations/04 — Control plane deep dive |
The recommended app.kubernetes.io/* label set in practice |
00-foundations/06 — The declarative API model |
| RBAC rules expressed in GVR (apiGroups/resources) | 05-security/01 — Authn, authz, RBAC |
| Deprecation/removal in practice (PSP→PSA, version pinning) | 05-security/02 — Pod security |
| YAML + the immutable-selector footgun in Helm | 07-delivery/01 — Packaging with Helm |
YAML + the commonLabels immutable-selector footgun in Kustomize |
07-delivery/02 — Packaging with Kustomize |
| Finding/replacing removed APIs across a cluster upgrade | 08-day-2-operations/01 — Cluster lifecycle |
| Finalizers, ownerReferences, CRDs, structural schema, conversion | 08-day-2-operations/05 — Operators and CRDs |
See also: Appendix A — kubectl cheatsheet (the commands), Appendix B — Glossary (every term above defined), and the official references: API concepts https://kubernetes.io/docs/reference/using-api/api-concepts/, Server-Side Apply https://kubernetes.io/docs/reference/using-api/server-side-apply/, the deprecation policy https://kubernetes.io/docs/reference/using-api/deprecation-policy/, and CRDs https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/.