We reach the moment the course has been promising since module 1: publishing Rutas Norte on the internet. We have five components deployed, all with ClusterIP Services that exist only inside the cluster, and no customer can buy a ticket. In 04-02 we saw that LoadBalancer would solve the problem at the price of one balancer and one public IP per service, with no routing by domain or by path. This lesson presents the right answer: a single HTTP entry point that distributes traffic according to the requested domain and path. By the end, www.rutasnorte.example will serve the store and api.rutasnorte.example will serve the API, both from the same balancer, and you will clearly distinguish between the Ingress resource —a rule that does nothing on its own— and the Ingress controller, the program that turns it into real configuration.

Contents

  1. The problem: several HTTP services, a single entry point
  2. Resource versus controller: the essential distinction
  3. An overview of controllers
  4. IngressClass and the legacy annotation
  5. Anatomy of an Ingress
  6. pathType: the three values and the confusing cases
  7. Hands-on: publishing the store and the API
  8. Host-based versus path-based routing and rewriting
  9. Everyday annotations
  10. Debugging
  11. Gateway API: the successor

  1. The problem: several HTTP services, a single entry point

Rutas Norte needs to publish, as a minimum:

Public destination Component Notes
www.rutasnorte.example web-store Massive traffic on bank holidays
api.rutasnorte.example bookings-api Consumed by the SPA and by agencies
www.rutasnorte.example/api bookings-api Alternative without a separate domain
admin.rutasnorte.example Internal panel Office network only

With type: LoadBalancer that would be four balancers, four public IPs and four TLS certificates managed separately, multiplied by the three environments. And even then the essential part would be missing: an L4 balancer does not understand HTTP. It cannot read the Host header to decide which service a request goes to, nor look at the path, nor rewrite it, nor add headers. Ingress moves the decision to layer 7:

flowchart TB
    C1["Client<br/>www.rutasnorte.example"] --> LB
    C2["Client<br/>api.rutasnorte.example"] --> LB
    LB["ONE LoadBalancer Service<br/>single public IP"] --> IC["Ingress controller<br/>(nginx pods in the cluster)"]
    IC -->|"Host: www.rutasnorte.example"| S1["Service web-store<br/>ClusterIP"]
    IC -->|"Host: api.rutasnorte.example"| S2["Service bookings-api<br/>ClusterIP"]
    IC -->|"/admin"| S3["Service admin-panel<br/>ClusterIP"]
    S1 --> P1["web-store pods"]
    S2 --> P2["bookings-api pods"]

One balancer, one IP, one place to terminate TLS (04-05), and all the internal services stay ClusterIP.

  1. Resource versus controller: the essential distinction

This is the number one confusion of the lesson and it must be nailed down before writing anything.

Ingress resource Ingress controller
What it is An API object, networking.k8s.io/v1 A program running in pods in the cluster
What it does Nothing. It declares an intention Watches the Ingresses and routes the real traffic
Who creates it You, with kubectl apply The administrator, once per cluster
Analogy The traffic sign drawn on a plan The tarmac, the lanes and the officer
If the other is missing It is created without error and nothing happens It has nowhere to send anything

The symptom of creating an Ingress with no controller is deceptive: kubectl apply answers created, kubectl get ingress lists it and the domain does not respond. The clue is in the ADDRESS column, empty forever.

NAME          CLASS   HOSTS                          ADDRESS   PORTS   AGE
rutas-norte   nginx   www.rutasnorte.example,api...             80      12m

A controller is, on the inside, a loop identical to the one from module 1: it watches the API, reads the Ingresses, Services and EndpointSlices, generates a configuration file (an nginx.conf, in ingress-nginx) and reloads its proxy, using a ServiceAccount with permissions (03-06).

  1. An overview of controllers

