All skills
Skillintermediate

Kubernetes Resource Patterns

Canonical YAML patterns for all resources generated by this skill. Each section shows a full pattern with safe defaults, lists required fields, and notes common mistakes.

Claude Code Knowledge Pack7/10/2026

Overview

Kubernetes Resource Patterns

Canonical YAML patterns for all resources generated by this skill. Each section shows a full pattern with safe defaults, lists required fields, and notes common mistakes.


Deployment

Required fields: apiVersion, kind, metadata.name, spec.selector, spec.template, spec.template.spec.containers

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  namespace: default
  labels:
    app.kubernetes.io/name: my-app
    app.kubernetes.io/instance: my-app
    app.kubernetes.io/version: "1.0.0"
    app.kubernetes.io/part-of: my-app
    app.kubernetes.io/managed-by: kubectl
spec:
  replicas: 2
  selector:
    matchLabels:
      app.kubernetes.io/name: my-app
  template:
    metadata:
      labels:
        app.kubernetes.io/name: my-app   # Must match selector.matchLabels
    spec:
      containers:
      - name: my-app
        image: my-app:1.0.0
        imagePullPolicy: IfNotPresent
        ports:
        - name: http
          containerPort: 8080
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "256Mi"
            cpu: "500m"

Common mistakes:

  • selector.matchLabels must be a subset of template.metadata.labels — mismatch causes Invalid value error
  • selector is immutable after creation; changing it requires deleting and recreating the Deployment
  • Missing resources.limits causes pods to be scheduled without constraints

Service

Required fields: apiVersion, kind, metadata.name, spec.ports, spec.selector

# ClusterIP (default, internal only)
apiVersion: v1
kind: Service
metadata:
  name: my-app
  namespace: default
spec:
  type: ClusterIP
  selector:
    app.kubernetes.io/name: my-app
  ports:
  - name: http
    port: 80          # Port exposed on the Service
    targetPort: http  # Named port or number on the Pod
    protocol: TCP
---
# NodePort (external via node IP + port)
apiVersion: v1
kind: Service
metadata:
  name: my-app-nodeport
  namespace: default
spec:
  type: NodePort
  selector:
    app.kubernetes.io/name: my-app
  ports:
  - name: http
    port: 80
    targetPort: http
    nodePort: 30080   # 30000–32767; omit to auto-assign
---
# LoadBalancer (external via cloud LB)
apiVersion: v1
kind: Service
metadata:
  name: my-app-lb
  namespace: default
spec:
  type: LoadBalancer
  selector:
    app.kubernetes.io/name: my-app
  ports:
  - name: http
    port: 80
    targetPort: http

Common mistakes:

  • targetPort must match a containerPort name or number defined in the pod spec
  • selector must match labels on pods, not on the Deployment itself

StatefulSet

Required fields: spec.serviceName (must match a headless Service), spec.selector, spec.template, volumeClaimTemplates (if using persistent storage)

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: my-db
  namespace: default
spec:
  serviceName: my-db-headless   # Must reference a headless Service (clusterIP: None)
  replicas: 3
  podManagementPolicy: OrderedReady  # Or Parallel for faster scale
  selector:
    matchLabels:
      app.kubernetes.io/name: my-db
  template:
    metadata:
      labels:
        app.kubernetes.io/name: my-db
    spec:
      containers:
      - name: my-db
        image: postgres:15
        volumeMounts:
        - name: data
          mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes:
      - ReadWriteOnce
      storageClassName: standard
      resources:
        requests:
          storage: 10Gi

Common mistakes:

  • volumeClaimTemplates are immutable; to resize, delete the StatefulSet (keep pods), update PVCs, then recreate
  • The headless Service (clusterIP: None) must exist before the StatefulSet, or pods stay Pending
  • Pod DNS: <pod>.<serviceName>.<namespace>.svc.cluster.local

DaemonSet

Required fields: spec.selector, spec.template

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: my-agent
  namespace: default
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: my-agent
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
  template:
    metadata:
      labels:
        app.kubernetes.io/name: my-agent
    spec:
      tolerations:
      - key: node-role.kubernetes.io/control-plane
        effect: NoSchedule
      containers:
      - name: my-agent
        image: my-agent:1.0.0
        resources:
          requests:
            memory: "64Mi"
            cpu: "50m"
          limits:
            memory: "128Mi"
            cpu: "200m"

