Lab CKAD-07 — Kustomize, Drift & API Deprecations
In this lab you package podinfo with Kustomize (a base and two overlays), deploy two environments from it, change the live objects by hand and work through the ownership conflict that server-side apply reports, then migrate a manifest written for an API version the cluster no longer serves.
This lab uses its own namespaces and does not touch default/podinfo.
Concepts: Kustomize, Deprecations and Backup and Restore.
Tested 2026-10-13 on kind v0.33.0 with Kubernetes v1.37.0.
flowchart TD subgraph Files["Manifests on disk"] Base["base/<br/>• deployment.yaml<br/>• service.yaml<br/>• kustomization.yaml"] DevOverlay["overlays/dev/<br/>(1 replica, blue UI)"] ProdOverlay["overlays/prod/<br/>(3 replicas, green UI)"] Base --> DevOverlay Base --> ProdOverlay end Build["kubectl kustomize overlays/prod"] Live["Live objects<br/>(each field has an owner)"] ProdOverlay --> Build Build -->|kubectl apply -k --server-side| Live Hand["kubectl scale / set image"] -->|takes ownership of a field| Live
1. Prerequisites & Starting State
- Working
k8s-labcluster from Lab CKAD-01. No other lab is required. - A working directory:
mkdir -p /tmp/k8s-gitops/{base,overlays/dev,overlays/prod}
cd /tmp/k8s-gitops2. A Base and Two Overlays
Kustomize does not use templates. A base holds plain manifests, and each overlay lists the changes one environment needs. It is built into kubectl (kubectl kustomize, kubectl apply -k).
Step 1: The base
cat << 'EOF' > base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: podinfo
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: podinfo
template:
metadata:
labels:
app.kubernetes.io/name: podinfo
spec:
containers:
- name: podinfo
image: ghcr.io/stefanprodan/podinfo:6.7.1
ports:
- name: http
containerPort: 9898
resources:
requests:
cpu: 50m
memory: 32Mi
limits:
cpu: 200m
memory: 128Mi
EOF
cat << 'EOF' > base/service.yaml
apiVersion: v1
kind: Service
metadata:
name: podinfo
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: podinfo
ports:
- name: http
port: 9898
targetPort: http
EOF
cat << 'EOF' > base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
labels:
- pairs:
app.kubernetes.io/part-of: shop
EOFlabels adds the label to every object’s metadata. The older commonLabels field is deprecated; it also rewrote selectors, which cannot be changed on an existing Deployment.
Step 2: The overlays
Each overlay sets a namespace and name prefix, generates a ConfigMap, and patches the Deployment to read it. The production overlay also sets the replica count and pins the image tag.
cat << 'EOF' > overlays/dev/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
namespace: podinfo-dev
namePrefix: dev-
configMapGenerator:
- name: ui
literals:
- PODINFO_UI_COLOR=#3498db
- PODINFO_UI_MESSAGE=Development Environment
patches:
- target:
kind: Deployment
name: podinfo
patch: |-
- op: add
path: /spec/template/spec/containers/0/envFrom
value:
- configMapRef:
name: ui
EOF
cat << 'EOF' > overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
namespace: podinfo-prod
namePrefix: prod-
replicas:
- name: podinfo
count: 3
images:
- name: ghcr.io/stefanprodan/podinfo
newTag: 6.7.1
configMapGenerator:
- name: ui
literals:
- PODINFO_UI_COLOR=#2ecc71
- PODINFO_UI_MESSAGE=Production Environment
patches:
- target:
kind: Deployment
name: podinfo
patch: |-
- op: add
path: /spec/template/spec/containers/0/envFrom
value:
- configMapRef:
name: ui
EOF3. Build, Inspect and Deploy
Step 1: Render without applying
kubectl kustomize overlays/prod | grep -E "^kind:| name:|namespace:|replicas:|part-of"kind: ConfigMap
name: prod-ui-68dhtt6gk2
namespace: podinfo-prod
kind: Service
app.kubernetes.io/part-of: shop
name: prod-podinfo
namespace: podinfo-prod
kind: Deployment
app.kubernetes.io/part-of: shop
name: prod-podinfo
namespace: podinfo-prod
replicas: 3
name: prod-ui-68dhtt6gk2
name: podinfo
name: http
Three things to notice:
- names carry the prefix and every object has the namespace;
- the generated ConfigMap name ends in a hash of its content, and the reference inside the Deployment was rewritten to match. Change a literal and the name changes, which changes the Pod template and so triggers a rollout;
- the patch wrote
name: ui, and Kustomize resolved it.
Step 2: Deploy both environments
kubectl create namespace podinfo-dev
kubectl create namespace podinfo-prod
kubectl apply -k overlays/dev --server-side
kubectl apply -k overlays/prod --server-side
kubectl rollout status deployment/prod-podinfo -n podinfo-prod --timeout=120s
kubectl get deployment -n podinfo-dev
kubectl get deployment -n podinfo-prod
kubectl exec -n podinfo-prod deployment/prod-podinfo -- env | grep PODINFO_UI_NAME READY UP-TO-DATE AVAILABLE AGE
dev-podinfo 1/1 1 1 2s
NAME READY UP-TO-DATE AVAILABLE AGE
prod-podinfo 3/3 3 3 2s
PODINFO_UI_COLOR=#2ecc71
PODINFO_UI_MESSAGE=Production Environment
4. Controlled Scenario: Drift and Field Ownership
With server-side apply, the API server records which manager set each field. That record decides what happens when two sources disagree.
Step 1: Change the live object by hand
kubectl scale deployment/prod-podinfo -n podinfo-prod --replicas=1
kubectl set image deployment/prod-podinfo -n podinfo-prod podinfo=ghcr.io/stefanprodan/podinfo:6.7.0The cluster now differs from the files. See who owns what:
kubectl get deployment prod-podinfo -n podinfo-prod \
-o jsonpath='{range .metadata.managedFields[*]}{.manager}{" ("}{.operation}{") "}{.subresource}{"\n"}{end}'kubectl (Apply)
kubectl (Update) scale
kube-controller-manager (Update) status
kubectl-set (Update)
kubectl scale and kubectl set image each became the owner of the field they changed.
Step 2: Apply the files again and read the conflict
kubectl apply -k overlays/prod --server-sideconfigmap/prod-ui-68dhtt6gk2 serverside-applied
service/prod-podinfo serverside-applied
error: Apply failed with 2 conflicts: conflicts with "kubectl" with subresource "scale" using apps/v1:
- .spec.replicas
conflicts with "kubectl-set" using apps/v1:
- .spec.template.spec.containers[name="podinfo"].image
Please review the fields above--they currently have other managers. Here
are the ways you can resolve this warning:
* If you intend to manage all of these fields, please re-run the apply
command with the `--force-conflicts` flag.
* If you do not intend to manage all of the fields, please edit your
manifest to remove references to the fields that should keep their
current managers.
* You may co-own fields by updating your manifest to match the existing
value; in this case, you'll become the manager if the other manager(s)
stop managing the field (remove it from their configuration).
The apply is refused, and the Deployment is unchanged. Server-side apply will not silently overwrite a field another manager set. The message lists the three choices, and they are a decision, not a formality:
| Choice | When it is right |
|---|---|
--force-conflicts | The files are the source of truth and the manual change was a mistake |
| Remove the field from the manifest | Something else should own it. The usual case is replicas when a HorizontalPodAutoscaler scales the Deployment |
| Change the manifest to the live value | The manual change was correct and the files are behind |
Step 3: Resolve it
Here the files are the source of truth:
kubectl apply -k overlays/prod --server-side --force-conflicts
kubectl get deployment prod-podinfo -n podinfo-prod \
-o jsonpath='{.spec.replicas} {.spec.template.spec.containers[0].image}{"\n"}'deployment.apps/prod-podinfo serverside-applied
3 ghcr.io/stefanprodan/podinfo:6.7.1
Client-side apply behaves differently
Without
--server-side,kubectl apply -kwould have overwritten both fields with no conflict, because both are in the file. It would not remove a field added by hand that the file does not mention; Lab CKAD-04 shows that case. Preview either kind of apply withkubectl diff -k overlays/prod.
Running kubectl apply by hand is one reconciliation. A GitOps controller such as Argo CD or Flux does the same thing on a loop; nothing in this lab installs one.
5. Migrating a Manifest From a Removed API Version
A manifest keeps working only while the cluster serves its apiVersion. Create one written for an old cluster:
cat << 'EOF' > old-ingress.yaml
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: shop
namespace: podinfo-dev
spec:
rules:
- host: shop.example.test
http:
paths:
- path: /
backend:
serviceName: dev-podinfo
servicePort: 9898
EOF
kubectl apply -f old-ingress.yamlerror: resource mapping not found for name: "shop" namespace: "podinfo-dev" from "old-ingress.yaml": no matches for kind "Ingress" in version "networking.k8s.io/v1beta1"
ensure CRDs are installed first
no matches for kind ... in version ... means the kind exists but not at that version. Find the version the cluster serves and what its schema requires:
kubectl api-resources --api-group=networking.k8s.io
kubectl explain ingress.spec.rules.http.paths --api-version=networking.k8s.io/v1NAME SHORTNAMES APIVERSION NAMESPACED KIND
ingressclasses networking.k8s.io/v1 false IngressClass
ingresses ing networking.k8s.io/v1 true Ingress
ipaddresses ip networking.k8s.io/v1 false IPAddress
networkpolicies netpol networking.k8s.io/v1 true NetworkPolicy
servicecidrs networking.k8s.io/v1 false ServiceCIDR
kubectl explain shows that in v1 both backend and pathType are required, and backend is now a service object with a name and a port. Changing only the apiVersion line is not enough. Write the migrated file and validate it against the API server without creating anything:
cat << 'EOF' > new-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: shop
namespace: podinfo-dev
spec:
rules:
- host: shop.example.test
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: dev-podinfo
port:
number: 9898
EOF
kubectl apply --dry-run=server -f new-ingress.yamlingress.networking.k8s.io/shop created (server dry run)
Two related checks:
-
Gateway API is a different API group,
gateway.networking.k8s.io, and is served only after its CRDs are installed. It does not appear in the listing above, and its absence there says nothing about deprecation. -
Before an upgrade, the API server reports which deprecated versions clients are still calling:
kubectl get --raw /metrics | grep '^apiserver_requested_deprecated_apis'The deprecated API migration guide lists what each release removes.
6. Recreating a Namespace From Its Manifests
Delete the production namespace and rebuild it from the overlay:
kubectl delete namespace podinfo-prod
kubectl create namespace podinfo-prod
kubectl apply -k overlays/prod --server-side
kubectl rollout status deployment/prod-podinfo -n podinfo-prod --timeout=120s
kubectl exec -n podinfo-prod deployment/prod-podinfo -- env | grep PODINFO_UI_MESSAGEPODINFO_UI_MESSAGE=Production Environment
Everything came back, including the configuration, because all of it is in the files. Be precise about what this proves: it redeploys declared objects. It does not restore anything created at runtime, such as a Secret added with kubectl create, the contents of a PersistentVolume, or a database. Those need a real backup, which is the subject of Backup and Restore and Lab CKA-05.
7. Cleanup
kubectl delete namespace podinfo-dev podinfo-prod
cd ~ && rm -rf /tmp/k8s-gitopsPractice Without the Answer
Start again from the base and overlays above (re-create them if you cleaned up). Suggested time: 10 minutes.
- Add an overlay
overlays/stagingthat deploys to namespacepodinfo-stagingwith prefixstg-, two replicas, the UI messageStaging Environment, and an extra labeltier: stagingon every object. - Without editing the base, make staging use image tag
6.7.0. - Render it and save the output to
/tmp/staging.yaml.
Check yourself:
grep -c "tier: staging" /tmp/staging.yaml # 3 or more
grep "image:" /tmp/staging.yaml # ...podinfo:6.7.0
grep -E "replicas:|namespace:" /tmp/staging.yaml # replicas: 2, namespace: podinfo-staging
kubectl apply --dry-run=server -f /tmp/staging.yaml # after creating the namespaceFurther reading
Next Lab
Proceed to Lab CKAD-08 — Build & Ship Container Images, or return to the CKAD Track.