Controller Based on Strengths Things to bear in mind
ingress-nginx NGINX The most widespread; maintained by the Kubernetes project; huge catalogue of annotations Reloads its configuration on change; many features live in non-standardised annotations
Traefik Go, its own Dynamic configuration with no reloads; native ACME integration; good dashboard Its advanced features use its own CRDs (IngressRoute)
HAProxy Ingress HAProxy Performance and stability at L4/L7; excellent for high load Smaller community
AWS ALB / GKE / AGIC Cloud balancer Traffic never even enters a pod; integrates with WAF and managed certificates Tied to the provider; less fine-grained control
Istio Gateway / Cilium Mesh / eBPF mTLS, L7 policies, deep telemetry Considerable complexity; usually comes with a full mesh

Beware one historical detail: there are two controllers based on NGINX. ingress-nginx is the Kubernetes project's one; "NGINX Ingress Controller" is F5/NGINX Inc's. Their annotations are not compatible, and many hours have been lost copying the one from the wrong controller. Here we use ingress-nginx, the one in the minikube addon.

minikube -p rutas-norte addons enable ingress
kubectl get pods -n ingress-nginx
NAME                                        READY   STATUS      RESTARTS   AGE
ingress-nginx-admission-create-9k2lm        0/1     Completed   0          70s
ingress-nginx-admission-patch-x4dpq         0/1     Completed   0          70s
ingress-nginx-controller-7d4b8f6c8d-2vq7z   1/1     Running     0          70s

The two Completed ones are Jobs (06-03) that install the validating webhook: thanks to it, an Ingress with invalid syntax is rejected at apply time instead of breaking the proxy configuration.

  1. IngressClass and the legacy annotation

A cluster can have several controllers: a public one for web-store and bookings-api, an internal one for the admin panel. IngressClass says which controller serves which Ingress.

apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
  name: nginx
  annotations:
    ingressclass.kubernetes.io/is-default-class: "true"   # serves classless Ingresses
spec:
  controller: k8s.io/ingress-nginx

In the Ingress it is referenced with the field spec.ingressClassName: nginx. Before Kubernetes 1.18 this was done with the annotation kubernetes.io/ingress.class: "nginx", which still works in many controllers for compatibility but is discouraged and will disappear. You will see a huge number of examples on the internet using it: they are old. Use ingressClassName.

If you specify no class and none is marked as the default, no controller picks up the Ingress: empty ADDRESS and silence.

  1. Anatomy of an Ingress

apiVersion: networking.k8s.io/v1     # CAREFUL: v1, not the obsolete extensions/v1beta1
kind: Ingress
metadata:
  name: rutas-norte
  namespace: rutas-norte-pro         # must be the SAME as the Services'
spec:
  ingressClassName: nginx            # which controller serves it

  defaultBackend:                    # optional: where anything matching no rule goes
    service:
      name: web-store
      port:
        number: 80

  rules:
    - host: www.rutasnorte.example   # optional: with no host, the rule matches any domain
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web-store      # name of a Service in the SAME namespace
                port:
                  number: 80         # or name: http, if the port has a name

Constraints that cause most of the failures:

  • The Ingress and the Services must be in the same namespace. An Ingress in rutas-norte-pro cannot point at a Service in rutas-norte-dev: it is a security design decision, not a technical limitation.
  • The port can be number or name, but not both. Referencing it by name survives a change of number.
  • host accepts a wildcard at the first level (*.rutasnorte.example), which matches api.rutasnorte.example but not www.api.rutasnorte.example nor the bare domain. Rules without host match any domain: handy for testing by IP, dangerous in production.

defaultBackend is the destination for anything that matches nothing: without it, the controller returns its generic 404; with it, a customer who mistypes the subdomain ends up in the store.

  1. pathType: the three values and the confusing cases

pathType is mandatory as of networking.k8s.io/v1 and its semantics surprise almost everybody.

