Install Multiple Istio Control Planes in a Single Cluster
This guide walks you through the process of installing multiple Istio control planes within a single cluster and then a way to scope workloads to specific control planes. This deployment model has a single Kubernetes control plane with multiple Istio control planes and meshes. The separation between the meshes is provided by Kubernetes namespaces and RBAC.
Using discoverySelectors
, you can scope Kubernetes resources in a cluster to specific namespaces managed by an Istio control plane. This includes the Istio custom resources (e.g., Gateway, VirtualService, DestinationRule, etc.) used to configure the mesh. Furthermore, discoverySelectors
can be used to configure which namespaces should include the istio-ca-root-cert
config map for a particular Istio control plane. Together, these functions allow mesh operators to specify the namespaces for a given control plane, enabling soft multi-tenancy for multiple meshes based on the boundary of one or more namespaces. This guide uses discoverySelectors
, along with the revisions capability of Istio, to demonstrate how two meshes can be deployed on a single cluster, each working with a properly scoped subset of the cluster’s resources.
Before you begin
This guide requires that you have a Kubernetes cluster with any of the supported Kubernetes versions: 1.28, 1.29, 1.30, 1.31.
This cluster will host two control planes installed in two different system namespaces. The mesh application workloads will run in multiple application-specific namespaces, each namespace associated with one or the other control plane based on revision and discovery selector configurations.
Cluster configuration
Deploying multiple control planes
Deploying multiple Istio control planes on a single cluster can be achieved by using different system namespaces for each control plane.
Istio revisions and discoverySelectors
are then used to scope the resources and workloads that are managed by each control plane.
Create the first system namespace,
usergroup-1
, and deploy istiod in it:$ kubectl create ns usergroup-1 $ kubectl label ns usergroup-1 usergroup=usergroup-1 $ istioctl install -y -f - <<EOF apiVersion: install.istio.io/v1alpha1 kind: IstioOperator metadata: namespace: usergroup-1 spec: profile: minimal revision: usergroup-1 meshConfig: discoverySelectors: - matchLabels: usergroup: usergroup-1 values: global: istioNamespace: usergroup-1 EOF
Create the second system namespace,
usergroup-2
, and deploy istiod in it:$ kubectl create ns usergroup-2 $ kubectl label ns usergroup-2 usergroup=usergroup-2 $ istioctl install -y -f - <<EOF apiVersion: install.istio.io/v1alpha1 kind: IstioOperator metadata: namespace: usergroup-2 spec: profile: minimal revision: usergroup-2 meshConfig: discoverySelectors: - matchLabels: usergroup: usergroup-2 values: global: istioNamespace: usergroup-2 EOF
Deploy a policy for workloads in the
usergroup-1
namespace to only accept mutual TLS traffic:$ kubectl apply -f - <<EOF apiVersion: security.istio.io/v1 kind: PeerAuthentication metadata: name: "usergroup-1-peerauth" namespace: "usergroup-1" spec: mtls: mode: STRICT EOF
Deploy a policy for workloads in the
usergroup-2
namespace to only accept mutual TLS traffic:$ kubectl apply -f - <<EOF apiVersion: security.istio.io/v1 kind: PeerAuthentication metadata: name: "usergroup-2-peerauth" namespace: "usergroup-2" spec: mtls: mode: STRICT EOF
Verify the multiple control plane creation
Check the labels on the system namespaces for each control plane:
$ kubectl get ns usergroup-1 usergroup2 --show-labels NAME STATUS AGE LABELS usergroup-1 Active 13m kubernetes.io/metadata.name=usergroup-1,usergroup=usergroup-1 usergroup-2 Active 12m kubernetes.io/metadata.name=usergroup-2,usergroup=usergroup-2
Verify the control planes are deployed and running:
$ kubectl get pods -n usergroup-1 NAMESPACE NAME READY STATUS RESTARTS AGE usergroup-1 istiod-usergroup-1-5ccc849b5f-wnqd6 1/1 Running 0 12m
$ kubectl get pods -n usergroup-2 NAMESPACE NAME READY STATUS RESTARTS AGE usergroup-2 istiod-usergroup-2-658d6458f7-slpd9 1/1 Running 0 12m
You will notice that one istiod deployment per usergroup is created in the specified namespaces.
Run the following commands to list the installed webhooks:
$ kubectl get validatingwebhookconfiguration NAME WEBHOOKS AGE istio-validator-usergroup-1-usergroup-1 1 18m istio-validator-usergroup-2-usergroup-2 1 18m istiod-default-validator 1 18m
$ kubectl get mutatingwebhookconfiguration NAME WEBHOOKS AGE istio-revision-tag-default-usergroup-1 4 18m istio-sidecar-injector-usergroup-1-usergroup-1 2 19m istio-sidecar-injector-usergroup-2-usergroup-2 2 18m
Note that the output includes
istiod-default-validator
andistio-revision-tag-default-usergroup-1
, which are the default webhook configurations used for handling requests coming from resources which are not associated with any revision. In a fully scoped environment where every control plane is associated with its resources through proper namespace labeling, there is no need for these default webhook configurations. They should never be invoked.
Deploy application workloads per usergroup
Create three application namespaces:
$ kubectl create ns app-ns-1 $ kubectl create ns app-ns-2 $ kubectl create ns app-ns-3
Label each namespace to associate them with their respective control planes:
$ kubectl label ns app-ns-1 usergroup=usergroup-1 istio.io/rev=usergroup-1 $ kubectl label ns app-ns-2 usergroup=usergroup-2 istio.io/rev=usergroup-2 $ kubectl label ns app-ns-3 usergroup=usergroup-2 istio.io/rev=usergroup-2
Deploy one
sleep
andhttpbin
application per namespace:$ kubectl -n app-ns-1 apply -f samples/sleep/sleep.yaml $ kubectl -n app-ns-1 apply -f samples/httpbin/httpbin.yaml $ kubectl -n app-ns-2 apply -f samples/sleep/sleep.yaml $ kubectl -n app-ns-2 apply -f samples/httpbin/httpbin.yaml $ kubectl -n app-ns-3 apply -f samples/sleep/sleep.yaml $ kubectl -n app-ns-3 apply -f samples/httpbin/httpbin.yaml
Wait a few seconds for the
httpbin
andsleep
pods to be running with sidecars injected:$ kubectl get pods -n app-ns-1 NAME READY STATUS RESTARTS AGE httpbin-9dbd644c7-zc2v4 2/2 Running 0 115m sleep-78ff5975c6-fml7c 2/2 Running 0 115m
$ kubectl get pods -n app-ns-2 NAME READY STATUS RESTARTS AGE httpbin-9dbd644c7-sd9ln 2/2 Running 0 115m sleep-78ff5975c6-sz728 2/2 Running 0 115m
$ kubectl get pods -n app-ns-3 NAME READY STATUS RESTARTS AGE httpbin-9dbd644c7-8ll27 2/2 Running 0 115m sleep-78ff5975c6-sg4tq 2/2 Running 0 115m
Verify the application to control plane mapping
Now that the applications are deployed, you can use the istioctl ps
command to confirm that the application workloads are managed by their respective control plane, i.e., app-ns-1
is managed by usergroup-1
, app-ns-2
and app-ns-3
are managed by usergroup-2
:
$ istioctl ps -i usergroup-1
NAME CLUSTER CDS LDS EDS RDS ECDS ISTIOD VERSION
httpbin-9dbd644c7-hccpf.app-ns-1 Kubernetes SYNCED SYNCED SYNCED SYNCED NOT SENT istiod-usergroup-1-5ccc849b5f-wnqd6 1.17-alpha.f5212a6f7df61fd8156f3585154bed2f003c4117
sleep-78ff5975c6-9zb77.app-ns-1 Kubernetes SYNCED SYNCED SYNCED SYNCED NOT SENT istiod-usergroup-1-5ccc849b5f-wnqd6 1.17-alpha.f5212a6f7df61fd8156f3585154bed2f003c4117
$ istioctl ps -i usergroup-2
NAME CLUSTER CDS LDS EDS RDS ECDS ISTIOD VERSION
httpbin-9dbd644c7-vvcqj.app-ns-3 Kubernetes SYNCED SYNCED SYNCED SYNCED NOT SENT istiod-usergroup-2-658d6458f7-slpd9 1.17-alpha.f5212a6f7df61fd8156f3585154bed2f003c4117
httpbin-9dbd644c7-xzgfm.app-ns-2 Kubernetes SYNCED SYNCED SYNCED SYNCED NOT SENT istiod-usergroup-2-658d6458f7-slpd9 1.17-alpha.f5212a6f7df61fd8156f3585154bed2f003c4117
sleep-78ff5975c6-fthmt.app-ns-2 Kubernetes SYNCED SYNCED SYNCED SYNCED NOT SENT istiod-usergroup-2-658d6458f7-slpd9 1.17-alpha.f5212a6f7df61fd8156f3585154bed2f003c4117
sleep-78ff5975c6-nxtth.app-ns-3 Kubernetes SYNCED SYNCED SYNCED SYNCED NOT SENT istiod-usergroup-2-658d6458f7-slpd9 1.17-alpha.f5212a6f7df61fd8156f3585154bed2f003c4117
Verify the application connectivity is ONLY within the respective usergroup
Send a request from the
sleep
pod inapp-ns-1
inusergroup-1
to thehttpbin
service inapp-ns-2
inusergroup-2
. The communication should fail:$ kubectl -n app-ns-1 exec "$(kubectl -n app-ns-1 get pod -l app=sleep -o jsonpath={.items..metadata.name})" -c sleep -- curl -sIL http://httpbin.app-ns-2.svc.cluster.local:8000 HTTP/1.1 503 Service Unavailable content-length: 95 content-type: text/plain date: Sat, 24 Dec 2022 06:54:54 GMT server: envoy
Send a request from the
sleep
pod inapp-ns-2
inusergroup-2
to thehttpbin
service inapp-ns-3
inusergroup-2
. The communication should work:$ kubectl -n app-ns-2 exec "$(kubectl -n app-ns-2 get pod -l app=sleep -o jsonpath={.items..metadata.name})" -c sleep -- curl -sIL http://httpbin.app-ns-3.svc.cluster.local:8000 HTTP/1.1 200 OK server: envoy date: Thu, 22 Dec 2022 15:01:36 GMT content-type: text/html; charset=utf-8 content-length: 9593 access-control-allow-origin: * access-control-allow-credentials: true x-envoy-upstream-service-time: 3
Cleanup
Clean up the first usergroup:
$ istioctl uninstall --revision usergroup-1 --set values.global.istioNamespace=usergroup-1 $ kubectl delete ns app-ns-1 usergroup-1
Clean up the second usergroup:
$ istioctl uninstall --revision usergroup-2 --set values.global.istioNamespace=usergroup-2 $ kubectl delete ns app-ns-2 app-ns-3 usergroup-2