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:helmjob runshelm upgrade --installwith--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.neton the deploy job. The URL appears as a clickable link in the GitLab pipeline UI under Operate → Environments → production. - For the demo
guestbookapp, the URL ishttps://guestbook.apps.example.net(default base domain isexample.net; app URLs are namespaced under*.apps.example.netto 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 pushaway.
End-to-end routing path¶
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¶
-
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). Trustca.pemin your system trust store or in your browser. -
Add
/etc/hostsentries for the four infra FQDNs so the host machine resolves them to127.0.0.1:App FQDNs (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*.apps.example.net) do NOT need/etc/hostsentries — Docker's internal resolver handles them. -
Run the bootstrap port-forward in the background. This binds
Port127.0.0.1:8443to the chart's Envoy Gateway Service and survives until killed:8443(not443) because443on the host is reserved by the kind control-plane's API server. The same Gateway Service frontsgitlab.example.net,guestbook.apps.example.net, and every other FQDN under either wildcard — no per-app port-forward is needed. -
(Optional) Verify the demo app.
Should returnHTTP/2 200from 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¶
- Edit code in
apps/<name>/— canonical, frozen source. uv run blueprint-phase3:git pull --ff-onlyon the matching repo underapps-local/blueprint-apps/<name>/rsyncfromapps/<name>/into the working tree- render
.gitlab-ci.ymlfromapps/shared-code/templates/per-project-app.gitlab-ci.yml.tpl(substitutes$name,$release,$namespace,$dockerfile_path) git commit + push origin mainfromapps-local/- GitLab webhook fires → Runner picks up the pipeline:
- build:image —
kaniko-executorbuilds the new image with--destination ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}and pushes to the in-cluster registry. - deploy:helm —
helm 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. - The pipeline UI shows the environment URL (e.g.
https://guestbook.apps.example.net) — click it to verify the new version. - 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:
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¶
- docs/phase-3.md — full Phase 3 runbook
- docs/phase-2.md §14 — NodePort data-plane exposure — how the host-side port-forward maps to the Envoy Gateway NodePort
- docs/smart-docs/3-workflow-overview.md — control-plane + data-plane sequence diagrams
- docs/smart-docs/deep-dive/sample-apps.md — the four demo apps + per-app chart structure
- docs/smart-docs/deep-dive/phase-3-pipeline.md — internal mechanics of the per-project pipeline
infra/scripts/bootstrap/phase2/apps_gateway.py— the listener-patch installerinfra/scripts/bootstrap/phase2/references/apps-gateway-listener.yaml— the declarative patch document