Skip to content
Tech Blog
Go back

Building a Local Karmada Failover Demo: Multi-Cluster Kubernetes Traffic Switching on One Host

Edit page

Building a Local Karmada Failover Demo: Multi-Cluster Kubernetes Traffic Switching on One Host

Introduction

In this article I will walk through a complete local Karmada demo environment that runs on a single Linux host and demonstrates Kubernetes workload failover between two member clusters.

The goal is not just to install Karmada, but to build something visual enough to prove that traffic really moves between clusters:

I used this setup to demonstrate a simple but useful high-availability story:

  1. traffic starts on member1
  2. the browser always uses the same URL on the host
  3. member1 fails
  4. traffic switches to member2
  5. the page shows that the backend changed, while the probe timeline keeps moving

This is not a stateful migration demo. There is no shared database and no persistent session replication. Instead, this is a stateless failover demo designed to make cluster switching easy to observe.

The final blog output and demo assets are based on a live environment under:


What We Are Building

The final topology is:

The browser only talks to the host proxy. The proxy sends traffic to member1 first and keeps member2 as a hot standby. When member1 fails, traffic is switched to member2.


Why Karmada for This Demo

Karmada is useful here because it gives us a real multi-cluster control plane:

In this demo, Karmada manages workload distribution, while HAProxy provides the single stable ingress point for the user.

That division of responsibility is intentional:

This makes the demo honest and easy to reason about.


Host Environment

The environment used for this demo was a single Rocky Linux host with Docker enabled. The host already had the Karmada repository checked out at:

/root/karmada

The following tools were required:

For the Karmada repository version used in this environment, Go 1.25.7 was installed.


A Critical Fix for kind on This Host

Before I could reliably run Karmada on kind clusters, I hit a host-level issue:

I fixed that by adding:

# /etc/sysctl.d/99-karmada-kind.conf
fs.inotify.max_user_instances = 8192
fs.inotify.max_user_watches = 1048576

The actual file on this host is:

Apply it immediately with:

sysctl --system

If you skip this and kind networking behaves strangely, fix this first.


Step 1: Clone the Karmada Repository

git clone https://github.com/karmada-io/karmada.git /root/karmada
cd /root/karmada

This demo uses Karmada’s own repository both for control plane deployment and for the sample application assets.


Step 2: Create Three kind Clusters

I did not use the default local-up-karmada.sh end to end because I wanted a tighter environment:

The cluster names are:

The Karmada repository already includes kind configs and helper logic in hack/.

The rough flow is:

export PATH=/usr/local/go/bin:/root/go/bin:/root/.local/bin:$PATH
cd /root/karmada

source hack/util.sh

mkdir -p /root/.kube

util::create_cluster "karmada-host" "/root/.kube/karmada.config" "${DEFAULT_CLUSTER_VERSION}" "/tmp/karmada-manual"
util::create_cluster "member1" "/root/.kube/member-tmp-member1.config" "${DEFAULT_CLUSTER_VERSION}" "/tmp/karmada-manual" "/root/karmada/artifacts/kindClusterConfig/member1.yaml"
util::create_cluster "member2" "/root/.kube/member-tmp-member2.config" "${DEFAULT_CLUSTER_VERSION}" "/tmp/karmada-manual" "/root/karmada/artifacts/kindClusterConfig/member2.yaml"

Then wait for all three clusters:

util::check_clusters_ready "/root/.kube/karmada.config" "karmada-host"
util::check_clusters_ready "/root/.kube/member-tmp-member1.config" "member1"
util::check_clusters_ready "/root/.kube/member-tmp-member2.config" "member2"

At this point you should have:


Step 3: Connect Pod Networks Between Clusters

kind clusters on one machine do not magically understand each other’s Pod CIDRs. Karmada needs connectivity between clusters, so routes must be added between them.

I added routes between:

The helper function from hack/util.sh is:

util::add_routes <src_cluster> <kubeconfig> <context>

The concrete calls were:

util::add_routes member1 /root/.kube/member-tmp-member2.config member2
util::add_routes member2 /root/.kube/member-tmp-member1.config member1
util::add_routes karmada-host /root/.kube/member-tmp-member1.config member1
util::add_routes member1 /root/.kube/karmada.config karmada-host
util::add_routes karmada-host /root/.kube/member-tmp-member2.config member2
util::add_routes member2 /root/.kube/karmada.config karmada-host

Then merge the member kubeconfigs:

export KUBECONFIG=/root/.kube/member-tmp-member1.config:/root/.kube/member-tmp-member2.config
kubectl config view --flatten > /root/.kube/members.config

Final kubeconfig files:


Step 4: Deploy the Karmada Control Plane

With karmada-host ready, deploy the control plane using the Karmada repository’s helper:

cd /root/karmada
export PATH=/usr/local/go/bin:/root/go/bin:/root/.local/bin:$PATH
./hack/deploy-karmada.sh /root/.kube/karmada.config karmada-host

This installs:

Verify:

kubectl --kubeconfig=/root/.kube/karmada.config --context=karmada-host -n karmada-system get deploy

The Karmada API context generated by the deploy script is:


Step 5: Join the Member Clusters

Now join member1 and member2 to Karmada:

export KUBECONFIG=/root/.kube/karmada.config

/root/go/bin/karmadactl join \
  --karmada-context=karmada-apiserver \
  member1 \
  --cluster-kubeconfig=/root/.kube/members.config \
  --cluster-context=member1

/root/go/bin/karmadactl join \
  --karmada-context=karmada-apiserver \
  member2 \
  --cluster-kubeconfig=/root/.kube/members.config \
  --cluster-context=member2

Then deploy Karmada scheduler estimators:

./hack/deploy-scheduler-estimator.sh /root/.kube/karmada.config karmada-host /root/.kube/members.config member1
./hack/deploy-scheduler-estimator.sh /root/.kube/karmada.config karmada-host /root/.kube/members.config member2

And metrics-server to both member clusters:

./hack/deploy-k8s-metrics-server.sh /root/.kube/members.config member1
./hack/deploy-k8s-metrics-server.sh /root/.kube/members.config member2

Verify:

kubectl --kubeconfig=/root/.kube/karmada.config --context=karmada-apiserver get clusters

Expected:


Step 6: Build the Stateless Demo Application

The demo application lives here:

It is intentionally tiny:

The important part is not the backend logic. The frontend is where the observability lives:

That makes failover visible without requiring a shared database.

Build and load the image into both member clusters:

cd /root/karmada/samples/failover-demo
./scripts/build-and-load.sh

This script:


Step 7: Apply the Karmada Manifests

The manifest directory is:

It contains:

The pattern is:

Deploy them:

./scripts/deploy.sh

That script applies everything to the Karmada API server and then waits until the Deployment exists and rolls out on both member clusters.

Verify directly:

kubectl --kubeconfig=/root/.kube/members.config --context=member1 get deploy,svc,pod -n default
kubectl --kubeconfig=/root/.kube/members.config --context=member2 get deploy,svc,pod -n default

At this point:

Both are ready, but users still need a single host-side entrypoint.


Step 8: Start the Host Proxy

The proxy script is:

It starts an HAProxy container on the host and exposes:

http://127.0.0.1:8088/

The proxy design is:

Start it:

./scripts/start-proxy.sh

The script creates a config in:

Then launches:

Verify:

curl http://127.0.0.1:8088/status

If the proxy is healthy and member1 is primary, the response should look like:

{"cluster":"cluster 1","instance":"...","timestamp":"...","message":"serving from cluster 1"}

Open the page in a browser:

http://127.0.0.1:8088/

You should see:

This is much clearer than a simple local counter because it directly answers the question: Did requests keep succeeding while the backend changed?


Step 9: Simulate Cluster Failover

To simulate failure of the primary cluster, stop member1:

./scripts/fail-member1.sh

That script simply runs:

docker stop member1-control-plane

Because HAProxy is already health-checking both backends:

Verify from the host:

curl http://127.0.0.1:8088/status

Now you should get:

{"cluster":"cluster 2","instance":"...","timestamp":"...","message":"serving from cluster 2"}

The browser page should continue updating and should record a switch event such as:

This is the key moment in the demo.


Step 10: Converge Placement to member2

Traffic switching alone is not the full story. After the host proxy fails over, we can also ask Karmada to converge the workload placement to member2 only.

That script is:

Run it:

./scripts/promote-member2.sh

This script:

In other words:

This creates a nice two-step story:

  1. fast traffic failover
  2. control plane convergence

Step 11: Switch Traffic Back to member1

The reverse direction is also useful for demos.

To bring member1 back:

docker start member1-control-plane

Wait for it to become Ready again:

