10.32 - YAML Reference
Manifest anatomy, then a copy-paste example for every resource you will use
this week. All files belong in ~/k8s/manifests/ grouped by app, mirroring
your ~/Containers/ habit:
~/k8s/manifests/
├── hello/
│ ├── deployment.yaml
│ └── service.yaml
└── blog/ # day 7 final project
├── namespace.yaml
├── configmap.yaml
├── deployment.yaml
├── service.yaml
└── cronjob.yaml
Anatomy of any manifest
apiVersion: apps/v1 # which API group/version (see table below)
kind: Deployment # the object type
metadata: # identity
name: hello # unique per kind+namespace
namespace: lab # optional; default is 'default'
labels: # arbitrary, for YOUR organization
app: hello
spec: # desired state, kind-specific
replicas: 2
apiVersion by kind (memorize the four you use most):
| kind | apiVersion |
|---|---|
| Pod, Service, ConfigMap, Secret, PVC, Ingress (v1) | v1 |
| Deployment, ReplicaSet, StatefulSet, DaemonSet | apps/v1 |
| Job, CronJob | batch/v1 |
| HorizontalPodAutoscaler | autoscaling/v2 |
Always run kubectl apply --dry-run=client -f x.yaml before applying, and
kubectl explain <kind>.<field> when unsure.
Deployment (the workhorse)
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello
labels: { app: hello }
spec:
replicas: 2
selector:
matchLabels: { app: hello } # immutable after creation
template:
metadata:
labels: { app: hello } # MUST match selector
spec:
containers:
- name: hello
image: nginx:alpine
ports:
- containerPort: 80
env: # see 10.43
- name: MESSAGE
value: hi
resources: # see C18
requests: { cpu: 100m, memory: 128Mi }
limits: { cpu: 500m, memory: 256Mi }
livenessProbe: # see 10.51
httpGet: { path: /, port: 80 }
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet: { path: /, port: 80 }
Service
apiVersion: v1
kind: Service
metadata:
name: hello
spec:
type: ClusterIP # ClusterIP | NodePort | LoadBalancer
selector: { app: hello } # label selector, must match pods
ports:
- port: 80 # the port DNS/ClusterIP serves
targetPort: 80 # the port the pod listens on
# nodePort: 30080 # only for type: NodePort, 30000-32767
PVC and its use
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mydata
spec:
accessModes: [ReadWriteOnce]
resources:
requests: { storage: 1Gi }
# storageClassName omitted -> default (local-path on k3s)
# inside a Deployment's pod template:
spec:
template:
spec:
volumes:
- name: data
persistentVolumeClaim: { claimName: mydata }
containers:
- name: writer
image: busybox
command: ["sh", "-c", "echo hi > /data/marker; sleep 3600"]
volumeMounts:
- { name: data, mountPath: /data }
StatefulSet
apiVersion: v1
kind: Service # headless service (no ClusterIP)
metadata:
name: db
spec:
clusterIP: None
selector: { app: db }
ports: [{ port: 3306 }]
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: db
spec:
serviceName: db # required, points at headless service
replicas: 3
selector: { matchLabels: { app: db } }
template:
metadata:
labels: { app: db }
spec:
containers:
- name: db
image: mysql:8
volumeMounts: [{ name: data, mountPath: /var/lib/mysql }]
volumeClaimTemplates: # per-pod PVC, this is the magic
- metadata: { name: data }
spec:
accessModes: [ReadWriteOnce]
resources: { requests: { storage: 1Gi } }
Pods are named db-0, db-1, db-2; each gets its own PVC data-db-0, etc.
Delete the StatefulSet and the PVCs survive (data keeps). Scale down keeps
identity.
DaemonSet
apiVersion: apps/v1
kind: DaemonSet
metadata: { name: nodeinfo }
spec:
selector: { matchLabels: { app: nodeinfo } }
template:
metadata: { labels: { app: nodeinfo } }
spec:
containers:
- name: nodeinfo
image: nginx:alpine
Job and CronJob
apiVersion: batch/v1
kind: Job
metadata: { name: once }
spec:
template:
spec:
restartPolicy: Never # Jobs require this
containers:
- name: once
image: busybox
command: ["sh", "-c", "echo done && sleep 2"]
---
apiVersion: batch/v1
kind: CronJob
metadata: { name: nightly }
spec:
schedule: "0 2 * * *" # cron syntax, cluster TZ
jobTemplate:
spec:
template:
spec:
restartPolicy: Never
containers:
- name: backup
image: busybox
command: ["sh", "-c", "echo backup at $(date) >> /data/backup.log"]
ConfigMap and Secret
apiVersion: v1
kind: ConfigMap
metadata: { name: greeting }
data:
message: hello-from-configmap
style: minimal
---
apiVersion: v1
kind: Secret
metadata: { name: dbpass }
type: Opaque
stringData: # plaintext in the file; API stores base64
password: supersecret
Wiring both into a pod:
spec:
template:
spec:
containers:
- name: app
image: busybox
command: ["sh", "-c", "sleep 3600"]
env:
- name: MESSAGE
valueFrom:
configMapKeyRef: { name: greeting, key: message }
- name: DB_PASSWORD
valueFrom:
secretKeyRef: { name: dbpass, key: password }
volumeMounts:
- { name: cfg, mountPath: /etc/config }
volumes:
- name: cfg
configMap: { name: greeting }
Ingress (after enabling Traefik, see 10.41)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata: { name: web }
spec:
rules:
- host: lab.example
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web
port: { number: 80 }
HPA (autoscaling/v2)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: { name: hello }
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: hello
minReplicas: 1
maxReplicas: 5
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 50
The target Deployment needs a CPU request or the HPA has nothing to
compute utilization against.
Namespace
apiVersion: v1
kind: Namespace
metadata: { name: lab }
Golden rules for manifests
- One file per resource,
---to stack several in one file is fine. - Indent with spaces, no tabs. Two spaces per level.
selector.matchLabelsmust matchtemplate.metadata.labelsexactly, or apply fails (Deployment/StatefulSet) or silently matches nothing (Service, the C22 trap).restartPolicy: Alwaysfor pods created by controllers;Neverfor Jobs. Bare Pods always restart with Always semantics (that’s why bare pods are bad for jobs).- Apply dry-run first:
kubectl apply --dry-run=client -f x.yamlcatches YAML errors;kubectl apply --dry-run=server -f x.yamlcatches schema errors against the real API. - Never put secrets in the image. ConfigMap/Secret only. 10.43 has the why.