Value How it matches Recommendation
Exact Literal exact match, case-sensitive Specific paths, with no descendants
Prefix By path segments separated by /, not by characters The de facto default value
ImplementationSpecific Decided by the controller; in ingress-nginx it enables regular expressions Only when you need regex, and knowing it is not portable

The key to Prefix is in those three words "by path segments". /api is not a character prefix: it matches /api and /api/whatever, but it does not match /apiary.

Rule Request Match? Why
/api Prefix /api Yes Exact
/api Prefix /api/ Yes Complete segment
/api/ Prefix /api Yes The trailing slash is ignored when matching
/api Prefix /api/bookings/33 Yes Descendant by segments
/api Prefix /apiary No apiary is a different segment
/api Exact /api Yes
/api Exact /api/ No The trailing slash makes it different
/api Exact /api/bookings No There are no descendants
/API Exact /api No It is case-sensitive

Tie-breaking rule when several match: the longest path wins, regardless of pathType.

paths:
  - path: /              # matches everything
    pathType: Prefix
    backend: { service: { name: web-store,    port: { number: 80 } } }
  - path: /api           # longer: wins for /api/*
    pathType: Prefix
    backend: { service: { name: bookings-api, port: { number: 3000 } } }

A request to /api/bookings matches both rules, and goes to bookings-api because /api is longer than /. This ordering by length, rather than by position in the YAML, avoids the "I put the more specific rule at the top and it does not work" mistake.

  1. Hands-on: publishing the store and the API

A single Ingress publishes both Rutas Norte domains.

# k8s/environments/pro/ingress-rutas-norte.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: rutas-norte
  namespace: rutas-norte-pro
  labels:
    app.kubernetes.io/part-of: rutas-norte
    environment: pro
  annotations:
    # Body size: the PDF receipts uploaded by customer support
    nginx.ingress.kubernetes.io/proxy-body-size: "8m"
    # The payment gateway can be slow; give it some room
    nginx.ingress.kubernetes.io/proxy-read-timeout: "60"
spec:
  ingressClassName: nginx
  rules:
    - host: www.rutasnorte.example
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web-store
                port:
                  name: http           # referenced by port name
    - host: api.rutasnorte.example
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: bookings-api
                port:
                  name: http

Notice what you do not have to do: the Services are not changed to NodePort or LoadBalancer. They stay ClusterIP, because the one talking to them is the controller, which is inside the cluster.

Applying and checking:

kubectl apply -f k8s/environments/pro/ingress-rutas-norte.yaml
kubectl get ingress -n rutas-norte-pro

# The .example domains do not exist in public DNS: resolve them by hand
echo "$(minikube -p rutas-norte ip) www.rutasnorte.example api.rutasnorte.example" \
  | sudo tee -a /etc/hosts

curl -s -o /dev/null -w 'store: %{http_code}\n' http://www.rutasnorte.example/
curl -s -w '\n' http://api.rutasnorte.example/health
NAME          CLASS   HOSTS                                          ADDRESS        PORTS   AGE
rutas-norte   nginx   www.rutasnorte.example,api.rutasnorte.example   192.168.49.2   80      40s

store: 200
{"status":"ok","version":"2.4.1","environment":"pro"}

ADDRESS holding the node's IP means the controller has picked up the Ingress. That field takes anything from a few seconds to a couple of minutes to appear; if it is still empty after five, something is wrong (section 10).

Rutas Norte's customers can now reach the platform: it is the first time in the course that traffic really comes in from outside the cluster. That the routing is done by the Host header and not by the IP is what we will check in exercise 1: same IP, same port, different results depending on the Host. That is L7 routing.

  1. Host-based versus path-based routing and rewriting

The alternative to two domains is serving the API under /api of the main domain.

By host (api.rutasnorte.example) By path (www.rutasnorte.example/api)
TLS certificates One per domain (or a wildcard) Just one
DNS records One per subdomain One
CORS in the SPA Yes: different origin No: same origin
Cookies Not shared between subdomains (unless configured) Shared
Path rewriting Not needed Almost always needed
Scaling separately Very easy Easy

