Ever needed a robust, highly available MongoDB setup that spans multiple Kubernetes clusters on GCP? This step-by-step guide shows you how to deploy the Percona Operator for MongoDB in two GKE clusters, linking them using Multi-Cluster Services (MCS) for seamless cross-cluster discovery and connectivity.
1. Prepare your account for Multi-Cluster Services (MCS)
|
1 |
export PROJECT_ID=your_project_id<br><br>gcloud services enable <br> multiclusterservicediscovery.googleapis.com <br> gkehub.googleapis.com <br> cloudresourcemanager.googleapis.com <br> trafficdirector.googleapis.com <br> dns.googleapis.com<br> |
2. Create Two GKE Clusters
Use your preferred method (e.g., gcloud container clusters create) to set up main-cluster and replica-cluster. Workload Identity Federation is recommended by Google for MCS.
|
1 |
gcloud container clusters create main-cluster <br> --zone us-central1-a <br> --cluster-version 1.32 <br> --machine-type n1-standard-4 <br> --num-nodes=3 <br> --workload-pool=$PROJECT_ID.svc.id.goog<br> |
|
1 |
gcloud container clusters create replica-cluster <br> --zone us-central1-a <br> --cluster-version 1.32 <br> --machine-type n1-standard-4 <br> --num-nodes=3 <br> --workload-pool=$PROJECT_ID.svc.id.goog<br> |
3. Enable Multi-Cluster Services (MCS)
In your fleet host project, run:
|
1 |
gcloud container fleet multi-cluster-services enable --project $PROJECT_ID<br> |
4. Register the clusters to the fleet
|
1 |
gcloud container fleet memberships register main-cluster <br> --gke-cluster us-central1-a/main-cluster <br> --enable-workload-identity <br> |
|
1 |
gcloud container fleet memberships register replica-cluster <br> --gke-cluster us-central1-a/replica-cluster <br> --enable-workload-identity <br> |
5. Grant the required Identity and Access Management (IAM) permissions for MCS Importer
|
1 |
# get the fleet project number <br>PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format="value(projectNumber)")<br><br>gcloud projects add-iam-policy-binding $PROJECT_ID <br> --member "principal://iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$PROJECT_ID.svc.id.goog/subject/ns/gke-mcs/sa/gke-mcs-importer" <br> --role "roles/compute.networkViewer"<br> |
6. Verify that MCS is enabled
|
1 |
gcloud container fleet multi-cluster-services describe --project $PROJECT_ID<br> |
Install the Operator in each cluster (main Cluster and replica Cluster):
1. Generate a kubeconfig file for each GKE cluster
|
1 |
KUBECONFIG=./gcp-main_config gcloud container clusters get-credentials main-cluster --zone us-central1-a <br><br>KUBECONFIG=./gcp-replica_config gcloud container clusters get-credentials replica-cluster --zone us-central1-a <br> |
2. Give permissions to your account to manage the GKE clusters
|
1 |
kubectl --kubeconfig gcp-main_config create clusterrolebinding cluster-admin-binding --clusterrole cluster-admin --user $(gcloud config get-value core/account)<br><br>kubectl --kubeconfig gcp-replica_config create clusterrolebinding cluster-admin-binding --clusterrole cluster-admin --user $(gcloud config get-value core/account)<br> |
3. (Optional) Open one terminal to manage each cluster
Terminal 1:
|
1 |
kubectl --kubeconfig gcp-main_config config set-context $(kubectl config current-context)<br> |
Terminal 2:
|
1 |
kubectl --kubeconfig gcp-replica_config config set-context $(kubectl config current-context)<br> |
4. Create the same namespace on both clusters and install the Percona Operator for MongoDB.
Terminal 1:
|
1 |
kubectl create ns psmdb<br>kubectl config set-context --current --namespace=psmdb<br>kubectl apply --server-side -f https://raw.githubusercontent.com/percona/percona-server-mongodb-operator/v1.20.1/deploy/bundle.yaml<br> |
Terminal 2:
|
1 |
kubectl create ns psmdb<br>kubectl config set-context --current --namespace=psmdb<br>kubectl apply --server-side -f https://raw.githubusercontent.com/percona/percona-server-mongodb-operator/v1.20.1/deploy/bundle.yaml<br> |
Make sure to create it in the psmdb namespace and use ClusterIP services (which are required for MCS).
|
1 |
apiVersion: psmdb.percona.com/v1<br>kind: PerconaServerMongoDB<br>metadata:<br> name: main-cluster<br>spec:<br> crVersion: 1.20.1<br> image: percona/percona-server-mongodb:7.0.14-8-multi<br> updateStrategy: SmartUpdate<br> multiCluster:<br> enabled: true<br> DNSSuffix: svc.clusterset.local<br> upgradeOptions:<br> apply: disabled<br> schedule: "0 2 * * *"<br> secrets:<br> users: my-cluster-name-secrets<br> encryptionKey: my-cluster-name-mongodb-encryption-key<br> replsets:<br> - name: rs0<br> size: 3 <br> expose:<br> enabled: true<br> type: ClusterIP<br> volumeSpec:<br> persistentVolumeClaim:<br> resources:<br> requests:<br> storage: 3Gi <br><br> sharding:<br> enabled: true<br> configsvrReplSet:<br> size: 3<br> expose:<br> enabled: true<br> type: ClusterIP<br> volumeSpec:<br> persistentVolumeClaim:<br> resources:<br> requests:<br> storage: 3Gi<br><br> mongos:<br> size: 3<br> expose:<br> type: ClusterIP<br> |
|
1 |
kubectl apply -f cr-main.yml<br> |
|
1 |
kubectl get secret my-cluster-name-secrets -o yaml > my-cluster-secrets.yml<br>kubectl get secret main-cluster-ssl -o yaml > main-cluster-ssl.yml<br>kubectl get secret main-cluster-ssl-internal -o yaml > main-cluster-ssl-internal.yml<br>kubectl get secret my-cluster-name-mongodb-encryption-key -o yaml > my-cluster-name-mongodb-encryption-key.yml<br> |
You need to remove the fields:
|
1 |
ownerReferences<br>annotations<br>creationTimestamp<br>resourceVersion<br>selfLink<br>uid<br> |
The following helper scripts can be used:
|
1 |
yq eval 'del(.metadata.ownerReferences, .metadata.annotations, .metadata.creationTimestamp, .metadata.resourceVersion, .metadata.selfLink, .metadata.uid)' my-cluster-secrets.yml > my-cluster-secrets-replica.yaml<br>sed -i '' 's/main-cluster/replica-cluster/g' my-cluster-secrets-replica.yaml<br><br>yq eval 'del(.metadata.ownerReferences, .metadata.annotations, .metadata.creationTimestamp, .metadata.resourceVersion, .metadata.selfLink, .metadata.uid)' main-cluster-ssl.yml > replica-cluster-ssl.yml<br>sed -i '' 's/main-cluster/replica-cluster/g' replica-cluster-ssl.yml<br><br>yq eval 'del(.metadata.ownerReferences, .metadata.annotations, .metadata.creationTimestamp, .metadata.resourceVersion, .metadata.selfLink, .metadata.uid)' main-cluster-ssl-internal.yml > replica-cluster-ssl-internal.yml<br>sed -i '' 's/main-cluster/replica-cluster/g' replica-cluster-ssl-internal.yml<br><br>yq eval 'del(.metadata.ownerReferences, .metadata.annotations, .metadata.creationTimestamp, .metadata.resourceVersion, .metadata.selfLink, .metadata.uid)' my-cluster-name-mongodb-encryption-key.yml > my-cluster-name-mongodb-encryption-key2.yml<br><br>sed -i '' 's/main-cluster/replica-cluster/g' my-cluster-name-mongodb-encryption-key2.yml<br> |
Now create the modified secrets on the GKE replica cluster:
|
1 |
kubectl apply -f my-cluster-secrets-replica.yaml<br>kubectl apply -f replica-cluster-ssl.yml<br>kubectl apply -f replica-cluster-ssl-internal.yml<br>kubectl apply -f my-cluster-name-mongodb-encryption-key2.yml<br> |
Make sure to create it in the psmdb namespace and remember to set “unmanaged: true” and updateStrategy to RollingUpdate or OnDelete.
|
1 |
apiVersion: psmdb.percona.com/v1<br>kind: PerconaServerMongoDB<br>metadata:<br> name: replica-cluster<br>spec:<br> unmanaged: true<br> crVersion: 1.20.1<br> image: percona/percona-server-mongodb:7.0.14-8-multi<br> multiCluster:<br> enabled: true<br> DNSSuffix: svc.clusterset.local<br> updateStrategy: RollingUpdate<br> upgradeOptions:<br> apply: disabled<br> schedule: "0 2 * * *"<br> secrets:<br> users: my-cluster-name-secrets<br> encryptionKey: my-cluster-name-mongodb-encryption-key<br> ssl: replica-cluster-ssl<br> sslInternal: replica-cluster-ssl-internal<br> replsets:<br> - name: rs0<br> size: 3<br> expose:<br> enabled: true<br> type: ClusterIP<br> volumeSpec:<br> persistentVolumeClaim:<br> resources:<br> requests:<br> storage: 3Gi<br><br> sharding:<br> enabled: true<br> configsvrReplSet:<br> size: 3<br> expose:<br> enabled: true<br> type: ClusterIP<br> volumeSpec:<br> persistentVolumeClaim:<br> resources:<br> requests:<br> storage: 3Gi<br><br> mongos:<br> size: 1<br> expose:<br> type: ClusterIP<br> |
Run this step on both clusters:
|
1 |
kubectl get services<br>NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE<br>gke-mcs-51sqttasr2 ClusterIP 34.118.225.98 27017/TCP 8m1s<br>gke-mcs-5ujp4u9efu ClusterIP 34.118.236.255 27017/TCP 2m39s<br>gke-mcs-87ngrkrek2 ClusterIP 34.118.236.74 27017/TCP 10m<br>gke-mcs-8gvu7dss6j ClusterIP 34.118.233.222 27017/TCP 2m41s<br>gke-mcs-covcmpalsn ClusterIP 34.118.235.127 27017/TCP 10m<br>gke-mcs-f9h00il386 ClusterIP 34.118.238.35 27017/TCP 8m7s<br>gke-mcs-ksa245qkbn ClusterIP 34.118.231.205 27017/TCP 8m6s<br>gke-mcs-mhng4qml79 ClusterIP 34.118.231.157 27017/TCP 8m3s<br>gke-mcs-ukshfk963v ClusterIP 34.118.234.152 27017/TCP 10m<br>gke-mcs-v2opr5f985 ClusterIP 34.118.233.131 27017/TCP 2m44s<br>main-cluster-cfg ClusterIP None 27017/TCP 15m<br>main-cluster-cfg-0 ClusterIP 34.118.230.116 27017/TCP 15m<br>main-cluster-cfg-1 ClusterIP 34.118.239.70 27017/TCP 14m<br>main-cluster-cfg-2 ClusterIP 34.118.233.221 27017/TCP 14m<br>main-cluster-mongos ClusterIP 34.118.239.128 27017/TCP 15m<br>main-cluster-rs0 ClusterIP None 27017/TCP 15m<br>main-cluster-rs0-0 ClusterIP 34.118.229.138 27017/TCP 15m<br>main-cluster-rs0-1 ClusterIP 34.118.225.147 27017/TCP 14m<br>main-cluster-rs0-2 ClusterIP 34.118.237.74 27017/TCP 14m<br> |
If ServiceImports are missing, check the MCS controller logs and ensure ServiceExports were created by the operator. Replace the pod name below with your own operator pod:
|
1 |
kubectl get serviceimports<br>kubectl logs pod/percona-server-mongodb-operator-64976cdb47-2zdqj |
Using the service names we got from the previous step, add each node from the replica side to the main cluster. Perform this for every shard and the config server replica set.
|
1 |
apiVersion: psmdb.percona.com/v1<br>kind: PerconaServerMongoDB<br>metadata:<br> name: main-cluster<br>spec:<br> crVersion: 1.20.1<br> image: percona/percona-server-mongodb:7.0.14-8-multi<br> updateStrategy: SmartUpdate<br> multiCluster:<br> enabled: true<br> DNSSuffix: svc.clusterset.local<br> upgradeOptions:<br> apply: disabled<br> schedule: "0 2 * * *"<br> secrets:<br> users: my-cluster-name-secrets<br> encryptionKey: my-cluster-name-mongodb-encryption-key<br> replsets:<br> - name: rs0<br> size: 3<br> externalNodes:<br> - host: replica-cluster-rs0-0.psmdb.svc.clusterset.local<br> votes: 1<br> priority: 1<br> - host: replica-cluster-rs0-1.psmdb.svc.clusterset.local<br> votes: 1<br> priority: 1<br> - host: replica-cluster-rs0-2.psmdb.svc.clusterset.local<br> votes: 0<br> priority: 0<br> expose:<br> enabled: true<br> type: ClusterIP <br> volumeSpec:<br> persistentVolumeClaim:<br> resources:<br> requests:<br> storage: 3Gi<br><br> sharding:<br> enabled: true<br> configsvrReplSet:<br> size: 3<br> externalNodes:<br> - host: replica-cluster-cfg0-1.psmdb.svc.clusterset.local<br> votes: 1<br> priority: 1<br> - host: replica-cluster-cfg0-1.psmdb.svc.clusterset.local<br> votes: 1<br> priority: 1<br> - host: replica-cluster-cfg0-1.psmdb.svc.clusterset.local<br> votes: 0<br> priority: 0<br> expose:<br> enabled: true<br> type: LoadBalancer<br> volumeSpec:<br> persistentVolumeClaim:<br> resources:<br> requests:<br> storage: 3Gi<br><br> mongos:<br> size: 3<br> expose:<br> type: ClusterIP<br> |
|
1 |
kubectl apply -f cr-main-after.yml<br> |
Similarly to the previous step, edit the yaml file on the replica side, and add the main nodes as external:
|
1 |
replsets:<br> - name: rs0<br> size: 3<br> externalNodes:<br><br> - host: main-cluster-rs0-0.psmdb.svc.clusterset.local<br> votes: 1<br> priority: 1<br> - host: main-cluster-rs0-1.psmdb.svc.clusterset.local<br> votes: 1<br> priority: 1<br> - host: main-cluster-rs0-2.psmdb.svc.clusterset.local<br> votes: 0<br> priority: 0<br><br> sharding:<br> configsvrReplSet:<br> externalNodes:<br> - host: main-cluster-cfg-0.psmdb.svc.clusterset.local<br> votes: 1<br> priority: 1<br> - host: main-cluster-cfg-1.psmdb.svc.clusterset.local<br> votes: 1<br> priority: 1<br> - host: main-cluster-cfg-2.psmdb.svc.clusterset.local<br> votes: 0<br> priority: 0<br> |
Connect to a member of each replica set. I am using the config servers in the example:
|
1 |
kubectl exec -it main-cluster-cfg-0 -- /bin/bash<br>mongosh admin -u clusterAdmin -p ****<br> |
Verify all members are present (I’ve removed some fields from the output for readability)
|
1 |
rs.status().members<br>[<br> {<br> _id: 0,<br> name: 'main-cluster-cfg-0.psmdb.svc.clusterset.local:27017',<br> health: 1,<br> state: 1,<br> stateStr: 'PRIMARY',<br> },<br> {<br> _id: 1,<br> name: 'main-cluster-cfg-1.psmdb.svc.clusterset.local:27017',<br> health: 1,<br> state: 2,<br> stateStr: 'SECONDARY', <br> },<br> {<br> _id: 2,<br> name: 'main-cluster-cfg-2.psmdb.svc.clusterset.local:27017',<br> health: 1,<br> state: 2,<br> stateStr: 'SECONDARY',<br> },<br> {<br> _id: 3,<br> name: 'replica-cluster-cfg-0.psmdb.svc.clusterset.local:27017',<br> health: 1,<br> state: 2,<br> stateStr: 'SECONDARY',<br> },<br> {<br> _id: 4,<br> name: 'replica-cluster-cfg-1.psmdb.svc.clusterset.local:27017',<br> health: 1,<br> state: 2,<br> stateStr: 'SECONDARY', <br> },<br> {<br> _id: 5,<br> name: 'replica-cluster-cfg-2.psmdb.svc.clusterset.local:27017',<br> health: 1,<br> state: 2,<br> stateStr: 'SECONDARY', }]<br> |
Set the main cluster to unmanaged by editing the yaml file and applying the changes:
|
1 |
vi cr-main.yml<br> |
|
1 |
spec:<br> unmanaged: true<br> updateStrategy: RollingUpdate<br> |
|
1 |
kubectl exec -f cr-main.yml<br> |
Now set the replica cluster to managed, so the operator assumes control of the nodes on the replica side:
|
1 |
vi cr-replica.yml<br> |
|
1 |
spec:<br>unmanaged: false<br> |
|
1 |
kubectl exec -f cr-replica.yml<br> |
Verify a new primary was elected in the replica side
|
1 |
rs.status().members<br>[<br> {<br> _id: 0,<br> name: 'main-cluster-cfg-0.psmdb.svc.clusterset.local:27017',<br> health: 1,<br> state: 2,<br> stateStr: 'SECONDARY',<br> },<br>...<br> },<br> {<br> _id: 3,<br> name: 'replica-cluster-cfg-0.psmdb.svc.clusterset.local:27017',<br> health: 1,<br> state: 1,<br> stateStr: 'PRIMARY',<br> },<br>...<br>]<br> |
By combining the power of Percona Operator for MongoDB with GKE’s Multi-Cluster Services, you gain a resilient, scalable, and multi-region replica set architecture. Perfect for high-availability applications and disaster recovery use cases.
Resources
RELATED POSTS