Install Istio with an External Control Plane

This guide walks you through the process of installing an external control plane and then connecting one or more remote clusters to it. The external control plane deployment model allows a mesh operator to install and manage a control plane on an external cluster, separate from the data plane cluster (or multiple clusters) comprising the mesh. This deployment model allows a clear separation between mesh operators and mesh administrators. Mesh operators install and manage Istio control planes while mesh admins only need to configure the mesh.

External control plane cluster and remote cluster
External control plane cluster and remote cluster

Envoy proxies (sidecars and gateways) running in the remote cluster access the external istiod via an ingress gateway which exposes the endpoints needed for discovery, CA, injection, and validation.

While configuration and management of the external control plane is done by the mesh operator in the external cluster, the first remote cluster connected to an external control plane serves as the config cluster for the mesh itself. The mesh administrator will use the config cluster to configure the mesh resources (gateways, virtual services, etc.) in addition to the mesh services themselves. The external control plane will remotely access this configuration from the Kubernetes API server, as shown in the above diagram.

Before you begin

Clusters

This guide requires that you have two Kubernetes clusters with any of the supported Kubernetes versions: 1.27, 1.28, 1.29, 1.30.

The first cluster will host the external control plane installed in the external-istiod namespace. An ingress gateway is also installed in the istio-system namespace to provide cross-cluster access to the external control plane.

The second cluster is a remote cluster that will run the mesh application workloads. Its Kubernetes API server also provides the mesh configuration used by the external control plane (istiod) to configure the workload proxies.

API server access

The Kubernetes API server in the remote cluster must be accessible to the external control plane cluster. Many cloud providers make API servers publicly accessible via network load balancers (NLBs). If the API server is not directly accessible, you will need to modify the installation procedure to enable access. For example, the east-west gateway used in a multicluster configuration could also be used to enable access to the API server.

Environment Variables

The following environment variables will be used throughout to simplify the instructions:

VariableDescription
CTX_EXTERNAL_CLUSTERThe context name in the default Kubernetes configuration file used for accessing the external control plane cluster.
CTX_REMOTE_CLUSTERThe context name in the default Kubernetes configuration file used for accessing the remote cluster.
REMOTE_CLUSTER_NAMEThe name of the remote cluster.
EXTERNAL_ISTIOD_ADDRThe hostname for the ingress gateway on the external control plane cluster. This is used by the remote cluster to access the external control plane.
SSL_SECRET_NAMEThe name of the secret that holds the TLS certs for the ingress gateway on the external control plane cluster.

Set the CTX_EXTERNAL_CLUSTER, CTX_REMOTE_CLUSTER, and REMOTE_CLUSTER_NAME now. You will set the others later.

  1. $ export CTX_EXTERNAL_CLUSTER=<your external cluster context>
  2. $ export CTX_REMOTE_CLUSTER=<your remote cluster context>
  3. $ export REMOTE_CLUSTER_NAME=<your remote cluster name>

Cluster configuration

Mesh operator steps

A mesh operator is responsible for installing and managing the external Istio control plane on the external cluster. This includes configuring an ingress gateway on the external cluster, which allows the remote cluster to access the control plane, and installing the sidecar injector webhook configuration on the remote cluster so that it will use the external control plane.

