構成
- kind で cluster1 / cluster2 を作成
- cloud-provider-kind で LoadBalancer を使えるようにする
- Istio Ambient mode で multi-primary 構成
- サンプルアプリでクロスクラスターロードバランスを確認
このガイドについて
- istioctl ではなく Helm を使用: ArgoCD/FluxCD 等の GitOps ツールとの統合を想定しているため
- 構成の詳細: Istio Ambient モードのマルチクラスター構成の詳細は 公式ブログ と Ambient Mesh ドキュメント を参照
- 作業場所: Istio リポジトリのルートディレクトリで実行
前提
-
macOS + Docker Desktop
-
kubectl/kind/helm/cloud-provider-kindがインストール済み -
Istio リポジトリをクローンし、そのルートディレクトリで作業:
git clone https://github.com/istio/istio.git cd istio # 以降、全てこのディレクトリで実行
手順
1. kind クラスタを2つ作る
kind create cluster --name cluster1
kind create cluster --name cluster2
2. Gateway API CRD をインストール
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. cloud-provider-kind を起動
別ターミナルで起動しておく:
sudo cloud-provider-kind
4. 両クラスタで同じ CA を使う準備
マルチクラスターでは mTLS が成立する必要があるため、同じルート CA を使用します:
kubectl create ns istio-system --context kind-cluster1
kubectl create ns istio-system --context kind-cluster2
# 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. 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 -
meshConfig:
# trustDomain は SPIFFE ID(例: spiffe://<trustDomain>/ns/<ns>/sa/<sa>)の先頭になります。
#
# 重要: ambient multicluster (alpha) では east-west gateway との mTLS で
# 「期待する gateway の SPIFFE SAN」と「実際の gateway 証明書の SPIFFE SAN」がズレると、
# ztunnel が `invalid peer certificate ... expected SAN ... got ...` を出して通信が RST されます。
#
# そのため、現時点では **全クラスタで同じ trustDomain** を使う必要があります。
# 対応Issue: https://github.com/istio/istio/issues/58427
#
# trustDomain: "cluster1.local" # 将来メモ(#58427 系が解消したら)コメントアウトを解除する
trustDomain: "cluster.local"
global:
# meshID はメッシュ識別子。マルチクラスタで「同一メッシュ」として束ねるなら **全クラスタ同じ値**にする。
# 未設定の場合は trustDomain が使われる挙動もあるため、明示するかは運用ポリシー次第。
meshID: mesh1
multiCluster:
# clusterName は **クラスタ識別子**(衝突不可)。
# 重要: istiod/ztunnel でこの値がズレると XDS 認証が失敗し、ztunnel が `Unauthenticated` を出して
# マルチクラスタのエンドポイント配信(結果としてクロスクラスタLB)まで崩れます。
clusterName: cluster1
# network は「ネットワーク境界」の識別子(Split Horizon EDS / east-west gateway 経由の判定に使う)。
# - クラスタ間で Pod IP が直結到達できないならクラスタ間で **分ける** 必要があり、その場合だとgateway経由となる。
# - Amazon VPC CNI みたいにPod間が直結で到達できるなら同じ値だとpod to podになり最適化される。
network: network1
env:
# Enables assigning multi-cluster services an IP address
# マルチクラスタサービス(例: istio.io/global=true を付けた Service)を VIP 化して、
# DNS キャプチャ経由で L4/TCP 含むルーティングを成立させるための前提。
PILOT_ENABLE_IP_AUTOALLOCATE: "true"
# Required for east-west gateway / multi-network (ambient multicluster)
# これを有効にすると、`GatewayClass=istio-east-west`(east-west gateway)等の
# multi-network 用コントローラが動き、`topology.istio.io/network` を前提に
# 異ネットワーク宛の到達経路を組み立てられるようになります。
AMBIENT_ENABLE_MULTI_NETWORK: "true"
# trustDomain をクラスタごとに分ける場合、異なる trustDomain の通信を許可するために必要。
# ただし、これを有効化しても ztunnel の証明書 SAN 検証(`expected SAN ... got ...`)は **現時点では istio/istio#58427 の影響により解決しません**。
# 本チュートリアルは trustDomain を揃えるため不要ですが、将来分ける為残しておきます。
PILOT_SKIP_VALIDATE_TRUST_DOMAIN: "true"
platforms:
peering:
# ambient のマルチクラスタ(peer でのクラスタ間連携)を有効化する。
enabled: 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:
# nftables モード(Ambient の iptables ルールを nft に置き換える)
# 備考: istioctl/Operator での "--set values.global.nativeNftables=true" に相当。
# Helm でチャートを直接インストールする場合は "values." プレフィックスは付けずに指定する。
nativeNftables: true
# マルチクラスターサービスへの IP アドレス割り当てを有効化
ambient:
dnsCapture: true
EOF
cat <<'EOF' | helm upgrade --install ztunnel manifests/charts/ztunnel \
-n istio-system --kube-context kind-cluster1 \
-f manifests/helm-profiles/ambient.yaml -f -
multiCluster:
# ztunnel 側のクラスタ識別子。
# istiod が認識する clusterName(global.multiCluster.clusterName)と **必ず一致**させること。
clusterName: cluster1
env:
# trustDomain をクラスタごとに分ける場合、異なる trustDomain の通信を許可するために必要。
# ただし、これを有効化しても ztunnel の証明書 SAN 検証(`expected SAN ... got ...`)は **現時点では istio/istio#58427 の影響により解決しません**。
# 本チュートリアルは trustDomain を揃えるため不要ですが、将来分ける為残しておきます。
SKIP_VALIDATE_TRUST_DOMAIN: "true"
network: network1
EOF
# istiod が「このクラスタのデフォルト network」を解釈するためのラベル。
# Split Horizon(同一 network 優先/異 network は gateway 経由)や east-west gateway の生成に影響する。
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 -
meshConfig:
trustDomain: "cluster.local"
global:
meshID: mesh1
multiCluster:
clusterName: cluster2
network: network2
env:
PILOT_ENABLE_IP_AUTOALLOCATE: "true"
AMBIENT_ENABLE_MULTI_NETWORK: "true"
PILOT_SKIP_VALIDATE_TRUST_DOMAIN: "true"
platforms:
peering:
enabled: 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:
# nftables モード(Ambient の iptables ルールを nft に置き換える)
# 備考: istioctl/Operator での "--set values.global.nativeNftables=true" に相当。
# Helm でチャートを直接インストールする場合は "values." プレフィックスは付けずに指定する。
nativeNftables: true
# マルチクラスターサービスへの IP アドレス割り当てを有効化
ambient:
dnsCapture: true
EOF
cat <<'EOF' | helm upgrade --install ztunnel manifests/charts/ztunnel \
-n istio-system --kube-context kind-cluster2 \
-f manifests/helm-profiles/ambient.yaml -f -
multiCluster:
clusterName: cluster2
env:
SKIP_VALIDATE_TRUST_DOMAIN: "true"
network: network2
EOF
# cluster1 側と同様、デフォルト network ラベルを設定
kubectl label ns istio-system --context kind-cluster2 topology.istio.io/network=network2 --overwrite
6. リモートシークレットを交換
# macOS と Linux の base64 コマンドの違いを吸収
b64dec() {
if base64 --help 2>&1 | grep -q -- '--decode'; then
base64 --decode
else
base64 -D
fi
}
# kind の control-plane ノードの IP アドレスを取得
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"
# 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
# トークンが生成されるまで待機
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}')"
# cluster2 に 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
# 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
# トークンが生成されるまで待機
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}')"
# cluster1 に 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. east-west gateway を作成
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:
# NOTE: istio-east-west GatewayClass では HBONE listener は Terminate が必須。
# Passthrough にすると Gateway が UnsupportedProtocol 扱いとなり、NetworkGateway として認識されず
# クロスクラスタの経路(= gateway 経由)が組み立てられません。
mode: Terminate
# Gateway API の validation で Terminate には certificateRefs か options が必須。
# Istio の east-west gateway では ISTIO_MUTUAL を指定して自動 mTLS 終端を行う。
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
EXTERNAL-IP が割り当てられるまで待機:
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. 動作確認
sample namespace を作成して ambient モードを有効化:
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
helloworld をデプロイ(cluster1 に v1、cluster2 に v2):
# Service を両クラスタに作成
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
# Deployment を各クラスタに異なるバージョンで作成
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
# グローバルサービスとしてマーク(クロスクラスター通信を有効化)
# `istio.io/global=true` を付けることで、istiod が「クラスタを跨いだサービス」として扱う対象になります。
# ここを忘れると “各クラスタのローカルサービス” として扱われ続け、クロスクラスタのエンドポイントが配信されません。
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
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
クロスクラスターロードバランスを確認:
# cluster1 の curl から helloworld を呼び出し
for i in {1..10}; do
kubectl exec --context kind-cluster1 -n sample deploy/curl -c curl -- \
curl -sS "helloworld.sample:5000/hello"
done
# 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
v1 と v2 が混在して返ってくれば成功です。
例:
$ 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
追加設定オプション
グローバルサービスの動作制御(Istio 1.25+)
デフォルトでは、istio.io/global=true ラベルを付けると <name>.<namespace>.mesh.internal という新しいサービスが作成され、元のサービス <name>.<namespace>.svc.cluster.local はローカルエンドポイントのみを含みます。
もし元のサービス名でリモートエンドポイントも含めたい場合は、solo.io/service-scope=global-only ラベルを使用できます(Gloo Mesh を使用している場合)。
トラフィック分散の制御
Service に networking.istio.io/traffic-distribution アノテーションを付けることで、トラフィックの分散方法を制御できます:
# ネットワーク/リージョン/ゾーン等を考慮して最も近いエンドポイントを優先
kubectl annotate svc helloworld -n sample networking.istio.io/traffic-distribution=PreferClose
# 同一ゾーン(→リージョン→ネットワーク)を優先
kubectl annotate svc helloworld -n sample networking.istio.io/traffic-distribution=PreferSameZone
# 同一ノード(→サブゾーン→ゾーン→リージョン→ネットワーク)を優先
kubectl annotate svc helloworld -n sample networking.istio.io/traffic-distribution=PreferSameNode
# 全てのエンドポイントを均等に扱う
kubectl annotate svc helloworld -n sample networking.istio.io/traffic-distribution=Any
ハマりポイント
-
cloud-provider-kind が起動していない: EXTERNAL-IP が
<pending>のまま割り当てられない -
CA が一致していない:
curl: (56) Recv failure: Connection reset by peerなどのエラーが発生 -
istio.io/global=trueラベルの付け忘れ: 両クラスタの Service に付けないとクロスクラスター通信が発生しない -
Istio リポジトリ外での実行:
samples/ディレクトリやmanifests/ディレクトリが見つからずエラーになる -
east-west gateway が
UnsupportedProtocolになる:Gateway.status.listeners[].conditions[type=Accepted].reason=UnsupportedProtocolになっている場合、
HBONE listener のtls.modeやtls.optionsが期待値と合っておらず、NetworkGateway として扱われません
参考資料
- Istio / Introducing multicluster support for ambient mode (alpha)
- Istio / Install Multicluster
- Ambient Mesh / Multi-cluster
- Gateway API
- Istio / Plug in CA Certificates
- istio/tools/istio-nftables/pkg/README.md at master · istio/istio
- zTunnel error io error: invalid peer certificate: Other(OtherError(identity verification error: peer did not present the expected SAN · Issue #58630 · istio/istio
- Istio / Ztunnel traffic redirection
- Istio / Ambient data plane
- Istio / Ambient and the Istio control plane