1# gcp-service-to-service
2
3One Cloud Run service calling another, private one with Google-signed
4identity. This is the GCP counterpart of `AWS.Lambda.InvokeFunction`.
5
6| Service | Access | Routes |
7| --------- | ------- | ----------------------------------------- |
8| `Gateway` | public | `GET /` (health), `GET /quote?n=` (proxy) |
9| `Quotes` | private | `GET /quote?n=` |
10
11- `Quotes` ([src/Quotes.ts](./src/Quotes.ts)) keeps Cloud Run's invoker
12 IAM check on (the default). Requests without a valid ID token get `403`
13 from Cloud Run's front end and never reach the container.
14- `Gateway` ([src/Gateway.ts](./src/Gateway.ts)) sets
15 `invokerIamDisabled: true` so anyone can call it. It forwards
16 `GET /quote` to `Quotes` and relays the response.
17
18## The binding
19
20```ts
21const quotes = yield* GCP.Run.InvokeService(Quotes);
22const response = yield* quotes.fetch("/quote");
23```
24
25Provide `GCP.Run.InvokeServiceHttp` on the caller.
26
27| When | What `InvokeService` does |
28| ------- | ------------------------- |
29| Deploy | Grants `roles/run.invoker` on the `Quotes` service's own IAM policy to the `Gateway`'s runtime service account (no project-level role). Binds the `Quotes` URL into the `Gateway`. |
30| Runtime | Mints an ID token from the metadata server (`.../service-accounts/default/identity?audience=<Quotes URL>`), caches it until 5 minutes before it expires, and sends it as `Authorization: Bearer`. |
31
32Alchemy mints each host's runtime service account, so the grant applies
33to the `Gateway` only. Other services in the project still get `403`.
34
35## Deploy
36
37Credentials come from your alchemy profile: run `alchemy profile` once and pick GCP (*Service account JSON* for a key file, or *Stored* for an access token or key kept in `~/.alchemy/credentials`, plus a default region), then deploy with `--profile <name>`.
38
39```sh
40pnpm deploy --profile <name>
41```
42
43This needs Docker (Alchemy builds both images locally). The stack outputs `url` (the gateway) and
44`quotesUrl`:
45
46```sh
47curl "$url/quote?n=1" # 200 — served by Quotes via the gateway
48curl "$quotesUrl/quote" # 403 — no Google identity
49```
50
51To call `Quotes` as yourself, send your own identity token. This works
52only if your account holds `run.invoker` on it (project owners do):
53
54```sh
55curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" "$quotesUrl/quote"
56```
57
58## Test
59
60```sh
61ALCHEMY_PROFILE=<name> bun test
62```
63
64The test deploys the stack and checks three things: the gateway returns
65`200` with a body from `Quotes`, a direct unauthenticated call to
66`Quotes` returns `403`, and `run.invoker` on `Quotes` is held by the
67gateway's service account only. It then destroys the stack and confirms
68both services are gone. The test is skipped when
69Docker is not running.
70
71## Destroy
72
73```sh
74pnpm destroy --profile <name>
75```