Skip to content

Accessing a deployed app

How a developer reaches a freshly-deployed workload on https://<app>.apps.example.net after a git push to one of the GitLab projects under the blueprint-apps group.

This doc focuses on the dev-side access path — what the developer's machine needs in place once, and what the running pipeline does so the URL becomes reachable.

TL;DR

  • After a push, the deploy:helm job runs helm upgrade --install with --set image.tag=$CI_COMMIT_SHORT_SHA. The new pods replace the old ones — the Service and HTTPRoute are unchanged, so the URL is live as soon as the rollout completes.
  • The pipeline sets environment.url: https://${CI_HELM_RELEASE}.apps.example.net on the deploy job. The URL appears as a clickable link in the GitLab pipeline UI under Operate → Environments → production.
  • For the demo guestbook app, the URL is https://guestbook.apps.example.net (default base domain is example.net; app URLs are namespaced under *.apps.example.net to keep them separate from the four infra FQDNs under *.example.net).
  • Reachability requires a one-time host-side setup (described below). After that, every redeploy is just a git push away.

End-to-end routing path

Hold "Alt" / "Option" to enable pan & zoom
graph TB
  Dev["Developer<br/>(edit apps/)"] -->|"uv run blueprint-phase3"| P3["blueprint-phase3"]
  P3 -->|"rsync + render + push"| GL["GitLab<br/>blueprint-apps/guestbook"]
  GL -->|"webhook → pipeline"| Runner["GitLab Runner<br/>(k8s executor)"]
  Runner -->|"kaniko build + push"| Reg["in-cluster registry<br/>gitlab-registry.gitlab.svc:5000"]
  Runner -->|"helm upgrade --install<br/>--set image.tag=$CI_COMMIT_SHORT_SHA"| Cluster["kind cluster"]
  Cluster -->|"Service frontend-http<br/>+ HTTPRoute guestbook<br/>parentRef sectionName=apps-https"| EG["Envoy Gateway<br/>(gitlab-gw data plane)<br/>listener apps-https<br/>NodePort 30443"]
  EG -->|"TLS terminated<br/>(wildcard cert *.apps.example.net)"| Browser["Developer browser<br/>https://guestbook.apps.example.net"]

Architecture: two listeners, two certs, one Envoy proxy

The chart-managed Envoy Gateway (gitlab-gw) owns a single Envoy proxy pod + NodePort service. That proxy serves two listeners with separate FQDN families:

Listener name Hostname TLS Secret allowedRoutes Purpose
gitlab-web, registry-web, kas-web, openbao-web *.example.net gitlab-wildcard-tls Same (chart default) The four GitLab infra FQDNs
apps-https *.apps.example.net apps-wildcard-tls All All Phase-3 apps

Why two certs instead of reusing the infra wildcard? Sharing one cert across two listeners with different hostnames triggers Envoy's OverlappingTLSConfig: OverlappingCertificates warning, which forces HTTP/1.1 (no HTTP/2 connection coalescing). A dedicated cert for each listener keeps ownership unambiguous.

Both certs descend from the same CA (infra/tls/wildcard/ca.pem), so a single trust anchor covers both FQDN families.

Apps attach their HTTPRoutes to the apps-https listener via:

parentRefs:
  - group: gateway.networking.k8s.io
    kind: Gateway
    name: gitlab-gw
    namespace: gitlab
    sectionName: apps-https   # ← names the listener on the Gateway

One-time host-side setup

  1. Trust the wildcard CA on the host machine. The Phase 2 installer mints a self-signed CA at infra/tls/wildcard/ca.pem. This CA signs BOTH the infra wildcard cert (*.example.net) and the apps wildcard cert (*.apps.example.net). Trust ca.pem in your system trust store or in your browser.

  2. Add /etc/hosts entries for the four infra FQDNs so the host machine resolves them to 127.0.0.1:

    127.0.0.1   gitlab.example.net
    127.0.0.1   registry.example.net
    127.0.0.1   kas.example.net
    127.0.0.1   minio.example.net
    
    App FQDNs (*.apps.example.net) do NOT need /etc/hosts entries — Docker's internal resolver handles them.

  3. Run the bootstrap port-forward in the background. This binds 127.0.0.1:8443 to the chart's Envoy Gateway Service and survives until killed:

    uv run blueprint-bootstrap --port-forward
    
    Port 8443 (not 443) because 443 on the host is reserved by the kind control-plane's API server. The same Gateway Service fronts gitlab.example.net, guestbook.apps.example.net, and every other FQDN under either wildcard — no per-app port-forward is needed.

  4. (Optional) Verify the demo app.

    curl -i https://guestbook.apps.example.net
    
    Should return HTTP/2 200 from the frontend pod (when run inside the cluster; from the host machine, replace the hostname with the resolved 127.0.0.1).

