Kubernetes has become the de facto standard for container orchestration, but deploying applications effectively requires understanding several key concepts and best practices. In this article, we'll explore essential patterns for production-ready Kubernetes deployments.
Why Deployment Best Practices Matter
Poor deployment configurations can lead to:
- Application downtime during updates
- Resource exhaustion affecting other workloads
- Security vulnerabilities in your cluster
- Unpredictable scaling behavior
Let's dive into the critical areas you need to master.
1. Resource Requests and Limits
Always define resource requests and limits for your containers. This ensures proper scheduling and prevents resource starvation.
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 3
template:
spec:
containers:
- name: app
image: my-app:1.0.0
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
Why This Matters
| Resource Type | Purpose | Impact if Not Set |
|---|---|---|
| CPU Request | Scheduling guarantee | Poor pod placement |
| Memory Request | Scheduling guarantee | OOMKill risk |
| CPU Limit | Throttling threshold | Noisy neighbor problems |
| Memory Limit | Hard cap | Potential OOMKill |
2. Health Checks: Liveness and Readiness Probes
Kubernetes needs to know when your application is ready to serve traffic and when it's unhealthy.
# Example Flask health check endpoints
from flask import Flask, jsonify
import psutil
app = Flask(__name__)
@app.route('/healthz')
def liveness():
"""Liveness probe - is the app running?"""
return jsonify({"status": "alive"}), 200
@app.route('/ready')
def readiness():
"""Readiness probe - can the app handle requests?"""
# Check dependencies like database, cache, etc.
try:
# Verify database connection
db.session.execute('SELECT 1')
return jsonify({"status": "ready"}), 200
except Exception as e:
return jsonify({"status": "not ready", "error": str(e)}), 503
Configure these probes in your deployment:
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 2
Pro Tip: Liveness probes should check if the app is alive, not if dependencies are available. If your database is down, the app is still "alive" - it's just not "ready".
3. Rolling Update Strategy
Configure your deployment strategy to ensure zero-downtime updates:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # Max pods above desired count during update
maxUnavailable: 0 # Ensures at least N replicas always available
This configuration ensures:
1. New pods start before old ones terminate
2. Service remains available throughout the update
3. Gradual rollout reduces blast radius
4. Pod Disruption Budgets
Protect your application from voluntary disruptions like node drains:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: my-app-pdb
spec:
minAvailable: 2
selector:
matchLabels:
app: my-app
5. Security Context
Run containers with minimal privileges:
securityContext:
runAsNonRoot: true
runAsUser: 1000
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
Complete Example Deployment
Here's a production-ready deployment incorporating all best practices:
apiVersion: apps/v1
kind: Deployment
metadata:
name: production-app
labels:
app: production-app
version: v1.0.0
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: production-app
template:
metadata:
labels:
app: production-app
version: v1.0.0
spec:
serviceAccountName: production-app-sa
securityContext:
fsGroup: 1000
containers:
- name: app
image: gcr.io/my-project/app:1.0.0
ports:
- containerPort: 8080
name: http
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
securityContext:
runAsNonRoot: true
runAsUser: 1000
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
env:
- name: ENV
value: "production"
- name: LOG_LEVEL
value: "info"
volumeMounts:
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /app/cache
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir: {}
Deployment Checklist
Before deploying to production, verify:
- ✅ Resource requests and limits defined
- ✅ Liveness and readiness probes configured
- ✅ Rolling update strategy set with
maxUnavailable: 0 - ✅ Pod Disruption Budget created
- ✅ Security context configured (non-root, read-only filesystem)
- ✅ Multiple replicas for high availability
- ✅ Proper labels for observability
- ✅ Monitoring and logging configured
Monitoring Your Deployments
Use these commands to monitor deployment progress:
# Watch deployment rollout
kubectl rollout status deployment/my-app
# Check deployment history
kubectl rollout history deployment/my-app
# Rollback if needed
kubectl rollout undo deployment/my-app
# View pod events
kubectl get events --sort-by='.lastTimestamp' | grep my-app
Common Pitfalls to Avoid
- No resource limits - Can cause node resource exhaustion
- Missing probes - Kubernetes can't manage unhealthy pods properly
- Aggressive probe timing - Can cause cascading failures
- Single replica - No high availability
- Running as root - Security risk
- No PodDisruptionBudget - Vulnerable to disruptions
Conclusion
Kubernetes deployments require thoughtful configuration to achieve reliability, security, and performance. By following these best practices, you'll build resilient applications that can handle failures gracefully and scale with confidence.
Remember: Start with good defaults, then tune based on your application's specific needs and monitoring data.
Have questions about Kubernetes deployments? Reach out to us for expert DevOps consulting.