Rutas Norte uses both: api.rutasnorte.example for the external agencies, and www.rutasnorte.example/api so the SPA does not have to deal with CORS.

Path rewriting

The problem: the SPA asks for /api/bookings but bookings-api expects /bookings; without rewriting it would receive /api/bookings and return 404.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: rutas-norte-api-by-path
  namespace: rutas-norte-pro
  annotations:
    # $2 = second capture group of the path regular expression
    nginx.ingress.kubernetes.io/rewrite-target: /$2
    nginx.ingress.kubernetes.io/use-regex: "true"
spec:
  ingressClassName: nginx
  rules:
    - host: www.rutasnorte.example
      http:
        paths:
          - path: /api(/|$)(.*)          # group 1: /(or end), group 2: the rest
            pathType: ImplementationSpecific
            backend:
              service:
                name: bookings-api
                port:
                  name: http

How to read that expression: /api matches the literal prefix; (/|$) is group 1 (a slash or the end of the string, so that both /api and /api/... match); (.*) is group 2, everything that comes after; and rewrite-target: /$2 rebuilds the path with group 2 alone.

Browser request Group 2 What bookings-api receives
/api/bookings bookings /bookings
/api/bookings/33 bookings/33 /bookings/33
/api (empty) /
/api/health?v=2 health /health?v=2 (the query string is preserved)

And the warning: pathType becomes ImplementationSpecific because we are using a regular expression, something Prefix does not cover. That Ingress is not portable to Traefik or to the AWS ALB. It is a conscious trade-off.

A much-repeated caveat: rewriting affects the request path, not the URLs the application generates in its HTML or in its redirects. If bookings-api answers Location: /bookings/33, the browser will go there and not to /api/bookings/33. Applications served under a prefix must know about it (typically through a BASE_PATH variable).

  1. Everyday annotations

Annotations are the mechanism by which each controller exposes its features; these are the ingress-nginx ones used on any real platform.

Annotation What for Typical value in Rutas Norte
proxy-body-size Maximum body size. 413 Request Entity Too Large if exceeded 8m
proxy-read-timeout / proxy-send-timeout Seconds to wait on the backend. 504 when exhausted 60
proxy-connect-timeout Wait to establish the connection 5
limit-rps / limit-connections Request and connection limiting per client IP 20 rps
limit-whitelist IPs exempt from the limit Office range
whitelist-source-range Only these IPs may access Admin panel
configuration-snippet Bespoke NGINX configuration fragment Security headers
enable-cors / cors-allow-origin CORS headers handled by the proxy For the external agencies
backend-protocol HTTP, HTTPS, GRPC HTTP

An example applied to the bank-holiday and summer traffic peaks:

metadata:
  annotations:
    # Limiting: 20 requests per second per client, with burst headroom
    nginx.ingress.kubernetes.io/limit-rps: "20"
    nginx.ingress.kubernetes.io/limit-burst-multiplier: "3"
    # The office network is never limited
    nginx.ingress.kubernetes.io/limit-whitelist: "10.20.30.0/24"
    # Security headers towards the client
    nginx.ingress.kubernetes.io/configuration-snippet: |
      more_set_headers "X-Content-Type-Options: nosniff";
      more_set_headers "X-Frame-Options: SAMEORIGIN";

Two important warnings:

  • configuration-snippet is disabled by default in recent versions of ingress-nginx for security reasons (it allowed arbitrary proxy configuration to be injected from any namespace). It has to be enabled in the controller's ConfigMap, and it is worth thinking twice about.
  • The limiting is applied per controller replica. With three replicas, a 20 rps limit is in practice 60.

Global settings (default headers, buffer sizes, log format) do not go in annotations but in the controller's ConfigMap, ingress-nginx-controller in the ingress-nginx namespace.

  1. Debugging

kubectl describe ingress

Always the first stop:

kubectl describe ingress rutas-norte -n rutas-norte-pro
Name:             rutas-norte
Namespace:        rutas-norte-pro
Address:          192.168.49.2
Ingress Class:    nginx
Rules:
  Host                      Path  Backends
  ----                      ----  --------
  www.rutasnorte.example
                            /     web-store:http (10.244.0.22:8080,10.244.0.29:8080)
  api.rutasnorte.example
                            /     bookings-api:http (10.244.0.31:3000)
Events:
  Type    Reason  Age   From                      Message
  ----    ------  ----  ------------------------  -------
  Normal  Sync    35s   nginx-ingress-controller  Scheduled for sync

What to look at, in this order: Address filled in (somebody has picked up the Ingress); the right Ingress Class; the backends with pod IPs in brackets —if it says <error: endpoints "web-store" not found> or shows an empty list, the problem is not the Ingress, it is the Service, and you go back to the diagnosis of 02-05—; and Events with Sync, which confirms the controller applied the configuration.

Controller logs

kubectl logs -n ingress-nginx -l app.kubernetes.io/component=controller -f
192.168.49.1 - - [05/Aug/2026:11:20:14 +0000] "GET /bookings/33 HTTP/1.1" 200 812
  "-" "curl/8.5.0" 141 0.043 [rutas-norte-pro-bookings-api-http] [] 10.244.0.31:3000 812 0.042 200

The fields in brackets are gold: [rutas-norte-pro-bookings-api-http] is the chosen upstream, and 10.244.0.31:3000 the specific pod. If the upstream appears empty, no rule matched.

Failure chart

Symptom Probable cause Check
ADDRESS empty after 5 min No controller, or ingressClassName does not match kubectl get pods -n ingress-nginx; kubectl get ingressclass
404 from the controller (nginx) No rule matches: wrong Host or path curl -H 'Host: ...'; review pathType
503 Service Temporarily Unavailable The Service has no endpoints kubectl get endpointslices -n <ns>
502 Bad Gateway The pod refuses the connection or the targetPort is wrong kubectl exec and curl the pod directly
504 Gateway Time-out The backend takes longer than proxy-read-timeout Raise the timeout or fix the slowness
413 Body larger than proxy-body-size Adjust the annotation
Works by IP but not by domain Bad /etc/hosts or DNS getent hosts www.rutasnorte.example
The Ingress is not created The validating webhook rejects it Read the message: usually an invalid regex or a malformed path

The distinction is worth memorising: 503 means there is nobody to send to (empty endpoints) and 502 means there is somebody to send to but they do not answer properly. Different layers.

The definitive check: the generated configuration

Looking at the nginx.conf the controller generated from your Ingress settles any argument about what is going on: kubectl exec -n ingress-nginx deploy/ingress-nginx-controller -- cat /etc/nginx/nginx.conf | grep -A5 "server_name api.rutasnorte.example".

  1. Gateway API: the successor

Ingress has aged badly. Its specification is so minimal that everything interesting ended up in proprietary annotations: rewriting, CORS, rate limiting, headers... None of it is portable. It also mixes responsibilities: whoever administers the infrastructure and whoever publishes an application edit the same object.

The Gateway API (gateway.networking.k8s.io) is the official answer, already stable for its main resources.

Concept Resource Who manages it
Which implementation is used GatewayClass Cluster administrator
The entry point: ports, TLS, domains Gateway Platform team
The HTTP rules: hosts, paths, filters, weights HTTPRoute Application team
Other protocols TCPRoute, GRPCRoute, TLSRoute Application team

Concrete advantages over Ingress: role separation (the Rutas Norte team creates its HTTPRoute without touching the entry point); features in the model, not in annotations —rewriting, redirection, headers and weighted splitting are API fields, so the canary of 11-04 no longer needs specific annotations—; cross-namespace routes with explicit permission from the Gateway owner; and real portability between implementations.