Common mistakes:

  • DaemonSets run on every node by default; use tolerations + nodeSelector to restrict to specific nodes
  • Without tolerations for control-plane taint, the pod won't run on control-plane nodes

Job

Required fields: spec.template.spec.containers, spec.template.spec.restartPolicy (must be Never or OnFailure)

apiVersion: batch/v1
kind: Job
metadata:
  name: my-batch-job
  namespace: default
spec:
  completions: 1
  parallelism: 1
  backoffLimit: 3
  ttlSecondsAfterFinished: 300   # Auto-delete after 5 minutes
  template:
    spec:
      restartPolicy: Never   # Never or OnFailure; not Always
      containers:
      - name: my-job
        image: my-job:1.0.0
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "256Mi"
            cpu: "500m"

Common mistakes:

  • restartPolicy: Always is forbidden in Jobs — use Never or OnFailure
  • Without ttlSecondsAfterFinished, completed Jobs and their pods accumulate indefinitely

CronJob

Required fields: spec.schedule, spec.jobTemplate

apiVersion: batch/v1
kind: CronJob
metadata:
  name: my-cron
  namespace: default
spec:
  schedule: "0 2 * * *"           # Daily at 02:00 UTC
  timeZone: "UTC"                  # Explicit timezone (K8s 1.27+ GA)
  concurrencyPolicy: Forbid        # Allow, Forbid, or Replace
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 1
  startingDeadlineSeconds: 600
  jobTemplate:
    spec:
      backoffLimit: 2
      ttlSecondsAfterFinished: 600
      template:
        spec:
          restartPolicy: OnFailure
          containers:
          - name: my-cron
            image: my-job:1.0.0
            resources:
              requests:
                memory: "64Mi"
                cpu: "50m"
              limits:
                memory: "128Mi"
                cpu: "200m"

Common mistakes:

  • schedule uses UTC by default; use timeZone to specify local timezone
  • concurrencyPolicy: Allow (default) can cause job pile-up if runs take longer than the interval

ConfigMap

Required fields: apiVersion, kind, metadata.name

apiVersion: v1
kind: ConfigMap
metadata:
  name: my-app-config
  namespace: default
immutable: false   # Set to true to prevent accidental changes
data:
  KEY: value
  config.yaml: |
    server:
      port: 8080
binaryData:
  logo.png: <base64-encoded bytes>   # Use binaryData for non-UTF-8 content

Common mistakes:

  • data values must be strings; use binaryData for binary content
  • immutable: true prevents updates; pod must be restarted manually after recreation

Secret

Required fields: apiVersion, kind, metadata.name, type

# Opaque (arbitrary key-value pairs)
apiVersion: v1
kind: Secret
metadata:
  name: my-app-secret
  namespace: default
type: Opaque
immutable: false
data:
  username: bXktdXNlcg==      # base64-encoded; echo -n 'my-user' | base64
  password: c2VjcmV0          # base64-encoded
---
# TLS secret (for Ingress TLS)
apiVersion: v1
kind: Secret
metadata:
  name: my-app-tls
  namespace: default
type: kubernetes.io/tls
data:
  tls.crt: <base64-encoded cert>
  tls.key: <base64-encoded key>

Common mistakes:

  • Values in data must be base64-encoded; use stringData for plain text (Kubernetes handles encoding)
  • stringData is write-only; it's always read back as base64 in data

PersistentVolumeClaim

Required fields: spec.accessModes, spec.resources.requests.storage

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-data-pvc
  namespace: default
spec:
  accessModes:
  - ReadWriteOnce    # RWO (single node), RWX (many nodes), ROX (read-only many)
  storageClassName: standard
  resources:
    requests:
      storage: 10Gi

Common mistakes:

  • accessModes must match what the StorageClass/PV supports; ReadWriteMany requires specific storage backends
  • PVC spec (except resources.requests.storage) is immutable after binding

Ingress

Required fields: spec.rules

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-ingress
  namespace: default
spec:
  ingressClassName: nginx    # Required in K8s 1.18+; do not use deprecated annotation
  tls:
  - hosts:
    - my-app.example.com
    secretName: my-app-tls
  rules:
  - host: my-app.example.com
    http:
      paths:
      - path: /
        pathType: Prefix   # Prefix, Exact, or ImplementationSpecific
        backend:
          service:
            name: my-app
            port:
              name: http

Common mistakes:

  • Use ingressClassName field, not the deprecated kubernetes.io/ingress.class annotation
  • pathType: ImplementationSpecific behavior varies by controller; prefer Prefix or Exact