Set up a gateway in the external cluster

  1. Create the Istio install configuration for the ingress gateway that will expose the external control plane ports to other clusters:

    1. $ cat <<EOF > controlplane-gateway.yaml
    2. apiVersion: install.istio.io/v1alpha1
    3. kind: IstioOperator
    4. metadata:
    5. namespace: istio-system
    6. spec:
    7. components:
    8. ingressGateways:
    9. - name: istio-ingressgateway
    10. enabled: true
    11. k8s:
    12. service:
    13. ports:
    14. - port: 15021
    15. targetPort: 15021
    16. name: status-port
    17. - port: 15012
    18. targetPort: 15012
    19. name: tls-xds
    20. - port: 15017
    21. targetPort: 15017
    22. name: tls-webhook
    23. EOF

    Then, install the gateway in the istio-system namespace of the external cluster:

    1. $ istioctl install -f controlplane-gateway.yaml --context="${CTX_EXTERNAL_CLUSTER}"
  2. Run the following command to confirm that the ingress gateway is up and running:

    1. $ kubectl get po -n istio-system --context="${CTX_EXTERNAL_CLUSTER}"
    2. NAME READY STATUS RESTARTS AGE
    3. istio-ingressgateway-9d4c7f5c7-7qpzz 1/1 Running 0 29s
    4. istiod-68488cd797-mq8dn 1/1 Running 0 38s

    You will notice an istiod deployment is also created in the istio-system namespace. This is used to configure the ingress gateway and is NOT the control plane used by remote clusters.

    This ingress gateway could be configured to host multiple external control planes, in different namespaces on the external cluster, although in this example you will only deploy a single external istiod in the external-istiod namespace.

  3. Configure your environment to expose the Istio ingress gateway service using a public hostname with TLS.

    Set the EXTERNAL_ISTIOD_ADDR environment variable to the hostname and SSL_SECRET_NAME environment variable to the secret that holds the TLS certs:

    1. $ export EXTERNAL_ISTIOD_ADDR=<your external istiod host>
    2. $ export SSL_SECRET_NAME=<your external istiod secret>

    These instructions assume that you are exposing the external cluster’s gateway using a hostname with properly signed DNS certs as this is the recommended approach in a production environment. Refer to the secure ingress task for more information on exposing a secure gateway.

    Your environment variables should look something like this:

    1. $ echo "$EXTERNAL_ISTIOD_ADDR" "$SSL_SECRET_NAME"
    2. myhost.example.com myhost-example-credential

    If you don’t have a DNS hostname but want to experiment with an external control plane in a test environment, you can access the gateway using its external load balancer IP address:

    1. $ export EXTERNAL_ISTIOD_ADDR=$(kubectl -n istio-system --context="${CTX_EXTERNAL_CLUSTER}" get svc istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
    2. $ export SSL_SECRET_NAME=NONE

    Doing this will also require a few other changes in the configuration. Make sure to follow all of the related steps in the instructions below.

Set up the remote config cluster

  1. Use the remote profile to configure the remote cluster’s Istio installation. This installs an injection webhook that uses the external control plane’s injector, instead of a locally deployed one. Because this cluster will also serve as the config cluster, the Istio CRDs and other resources that will be needed on the remote cluster are also installed by setting global.configCluster and pilot.configMap to true:

    1. $ cat <<EOF > remote-config-cluster.yaml
    2. apiVersion: install.istio.io/v1alpha1
    3. kind: IstioOperator
    4. metadata:
    5. namespace: external-istiod
    6. spec:
    7. profile: remote
    8. values:
    9. global:
    10. istioNamespace: external-istiod
    11. configCluster: true
    12. pilot:
    13. configMap: true
    14. istiodRemote:
    15. injectionURL: https://${EXTERNAL_ISTIOD_ADDR}:15017/inject/cluster/${REMOTE_CLUSTER_NAME}/net/network1
    16. base:
    17. validationURL: https://${EXTERNAL_ISTIOD_ADDR}:15017/validate
    18. EOF

    If your cluster name contains / (slash) characters, replace them with --slash-- in the injectionURL, e.g., injectionURL: https://1.2.3.4:15017/inject/cluster/``cluster--slash--1``/net/network1.

  2. If you are using an IP address for the EXTERNAL_ISTIOD_ADDR, instead of a proper DNS hostname, modify the configuration to specify the discovery address and paths, instead of URLs:

    This is not recommended in a production environment.

    1. $ sed -i'.bk' \
    2. -e "s|injectionURL: https://${EXTERNAL_ISTIOD_ADDR}:15017|injectionPath: |" \
    3. -e "/istioNamespace:/a\\
    4. remotePilotAddress: ${EXTERNAL_ISTIOD_ADDR}" \
    5. -e '/base:/,+1d' \
    6. remote-config-cluster.yaml; rm remote-config-cluster.yaml.bk
  3. Install the configuration on the remote cluster:

    1. $ kubectl create namespace external-istiod --context="${CTX_REMOTE_CLUSTER}"
    2. $ istioctl manifest generate -f remote-config-cluster.yaml --set values.defaultRevision=default | kubectl apply --context="${CTX_REMOTE_CLUSTER}" -f -
  4. Confirm that the remote cluster’s injection webhook configuration has been installed:

    1. $ kubectl get mutatingwebhookconfiguration --context="${CTX_REMOTE_CLUSTER}"
    2. NAME WEBHOOKS AGE
    3. istio-sidecar-injector-external-istiod 4 6m24s
  5. Confirm that the remote cluster’s validation webhook configurations have been installed:

    1. $ kubectl get validatingwebhookconfiguration --context="${CTX_REMOTE_CLUSTER}"
    2. NAME WEBHOOKS AGE
    3. istio-validator-external-istiod 1 6m53s
    4. istiod-default-validator 1 6m53s

Set up the control plane in the external cluster

  1. Create the external-istiod namespace, which will be used to host the external control plane:

    1. $ kubectl create namespace external-istiod --context="${CTX_EXTERNAL_CLUSTER}"
  2. The control plane in the external cluster needs access to the remote cluster to discover services, endpoints, and pod attributes. Create a secret with credentials to access the remote cluster’s kube-apiserver and install it in the external cluster:

    1. $ kubectl create sa istiod-service-account -n external-istiod --context="${CTX_EXTERNAL_CLUSTER}"
    2. $ istioctl create-remote-secret \
    3. --context="${CTX_REMOTE_CLUSTER}" \
    4. --type=config \
    5. --namespace=external-istiod \
    6. --service-account=istiod \
    7. --create-service-account=false | \
    8. kubectl apply -f - --context="${CTX_EXTERNAL_CLUSTER}"
  3. Create the Istio configuration to install the control plane in the external-istiod namespace of the external cluster. Notice that istiod is configured to use the locally mounted istio configmap and the SHARED_MESH_CONFIG environment variable is set to istio. This instructs istiod to merge the values set by the mesh admin in the config cluster’s configmap with the values in the local configmap set by the mesh operator, here, which will take precedence if there are any conflicts:

    1. $ cat <<EOF > external-istiod.yaml
    2. apiVersion: install.istio.io/v1alpha1
    3. kind: IstioOperator
    4. metadata:
    5. namespace: external-istiod
    6. spec:
    7. profile: empty
    8. meshConfig:
    9. rootNamespace: external-istiod
    10. defaultConfig:
    11. discoveryAddress: $EXTERNAL_ISTIOD_ADDR:15012
    12. proxyMetadata:
    13. XDS_ROOT_CA: /etc/ssl/certs/ca-certificates.crt
    14. CA_ROOT_CA: /etc/ssl/certs/ca-certificates.crt
    15. components:
    16. pilot:
    17. enabled: true
    18. k8s:
    19. overlays:
    20. - kind: Deployment
    21. name: istiod
    22. patches:
    23. - path: spec.template.spec.volumes[100]
    24. value: |-
    25. name: config-volume
    26. configMap:
    27. name: istio
    28. - path: spec.template.spec.volumes[100]
    29. value: |-
    30. name: inject-volume
    31. configMap:
    32. name: istio-sidecar-injector
    33. - path: spec.template.spec.containers[0].volumeMounts[100]
    34. value: |-
    35. name: config-volume
    36. mountPath: /etc/istio/config
    37. - path: spec.template.spec.containers[0].volumeMounts[100]
    38. value: |-
    39. name: inject-volume
    40. mountPath: /var/lib/istio/inject
    41. env:
    42. - name: INJECTION_WEBHOOK_CONFIG_NAME
    43. value: ""
    44. - name: VALIDATION_WEBHOOK_CONFIG_NAME
    45. value: ""
    46. - name: EXTERNAL_ISTIOD
    47. value: "true"
    48. - name: LOCAL_CLUSTER_SECRET_WATCHER
    49. value: "true"
    50. - name: CLUSTER_ID
    51. value: ${REMOTE_CLUSTER_NAME}
    52. - name: SHARED_MESH_CONFIG
    53. value: istio
    54. values:
    55. global:
    56. caAddress: $EXTERNAL_ISTIOD_ADDR:15012
    57. istioNamespace: external-istiod
    58. operatorManageWebhooks: true
    59. configValidation: false
    60. meshID: mesh1
    61. multiCluster:
    62. clusterName: ${REMOTE_CLUSTER_NAME}
    63. network: network1
    64. EOF
  4. If you are using an IP address for the EXTERNAL_ISTIOD_ADDR, instead of a proper DNS hostname, delete the proxy metadata and update the webhook config environment variables in the configuration:

    This is not recommended in a production environment.

    1. $ sed -i'.bk' \
    2. -e '/proxyMetadata:/,+2d' \
    3. -e '/INJECTION_WEBHOOK_CONFIG_NAME/{n;s/value: ""/value: istio-sidecar-injector-external-istiod/;}' \
    4. -e '/VALIDATION_WEBHOOK_CONFIG_NAME/{n;s/value: ""/value: istio-validator-external-istiod/;}' \
    5. external-istiod.yaml ; rm external-istiod.yaml.bk
  5. Apply the Istio configuration on the external cluster:

    1. $ istioctl install -f external-istiod.yaml --context="${CTX_EXTERNAL_CLUSTER}"
  6. Confirm that the external istiod has been successfully deployed:

    1. $ kubectl get po -n external-istiod --context="${CTX_EXTERNAL_CLUSTER}"
    2. NAME READY STATUS RESTARTS AGE
    3. istiod-779bd6fdcf-bd6rg 1/1 Running 0 70s
  7. Create the Istio Gateway, VirtualService, and DestinationRule configuration to route traffic from the ingress gateway to the external control plane:

    1. $ cat <<EOF > external-istiod-gw.yaml
    2. apiVersion: networking.istio.io/v1
    3. kind: Gateway
    4. metadata:
    5. name: external-istiod-gw
    6. namespace: external-istiod
    7. spec:
    8. selector:
    9. istio: ingressgateway
    10. servers:
    11. - port:
    12. number: 15012
    13. protocol: https
    14. name: https-XDS
    15. tls:
    16. mode: SIMPLE
    17. credentialName: $SSL_SECRET_NAME
    18. hosts:
    19. - $EXTERNAL_ISTIOD_ADDR
    20. - port:
    21. number: 15017
    22. protocol: https
    23. name: https-WEBHOOK
    24. tls:
    25. mode: SIMPLE
    26. credentialName: $SSL_SECRET_NAME
    27. hosts:
    28. - $EXTERNAL_ISTIOD_ADDR
    29. ---
    30. apiVersion: networking.istio.io/v1
    31. kind: VirtualService
    32. metadata:
    33. name: external-istiod-vs
    34. namespace: external-istiod
    35. spec:
    36. hosts:
    37. - $EXTERNAL_ISTIOD_ADDR
    38. gateways:
    39. - external-istiod-gw
    40. http:
    41. - match:
    42. - port: 15012
    43. route:
    44. - destination:
    45. host: istiod.external-istiod.svc.cluster.local
    46. port:
    47. number: 15012
    48. - match:
    49. - port: 15017
    50. route:
    51. - destination:
    52. host: istiod.external-istiod.svc.cluster.local
    53. port:
    54. number: 443
    55. ---
    56. apiVersion: networking.istio.io/v1
    57. kind: DestinationRule
    58. metadata:
    59. name: external-istiod-dr
    60. namespace: external-istiod
    61. spec:
    62. host: istiod.external-istiod.svc.cluster.local
    63. trafficPolicy:
    64. portLevelSettings:
    65. - port:
    66. number: 15012
    67. tls:
    68. mode: SIMPLE
    69. connectionPool:
    70. http:
    71. h2UpgradePolicy: UPGRADE
    72. - port:
    73. number: 443
    74. tls:
    75. mode: SIMPLE
    76. EOF
  8. If you are using an IP address for the EXTERNAL_ISTIOD_ADDR, instead of a proper DNS hostname, modify the configuration. Delete the DestinationRule, don’t terminate TLS in the Gateway, and use TLS routing in the VirtualService:

    This is not recommended in a production environment.

    1. $ sed -i'.bk' \
    2. -e '55,$d' \
    3. -e 's/mode: SIMPLE/mode: PASSTHROUGH/' -e '/credentialName:/d' -e "s/${EXTERNAL_ISTIOD_ADDR}/\"*\"/" \
    4. -e 's/http:/tls:/' -e 's/https/tls/' -e '/route:/i\
    5. sniHosts:\
    6. - "*"' \
    7. external-istiod-gw.yaml; rm external-istiod-gw.yaml.bk
  9. Apply the configuration on the external cluster:

    1. $ kubectl apply -f external-istiod-gw.yaml --context="${CTX_EXTERNAL_CLUSTER}"

Mesh admin steps

Now that Istio is up and running, a mesh administrator only needs to deploy and configure services in the mesh, including gateways, if needed.

Some of the istioctl CLI commands won’t work by default on a remote cluster, although you can easily configure istioctl to make it fully functional. See the Istioctl-proxy Ecosystem project for details.

Deploy a sample application

  1. Create, and label for injection, the sample namespace on the remote cluster:

    1. $ kubectl create --context="${CTX_REMOTE_CLUSTER}" namespace sample
    2. $ kubectl label --context="${CTX_REMOTE_CLUSTER}" namespace sample istio-injection=enabled
  2. Deploy the helloworld (v1) and sleep samples:

    ZipZipZip

    1. $ kubectl apply -f @samples/helloworld/helloworld.yaml@ -l service=helloworld -n sample --context="${CTX_REMOTE_CLUSTER}"
    2. $ kubectl apply -f @samples/helloworld/helloworld.yaml@ -l version=v1 -n sample --context="${CTX_REMOTE_CLUSTER}"
    3. $ kubectl apply -f @samples/sleep/sleep.yaml@ -n sample --context="${CTX_REMOTE_CLUSTER}"
  3. Wait a few seconds for the helloworld and sleep pods to be running with sidecars injected:

    1. $ kubectl get pod -n sample --context="${CTX_REMOTE_CLUSTER}"
    2. NAME READY STATUS RESTARTS AGE
    3. helloworld-v1-776f57d5f6-s7zfc 2/2 Running 0 10s
    4. sleep-64d7d56698-wqjnm 2/2 Running 0 9s
  4. Send a request from the sleep pod to the helloworld service:

    1. $ kubectl exec --context="${CTX_REMOTE_CLUSTER}" -n sample -c sleep \
    2. "$(kubectl get pod --context="${CTX_REMOTE_CLUSTER}" -n sample -l app=sleep -o jsonpath='{.items[0].metadata.name}')" \
    3. -- curl -sS helloworld.sample:5000/hello
    4. Hello version: v1, instance: helloworld-v1-776f57d5f6-s7zfc

Enable gateways

Istio supports the Kubernetes Gateway API and intends to make it the default API for traffic management in the future. If you use the Gateway API, you will not need to install any gateway components. You can skip the following instructions and proceed directly to configure and test an ingress gateway.

Enable an ingress gateway on the remote cluster:

  1. $ cat <<EOF > istio-ingressgateway.yaml
  2. apiVersion: install.istio.io/v1alpha1
  3. kind: IstioOperator
  4. spec:
  5. profile: empty
  6. components:
  7. ingressGateways:
  8. - namespace: external-istiod
  9. name: istio-ingressgateway
  10. enabled: true
  11. values:
  12. gateways:
  13. istio-ingressgateway:
  14. injectionTemplate: gateway
  15. EOF
  16. $ istioctl install -f istio-ingressgateway.yaml --set values.global.istioNamespace=external-istiod --context="${CTX_REMOTE_CLUSTER}"
  1. $ helm install istio-ingressgateway istio/gateway -n external-istiod --kube-context="${CTX_REMOTE_CLUSTER}"

See Installing Gateways for in-depth documentation on gateway installation.

You can optionally enable other gateways as well. For example, an egress gateway:

  1. $ cat <<EOF > istio-egressgateway.yaml
  2. apiVersion: install.istio.io/v1alpha1
  3. kind: IstioOperator
  4. spec:
  5. profile: empty
  6. components:
  7. egressGateways:
  8. - namespace: external-istiod
  9. name: istio-egressgateway
  10. enabled: true
  11. values:
  12. gateways:
  13. istio-egressgateway:
  14. injectionTemplate: gateway
  15. EOF
  16. $ istioctl install -f istio-egressgateway.yaml --set values.global.istioNamespace=external-istiod --context="${CTX_REMOTE_CLUSTER}"
  1. $ helm install istio-egressgateway istio/gateway -n external-istiod --kube-context="${CTX_REMOTE_CLUSTER}" --set service.type=ClusterIP

See Installing Gateways for in-depth documentation on gateway installation.

Configure and test an ingress gateway

The following instructions allow you to choose to use either the Gateway API or the Istio configuration API when configuring traffic management in the mesh. Follow instructions under either the Gateway API or Istio APIs tab, according to your preference.

  1. Make sure that the cluster is ready to configure the gateway:

Confirm that the Istio ingress gateway is running:

  1. $ kubectl get pod -l app=istio-ingressgateway -n external-istiod --context="${CTX_REMOTE_CLUSTER}"
  2. NAME READY STATUS RESTARTS AGE
  3. istio-ingressgateway-7bcd5c6bbd-kmtl4 1/1 Running 0 8m4s

The Kubernetes Gateway API CRDs do not come installed by default on most Kubernetes clusters, so make sure they are installed before using the Gateway API:

  1. $ kubectl get crd gateways.gateway.networking.k8s.io --context="${CTX_REMOTE_CLUSTER}" &> /dev/null || \
  2. { kubectl kustomize "github.com/kubernetes-sigs/gateway-api/config/crd?ref=v1.1.0" | kubectl apply -f - --context="${CTX_REMOTE_CLUSTER}"; }
  1. Expose the helloworld application on an ingress gateway:

Zip

  1. $ kubectl apply -f @samples/helloworld/helloworld-gateway.yaml@ -n sample --context="${CTX_REMOTE_CLUSTER}"

Zip

  1. $ kubectl apply -f @samples/helloworld/gateway-api/helloworld-gateway.yaml@ -n sample --context="${CTX_REMOTE_CLUSTER}"
  1. Set the GATEWAY_URL environment variable (see determining the ingress IP and ports for details):
  1. $ export INGRESS_HOST=$(kubectl -n external-istiod --context="${CTX_REMOTE_CLUSTER}" get service istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
  2. $ export INGRESS_PORT=$(kubectl -n external-istiod --context="${CTX_REMOTE_CLUSTER}" get service istio-ingressgateway -o jsonpath='{.spec.ports[?(@.name=="http2")].port}')
  3. $ export GATEWAY_URL=$INGRESS_HOST:$INGRESS_PORT
  1. $ kubectl -n sample --context="${CTX_REMOTE_CLUSTER}" wait --for=condition=programmed gtw helloworld-gateway
  2. $ export INGRESS_HOST=$(kubectl -n sample --context="${CTX_REMOTE_CLUSTER}" get gtw helloworld-gateway -o jsonpath='{.status.addresses[0].value}')
  3. $ export GATEWAY_URL=$INGRESS_HOST:80
  1. Confirm you can access the helloworld application through the ingress gateway:

    1. $ curl -s "http://${GATEWAY_URL}/hello"
    2. Hello version: v1, instance: helloworld-v1-776f57d5f6-s7zfc

Adding clusters to the mesh (optional)

This section shows you how to expand an existing external control plane mesh to multicluster by adding another remote cluster. This allows you to easily distribute services and use location-aware routing and fail over to support high availability of your application.

External control plane with multiple remote clusters
External control plane with multiple remote clusters

Unlike the first remote cluster, the second and subsequent clusters added to the same external control plane do not provide mesh config, but instead are only sources of endpoint configuration, just like remote clusters in a primary-remote Istio multicluster configuration.

To proceed, you’ll need another Kubernetes cluster for the second remote cluster of the mesh. Set the following environment variables to the context name and cluster name of the cluster:

  1. $ export CTX_SECOND_CLUSTER=<your second remote cluster context>
  2. $ export SECOND_CLUSTER_NAME=<your second remote cluster name>

Register the new cluster

  1. Create the remote Istio install configuration, which installs the injection webhook that uses the external control plane’s injector, instead of a locally deployed one:

    1. $ cat <<EOF > second-remote-cluster.yaml
    2. apiVersion: install.istio.io/v1alpha1
    3. kind: IstioOperator
    4. metadata:
    5. namespace: external-istiod
    6. spec:
    7. profile: remote
    8. values:
    9. global:
    10. istioNamespace: external-istiod
    11. istiodRemote:
    12. injectionURL: https://${EXTERNAL_ISTIOD_ADDR}:15017/inject/cluster/${SECOND_CLUSTER_NAME}/net/network2
    13. EOF
  2. If you are using an IP address for the EXTERNAL_ISTIOD_ADDR, instead of a proper DNS hostname, modify the configuration to specify the discovery address and path, instead of an injection URL:

    This is not recommended in a production environment.

    1. $ sed -i'.bk' \
    2. -e "s|injectionURL: https://${EXTERNAL_ISTIOD_ADDR}:15017|injectionPath: |" \
    3. -e "/istioNamespace:/a\\
    4. remotePilotAddress: ${EXTERNAL_ISTIOD_ADDR}" \
    5. second-remote-cluster.yaml; rm second-remote-cluster.yaml.bk
  3. Create and annotate the system namespace on the remote cluster:

    1. $ kubectl create namespace external-istiod --context="${CTX_SECOND_CLUSTER}"
    2. $ kubectl annotate namespace external-istiod "topology.istio.io/controlPlaneClusters=${REMOTE_CLUSTER_NAME}" --context="${CTX_SECOND_CLUSTER}"

    The topology.istio.io/controlPlaneClusters annotation specifies the cluster ID of the external control plane that should manage this remote cluster. Notice that this is the name of the first remote (config) cluster, which was used to set the cluster ID of the external control plane when it was installed in the external cluster earlier.

  4. Install the configuration on the remote cluster:

    1. $ istioctl manifest generate -f second-remote-cluster.yaml | kubectl apply --context="${CTX_SECOND_CLUSTER}" -f -
  5. Confirm that the remote cluster’s injection webhook configuration has been installed:

    1. $ kubectl get mutatingwebhookconfiguration --context="${CTX_SECOND_CLUSTER}"
    2. NAME WEBHOOKS AGE
    3. istio-sidecar-injector-external-istiod 4 4m13s
  6. Create a secret with credentials to allow the control plane to access the endpoints on the second remote cluster and install it:

    1. $ istioctl create-remote-secret \
    2. --context="${CTX_SECOND_CLUSTER}" \
    3. --name="${SECOND_CLUSTER_NAME}" \
    4. --type=remote \
    5. --namespace=external-istiod \
    6. --create-service-account=false | \
    7. kubectl apply -f - --context="${CTX_EXTERNAL_CLUSTER}"

    Note that unlike the first remote cluster of the mesh, which also serves as the config cluster, the --type argument is set to remote this time, instead of config.

Setup east-west gateways

  1. Deploy east-west gateways on both remote clusters:

    Zip

    1. $ @samples/multicluster/gen-eastwest-gateway.sh@ \
    2. --network network1 > eastwest-gateway-1.yaml
    3. $ istioctl manifest generate -f eastwest-gateway-1.yaml \
    4. --set values.global.istioNamespace=external-istiod | \
    5. kubectl apply --context="${CTX_REMOTE_CLUSTER}" -f -

    Zip

    1. $ @samples/multicluster/gen-eastwest-gateway.sh@ \
    2. --network network2 > eastwest-gateway-2.yaml
    3. $ istioctl manifest generate -f eastwest-gateway-2.yaml \
    4. --set values.global.istioNamespace=external-istiod | \
    5. kubectl apply --context="${CTX_SECOND_CLUSTER}" -f -
  2. Wait for the east-west gateways to be assigned external IP addresses:

    1. $ kubectl --context="${CTX_REMOTE_CLUSTER}" get svc istio-eastwestgateway -n external-istiod
    2. NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
    3. istio-eastwestgateway LoadBalancer 10.0.12.121 34.122.91.98 ... 51s
    1. $ kubectl --context="${CTX_SECOND_CLUSTER}" get svc istio-eastwestgateway -n external-istiod
    2. NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
    3. istio-eastwestgateway LoadBalancer 10.0.12.121 34.122.91.99 ... 51s
  3. Expose services via the east-west gateways:

    Zip

    1. $ kubectl --context="${CTX_REMOTE_CLUSTER}" apply -n external-istiod -f \
    2. @samples/multicluster/expose-services.yaml@

Validate the installation

  1. Create, and label for injection, the sample namespace on the remote cluster:

    1. $ kubectl create --context="${CTX_SECOND_CLUSTER}" namespace sample
    2. $ kubectl label --context="${CTX_SECOND_CLUSTER}" namespace sample istio-injection=enabled
  2. Deploy the helloworld (v2) and sleep samples:

    ZipZipZip

    1. $ kubectl apply -f @samples/helloworld/helloworld.yaml@ -l service=helloworld -n sample --context="${CTX_SECOND_CLUSTER}"
    2. $ kubectl apply -f @samples/helloworld/helloworld.yaml@ -l version=v2 -n sample --context="${CTX_SECOND_CLUSTER}"
    3. $ kubectl apply -f @samples/sleep/sleep.yaml@ -n sample --context="${CTX_SECOND_CLUSTER}"
  3. Wait a few seconds for the helloworld and sleep pods to be running with sidecars injected:

    1. $ kubectl get pod -n sample --context="${CTX_SECOND_CLUSTER}"
    2. NAME READY STATUS RESTARTS AGE
    3. helloworld-v2-54df5f84b-9hxgw 2/2 Running 0 10s
    4. sleep-557747455f-wtdbr 2/2 Running 0 9s
  4. Send a request from the sleep pod to the helloworld service:

    1. $ kubectl exec --context="${CTX_SECOND_CLUSTER}" -n sample -c sleep \
    2. "$(kubectl get pod --context="${CTX_SECOND_CLUSTER}" -n sample -l app=sleep -o jsonpath='{.items[0].metadata.name}')" \
    3. -- curl -sS helloworld.sample:5000/hello
    4. Hello version: v2, instance: helloworld-v2-54df5f84b-9hxgw
  5. Confirm that when accessing the helloworld application several times through the ingress gateway, both version v1 and v2 are now being called:

    1. $ for i in {1..10}; do curl -s "http://${GATEWAY_URL}/hello"; done
    2. Hello version: v1, instance: helloworld-v1-776f57d5f6-s7zfc
    3. Hello version: v2, instance: helloworld-v2-54df5f84b-9hxgw
    4. Hello version: v1, instance: helloworld-v1-776f57d5f6-s7zfc
    5. Hello version: v2, instance: helloworld-v2-54df5f84b-9hxgw
    6. ...

Cleanup

Clean up the external control plane cluster:

  1. $ kubectl delete -f external-istiod-gw.yaml --context="${CTX_EXTERNAL_CLUSTER}"
  2. $ istioctl uninstall -y --purge --context="${CTX_EXTERNAL_CLUSTER}"
  3. $ kubectl delete ns istio-system external-istiod --context="${CTX_EXTERNAL_CLUSTER}"
  4. $ rm controlplane-gateway.yaml external-istiod.yaml external-istiod-gw.yaml

Clean up the remote config cluster:

  1. $ kubectl delete ns sample --context="${CTX_REMOTE_CLUSTER}"
  2. $ istioctl manifest generate -f remote-config-cluster.yaml --set values.defaultRevision=default | kubectl delete --context="${CTX_REMOTE_CLUSTER}" -f -
  3. $ kubectl delete ns external-istiod --context="${CTX_REMOTE_CLUSTER}"
  4. $ rm remote-config-cluster.yaml istio-ingressgateway.yaml
  5. $ rm istio-egressgateway.yaml eastwest-gateway-1.yaml || true

Clean up the optional second remote cluster if you installed it:

  1. $ kubectl delete ns sample --context="${CTX_SECOND_CLUSTER}"
  2. $ istioctl manifest generate -f second-remote-cluster.yaml | kubectl delete --context="${CTX_SECOND_CLUSTER}" -f -
  3. $ kubectl delete ns external-istiod --context="${CTX_SECOND_CLUSTER}"
  4. $ rm second-remote-cluster.yaml eastwest-gateway-2.yaml