Wendy GCP Deployment
How Wendy deploys to Google Cloud. Status: intermediate. These conventions hold until v1 of the stack runs and the compute workload is consolidated (possibly k8s/GitOps later). The methodology (Pulumi, OIDC, PoLP) is expected to survive that transition; specific names may not.
Hard rules (non-negotiable)
- Requirements before infrastructure. Never design infra without asking the user about the app's requirements first (see interview below). Never invent project names, regions, or scale assumptions.
- Cost-consciousness is a MUST, not a preference. Read
references/cost.md before choosing compute or a database.
- PoLP via custom IAM roles. Prefer a custom role with the exact permissions needed over any predefined broad role. Needing to alter one DNS record never justifies
roles/dns.admin. See references/iam-polp.md.
- No service account keys. Ever. CI authenticates via GitHub OIDC / Workload Identity Federation (
references/github-oidc.md); workloads use attached service accounts. If you are about to write credentials_json, export a key, or suggest one "as a stopgap" — stop; that path does not exist here.
- No GCP credentials client-side. Frontends, device apps, anything running outside Wendy's server perimeter never holds GCP credentials of any form. Devices get mediated access via server-side proxies minting short-lived downscoped credentials (wendy-proxy pattern). Authority: the AAA contract,
/home/sem/wendy/aaa-contract-*.md (read the latest version when touching auth between services).
- dev/prod split by default. Default branch deploys to dev; semver tags (
v*) deploy to prod. Skip the split only with a stated good reason. Both live in the same GCP project for now, split by resource naming (-dev / -prod suffixes).
- Never clash with pre-existing resources. Check before creating — especially DNS records (
references/dns.md) and anything in shared projects.
Requirements interview (always first)
Before proposing any design, ask the user (don't assume):
- Traffic shape and volume; public or internal?
- State: does it truly need a database? What kind/size?
- Latency and availability requirements (is scale-to-zero cold start acceptable?)
- Which project does it belong in (see map below)? Dev/prod needed?
- Anything that must survive redeploys (volumes, IPs, data)?
Shape the design to the answers. When requirements are modest, the infrastructure must be too.
Project map (current truth — verify with gcloud projects list if in doubt)
| Project |
Purpose |
wendy-customer |
All customer-facing apps (apps not part of the cloud stack). Per-app isolation inside this project matters: own SA, own custom role, own resources per app. |
wendy-auth |
wendy-auth |
wendy-pki-secure |
pki-core engine |
wendy-pki-services |
pki-core frontends |
cloud-c7e56 |
SaaS cloud stack; hosts the *.wendy.sh and *.wendy.dev DNS zones |
Default region: us-central1 (current home; movable given a real use case — ask, don't switch unilaterally).
Compute selection
- Cloud Run first. Scale-to-zero, request-based billing, no OS to maintain. Default for services and jobs.
- VMs are leaned against, for one specific reason: once spun up, nobody maintains the OS. A VM is acceptable only if the OS is completely maintenance-free (e.g. Container-Optimized OS with automated updates, MIGs with automated image rotation). If the plan involves a human ever SSHing in to patch it, it's not an option.
- Databases: the cheapest thing that meets requirements. Read
references/cost.md — it exists because of a real incident (expensive Cloud SQL clusters provisioned for apps that didn't need them).
Network shape (preferred for bigger projects)
Source of truth: Linear doc Wendy Network Architecture (WDY-2840, approved 2026-09-03). Anything with multiple services, VMs, or a database gets this shape.
Carve-out: a single, fully-native deployment (e.g. Firebase Hosting, a lone Cloud Run service behind an ALB) does not carry the strict VPC requirements below. Dual-stack (rule 6) remains required even there.
- Per-project custom-mode VPCs, deliberately separate — authority isolation. Never the auto-mode
default VPC; delete it from projects we control.
- Cross-project service traffic only via Private Service Connect — one published service, one direction, consumer-allowlisted. Never VPC peering (firewall rules can't reference peer SAs/tags; CIDR coupling).
- Databases/caches private-only. Cloud SQL: no public IP,
sslMode: ENCRYPTED_ONLY, PSC preferred. Valkey/Memorystore: transit encryption + AUTH.
- Egress deny-by-default, with per-service-account egress firewall rules (v4/v6 symmetric). Every service gets an explicit egress class: zero / allowlist / broad-443. Cloud NAT for v4 egress; a service granted v6 egress carries external v6 + deny-all ingress.
- No public IPs on VMs. Ingress only through LB forwarding rules; operator access via IAP, never public SSH/RDP.
- Dual-stack (IPv6+IPv4) is mandatory for everything we build — LB frontends, MIGs, VMs. Internal v6 is ULA (
ipv6AccessType=INTERNAL) by default; AAAA records on every public host. Only exception: a written one, for Google-managed endpoints with no v6 path (Cloud SQL, Memorystore internals).
- Server certs: endpoints reachable by default-trust clients (browsers, generic tooling) use Let's Encrypt (lego DNS-01); endpoints called exclusively by wendy software may use pki-core-issued certs. The exact machine-endpoint policy is still being finalized on WDY-2840 — apply the principle, don't hardcode per-host assignments.
Deployment workflow
- Requirements interview (above).
- Design: compute, data, network shape (above), IAM (custom roles per
references/iam-polp.md), DNS (references/dns.md), dev/prod stacks.
- Bootstrap privileged resources (deploy SA, custom role, WIF binding) via the human's own gcloud, with their approval per command. If the human lacks permission: produce the exact commands + a use-case writeup to hand to an admin (template in
references/iam-polp.md).
- Pulumi program — Go preferred, GCS state backend, KMS secrets provider, conventions in
references/pulumi.md.
- GitHub Actions workflow — OIDC only, per
references/github-oidc.md. Also invoke the github-actions skill when writing the workflow.
Rationalizations that don't fly
| Excuse |
Reality |
| "roles/dns.admin scoped to the zone is minimal enough" |
Cloud DNS supports per-RRset IAM conditions. Zone-wide admin is not the floor; the exact records are. |
| "A predefined role is simpler than a custom role" |
Simpler to type, larger to breach. Custom roles are a one-time cost; see the recipe. |
| "credentials_json just to get it working" |
A leaked key is a standing credential. WIF setup takes minutes; there is no keys-temporarily path. |
| "It'll need Cloud SQL HA when it grows" |
Provision for today's requirements. Growth is a config change, not a sunk cost. |
| "Skipping dev, it's a small app" |
Small apps get semver-tagged prod deploys too. Skipping the split is a decision the user makes, with a reason. |
| "The zone probably has no record with this name" |
gcloud dns record-sets list takes two seconds. Check. |
| "VPC peering is simpler than PSC" |
Peered firewalls can't reference the peer's SAs/tags, and CIDRs couple. PSC publishes one service, one direction, allowlisted. |
| "IPv4-only for now, v6 later" |
Dual-stack is mandatory from day one. The only exception is a written one for Google-managed endpoints with no v6 path. |
Red flags — stop and reconsider
- You wrote
roles/*.admin for a deploy SA
- You wrote
credentials_json or gcloud iam service-accounts keys create
- A GCP credential (of any form) ends up in frontend code, a device image, or an app config shipped outside Wendy's servers
- You picked a region, tier, or project name the user never confirmed
- Your design has no
-dev stack and the user never said why
- You're about to create a DNS record without listing existing ones first
- Your design uses the
default VPC, VPC peering, or a public IP on a VM or database
- An LB frontend, MIG, or public host is IPv4-only with no written exception
1---2name: wendy-gcp-deployment3description: Expert guidance on deploying Wendy applications and infrastructure to Google Cloud. Use when: (1) deploying anything to GCP, (2) writing or changing Pulumi programs, (3) creating GCP service accounts, IAM roles, or custom roles, (4) touching wendy.sh / wendy.dev DNS records, (5) choosing GCP compute or databases, (6) setting up CI/CD deployment to GCP, (7) provisioning Cloud Run, Cloud SQL, or Artifact Registry.4---56# Wendy GCP Deployment78How Wendy deploys to Google Cloud. **Status: intermediate.** These conventions hold until v1 of the stack runs and the compute workload is consolidated (possibly k8s/GitOps later). The methodology (Pulumi, OIDC, PoLP) is expected to survive that transition; specific names may not.910## Hard rules (non-negotiable)11121. **Requirements before infrastructure.** Never design infra without asking the user about the app's requirements first (see interview below). Never invent project names, regions, or scale assumptions.132. **Cost-consciousness is a MUST**, not a preference. Read `references/cost.md` before choosing compute or a database.143. **PoLP via custom IAM roles.** Prefer a custom role with the exact permissions needed over any predefined broad role. Needing to alter one DNS record never justifies `roles/dns.admin`. See `references/iam-polp.md`.154. **No service account keys. Ever.** CI authenticates via GitHub OIDC / Workload Identity Federation (`references/github-oidc.md`); workloads use attached service accounts. If you are about to write `credentials_json`, export a key, or suggest one "as a stopgap" — stop; that path does not exist here.165. **No GCP credentials client-side.** Frontends, device apps, anything running outside Wendy's server perimeter never holds GCP credentials of any form. Devices get mediated access via server-side proxies minting short-lived downscoped credentials (wendy-proxy pattern). Authority: the AAA contract, `/home/sem/wendy/aaa-contract-*.md` (read the latest version when touching auth between services).176. **dev/prod split by default.** Default branch deploys to dev; semver tags (`v*`) deploy to prod. Skip the split only with a stated good reason. Both live in the same GCP project for now, split by resource naming (`-dev` / `-prod` suffixes).187. **Never clash with pre-existing resources.** Check before creating — especially DNS records (`references/dns.md`) and anything in shared projects.1920## Requirements interview (always first)2122Before proposing any design, ask the user (don't assume):2324- Traffic shape and volume; public or internal?25- State: does it truly need a database? What kind/size?26- Latency and availability requirements (is scale-to-zero cold start acceptable?)27- Which project does it belong in (see map below)? Dev/prod needed?28- Anything that must survive redeploys (volumes, IPs, data)?2930Shape the design to the answers. When requirements are modest, the infrastructure must be too.3132## Project map (current truth — verify with `gcloud projects list` if in doubt)3334| Project | Purpose |35|---|---|36| `wendy-customer` | All customer-facing apps (apps not part of the cloud stack). Per-app isolation inside this project matters: own SA, own custom role, own resources per app. |37| `wendy-auth` | wendy-auth |38| `wendy-pki-secure` | pki-core engine |39| `wendy-pki-services` | pki-core frontends |40| `cloud-c7e56` | SaaS cloud stack; hosts the `*.wendy.sh` and `*.wendy.dev` DNS zones |4142Default region: **`us-central1`** (current home; movable given a real use case — ask, don't switch unilaterally).4344## Compute selection4546- **Cloud Run first.** Scale-to-zero, request-based billing, no OS to maintain. Default for services and jobs.47- **VMs are leaned against**, for one specific reason: once spun up, nobody maintains the OS. A VM is acceptable **only** if the OS is completely maintenance-free (e.g. Container-Optimized OS with automated updates, MIGs with automated image rotation). If the plan involves a human ever SSHing in to patch it, it's not an option.48- **Databases:** the cheapest thing that meets requirements. Read `references/cost.md` — it exists because of a real incident (expensive Cloud SQL clusters provisioned for apps that didn't need them).4950## Network shape (preferred for bigger projects)5152Source of truth: Linear doc [Wendy Network Architecture](https://linear.app/wendylabsinc/document/wendy-network-architecture-2d114f8c6c19) (WDY-2840, approved 2026-09-03). Anything with multiple services, VMs, or a database gets this shape.5354**Carve-out:** a single, fully-native deployment (e.g. Firebase Hosting, a lone Cloud Run service behind an ALB) does **not** carry the strict VPC requirements below. Dual-stack (rule 6) remains required even there.55561. **Per-project custom-mode VPCs**, deliberately separate — authority isolation. Never the auto-mode `default` VPC; delete it from projects we control.572. **Cross-project service traffic only via Private Service Connect** — one published service, one direction, consumer-allowlisted. Never VPC peering (firewall rules can't reference peer SAs/tags; CIDR coupling).583. **Databases/caches private-only.** Cloud SQL: no public IP, `sslMode: ENCRYPTED_ONLY`, PSC preferred. Valkey/Memorystore: transit encryption + AUTH.594. **Egress deny-by-default**, with per-service-account egress firewall rules (v4/v6 symmetric). Every service gets an explicit egress class: zero / allowlist / broad-443. Cloud NAT for v4 egress; a service granted v6 egress carries external v6 + deny-all ingress.605. **No public IPs on VMs.** Ingress only through LB forwarding rules; operator access via IAP, never public SSH/RDP.616. **Dual-stack (IPv6+IPv4) is mandatory** for everything we build — LB frontends, MIGs, VMs. Internal v6 is ULA (`ipv6AccessType=INTERNAL`) by default; AAAA records on every public host. Only exception: a written one, for Google-managed endpoints with no v6 path (Cloud SQL, Memorystore internals).627. **Server certs:** endpoints reachable by default-trust clients (browsers, generic tooling) use Let's Encrypt (lego DNS-01); endpoints called exclusively by wendy software may use pki-core-issued certs. The exact machine-endpoint policy is still being finalized on WDY-2840 — apply the principle, don't hardcode per-host assignments.6364## Deployment workflow65661. Requirements interview (above).672. Design: compute, data, network shape (above), IAM (custom roles per `references/iam-polp.md`), DNS (`references/dns.md`), dev/prod stacks.683. Bootstrap privileged resources (deploy SA, custom role, WIF binding) via the human's own gcloud, with their approval per command. If the human lacks permission: produce the exact commands + a use-case writeup to hand to an admin (template in `references/iam-polp.md`).694. Pulumi program — Go preferred, GCS state backend, KMS secrets provider, conventions in `references/pulumi.md`.705. GitHub Actions workflow — OIDC only, per `references/github-oidc.md`. Also invoke the `github-actions` skill when writing the workflow.7172## Rationalizations that don't fly7374| Excuse | Reality |75|---|---|76| "roles/dns.admin scoped to the zone is minimal enough" | Cloud DNS supports per-RRset IAM conditions. Zone-wide admin is not the floor; the exact records are. |77| "A predefined role is simpler than a custom role" | Simpler to type, larger to breach. Custom roles are a one-time cost; see the recipe. |78| "credentials_json just to get it working" | A leaked key is a standing credential. WIF setup takes minutes; there is no keys-temporarily path. |79| "It'll need Cloud SQL HA when it grows" | Provision for today's requirements. Growth is a config change, not a sunk cost. |80| "Skipping dev, it's a small app" | Small apps get semver-tagged prod deploys too. Skipping the split is a decision the user makes, with a reason. |81| "The zone probably has no record with this name" | `gcloud dns record-sets list` takes two seconds. Check. |82| "VPC peering is simpler than PSC" | Peered firewalls can't reference the peer's SAs/tags, and CIDRs couple. PSC publishes one service, one direction, allowlisted. |83| "IPv4-only for now, v6 later" | Dual-stack is mandatory from day one. The only exception is a written one for Google-managed endpoints with no v6 path. |8485## Red flags — stop and reconsider8687- You wrote `roles/*.admin` for a deploy SA88- You wrote `credentials_json` or `gcloud iam service-accounts keys create`89- A GCP credential (of any form) ends up in frontend code, a device image, or an app config shipped outside Wendy's servers90- You picked a region, tier, or project name the user never confirmed91- Your design has no `-dev` stack and the user never said why92- You're about to create a DNS record without listing existing ones first93- Your design uses the `default` VPC, VPC peering, or a public IP on a VM or database94- An LB frontend, MIG, or public host is IPv4-only with no written exception