Environments

Every app moves through the same ladder: preview environments per pull request, then dev → qa → prod. dev, qa, and preview all live on the shared nonprod cluster; prod is a separate cluster reached only by a manually-approved promotion.

At a glance

EnvTriggerCluster / projectNamespaceHostnameGateway / TLS
devpush to mainu2i-nonprod / c-u2i-nonprod{app}-devdev.{app}.u2i.deveg (Envoy) + Let's Encrypt
qaversion tag v*u2i-nonprod / c-u2i-nonprod{app}-qaqa.{app}.u2i.deveg (Envoy) + Let's Encrypt
previewpull requestu2i-nonprod / c-u2i-nonprod{app}-pr-{N}pr-{N}.{app}.u2i.deveg (Envoy) + Let's Encrypt
prodmanual promotion (approval)u2i-prod / c-u2i-prod{app}-prod{app}.u2i.devmanaged-lb + Certificate Manager

dev

Continuous integration target — always reflects main.

  • Fires on every push to main via the {app}-dev-trigger Cloud Build trigger.
  • Builds the image (tag dev-{SHA}), packages the Helm chart, pushes it to the config-sync OCI registry, and updates the RepoSync — Config Sync then reconciles it into the {app}-dev namespace.
  • Rolls out automatically; no approval gate.
  • Served in-cluster via the Envoy Gateway (gatewayMode: eg) with a cert-manager Let's Encrypt certificate; external-dns publishes the A record automatically.

qa

Release candidate — what you cut a version tag for.

  • Fires when a version tag (v*) is pushed, via the {app}-qa-deployment trigger.
  • Creates a release in the {app}-qa-prod-pipeline; the qa target deploys automatically.
  • Runs on the same nonprod cluster as dev, in the {app}-qa namespace, with prod-like resource limits for realistic testing.
  • Same eg gateway + Let's Encrypt TLS as dev.

preview (per-PR)

Ephemeral environment for reviewing a pull request.

  • Fires when a PR is opened, via the {app}-preview-deployment trigger.
  • Spins up a dynamic {app}-pr-{N} namespace and applies the manifests directly (bypassing Config Sync) so the review URL comes up fast.
  • Runs on spot nodes with smaller resource and cache limits to keep previews cheap.
  • Reachable at pr-{N}.{app}.u2i.dev; the namespace is torn down automatically when the PR is closed.

prod

Production — promoted deliberately, never on a push.

  • Reached by promoting a qa release through the {app}-qa-prod-pipeline; the prod target is gated behind a manual approval from the app's approver_group.
  • Runs on the separate u2i-prod cluster / c-u2i-prod project, in the {app}-prod namespace.
  • The prod cluster has no in-cluster Envoy gateway, so it uses gatewayMode: managed-lb — a Google Cloud load balancer with a global static IP and a Certificate Manager cert-map.
  • DNS A-records for the apex hostname are declared explicitly (no external-dns auto-publish in prod).

Domains & DNS conventions

Which domain an app lives on depends on whether it is internal or customer-facing. The rule of thumb: u2i.dev is our internal / nonprod world; customer-facing production lives on its own brand domain.

Internal projects → u2i.dev

Every internal project gets a subdomain under u2i.dev. The apex is the internal-prod tier; deeper subdomains are the nonprod tiers. For a project titra: prod is titra.u2i.dev, and dev / qa / preview are dev.titra.u2i.dev, qa.titra.u2i.dev, pr-N.titra.u2i.dev. All of it — including the prod apex — can sit in one nonprod-hosted DNS zone; u2i.dev is internal, so there is no need to split it across projects.

Customer-facing production → its own brand domain

When an app has a real production domain (e.g. retrotool.io), production lives there and nowhere else — a prod-only DNS zone in the prod project, records managed automatically. Its nonprod tiers still live under u2i.dev (retrotool.u2i.dev, dev.retrotool.u2i.dev, …). Do not put dev / qa / preview under the brand domain: keeping pre-release off the customer domain avoids search-engine indexing of staging, brand confusion, and cert/DNS sprawl on the brand domain.

How the records actually get written

Nonprod hostnames are published automatically by external-dns — an add-on (not native GKE) that watches each app's HTTPRoute, reads the gateway's IP, and writes the matching A-record. Each environment's external-dns can only write to DNS zones in its own GCP project. That is why an internal-prod apex whose zone lives in the nonprod project (e.g. retrotool.u2i.dev today) is declared as an explicit in-code record instead: the prod cluster's external-dns cannot reach a nonprod-hosted zone. A genuine customer-prod domain avoids this entirely — its zone lives in the prod project, so its own external-dns manages it automatically.

Two layers: DNS lives in one place, serving in another

For an internal-domain prod apex (e.g. gcp-platform-docs.u2i.dev), it's worth separating two things. The DNS — the zone and its A-record — lives entirely in the nonprod project (that's where the u2i.dev subzone was created). The serving stack — the pod, the HTTPRoute, the TLS cert, the Gateway, and the static IP the record points at — lives entirely in prod, on the shared managed-lb cluster-gateway. So the record in nonprod is just a pointer to the prod Gateway's IP; the traffic goes to prod. A real customer domain collapses this — its zone sits in prod too, so both layers are in the same place and no cross-project declarative record is needed.