What happens on each redeploy

  1. Edit code in apps/<name>/ — canonical, frozen source.
  2. uv run blueprint-phase3:
  3. git pull --ff-only on the matching repo under apps-local/blueprint-apps/<name>/
  4. rsync from apps/<name>/ into the working tree
  5. render .gitlab-ci.yml from apps/shared-code/templates/per-project-app.gitlab-ci.yml.tpl (substitutes $name, $release, $namespace, $dockerfile_path)
  6. git commit + push origin main from apps-local/
  7. GitLab webhook fires → Runner picks up the pipeline:
  8. build:imagekaniko-executor builds the new image with --destination ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA} and pushes to the in-cluster registry.
  9. deploy:helmhelm upgrade --install <release> ./helm-chart --set image.tag=$CI_COMMIT_SHORT_SHA --wait. The chart renders Deployment + Service + HTTPRoute + ReferenceGrant. The rollout replaces the Deployment's pods one by one. The Service + HTTPRoute + Gateway listener are untouched, so the URL stays green throughout.
  10. The pipeline UI shows the environment URL (e.g. https://guestbook.apps.example.net) — click it to verify the new version.
  11. End-to-end time: ~60–120 s, dominated by the kaniko build (the chart rollout itself is ~5–10 s after the image push completes).

URL pattern

The URL for a given app is built from helm_release in infra/scripts/bootstrap/phase3/apps_manifest.yaml:

App helm_release URL
guestbook guestbook https://guestbook.apps.example.net
redis redis-master-demo n/a (no Ingress; accessed in-cluster only)
redis-slave redis-slave-demo n/a (no Ingress; accessed in-cluster only)
shared-code shared-code n/a (CI templates only, no workload)

Only guestbook has a user-facing URL because it's the only app with an HTTPRoute enabled in its helm values; the redis demos are intra-cluster dependencies.

Why no Ingress resource?

apps/guestbook/helm-chart/templates/guestbook-ingress.yaml renders an Ingress only when .Values.ingress.host is non-empty (default ingress.enabled: false). The chart-managed Envoy Gateway + HTTPRoute is the primary routing path for this blueprint, so the Ingress is intentionally opt-in. The default kubectl port-forward + Envoy Gateway combo covers all access without requiring an extra per-app Ingress resource.

Configuration

The base domain (example.net in the examples above) is set in infra/tofu/variables.tf:

# infra/tofu/variables.tf:16
variable "hosts_domain" {
  default     = "example.net"
  ...
}

Override via infra/tofu/terraform.tfvars or a -var flag on tofu apply. After changing the domain, re-run Phase 2 (it re-mints both wildcard certs with the new SAN) and re-push through blueprint-phase3.

Troubleshooting

Symptom Likely cause Fix
curl: (6) Could not resolve host for *.example.net (infra FQDNs) Missing /etc/hosts entries Re-check /etc/hosts; restart uv run blueprint-bootstrap --port-forward
curl: (6) Could not resolve host for *.apps.example.net (app FQDNs) Docker's internal resolver not intercepting docker network inspect bridge should show the apps.example.net subnet; restart port-forward
curl: (60) SSL certificate problem: unable to get local issuer certificate Wildcard CA not trusted by curl/browser trust infra/tls/wildcard/ca.pem into your system store (or browser's certificate manager)
502 Bad Gateway from https://<app>.apps.example.net HTTPRoute points to a Service that has no ready endpoints kubectl -n <app> get pods,svc,httproute; check the rollout completed (kubectl rollout status deploy/frontend)
connection refused on 127.0.0.1:8443 port-forward process died or never started pgrep -af port-forward; restart uv run blueprint-bootstrap --port-forward
GitLab environment URL doesn't appear Pipeline didn't reach deploy:helm (build or lint failed) Open the pipeline in the GitLab UI; the failed job's log explains
Old image still served after git push Image pull from in-cluster registry failed (cert trust on kind nodes) See docs/phase-2.md §14 for the containerd trust wiring (bootstrap/phase2/kind_node_trust.py)
Operator log: Service ... is invalid: spec.externalIPs[0]: Invalid value: "127.0.0.1" Chart added 127.0.0.1 to gitlab-gw.spec.addresses; controller propagates it to the data-plane Service The bootstrap's _patch_gateway_data_plane strips this automatically; on a fresh cluster run bootstrap --phase 2 again. If running outside the bootstrap, manually kubectl patch gateway gitlab-gw --type=json -p='[{"op":"replace","path":"/spec/addresses","value":[]}]'
Host-side curl hangs in TLS handshake (no RST, no response) externalTrafficPolicy: Local on the data-plane Service — kube-proxy DROPs NodePort traffic on the control-plane (no local Envoy endpoints there) Already enforced by infra/scripts/bootstrap/phase2/references/helm-values-gitlab.yaml (externalTrafficPolicy: Cluster) + the defensive patch in gitlab.py:_patch_gateway_data_plane. On a wiped cluster, re-run bootstrap --phase 2. To verify on a live cluster: kubectl -n gitlab get svc envoy-gitlab-gitlab-gw-80260869 -o jsonpath='{.spec.externalTrafficPolicy}' should return Cluster.

See also