NetworkPolicy

Required fields: spec.podSelector (empty = select all pods in namespace)

# Default deny all ingress and egress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: default
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress
---
# Allow ingress from specific namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-from-monitoring
  namespace: default
spec:
  podSelector:
    matchLabels:
      app.kubernetes.io/name: my-app
  policyTypes:
  - Ingress
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: monitoring
---
# Allow egress DNS only
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-egress-dns
  namespace: default
spec:
  podSelector: {}
  policyTypes:
  - Egress
  egress:
  - ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53

Common mistakes:

  • NetworkPolicy is additive: multiple policies union their rules; a pod not selected by any policy has no restrictions
  • Always create a default-deny-all before adding allow rules to avoid gaps
  • Forgetting to allow egress DNS (port 53) breaks service discovery

ServiceAccount

Required fields: apiVersion, kind, metadata.name

apiVersion: v1
kind: ServiceAccount
metadata:
  name: my-app
  namespace: default
automountServiceAccountToken: false   # Disable for workloads that don't need API access
imagePullSecrets:
- name: my-registry-secret   # Attach pull secrets to the SA instead of each pod

Role / ClusterRole

# Namespace-scoped
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: my-app-role
  namespace: default
rules:
- apiGroups: [""]          # "" = core API group
  resources: ["configmaps"]
  verbs: ["get", "list", "watch"]
- apiGroups: [""]
  resources: ["secrets"]
  resourceNames: ["my-app-secret"]   # Restrict to named resources
  verbs: ["get"]
---
# Cluster-scoped
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: my-app-reader
rules:
- apiGroups: [""]
  resources: ["nodes", "namespaces"]
  verbs: ["get", "list", "watch"]

RoleBinding / ClusterRoleBinding

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: my-app-rolebinding
  namespace: default
subjects:
- kind: ServiceAccount
  name: my-app
  namespace: default
roleRef:
  kind: Role           # Role or ClusterRole
  name: my-app-role
  apiGroup: rbac.authorization.k8s.io
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: my-app-clusterrolebinding
subjects:
- kind: ServiceAccount
  name: my-app
  namespace: default
roleRef:
  kind: ClusterRole
  name: my-app-reader
  apiGroup: rbac.authorization.k8s.io

Common mistakes:

  • roleRef is immutable; changing it requires deleting and recreating the binding
  • Use Role+RoleBinding for namespace-scoped access; ClusterRole+ClusterRoleBinding for cluster-wide access
  • A ClusterRole can be bound with a RoleBinding to limit it to one namespace

HorizontalPodAutoscaler (v2)

Required fields: spec.scaleTargetRef, spec.minReplicas, spec.maxReplicas, spec.metrics

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: my-app-hpa
  namespace: default
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-app
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80

Common mistakes:

  • Use autoscaling/v2 (available since Kubernetes v1.23, stable); autoscaling/v2beta2 is deprecated
  • HPA requires resources.requests to be set on containers; without requests, CPU/memory utilization cannot be calculated

PodDisruptionBudget

Required fields: spec.selector, one of minAvailable or maxUnavailable

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: my-app-pdb
  namespace: default
spec:
  minAvailable: 1     # Or use maxUnavailable; not both
  selector:
    matchLabels:
      app.kubernetes.io/name: my-app

Common mistakes:

  • Use policy/v1 (GA since K8s 1.21); policy/v1beta1 is removed
  • Setting minAvailable equal to replicas blocks all voluntary disruptions (node drain fails)

Namespace

apiVersion: v1
kind: Namespace
metadata:
  name: my-namespace
  labels:
    kubernetes.io/metadata.name: my-namespace   # Automatically set by K8s 1.21+

ResourceQuota

apiVersion: v1
kind: ResourceQuota
metadata:
  name: my-namespace-quota
  namespace: my-namespace
spec:
  hard:
    requests.cpu: "4"
    requests.memory: 8Gi
    limits.cpu: "8"
    limits.memory: 16Gi
    pods: "20"
    services: "10"
    persistentvolumeclaims: "10"

LimitRange

apiVersion: v1
kind: LimitRange
metadata:
  name: my-namespace-limits
  namespace: my-namespace
spec:
  limits:
  - type: Container
    default:          # Applied when container has no limits set
      cpu: "500m"
      memory: "256Mi"
    defaultRequest:   # Applied when container has no requests set