Istio Training 13 πŸ”¬ Multi-cluster Istio

Install an Istio mesh across multiple Kubernetes clusters.

Multi-cluster Istio

In this lab, we explore some of Istio's multi-cluster capabilities. Istio actually has a few different approaches to multi-cluster as you can see in the documentation but we recommend you chose an approach that favors running multiple control planes (starting with one per cluster and optimizing from there).

Multi-cluster service-mesh architectures are extremely important to enable things like high availability, failover, isolation, and regulatory/compliance. We have found the teams that chose to run a single large cluster with a single Istio control plane (or try run a single Istio control plane across multiple clusters) have issues with tenancy, blast radius, and overall stability of their platform long-term. If you run multiple clusters, you will likely need your service mesh deployed accordingly.

In the model we explore here in this lab, we will deploy an individual Istio control plane per cluster and then connect them through a remote-access protocol.

This is the approach suggested in the Multi-Primary documentation on the Istio website, however, THERE ARE SOME SERIOUS DRAWBACKS TO THIS APPROACH, specifically in the area of security posture.

We will walk through this set up and then discuss the drawbacks at the end.

Let's dive in!

A multi-cluster environment

In this lab, we have 3 clusters configured for you. The contexts are: cluster1, cluster2, and cluster3. *Commands in this challenge are run in the Cluster 1 tab unless otherwise directed.

Installing Istio across multiple clusters

Create Namespaces and Apply Network Labels

Let's install Istio across multiple clusters. Note, the various clusters are referred to as $CLUSTER1, $CLUSTER2, and $CLUSTER3.

We will create the namespaces for each of the installations into their respective clusters. Note, we will also label the namespaces with an indication of which network to which they belong. Istio uses a network designator to know whether a service communication can (or should) cross a network boundary. Perform the following prep for our installation:

kubectl --context ${CLUSTER1} create ns istio-system
kubectl --context ${CLUSTER1} label namespace istio-system topology.istio.io/network=network1

kubectl --context ${CLUSTER2} create ns istio-system
kubectl --context ${CLUSTER2} label namespace istio-system topology.istio.io/network=network2

kubectl --context ${CLUSTER3} create ns istio-system
kubectl --context ${CLUSTER3} label namespace istio-system topology.istio.io/network=network3

Install secrets

Before we actually do the installation of Istio, we'll make sure to use an intermediate signing certificate rooted in the same Root CA (see Challenge 04 [Certificate Rotation] for more on certificates and rotation). We will set up these secrets ahead of time named cacerts in the istio-system namespace.

kubectl --context ${CLUSTER1} create secret generic cacerts -n istio-system \
--from-file=labs/05/certs/cluster1/ca-cert.pem \
--from-file=labs/05/certs/cluster1/ca-key.pem \
--from-file=labs/05/certs/cluster1/root-cert.pem \
--from-file=labs/05/certs/cluster1/cert-chain.pem

kubectl --context ${CLUSTER2} create secret generic cacerts -n istio-system   \
--from-file=labs/05/certs/cluster2/ca-cert.pem  \
--from-file=labs/05/certs/cluster2/ca-key.pem  \
--from-file=labs/05/certs/cluster2/root-cert.pem \
--from-file=labs/05/certs/cluster2/cert-chain.pem

kubectl --context ${CLUSTER3} create secret generic cacerts -n istio-system  \
--from-file=labs/05/certs/cluster3/ca-cert.pem  \
--from-file=labs/05/certs/cluster3/ca-key.pem  \
--from-file=labs/05/certs/cluster3/root-cert.pem \
--from-file=labs/05/certs/cluster3/cert-chain.pem

When the Istio control plane comes up in each of these clusters, it will use this intermediate signing CA to issue workload certificates to each of the running applications and associated sidecar. Although the workloads in each of the clusters is issuing leaf certificates signed by different signing CAs, they are all rooted in the same root CA, so traffic can be verified and trusted.

Install Istio with East-West Gateway (ew-gw)

Now that we've correctly configured the namespaces and the signing certificates for each control plane. We will use the istioctl CLI to first install the IstioOperator for the Control Plane, then we'll again use istioctl to install the east-west gateway, and lastly configure the Gateway resource to expose the east-west gateway.

istioctl install -y --context ${CLUSTER1} -f labs/05/istio/cluster1.yaml
istioctl install -y --context ${CLUSTER1} -f labs/05/istio/ew-gateway1.yaml
kubectl --context=${CLUSTER1} apply -n istio-system -f labs/05/istio/expose-services.yaml

