Learn how to integrate Jaeger with Istio service mesh to get end-to-end distributed tracing across Kubernetes microservices, including sidecar injection, trace sampling, and ingress gateway access to the Jaeger UI.
Prerequisites
- A running Kubernetes cluster version 1.27 or later with at least 3 worker nodes
- kubectl configured with cluster-admin access
- At least 4 vCPU and 8GB RAM available for Istio and Jaeger components
- Helm 3 installed
- Basic familiarity with Kubernetes networking and Istio traffic management concepts
What this solves
When requests flow through dozens of microservices behind an Istio service mesh, pinpointing latency or failures without distributed tracing is nearly impossible. This tutorial integrates Jaeger with Istio so every Envoy sidecar automatically emits spans, giving you a full request timeline across services without touching application code.
You will install Istio with tracing enabled, deploy Jaeger as the tracing backend, configure sampling and telemetry, and verify trace propagation end to end through the ingress gateway.
Prerequisites and Kubernetes cluster preparation
Confirm cluster access and resources
You need a running Kubernetes cluster (1.27+) with at least 3 worker nodes, 4 vCPU and 8GB RAM free for Istio control plane, Jaeger, and demo workloads. Verify kubectl access before continuing.
kubectl version --short
kubectl get nodes
kubectl cluster-infoInstall prerequisite tools
You need curl, istioctl, and Helm to install Istio and Jaeger components.
sudo apt update && sudo apt install -y curl tar unzipsudo dnf install -y curl tar unzipDownload and install istioctl
The istioctl binary manages the Istio installation profile and lets you enable tracing at install time.
curl -L https://istio.io/downloadIstio | ISTIO_VERSION=1.23.2 sh -
cd istio-1.23.2
export PATH=$PWD/bin:$PATH
istioctl version --remote=falseInstall Helm
Helm is required to deploy Jaeger with the official chart in a repeatable way.
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bashcurl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bashInstalling Istio service mesh with tracing enabled
Create the istio-system namespace
All Istio control plane components and observability tools live in this dedicated namespace.
kubectl create namespace istio-systemInstall Istio with tracing configuration
The demo profile enables the ingress gateway and sets a default trace sampling percentage. Production deployments should use a custom IstioOperator manifest instead.
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
profile: default
meshConfig:
enableTracing: true
defaultConfig:
tracing:
sampling: 10.0
extensionProviders:
- name: jaeger
opentelemetry:
service: jaeger-collector.istio-system.svc.cluster.local
port: 4317
values:
global:
proxy:
tracer: "openelemetry"istioctl install -f istio-config.yaml -yEnable automatic sidecar injection
Label the namespace where your microservices run so Istio automatically injects the Envoy sidecar into every pod.
kubectl create namespace demo-app
kubectl label namespace demo-app istio-injection=enabledDeploying Jaeger and configuring the tracing backend
Add the Jaeger Helm repository
The official Jaegertracing chart deploys the collector, query service, and UI as separate components, which scales better than the all-in-one image.
helm repo add jaegertracing https://jaegertracing.github.io/helm-charts
helm repo updateDeploy Jaeger with production values
This example uses in-memory storage for a quick start. For a durable backend with retention and encryption, see the Elasticsearch backend guide linked below.
provisionDataStore:
cassandra: false
elasticsearch: false
storage:
type: memory
collector:
service:
otlp:
grpc:
name: otlp-grpc
port: 4317
http:
name: otlp-http
port: 4318
query:
service:
type: ClusterIPhelm install jaeger jaegertracing/jaeger \
-n istio-system \
-f jaeger-values.yamlConfirm Jaeger components are running
You should see collector, query, and agent pods (if enabled) in a Running state.
kubectl get pods -n istio-system -l app.kubernetes.io/name=jaegerConfiguring Istio telemetry and trace sampling rates
Create a Telemetry resource
Istio's Telemetry API controls sampling rate and trace propagation headers mesh-wide or per namespace. A 100 percent sample rate is useful for testing but too expensive for production traffic volumes.
apiVersion: telemetry.istio.io/v1alpha1
kind: Telemetry
metadata:
name: mesh-tracing
namespace: istio-system
spec:
tracing:
- providers:
- name: jaeger
randomSamplingPercentage: 10.00kubectl apply -f telemetry.yamlOverride sampling for a specific namespace
You can apply a higher sampling rate temporarily in a staging or debugging namespace without affecting production traffic elsewhere in the mesh.
apiVersion: telemetry.istio.io/v1alpha1
kind: Telemetry
metadata:
name: debug-tracing
namespace: demo-app
spec:
tracing:
- providers:
- name: jaeger
randomSamplingPercentage: 100.00kubectl apply -f telemetry-debug.yamlFor high-volume production environments, tune sampling with head-based and tail-based strategies as covered in setting up Jaeger sampling strategies for high-volume production tracing.
Enabling automatic sidecar trace propagation
Deploy a sample multi-service application
Istio's Envoy sidecars generate spans automatically, but your application code must forward the trace context headers on outbound calls, otherwise spans from different services will not link together.
kubectl apply -n demo-app -f https://raw.githubusercontent.com/istio/istio/release-1.23/samples/bookinfo/platform/kube/bookinfo.yamlVerify header propagation in application code
Each service in the call chain must propagate these headers from incoming requests to any outgoing requests it makes: x-request-id, x-b3-traceid, x-b3-spanid, x-b3-parentspanid, x-b3-sampled, x-b3-flags, and b3 or traceparent for W3C trace context.
kubectl logs -n demo-app deploy/productpage-v1 -c istio-proxy | grep -i traceExpose the sample app through the ingress gateway
This lets you generate real traffic through the mesh entry point to validate end-to-end tracing.
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: bookinfo-gateway
namespace: demo-app
spec:
selector:
istio: ingressgateway
servers:
- port:
number: 80
name: http
protocol: HTTP
hosts:
- "*"
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: bookinfo
namespace: demo-app
spec:
hosts:
- "*"
gateways:
- bookinfo-gateway
http:
- match:
- uri:
exact: /productpage
route:
- destination:
host: productpage
port:
number: 9080kubectl apply -f bookinfo-gateway.yamlVerifying distributed traces across microservices
Generate traffic
Send repeated requests through the ingress gateway to generate a trace chain across productpage, details, reviews, and ratings services.
export INGRESS_HOST=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
for i in $(seq 1 20); do curl -s -o /dev/null http://$INGRESS_HOST/productpage; donePort-forward the Jaeger query UI
Access Jaeger locally to confirm traces are arriving with all expected spans.
kubectl port-forward -n istio-system svc/jaeger-query 16686:16686Open http://localhost:16686, select the productpage.demo-app service, and search for recent traces. Each trace should show connected spans from productpage through details, reviews, and ratings.
Integrating Jaeger UI with Istio ingress gateway
Create a dedicated gateway for Jaeger
Exposing Jaeger through the Istio ingress gateway avoids running a separate load balancer just for observability tools.
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: jaeger-gateway
namespace: istio-system
spec:
selector:
istio: ingressgateway
servers:
- port:
number: 80
name: http-jaeger
protocol: HTTP
hosts:
- "jaeger.example.com"
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: jaeger-vs
namespace: istio-system
spec:
hosts:
- "jaeger.example.com"
gateways:
- jaeger-gateway
http:
- route:
- destination:
host: jaeger-query
port:
number: 16686kubectl apply -f jaeger-gateway.yamlPoint DNS and test access
Add an A record for jaeger.example.com pointing to the ingress gateway's external IP, then confirm the UI loads.
curl -s -o /dev/null -w "%{http_code}\n" -H "Host: jaeger.example.com" http://203.0.113.10/Verify your setup
kubectl get pods -n istio-system
kubectl get telemetry -A
istioctl proxy-config log deploy/productpage-v1.demo-app
kubectl exec -n demo-app deploy/productpage-v1 -c istio-proxy -- curl -s localhost:15000/stats | grep tracingA healthy setup shows all Istio and Jaeger pods Running, at least one Telemetry resource applied, and non-zero tracing counters on the sidecar's admin stats endpoint.
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Traces show single spans with no parent-child links | Application not propagating trace headers on outbound calls | Forward incoming x-request-id, x-b3-*, and traceparent headers on every outbound HTTP client call |
| No traces appear in Jaeger UI at all | Sampling percentage set too low or Telemetry resource not applied to the right namespace | Check kubectl get telemetry -A and temporarily raise randomSamplingPercentage to 100 for testing |
| Jaeger collector pod crashlooping | OTLP gRPC port misconfigured or storage backend unreachable | Check kubectl logs -n istio-system deploy/jaeger-collector and confirm storage.type matches your backend |
| Sidecar not injected into pods | Namespace missing the istio-injection label | Run kubectl label namespace demo-app istio-injection=enabled then restart the deployment |
| Jaeger UI returns 502 through ingress gateway | VirtualService pointing to wrong service name or port | Confirm the Jaeger query service name with kubectl get svc -n istio-system and match the port in the VirtualService |
| High trace volume overwhelming storage backend | Sampling rate too high for production traffic | Lower randomSamplingPercentage and use tail-based sampling for error and slow-request capture |
Next steps
- Implement Istio observability with Jaeger tracing and Kiali dashboard for Kubernetes service mesh
- Configure Jaeger distributed tracing on Kubernetes cluster with Helm charts and Elasticsearch backend
- Configure Istio distributed tracing with Jaeger and Zipkin for comprehensive microservices observability
- Configure advanced Jaeger sampling strategies for high-traffic environments
- Set up Istio multi-cluster service mesh with cross-cluster communication
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# ---------------------------------------------------------------------------
# Istio + Jaeger distributed tracing installer
# Supports: Ubuntu, Debian, AlmaLinux, Rocky Linux
# ---------------------------------------------------------------------------
ISTIO_VERSION="${1:-1.23.2}"
WORKDIR="$(pwd)/istio-jaeger-install"
LOG_FILE="/tmp/istio-jaeger-install.log"
# --- Colors ---
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
usage() {
echo "Usage: $0 [ISTIO_VERSION]"
echo " ISTIO_VERSION Optional. Default: 1.23.2"
echo "Example: $0 1.23.2"
exit 1
}
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
usage
fi
# --- Rollback on failure ---
cleanup_on_error() {
log_error "Installation failed. See $LOG_FILE for details."
log_warn "Attempting rollback of partially created resources..."
if command -v istioctl >/dev/null 2>&1; then
istioctl uninstall --purge -y >>"$LOG_FILE" 2>&1 || true
fi
if command -v helm >/dev/null 2>&1; then
helm uninstall jaeger -n istio-system >>"$LOG_FILE" 2>&1 || true
fi
if command -v kubectl >/dev/null 2>&1; then
kubectl delete namespace istio-system --ignore-not-found >>"$LOG_FILE" 2>&1 || true
fi
exit 1
}
trap cleanup_on_error ERR
# --- Root/sudo check ---
if [[ "$EUID" -ne 0 ]]; then
if ! command -v sudo >/dev/null 2>&1; then
log_error "This script must be run as root or with sudo available."
exit 1
fi
SUDO="sudo"
else
SUDO=""
fi
# --- Distro detection ---
echo "[1/10] Detecting operating system..."
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian) PKG_MGR="apt"; PKG_INSTALL="apt install -y" ;;
almalinux|rocky|centos|rhel|ol|fedora) PKG_MGR="dnf"; PKG_INSTALL="dnf install -y" ;;
amzn) PKG_MGR="yum"; PKG_INSTALL="yum install -y" ;;
*) log_error "Unsupported distro: $ID"; exit 1 ;;
esac
else
log_error "/etc/os-release not found. Cannot detect distro."
exit 1
fi
log_info "Detected distro: $ID ($PKG_MGR)"
# --- Prerequisite checks ---
echo "[2/10] Checking prerequisite tools..."
if ! command -v kubectl >/dev/null 2>&1; then
log_error "kubectl not found. Please install kubectl and configure cluster access first."
exit 1
fi
log_info "Verifying cluster access..."
if ! kubectl cluster-info >>"$LOG_FILE" 2>&1; then
log_error "Cannot reach Kubernetes cluster. Check your kubeconfig."
exit 1
fi
NODE_COUNT=$(kubectl get nodes --no-headers 2>>"$LOG_FILE" | wc -l)
if [ "$NODE_COUNT" -lt 1 ]; then
log_error "No worker nodes found in cluster."
exit 1
fi
log_info "Cluster reachable with $NODE_COUNT node(s)."
# --- Install OS packages ---
echo "[3/10] Installing required packages (curl, tar, unzip)..."
if [ "$PKG_MGR" = "apt" ]; then
$SUDO apt update >>"$LOG_FILE" 2>&1
$SUDO $PKG_INSTALL curl tar unzip >>"$LOG_FILE" 2>&1
else
$SUDO $PKG_INSTALL curl tar unzip >>"$LOG_FILE" 2>&1
fi
log_info "Base packages installed."
# --- Prepare working directory ---
echo "[4/10] Preparing working directory..."
mkdir -p "$WORKDIR"
chmod 755 "$WORKDIR"
cd "$WORKDIR"
# --- Install istioctl ---
echo "[5/10] Downloading and installing istioctl ${ISTIO_VERSION}..."
if [ ! -d "istio-${ISTIO_VERSION}" ]; then
curl -sL "https://istio.io/downloadIstio" | ISTIO_VERSION="${ISTIO_VERSION}" sh - >>"$LOG_FILE" 2>&1
fi
export PATH="${WORKDIR}/istio-${ISTIO_VERSION}/bin:$PATH"
if ! command -v istioctl >/dev/null 2>&1; then
log_error "istioctl installation failed."
exit 1
fi
log_info "istioctl installed: $(istioctl version --remote=false 2>>"$LOG_FILE")"
# --- Install Helm ---
echo "[6/10] Installing Helm (if not already present)..."
if ! command -v helm >/dev/null 2>&1; then
curl -sL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 -o get-helm-3.sh
chmod 755 get-helm-3.sh
$SUDO ./get-helm-3.sh >>"$LOG_FILE" 2>&1
fi
log_info "Helm version: $(helm version --short 2>>"$LOG_FILE")"
# --- Create istio-system namespace ---
echo "[7/10] Creating istio-system namespace..."
kubectl get namespace istio-system >/dev/null 2>&1 || kubectl create namespace istio-system
# --- Install Istio with tracing enabled ---
echo "[8/10] Installing Istio control plane with tracing enabled..."
cat > "${WORKDIR}/istio-config.yaml" <<'EOF'
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
profile: default
meshConfig:
enableTracing: true
defaultConfig:
tracing:
sampling: 10.0
extensionProviders:
- name: jaeger
opentelemetry:
service: jaeger-collector.istio-system.svc.cluster.local
port: 4317
values:
global:
proxy:
tracer: "opentelemetry"
EOF
chmod 644 "${WORKDIR}/istio-config.yaml"
istioctl install -f "${WORKDIR}/istio-config.yaml" -y >>"$LOG_FILE" 2>&1
log_info "Istio installed with tracing enabled."
# --- Enable sidecar injection on demo namespace ---
echo "[9/10] Creating demo-app namespace with sidecar injection enabled..."
kubectl get namespace demo-app >/dev/null 2>&1 || kubectl create namespace demo-app
kubectl label namespace demo-app istio-injection=enabled --overwrite >>"$LOG_FILE" 2>&1
# --- Deploy Jaeger via Helm ---
echo "[10/10] Deploying Jaeger via Helm..."
helm repo add jaegertracing https://jaegertracing.github.io/helm-charts >>"$LOG_FILE" 2>&1
helm repo update >>"$LOG_FILE" 2>&1
cat > "${WORKDIR}/jaeger-values.yaml" <<'EOF'
provisionDataStore:
cassandra: false
elasticsearch: false
storage:
type: memory
collector:
service:
otlp:
grpc:
name: otlp-grpc
port: 4317
http:
name: otlp-http
port: 4318
query:
service:
type: ClusterIP
EOF
chmod 644 "${WORKDIR}/jaeger-values.yaml"
if helm status jaeger -n istio-system >/dev/null 2>&1; then
log_warn "Jaeger release already exists, upgrading..."
helm upgrade jaeger jaegertracing/jaeger -n istio-system -f "${WORKDIR}/jaeger-values.yaml" >>"$LOG_FILE" 2>&1
else
helm install jaeger jaegertracing/jaeger -n istio-system -f "${WORKDIR}/jaeger-values.yaml" >>"$LOG_FILE" 2>&1
fi
# --- Verification ---
echo "Waiting for Jaeger pods to become ready..."
if ! kubectl wait --for=condition=Ready pod -l app.kubernetes.io/name=jaeger -n istio-system --timeout=180s >>"$LOG_FILE" 2>&1; then
log_error "Jaeger pods did not become ready in time."
exit 1
fi
echo "Verifying Istio control plane pods..."
if ! kubectl get pods -n istio-system -l app=istiod --no-headers | grep -q Running; then
log_error "Istiod pod is not running."
exit 1
fi
log_info "Istio control plane pods:"
kubectl get pods -n istio-system
log_info "Jaeger components:"
kubectl get pods -n istio-system -l app.kubernetes.io/name=jaeger
log_info "Installation complete."
log_info "Deploy your microservices into the 'demo-app' namespace to get automatic sidecar injection and tracing."
log_info "Full log available at: $LOG_FILE"
trap - ERR
exit 0
Review the script before running. Execute with: bash install.sh