Applications fail. They crash, freeze, or become unresponsive. Kubernetes probes detect these failures and take action. Liveness probes restart unhealthy containers. Readiness probes stop sending traffic to unprepared pods. Startup probes handle slow-starting applications. In this lesson, you'll master application health management.
1. Learning Objectives
By the end of this lesson, you will be able to:
- Configure liveness probes for automatic restart
- Set up readiness probes for traffic management
- Use startup probes for slow-starting apps
- Choose between HTTP, TCP, and exec probe types
- Configure probe parameters (delay, period, timeout)
- Debug probe failures
2. Why This Matters
Real-world scenario: Your app deadlocks and stops responding. Without liveness probes, it stays broken forever. With liveness probes, Kubernetes restarts it in seconds. Your app takes 30 seconds to warm up. Without readiness probes, it receives traffic before ready. With readiness probes, Kubernetes waits.
3. Core Concepts
Probe Types
Three Probe Types:
1. Liveness Probe: Is the container alive?
- If fails: Container is restarted
- Purpose: Recover from deadlocks, crashes
2. Readiness Probe: Is the container ready to serve traffic?
- If fails: Traffic is NOT sent to this pod
- Purpose: Ensure only healthy pods receive requests
3. Startup Probe: Has the application started successfully?
- If fails: Container is restarted
- Purpose: Slow-starting apps (overrides liveness during startup)
Probe types explained
HTTP Probe (Most Common)
# deployment-with-http-probes.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp
spec:
replicas: 3
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
spec:
containers:
- name: app
image: myapp:latest
ports:
- containerPort: 8080
# Liveness Probe
livenessProbe:
httpGet:
path: /health
port: 8080
httpHeaders:
- name: Custom-Header
value: liveness-check
initialDelaySeconds: 30 # Wait before first probe
periodSeconds: 10 # Check every 10 seconds
timeoutSeconds: 5 # Wait 5 seconds for response
successThreshold: 1 # Consecutive successes needed
failureThreshold: 3 # Consecutive failures to restart
# Readiness Probe
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
successThreshold: 1
failureThreshold: 3
# Startup Probe (for slow-starting apps)
startupProbe:
httpGet:
path: /startup
port: 8080
initialDelaySeconds: 0
periodSeconds: 5
failureThreshold: 30 # Allow 30 * 5 = 150 seconds to start
# Application endpoints needed:
# /health - returns 200 if app is alive, 5xx if dead
# /ready - returns 200 if app ready for traffic
# /startup - returns 200 when app fully started
HTTP probes configuration
TCP Probe
# tcp-probe.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: database
spec:
template:
spec:
containers:
- name: postgres
image: postgres:15-alpine
ports:
- containerPort: 5432
livenessProbe:
tcpSocket:
port: 5432
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
readinessProbe:
tcpSocket:
port: 5432
initialDelaySeconds: 5
periodSeconds: 5
# TCP probe checks if the port is open
# No need for custom health endpoints
# Works for databases, Redis, etc.
TCP probes for databases and services
Exec Probe (Command Execution)
# exec-probe.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-with-exec-probe
spec:
template:
spec:
containers:
- name: app
image: myapp:latest
livenessProbe:
exec:
command:
- /bin/sh
- -c
- |
# Check if process is responding
pgrep myapp && \
curl -f http://localhost:8080/internal-health
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command:
- test
- -f
- /tmp/ready
initialDelaySeconds: 5
periodSeconds: 5
# Exec probe runs command inside container
# Exit code 0 = success, non-zero = failure
# Good for custom health logic
Exec probes for custom health checks
Complete Application Health Endpoints
# Python Flask example with health endpoints
from flask import Flask, jsonify
import redis
import psycopg2
import os
app = Flask(__name__)
# Get dependencies from environment
redis_client = redis.Redis(host=os.environ.get('REDIS_HOST', 'localhost'))
def get_db_connection():
return psycopg2.connect(
host=os.environ.get('DB_HOST', 'localhost'),
database=os.environ.get('DB_NAME', 'app'),
user=os.environ.get('DB_USER', 'postgres'),
password=os.environ.get('DB_PASSWORD', '')
)
@app.route('/health')
def health():
"""Liveness probe - just check if process is alive"""
return jsonify({'status': 'alive'}), 200
@app.route('/ready')
def ready():
"""Readiness probe - check if ready to serve traffic"""
checks = {
'database': False,
'redis': False,
'disk_space': False
}
# Check database
try:
conn = get_db_connection()
conn.close()
checks['database'] = True
except Exception:
pass
# Check Redis
try:
redis_client.ping()
checks['redis'] = True
except Exception:
pass
# Check disk space
import shutil
disk = shutil.disk_usage('/')
if disk.free / disk.total > 0.1: # At least 10% free
checks['disk_space'] = True
if all(checks.values()):
return jsonify({'status': 'ready', 'checks': checks}), 200
else:
return jsonify({'status': 'not ready', 'checks': checks}), 503
@app.route('/startup')
def startup():
"""Startup probe - check if app is fully initialized"""
# Check if migrations are complete
# Check if cache is warmed
# Check if all services are connected
return jsonify({'status': 'started'}), 200
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)
Application health endpoints implementation
Node.js Health Endpoints
// Node.js/Express health endpoints
const express = require('express');
const mongoose = require('mongoose');
const redis = require('redis');
const app = express();
const redisClient = redis.createClient();
// Liveness probe
app.get('/health', (req, res) => {
res.status(200).json({ status: 'alive', timestamp: Date.now() });
});
// Readiness probe
app.get('/ready', async (req, res) => {
const checks = { database: false, redis: false };
let ready = true;
// Check MongoDB connection
if (mongoose.connection.readyState !== 1) {
checks.database = false;
ready = false;
} else {
checks.database = true;
}
// Check Redis connection
try {
await redisClient.ping();
checks.redis = true;
} catch (err) {
checks.redis = false;
ready = false;
}
if (ready) {
res.status(200).json({ status: 'ready', checks });
} else {
res.status(503).json({ status: 'not ready', checks });
}
});
// Startup probe
let appStarted = false;
app.get('/startup', (req, res) => {
if (appStarted) {
res.status(200).json({ status: 'started' });
} else {
res.status(503).json({ status: 'starting' });
}
});
// Mark as started after initialization
setTimeout(() => {
appStarted = true;
console.log('Application started successfully');
}, 30000); // 30 second startup time
app.listen(8080);
Node.js health endpoints
4. Complete Project: Production Health Checks
# complete-health-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: production-app
namespace: production
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: production-app
template:
metadata:
labels:
app: production-app
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/metrics"
spec:
containers:
- name: app
image: myapp:latest
ports:
- containerPort: 8080
name: http
- containerPort: 9090
name: metrics
# Environment variables
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-secret
key: url
- name: REDIS_URL
valueFrom:
configMapKeyRef:
name: app-config
key: redis_url
# Resource limits
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
# Startup probe (for slow-starting app)
startupProbe:
httpGet:
path: /startup
port: 8080
initialDelaySeconds: 0
periodSeconds: 5
failureThreshold: 60 # 5 minutes total
timeoutSeconds: 5
# Liveness probe (after startup)
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 0 # Startup probe handles initial delay
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
successThreshold: 1
# Readiness probe (traffic only when ready)
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 0
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
successThreshold: 1
# Lifecycle hooks
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- |
echo "Shutting down..."
sleep 15
echo "Goodbye"
# Graceful termination period
terminationGracePeriodSeconds: 30
---
# Service
apiVersion: v1
kind: Service
metadata:
name: production-app-service
spec:
selector:
app: production-app
ports:
- port: 80
targetPort: 8080
type: ClusterIP
---
# PodDisruptionBudget (ensure availability during voluntary disruptions)
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: production-app-pdb
spec:
minAvailable: 2
selector:
matchLabels:
app: production-app
# Test probes
kubectl get pods -w
kubectl describe pod -l app=production-app
kubectl logs -l app=production-app --tail=20
# Simulate failure
kubectl exec -it <pod-name> -- kill 1
# Watch Kubernetes restart the pod
kubectl get pods -w
Production health checks with all probe types
5. Probe Troubleshooting
# Check probe status in pod events
kubectl describe pod <pod-name>
# Look for:
# Liveness: ...
# Readiness: ...
# Check if pod is ready
kubectl get pods
# READY column shows: 1/1 = ready, 0/1 = not ready
# Check pod conditions
kubectl get pod <pod-name> -o jsonpath='{.status.conditions}' | jq .
# View probe failures in logs
kubectl logs <pod-name> --previous # See why it restarted
# Temporarily disable probes for debugging (edit deployment)
kubectl edit deployment <deployment-name>
# Remove or comment out probe sections
# Common probe failure reasons:
# 1. Health endpoint returns 5xx
# 2. Health endpoint times out
# 3. Wrong port or path
# 4. initialDelaySeconds too short
# 5. failureThreshold too low
Debugging probe failures
6. Common Errors & Solutions
7. Summary Checklist
8. Next Steps
Next lesson: Kubernetes Lesson 6.7: Helm Charts
Comments (0)
This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!
ReplyLeave a Comment