You can see we install the Istio control plane as well as an east-west gateway that will be used to connect traffic between clusters. You can run the following command to verify that these components came up successfully:

kubectl --context ${CLUSTER1} get pods -n istio-system
# OUTPUT
NAME                                     READY   STATUS    RESTARTS   AGE
istio-eastwestgateway-85c5855c76-6swpx   1/1     Running   0          131m
istiod-76ddb688f7-2trh9                  1/1     Running   0          132m

If that looks good, let's finish by doing the same installation to clusters 2 and 3:

istioctl install -y --context ${CLUSTER2} -f labs/05/istio/cluster2.yaml
istioctl install -y --context ${CLUSTER2} -f labs/05/istio/ew-gateway2.yaml
kubectl --context=${CLUSTER2} apply -n istio-system -f labs/05/istio/expose-services.yaml

istioctl install -y --context ${CLUSTER3} -f labs/05/istio/cluster3.yaml
istioctl install -y --context ${CLUSTER3} -f labs/05/istio/ew-gateway3.yaml
kubectl --context=${CLUSTER3} apply -n istio-system -f labs/05/istio/expose-services.yaml

At this point, all we've done is installed separate Istio control planes into each of the clusters. We could deploy workloads into each of the clusters but they would be isolated in the mesh. Let's see what steps we need to connect the various control planes and service-mesh networks.

Set up Endpoint Discovery

In this section, we will use the istioctl CLI to automate setting up connectivity between each of the clusters. We do this by running istioctl x create-remote-secret for each of the other peer clusters. Let's run the following commands and then explore what happened:

istioctl x create-remote-secret --context=${CLUSTER2} --name=cluster2 | kubectl apply -f - --context=${CLUSTER1}
istioctl x create-remote-secret --context=${CLUSTER3} --name=cluster3 | kubectl apply -f - --context=${CLUSTER1}

These two commands will create a secret for each of cluster 2 and cluster 3 and store it into the istio-system namespace in cluster 1. After running the above commands, let's verify what was created:

kubectl --context ${CLUSTER1} get secret -n istio-system
# OUTPUT
NAME                                           TYPE                                  DATA   AGE
cacerts                                        Opaque                                4      135m
default-token-npwbd                            kubernetes.io/service-account-token   3      135m
istio-eastwestgateway-service-account-token    kubernetes.io/service-account-token   3      135m
istio-reader-service-account-token-pwf7h       kubernetes.io/service-account-token   3      135m
istio-remote-secret-cluster2                   Opaque                                1      134m
istio-remote-secret-cluster3                   Opaque                                1      134m
istiod-service-account-token-gqlwc             kubernetes.io/service-account-token   3      135m
istiod-token-n6hs8                             kubernetes.io/service-account-token   3      135m

You can see a secret called istio-remote-secret-cluster2 and istio-remote-secret-cluster3. Let's take a look at one of these secrets:

kubectl --context ${CLUSTER1} get secret istio-remote-secret-cluster2 -n istio-system -o yaml

Woah! That's a lot of... hidden stuff... let's take a look at what is in the cluster2 key in the secret:

kubectl --context ${CLUSTER1} get secret istio-remote-secret-cluster2 -n istio-system -o jsonpath="{.data.cluster2}" | base64 --decode

You should see an output like this (with the keys/certs redacted in this listing for brevity):

apiVersion: v1
clusters:
- cluster:
    certificate-authority-data:
 < CERT DATA REDACTED >
    server: https://10.128.0.37:7002
  name: cluster2
contexts:
- context:
    cluster: cluster2
    user: cluster2
  name: cluster2
current-context: cluster2
kind: Config
preferences: {}
users:
- name: cluster2
  user:
    token: < TOKEN DATA REDACTED >

Interesting... isn't this a bit familiar??

If you said "this is a kubeconfig file for accessing a cluster", you would be correct!

For this form of multi-cluster, Istio creates a kubeconfig for each of the remote clusters with direct access to the Kubernetes API of the remote cluster. We will come back to this at the end of the lab, but for now, let's continue enabling endpoint discovery (through the Kubernetes API) for each of the other clusters:

istioctl x create-remote-secret --context=${CLUSTER1} --name=cluster1 | kubectl apply -f - --context=${CLUSTER2}
istioctl x create-remote-secret --context=${CLUSTER3} --name=cluster3 | kubectl apply -f - --context=${CLUSTER2}

istioctl x create-remote-secret --context=${CLUSTER1} --name=cluster1 | kubectl apply -f - --context=${CLUSTER3}
istioctl x create-remote-secret --context=${CLUSTER2} --name=cluster2 | kubectl apply -f - --context=${CLUSTER3}

