MastertheMesh
Setup guide · where to run your kind clusters
How-to

Where should your kind clusters live?

TO
Tom O'Rourke
EMEA Field CTO · Solo.io

Every multi-cluster lab on this site needs two kind clusters running somewhere. Each cluster eats roughly 8 CPU / 16 GB RAM. This page walks through the three sensible places to host them — pick the tab that matches the Macs you have available.

kind Tailscale SSH k9s

Pick your scenario

About — what these Mac scenarios share & why

What: Three host topologies for the two-cluster labs on macOS. Each scenario is end-to-end self-contained: stand the clusters up, get kubectl + k9s talking to them from wherever you're sitting, and (for the two-Mac case) wire the clusters together at the network layer so mesh-peering steps in the labs can succeed.

Why: Two kind clusters can be heavy. Most laptops cope, but a 4-hour demo session with two clusters + a few apps + browser tabs pushes the limits. Off-loading to a Mac mini or splitting across two Macs frees your dev machine. The labs themselves don't care where the clusters live — only that kubectl can reach them and the clusters can reach each other.

The three building blocks (used in different combinations per tab):

  • Tailscale — WireGuard mesh VPN with stable hostnames (MagicDNS). The two Macs see each other from anywhere; you don't deal with router NAT, firewalls, or LAN IPs that change.
  • scripts/fetch-kind-kubeconfigs.sh — laptop-side. Pulls each cluster's kubeconfig from a remote Mac via SSH, then holds open an SSH tunnel so kubectl 127.0.0.1:<port> on your laptop hits the cluster's API server on the remote.
  • scripts/expose-ew-on-host.sh — runs on a remote Mac. Republishes the cluster's east-west gateway + API server on the host's LAN/Tailscale IP via socat, so the other Mac's cluster can reach it.

Pick a tab below — each lays out exactly which of these you need and the order to run them in.

① One Mac, two Kind Clusters

Both clusters run on the Mac you're sitting at. The labs treat this as the default.

┌──────────────────────────── Your Mac ────────────────────────────┐
│                                                                  │
│   ┌─ Docker Desktop ─────────────────────────────────────────┐   │
│   │                                                          │   │
│   │   kind cluster: east     kind cluster: west              │   │
│   │   (control-plane + workers via Docker containers)        │   │
│   │                                                          │   │
│   └──────────────────────────────────────────────────────────┘   │
│                                                                  │
│   kubectl, k9s, helm  ◄── all local, talks to 127.0.0.1 ports    │
└──────────────────────────────────────────────────────────────────┘

You need: no Tailscale no SSH Docker Desktop ~16 CPU / 32 GB headroom

Steps

STEP 01

Install prereqs once

The audit script tells you what's missing; the --install flag installs everything via Homebrew (Docker Desktop, kind, kubectl, helm, k9s, Solo istioctl, etc).

git clone https://github.com/tjorourke/solo ~/code/solo/solo-demos
cd ~/code/solo/solo-demos
./scripts/install-prereqs.sh --install
STEP 02

Run the lab's quick.sh

Pick a lab — each one spins up its own two-cluster topology end-to-end:

# Solo Istio (Gateway API ingress + Ambient multicluster):
cd ~/code/solo/solo-demos/istio-gw-multi-cluster-kind
export SOLO_ISTIO_LICENSE_KEY=eyJ...
./scripts/quick.sh

# OR — Enterprise agentgateway + Ambient multicluster:
cd ~/code/solo/solo-demos/agentgw-multi-cluster-kind
export SOLO_ISTIO_LICENSE_KEY=eyJ...
export AGENTGATEWAY_LICENSE_KEY=eyJ...
./scripts/quick.sh

It ends with ✓ infrastructure smoke test — PASS when the clusters are up.

STEP 03

Use the clusters

kind merges its kubeconfig entries into ~/.kube/config automatically, so:

k9s            # then ':ctx' to switch between kind-east-* / kind-west-*
# or scope to one cluster:
kubectl --context kind-east-istio get pods -A
Done. This is the path most demos assume — no network plumbing, no remote anything.

