Understanding Kubernetes Ingress: Routing External Traffic to Your Services
-
Ahmed Muhi - 20 Jun, 2025
Introduction
In the previous article, we looked at Kubernetes Services and the problem they solve.
A ReplicaSet can keep the right number of pods running, but those pods still change. They get new names, new IP addresses, and they can be replaced at any time. A Service gives those changing pods a stable address inside the cluster. Other pods do not need to know which frontend pod is running right now. They can call the Service, and the Service sends traffic to one of the pods behind it.
That solved the internal traffic problem.
But there is still another question: how does someone outside the cluster reach your application?
A ClusterIP Service works inside Kubernetes. It gives other pods a stable way to reach your application, but it does not give users on the internet a clean web address. You could expose a Service directly with NodePort or LoadBalancer, and sometimes that is enough. But web applications usually need more than just an open port.
They need hostnames.
They need paths.
They need HTTP and HTTPS routing.
You might want shop.example.com to go to one Service, api.example.com to go to another, and /admin to route somewhere different again. You do not want to create a separate external load balancer for every small routing decision.
That is the gap Ingress fills.
Ingress gives Kubernetes a way to route external HTTP and HTTPS traffic to Services inside the cluster. The Service still sends traffic to the pods. Ingress sits one step above that and decides which Service should receive the request in the first place.
That gives us the next piece of the picture:
Pod: runs the application
ReplicaSet: keeps enough pods running
Service: gives those pods a stable internal address
Ingress: routes external HTTP/HTTPS traffic to Services
In this article, we will look at what Ingress does, why it is different from a Service, how host and path routing work, and why an Ingress controller is needed to make Ingress rules actually do something.
What Ingress Solves
A Service gives your pods a stable address, but that address is mostly useful inside the cluster.
That is fine when one pod needs to call another pod. For example, a frontend pod can call an API Service, or an API pod can call a database Service. Everything starts inside Kubernetes, so a ClusterIP Service works well.
But users are outside the cluster.
They are not going to call a pod IP. They are not going to call a ClusterIP. They expect a normal web address, like:
https://shop.example.com
or:
https://example.com/api
That is a different kind of problem.
You are no longer just asking, “How does one pod reach another pod?”
You are asking:
When an external HTTP request arrives, which Service should receive it?
That is what Ingress solves.
Ingress lets you define routing rules for external HTTP and HTTPS traffic. You can route by hostname, by path, or by both.
For example, you might want:
shop.example.com → frontend Service
api.example.com → api Service
Or you might want one hostname with different paths:
example.com/ → frontend Service
example.com/api → api Service
example.com/admin → admin Service
Without Ingress, you would usually need to expose each Service separately. That might mean multiple external load balancers, multiple public IP addresses, and more networking pieces to manage.
Ingress gives you one cleaner entry point.
The request comes in from outside the cluster. Ingress looks at the hostname and path, then sends the request to the right Service. The Service then sends it to one of the pods behind it.
The flow looks like this:
External user → Ingress → Service → Pod
That is the main idea.
A Service gives stable access to a group of pods. Ingress gives external web traffic a clean way to reach the right Service.
Ingress and Ingress Controllers
There is one important detail to understand before we write the YAML.
An Ingress is a set of routing rules. It says things like:
Send traffic for shop.example.com to the frontend Service.
Send traffic for api.example.com to the api Service.
Send traffic for example.com/admin to the admin Service.
But the Ingress object does not move traffic by itself.
It is only the rule.
Something in the cluster has to read that rule, listen for incoming HTTP and HTTPS requests, and send those requests to the right Service. That something is called an Ingress controller.
This is the difference:
Ingress: the routing rule
Ingress controller: the thing that enforces the rule
You can think of it like a road sign and a traffic officer.
The Ingress is the sign. It says where traffic should go.
The Ingress controller is the traffic officer. It watches the signs and directs the traffic.
In a real cluster, the Ingress controller might be NGINX Ingress Controller, Traefik, HAProxy, Azure Application Gateway Ingress Controller, Cilium, or another implementation. They do not all work exactly the same way, but they all play the same basic role: they make Ingress rules real.
This is different from some Kubernetes objects you have already seen.
When you create a ReplicaSet, the ReplicaSet controller is already part of Kubernetes. When you create a Service, Kubernetes already has the machinery to give it a stable address and route traffic to pods.
Ingress is different. Kubernetes gives you the Ingress resource, but your cluster still needs an Ingress controller installed to do the actual routing.
So before you use Ingress, remember this:
Creating an Ingress rule is not enough.
You also need an Ingress controller watching and applying that rule.
Once that is in place, the flow looks like this:
External user → Ingress controller → Service → Pod
The Ingress rule decides which Service should receive the request. The Ingress controller is the component that receives the request and follows that rule.
An Ingress in YAML
Now that the idea is clear, let’s write an Ingress rule.
In this example, we want traffic for shop.example.com to go to the frontend Service.
Here is the YAML:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: frontend-ingress
spec:
rules:
- host: shop.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: frontend
port:
number: 80
Read it from the top, and the shape should look familiar: apiVersion, kind, metadata, and spec.
The apiVersion is different this time:
apiVersion: networking.k8s.io/v1
Ingress belongs to the networking part of the Kubernetes API, so it uses networking.k8s.io/v1.
The kind is Ingress:
kind: Ingress
That tells Kubernetes this object is an Ingress rule.
The name is just the name of this Ingress object:
metadata:
name: frontend-ingress
Then comes the important part: the spec.
rules:
- host: shop.example.com
This rule applies when the request is for shop.example.com.
So if a user opens:
https://shop.example.com
this is the rule that should handle it.
Inside that host rule, we define HTTP paths:
http:
paths:
- path: /
pathType: Prefix
The path / means the root of the website. Because the pathType is Prefix, this rule also matches anything underneath /, such as /products, /cart, or /login.
Then we tell Ingress where to send matching traffic:
backend:
service:
name: frontend
port:
number: 80
This is the destination.
It says: send matching requests to the Service named frontend on port 80.
Notice what the Ingress points to. It does not point directly to pods. It points to a Service.
That is the pattern:
Ingress → Service → Pods
The Ingress decides which Service should receive the request. The Service then sends the request to one of the pods behind it.
So in plain English, this Ingress says:
When a request comes in for shop.example.com, send it to the frontend Service on port 80.
That is the whole rule. One hostname, one path, one Service behind it.
How the Request Flows
Now let’s follow one request through the setup.
A user opens:
https://shop.example.com/products
The request first reaches the Ingress controller. Remember, the Ingress rule is only the instruction. The Ingress controller is the component that receives the traffic and applies that instruction.
The controller looks at the request and asks two questions.
First:
Which hostname is this request for?
The answer is:
shop.example.com
That matches the host in our Ingress rule.
Then it asks:
Which path is being requested?
The answer is:
/products
Our rule uses:
path: /
pathType: Prefix
So /products matches, because it starts with /.
Now the Ingress controller knows where to send the request. The backend in the rule points to the frontend Service on port 80.
So the request moves like this:
User → Ingress controller → frontend Service → one frontend pod
The Service still does the same job it did in the previous article. It finds the pods labelled as frontend pods and sends traffic to one of them.
That separation is important.
The Ingress controller does not need to track every frontend pod. It only needs to know which Service should receive the request.
The Service does not need to understand hostnames like shop.example.com or paths like /products. It only needs to know which pods sit behind it.
Each object has one job:
Ingress: choose the Service
Service: choose the Pod
Pod: run the application
That is the clean mental model. External traffic comes in through the Ingress controller, the Ingress rule chooses the right Service, and the Service forwards the request to one of the pods behind it.
Why Not Just Use a LoadBalancer Service?
A LoadBalancer Service can expose an application to the outside world.
That is useful, and in some cases it is enough. If you have one application and you want one external IP address pointing to it, a LoadBalancer Service can do that job.
But web applications often need more than that.
You might have one Service for the frontend, another Service for the API, and another Service for an admin area. You might want different hostnames to go to different Services:
shop.example.com → frontend Service
api.example.com → api Service
Or you might want one hostname with different paths:
example.com/ → frontend Service
example.com/api → api Service
example.com/admin → admin Service
If you expose each Service separately, you can quickly end up with multiple external load balancers, multiple public IP addresses, and more networking pieces to manage.
Ingress gives you a cleaner model.
Instead of exposing every Service directly, you expose one HTTP and HTTPS entry point. The Ingress rules then decide which Service should receive each request based on the hostname and path.
So the difference is simple:
LoadBalancer Service: expose this one Service
Ingress: route web traffic to the right Service
A LoadBalancer Service is still useful, and many Ingress controllers use one behind the scenes to receive external traffic. But Ingress gives you the routing layer that a Service by itself does not provide.
Where This Leads
You now have the next piece of the Kubernetes traffic path.
A pod runs your application. A ReplicaSet keeps enough copies of that pod running. A Service gives those pods a stable address inside the cluster. Ingress gives external HTTP and HTTPS traffic a clean way to reach the right Service.
The full path now looks like this:
External user → Ingress controller → Service → Pod
Each object has its own job:
Pod: runs the application
ReplicaSet: keeps enough pods running
Service: gives pods a stable internal address
Ingress: routes external HTTP/HTTPS traffic to Services
That separation is what makes the model easier to reason about. Ingress does not replace Services. It builds on top of them. Services do not replace pods. They give stable access to them. ReplicaSets do not expose applications. They keep the right number of pods alive.
There is one more important step in the workload story.
So far, we have looked at ReplicaSets directly because they make the core idea easy to see: keep this many pods running. But when you release real applications in Kubernetes, you usually want more than just a fixed number of pods. You want to roll out new versions, replace old pods gradually, pause a rollout if something goes wrong, and roll back if the new version is broken.
That is the job of a Deployment.
A Deployment sits above ReplicaSets. You describe the version of the application you want, and the Deployment manages the ReplicaSets needed to get there. That is the next step in the series: moving from keeping pods alive to managing application releases.