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 sokubectl 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
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
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.
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
② 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
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
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
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 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.
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
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.
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
③ 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
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
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
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
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
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
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
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
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
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
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
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
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.
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).
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
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).
./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.
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
Common failures + fixes (macOS scenarios ① / ② / ③)
SSH from laptop hangs / times out
- All machines signed in to the same Tailscale tailnet?
tailscale statuson 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 clustersto 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.shon the Mac you're trying to reach?docker ps | grep ew-fwdon 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=trueyou'll also needgcloud 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.shon each Mac.
Where to next
- Solo Enterprise agentgateway lab — Ambient + N/S agentgateway ingress
- Solo Istio Ambient lab — multicluster mesh with the istio-gateway ingress
- Cloud Connectivity lab — cross-cluster failover, waypoints, egress
- Agentic / MCP lab — MCP federation, JWT RBAC, OAuth2 token exchange
- Tailscale knowledge base — advanced ACLs, exit nodes, subnet routes
- kind documentation