② Remote Mac, two Kind Clusters

A Mac mini (or any always-on Mac) runs both kind clusters. Your laptop talks to them over SSH tunnels.

┌─ Your laptop ─┐                       ┌─ Mac mini ───────────────┐
│               │     Tailscale         │                          │
│ kubectl, k9s  │ ◄── SSH tunnels ───►  │  Docker Desktop          │
│ fetch script  │   (per-cluster API     │   ├─ kind: east          │
│               │    ports forwarded)    │   └─ kind: west          │
└───────────────┘                       └──────────────────────────┘

You need: Tailscale on both Macs SSH key auth laptop → mini Docker Desktop on the mini

Steps

STEP 01

Install Tailscale on both Macs

Sign in with the same Tailscale account on both — that's how they see each other.

# Run on BOTH Macs:
brew install --cask tailscale
open -a Tailscale     # then click "Log in" in the menu bar

# Confirm they see each other (from either Mac):
tailscale status
# Expected: a list with both machines, each with a 100.x.x.x IP and a short
# hostname like 'laptop' and 'mac-mini'.

# Sanity check — short hostname should resolve via MagicDNS:
ping mac-mini
MagicDNS turns each device's name into a hostname you can use directly. Enable it in the Tailscale admin console (DNS → MagicDNS) if it's not already on.
STEP 02

SSH key auth from laptop → mini

# On the mini: System Settings → General → Sharing → toggle on "Remote Login"

# On your laptop — generate a key if you don't have one:
test -f ~/.ssh/id_ed25519 || ssh-keygen -t ed25519 -N "" -f ~/.ssh/id_ed25519

# Copy it to the mini (asks for the mini's login password once):
ssh-copy-id <user>@mac-mini

# Verify — should NOT prompt for a password:
ssh <user>@mac-mini hostname

Optional but recommended — a short alias:

cat >> ~/.ssh/config <<'EOF'
Host mini
  HostName mac-mini
  User <your-user>
EOF
# Then: ssh mini
STEP 03

Install prereqs on the mini

ssh mini   # or ssh <user>@mac-mini

git clone https://github.com/tjorourke/solo ~/code/solo/solo-demos
cd ~/code/solo/solo-demos
./scripts/install-prereqs.sh --install
Docker Desktop needs an unlocked GUI session on macOS. Its socket only exists while a user is logged in (physically or via Screen Sharing). If docker info fails over SSH, log in to the mini's GUI once — and tick "Start at login" in Docker Desktop's preferences so it survives reboots.
STEP 04

Stand up a lab on the mini

# Still SSH'd into the mini:
cd ~/code/solo/solo-demos/istio-gw-multi-cluster-kind   # or any lab
export SOLO_ISTIO_LICENSE_KEY=eyJ...
./scripts/quick.sh

# When it ends with "✓ infrastructure smoke test — PASS", exit:
exit
STEP 05

Pull kubeconfigs back to the laptop + open the tunnel

The fetch script SSHes to the mini, runs an export helper there, copies one kubeconfig per cluster back, merges them into your laptop's ~/.kube/config, and then holds open an SSH tunnel forwarding each cluster's API port — so kubectl on your laptop can reach them.

# On your laptop:
cd ~/code/solo/solo-demos
./scripts/fetch-kind-kubeconfigs.sh mini    # or <user>@mac-mini

# Output ends with:
#   ✓ 2 kubeconfigs in ~/.kube/kind/
#   ✓ merged 2 contexts into ~/.kube/config
#   ✓ opening SSH tunnel (Ctrl-C to close)
#
# LEAVE THIS TERMINAL RUNNING — it's holding the tunnel open.
STEP 06

Use the clusters from a second terminal on your laptop

k9s          # both contexts already in ~/.kube/config — :ctx to switch
# or:
kubectl --context kind-east-istio get pods -A
Teardown / rebuild on the mini → re-run the fetch script on the laptop. kind picks new random API ports on each rebuild, so the old tunnels stop working until you re-fetch.

③ Two Macs, One Kind cluster on each

