Kubernetes Ingress Generator
Build a networking.k8s.io/v1 Ingress with the required fields present and the controller-specific parts named as what they are. It says which controller reads each annotation, what the TLS secret has to look like, and where a host or path silently matches nothing.
ingress.yaml
updates as you type Common mistakes
These are the ones that fail silently. The config is accepted, nothing raises an error, and the consequence arrives later.
Omitting pathType
It is required since networking.k8s.io/v1. Prefix and Exact behave differently, and ImplementationSpecific hands the decision to the controller.
Instead:Set it explicitly. Prefix is usually what people mean.
Referencing a TLS secret that does not exist yet
The Ingress is accepted and serves the controller's default certificate, so the failure is a certificate warning rather than an error.
Instead:Create the secret first, or watch for the controller event.
Assuming annotations are portable
nginx, traefik and cloud controllers each have their own annotation namespaces, and one controller ignores another's entirely.
Instead:Match the annotations to the controller actually installed.
What an Ingress actually controls, and what the controller controls
An Ingress object is a small amount of routing plus a large amount of delegation. Kubernetes validates the shape of the rules and then hands the whole thing to a controller, which decides what the annotations mean, which certificate to serve, and what to do when two objects claim the same path. That split is why almost every Ingress problem is silent: the manifest applies, the status stays empty, and the behaviour is decided somewhere the API never looks.
pathType is required, and the three values match differently
In networking.k8s.io/v1 every path needs a pathType and there is no default, which is why bumping an old beta manifest to v1 gets it rejected. Prefix compares whole path segments, Exact compares the string, and ImplementationSpecific means the controller decides, which is the only mode where a regular expression in the path is read as one.
path: /api
request Prefix Exact
/api yes yes
/api/ yes no trailing slash
/api/v1 yes no
/apiary no no not a segment
pathType: ImplementationSpecific
path: /api(/|$)(.*) read as a regex by
ingress-nginx, and as
something else, or as a
literal, by everyone else Nothing serves an Ingress that names no class
spec.ingressClassName points at an IngressClass object and is the supported mechanism since 1.18. kubernetes.io/ingress.class is the annotation it replaced. Setting both is where it gets interesting: the API does not define a precedence, so each controller decided for itself and they disagree. Setting neither is quieter still, because the Ingress is then only claimed if some IngressClass is marked as the cluster default.
spec:
ingressClassName: nginx # supported
metadata:
annotations:
kubernetes.io/ingress.class: "nginx" # deprecated
# both set: controller-dependent, and in the bad case
# two controllers each claim it and the same host is
# served from two load balancers
kubectl get ingressclass
NAME CONTROLLER PARAMETERS AGE
nginx k8s.io/ingress-nginx <none> 40d
kubectl get ingress
NAME CLASS HOSTS ADDRESS PORTS
web nginx app.example.com 80
^ empty forever if no
controller claims it The TLS secret, and the certificate that hides the mistake
spec.tls names a Secret, and it has to be of type kubernetes.io/tls with tls.crt and tls.key, in the same namespace as the Ingress. There is no cross-namespace reference. Get any of that wrong and nothing fails: ingress-nginx falls back to its own self-signed certificate, so the site answers on 443, the browser warns, and the Ingress status is exactly as empty as it was when everything was correct.
kubectl create secret tls web-tls \
--cert=fullchain.pem --key=privkey.pem -n default
kubectl get secret web-tls -o jsonpath='{.type}'
kubernetes.io/tls
# wrong namespace, wrong type or missing keys:
openssl s_client -connect app.example.com:443 \
-servername app.example.com
subject=O = Acme Co, CN = Kubernetes Ingress
Controller Fake Certificate
# that subject line is the whole diagnosis A wildcard host covers exactly one label, on the left
Ingress host matching allows a single wildcard and only as the leftmost label. It stands for exactly one label: not zero, so the apex is not covered, and not two, so a deeper subdomain is not covered either. Certificates behave the same way, which is why a wildcard certificate plus an apex redirect is a pair rather than one thing.
host: "*.example.com"
a.example.com matches
www.example.com matches
example.com no: one label short
a.b.example.com no: one label too many
host: "*.eu.example.com" valid
host: "a.*.example.com" rejected: not leftmost
host: "*example.com" rejected: needs "*."
host: "*.*.example.com" rejected: one only
# the wildcard also has to be quoted in YAML, or a
# bare * is parsed as an alias node Annotations belong to a controller, not to Kubernetes
The API server does not validate annotations, so an annotation meant for a controller you are not running is accepted, stored, and ignored without an error, an event or a log line. The symptom is the feature simply not happening. This also applies between the two nginx controllers, which are different projects with different annotation prefixes.
nginx.ingress.kubernetes.io/rewrite-target: /$2
read by: ingress-nginx (kubernetes/ingress-nginx)
ignored by: Traefik, ALB, HAProxy, nginx.org
nginx.org/rewrites: "serviceName=api rewrite=/"
NGINX Inc's controller. Different project, and
the two do not share annotations.
traefik.ingress.kubernetes.io/router.middlewares:
default-strip-api@kubernetescrd
alb.ingress.kubernetes.io/target-type: ip
the ALB controller has no rewrite at all: it can
redirect, so the prefix has to be handled in the
application rewrite-target keeps only what the path captured
ingress-nginx rewrites the URI to exactly the annotation's value. Anything from the original request that should survive has to come out of a capture group in the path, and a Prefix path has no groups. That is why the documented pattern replaces the path with a regex and sets pathType to ImplementationSpecific: the annotation on its own sends every request to the same place.
# what people write
path: /api
pathType: Prefix
rewrite-target: /
/api/v1/users -> / everything, always
# what actually works
path: /api(/|$)(.*)
pathType: ImplementationSpecific
rewrite-target: /$2
/api -> /
/api/v1/users -> /v1/users The backend block, and the field that was renamed
A backend port is either a number or a name, never both, and the name refers to a port on the Service rather than on the container. Service port names are IANA_SVC_NAME: 15 characters at most. The other v1 rename to know is spec.backend, which became spec.defaultBackend, and which only covers requests reaching this Ingress and matching none of its rules.
backend:
service:
name: api
port:
number: 8080 # or:
name: http # must match a Service port
# name, max 15 characters
spec:
defaultBackend: # spec.backend in v1beta1
service:
name: fallback
port:
number: 80
# a port name that matches nothing gives an Ingress
# with no endpoints, not an error Two Ingresses, one host and path
Each Ingress is validated on its own, so nothing stops two of them claiming the same host and path. The API has no answer for what happens next and leaves it to the controller. ingress-nginx settles it by creation timestamp, so the older object wins and the newer one is dropped with a log line most people never read. Splitting a host across several Ingresses is supported and normal, as long as the paths do not collide.
ingress-a app.example.com / created 09:00
ingress-b app.example.com / created 11:00
ingress-nginx:
ingress-a wins, ingress-b is ignored, and the
controller logs that host and path are already
defined in another ingress
# fine, and the usual way a host is split:
ingress-a app.example.com /
ingress-b app.example.com /api