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

  1. One file per resource, --- to stack several in one file is fine.
  2. Indent with spaces, no tabs. Two spaces per level.
  3. selector.matchLabels must match template.metadata.labels exactly, or apply fails (Deployment/StatefulSet) or silently matches nothing (Service, the C22 trap).
  4. restartPolicy: Always for pods created by controllers; Never for Jobs. Bare Pods always restart with Always semantics (that’s why bare pods are bad for jobs).
  5. Apply dry-run first: kubectl apply --dry-run=client -f x.yaml catches YAML errors; kubectl apply --dry-run=server -f x.yaml catches schema errors against the real API.
  6. Never put secrets in the image. ConfigMap/Secret only. 10.43 has the why.

This site uses Just the Docs, a documentation theme for Jekyll.