Two Macs only — your own Mac (where you also work) plus one other Mac. Each Mac runs one kind cluster, getting its full CPU/RAM. The clusters reach each other across Tailscale via a small socat helper.

┌─ Your Mac ──────────────────┐         ┌─ Remote Mac ────────────────┐
│  kubectl, k9s (local)       │         │                             │
│                             │         │                             │
│  kind cluster: east         │◄───────►│   kind cluster: west        │
│  (Docker + kind)            │  HBONE  │   (Docker + kind)           │
│  socat republishes ports    │  + API  │   socat republishes ports   │
│  15021/15008/15012 + 6443   │  on TS  │   15021/15008/15012 + 6443  │
│  on this Mac's Tailscale IP │   IPs   │   on this Mac's Tailscale IP│
└─────────────────┬───────────┘         └─────────────────┬───────────┘
                  │                                       │
                  │       Tailscale mesh between Macs     │
                  └───────────────────────────────────────┘
                                  ▲
                                  │  SSH tunnel for remote cluster's API
                                  │  (so your local kubectl reaches it)
                                  └── from Your Mac to Remote Mac

You need: Tailscale on both Macs SSH key auth your Mac → remote Mac Docker Desktop on both Macs

Scope of this tab. This is infrastructure setup only — two clusters reachable from your Mac and reachable from each other on the right ports. Actual mesh peering (root-CA propagation, remote-secrets, east-west gateways, multicluster federation) lives in the individual lab pages. Once the steps below are green, the labs' peering step has everything it needs.
Shortcut: one command for the whole Solo Enterprise mgmt-plane lab. If you want the full topology — two kind clusters with a shared root CA, Ambient mesh, Solo Enterprise mgmt plane on one cluster + workload agent on the other, manual LAN-IP peer Gateways, optional bookinfo + AccessPolicy + cross-cluster failover test — end-to-end on two Macs, use scripts/super-quick.sh. It does STEP 04 → STEP 07 below plus the gloo-platform install + peering + (optional) demo workloads, in one idempotent run. Re-run anytime; phases that are already done fast-skip.
# Prereqs: STEP 01–03 below (Tailscale or LAN, SSH keys, install-prereqs.sh on both Macs).
# Then, from this Mac:
./scripts/super-quick.sh                    # prompts for SSH user/host + cluster names
./scripts/super-quick.sh --deploy-bookinfo  # also deploys bookinfo + runs the failover test

# Nightly build via a Solo OCI registry. NIGHTLY=true builds the
# oci:// chart URLs from a single base path; runs `gcloud auth login` on
# this Mac the first time and forwards the access token over SSH so the
# remote Mac does NOT need gcloud:
GLOO_PLATFORM_NIGHTLY=true \
GLOO_PLATFORM_NIGHTLY_REGISTRY=<host>/<proj>/<repo> \
GLOO_PLATFORM_VERSION=<chart-tag> \
GLOO_PLATFORM_IMAGE_TAG=<image-tag> \
  ./scripts/super-quick.sh

# Or override each piece individually (when chart names / image registry
# don't follow the default convention):
GLOO_PLATFORM_CHART=oci://<host>/<path>/<chart-name> \
GLOO_PLATFORM_CRDS_CHART=oci://<host>/<path>/<chart-name>-crds \
GLOO_PLATFORM_VERSION=<chart-tag> \
GLOO_PLATFORM_IMAGE_REGISTRY=<host>/<path> \
GLOO_PLATFORM_IMAGE_TAG=<image-tag> \
  ./scripts/super-quick.sh

# Tear down both clusters (pass the names you used at standup):
./scripts/super-quick.sh --east-name <name> --west-name <name> teardown
Pin/override individually with GLOO_PLATFORM_CHART, GLOO_PLATFORM_CRDS_CHART, GLOO_PLATFORM_VERSION, GLOO_PLATFORM_IMAGE_REGISTRY. Cluster names can be set via CLUSTER1 / CLUSTER2 env vars or the --east-name / --west-name flags to skip the prompt.

The manual STEP 04 → STEP 07 below is still the right reference if you want to drive the lab step-by-step (good for learning), or if you only need the bare-Istio peering scenario without the Solo Enterprise mgmt plane.

