End-to-end setup for a fresh GKE cluster: Envoy Gateway as the ingress data
plane, cert-manager issuing Let's Encrypt certs through the Gateway API, and
Dex deployed at https://identity.example.com.
Namespace layout:
envoy-gateway-system— Envoy Gateway controller (installed by Helm).cert-manager— cert-manager controller, webhook, cainjector.gateway— theGatewayresource and the TLS secrets cert-manager issues for its listeners.identity— Dex and itsHTTPRoute.
kubectlcontext pointed at the target cluster.helminstalled.- DNS record for
identity.example.compointing at the Envoy Gateway's external IP. The IP only exists after step 3, so create the record then.
Reference: https://gateway.envoyproxy.io/docs/tasks/security/tls-cert-manager/
helm install eg oci://docker.io/envoyproxy/gateway-helm \
--version v1.7.2 \
-n envoy-gateway-system --create-namespaceWe deliberately skip the upstream quickstart.yaml — it creates a
GatewayClass, a Gateway in default, and a demo backend. We ship our
own GatewayClass and Gateway in gateway.yaml so
nothing of ours lands in default.
crds.enabled=true is required — without it, the post-install
startupapicheck job times out because the CRDs are missing.
config.enableGatewayAPI=true turns on the Gateway API integration so
cert-manager can issue certs for Gateway listeners.
helm repo add jetstack https://charts.jetstack.io
helm install \
cert-manager jetstack/cert-manager \
--version v1.17.0 \
--create-namespace --namespace cert-manager \
--set crds.enabled=true \
--set config.apiVersion="controller.config.cert-manager.io/v1alpha1" \
--set config.kind="ControllerConfiguration" \
--set config.enableGatewayAPI=truegateway.yaml creates:
- The
gatewaynamespace. - A
GatewayClassnamedegbound to the Envoy Gateway controller. - A
Gatewaynamedegin thegatewaynamespace with two listeners:- HTTP on
:80— required for the ACME HTTP-01 challenge. - HTTPS on
:443foridentity.example.com, terminating TLS using the secretidentity-example-com-tls.
- HTTP on
- The
cert-manager.io/cluster-issuer: letsencrypt-prodannotation, so cert-manager auto-creates aCertificatefor every TLS listener and writes the cert into the secret named incertificateRefs. allowedRoutes.namespaces.from: Allon both listeners, so per-appHTTPRoutes can live with their app rather than centrally.
kubectl apply -f gateway.yamlOnce the Gateway is Programmed=True, grab its external IP and create the
A record for identity.example.com:
kubectl get gateway eg -n gateway \
-o jsonpath='{.status.addresses[0].value}'cluster-issuer.yaml defines a letsencrypt-prod
ClusterIssuer that solves HTTP-01 challenges by attaching a temporary
HTTPRoute to the eg Gateway in the gateway namespace.
kubectl apply -f cluster-issuer.yamlUpdate the email: field before applying if reusing this for a different
project — Let's Encrypt sends expiry notifications to it.
This step has to come after step 3: cert-manager only starts issuing once
both the Gateway (with its cluster-issuer annotation) and the
ClusterIssuer exist, but the ClusterIssuer's HTTP-01 solver references the
Gateway, so the Gateway must already be there.
dex.yaml bundles everything for the identity service:
identityNamespaceHTTPRoutebindingidentity.example.com→ thedexService via theidentity-httpslistener on the Gateway ingateway- Dex
Secret(config),Deployment,Service,ServiceAccount
kubectl apply -f dex.yamlThe Secret ships a development static-password setup. Replace
staticPasswords and staticClients before exposing this to anything that
matters.
# Certificate (auto-created by cert-manager from the Gateway annotation)
# should reach Ready=True within ~1–2 minutes.
kubectl get certificate -n gateway -w
# Gateway listeners should report Programmed=True.
kubectl get gateway eg -n gateway -o yaml | yq '.status.listeners'
# Dex should respond with its discovery document over HTTPS.
curl -fsS https://identity.example.com/.well-known/openid-configuration | jq .Assumes the GCP infrastructure (buckets, AlloyDB, IAM bindings) has
already been provisioned via the polarsignals-gcp terraform module per
polarsignals/deploy/public-byoc/GCP.md.
gateway.yaml carries HTTPS listeners for the three
public cloud endpoints in addition to identity:
cloud.example.com(main UI)api.cloud.example.com(REST API)grpc.cloud.example.com(gRPC API)
Apply (or re-apply) the Gateway, then point all three hostnames at the Gateway's external IP via DNS A records:
kubectl apply -f gateway.yaml
kubectl get gateway eg -n gateway \
-o jsonpath='{.status.addresses[0].value}'cert-manager will issue a Let's Encrypt cert per listener as soon as DNS resolves. Watch progress:
kubectl get certificate -n gateway -wThe chart renders ServiceMonitor, PrometheusRule, and
ServiceLevelObjective resources. We install only the CRDs so
kubectl apply of generated.yaml succeeds — no controllers required:
# monitoring.coreos.com/v1 (ServiceMonitor, PrometheusRule)
kubectl apply --server-side -f \
https://github.com/prometheus-operator/prometheus-operator/releases/latest/download/stripped-down-crds.yaml
# pyrra.dev/v1alpha1 (ServiceLevelObjective)
kubectl apply --server-side -f \
https://raw.githubusercontent.com/pyrra-dev/pyrra/main/examples/kubernetes/manifests/setup/pyrra-slo-CustomResourceDefinition.yamlThe CRDs alone make the kubectl apply validate; nothing scrapes the
metrics or evaluates the rules until you bring up the matching
controllers.
The chart's release namespace is polarsignals-byoc (set in the
Makefile). Three Secrets must exist there before applying generated.yaml:
| File | Contents |
|---|---|
db-secret.yaml |
polarsignals-db — Postgres URL for the AlloyDB proxy sidecar. |
token-secret.yaml |
polarsignals-token-signing-key — opaque signing key. |
oidc-secret.yaml |
polarsignals-oidc — {clientID, clientSecret} matching the Dex private-client. |
values.yaml sets global.oidc.issuerURL to the Dex URL,
so both --oidc.issuer-url and --sharing-oidc.issuer-url in the
rendered manifests point at Dex.
polarsignals-routes.yaml wires each
hostname to the matching chart Service:
cloud.example.com→Service/main-ui(HTTPRoute)api.cloud.example.com→Service/apiporthttp(HTTPRoute)grpc.cloud.example.com→Service/apiportgrpc(GRPCRoute)
make deploy applies all of the above (gateway, issuer, dex, namespace,
secrets, the rendered chart, and the routes) in dependency order. It does
not re-render the chart — run make generate first if values.yaml
changed.
make generate # only if values.yaml changed
make deployThe sharing endpoints (sharing.cloud.example.com,
api.sharing.cloud.example.com, grpc.sharing.cloud.example.com)
are not wired up yet — add three more listeners + routes following the
same pattern when you need them.
Follow the same per-app pattern Dex uses:
- Add a new TLS listener to
gateway.yamlwith a uniquename, the newhostname, and a freshcertificateRefs[0].name. - Apply the updated
gateway.yaml. cert-manager will issue a new cert into the named secret in thegatewaynamespace. - In the app's namespace, create an
HTTPRoutewhoseparentRefspoints ateg/gatewaywithsectionNamematching the listener name.