At this point, multi-cluster service and endpoint discovery has been enabled. We don't have any sample apps deployed, yet, so let's take a look at that:

Set Up Sample Applications

Let's set up a couple of sample applications to test our multi-cluster connectivity. First, let's create the right namespaces and label them for injection on all of the clusters:

kubectl --context ${CLUSTER1} create ns sample
kubectl label --context=${CLUSTER1} namespace sample istio-injection=enabled

kubectl --context ${CLUSTER2} create ns sample
kubectl label --context=${CLUSTER2} namespace sample istio-injection=enabled

kubectl --context ${CLUSTER3} create ns sample
kubectl label --context=${CLUSTER3} namespace sample istio-injection=enabled

Next, let's deploy services into each of the clusters:

kubectl apply --context=${CLUSTER1} -f labs/05/istio/helloworld.yaml -l service=helloworld -n sample
kubectl apply --context=${CLUSTER2} -f labs/05/istio/helloworld.yaml -l service=helloworld -n sample
kubectl apply --context=${CLUSTER3} -f labs/05/istio/helloworld.yaml -l service=helloworld -n sample

kubectl apply --context=${CLUSTER1} -f labs/05/istio/helloworld.yaml -l version=v1 -n sample
kubectl apply --context=${CLUSTER2} -f labs/05/istio/helloworld.yaml -l version=v1 -n sample
kubectl apply --context=${CLUSTER3} -f labs/05/istio/helloworld.yaml -l version=v2 -n sample

Lastly, let's deploy a sample "sleep" application into each cluster that we will use as the "client" to connect to services across clusters:

kubectl apply --context=${CLUSTER1} -f labs/05/istio/sleep.yaml -n sample
kubectl apply --context=${CLUSTER2} -f labs/05/istio/sleep.yaml -n sample
kubectl apply --context=${CLUSTER3} -f labs/05/istio/sleep.yaml -n sample

From the sleep client in cluster 1, let's make a call to the helloworld service:

kubectl exec --context ${CLUSTER1} -n sample -c sleep deploy/sleep -- curl -sS helloworld.sample:5000/hello
# OUTPUT
Hello version: v1, instance: helloworld-v1-776f57d5f6-b946k

You should see the helloworld that's deployed in the same cluster respond. Let's take a look at the endpoints that the sleep pod's Istio service proxy knows about

istioctl --context $CLUSTER1 pc endpoints deploy/sleep.sample --cluster "outbound|5000||helloworld.sample.svc.cluster.local"
# OUTPUT
ENDPOINT               STATUS      OUTLIER CHECK     CLUSTER
10.132.0.102:15443     HEALTHY     OK                outbound|5000||helloworld.sample.svc.cluster.local
10.132.0.112:15443     HEALTHY     OK                outbound|5000||helloworld.sample.svc.cluster.local
10.42.0.16:5000        HEALTHY     OK                outbound|5000||helloworld.sample.svc.cluster.local

You can see from this output that Istio's service proxy knows about three endpoints for the helloworld service. One of the endpoints is local (10.42.x.x) while two of them are remote (10.132.x.x). What are those remote IP addresses?

Those are the External IP addresses of the east-west gateways in cluster 2 and cluster 3! Let's verify:

Cluster 2:

kubectl --context $CLUSTER2 get svc -n istio-system
# OUTPUT
NAME                    TYPE           CLUSTER-IP     EXTERNAL-IP    PORT(S)                                                           AGE
istiod                  ClusterIP      10.43.26.28    <none>         15010/TCP,15012/TCP,443/TCP,15014/TCP                             4m19s
istio-eastwestgateway   LoadBalancer   10.43.146.20   10.132.0.102   15021:31687/TCP,15443:30582/TCP,15012:32477/TCP,15017:30127/TCP   4m4s

Cluster 3:

kubectl --context $CLUSTER3 get svc -n istio-system
# OUTPUT
NAME                    TYPE           CLUSTER-IP      EXTERNAL-IP    PORT(S)                                                           AGE
istiod                  ClusterIP      10.43.210.62    <none>         15010/TCP,15012/TCP,443/TCP,15014/TCP                             4m1s
istio-eastwestgateway   LoadBalancer   10.43.159.182   10.132.0.112   15021:30165/TCP,15443:31056/TCP,15012:32060/TCP,15017:32290/TCP   3m48s

