Expose an app on an ingress¶
Decide who should reach it, then write the Ingress. Classes, issuers and DNS behaviour are in ingress classes and certificates.
1. Who should reach it?¶
| Audience | What to create |
|---|---|
| Only the house LAN | one private Ingress |
| Anyone, from anywhere | a public Ingress and a cloudflare Ingress, same host |
There is no single "public" Ingress that works both inside and outside. The pair is required, and section 3 explains why.
2. LAN only¶
Host it under ankhmorpork.thaum.xyz:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: private
rules:
- host: myapp.ankhmorpork.thaum.xyz
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp
port:
name: http
tls:
- hosts:
- myapp.ankhmorpork.thaum.xyz
secretName: myapp-tls
That is the whole job. external-dns publishes the name to UniFi's resolver and
cert-manager obtains the certificate; neither is mentioned in the manifest.
3. Reachable from outside¶
Use a krupa.net.pl host, and write two Ingresses for it. The second differs
only in class, and carries no TLS block or issuer:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp-cloudflare
spec:
ingressClassName: cloudflare
rules:
- host: myapp.krupa.net.pl
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp
port:
name: http
Both are needed, for opposite reasons:
- Without the
cloudflareone, off-LAN clients still resolve the name — the*.krupa.net.plwildcard sends them to Cloudflare — but the tunnel has no route for it, so they get nothing. - Without the
publicone, external-dns publishes only the tunnel CNAME (*.cfargotunnel.com), which resolves nowhere internally. The house resolver hands out a name with no usable address.
Keep the two names distinct (myapp and myapp-cloudflare). If a Helm chart also
renders an Ingress, set ingress.enabled: false in its values and own both here —
a chart upgrade that disables its Ingress will otherwise delete any Ingress
sharing that name.
4. Validate¶
validate-ingress-contract denies on apply, so a mistake here is a failed
reconcile rather than a warning. The four that catch people:
ingressClassNamemissing — the default class exists but the policy still requires the field- the deprecated
kubernetes.io/ingress.classannotation used instead - a
publicorprivateIngress with nospec.tls - a TLS entry with no
secretName
5. After it rolls out¶
The certificate takes a minute or two:
Once READY is True, the host answers. If it does not:
# Did external-dns publish it?
kubectl -n platform-network logs deploy/external-dns | grep myapp
# Did the ingress controller accept it?
kubectl -n myapp describe ingress myapp
A name that resolves but times out from off-LAN is almost always the missing
cloudflare Ingress from section 3.
Requiring a login¶
Neither class authenticates anything. To put a login in front, see require a login.