Deploy a Multi-Data Center Cassandra Cluster in Kubernetes

Recently, we’ve been examining different patterns for deploying Apache Cassandra clusters on Kubernetes (K8s) using K8ssandra with different deployment topologies. We’ve explored how to deploy a Cassandra cluster with two data centers in a single cloud region using separate K8s namespaces to isolate operational and analytics workloads.
We also deployed a Cassandra cluster across K8s clusters in multiple Google Cloud Platform (CGP) regions. One of the shortcomings of this approach was the way we handled the networking. Specifically, we used hard-coded IPs of Cassandra nodes in the first data center to bootstrap the second data center using what Cassandra calls “seed nodes.” Using hard-coded IPs is a more labor-intensive solution and doesn’t scale well. What we really need is to be able to locate seed nodes with a domain name server (DNS).
Here, you’ll learn how to create a Cassandra cluster spanning Google Kubernetes Engine (GKE) clusters in two Google Cloud regions using Google’s Cloud DNS Service to provide name resolution between the two GKE clusters. For the purpose of this exercise, we want to use the same network. So, we’ll create GKE clusters in two separate regions under the same Google Cloud project.
A quick note on terminology. The word “cluster” is used a lot in distributed technology, and it can get confusing fast when there are two different distributed technologies working together. A Cassandra cluster can span multiple data centers and presents itself as a single entity to the external user, no matter which data center is being accessed. Kubernetes considers a cluster to be a local data center and requires a network interconnect between data centers to make it multicluster. Combining these two concepts, a single Cassandra cluster will span multiple Kubernetes clusters. Hopefully that helps to clarify some of the terminology. With that, let’s get started.
Preparing the First GKE Cluster
First, you’re going to need a K8s cluster in which you can create the first Cassandra data center. We’ll show how to do this with the gcloud
command line. For the commands in this post, we’ll assume your context is set to have a current project, region and zone. You can check what values are set by running something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
% gcloud config list [compute] region = us-west4 zone = us-west4-a [core] account = j.c@abc.com disable_usage_reporting = False project = my-project |
First, you’ll need a network:
1 |
gcloud compute networks create dev-k8ssandra-network |
Then, to create your first GKE cluster, you’ll need a subnet in the zone we plan to use. I chose us-west4
.
1 |
gcloud compute networks subnets create dev-k8ssandra-subnet <a href="https://cloud.google.com/sdk/gcloud/reference/compute/networks/subnets/create#--network">--network</a>=dev-k8ssandra-network <a href="https://cloud.google.com/sdk/gcloud/reference/compute/networks/subnets/create#--range">--range</a>=10.1.0.0/20 --region=us-west4 |
Now you can create a GKE cluster using that subnet and compute specs that meet the K8ssandra minimum requirements. Here’s what my command looked like using zones in the us-west4
region:
1 |
gcloud beta container clusters create "k8ssandra" --region "us-west4" --machine-type "e2-highmem-8" --disk-type "pd-standard" --disk-size "100" --num-nodes "1" --network dev-k8ssandra-network --subnetwork dev-k8ssandra-subnet --node-locations "us-west4-a","us-west4-b","us-west4-c" --cluster-dns clouddns --cluster-dns-scope vpc --cluster-dns-domain cluster1 |
Note also the --cluster-dns*
attributes, which configure the new cluster to use the Cloud DNS service with virtual private cloud (VPC) level scoping, and the domain cluster1
, which we’ll be able to use below for name resolution. For more information on this configuration, see the documentation for using Cloud DNS for GKE.
This should change your kubectl
context to the new cluster, but you can make sure by checking the output of kubectl config current-context
.
Note: If you’d rather use Terraform scripts to create your GKE cluster, the K8ssandra project documentation includes instructions for K8ssandra on Google Kubernetes Engine (GKE), which references sample scripts provided as part of the K8ssandra GCP Terraform Example.
Creating the First Cassandra Data Center
Now you’re ready to create the first Cassandra data center. First, you’ll create Cassandra administrator credentials. Create a namespace for the first data center and add a secret within the namespace:
1 2 3 |
kubectl create namespace k8ssandra kubectl create secret generic cassandra-admin-secret --from-literal=username=cassandra-admin --from-literal=password=cassandra-admin-password -n k8ssandra |
The next step is to create a K8ssandra deployment for the first data center. You’ll need Helm installed for this step, as described on the K8ssandra GKE docs page. Create the configuration for the first data center in a file called dc1.yaml
, making sure to change the affinity labels to match zones used in your GKE cluster:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
cassandra: <strong>auth:</strong> <strong>superuser:</strong> <strong>secret: cassandra-admin-secret</strong> cassandraLibDirVolume: <strong>storageClass: standard-rwo</strong> clusterName: multi-region datacenters: - name: dc1 size: 3 racks: - name: rack1 affinityLabels: <strong>failure-domain.beta.kubernetes.io/zone: us-west4-a</strong> - name: rack2 affinityLabels: <strong>failure-domain.beta.kubernetes.io/zone: us-west4-b</strong> - name: rack3 affinityLabels: <strong>failure-domain.beta.kubernetes.io/zone: us-west4-c</strong> |
In addition to requesting three nodes in the data center, this configuration specifies an appropriate storage class for the GKE environment (standard-rwo
) and uses affinity to specify how the racks are mapped to GCP zones. Make sure to change the referenced zones to match your configuration. For more details, please refer to this blog post.
Deploy the release using this command:
1 |
helm install k8ssandra k8ssandra/k8ssandra -f dc1.yaml -n k8ssandra |
This causes the K8ssandra release named k8ssandra
to be installed in the namespace k8ssandra
.
As would be the case for any Cassandra cluster deployment, you’ll want to wait for the first data center to be completely up before adding a second data center. Since you’ll now be creating additional infrastructure for the second data center, you probably don’t need to wait. But if you’re interested, one simple way to make sure the data center is up is to watch until the Stargate pod shows as initialized, since it depends on Cassandra being ready:
1 2 3 4 5 |
kubectl get pods -n k8ssandra kubectl get pods -n us-west4 --watch --selector app=k8ssandra-dc1-stargate NAME READY STATUS RESTARTS AGE k8ssandra-dc1-stargate-58bf5657ff-ns5r7 1/1 Running 0 15m |
This is a great point to get some information you’ll need below to configure the second Cassandra data center: seeds. Conveniently for us, K8ssandra creates a headless K8s service called the seed service, which points to a couple of the Cassandra nodes that can be used to bootstrap new nodes or data centers into a Cassandra cluster:
1 2 3 4 5 6 7 8 9 10 |
% kubectl get svc -n k8ssandra --selector cassandra.datastax.com/cluster=multi-region NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE multi-region-dc1-all-pods-service ClusterIP None <none> 9042/TCP,8080/TCP,9103/TCP 23m multi-region-dc1-service ClusterIP None <none> 9042/TCP,9142/TCP,8080/TCP,9103/TCP,9160/TCP 23m multi-region-seed-service ClusterIP None <none> <none> 23m |
If you’re curious, you can get a quick look at the IP addresses of the pods behind this service that are labeled as seed nodes using the same selector that the service uses:
1 |
kubectl get pods -n k8ssandra -o jsonpath="{.items[*].status.podIP}" --selector cassandra.datastax.com/seed-node=true |
Which produces output that looks like this:
1 |
10.240.0.8 10.240.2.6 10.240.1.10 |
Preparing the Second GKE Cluster
Now you’ll need a second K8s cluster that will be used to host the second Cassandra data center in a different region. For example, I chose the us-central1
region for my second cluster. First, I explicitly created a subnet in that region as part of the dev-k8ssandra-network
.
1 |
gcloud compute networks subnets create dev-k8ssandra-subnet2 <a href="https://cloud.google.com/sdk/gcloud/reference/compute/networks/subnets/create#--network">--network</a>=dev-k8ssandra-network <a href="https://cloud.google.com/sdk/gcloud/reference/compute/networks/subnets/create#--range">--range</a>=10.2.0.0/20 --region=us-central1 |
Then I created the second GKE cluster using that network and the same compute specs as the first cluster:
1 |
gcloud beta container clusters create "k8ssandra-2" --region "us-central1" --machine-type "e2-highmem-8" --disk-type "pd-standard" --disk-size "100" --num-nodes "1" --network dev-k8ssandra-network --subnetwork dev-k8ssandra-subnet2 --node-locations "us-central1-b","us-central1-c","us-central1-f" --cluster-dns clouddns --cluster-dns-scope vpc --cluster-dns-domain cluster2 |
Make sure the kubectl
context has changed to the second data center.
Enabling Traffic Between Kubernetes Clusters
Next, you’ll need to create a firewall rule to allow traffic between the two clusters. Recall the IP space of the subnets you defined above (I used 10.0.0.0/20,10.2.0.0/20
), and then obtain the IP spaces of each GKE cluster. For example:
1 2 3 4 5 6 7 8 9 |
% gcloud beta container clusters describe k8ssandra --zone us-west4 | grep clusterIpv4Cidr clusterIpv4Cidr: <strong>10.240.0.0/14</strong> % gcloud beta container clusters describe k8ssandra-2 --zone us-central1 | grep clusterIpv4Cidr clusterIpv4Cidr: <strong>10.24.0.0/14</strong> |
Use these IP spaces to create a rule to allow all traffic:
1 |
gcloud compute firewall-rules create k8ssandra-multi-region-rule --direction=INGRESS --network=dev-k8ssandra-network --action=ALLOW --rules=all --source-ranges=10.0.0.0/20,10.2.0.0/20,10.240.0.0/14,10.24.0.0/14 |
If desired, you could create a more targeted rule to only allow transmission control protocol (TCP) traffic between ports used by Cassandra.
Adding a Second Cassandra Data Center
Let’s start by creating a namespace for the new data center matching the GCP region name. We also need to create administrator credentials to match those created for the first data center, since the secrets are not automatically replicated between clusters.
1 2 3 |
kubectl create namespace k8ssandra kubectl create secret generic cassandra-admin-secret --from-literal=username=cassandra-admin --from-literal=password=cassandra-admin-password -n k8ssandra |
Now you’ll create a configuration to deploy an additional Cassandra data center, dc2
, in the new GKE cluster. For the nodes in dc2
to be able to join the Cassandra cluster, a few steps are required:
- You’ve already taken care of the first one: using the same Google Cloud network for both GKE clusters means the nodes in the new data center will be able to communicate with nodes in the original data center.
- Second, make sure to use the same Cassandra cluster name as for the first data center.
- Finally, you’ll need to provide the fully qualified name of the seed service so that nodes in the new data center know how to contact nodes in the first data center to join the cluster.
This last step is where Google Cloud DNS is doing its work for us. According to the Kubernetes documentation, the fully qualified domain name (FQDN) of a service follows the pattern:
1 |
my-svc.my-namespace.svc.cluster-domain.example |
For our purposes, the service name is multi-region-seed-service
, the namespace is k8ssandra
, and the domain is the same DNS domain we assigned the GKE cluster in Google Cloud DNS: cluster1
. Therefore, the FQDN we need is:
multi-region-seed-service.k8ssandra.svc.cluster1
Now create a configuration in a file called dc2.yaml
. Here’s what my file looked like with the FQDN for the seed service. Make sure to change the affinity labels appropriately for your chosen region and zones:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
cassandra: auth: superuser: secret: cassandra-admin-secret <strong>additionalSeeds: [ multi-region-seed-service.k8ssandra.svc.cluster1 ]</strong> cassandraLibDirVolume: storageClass: standard-rwo clusterName: multi-region datacenters: - name: dc2 size: 3 racks: - name: rack1 affinityLabels: failure-domain.beta.kubernetes.io/zone: us-central1-f - name: rack2 affinityLabels: failure-domain.beta.kubernetes.io/zone: us-central1-b - name: rack3 affinityLabels: failure-domain.beta.kubernetes.io/zone: us-central1-c |
Similar to the configuration for dc1, this configuration also uses affinity. A similar allocation of racks can be used to make sure Cassandra nodes are evenly spread across the remaining workers. Deploy the release using a command such as this:
helm install k8ssandra k8ssandra/k8ssandra -f dc2.yaml -n k8ssandra
If you look at the resources in this namespace, using a command such as kubectl get services,pods
, you’ll note that there are a similar set of pods and services as for dc1
, including Stargate, Prometheus, Grafana and Reaper. Depending on how you wish to manage your application, this may or may not be to your liking, but you are free to tailor the configuration to disable any components you don’t need.
Configuring Cassandra Keyspaces
Once the second data center is online, you’ll want to configure Cassandra keyspaces to replicate across both clusters.
Important: You’ll likely need to first change your kubectl
context back to the first GKE cluster, for example, using the kubectl config use-context
command. You can list existing contexts using kubectl config get-contexts
.
To update keyspaces, connect to a node in the first data center and execute cqlsh
.
kubectl exec multi-region-dc1-rack1-sts-0 cassandra -it -n k8ssandra -- cqlsh -u cassandra-admin -p cassandra-admin-password
Use the DESCRIBE KEYSPACES
to list the keyspaces and DESCRIBE KEYSPACE <name>command
to identify those using the NetworkTopologyStrategy
. For example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
cassandra: auth: superuser: secret: cassandra-admin-secret <strong>additionalSeeds: [ multi-region-seed-service.k8ssandra.svc.cluster1 ]</strong> cassandraLibDirVolume: storageClass: standard-rwo clusterName: multi-region datacenters: - name: dc2 size: 3 racks: - name: rack1 affinityLabels: failure-domain.beta.kubernetes.io/zone: us-central1-f - name: rack2 affinityLabels: failure-domain.beta.kubernetes.io/zone: us-central1-b - name: rack3 affinityLabels: failure-domain.beta.kubernetes.io/zone: us-central1-c |
Typically, you’ll find that the system_auth, system_traces
and system_distributed
keyspaces use NetworkTopologyStrategy,
as well as data_endpoint_auth
if you’ve enabled Stargate. You can then update the replication strategy to ensure data is replicated to the new data center. You’ll execute something like the following for each of these keyspaces:
ALTER KEYSPACE system_auth WITH replication = {'class': 'NetworkTopologyStrategy', 'dc1': 3, 'dc2': 3}
- Important: Remember to create or alter the replication strategy for any keyspaces you need for your application so that you have the desired number of replicas in each data center.
After exiting cqlsh
, make sure existing data is properly replicated to the new data center with the nodetool rebuild
command.
- Important: Remember to change your
kubectl context
back to the second GKE cluster.
Rebuild needs to be run on each node in the new data center. For example:
1 |
kubectl exec multi-region-dc2-rack1-sts-0 -n k8ssandra -- nodetool --username cassandra-admin --password cassandra-admin-password rebuild dc1 |
Repeat for the other nodes multi-region-dc2-rack2-sts-0
and multi-region-dc2-rack3-sts-0
.
Testing the Configuration
Let’s verify the second data center has joined the cluster. To do this, you’ll pick a Cassandra node to execute the nodetool status
command against. Execute the nodetool
command against the node:
1 |
kubectl exec multi-region-dc2-rack1-sts-0 -n k8ssandra cassandra -- nodetool --username cassandra-admin --password cassandra-admin-password status |
This will produce output similar to the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
Datacenter: dc1 =============== Status=Up/Down |/ State=Normal/Leaving/Joining/Moving -- Address Load Tokens Owns Host ID Rack UN 10.56.2.8 835.57 KiB 256 ? 8bc5cd4a-7953-497a-8ac0-e89c2fcc8729 rack1 UN 10.56.5.8 1.19 MiB 256 ? fdd96600-5a7d-4c88-a5cc-cf415b3b79f0 rack2 UN 10.56.4.7 830.98 KiB 256 ? d4303a9f-8818-40c2-a4b5-e7f2d6d78da6 rack3 Datacenter: dc2 =============== Status=Up/Down |/ State=Normal/Leaving/Joining/Moving -- Address Load Tokens Owns Host ID Rack UN 10.24.4.99 418.52 KiB 256 ? d2e71ab4-6747-4ac6-b314-eaaa76d3111e rack3 UN 10.24.7.37 418.17 KiB 256 ? 24708e4a-61fc-4004-aee0-6bcc5533a48f rack2 UN 10.24.1.214 398.22 KiB 256 ? 76c0d2ba-a9a8-46c0-87e5-311f7e05450a rack1 |
If everything has been configured correctly, you’ll be able to see both data centers in the cluster output. Here’s a picture that depicts what you’ve just deployed, focusing on the Cassandra nodes and networking:
What’s Next
We’re hard at work building a K8s operator for K8ssandra, which will help support multiple topologies beyond the one we’ve described here, and to do so more simply. If you’re interested in learning more about deploying Cassandra on K8s, or getting involved in the project, we encourage you to check out the K8ssandra project on GitHub, or the K8ssandra blog for other tutorials. Please feel free to reach out with any questions you have on the forum or our Discord channel.