---
title: "Kubernetes Resource Patterns"
description: "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."
type: skill
canonical_url: https://claudary.paisolsolutions.com/skills/resource-patterns
source: "Claudary"
difficulty: intermediate
author: "Claude Code Knowledge Pack"
date: 2026-07-10T11:46:08.095Z
license: CC-BY-4.0
attribution: "Kubernetes Resource Patterns — Claudary (https://claudary.paisolsolutions.com/skills/resource-patterns)"
---

# 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.

## 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`

```yaml
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`

```yaml
# 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)

```yaml
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`

```yaml
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`)

```yaml
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`

```yaml
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`

```yaml
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`

```yaml
# 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`

```yaml
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`

```yaml
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)

```yaml
# 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`

```yaml
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

```yaml
# 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

```yaml
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`

```yaml
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`

```yaml
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

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

---

## ResourceQuota

```yaml
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

```yaml
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

---

Source: [Claudary](https://claudary.paisolsolutions.com/skills/resource-patterns) · https://claudary.paisolsolutions.com