Steps

STEP 01

Tailscale on both Macs

Sign both Macs in to the same Tailscale account. That's the whole network setup — no router config, no port forwarding.

# Run on BOTH Macs:
brew install --cask tailscale
open -a Tailscale     # then log in via the menu bar

# From your Mac, the other should be visible:
tailscale status
#   100.74.12.34  your-mac     you@   macOS   active
#   100.74.12.99  remote-mac   you@   macOS   active

# Sanity check MagicDNS:
ping remote-mac
STEP 02

SSH key auth: your Mac → remote Mac

# On the remote Mac: System Settings → General → Sharing → toggle on "Remote Login"

# On your Mac — generate a key if you don't have one:
test -f ~/.ssh/id_ed25519 || ssh-keygen -t ed25519 -N "" -f ~/.ssh/id_ed25519

# Copy it to the remote Mac (asks for its login password once):
ssh-copy-id <user>@remote-mac

# Verify — should NOT prompt for a password:
ssh <user>@remote-mac hostname

Optional alias:

cat >> ~/.ssh/config <<'EOF'
Host remote-mac
  HostName remote-mac
  User <your-user>
EOF
# Then just: ssh remote-mac
STEP 03

Install prereqs on both Macs

# On your Mac (runs locally):
git clone https://github.com/tjorourke/solo ~/code/solo/solo-demos
cd ~/code/solo/solo-demos
./scripts/install-prereqs.sh --install

# On the remote Mac (over SSH):
ssh remote-mac
  git clone https://github.com/tjorourke/solo ~/code/solo/solo-demos
  cd ~/code/solo/solo-demos
  ./scripts/install-prereqs.sh --install
  exit
STEP 04

Stand up one cluster per Mac

Use quick-single.sh from one of the labs — same installer as quick.sh but spins up exactly one named cluster.

# On your Mac — the 'east' cluster, locally:
cd ~/code/solo/solo-demos/istio-gw-multi-cluster-kind
export SOLO_ISTIO_LICENSE_KEY=eyJ...
CLUSTER_NAME=east ./scripts/quick-single.sh

# On the remote Mac — the 'west' cluster, via SSH:
ssh remote-mac
  cd ~/code/solo/solo-demos/istio-gw-multi-cluster-kind
  export SOLO_ISTIO_LICENSE_KEY=eyJ...
  CLUSTER_NAME=west ./scripts/quick-single.sh
  exit
Use distinct cluster names per Mac so when you merge both kubeconfigs on your Mac they don't collide.
STEP 05

Publish each cluster's east-west GW + API on the Mac's Tailscale IP

kind binds everything to the Mac's loopback (127.0.0.1) — but the other Mac needs to reach it across Tailscale. The expose-ew-on-host.sh helper spins up small socat containers on the Docker kind network that republish the east-west GW ports (15021, 15008, 15012) and the kind API server (6443) on the Mac's own Tailscale IP.

# On your Mac — publish 'east' for the remote Mac to reach:
cd ~/code/solo/solo-demos
./scripts/expose-ew-on-host.sh east

# On the remote Mac — publish 'west' for your Mac to reach:
ssh remote-mac
  cd ~/code/solo/solo-demos
  ./scripts/expose-ew-on-host.sh west
  exit

Each invocation prints what it published, e.g.:

✓ host LAN IP: 100.74.12.99
✓ auto-detected ports: 15021 15008 15012
✓ started ew-fwd-west-15021  →  100.74.12.99:15021 → 172.22.255.100:15021
✓ started ew-fwd-west-15008  →  100.74.12.99:15008 → 172.22.255.100:15008
✓ started ew-fwd-west-15012  →  100.74.12.99:15012 → 172.22.255.100:15012
✓ Republishing kind API server on 100.74.12.99:6443
STEP 06

Confirm Mac-to-Mac reachability

A 30-second sanity check before doing anything mesh-related — if these fail, no amount of istioctl multicluster link will fix it.

