Deploy and configure Istio across multiple Kubernetes clusters with secure cross-cluster communication, shared service discovery, and unified traffic management for distributed microservices architecture.
Prerequisites
- Two or more Kubernetes clusters with admin access
- kubectl configured with multiple cluster contexts
- LoadBalancer support or NodePort access
- Network connectivity between clusters
- Basic understanding of Kubernetes networking and service mesh concepts
What this solves
This tutorial shows you how to set up Istio service mesh across multiple Kubernetes clusters with cross-cluster communication. You need this when your microservices are distributed across different clusters for high availability, geographic distribution, or organizational boundaries while maintaining unified service discovery and security policies.
Step-by-step configuration
Install required tools and dependencies
Install kubectl, istioctl, and Helm on your management machine to interact with multiple clusters.
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
curl -sL https://istio.io/downloadIstio | sh -
sudo mv istio-*/bin/istioctl /usr/local/bin/
curl https://baltocdn.com/helm/signing.asc | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg > /dev/null
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 update && sudo apt install -y helm
Set up cluster contexts and network configuration
Configure kubectl contexts for both clusters and set unique cluster names and network identifiers.
export CTX_CLUSTER1=cluster1
export CTX_CLUSTER2=cluster2
Verify cluster access
kubectl --context="${CTX_CLUSTER1}" get nodes
kubectl --context="${CTX_CLUSTER2}" get nodes
Label clusters with unique network identifiers
kubectl --context="${CTX_CLUSTER1}" label namespace istio-system topology.istio.io/network=network1
kubectl --context="${CTX_CLUSTER2}" label namespace istio-system topology.istio.io/network=network2
Create Istio namespace and certificates
Create the istio-system namespace and generate shared root certificates for secure cross-cluster communication.
kubectl --context="${CTX_CLUSTER1}" create namespace istio-system
kubectl --context="${CTX_CLUSTER2}" create namespace istio-system
Generate root CA certificates
mkdir -p certs
cd certs
Create root CA
openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 \
-subj '/O=example Inc./CN=example.com' \
-keyout root-key.pem -out root-cert.pem
Create intermediate CA for cluster1
openssl req -newkey rsa:2048 -nodes -keyout cluster1-key.pem \
-subj '/O=example Inc./CN=cluster1.example.com' \
-out cluster1.csr
openssl x509 -req -days 365 -CA root-cert.pem -CAkey root-key.pem \
-set_serial 0 -in cluster1.csr -out cluster1-cert.pem
Create intermediate CA for cluster2
openssl req -newkey rsa:2048 -nodes -keyout cluster2-key.pem \
-subj '/O=example Inc./CN=cluster2.example.com' \
-out cluster2.csr
openssl x509 -req -days 365 -CA root-cert.pem -CAkey root-key.pem \
-set_serial 1 -in cluster2.csr -out cluster2-cert.pem
Install certificates in both clusters
Deploy the CA certificates to each cluster for mutual TLS authentication between clusters.
# Install certificates in cluster1
kubectl --context="${CTX_CLUSTER1}" create secret generic cacerts -n istio-system \
--from-file=root-cert.pem \
--from-file=cert-chain.pem=cluster1-cert.pem \
--from-file=ca-cert.pem=cluster1-cert.pem \
--from-file=ca-key.pem=cluster1-key.pem
Install certificates in cluster2
kubectl --context="${CTX_CLUSTER2}" create secret generic cacerts -n istio-system \
--from-file=root-cert.pem \
--from-file=cert-chain.pem=cluster2-cert.pem \
--from-file=ca-cert.pem=cluster2-cert.pem \
--from-file=ca-key.pem=cluster2-key.pem
cd ..
Configure Istio installation values
Create configuration files for both clusters with specific values for multi-cluster setup.
values:
pilot:
env:
EXTERNAL_ISTIOD: false
global:
meshID: mesh1
clusterName: cluster1
network: network1
remotePilotAddress: ""
values:
pilot:
env:
EXTERNAL_ISTIOD: false
global:
meshID: mesh1
clusterName: cluster2
network: network2
remotePilotAddress: ""
Install Istio control plane on primary cluster
Install Istio on the first cluster as the primary control plane with cross-cluster discovery enabled.
istioctl install --context="${CTX_CLUSTER1}" --values=cluster1-values.yaml -f - <
Create eastwest gateway for cross-cluster communication
Deploy the eastwest gateway to handle inter-cluster traffic and service discovery.
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
metadata:
name: eastwest
spec:
revision: ""
components:
ingressGateways:
- name: istio-eastwestgateway
label:
istio: eastwestgateway
app: istio-eastwestgateway
enabled: true
k8s:
service:
type: LoadBalancer
ports:
- port: 15021
targetPort: 15021
name: status-port
- port: 15010
targetPort: 15010
name: tls
- port: 15011
targetPort: 15011
name: tls
- port: 15012
targetPort: 15012
name: tls
istioctl --context="${CTX_CLUSTER1}" install -f eastwest-gateway.yaml
Expose Istio control plane through eastwest gateway
Create Gateway and VirtualService resources to expose the control plane for cross-cluster access.
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
name: istiod-gateway
namespace: istio-system
spec:
selector:
istio: eastwestgateway
servers:
- port:
number: 15010
name: tls
protocol: TLS
tls:
mode: PASSTHROUGH
hosts:
- "*"
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: istiod-vs
namespace: istio-system
spec:
hosts:
- "*"
gateways:
- istiod-gateway
tls:
- match:
- port: 15010
sniHosts:
- "*"
route:
- destination:
host: istiod.istio-system.svc.cluster.local
port:
number: 15010
kubectl --context="${CTX_CLUSTER1}" apply -f expose-istiod.yaml
Get eastwest gateway external IP
Retrieve the external IP address of the eastwest gateway for the secondary cluster configuration.
export DISCOVERY_ADDRESS=$(kubectl --context="${CTX_CLUSTER1}" get svc istio-eastwestgateway \
-n istio-system -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
echo "Discovery address: $DISCOVERY_ADDRESS"
If using a cloud provider that returns hostname instead of IP
if [ -z "$DISCOVERY_ADDRESS" ]; then
export DISCOVERY_ADDRESS=$(kubectl --context="${CTX_CLUSTER1}" get svc istio-eastwestgateway \
-n istio-system -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')
fi
Install Istio on secondary cluster
Install Istio on the second cluster configured to connect to the primary cluster's control plane.
istioctl install --context="${CTX_CLUSTER2}" -f - <
Create cluster secrets for cross-cluster discovery
Create secrets containing kubeconfig information for cross-cluster service discovery.
# Create secret for cluster2 in cluster1
kubectl --context="${CTX_CLUSTER1}" create secret generic cluster2-secret \
--from-file=cluster2=/path/to/cluster2/kubeconfig -n istio-system
kubectl --context="${CTX_CLUSTER1}" label secret cluster2-secret istio/cluster=cluster2 -n istio-system
kubectl --context="${CTX_CLUSTER1}" annotate secret cluster2-secret networking.istio.io/cluster=cluster2 -n istio-system
Create secret for cluster1 in cluster2
kubectl --context="${CTX_CLUSTER2}" create secret generic cluster1-secret \
--from-file=cluster1=/path/to/cluster1/kubeconfig -n istio-system
kubectl --context="${CTX_CLUSTER2}" label secret cluster1-secret istio/cluster=cluster1 -n istio-system
kubectl --context="${CTX_CLUSTER2}" annotate secret cluster1-secret networking.istio.io/cluster=cluster1 -n istio-system
Enable sidecar injection
Label namespaces in both clusters to enable automatic Istio sidecar injection.
kubectl --context="${CTX_CLUSTER1}" create namespace sample
kubectl --context="${CTX_CLUSTER1}" label namespace sample istio-injection=enabled
kubectl --context="${CTX_CLUSTER2}" create namespace sample
kubectl --context="${CTX_CLUSTER2}" label namespace sample istio-injection=enabled
Deploy test applications
Deploy sample applications in both clusters to test cross-cluster communication.
apiVersion: apps/v1
kind: Deployment
metadata:
name: httpbin
namespace: sample
spec:
replicas: 1
selector:
matchLabels:
app: httpbin
version: v1
template:
metadata:
labels:
app: httpbin
version: v1
spec:
containers:
- image: docker.io/kennethreitz/httpbin
imagePullPolicy: IfNotPresent
name: httpbin
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: httpbin
namespace: sample
labels:
app: httpbin
spec:
ports:
- name: http
port: 8000
targetPort: 80
selector:
app: httpbin
kubectl --context="${CTX_CLUSTER1}" apply -f app-cluster1.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: sleep
namespace: sample
spec:
replicas: 1
selector:
matchLabels:
app: sleep
template:
metadata:
labels:
app: sleep
spec:
containers:
- name: sleep
image: curlimages/curl
command: ["/bin/sleep", "infinity"]
imagePullPolicy: IfNotPresent
kubectl --context="${CTX_CLUSTER2}" apply -f app-cluster2.yaml
Configure cross-cluster service discovery
Create ServiceEntry resources to enable service discovery across clusters.
apiVersion: networking.istio.io/v1beta1
kind: ServiceEntry
metadata:
name: httpbin-cross-cluster
namespace: sample
spec:
hosts:
- httpbin.sample.global
location: MESH_EXTERNAL
ports:
- number: 8000
name: http
protocol: HTTP
resolution: DNS
addresses:
- 240.0.0.1
endpoints:
- address: httpbin.sample.svc.cluster.local
network: network1
kubectl --context="${CTX_CLUSTER2}" apply -f cross-cluster-service.yaml
Create traffic management policies
Configure DestinationRule and VirtualService for cross-cluster traffic management.
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: httpbin-destination
namespace: sample
spec:
host: httpbin.sample.global
trafficPolicy:
tls:
mode: ISTIO_MUTUAL
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: httpbin-virtual-service
namespace: sample
spec:
hosts:
- httpbin.sample.global
http:
- match:
- headers:
end-user:
exact: jason
route:
- destination:
host: httpbin.sample.global
subset: v1
- route:
- destination:
host: httpbin.sample.global
kubectl --context="${CTX_CLUSTER2}" apply -f traffic-policy.yaml
Verify your setup
# Check Istio installation status in both clusters
istioctl --context="${CTX_CLUSTER1}" proxy-status
istioctl --context="${CTX_CLUSTER2}" proxy-status
Verify cross-cluster connectivity
kubectl --context="${CTX_CLUSTER2}" exec -n sample -c sleep \
$(kubectl --context="${CTX_CLUSTER2}" get pod -n sample -l app=sleep -o jsonpath='{.items[0].metadata.name}') \
-- curl -v httpbin.sample.global:8000/headers
Check service endpoints
istioctl --context="${CTX_CLUSTER1}" proxy-config endpoints \
$(kubectl --context="${CTX_CLUSTER1}" get pod -n istio-system -l app=istiod -o jsonpath='{.items[0].metadata.name}') \
-n istio-system
Verify mTLS configuration
istioctl --context="${CTX_CLUSTER1}" authn tls-check httpbin.sample.svc.cluster.local
istioctl --context="${CTX_CLUSTER2}" authn tls-check sleep.sample.svc.cluster.local
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Cross-cluster services not discoverable | Missing cluster secrets or incorrect network labels | Verify cluster secrets with kubectl get secrets -n istio-system and check network labels |
| mTLS connection failures | Certificate mismatch or missing CA certs | Recreate CA certificates and ensure they match across clusters |
| Eastwest gateway not accessible | LoadBalancer service not getting external IP | Check cloud provider configuration or use NodePort service type |
| Pilot not starting on secondary cluster | Incorrect remotePilotAddress configuration | Verify DISCOVERY_ADDRESS variable and eastwest gateway service |
| Services showing as unhealthy | Network connectivity issues between clusters | Test network connectivity between cluster nodes and check firewall rules |
Next steps
- Implement Istio security policies with authorization and authentication
- Configure Istio traffic management with virtual services and destination rules
- Monitor Istio service mesh with Prometheus and Grafana dashboards
- Configure Istio multi-cluster disaster recovery and failover
- Implement Istio multi-cluster canary deployments and traffic splitting
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Default values
CTX_CLUSTER1="${1:-cluster1}"
CTX_CLUSTER2="${2:-cluster2}"
DOMAIN="${3:-example.com}"
ORG="${4:-example Inc.}"
usage() {
echo "Usage: $0 [cluster1_context] [cluster2_context] [domain] [organization]"
echo "Example: $0 prod-east prod-west mycompany.com 'MyCompany Inc.'"
exit 1
}
log() {
echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1"
}
warn() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
error() {
echo -e "${RED}[ERROR]${NC} $1" >&2
exit 1
}
cleanup() {
if [[ -d "certs" ]]; then
rm -rf certs
fi
if [[ -f "get_helm.sh" ]]; then
rm -f get_helm.sh
fi
}
trap cleanup ERR
check_prerequisites() {
log "[1/8] Checking prerequisites..."
if [[ $EUID -eq 0 ]]; then
error "This script should not be run as root. Use sudo for individual commands."
fi
if ! command -v sudo &> /dev/null; then
error "sudo is required but not installed"
fi
if ! sudo -n true 2>/dev/null; then
error "This script requires passwordless sudo access"
fi
}
detect_distro() {
log "[2/8] Detecting distribution..."
if [[ ! -f /etc/os-release ]]; then
error "Cannot detect distribution - /etc/os-release not found"
fi
source /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_INSTALL="apt install -y"
PKG_UPDATE="apt update"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
PKG_UPDATE="dnf update -y"
if ! command -v dnf &> /dev/null; then
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
PKG_UPDATE="yum update -y"
fi
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
PKG_UPDATE="yum update -y"
;;
*)
error "Unsupported distribution: $ID"
;;
esac
log "Detected: $PRETTY_NAME using $PKG_MGR"
}
install_dependencies() {
log "[3/8] Installing system dependencies..."
sudo $PKG_UPDATE
case "$PKG_MGR" in
apt)
sudo $PKG_INSTALL curl wget gnupg2 software-properties-common apt-transport-https ca-certificates openssl
;;
dnf|yum)
sudo $PKG_INSTALL curl wget gnupg2 openssl which
;;
esac
}
install_kubectl() {
log "[4/8] Installing kubectl..."
if command -v kubectl &> /dev/null; then
warn "kubectl already installed, skipping..."
return
fi
KUBECTL_VERSION=$(curl -L -s https://dl.k8s.io/release/stable.txt)
curl -LO "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl"
chmod 755 kubectl
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
rm kubectl
kubectl version --client &> /dev/null || error "kubectl installation failed"
}
install_istioctl() {
log "[5/8] Installing istioctl..."
if command -v istioctl &> /dev/null; then
warn "istioctl already installed, skipping..."
return
fi
curl -sL https://istio.io/downloadIstio | sh -
ISTIO_DIR=$(find . -name "istio-*" -type d | head -1)
if [[ -z "$ISTIO_DIR" ]]; then
error "Failed to find Istio directory"
fi
sudo install -o root -g root -m 0755 "${ISTIO_DIR}/bin/istioctl" /usr/local/bin/istioctl
rm -rf istio-*
istioctl version --remote=false &> /dev/null || error "istioctl installation failed"
}
install_helm() {
log "[6/8] Installing Helm..."
if command -v helm &> /dev/null; then
warn "Helm already installed, skipping..."
return
fi
case "$PKG_MGR" in
apt)
curl https://baltocdn.com/helm/signing.asc | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg > /dev/null
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 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 700 get_helm.sh
./get_helm.sh
rm -f get_helm.sh
;;
esac
helm version &> /dev/null || error "Helm installation failed"
}
setup_cluster_contexts() {
log "[7/8] Setting up cluster contexts..."
export CTX_CLUSTER1
export CTX_CLUSTER2
log "Verifying cluster access for $CTX_CLUSTER1..."
if ! kubectl --context="${CTX_CLUSTER1}" get nodes &> /dev/null; then
error "Cannot access cluster context: $CTX_CLUSTER1"
fi
log "Verifying cluster access for $CTX_CLUSTER2..."
if ! kubectl --context="${CTX_CLUSTER2}" get nodes &> /dev/null; then
error "Cannot access cluster context: $CTX_CLUSTER2"
fi
log "Creating istio-system namespaces..."
kubectl --context="${CTX_CLUSTER1}" create namespace istio-system --dry-run=client -o yaml | kubectl --context="${CTX_CLUSTER1}" apply -f -
kubectl --context="${CTX_CLUSTER2}" create namespace istio-system --dry-run=client -o yaml | kubectl --context="${CTX_CLUSTER2}" apply -f -
log "Labeling clusters with network identifiers..."
kubectl --context="${CTX_CLUSTER1}" label namespace istio-system topology.istio.io/network=network1 --overwrite
kubectl --context="${CTX_CLUSTER2}" label namespace istio-system topology.istio.io/network=network2 --overwrite
}
generate_certificates() {
log "[8/8] Generating certificates..."
mkdir -p certs
cd certs
log "Creating root CA certificate..."
openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 \
-subj "/O=${ORG}/CN=${DOMAIN}" \
-keyout root-key.pem -out root-cert.pem
log "Creating intermediate CA for cluster1..."
openssl req -newkey rsa:2048 -nodes -keyout cluster1-key.pem \
-subj "/O=${ORG}/CN=cluster1.${DOMAIN}" \
-out cluster1.csr
openssl x509 -req -days 365 -CA root-cert.pem -CAkey root-key.pem \
-set_serial 0 -in cluster1.csr -out cluster1-cert.pem
log "Creating intermediate CA for cluster2..."
openssl req -newkey rsa:2048 -nodes -keyout cluster2-key.pem \
-subj "/O=${ORG}/CN=cluster2.${DOMAIN}" \
-out cluster2.csr
openssl x509 -req -days 365 -CA root-cert.pem -CAkey root-key.pem \
-set_serial 1 -in cluster2.csr -out cluster2-cert.pem
chmod 600 *.pem *.key
log "Installing certificates in cluster1..."
kubectl --context="${CTX_CLUSTER1}" create secret generic cacerts -n istio-system \
--from-file=root-cert.pem \
--from-file=cert-chain.pem=cluster1-cert.pem \
--from-file=ca-cert.pem=cluster1-cert.pem \
--from-file=ca-key.pem=cluster1-key.pem \
--dry-run=client -o yaml | kubectl --context="${CTX_CLUSTER1}" apply -f -
log "Installing certificates in cluster2..."
kubectl --context="${CTX_CLUSTER2}" create secret generic cacerts -n istio-system \
--from-file=root-cert.pem \
--from-file=cert-chain.pem=cluster2-cert.pem \
--from-file=ca-cert.pem=cluster2-cert.pem \
--from-file=ca-key.pem=cluster2-key.pem \
--dry-run=client -o yaml | kubectl --context="${CTX_CLUSTER2}" apply -f -
cd ..
}
verify_installation() {
log "Verifying installation..."
echo -e "${BLUE}Installed tools:${NC}"
kubectl version --client --short 2>/dev/null || echo "kubectl: installed"
istioctl version --remote=false 2>/dev/null | head -1 || echo "istioctl: installed"
helm version --short 2>/dev/null || echo "helm: installed"
echo -e "${BLUE}Cluster contexts:${NC}"
echo "Cluster 1: $CTX_CLUSTER1"
echo "Cluster 2: $CTX_CLUSTER2"
echo -e "${BLUE}Certificates:${NC}"
if [[ -f "certs/root-cert.pem" ]]; then
echo "✓ Root CA certificate generated"
fi
echo -e "${BLUE}Next steps:${NC}"
echo "1. Install Istio control plane: istioctl install --context=\$CTX_CLUSTER1 --set values.pilot.env.EXTERNAL_ISTIOD=true"
echo "2. Install Istio control plane: istioctl install --context=\$CTX_CLUSTER2 --set values.pilot.env.EXTERNAL_ISTIOD=true"
echo "3. Configure cross-network endpoint discovery"
echo "4. Set up cross-cluster service exposure"
}
main() {
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
usage
fi
check_prerequisites
detect_distro
install_dependencies
install_kubectl
install_istioctl
install_helm
setup_cluster_contexts
generate_certificates
verify_installation
log "Istio multi-cluster setup completed successfully!"
}
main "$@"
Review the script before running. Execute with: bash install.sh