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
- The problem: several HTTP services, a single entry point
- Resource versus controller: the essential distinction
- An overview of controllers
IngressClassand the legacy annotation- Anatomy of an Ingress
pathType: the three values and the confusing cases- Hands-on: publishing the store and the API
- Host-based versus path-based routing and rewriting
- Everyday annotations
- Debugging
- Gateway API: the successor
- 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.
- 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.
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).
- 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.
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 70sThe 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.
IngressClass and the legacy annotation
IngressClass and the legacy annotationA 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-nginxIn 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.
- 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 nameConstraints that cause most of the failures:
- The Ingress and the Services must be in the same namespace. An Ingress in
rutas-norte-procannot point at a Service inrutas-norte-dev: it is a security design decision, not a technical limitation. - The
portcan benumberorname, but not both. Referencing it by name survives a change of number. hostaccepts a wildcard at the first level (*.rutasnorte.example), which matchesapi.rutasnorte.examplebut notwww.api.rutasnorte.examplenor the bare domain. Rules withouthostmatch 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.
pathType: the three values and the confusing cases
pathType: the three values and the confusing casespathType 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.
- 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: httpNotice 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/healthNAME 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.
- 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: httpHow 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).
- 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-snippetis 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.
- Debugging
kubectl describe ingress
Always the first stop:
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 syncWhat 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
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 200The 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".
- 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
applygoes fine and nothing works. Checkkubectl get pods -n ingress-nginxfirst. - 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 usesnginx.org/*. They are not interchangeable. - Using
kubernetes.io/ingress.class. Obsolete. UseingressClassName. - Expecting
Prefixto compare characters./apidoes not match/apiary. It compares segments. - Confusing
503with502. 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
Hostheader. Testing withcurl http://IP/without-H 'Host: ...'will give404even 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
413on 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/)"
doneThe 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: httpkubectl 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=3The 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-apiThe 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
- What Is Kubernetes?
- Kubernetes Architecture
- Key Concepts and Terminology
- Setting Up a Kubernetes Cluster
- The Kubernetes CLI: kubectl
- Objects, YAML Manifests and the Declarative Model
- The Course Project: the Rutas Norte Platform
Module 2: Core Kubernetes Components
- Pods
- ReplicaSets
- Deployments
- Updates, Rollbacks and Deployment Strategies
- Services
- Namespaces
- Labels, Selectors and Annotations
Module 3: Configuration and Secret Management
- ConfigMaps
- Secrets
- Environment Variables
- Resource Quotas and Limits
- LimitRanges and Quality of Service (QoS) Classes
- ServiceAccounts and API Access from Pods
Module 4: Networking in Kubernetes
- Cluster Networking
- Service Types
- Internal DNS and Service Discovery
- Ingress Controllers
- TLS and Certificate Management with cert-manager
- Network Policies
Module 5: Storage in Kubernetes
- Volumes
- Persistent Volumes
- Persistent Volume Claims
- Storage Classes
- Dynamic Provisioning, Expansion and Snapshots
- Backup and Restore of Persistent Data
Module 6: Advanced Kubernetes Concepts
- StatefulSets
- DaemonSets
- Jobs and CronJobs
- Init Containers, Sidecars and Multi-Container Patterns
- Scheduling: Affinity, Taints and Tolerations
- Custom Resource Definitions (CRDs)
- Operators and the Controller Pattern
Module 7: Monitoring and Logging
- Health Checks and Probes
- Metrics Server and kubectl top
- Monitoring with Prometheus
- Visualization and Alerting with Grafana and Alertmanager
- Centralized Logging with Elasticsearch, Fluentd and Kibana (EFK)
- Application Debugging and Cluster Events
Module 8: Kubernetes Security
- Role-Based Access Control (RBAC)
- Security Contexts and Container Hardening
- Pod Security Policies and Pod Security Standards
- Network Security
- Image Security
- Auditing, Scanning and Vulnerability Management
Module 9: Scaling and Performance
- Horizontal Pod Autoscaling
- Vertical Pod Autoscaling
- Cluster Autoscaling
- Event-Driven and Custom-Metric Scaling with KEDA
- High Availability: PodDisruptionBudgets and Topology
- Performance Tuning
Module 10: Kubernetes Ecosystem and Tooling
- Minikube and Local Environments with kind
- Kubeadm
- Helm
- Kustomize
- GitOps with Argo CD and Flux
- Managed Kubernetes: EKS, AKS and GKE
Module 11: Case Studies and Real-World Applications
- Deploying a Web Application
- Running Stateful Applications
- CI/CD with Kubernetes
- Deployment Strategies: Blue-Green and Canary
- Multi-Cluster Management
- Production Operations: Incidents, Runbooks and Costs