# From your Mac, talk to the remote Mac's east-west GW + API:
curl -sk https://remote-mac:15021/healthz/ready    # should return 200
nc -zv remote-mac 6443                              # should say "succeeded"

# From the remote Mac, talk back to your Mac:
ssh remote-mac
  curl -sk https://your-mac:15021/healthz/ready
  nc -zv your-mac 6443
  exit
STEP 07

Pull the remote cluster's kubeconfig back to your Mac

The 'east' cluster on your Mac is already accessible locally — kind put it in ~/.kube/config when you ran quick-single.sh. You only need to fetch the remote cluster's kubeconfig + tunnel.

# Terminal 1 — holds the SSH tunnel to the remote cluster's API. Leave running.
cd ~/code/solo/solo-demos
./scripts/fetch-kind-kubeconfigs.sh remote-mac

# Terminal 2 — your work terminal:
k9s          # both contexts now in ~/.kube/config, switch with :ctx
Infra is now ready. Both clusters are reachable from your Mac and from each other on the right ports. Continue on the lab page itself for the mesh-peering steps (root CAs, remote-secrets, istioctl multicluster link, etc.) — they assume exactly this topology.

④ One Linux VM, two Kind Clusters

Both clusters run on a single Linux VM you provision yourself. The repo includes scripts/deploy-vm.sh — provisions an AWS EC2 instance, installs every prereq (docker, kind, kubectl, helm, gcloud, gh, meshctl, Solo istioctl), rsyncs your repo + secrets, all in one command. AWS-flavoured but the bootstrap step is generic Linux — adapt the provisioning bit for GCP / Azure / Hetzner / on-prem.

┌─ Your laptop ────────────────┐         ┌─ Linux VM (Ubuntu 24.04) ───┐
│  AWS CLI + ssh + rsync       │   SSH   │  Docker + kind              │
│                              ├────────►│                              │
│  deploy-vm.sh provisions:    │         │  kind cluster: east-ag      │
│   - 1× EC2 m6i.2xlarge       │         │  kind cluster: west-ag      │
│   - SG with :22 from 0/0     │         │                              │
│   - Bootstrap: docker, kind, │         │  Multicluster Ambient mesh  │
│     kubectl, helm, gcloud,   │         │  peering local to the VM     │
│     gh, meshctl, istioctl    │         │  (Docker bridge network)     │
│   - rsyncs repo + secrets    │         │                              │
└──────────────────────────────┘         └──────────────────────────────┘

You need: AWS CLI + authed profile ~/.ssh/solo-demo.pem (or auto-created) ~/code/solo/secrets/secrets-envs.sh

Other clouds. deploy-vm.sh is AWS-specific (uses aws ec2), but the bootstrap it runs (scripts/bootstrap-aws-linux.sh) is generic — provision a fresh Ubuntu 22.04+ VM by hand on GCP / Azure / Hetzner / bare-metal, scp the bootstrap, run it. The bootstrap auto-detects apt vs dnf vs pacman, so any Debian / Ubuntu / Fedora / Arch host works.

Steps

STEP 01

Provision the VM

From your laptop. deploy-vm.sh imports your SSH public key as an AWS key pair (if not already there), creates a security group with port 22 open from 0.0.0.0/0, launches an m6i.2xlarge in eu-west-2, then auto-runs the bootstrap + rsyncs your repo + secrets:

cd ~/code/solo/solo-demos

# Required (your shell env):
export AWS_PROFILE=<your-aws-profile>        # profile with EC2 perms

# Optional overrides:
export REGION=eu-west-2                       # AWS region
export INSTANCE_TYPE=m6i.2xlarge              # 8 vCPU / 32 GiB
export PEM_PATH=~/.ssh/solo-demo.pem          # SSH key path (imported as key-pair "solo-demo")

./scripts/deploy-vm.sh             # ~5 min: provision + bootstrap + rsync
./scripts/deploy-vm.sh ssh         # prints the ssh command to copy
First time? Generate a key first:
ssh-keygen -t rsa -b 4096 -N "" -f ~/.ssh/solo-demo.pem
The script imports the public key as an EC2 key pair named "solo-demo" on first run.
STEP 02

