- Published on
IngressClass Deep Dive — Running Multiple Ingress Controllers in One Cluster
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- Introduction: When One Controller Is Not Enough
- The IngressClass Resource and the Default Class
- Annotation vs spec.ingressClassName
- Controller Scope — Which Ingresses to Watch
- Multi-Controller Scenarios
- Conflict Prevention
- Cost and Operational Considerations
- Hands-On YAML
- Pitfalls and Troubleshooting
- Closing
- References
Introduction: When One Controller Is Not Enough
In a small cluster, a single Ingress Controller is enough. But as operational scale grows, situations naturally arise where one cluster needs more than one controller. For example, when you want to separate external traffic exposed to the internet from internal traffic exposed only on the corporate network; or when you want an independent entry point per team; or when you are gradually moving from ingress-nginx to a different controller.
The key mechanism here is IngressClass. IngressClass decides "which controller handles this Ingress resource." This article covers the structure of the IngressClass resource, default classes, multi-controller scenarios, controller scope settings, conflict prevention, and operational and cost considerations in depth from a practitioner's view.
The IngressClass Resource and the Default Class
IngressClass is a cluster-scoped resource that states, via spec.controller, which controller implementation owns this class.
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: nginx-external
spec:
controller: k8s.io/ingress-nginx
---
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: nginx-internal
spec:
controller: k8s.io/ingress-nginx
Note that both IngressClasses use the same controller string (k8s.io/ingress-nginx). The controller value only indicates "what kind of controller implementation this is"; the classes themselves are distinguished by name (nginx-external, nginx-internal). That is, you can run multiple instances of the same kind of controller, each owning a different class.
An Ingress resource declares which class it belongs to via spec.ingressClassName.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-ingress
spec:
ingressClassName: nginx-external
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service
port:
number: 80
The Default Class
Adding the following annotation to an IngressClass makes it the default class.
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: nginx-external
annotations:
ingressclass.kubernetes.io/is-default-class: "true"
spec:
controller: k8s.io/ingress-nginx
An Ingress that does not specify ingressClassName is automatically assigned to the default class. The caveat is that there must be only one default class in the cluster. Marking more than one as default makes it ambiguous which applies, so in a multi-controller environment it is safest to explicitly set ingressClassName on every Ingress.
Annotation vs spec.ingressClassName
In the past, the class was specified with the following annotation.
metadata:
annotations:
kubernetes.io/ingress.class: nginx
But this approach is deprecated, and the current recommended approach is the spec.ingressClassName field. The differences are as follows.
| Aspect | kubernetes.io/ingress.class (annotation) | spec.ingressClassName (field) |
|---|---|---|
| Status | deprecated | recommended |
| Validation | free string, no validation | can validate IngressClass existence |
| Default class | not supported | supported via is-default-class |
| Multi-controller | works but non-standard | standard mechanism |
In new environments, do not use the annotation; use spec.ingressClassName. That said, some controllers still recognize the annotation for backward compatibility, so the two approaches may coexist during migration. In that case, which approach takes precedence depends on the controller implementation, so standardizing on one reduces confusion.
Controller Scope — Which Ingresses to Watch
When running multiple instances of the same kind of controller, you must narrow each instance's scope so it processes only the Ingresses of the class it owns. ingress-nginx provides several arguments for this.
--ingress-class / class Settings
Assign an owned class to each controller instance. An example of ingress-nginx Helm values follows.
controller:
ingressClassResource:
name: nginx-external
enabled: true
default: false
controllerValue: "k8s.io/ingress-nginx"
ingressClass: nginx-external
electionID: ingress-nginx-external-leader
Deploy the internal controller with separate values.
controller:
ingressClassResource:
name: nginx-internal
enabled: true
default: false
controllerValue: "k8s.io/ingress-nginx"
ingressClass: nginx-internal
electionID: ingress-nginx-internal-leader
It is important to give a different electionID per instance here. Sharing the same leader-election key can cause the two instances to interfere with each other.
watch-namespace
You can also restrict a controller to watch Ingresses in a specific namespace only.
controller:
scope:
enabled: true
namespace: team-a
This way, the team A controller processes only Ingresses in the team-a namespace. Combining class-based separation with namespace-based separation enables finer isolation.
Multi-Controller Scenarios
Here are three scenarios that frequently appear in practice.
1. Internal/External Separation
The most common pattern. The external controller uses a LoadBalancer exposed to the internet, while the internal controller uses a corporate-network-only LoadBalancer (or internal LB).
Internet Internal Network
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ External LB │ │ Internal LB │
└────────┬─────────┘ └────────┬─────────┘
│ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ ingress-nginx │ │ ingress-nginx │
│ class: nginx-external │ │ class: nginx-internal │
└──────────┬───────────┘ └──────────┬───────────┘
│ │
▼ ▼
public services admin/internal services
Separate external Ingresses with ingressClassName: nginx-external and internal ones with ingressClassName: nginx-internal. This reduces accidents like exposing a sensitive service such as an admin page to the internet by mistake.
2. Per-Team Separation
A pattern of giving each team an independent controller in a multi-tenant environment. Each team uses only its class (e.g., team-a-nginx, team-b-nginx), and the controller is scoped to watch only the team namespace. This reduces the impact of one team's traffic surge or misconfiguration on other teams.
3. Migration
While moving from ingress-nginx to a different controller (e.g., Traefik, Envoy-based), the two controllers briefly coexist. Keep existing services on the existing class, move only new services or some traffic to the new class, then transition gradually. In the 2026 context, even when moving to Gateway API, the Ingress controller and the Gateway controller run in parallel for a while.
Conflict Prevention
The core risk of multi-controller is that two controllers process the same Ingress simultaneously, or expose the same host redundantly. Prevent it with these principles.
- Set ingressClassName on every Ingress: Not relying on the default class removes the ambiguity of "which controller will claim it."
- At most one default class: Do not mark more than one as default.
- Separate electionID: Use a different leader-election key for each instance of the same kind of controller.
- No host duplication: Ensure two classes do not expose the same host. During migration in particular, keep hosts separate until cutover.
- Narrow the scope: If needed, clearly restrict a controller's jurisdiction with watch-namespace.
Cost and Operational Considerations
Multi-controller gives isolation and safety but brings cost and operational complexity.
- Load balancer cost: Putting a LoadBalancer-type Service on each controller adds a cloud load balancer. You can cut cost by using an internal LB for the internal controller, or by branching on host behind a single LB.
- Resource overhead: Each controller consumes its own pods, memory, and CPU. More controllers means more things to manage.
- Operational consistency: If version, annotation syntax, and tuning parameters differ per controller, operations get harder. Where possible, splitting the same kind of controller into multiple classes is better for operational consistency.
- Observability: Collect metrics/logs per controller separately, and make it clear via dashboards which class handles which traffic.
Also, given that ingress-nginx is trending toward maintenance mode as of 2026, it is reasonable to also consider Gateway API's multi-Gateway model when designing new multiple entry points. Because Gateway API distinguishes multiple Gateways by GatewayClass, it can express multiple entry points in a standard way.
Hands-On YAML
A full example creating two classes, internal and external, and attaching an Ingress to each.
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: nginx-external
spec:
controller: k8s.io/ingress-nginx
---
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: nginx-internal
spec:
controller: k8s.io/ingress-nginx
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: public-api
namespace: shop
spec:
ingressClassName: nginx-external
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service
port:
number: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: admin-panel
namespace: shop
spec:
ingressClassName: nginx-internal
rules:
- host: admin.internal.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: admin-service
port:
number: 80
Verification commands after creation:
kubectl get ingressclass
kubectl get ingress -A -o custom-columns=NAME:.metadata.name,CLASS:.spec.ingressClassName,HOSTS:.spec.rules[*].host
Pitfalls and Troubleshooting
1. An Ingress is claimed by no controller.
Either ingressClassName differs from the name of an actually existing IngressClass, or there is no default class, leaving class-unspecified Ingresses unhandled. Cross-check the IngressClass list against the Ingress's class setting.
kubectl get ingressclass
kubectl describe ingress admin-panel
2. Two controllers process the same Ingress.
This can happen when controllers of the same kind do not narrow their class scope properly. Check that each controller's ingressClass setting and electionID are separated.
3. Behavior is unpredictable because there are two default classes.
Check whether is-default-class: "true" is on two IngressClasses, and keep only one.
kubectl get ingressclass -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.annotations.ingressclass\.kubernetes\.io/is-default-class}{"\n"}{end}'
4. An internal service got exposed to the internet.
This happens when an internal Ingress was wrongly given the external class. Re-check the ingressClassName of sensitive services, and consider enforcing it with policy (e.g., OPA/Kyverno).
5. Traffic mixes during migration. If the old class and new class expose the same host simultaneously, traffic gets split. Keep them separate by host or weight until cutover, then transition at once.
Closing
The starting point of multi-controller operation is understanding IngressClass precisely. Distinguish the controller kind with spec.controller, the instance with the IngressClass name, and specify which class handles it with the Ingress's spec.ingressClassName. Keep only one default class, separate same-kind controllers by ingressClass and electionID, and narrow jurisdiction with watch-namespace when needed.
Across the representative scenarios — internal/external separation, per-team isolation, and migration — multi-controller is a powerful tool. But it comes at the cost of load balancer expense and operational complexity, so it is important to keep to the conflict-prevention principles and to have observability. In the 2026 context, given the ingress-nginx maintenance trend, I recommend also considering Gateway API's multi-Gateway model for new multiple-entry-point designs.
References
- Kubernetes official docs — Ingress (IngressClass): https://kubernetes.io/docs/concepts/services-networking/ingress/#ingress-class
- Kubernetes official docs — Ingress Controllers: https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/
- ingress-nginx — Multiple controllers: https://kubernetes.github.io/ingress-nginx/user-guide/multiple-ingress/
- ingress-nginx official docs: https://kubernetes.github.io/ingress-nginx/
- Traefik official docs: https://doc.traefik.io/traefik/
- Project Contour official docs: https://projectcontour.io/docs/
- Gateway API official site: https://gateway-api.sigs.k8s.io/
- Kyverno official docs: https://kyverno.io/docs/
- cert-manager official docs: https://cert-manager.io/docs/
- HAProxy official docs: https://www.haproxy.com/documentation/