Ingress is not deprecated and will keep working for many years: too much is deployed. But for a new platform in 2026 the Gateway API is worth evaluating, especially if Istio, Cilium or Traefik are already in use. Here we carry on with Ingress because it is what you will run into and what the module 12 certifications assess.

Common Mistakes and Tips

  • Creating the Ingress with no controller. The apply goes fine and nothing works. Check kubectl get pods -n ingress-nginx first.
  • Pointing at a Service in another namespace. Not possible. The Ingress lives in the namespace of its Services.
  • Copying annotations from the wrong controller. nginx.ingress.kubernetes.io/* belongs to ingress-nginx; F5's uses nginx.org/*. They are not interchangeable.
  • Using kubernetes.io/ingress.class. Obsolete. Use ingressClassName.
  • Expecting Prefix to compare characters. /api does not match /apiary. It compares segments.
  • Confusing 503 with 502. The first is "no endpoints"; the second, "the pod does not answer properly". Different diagnoses.
  • Rewriting paths without the application knowing. The links and redirects it generates will still lack the prefix. Configure the application's BASE_PATH.
  • Forgetting that routing depends on the Host header. Testing with curl http://IP/ without -H 'Host: ...' will give 404 even when everything is fine.
  • Tip: one Ingress per application, not one giant one per cluster. They are deployed and rolled back independently.
  • Tip: the timeout and body-size annotations are among the first ones needed in production. Set them from the start with sensible values instead of waiting for the first 413 on a bank holiday.

Exercises

Exercise 1: Publish both Rutas Norte domains

Enable the minikube ingress addon, create the Ingress that publishes www.rutasnorte.example against web-store and api.rutasnorte.example against bookings-api, resolve the domains in /etc/hosts and check both routes. Also prove that the routing is done by the Host header.

Exercise 2: Serve the API under /api with rewriting

Add to the www.rutasnorte.example domain a /api path that reaches bookings-api without the prefix. Verify in the controller logs which path the backend actually receives and what happens with /apiary.

Exercise 3: Diagnose three broken Ingresses

Reproduce and diagnose: (A) an Ingress with ingressClassName: traefik in a cluster with only ingress-nginx; (B) a correct Ingress whose Service points at a selector that does not match; (C) an Ingress whose backend.service.port.number is 8080 when the Service exposes 80. State the exact symptom of each and the command that identifies it.

Solutions

Exercise 1

minikube -p rutas-norte addons enable ingress
kubectl wait --for=condition=ready pod \
  -l app.kubernetes.io/component=controller -n ingress-nginx --timeout=120s
kubectl apply -f k8s/environments/pro/ingress-rutas-norte.yaml
kubectl get ingress -n rutas-norte-pro -w      # wait for ADDRESS to appear

# Host-based routing: same IP, three results
IP=$(minikube -p rutas-norte ip)
for H in www.rutasnorte.example api.rutasnorte.example invented.example; do
  printf '%-28s %s\n' "$H" "$(curl -s -o /dev/null -w '%{http_code}' -H "Host: $H" http://$IP/)"
done
www.rutasnorte.example       200
api.rutasnorte.example       200
invented.example             404

The 404 on the third proves that the decision is taken by reading the Host header, not the destination IP.

Exercise 2

# k8s/environments/pro/ingress-api-by-path.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: rutas-norte-api-by-path
  namespace: rutas-norte-pro
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /$2
    nginx.ingress.kubernetes.io/use-regex: "true"
spec:
  ingressClassName: nginx
  rules:
    - host: www.rutasnorte.example
      http:
        paths:
          - path: /api(/|$)(.*)
            pathType: ImplementationSpecific
            backend:
              service:
                name: bookings-api
                port:
                  name: http
kubectl apply -f k8s/environments/pro/ingress-api-by-path.yaml
curl -s http://www.rutasnorte.example/api/health
kubectl logs -n ingress-nginx -l app.kubernetes.io/component=controller --tail=3
"GET /api/health HTTP/1.1" 200 ... [rutas-norte-pro-bookings-api-http] 10.244.0.31:3000

The client asked for /api/health; the backend received /health. With /apiary, the regex /api(/|$)(.*) would match (because use-regex compares characters, not segments) and the backend would receive /ary — one more reason to prefer Prefix except when rewriting is unavoidable, and to anchor the expression better if it is a problem.

Exercise 3

Case Symptom Command that identifies it
A non-existent class ADDRESS permanently empty; no Sync event; the domain does not answer kubectl get ingressclass (only nginx exists) and kubectl describe ingress with no events
B selector that does not match The Ingress is fine and has an ADDRESS, but returns 503 kubectl describe ingress shows the backend with no IPs; kubectl get endpointslices empty
C non-existent port 503 and an explicit error in describe <error: endpoints "bookings-api" not found> or an unresolved backend; the Ingress port must match the Service's port, not the container's
kubectl describe ingress <name> -n rutas-norte-pro | sed -n '/Rules/,/Annotations/p'
kubectl get endpointslices -n rutas-norte-pro -l kubernetes.io/service-name=bookings-api

The mistake in C is especially frequent: the Ingress port is the Service's (port), not the container's (targetPort). Confusing them produces an Ingress that looks correct and returns 503.

Conclusion

Rutas Norte is published. You understand the distinction that organises everything else: the Ingress resource is a declaration of intent that on its own does absolutely nothing, and the Ingress controller is the program that watches it, generates proxy configuration and actually routes the traffic; if the second is missing, the first is created without errors and the ADDRESS stays empty forever. You know the landscape of controllers, the danger of confusing ingress-nginx with F5's identically named controller, and you know how to select yours with ingressClassName instead of the obsolete kubernetes.io/ingress.class annotation.

You have mastered the object's anatomy: rules with host and http.paths, backend.service.name with the Service's port (not the container's), and defaultBackend for anything that matches nothing. You are clear that pathType: Prefix compares path segments and not characters —/api does not match /apiary— that Exact distinguishes the trailing slash and letter case, that ImplementationSpecific is the door to regular expressions at the cost of portability, and that when several rules match the longest one wins.

Above all, you have done the work: you enabled the ingress addon, published www.rutasnorte.example against web-store and api.rutasnorte.example against bookings-api with a single Ingress and a single entry point, resolved the domains with /etc/hosts and proved that the distribution is done by reading the Host header. You added the /api path with rewrite-target, understanding the regular expression group by group, and you know the annotations needed in production from day one: body size, timeouts, request limiting and security headers. You know how to debug with describe, with the controller logs and, as a last resort, by reading the generated nginx.conf, and you can tell a 503 (no endpoints) from a 502 (backend not answering) without hesitation. And you know that the Gateway API is the successor, with role separation and features in the model rather than in annotations.

One serious problem remains, and it is not one that can wait: all of this runs over plain HTTP. The personal data customers type in to make a booking —name, ID number, phone, email— and the data travelling towards the payment gateway are crossing the internet unencrypted. In 04-05 we will put HTTPS on both domains: first with a self-signed certificate for development, and then with cert-manager issuing and automatically renewing Let's Encrypt certificates through the ACME HTTP-01 and DNS-01 challenges.

Kubernetes Course

Module 1: Introduction to Kubernetes

Module 2: Core Kubernetes Components

Module 3: Configuration and Secret Management

Module 4: Networking in Kubernetes

Module 5: Storage in Kubernetes

Module 6: Advanced Kubernetes Concepts

Module 7: Monitoring and Logging

Module 8: Kubernetes Security

Module 9: Scaling and Performance

Module 10: Kubernetes Ecosystem and Tooling

Module 11: Case Studies and Real-World Applications

Module 12: Preparing for Kubernetes Certification

© Copyright 2026. All rights reserved