6 min read
Ingress controller retirement is an architecture migration
Start with the boundary, not the manifest
The retirement of Ingress NGINX in March 2026 is a reason to revisit an important service boundary, not just to translate one set of Kubernetes resources into another. The Kubernetes retirement statement says the project will receive no further releases or security patches after retirement, and warns that available alternatives are not direct drop-in replacements.
That last point is the architectural one. A controller migration changes what happens between an external request and an application. If I begin by searching for equivalent annotations, I risk preserving the visible configuration while missing responsibilities that were quietly attached to it: TLS termination, redirects, access restrictions, request limits, or routing decisions. Some of those may belong at the edge; others may belong in an application or a surrounding platform. The right split depends on the contract I need to preserve.
I would start by writing down what the current edge promises to callers and what it requires from services. That means more than listing hostnames and paths. For each route, I want to know which traffic it accepts, where it sends requests, what TLS behavior callers observe, and what policy is enforced before traffic reaches the application. I also want to identify controller-specific configuration whose purpose is not obvious from its name.
This inventory is not a Kubernetes manifest. It is a working description of responsibilities, with enough detail to expose assumptions before I choose a replacement:
routes:
- name: public-api
match: "api.example.invalid/*"
destination: "api-service"
edge-responsibilities:
- tls-termination
- redirect-http-to-https
- request-size-policy
application-responsibilities:
- authentication
- authorization
assumptions-to-check:
- "Clients depend on the current redirect behavior"
- "The service does not rely on controller-added headers"
The example uses reserved .invalid names and describes a hypothetical service, not a claim about a real deployment. The purpose is to force questions into the open. Is a policy genuinely an edge concern, or did it land there because the old controller made it convenient? Does the application make an assumption about headers or the original scheme? Is a redirect part of the public behavior, or an implementation detail? I would answer those before changing the resources.
Separate the public contract from its implementation
A replacement should preserve the behavior that external callers rely on, while allowing the implementation behind that boundary to change. I would write the contract in terms a client can observe: the hostname and path, the TLS expectations, the response to an HTTP request, and the behavior when a request is rejected or routed to an unavailable service. Then I would note the internal assumptions that make that behavior possible.
This distinction stops two opposite mistakes. The first is treating every old controller setting as essential and reproducing it blindly. That carries old coupling forward, including settings nobody can explain. The second is assuming that a new controller’s defaults are equivalent because the basic route works. A successful request proves only that one path works; it does not establish that the security policy, redirects, or failure behavior survived.
I would be especially deliberate about the division between edge and application security. Authentication and authorization often need application context, while some traffic controls may be enforced before a request reaches a service. I would not move a policy just to make a migration easier, nor keep it at the edge just because it was there before. For each policy, I would name its owner, the information it needs, and the failure mode if it is absent. If ownership is unclear, that is a design gap to resolve rather than a YAML detail.
Portability matters here, but I do not equate it with choosing the most abstract-looking API. I would prefer a boundary contract that is understandable without knowing a particular controller’s annotations, and keep unavoidable implementation-specific choices visible. That gives a future migration a better starting point. It does not promise that another controller can implement every behavior in the same way, or that changing the API makes the underlying operational choice portable by itself.
Choose a replacement against the inventory
Only after mapping responsibilities would I compare replacement options. I would check whether each candidate can express the required routing and policy, where it expects TLS material and configuration to live, and how its operational model fits the platform. I am not treating this as a claim about Kubernetes networking internals; it is a checklist for asking the right questions of the implementation and its maintainers.
I would also distinguish what should move from what should be retired. A controller-specific annotation may encode a real requirement, or it may be a workaround that is no longer needed. For every item, I would record one of three outcomes: preserve at the edge, move to the application or another platform component, or remove after confirming no caller depends on it. That makes review more useful than a line-by-line translation because it gives reviewers a reason for each behavior.
A small migration slice is a better test than a broad rewrite. I would choose a route that exercises meaningful parts of the contract, compare the old and new behavior, and make discrepancies explicit. I would include both expected requests and requests that should be rejected. The point is not to claim that one test proves the migration safe; it is to discover which assumptions the inventory missed before applying the design widely.
I would keep the contract and the migration notes close to the relevant configuration, ideally where changes to the route can be reviewed alongside its documented behavior. If the team uses diagrams, I prefer text-based diagrams reviewed with code rather than an image that can drift separately. The diagram should show the boundary and responsibility handoffs, not pretend to explain networking details it does not cover.
Keep the boundary understandable after cutover
A migration is not complete just because traffic reaches the same service. It is complete when the new arrangement has an accountable owner for each edge responsibility and the observable contract still makes sense to callers. That includes documenting which policies are implemented where, which configuration is specific to the chosen controller, and how someone can tell whether a change is safe.
I would resist calling the result portable if its essential behavior depends on undocumented annotations, controller defaults, or conventions known only to one operator. Some implementation coupling is unavoidable. The useful goal is to make that coupling explicit and contained, so the external-to-internal contract remains stable even when the mechanism changes.
The retirement creates urgency, but urgency is not a reason to mistake syntax for architecture. I would inventory the boundary, choose deliberately where each responsibility belongs, and validate the caller-visible contract before treating the replacement as done.
Drafted with AI assistance, reviewed and edited by Ali.