|
| 1 | +--- |
| 2 | +title: "Cloud Run Deployment" |
| 3 | +linkTitle: "Cloud Run Deployment" |
| 4 | +weight: 6 |
| 5 | +--- |
| 6 | + |
| 7 | +# Deploying a SAM Mesh on Cloud Run |
| 8 | + |
| 9 | +`sam-one` is the all-in-one SAM distribution: control plane, libp2p router, |
| 10 | +web console and storage in a single binary serving a single public port. |
| 11 | +Because everything — REST API, console and mesh (WebSocket) traffic — is |
| 12 | +multiplexed on one HTTP port, it runs on Cloud Run and any platform that |
| 13 | +forwards HTTP/WebSockets to a container. |
| 14 | + |
| 15 | +This guide was validated end to end on Cloud Run: node enrollment over |
| 16 | +`wss`, a relayed service call between two NAT-hidden nodes through the |
| 17 | +Cloud Run router, and the admin CLI against the public URL. |
| 18 | + |
| 19 | +## 1. Build and push the image |
| 20 | + |
| 21 | +```bash |
| 22 | +# Pick your project/region and an Artifact Registry docker repository. |
| 23 | +PROJECT=my-project |
| 24 | +REGION=us-central1 |
| 25 | +IMG=${REGION}-docker.pkg.dev/${PROJECT}/sam-mesh/sam-one:latest |
| 26 | + |
| 27 | +docker build -f Dockerfile.sam-one -t "$IMG" . |
| 28 | +gcloud auth configure-docker ${REGION}-docker.pkg.dev |
| 29 | +docker push "$IMG" |
| 30 | +``` |
| 31 | + |
| 32 | +## 2. Deploy the service |
| 33 | + |
| 34 | +Cloud Run has no persistent disk, so pass fixed join/admin tokens as |
| 35 | +environment variables — otherwise fresh ones are generated on every |
| 36 | +instance start: |
| 37 | + |
| 38 | +```bash |
| 39 | +JOIN_TOKEN="sam_tok_$(openssl rand -hex 16)" |
| 40 | +ADMIN_TOKEN="sam_adm_$(openssl rand -hex 16)" |
| 41 | + |
| 42 | +gcloud run deploy sam-one \ |
| 43 | + --project "$PROJECT" --region "$REGION" \ |
| 44 | + --image "$IMG" \ |
| 45 | + --allow-unauthenticated \ |
| 46 | + --min-instances 1 --max-instances 1 \ |
| 47 | + --port 8080 \ |
| 48 | + --no-cpu-throttling \ |
| 49 | + --timeout 3600 \ |
| 50 | + --set-env-vars "SAM_TOKEN=${JOIN_TOKEN},SAM_ADMIN_TOKEN=${ADMIN_TOKEN}" |
| 51 | +``` |
| 52 | + |
| 53 | +Flag notes, all load-bearing: |
| 54 | + |
| 55 | +* **`--min-instances 1 --max-instances 1`** — standalone mode is a |
| 56 | + singleton: the router's DHT and relay state live in the one process. |
| 57 | +* **`--no-cpu-throttling`** — the router runs background loops (leases, |
| 58 | + key sync, DHT); request-based CPU throttling stalls them between |
| 59 | + requests. |
| 60 | +* **`--timeout 3600`** — Cloud Run caps streaming requests; WebSocket mesh |
| 61 | + connections live inside that budget. Nodes reconnect automatically when |
| 62 | + the cap severs a connection, but a low timeout means needless churn. |
| 63 | +* **`--port 8080`** matches the image's default `--port 8080` argument |
| 64 | + (see the `CMD` in `Dockerfile.sam-one`). |
| 65 | + |
| 66 | +Then tell the router its public URL so it advertises a dialable `wss` |
| 67 | +multiaddr (the URL is only known after the first deploy): |
| 68 | + |
| 69 | +```bash |
| 70 | +URL=$(gcloud run services describe sam-one --project "$PROJECT" \ |
| 71 | + --region "$REGION" --format='value(status.url)') |
| 72 | + |
| 73 | +gcloud run services update sam-one \ |
| 74 | + --project "$PROJECT" --region "$REGION" \ |
| 75 | + --update-env-vars "SAM_EXTERNAL_URL=${URL}" |
| 76 | +``` |
| 77 | + |
| 78 | +## 3. Verify |
| 79 | + |
| 80 | +```bash |
| 81 | +curl -s "$URL/readyz" # 200 |
| 82 | +curl -s "$URL/info" | head -c 200 # advertises /dns4/<host>/tcp/443/wss/p2p/<peer-id> |
| 83 | +``` |
| 84 | + |
| 85 | +The web console is served from the same URL at `${URL}/console`. |
| 86 | + |
| 87 | +> [!NOTE] |
| 88 | +> `/healthz` returns a Google frontend 404 on `run.app` domains — the |
| 89 | +> path is reserved by the platform and never reaches the container. Use |
| 90 | +> `/readyz` for probes. |
| 91 | +
|
| 92 | +## 4. Join nodes from anywhere |
| 93 | + |
| 94 | +```bash |
| 95 | +sam-node run --control-plane "$URL" --bootstrap-token "$JOIN_TOKEN" |
| 96 | +``` |
| 97 | + |
| 98 | +The node enrolls over HTTPS, discovers the router's `wss` multiaddr from |
| 99 | +`/info`, and connects through the same public port. Nodes behind NAT are |
| 100 | +reachable by other nodes via relay circuits through the Cloud Run router — |
| 101 | +no inbound connectivity required on either side. |
| 102 | + |
| 103 | +## 5. Example: share a service across the mesh |
| 104 | + |
| 105 | +This walkthrough was run verbatim against a Cloud Run deployment: two |
| 106 | +nodes on different networks, neither reachable from the other |
| 107 | +(`--announce-private=false` withholds their private addresses, forcing all |
| 108 | +traffic through the Cloud Run relay). |
| 109 | + |
| 110 | +On the **provider** machine, run a node and any local HTTP backend: |
| 111 | + |
| 112 | +```bash |
| 113 | +sam-node run --control-plane "$URL" --bootstrap-token "$JOIN_TOKEN" \ |
| 114 | + --data-dir ~/provider --announce-private=false & |
| 115 | + |
| 116 | +mkdir -p /tmp/www && echo "hello from provider" > /tmp/www/hello.txt |
| 117 | +python3 -m http.server 9000 --bind 127.0.0.1 --directory /tmp/www & |
| 118 | +``` |
| 119 | + |
| 120 | +Register the backend as a mesh service through the node's local sidecar |
| 121 | +socket (owner-only permissions replace the API token) and note the node's |
| 122 | +PeerID from its startup output: |
| 123 | + |
| 124 | +```bash |
| 125 | +curl --unix-socket ~/provider/sam.sock -X POST \ |
| 126 | + -H "Content-Type: application/json" \ |
| 127 | + -d '{"service":{"type":"SERVICE_TYPE_MCP","name":"hello","description":"example"}, |
| 128 | + "targetUrl":"http://127.0.0.1:9000"}' \ |
| 129 | + http://localhost/sam/service/register |
| 130 | +# -> Service registered |
| 131 | +``` |
| 132 | + |
| 133 | +On the **consumer** machine, run a node the same way, then call the |
| 134 | +service by peer and name through the local egress proxy: |
| 135 | + |
| 136 | +```bash |
| 137 | +sam-node run --control-plane "$URL" --bootstrap-token "$JOIN_TOKEN" \ |
| 138 | + --data-dir ~/consumer --announce-private=false & |
| 139 | + |
| 140 | +curl --unix-socket ~/consumer/sam.sock \ |
| 141 | + "http://localhost/sam/<provider-peer-id>/mcp/hello/hello.txt" |
| 142 | +# -> hello from provider |
| 143 | +``` |
| 144 | + |
| 145 | +The request crosses consumer → Cloud Run router (relay circuit) → |
| 146 | +provider → backend, with mutual Biscuit authentication between the nodes |
| 147 | +and policy enforced on the service name. Allow a few seconds after |
| 148 | +registration for propagation on first call. |
| 149 | + |
| 150 | +## 6. Operate with the CLI |
| 151 | + |
| 152 | +The `sam-one` binary doubles as an admin client for the running service: |
| 153 | + |
| 154 | +```bash |
| 155 | +export SAM_ADMIN_TOKEN="$ADMIN_TOKEN" |
| 156 | + |
| 157 | +# Mint a scoped, single-use enrollment token |
| 158 | +sam-one token create --server "$URL" --role sam:role:node --max-usages 1 |
| 159 | + |
| 160 | +# List tokens and their usage |
| 161 | +sam-one token list --server "$URL" |
| 162 | + |
| 163 | +# Ban a peer from the mesh |
| 164 | +sam-one admin ban --server "$URL" <peer-id> |
| 165 | +``` |
| 166 | + |
| 167 | +## Operational notes |
| 168 | + |
| 169 | +* **State is ephemeral.** The SQLite database lives in the container's |
| 170 | + in-memory filesystem: enrolled nodes and minted tokens are lost on |
| 171 | + instance restart, and the router's peer identity rotates (nodes |
| 172 | + re-discover it via `/info` and re-enroll with the join token). Keep |
| 173 | + `SAM_TOKEN`/`SAM_ADMIN_TOKEN` pinned via env vars. For durable state, |
| 174 | + run `sam-one` on a VM with a disk, or point `--db-driver postgres` at a |
| 175 | + managed database. |
| 176 | +* **Rollouts briefly overlap revisions.** During a deploy, `/info` may |
| 177 | + advertise the new instance while some WebSocket upgrades still land on |
| 178 | + the draining one; nodes refuse the peer-ID mismatch and retry. Joins |
| 179 | + succeed once the old revision drains. |
| 180 | +* **Proxied traffic shares source IPs.** All traffic reaches the |
| 181 | + container from a handful of frontend proxy IPs. `sam-one` already |
| 182 | + raises libp2p's per-source-IP connection and rate budgets for this |
| 183 | + (`ConnsPerSourceIP`); if you front a discrete `sam-router` with a proxy |
| 184 | + yourself, set `--conns-per-source-ip` accordingly. |
| 185 | + |
| 186 | +## Anywhere else |
| 187 | + |
| 188 | +The same single-port binary runs on any host without flags: |
| 189 | + |
| 190 | +```bash |
| 191 | +sam-one --data-dir /var/lib/sam-one |
| 192 | +``` |
| 193 | + |
| 194 | +A free port is picked and published in the startup banner together with |
| 195 | +the generated tokens; pass `--port 8080` (and optionally |
| 196 | +`--bind-address`) for a fixed one, and `--external-url https://mesh.example.com` |
| 197 | +when fronted by a reverse proxy or DNS name. |
0 commit comments