Istio has discovered helloworld services running on cluster 2 and cluster 3 and has correctly configured the remote endpoints as the east-west gateways in the remote clusters. The east-west gateway in the respective clusters can then route to the local helloworld services and maintain an end-to-end mTLS connection. Let's verify the connectivity works as we expect:

Verify Installation

For each cluster, let's make a call to the helloworld service a few times and watch the load get balanced:

echo "Calling from cluster 1"
for i in {1..10}
do
    kubectl exec --context ${CLUSTER1} -n sample -c sleep deploy/sleep -- curl -sS helloworld.sample:5000/hello
done
echo "Calling from cluster 2"
for i in {1..10}
do
    kubectl exec --context ${CLUSTER2} -n sample -c sleep deploy/sleep -- curl -sS helloworld.sample:5000/hello
done
echo "Calling from cluster 3"
for i in {1..10}
do
    kubectl exec --context ${CLUSTER3} -n sample -c sleep deploy/sleep -- curl -sS helloworld.sample:5000/hello
done

for i in {1..10}
do
    kubectl exec --context ${CLUSTER3} -n sample -c sleep deploy/sleep -- curl -sS helloworld.sample:5000/hello
done
Calling from cluster 3
Hello version: v1, instance: helloworld-v1-776f57d5f6-sbc2j
Hello version: v2, instance: helloworld-v2-54df5f84b-4vdjq
Hello version: v2, instance: helloworld-v2-54df5f84b-4vdjq
Hello version: v1, instance: helloworld-v1-776f57d5f6-5tr8n
Hello version: v1, instance: helloworld-v1-776f57d5f6-sbc2j
Hello version: v2, instance: helloworld-v2-54df5f84b-4vdjq
Hello version: v1, instance: helloworld-v1-776f57d5f6-5tr8n
Hello version: v1, instance: helloworld-v1-776f57d5f6-5tr8n
Hello version: v1, instance: helloworld-v1-776f57d5f6-sbc2j
Hello version: v1, instance: helloworld-v1-776f57d5f6-sbc2j


root@cluster1:~/istio-workshops/istio-expert# k get po -A --context ${CLUSTER3} | grep helloworld
default                helloworld-f85896cd8-g8x4d                   2/2     Running            0          144m
sample                 helloworld-v2-54df5f84b-4vdjq                2/2     Running            0          6m10s
root@cluster1:~/istio-workshops/istio-expert# k get po -A --context ${CLUSTER2} | grep helloworld
sample                 helloworld-v1-776f57d5f6-sbc2j               2/2     Running   0          6m25s
root@cluster1:~/istio-workshops/istio-expert# k get po -A --context ${CLUSTER1} | grep helloworld
sample                 helloworld-v1-776f57d5f6-5tr8n               2/2     Running   0          6m30s

Drawbacks to this Approach

In this lab we explored one of Istio's multi-cluster approaches called "Multi-Primary" with endpoint discovery. As we saw, with this approach we create tokens/kube-configs in each of the clusters to give direct Kubernetes API access to each of the remote clusters. We have found organizations are hesitant to give direct Kubernetes API access to each of the clusters in a multi-cluster deployment. The access is "read-only" (for the most part.. not completely) and is a major risk: what if one of the clusters gets compromised? Do they have access to all of the other clusters in the fleet?

Another drawback we've seen with this approach is the lack of control over what constitutes a service across multiple clusters. Using the endpoint-discovery mechanism, services are matched based on name and must be in the same namespace. What if services don't run in the same namespace across clusters? This scenario is quite common, and would not work for the approach to multi-cluster as presented here.

At Solo.io, we have built our service mesh using Istio but NOT relying on this endpoint-discovery model. We do use the multi-primary control plane deployment, but instead of proliferating Kubernetes credentials to every cluster, we have a global service-discovery mechanism that uses information to then inform Istio which services to federate and how. This approach uses ServiceEntrys and does not rely on sharing Kubernetes credentials across clusters. Additionally, with Gloo Mesh, you get more control over what services are included in a "virtual destination" by specifying labels/namespaces/clusters and services can reside in other namespaces. We also do a lot of the certificate federation for you and handle rotation so to minimize the operational burden. Please see the Gloo Mesh workshop for more details about how this works.

What's Next?

Thanks for joining our Advanced Istio Workshop for Operationalizing Istio for Day 2! We hope it was able to show you powerful ways of leveraging Istio in your environment. That's the the end, however. The next step is to go to the next level and see what best practices for operating Istio in multi-tenant, multi-cluster architectures look like with Gloo Mesh. Please register for an upcoming Gloo Mesh workshop which will show how to simplify the operations and usage of Istio.