One manual step: gcloud auth (only if running the nightly)

The verified AGW_NIGHTLY=true bundle pulls Solo's pre-release agentgateway chart + image from a private Google Artifact Registry. You authenticate gcloud on the VM (headless flow — opens a browser on your laptop):

# ssh in:
./scripts/deploy-vm.sh ssh    # prints the ssh command

# On the VM:
gcloud auth login --no-launch-browser
gcloud config set project <solo-gcp-project-id>
gcloud auth configure-docker us-central1-docker.pkg.dev

Skip this step if you're running the public release (no AGW_NIGHTLY=true).

STEP 03

Run the demo

# Still on the VM:
newgrp docker     # pick up the docker group (skip if you've re-ssh'd since bootstrap)
cd ~/code/solo/solo-demos
export SECRETS_FILE=$HOME/code/solo/secrets/secrets-envs.sh

# Public release:
./agentgw-multi-cluster-kind/scripts/quick.sh

# Or the verified nightly:
AGW_NIGHTLY=true ./agentgw-multi-cluster-kind/scripts/quick.sh

Verify the standup before running the application labs:

./agentgw-multi-cluster-kind/scripts/health-check.sh
STEP 04

Tear down (cost reminder)

An m6i.2xlarge in eu-west-2 costs ~$0.40/hr (~$10/day). When you're done:

# from your laptop:
./scripts/deploy-vm.sh destroy

Terminates every instance tagged Project=solo-demo. The security group + key pair stay around for next time (delete manually with aws ec2 delete-security-group --group-name solo-demo-ssh-open if you want them gone too).

Re-prep an existing VM (e.g. after you re-rsync a script change):
./scripts/deploy-vm.sh prep <public-ip>
Skips the EC2 launch — just re-rsyncs + re-runs the bootstrap. Idempotent, so safe to re-run anytime.
Adapting to another cloud. The bootstrap script in scripts/bootstrap-aws-linux.sh is what does the real work — it installs Docker, kind, kubectl, helm, gcloud, gh, meshctl, Solo istioctl, and wires PATH. Manually provision a fresh Ubuntu 22.04+ VM (at least 8 vCPU / 32 GiB / 50 GiB disk), scp the script, run bash bootstrap-aws-linux.sh, then continue from STEP 02.

When something goes wrong

FYI

Common failures + fixes (macOS scenarios ① / ② / ③)

SSH from laptop hangs / times out

  • All machines signed in to the same Tailscale tailnet? tailscale status on each
  • Remote Login enabled on the remote Mac? System Settings → Sharing
  • SSH key authorized? Re-run ssh-copy-id

fetch-kind-kubeconfigs.sh says "no kind clusters found"

  • The lab on the remote Mac was never run, was torn down, or Docker Desktop is asleep.
  • SSH in: kind get clusters to confirm.

kubectl from laptop returns "connection refused"

  • The terminal holding the SSH tunnel (the one running fetch-kind-kubeconfigs.sh) closed. Re-run it.
  • The clusters got torn down + recreated on the remote — kind chose new random API ports. Re-run the fetch.

curl https://remote-mac:15021 times out (scenario ③)

  • Did you run expose-ew-on-host.sh on the Mac you're trying to reach? docker ps | grep ew-fwd on that Mac should list the socat containers.
  • Both Macs on the same Tailscale tailnet? tailscale status
  • macOS firewall blocking inbound? System Settings → Network → Firewall.

quick.sh on a remote Mac fails with "docker can't pull"

  • The login keychain is locked. The script now auto-prompts — say yes at the password prompt.
  • If you set AGW_NIGHTLY=true you'll also need gcloud auth login — the script guides you.

Remote Mac reboots — what survives?

  • Tailscale: auto-starts.
  • Docker Desktop: only if "Start at login" is enabled.
  • kind clusters: state on disk survives; pods come back after Docker starts.
  • SSH tunnels from your laptop: die. Re-run the fetch script.
  • socat containers (scenario ③): die. Re-run expose-ew-on-host.sh on each Mac.

Where to next