Deploy Jaeger distributed tracing on Kubernetes using Helm charts with Elasticsearch backend storage. Configure ingress, SSL certificates, and Prometheus integration for production-ready distributed tracing observability.
Prerequisites
- Kubernetes cluster with kubectl access
- Ingress controller (nginx-ingress recommended)
- At least 8GB RAM available for Elasticsearch cluster
- Storage class configured for persistent volumes
- DNS configured for ingress hostname
What this solves
Jaeger distributed tracing helps you monitor and troubleshoot microservices by tracking requests across multiple services. This tutorial sets up Jaeger on Kubernetes with Helm charts, using Elasticsearch for persistent storage and includes production configuration with SSL certificates and Prometheus monitoring. You'll get a complete distributed tracing solution that can handle production workloads.
Step-by-step configuration
Install Helm package manager
Install Helm to manage Kubernetes packages and dependencies. Helm simplifies deploying complex applications like Jaeger on Kubernetes clusters.
curl https://baltocdn.com/helm/signing.asc | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg > /dev/null
sudo apt-get install apt-transport-https --yes
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
sudo apt-get update
sudo apt-get install helm
Add Jaeger Helm repository
Add the official Jaeger Helm chart repository and update the local cache to access the latest chart versions.
helm repo add jaegertracing https://jaegertracing.github.io/helm-charts
helm repo update
Create namespace for Jaeger
Create a dedicated Kubernetes namespace to isolate Jaeger components from other applications running in your cluster.
kubectl create namespace jaeger-system
Deploy Elasticsearch for storage
Deploy Elasticsearch as the backend storage for Jaeger traces. This provides persistent storage and enables complex queries on trace data.
helm repo add elastic https://helm.elastic.co
helm repo update
replicas: 3
resources:
requests:
cpu: 1000m
memory: 2Gi
limits:
cpu: 2000m
memory: 4Gi
volumeClaimTemplate:
storageClassName: "standard"
resources:
requests:
storage: 50Gi
esConfig:
elasticsearch.yml: |
cluster.name: jaeger-elasticsearch
network.host: 0.0.0.0
bootstrap.memory_lock: false
discovery.seed_hosts: "elasticsearch-master-headless"
cluster.initial_master_nodes: "elasticsearch-master-0,elasticsearch-master-1,elasticsearch-master-2"
helm install elasticsearch elastic/elasticsearch -n jaeger-system -f elasticsearch-values.yaml
Configure Jaeger with Elasticsearch backend
Create Jaeger configuration that connects to Elasticsearch and includes production-ready settings with resource limits and replicas.
provisionDataStore:
cassandra: false
elasticsearch: true
storage:
type: elasticsearch
elasticsearch:
host: elasticsearch-master
port: 9200
scheme: http
user: ""
password: ""
nodesWanOnly: false
extraEnv:
- name: ES_SERVER_URLS
value: http://elasticsearch-master:9200
cmdlineParams:
es.num-shards: 5
es.num-replicas: 1
es.index-prefix: jaeger
query:
enabled: true
replicaCount: 2
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 256m
memory: 256Mi
service:
type: ClusterIP
port: 16686
collector:
enabled: true
replicaCount: 2
resources:
limits:
cpu: 1
memory: 1Gi
requests:
cpu: 500m
memory: 512Mi
service:
type: ClusterIP
grpc:
port: 14250
http:
port: 14268
zipkin:
port: 9411
agent:
enabled: true
daemonset:
useHostPort: true
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 128m
memory: 128Mi
Deploy Jaeger with Helm
Install Jaeger using the Helm chart with your custom configuration. This deploys all Jaeger components including collector, query, and agent.
helm install jaeger jaegertracing/jaeger -n jaeger-system -f jaeger-values.yaml
Configure ingress for Jaeger UI
Set up ingress to expose the Jaeger UI externally with SSL termination. This allows secure access to the Jaeger web interface.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: jaeger-ingress
namespace: jaeger-system
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
tls:
- hosts:
- jaeger.example.com
secretName: jaeger-tls
rules:
- host: jaeger.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: jaeger-query
port:
number: 16686
kubectl apply -f jaeger-ingress.yaml
Set up cert-manager for SSL certificates
Install cert-manager to automatically provision and manage SSL certificates for the Jaeger ingress. This ensures secure HTTPS access.
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.2/cert-manager.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@example.com
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
class: nginx
kubectl apply -f letsencrypt-issuer.yaml
Configure Prometheus monitoring
Set up Prometheus ServiceMonitor to collect metrics from Jaeger components. This enables monitoring of Jaeger performance and health.
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: jaeger-collector
namespace: jaeger-system
labels:
app: jaeger
spec:
selector:
matchLabels:
app.kubernetes.io/name: jaeger
app.kubernetes.io/component: collector
endpoints:
- port: admin-http
interval: 30s
path: /metrics
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: jaeger-query
namespace: jaeger-system
labels:
app: jaeger
spec:
selector:
matchLabels:
app.kubernetes.io/name: jaeger
app.kubernetes.io/component: query
endpoints:
- port: admin-http
interval: 30s
path: /metrics
kubectl apply -f jaeger-servicemonitor.yaml
Configure Jaeger production settings
Apply production-ready configuration including resource limits, health checks, and logging settings for optimal performance.
apiVersion: v1
kind: ConfigMap
metadata:
name: jaeger-production-config
namespace: jaeger-system
data:
collector.yaml: |
receivers:
jaeger:
protocols:
grpc:
endpoint: 0.0.0.0:14250
thrift_http:
endpoint: 0.0.0.0:14268
thrift_compact:
endpoint: 0.0.0.0:6831
thrift_binary:
endpoint: 0.0.0.0:6832
zipkin:
endpoint: 0.0.0.0:9411
processors:
batch:
timeout: 1s
send_batch_size: 1024
send_batch_max_size: 2048
memory_limiter:
limit_mib: 512
exporters:
elasticsearch:
endpoints: [http://elasticsearch-master:9200]
index: jaeger-span
mapping:
enabled: false
service:
pipelines:
traces:
receivers: [jaeger, zipkin]
processors: [memory_limiter, batch]
exporters: [elasticsearch]
extensions: [health_check]
kubectl apply -f jaeger-production-config.yaml
Verify your setup
Check that all Jaeger components are running correctly and can collect traces.
kubectl get pods -n jaeger-system
kubectl get svc -n jaeger-system
kubectl logs -n jaeger-system deployment/jaeger-collector
kubectl logs -n jaeger-system deployment/jaeger-query
Test the Jaeger UI is accessible:
kubectl port-forward -n jaeger-system svc/jaeger-query 16686:16686
Open your browser to http://localhost:16686 to access the Jaeger UI. You can also test trace ingestion:
curl -X POST http://localhost:14268/api/traces \
-H "Content-Type: application/json" \
-d '{"data":[{"traceID":"1","spanID":"1","operationName":"test-span","startTime":1234567890,"duration":1000,"process":{"serviceName":"test-service","tags":[]},"tags":[]}]}'
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Elasticsearch pods failing to start | Insufficient memory or storage | Increase resources in elasticsearch-values.yaml and redeploy |
| Jaeger collector can't connect to Elasticsearch | Network policy or service name mismatch | Check service names with kubectl get svc -n jaeger-system |
| SSL certificate not provisioned | DNS not pointing to cluster or cert-manager issues | Check cert-manager logs: kubectl logs -n cert-manager deployment/cert-manager |
| High memory usage in collector | Large trace volumes without proper batching | Adjust batch processor settings in production config |
| Traces not appearing in UI | Application not configured to send traces | Configure applications with Jaeger endpoint: http://jaeger-collector:14268 |
| Ingress returns 502 error | Jaeger query service not ready | Check query pod status: kubectl get pods -n jaeger-system -l app.kubernetes.io/component=query |
Next steps
- Configure Jaeger alerting with Prometheus and Grafana for distributed tracing observability
- Implement Istio observability with Jaeger tracing and Kiali dashboard for Kubernetes service mesh
- Configure Jaeger sampling strategies for production workloads
- Set up Jaeger multi-cluster federation for distributed tracing
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Jaeger distributed tracing installation script for Kubernetes
# Supports Ubuntu, Debian, AlmaLinux, Rocky Linux, CentOS, RHEL, Fedora
# Colors for output
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly NC='\033[0m'
# Default values
NAMESPACE="${1:-jaeger-system}"
ELASTICSEARCH_STORAGE="${2:-50Gi}"
# Usage message
usage() {
echo "Usage: $0 [namespace] [elasticsearch_storage_size]"
echo " namespace: Kubernetes namespace (default: jaeger-system)"
echo " elasticsearch_storage_size: Storage size for Elasticsearch (default: 50Gi)"
exit 1
}
# Logging functions
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Cleanup function for rollback
cleanup() {
log_error "Installation failed. Cleaning up..."
helm uninstall jaeger -n "$NAMESPACE" 2>/dev/null || true
helm uninstall elasticsearch -n "$NAMESPACE" 2>/dev/null || true
kubectl delete namespace "$NAMESPACE" 2>/dev/null || true
rm -f elasticsearch-values.yaml jaeger-values.yaml get_helm.sh
exit 1
}
trap cleanup ERR
# Detect distribution
detect_distro() {
if [ ! -f /etc/os-release ]; then
log_error "Cannot detect Linux distribution"
exit 1
fi
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_INSTALL="apt-get install -y"
PKG_UPDATE="apt-get update"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
PKG_UPDATE="dnf check-update || true"
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
PKG_UPDATE="yum check-update || true"
;;
*)
log_error "Unsupported distribution: $ID"
exit 1
;;
esac
log_info "Detected distribution: $PRETTY_NAME"
export PKG_MGR PKG_INSTALL PKG_UPDATE
}
# Check prerequisites
check_prerequisites() {
echo "[1/8] Checking prerequisites..."
if [[ $EUID -eq 0 ]]; then
log_error "This script should not be run as root for security reasons"
exit 1
fi
if ! command -v sudo >/dev/null 2>&1; then
log_error "sudo is required but not installed"
exit 1
fi
if ! kubectl cluster-info >/dev/null 2>&1; then
log_error "kubectl is not configured or Kubernetes cluster is not accessible"
exit 1
fi
log_info "Prerequisites check passed"
}
# Install Helm
install_helm() {
echo "[2/8] Installing Helm package manager..."
if command -v helm >/dev/null 2>&1; then
log_info "Helm is already installed"
return 0
fi
case "$PKG_MGR" in
apt)
curl -fsSL https://baltocdn.com/helm/signing.asc | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg > /dev/null
sudo $PKG_INSTALL apt-transport-https
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
sudo $PKG_UPDATE
sudo $PKG_INSTALL helm
;;
dnf|yum)
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod 755 get_helm.sh
./get_helm.sh
sudo mv /usr/local/bin/helm /usr/bin/ 2>/dev/null || true
;;
esac
log_info "Helm installed successfully"
}
# Add Helm repositories
add_helm_repos() {
echo "[3/8] Adding Helm repositories..."
helm repo add jaegertracing https://jaegertracing.github.io/helm-charts
helm repo add elastic https://helm.elastic.co
helm repo update
log_info "Helm repositories added and updated"
}
# Create namespace
create_namespace() {
echo "[4/8] Creating Kubernetes namespace..."
if kubectl get namespace "$NAMESPACE" >/dev/null 2>&1; then
log_warn "Namespace $NAMESPACE already exists"
else
kubectl create namespace "$NAMESPACE"
log_info "Namespace $NAMESPACE created"
fi
}
# Create Elasticsearch configuration
create_elasticsearch_config() {
echo "[5/8] Creating Elasticsearch configuration..."
cat > elasticsearch-values.yaml << 'EOF'
replicas: 3
minimumMasterNodes: 2
resources:
requests:
cpu: 1000m
memory: 2Gi
limits:
cpu: 2000m
memory: 4Gi
volumeClaimTemplate:
storageClassName: "standard"
resources:
requests:
storage: STORAGE_SIZE
esConfig:
elasticsearch.yml: |
cluster.name: jaeger-elasticsearch
network.host: 0.0.0.0
bootstrap.memory_lock: false
discovery.seed_hosts: "elasticsearch-master-headless"
cluster.initial_master_nodes: "elasticsearch-master-0,elasticsearch-master-1,elasticsearch-master-2"
xpack.security.enabled: false
persistence:
enabled: true
antiAffinity: "soft"
EOF
sed -i "s/STORAGE_SIZE/$ELASTICSEARCH_STORAGE/" elasticsearch-values.yaml
chmod 644 elasticsearch-values.yaml
log_info "Elasticsearch configuration created"
}
# Deploy Elasticsearch
deploy_elasticsearch() {
echo "[6/8] Deploying Elasticsearch..."
helm install elasticsearch elastic/elasticsearch \
-n "$NAMESPACE" \
-f elasticsearch-values.yaml \
--wait --timeout=10m
log_info "Elasticsearch deployed successfully"
}
# Create Jaeger configuration
create_jaeger_config() {
echo "[7/8] Creating Jaeger configuration..."
cat > jaeger-values.yaml << 'EOF'
provisionDataStore:
cassandra: false
elasticsearch: true
storage:
type: elasticsearch
elasticsearch:
host: elasticsearch-master
port: 9200
scheme: http
nodesWanOnly: false
extraEnv:
- name: ES_SERVER_URLS
value: http://elasticsearch-master:9200
cmdlineParams:
es.num-shards: 5
es.num-replicas: 1
es.index-prefix: jaeger
query:
enabled: true
replicaCount: 2
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 256m
memory: 256Mi
service:
type: ClusterIP
port: 16686
collector:
enabled: true
replicaCount: 2
resources:
limits:
cpu: 1
memory: 1Gi
requests:
cpu: 500m
memory: 512Mi
service:
type: ClusterIP
grpc:
port: 14250
http:
port: 14268
zipkin:
port: 9411
agent:
enabled: true
daemonset:
useHostPort: true
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 128m
memory: 128Mi
esIndexCleaner:
enabled: true
numberOfDays: 7
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 256m
memory: 256Mi
EOF
chmod 644 jaeger-values.yaml
log_info "Jaeger configuration created"
}
# Deploy Jaeger
deploy_jaeger() {
echo "[8/8] Deploying Jaeger..."
helm install jaeger jaegertracing/jaeger \
-n "$NAMESPACE" \
-f jaeger-values.yaml \
--wait --timeout=10m
log_info "Jaeger deployed successfully"
}
# Verify installation
verify_installation() {
echo "Verifying installation..."
log_info "Waiting for pods to be ready..."
kubectl wait --for=condition=ready pod -l app=elasticsearch-master -n "$NAMESPACE" --timeout=300s
kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=jaeger -n "$NAMESPACE" --timeout=300s
echo ""
log_info "Installation completed successfully!"
echo ""
echo "Jaeger components:"
kubectl get pods -n "$NAMESPACE" -l app.kubernetes.io/instance=jaeger
echo ""
echo "To access Jaeger UI, run:"
echo "kubectl port-forward -n $NAMESPACE svc/jaeger-query 16686:16686"
echo "Then visit: http://localhost:16686"
echo ""
echo "Jaeger collector endpoints:"
echo " gRPC: jaeger-collector.$NAMESPACE.svc.cluster.local:14250"
echo " HTTP: jaeger-collector.$NAMESPACE.svc.cluster.local:14268"
echo " Zipkin: jaeger-collector.$NAMESPACE.svc.cluster.local:9411"
}
# Main execution
main() {
if [[ "${1:-}" == "-h" ]] || [[ "${1:-}" == "--help" ]]; then
usage
fi
detect_distro
check_prerequisites
install_helm
add_helm_repos
create_namespace
create_elasticsearch_config
deploy_elasticsearch
create_jaeger_config
deploy_jaeger
verify_installation
# Cleanup temporary files
rm -f get_helm.sh
}
main "$@"
Review the script before running. Execute with: bash install.sh