spacehub

surshar / claude-code

Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.

Built by Anthropic · github.com/anthropics

claude-code / examples / gateway / gcp / terraform / README.md
160 lines · 7.7 KBPreviewCodeRaw
1# Claude Gateway — Terraform (Cloud Run)
2
3Terraform equivalent of `../setup.sh`. Lets end-users provision and manage
4the gateway with `terraform apply`. Covers the same scope ([walkthrough](https://code.claude.com/docs/en/claude-apps-gateway-on-gcp) §1–7): APIs →
5service account + IAM → Artifact Registry repo → VPC + Private Services Access →
6private-IP Cloud SQL (PG16) → secrets → Cloud Run with Direct VPC egress.
7
8## Files
9
10| File | Purpose |
11|------|---------|
12| `versions.tf` | Provider pins (google, random) |
13| `variables.tf` | All inputs (defaults match `setup.sh`'s) |
14| `main.tf` | Resources |
15| `outputs.tf` | Service URL, OAuth redirect URI, SA, DB info |
16| `terraform.tfvars.example` | Copy to `terraform.tfvars` and edit |
17
18## Prerequisites
19
201. **`../gateway.yaml` created and filled in** — copy the template first:
21 `cp ../gateway.yaml.example ../gateway.yaml`, then replace every `REPLACE_ME`
22 (Terraform reads this file and enforces no `REPLACE_ME` via a precondition).
23 Leave `public_url` at its placeholder for the first apply; set it to the
24 `run.app` URL (the `service_url` output) or your LB hostname and re-apply.
25 `gateway.yaml` is gitignored; the committed template is `gateway.yaml.example`.
262. A **remote backend** for shared use (see below). State holds secrets — never commit it.
27
28## Deploy
29
30Terraform creates the Artifact Registry repo but does **not** build/push the
31image, so the apply is two passes: a targeted apply to create the repo, then
32build/push, then the full apply.
33
34```bash
35cp terraform.tfvars.example terraform.tfvars # edit it
36terraform init
37
38# 1. Create just the Artifact Registry repo (the -target warning is expected):
39terraform apply -target=google_artifact_registry_repository.repo
40
41# 2. Download the public Claude Code linux-x64 release binary (it includes the
42# `gateway` subcommand; the Dockerfile picks it up at gcp/claude), verify its
43# sha256 against the release manifest, then build and push the image:
44BASE="https://downloads.claude.ai/claude-code-releases"
45VERSION="$(curl -fsSL --proto '=https' "${BASE}/latest")"
46curl -fL --proto '=https' --proto-redir '=https' -o ../claude \
47 "${BASE}/${VERSION}/linux-x64/claude"
48WANT="$(curl -fsSL --proto '=https' "${BASE}/${VERSION}/manifest.json" \
49 | tr -d '[:space:]' | grep -oE '"linux-x64"[^}]*' | grep -oE '[a-f0-9]{64}' | head -1)"
50[ "$(openssl dgst -sha256 ../claude | awk '{print $NF}')" = "${WANT}" ] \
51 && echo "sha256 OK" || { echo "checksum mismatch" >&2; rm -f ../claude; }
52gcloud auth configure-docker us-east5-docker.pkg.dev --quiet
53docker build --platform=linux/amd64 --provenance=false \
54 -f ../Dockerfile -t "us-east5-docker.pkg.dev/<project>/claude-gateway/gateway:${VERSION}" ..
55docker push "us-east5-docker.pkg.dev/<project>/claude-gateway/gateway:${VERSION}"
56
57# 3. Full apply:
58terraform apply
59```
60
61(`../setup.sh` §3 automates the same download-and-verify.)
62
63Set in `terraform.tfvars`:
64
65- `project_id`, `region`
66- `image_tag` (after building/pushing — step 2 above)
67- **`oidc_client_secret`** — required (the Cloud Run service mounts `latest` of
68 this secret; with no version the deploy fails). Terraform creates the
69 secret + version from it.
70- `invoker_iam_disabled` / `allow_unauthenticated` — the gateway runs its own
71 OIDC, so the Cloud Run invoker IAM check must be opened or disabled.
72 **Preferred:** `invoker_iam_disabled = true` (no `allUsers` binding; works
73 under Domain Restricted Sharing). **Fallback:** `allow_unauthenticated = true`
74 grants `allUsers` `run.invoker` — fine on a normal org, but DRS orgs reject
75 `allUsers` (set it `false` there, since an LB does **not** bypass the IAM
76 check). If both paths are blocked by org policy, use the GKE track.
77- `ingress` — defaults to **internal-only** (no public URL). Claude Code's `/login`
78 only accepts gateway hosts on private addresses, so public ingress cannot serve
79 clients; the two-pass OAuth bootstrap must be completed from inside the VPC (or a
80 PSC-connected corp network). See "Private access" below.
81
82Tear down a trial with `terraform destroy`: set `deletion_protection = false`,
83run `terraform apply` to record that in state (the provider checks the value in
84**state**, not config, so destroy would still refuse otherwise), then `terraform
85destroy`. The destroy will stop at the VPC network
86because the Private Services Access peering is intentionally left in place
87(`deletion_policy = ABANDON` — see Guard rails below); finish by deleting the
88peering manually once the Cloud SQL instance is gone, then re-run destroy:
89
90```bash
91gcloud services vpc-peerings delete --service=servicenetworking.googleapis.com \
92 --network=cc-gateway-vpc --project=<project>
93terraform destroy
94```
95
96## Guard rails
97
98Tuned so accidental deletion is hard but greenfield teardown stays easy:
99
100- `deletion_protection = true` (variable, default true) on Cloud SQL and Cloud Run —
101 blocks accidental deletion; set `false` when you intend to `terraform destroy`.
102- `disable_on_destroy = false` on APIs — tearing down config never disables APIs.
103- `deletion_policy = ABANDON` on the PSA peering — never tears down the
104 service-networking peering automatically (it's shared by every private-IP
105 service on the VPC). On the dedicated VPC this module creates, that means
106 `terraform destroy` stops at the network step; delete the peering manually
107 per the teardown note above.
108- IAM uses non-authoritative `_member` resources, so other project/secret bindings
109 are never clobbered.
110
111## Private access (internal ingress) — the default
112
113By default the service has **no public URL** (`ingress = "INGRESS_TRAFFIC_INTERNAL_ONLY"`),
114and there is no public-ingress option: Claude Code's `/login` rejects gateway hosts that
115resolve to public addresses, so public exposure cannot serve clients. Reach the service
116from inside the VPC, or via the private-access plumbing below.
117
118With internal-only ingress, `public_url` stays the `run.app` URL (Google-managed cert) —
119**no load balancer or your own certificate required**. But internal ingress alone does
120**not** let corporate on-prem clients reach `run.app`; that needs **operator /
121network-team-owned** plumbing that Cloud Run does **not** create for you (validate it's in
122place before relying on internal ingress):
123
1241. A **Private Service Connect endpoint** for Google APIs (an internal VIP in the VPC).
1252. A **Cloud DNS private zone for `run.app`** resolving `*.run.app` to that endpoint IP.
1263. **On-prem routing** to the endpoint over Cloud VPN / Interconnect.
127
128This is normally managed centrally in the network/hub project, so the module does not
129provision it. See [Private networking and Cloud Run](https://cloud.google.com/run/docs/securing/private-networking).
130For a greenfield trial without this plumbing, complete the OAuth bootstrap from inside
131the VPC — e.g. a browser proxied through an in-VPC VM (SSH SOCKS tunnel over IAP).
132
133For a **custom internal hostname or your own TLS cert**, use
134`INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER` and front the service with your own internal
135Application Load Balancer (also not provisioned by this module).
136
137## Remote state (recommended for teams)
138
139Add a backend so state is shared and locked (and out of git):
140
141```hcl
142# backend.tf
143terraform {
144 backend "gcs" {
145 bucket = "<your-tf-state-bucket>"
146 prefix = "claude-gateway/cloudrun"
147 }
148}
149```
150
151## After deploy
152
153- `terraform output service_url` / `oauth_redirect_uri`.
154- Register the redirect URI on the Google OAuth client and make sure
155 `../gateway.yaml` `public_url` matches the host.
156- Notes: Terraform does not build the image. To ship a new gateway version,
157 rerun the docker build/push under a new tag and bump `image_tag` — a bare
158 re-apply under an unchanged tag does **not** roll a new revision (Cloud Run
159 resolves the tag to a digest only at revision creation, and an unchanged
160 `image` attribute means no new revision).