Skip to content

feat(gcp): add --compute cloudrun bootstrap mode - #6

Draft
naxty wants to merge 4 commits into
feat/gcp-bootstrap-public-accessfrom
feat/cloudrun-compute
Draft

feat(gcp): add --compute cloudrun bootstrap mode#6
naxty wants to merge 4 commits into
feat/gcp-bootstrap-public-accessfrom
feat/cloudrun-compute

Conversation

@naxty

@naxty naxty commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a compute-substrate axis to the GCP bootstrap, orthogonal to --access:
--compute vm (default, unchanged) or --compute cloudrun. Cloud Run runs the agent as a
single always-on serverless service reached over the same external HTTPS LB as
--access public (via a serverless NEG), no VM/disk/startup-script.

Stacks on the BYO-cert work (feat/gcp-bootstrap-public-access) — base is that branch.

Draft: depends on the GCP plugin release carrying
formae-plugin-gcp#87
(template.vpcAccess, secret-volume items, Resolvable secret name). The gcp@ pin in
gcp/PklProject stays at 0.1.9 (vm modes unaffected); bump it to the #87 release to
enable --compute cloudrun. Undraft once that's published + pinned.

What it renders (--compute cloudrun)

Shared substrate (VPC/subnet/NAT/PSA, private Cloud SQL + formae db, db/api secrets, agent
SA + IAM) + a config-secret (whole agent config rendered by Pkl, mounted as a volume)
+ a Cloud Run Service (min=max=1, cpuIdle=false, port 49684, Direct VPC egress
PRIVATE_RANGES_ONLY, Cloud SQL connector volume, ingress=INTERNAL_LOAD_BALANCER) + a
serverless NEG + the reused LB chain (global address, backend service with no health
check
, url map, HTTPS proxy, forwarding rule) + cert (SELF_MANAGED/MANAGED/by-name).

Verification (render matrix)

Against the local #87 schema:

  • vm cells ({tailnet,public} × cert paths): byte-identical to pre-change formae eval (zero regression).
  • cloudrun cells (domain / cert-file / cert-name): render the expected set; service asserts min=max=1, ingress lock, vpcAccess.egress, and a no-health-check backend service.
  • validation throws: bad --compute; cloudrun+tailnet; cloudrun+ts-authkey; cloudrun with no cert.

Not included

Live E2E (real project + domain + cert + creds) — the manual gate; pending the plugin release.

naxty added 4 commits July 21, 2026 15:03
Adds a compute-substrate axis orthogonal to --access: vm (default,
unchanged) or cloudrun. --compute cloudrun runs the agent as a single
always-on Cloud Run service (min=max=1, cpuIdle=false) reached over the
same external HTTPS LB via a serverless NEG, with the run.app URL locked
to the LB (ingress=INTERNAL_LOAD_BALANCER). It reaches the private Cloud
SQL over Direct VPC egress (PRIVATE_RANGES_ONLY) + the built-in Cloud SQL
connector volume, and takes its whole config from a mounted Secret
Manager secret (no shell heredoc). Reuses the BYO-cert LB chain and the
shared VPC/PSA/Cloud SQL/SA/secret substrate.

- vars/validation: compute prop (default vm); access derives from compute
  when empty (vm->tailnet, cloudrun->public); cloudrun rejects tailnet /
  ts-authkey / ts-hostname and reuses the public cert validation.
- sizing: Cloud-Run-legal cpu/memory t-shirt map.
- All vm render cells byte-identical to pre-change (zero regression).

Requires the GCP plugin release carrying formae-plugin-gcp#87
(template.vpcAccess, secret-volume items, Resolvable secret name); bump
the gcp@ pin in gcp/PklProject before using --compute cloudrun.
README section (how it works, quickstart, single-instance invariant,
Direct-VPC-egress/Cloud-SQL path, config-as-secret, sizing, ingress lock),
flags table, cross-mode rejection, and the plugin version dependency
(#87). Adds the compute axis to the bootstrap description block.
Over the Cloud SQL unix socket the agent appends connectionParams as
'?sslmode=disable' onto the database NAME (URL-style DSN), producing
'database "formae?sslmode=disable" does not exist'. The VM's TCP path
tolerates it; a socket host does not. Sockets are unencrypted local, so
sslmode is moot — omit it for the cloudrun config. Found in live E2E.

(vpcAccess network/subnetwork keep the resolvable ref for create-order;
the plugin now normalizes the selfLink to Cloud Run's relative path.)
An external HTTPS LB reaches Cloud Run via the serverless NEG with no IAM
identity, so the service needs allUsers/roles/run.invoker or it 403s every
request (the VM/instance-group path never needed this). Reachability stays
locked to the LB by ingress=INTERNAL_LOAD_BALANCER; the agent's basic auth
is the gate. Adds a GCP::CloudRun::ServiceIamMember. Found in live E2E.

Caveat (documented): org policy iam.allowedPolicyMemberDomains forbids
allUsers and needs an exception for this binding.
@naxty

naxty commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

Live E2E validation (2026-07-22/23)

Ran --compute cloudrun --access public against a real project (development-477117) + domain (formae.naxty.dev, self-signed cert). Summary: the architecture is validated end-to-end up to an org-policy wall; five real bugs were found and fixed (all in this PR + plugin #87).

Validated ✅

  • Cloud Run revision goes READY (repeatedly): config-as-secret mounted, agent read it, connected to the private Cloud SQL over Direct VPC egress + the built-in connector unix socket, ran migrations, bound :49684. The novel/load-bearing parts work.
  • Ingress lock confirmed: hitting the *.run.app URL directly → 403 (only the LB is allowed in).
  • LB chain reaches Cloud Run via the serverless NEG.

Blocked (environmental, not code) ⛔

  • The external HTTPS LB forwards to Cloud Run unauthenticated, so the service needs allUsersroles/run.invoker. Granting it fails on this project with FAILED_PRECONDITION: ... not a permitted customer — org policy constraints/iam.allowedPolicyMemberDomains (domain-restricted sharing) forbids allUsers. So the final basic-auth-200-through-LB can't be demonstrated here without an org-policy exception or IAP. --compute cloudrun --access public cannot work in a domain-restricted org without one of those (documented in gcp/README.md).

Bugs found live → fixed

# Bug Fix
1 vpcAccess.network/subnetwork rejected the selfLink URL plugin normalizes selfLink→relative path (#87)
2 Cloud SQL socket DSN: connectionParams appended as ?sslmode=disable onto the DB name → database "formae?sslmode=disable" does not exist drop connectionParams for the socket path (this PR)
3 Cloud Run service create hung ~50min (min-instances keeps a failing revision warming → LRO never completes) Status succeeds once the service object exists (#87)
4 create-order race (service created before subnet on a fresh apply) resolvable net/subnet ref for the edge + plugin selfLink normalization (#87 + this PR)
5 Cloud Run 403s all LB traffic — no run.invoker add allUsers/run.invoker GCP::CloudRun::ServiceIamMember (this PR)

Follow-up (separate, not this feature)

  • formae's serverless-NEG create fails silently in-command (no error on the resource or in the agent log) while gcloud creates the identical NEG fine; because the command then fails, formae doesn't persist the already-successful service → next reconcile 409s on it. This is why the manual-gcloud LB was used to prove the path. Needs a formae-agent look at the compute serverless-NEG create/operation path.

Render matrix (against the #87 schema) stays green: 4 vm cells byte-identical to pre-change, 3 cloudrun cert cells render the full set, 4 validation throws. Unit tests pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant