Architecture
- Create cluster1 / cluster2 using kind
- Enable LoadBalancer support with cloud-provider-kind
- Configure Istio Ambient mode in multi-primary / multi-network setup
- Verify cross-cluster load balancing with sample applications
About This Guide
- Using Helm instead of istioctl: Designed for integration with GitOps tools like ArgoCD/FluxCD
- Configuration details: For detailed information about Istio Ambient mode multicluster configuration, refer to the official blog
- Working directory: Execute all commands from the root directory of the Istio repository
Prerequisites
- macOS + Docker Desktop
-
kubectl/kind/helm/cloud-provider-kindinstalled - Clone the Istio repository and work from its root directory:
git clone https://github.com/istio/istio.git
cd istio
# Execute all subsequent commands from this directory
Steps
1. Create Two kind Clusters
kind create cluster --name cluster1
kind create cluster --name cluster2
2. Install Gateway API CRDs
GATEWAY_API_VER=v1.4.1
kubectl apply --context kind-cluster1 --server-side --force-conflicts -f \
"https://github.com/kubernetes-sigs/gateway-api/releases/download/${GATEWAY_API_VER}/experimental-install.yaml"
kubectl apply --context kind-cluster2 --server-side --force-conflicts -f \
"https://github.com/kubernetes-sigs/gateway-api/releases/download/${GATEWAY_API_VER}/experimental-install.yaml"
3. Start cloud-provider-kind
Launch in a separate terminal:
sudo cloud-provider-kind
4. Configure the Same CA for Both Clusters
For mTLS to work in a multicluster setup, we need to use the same root CA:
kubectl create ns istio-system --context kind-cluster1
kubectl create ns istio-system --context kind-cluster2
# Use sample certificates from samples/certs/
kubectl create secret generic cacerts -n istio-system --context kind-cluster1 \
--from-file=samples/certs/ca-cert.pem \
--from-file=samples/certs/ca-key.pem \
--from-file=samples/certs/root-cert.pem \
--from-file=samples/certs/cert-chain.pem
kubectl create secret generic cacerts -n istio-system --context kind-cluster2 \
--from-file=samples/certs/ca-cert.pem \
--from-file=samples/certs/ca-key.pem \
--from-file=samples/certs/root-cert.pem \
--from-file=samples/certs/cert-chain.pem
5. Install Istio
cluster1:
helm upgrade --install istio-base manifests/charts/base \
-n istio-system --kube-context kind-cluster1
cat <<'EOF' | helm upgrade --install istiod manifests/charts/istio-control/istio-discovery \
-n istio-system --kube-context kind-cluster1 \
-f manifests/helm-profiles/ambient.yaml -f -
global:
meshID: mesh1
multiCluster:
clusterName: cluster1
network: network1
pilot:
env:
AMBIENT_ENABLE_MULTI_NETWORK: "true"
ENABLE_WILDCARD_HOST_SERVICE_ENTRIES_FOR_TLS: "true"
EOF
cat <<'EOF' | helm upgrade --install istio-cni manifests/charts/istio-cni \
-n istio-system --kube-context kind-cluster1 \
-f manifests/helm-profiles/ambient.yaml -f -
global:
meshID: mesh1
multiCluster:
clusterName: cluster1
network: network1
EOF
cat <<'EOF' | helm upgrade --install ztunnel manifests/charts/ztunnel \
-n istio-system --kube-context kind-cluster1 \
-f manifests/helm-profiles/ambient.yaml -f -
global:
meshID: mesh1
multiCluster:
clusterName: cluster1
network: network1
EOF
kubectl label ns istio-system --context kind-cluster1 topology.istio.io/network=network1 --overwrite
cluster2:
helm upgrade --install istio-base manifests/charts/base \
-n istio-system --kube-context kind-cluster2
cat <<'EOF' | helm upgrade --install istiod manifests/charts/istio-control/istio-discovery \
-n istio-system --kube-context kind-cluster2 \
-f manifests/helm-profiles/ambient.yaml -f -
global:
meshID: mesh1
multiCluster:
clusterName: cluster2
network: network2
pilot:
env:
AMBIENT_ENABLE_MULTI_NETWORK: "true"
ENABLE_WILDCARD_HOST_SERVICE_ENTRIES_FOR_TLS: "true"
EOF
cat <<'EOF' | helm upgrade --install istio-cni manifests/charts/istio-cni \
-n istio-system --kube-context kind-cluster2 \
-f manifests/helm-profiles/ambient.yaml -f -
global:
meshID: mesh1
multiCluster:
clusterName: cluster2
network: network2
EOF
cat <<'EOF' | helm upgrade --install ztunnel manifests/charts/ztunnel \
-n istio-system --kube-context kind-cluster2 \
-f manifests/helm-profiles/ambient.yaml -f -
global:
meshID: mesh1
multiCluster:
clusterName: cluster2
network: network2
EOF
kubectl label ns istio-system --context kind-cluster2 topology.istio.io/network=network2 --overwrite
6. Exchange Remote Secrets
# Handle differences between macOS and Linux base64 commands
b64dec() {
if base64 --help 2>&1 | grep -q -- '--decode'; then
base64 --decode
else
base64 -D
fi
}
# Get IP addresses of kind control-plane nodes
SERVER_CLUSTER1="https://$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' cluster1-control-plane):6443"
SERVER_CLUSTER2="https://$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' cluster2-control-plane):6443"
# Create service account token for cluster1
kubectl apply --context kind-cluster1 -f - <<'EOF'
apiVersion: v1
kind: Secret
metadata:
name: istio-reader-service-account-istio-remote-secret-token
namespace: istio-system
annotations:
kubernetes.io/service-account.name: istio-reader-service-account
type: kubernetes.io/service-account-token
EOF
# Wait for token generation
until kubectl get --context kind-cluster1 -n istio-system secret istio-reader-service-account-istio-remote-secret-token \
-o jsonpath='{.data.token}' 2>/dev/null | grep -q .; do
sleep 1
done
TOKEN_CLUSTER1="$(kubectl get --context kind-cluster1 -n istio-system secret istio-reader-service-account-istio-remote-secret-token \
-o jsonpath='{.data.token}' | b64dec)"
CA_B64_CLUSTER1="$(kubectl get --context kind-cluster1 -n istio-system secret istio-reader-service-account-istio-remote-secret-token \
-o jsonpath='{.data.ca\.crt}')"
# Create secret in cluster2 to access cluster1
kubectl apply --context kind-cluster2 -f - <<EOF
apiVersion: v1
kind: Secret
metadata:
name: istio-remote-secret-cluster1
namespace: istio-system
labels:
istio/multiCluster: "true"
annotations:
networking.istio.io/cluster: cluster1
stringData:
cluster1: |
apiVersion: v1
kind: Config
clusters:
- name: cluster1
cluster:
server: ${SERVER_CLUSTER1}
certificate-authority-data: ${CA_B64_CLUSTER1}
contexts:
- name: cluster1
context:
cluster: cluster1
user: cluster1
current-context: cluster1
users:
- name: cluster1
user:
token: ${TOKEN_CLUSTER1}
EOF
# Create service account token for cluster2
kubectl apply --context kind-cluster2 -f - <<'EOF'
apiVersion: v1
kind: Secret
metadata:
name: istio-reader-service-account-istio-remote-secret-token
namespace: istio-system
annotations:
kubernetes.io/service-account.name: istio-reader-service-account
type: kubernetes.io/service-account-token
EOF
# Wait for token generation
until kubectl get --context kind-cluster2 -n istio-system secret istio-reader-service-account-istio-remote-secret-token \
-o jsonpath='{.data.token}' 2>/dev/null | grep -q .; do
sleep 1
done
TOKEN_CLUSTER2="$(kubectl get --context kind-cluster2 -n istio-system secret istio-reader-service-account-istio-remote-secret-token \
-o jsonpath='{.data.token}' | b64dec)"
CA_B64_CLUSTER2="$(kubectl get --context kind-cluster2 -n istio-system secret istio-reader-service-account-istio-remote-secret-token \
-o jsonpath='{.data.ca\.crt}')"
# Create secret in cluster1 to access cluster2
kubectl apply --context kind-cluster1 --server-side -f - <<EOF
apiVersion: v1
kind: Secret
metadata:
name: istio-remote-secret-cluster2
namespace: istio-system
labels:
istio/multiCluster: "true"
annotations:
networking.istio.io/cluster: cluster2
stringData:
cluster2: |
apiVersion: v1
kind: Config
clusters:
- name: cluster2
cluster:
server: ${SERVER_CLUSTER2}
certificate-authority-data: ${CA_B64_CLUSTER2}
contexts:
- name: cluster2
context:
cluster: cluster2
user: cluster2
current-context: cluster2
users:
- name: cluster2
user:
token: ${TOKEN_CLUSTER2}
EOF
7. Create East-West Gateways
kubectl apply --context kind-cluster1 --server-side -f - <<'EOF'
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: istio-eastwestgateway
namespace: istio-system
labels:
topology.istio.io/network: "network1"
spec:
gatewayClassName: "istio-east-west"
listeners:
- name: mesh
port: 15008
protocol: HBONE
tls:
mode: Terminate
options:
gateway.istio.io/tls-terminate-mode: ISTIO_MUTUAL
EOF
kubectl apply --context kind-cluster2 --server-side -f - <<'EOF'
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: istio-eastwestgateway
namespace: istio-system
labels:
topology.istio.io/network: "network2"
spec:
gatewayClassName: "istio-east-west"
listeners:
- name: mesh
port: 15008
protocol: HBONE
tls:
mode: Terminate
options:
gateway.istio.io/tls-terminate-mode: ISTIO_MUTUAL
EOF
Wait for EXTERNAL-IP assignment:
for c in kind-cluster1 kind-cluster2; do
echo "=== waiting EXTERNAL-IP for $c"
until kubectl --context "$c" -n istio-system get svc istio-eastwestgateway \
-o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null | grep -Eq '[0-9]'; do
sleep 2
done
kubectl --context "$c" -n istio-system get svc istio-eastwestgateway
done
8. Verification
Create sample namespace and enable ambient mode:
kubectl create ns sample --context kind-cluster1
kubectl create ns sample --context kind-cluster2
kubectl label ns sample --context kind-cluster1 istio.io/dataplane-mode=ambient --overwrite
kubectl label ns sample --context kind-cluster2 istio.io/dataplane-mode=ambient --overwrite
Deploy helloworld (v1 to cluster1, v2 to cluster2):
# Create Service in both clusters
kubectl apply --context kind-cluster1 -n sample -f samples/helloworld/helloworld.yaml -l service=helloworld
kubectl apply --context kind-cluster2 -n sample -f samples/helloworld/helloworld.yaml -l service=helloworld
# Create Deployment with different versions in each cluster
kubectl apply --context kind-cluster1 -n sample -f samples/helloworld/helloworld.yaml -l version=v1
kubectl apply --context kind-cluster2 -n sample -f samples/helloworld/helloworld.yaml -l version=v2
# Mark as global service (enable cross-cluster communication)
kubectl label svc helloworld --context kind-cluster1 -n sample istio.io/global=true --overwrite
kubectl label svc helloworld --context kind-cluster2 -n sample istio.io/global=true --overwrite
Deploy curl:
kubectl apply --context kind-cluster1 -n sample -f samples/curl/curl.yaml
kubectl apply --context kind-cluster2 -n sample -f samples/curl/curl.yaml
Verify cross-cluster load balancing:
# Call helloworld from curl in cluster1
for i in {1..10}; do
kubectl exec --context kind-cluster1 -n sample deploy/curl -c curl -- \
curl -sS "helloworld.sample:5000/hello"
done
# Also verify from cluster2
for i in {1..10}; do
kubectl exec --context kind-cluster2 -n sample deploy/curl -c curl -- \
curl -sS "helloworld.sample:5000/hello"
done
Success if you see a mix of v1 and v2 responses.
Example:
$ for i in {1..10}; do
kubectl exec --context kind-cluster1 -n sample deploy/curl -c curl -- \
curl -sS "helloworld.sample:5000/hello"
done
Hello version: v1, instance: helloworld-v1-696f8879d6-w6g89
Hello version: v2, instance: helloworld-v2-59fc9f4558-g8r8b
Hello version: v1, instance: helloworld-v1-696f8879d6-w6g89
Hello version: v2, instance: helloworld-v2-59fc9f4558-g8r8b
Hello version: v1, instance: helloworld-v1-696f8879d6-w6g89
Hello version: v2, instance: helloworld-v2-59fc9f4558-g8r8b
Hello version: v1, instance: helloworld-v1-696f8879d6-w6g89
Hello version: v2, instance: helloworld-v2-59fc9f4558-g8r8b
Hello version: v2, instance: helloworld-v2-59fc9f4558-g8r8b
Hello version: v2, instance: helloworld-v2-59fc9f4558-g8r8b
Common Pitfalls
-
cloud-provider-kind not running: EXTERNAL-IP remains
<pending>and never gets assigned -
CA mismatch: Errors like
curl: (56) Recv failure: Connection reset by peeroccur -
Missing
istio.io/global=truelabel: Cross-cluster communication won't happen unless applied to Services in both clusters -
Running outside the Istio repository: Errors occur when
samples/ormanifests/directories are not found
Top comments (0)