Kubernetes YAML Patterns Cheat Sheet¶
Copy-paste-ready patterns for common Kubernetes resources.
Remember: Every production Deployment should have these five things: probes (startup + readiness + liveness), resource requests/limits, security context (non-root, read-only, drop caps), topology spread (across zones), and PDB (PodDisruptionBudget). Missing any of these causes outages during scaling, node maintenance, or security audits.
Deployment (Production-Ready)¶
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: prod
spec:
replicas: 3
revisionHistoryLimit: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
version: v2
spec:
serviceAccountName: api-sa
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: api
image: registry.example.com/api:v2.1.0@sha256:abc123
ports:
- containerPort: 8080
name: http
env:
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: api-config
key: db_host
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: api-secrets
key: db_password
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
startupProbe:
httpGet:
path: /healthz
port: http
failureThreshold: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: http
periodSeconds: 5
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 15
periodSeconds: 10
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: api
Service + Ingress¶
apiVersion: v1
kind: Service
metadata:
name: api
spec:
selector:
app: api
ports:
- name: http
port: 80
targetPort: http # References containerPort name
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
tls:
- hosts: [api.example.com]
secretName: api-tls
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api
port:
name: http
Gotcha: Service
targetPortcan reference a containerPort by name (e.g.,targetPort: http) instead of number. Named ports are strongly recommended because they decouple the Service from the container's actual port number. If you change the container port from 8080 to 9090, you only update the Deployment — the Service keeps working because it references the name, not the number.
CronJob¶
apiVersion: batch/v1
kind: CronJob
metadata:
name: db-backup
spec:
schedule: "0 2 * * *" # Daily at 2 AM
concurrencyPolicy: Forbid # Don't overlap
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 2
activeDeadlineSeconds: 3600
template:
spec:
restartPolicy: OnFailure
containers:
- name: backup
image: postgres:16
command: ["/bin/sh", "-c"]
args: ["pg_dump -h $DB_HOST -U $DB_USER $DB_NAME | gzip > /backup/db-$(date +%Y%m%d).sql.gz"]
envFrom:
- secretRef:
name: db-creds
volumeMounts:
- name: backup
mountPath: /backup
volumes:
- name: backup
persistentVolumeClaim:
claimName: backup-pvc
ConfigMap + Secret¶
apiVersion: v1
kind: ConfigMap
metadata:
name: api-config
data:
db_host: "db.prod.svc.cluster.local"
log_level: "info"
config.yaml: |
server:
port: 8080
read_timeout: 30s
---
apiVersion: v1
kind: Secret
metadata:
name: api-secrets
type: Opaque
stringData: # Plain text (encoded on apply)
db_password: "supersecret"
api_key: "key123"
PodDisruptionBudget¶
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api-pdb
spec:
minAvailable: 2 # OR: maxUnavailable: 1
selector:
matchLabels:
app: api
NetworkPolicy (Allow Specific Traffic)¶
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-netpol
spec:
podSelector:
matchLabels:
app: api
policyTypes: [Ingress, Egress]
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- port: 8080
egress:
- to:
- podSelector:
matchLabels:
app: db
ports:
- port: 5432
- ports: # Allow DNS
- port: 53
protocol: UDP
- port: 53
protocol: TCP
HPA + VPA¶
# HPA
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api
minReplicas: 2
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
behavior:
scaleDown:
stabilizationWindowSeconds: 300
---
# VPA (VerticalPodAutoscaler)
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: api
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: api
updatePolicy:
updateMode: "Off" # Recommendation only
ServiceAccount + RBAC¶
apiVersion: v1
kind: ServiceAccount
metadata:
name: api-sa
annotations:
# AWS IRSA
eks.amazonaws.com/role-arn: arn:aws:iam::123:role/api-role
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: api-role
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: api-binding
subjects:
- kind: ServiceAccount
name: api-sa
roleRef:
kind: Role
name: api-role
apiGroup: rbac.authorization.k8s.io
StatefulSet (Database)¶
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres
replicas: 3
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16
ports:
- containerPort: 5432
name: postgres
envFrom:
- secretRef:
name: postgres-creds
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: gp3
resources:
requests:
storage: 50Gi
---
# Headless service (required for StatefulSet)
apiVersion: v1
kind: Service
metadata:
name: postgres
spec:
clusterIP: None
selector:
app: postgres
ports:
- port: 5432
Default trap:
concurrencyPolicy: Allow(the CronJob default) lets multiple Job instances run simultaneously if previous ones have not finished. For database backups or cleanup jobs, this is usually wrong — useconcurrencyPolicy: Forbidto skip the new run if the previous one is still active, orReplaceto kill the old one and start fresh.
Init Container Pattern¶
spec:
initContainers:
- name: wait-for-db
image: busybox:1.36
command: ['sh', '-c', 'until nc -z db 5432; do echo waiting; sleep 2; done']
- name: migrate
image: myapp:v2
command: ['./migrate', '--target', 'latest']
envFrom:
- secretRef:
name: db-creds
containers:
- name: app
image: myapp:v2
# ... main container