Environment Reference #
Every project ID, cluster name, context, hostname, and endpoint.
Migration status (2026-08-11). Staging runs fully on GCP. Production runs on AWS ECS + Aurora. The production GCP stack is up and serves a view-only canary at
app-gcp.funnelstory.ai; its Cloud SQL instance is still a demoted replica of Aurora. Every production GCP row below is real but not yet customer-facing.
Who can do what #
Access is tiered on purpose. Database credentials and secret payloads stay with the GCP admins. Everyone else gets read-only.
| Tier | Who | Can | Cannot |
|---|---|---|---|
| GCP admin | Vivek, Preetam | everything, including DB logins and secret values | — |
| Developer (read-only) | everyone else, on request | read pods, pod logs, ConfigMaps, Cloud SQL and Secret Manager metadata, Grafana | read secret values, log in to a database, exec into a pod, port-forward, restart a workload |
The developer tier is these four roles.
Ask in #back-end to be added:
roles/viewer # project-wide read
roles/gkehub.gatewayReader # read-only kubectl through Connect Gateway
roles/gkehub.viewer # list memberships, run get-credentials
roles/container.viewer # in-cluster RBAC read
roles/viewer deliberately excludes secretmanager.versions.access, cloudsql.instances.login, container.secrets.*, container.pods.exec, and container.pods.portForward.
Secret values stay unreadable, and the database stays unusable without a role, while ordinary debugging still works.
Developers also hold roles/aiplatform.user for Vertex AI, and roles/browser for org and folder traversal.
Granted on both projects as of 2026-08-11: Pulkit, Mamta, Nitin, Avadhut.
Members are Terraform-owned in gcp/envs/<env>/terraform.tfvars under developer_members.
Add someone by extending that list and applying, so the next plan keeps the binding instead of proposing to destroy it.
Two rules follow from this and are worth stating plainly:
- Logs → Grafana. Backend app logs live in the non-default
backend-app-logsbucket, and reading a custom bucket view needsroles/logging.viewAccessoron top ofroles/viewer. Grafana holds that grant through its own service account, so it needs no extra IAM for you. - Deploys → an
opsrepo Helm PR. Nobody deploys from a laptop. Config and image changes go throughk8s/deploy/<env>/values.yamland the deploy workflow — see update env vars.
Projects and regions #
| Staging | Production | |
|---|---|---|
| GCP project ID | funnelstory-infra-staging |
funnelstory-infra-production |
| Project number | see command below | 905900603049 |
| Region | us-west1 |
us-west1 |
| Zone (bastion) | us-west1-a |
us-west1-a |
gcloud projects describe funnelstory-infra-staging --format='value(projectNumber)'
GKE #
| Staging | Production | |
|---|---|---|
| Cluster | funnelstory-staging |
funnelstory-production |
| Mode | Autopilot, private control plane | Autopilot, private control plane |
| Release channel | REGULAR |
STABLE |
| Fleet membership | funnelstory-staging |
funnelstory-production |
| kubectl context | connectgateway_funnelstory-infra-staging_global_funnelstory-staging |
connectgateway_funnelstory-infra-production_global_funnelstory-production |
| App namespace | fs-apps |
fs-apps |
| Grafana namespace | observability |
observability |
| App hostname | app.staging.funnelstory.ai |
app-gcp.funnelstory.ai (view-only canary) |
The control plane has no public endpoint.
All kubectl access goes through GKE Connect Gateway — see gcloud setup.
Cloud SQL (PostgreSQL 15) #
Endpoints are listed so you can read dashboards and Terraform. Connecting needs a database role, which is admin-only — see psql to Cloud SQL.
| Staging | Production | |
|---|---|---|
| App instance | funnelstory-staging |
funnelstory-production |
| Writer private IP | 172.21.32.7 |
172.20.32.5 |
| Read replica instance | funnelstory-staging-replica |
not created yet |
| Read replica private IP | 172.21.32.13 |
— |
| Database | funnelstory |
funnelstory |
| Edition / tier | Enterprise, db-custom-2-8192 |
Enterprise Plus, db-c4a-highmem-4 |
| Automated backups | off | off — re-enable after promote |
| PITR | off | off — re-enable after promote |
| Micro instance | funnelstory-micro-staging (172.21.32.9, db sample_db) |
funnelstory-micro-production |
| Grafana metadata instance | grafana-staging (db grafana) |
grafana-production (db grafana) |
The production read replica is created at cutover. A DMS-managed destination cannot have replicas until it is promoted.
Private IPs can change. Re-check before you trust one:
gcloud sql instances describe funnelstory-staging \
--project=funnelstory-infra-staging --format='value(ipAddresses[0].ipAddress)'
Network ranges #
Every range is private. WARP routes all of them, so with WARP connected you can reach a Cloud SQL instance or a pod IP straight from your laptop. Network reach is not the access control — credentials and IAM are.
| Range | Staging | Production |
|---|---|---|
| Primary subnet (nodes, internal LBs) | 172.21.0.0/20 |
172.20.0.0/20 |
| Cloud SQL private services access | 172.21.32.0/20 |
172.20.32.0/20 |
| GKE private control plane endpoint | 172.21.48.0/28 |
172.20.48.0/28 |
| GKE pods | 10.8.0.0/16 |
10.16.0.0/16 |
| GKE Services | 10.9.0.0/20 |
10.17.0.0/20 |
VPC names are funnelstory-staging and funnelstory-production.
The CIDRs avoid AWS production (172.30.0.0/16) and AWS staging (172.31.0.0/16), because an HA VPN joins them during the migration.
Ten routes are published, five per environment.
A route reaches your laptop once its CIDR is in the WARP split-tunnel include list, which lives on the account-wide device profile (Zero Trust → Settings → WARP Client → Device settings → Split Tunnels) rather than in Terraform.
Check yours with warp-cli settings, and check the connection with warp-cli status.
A missing entry is the usual cause of a hang on a private IP.
Monitoring links #
| Staging | Production | |
|---|---|---|
| Grafana | grafana-staging.funnelstory.io | grafana-production.funnelstory.io |
| PgHero | gone — the Aurora cluster it read was deleted 2026-07-21 | pghero-primary-production.funnelstory.io (AWS Aurora) |
Both Grafana instances sit behind Cloudflare Access, which is the only gate — no GCP IAM needed. See Grafana and PgHero.
Terraform #
| Staging | Production | |
|---|---|---|
| Directory | gcp/envs/staging |
gcp/envs/production |
| State bucket | funnelstory-tf-state-staging |
funnelstory-tf-state-production |
| State prefix | envs/staging |
envs/production |
See Terraform.
Other resources #
| Staging | Production | |
|---|---|---|
| App GCS bucket | fsio-staging-funnelstory |
fsio-production-funnelstory |
| Tunnel keys bucket | fs-tunnel-keys-staging |
fs-tunnel-keys-production |
| Cloudflare tunnel name | Staging GCP |
Production GCP |
| Cloudflare account | one account for both — see cloudflare/envs/*/terraform.tfvars |
same account |
Container images live in the staging project for both environments:
us-west1-docker.pkg.dev/funnelstory-infra-staging/apps/{back-end,front-end,highcharts-export}
Production GKE pulls from the staging Artifact Registry. Zero-copy promotion means production runs the same bytes staging validated. The production node service account holds a repository-scoped reader grant.
Deploys #
| Staging | Production | |
|---|---|---|
| Trigger | automatic — push to main, or repository_dispatch from the app repos |
manual workflow_dispatch only |
| Workflow | .github/workflows/helm-deploy.yml |
.github/workflows/helm-deploy-production.yml |
| Approval gate | none | production GitHub Environment reviewers |
All task guides #
Read-only, any developer:
- gcloud CLI setup — install, authenticate, switch contexts
- GKE diagnostics — find pods, read why one restarted
- Debug backend logs —
kubectl logs, Cloud Logging, BigQuery - Grafana — dashboards and ad hoc SQL
- Get pprof dumps — pod IP over WARP, no extra IAM
Admin only:
- Terraform — plan and apply infrastructure
- psql to Cloud SQL — direct database access
- Update secrets — Secret Manager
- Update env vars — needs a merged PR, deploy is gated
- Backup and restore — clone and recover data