2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

kind で Istio Ambient マルチクラスターを動かす

Last updated at Posted at 2026-01-19

構成

  • 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.modetls.options が期待値と合っておらず、NetworkGateway として扱われません

参考資料

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?