kubectl --kubeconfig=/root/.kube/members.config --context=member1 wait --for=condition=Ready node --all --timeout=180s
kubectl --kubeconfig=/root/.kube/karmada.config --context=karmada-apiserver wait --for=condition=Ready cluster member1 --timeout=180s

Then patch Karmada placement back to both clusters and make the replica count 2 again. That is exactly what I did in the live demo environment:

kubectl --kubeconfig=/root/.kube/karmada.config --context=karmada-apiserver patch deployment failover-demo -n default --type merge -p '{"spec":{"replicas":2}}'
kubectl --kubeconfig=/root/.kube/karmada.config --context=karmada-apiserver patch propagationpolicy failover-demo-deployment -n default --type merge -p '{"spec":{"placement":{"clusterAffinity":{"clusterNames":["member1","member2"]},"replicaScheduling":{"replicaDivisionPreference":"Weighted","replicaSchedulingType":"Divided","weightPreference":{"staticWeightList":[{"targetCluster":{"clusterNames":["member1"]},"weight":1},{"targetCluster":{"clusterNames":["member2"]},"weight":1}]}}}}}'
kubectl --kubeconfig=/root/.kube/karmada.config --context=karmada-apiserver patch propagationpolicy failover-demo-service -n default --type merge -p '{"spec":{"placement":{"clusterAffinity":{"clusterNames":["member1","member2"]}}}}'

Finally, restart the host proxy configuration so member1 becomes primary again:

./scripts/start-proxy.sh

After that:

curl http://127.0.0.1:8088/status

Should return cluster 1 again.


How the Demo Proves Continuity

The original version of the page had a local browser counter. That was not convincing enough because a local counter does not prove the backend remained reachable.

The improved page demonstrates continuity in three better ways:

1. Successful probe count

The count only increments when /status succeeds.

2. Heartbeat timeline

Each probe is visualized:

If the timeline stays green through the switch, the audience immediately understands that the service did not visibly drop.

3. Switch events

The page records backend transitions:

That is much more intuitive in a live demo than reading raw JSON from the terminal.


Why This Demo Is Stateless

This demo does not synchronize backend state between clusters.

That is intentional.

The backend only exposes:

Everything else needed for the visual experience is browser-local.

This means:

That keeps the demo focused on traffic failover, not state migration.

If you later want a stateful version, the next step is to add a shared backend such as Redis or PostgreSQL and move the continuity signal from the browser into the shared store.


Useful Commands

Check Karmada

export PATH=/usr/local/go/bin:/root/go/bin:/root/.local/bin:$PATH
kubectl --kubeconfig=/root/.kube/karmada.config --context=karmada-apiserver get clusters
kubectl --kubeconfig=/root/.kube/karmada.config --context=karmada-apiserver get deployment failover-demo -n default
kubectl --kubeconfig=/root/.kube/karmada.config --context=karmada-apiserver get propagationpolicy -n default
kubectl --kubeconfig=/root/.kube/karmada.config --context=karmada-apiserver get resourcebinding -n default

Check Member Clusters

kubectl --kubeconfig=/root/.kube/members.config --context=member1 get deploy,svc,pod -n default
kubectl --kubeconfig=/root/.kube/members.config --context=member2 get deploy,svc,pod -n default

Check the Unified Host Entry Point

curl http://127.0.0.1:8088/status

Stop the Proxy

cd /root/karmada/samples/failover-demo
./scripts/stop-proxy.sh

Lessons Learned

1. Keep the demo honest

Karmada is not a traffic router. It is a multi-cluster orchestration layer. In this demo, HAProxy owns ingress and failover speed, while Karmada owns workload distribution and convergence.

That separation makes the demo clearer and easier to debug.

2. A visual demo needs strong signals

A local counter is not enough. A probe timeline plus switch events is much easier for an audience to trust.

3. kind is convenient, but the host still matters

The biggest infrastructure issue in this environment was not Karmada itself. It was the host’s inotify limit, which broke kind networking components in subtle ways.

4. Start with stateless failover first

If the goal is to teach multi-cluster switching, a stateless demo gets you there faster and with fewer moving parts.


Conclusion

A single machine is enough to build a realistic Karmada failover lab:

With that setup, you can demonstrate:

The result is a compact but convincing multi-cluster Kubernetes demo that is practical for local development, workshops, and technical deep dives.


Edit page
Share this post on:

Previous Post
Building a Reusable SQL Adaptation Skill for Huawei Cloud GaussDB
Next Post
Running LangGraph with openGauss: Integration Guide