Orchestration & Kubernetes

Ingress and Gateway: Getting Traffic In

Internet → load balancer → ingress controller → Service → pods. One external entry point, one place where host and path routing and TLS termination live, and N internal services behind it instead of N public load balancers.

The question this answers

Infrastructure question

How does a request from the public internet reach the right pod, with TLS terminated and without one load balancer per service?

Application requirement

Three services must be publicly reachable: the marketing site on example.com, the API on api.example.com, and an admin panel on example.com/admin restricted by source address. All must be HTTPS with automatically renewed certificates.

What it provides

A single external entry point with host- and path-based routing to internal Services, TLS terminated in one place, and a certificate lifecycle that renews without anyone remembering.

Application RequirementInfrastructure RequirementComputeNetworkStorageIdentityDeploymentScalingReliabilityObservabilitySecurityCostTrade-offs

The path, hop by hop

A request from a browser makes five hops. DNS resolves api.example.com to the address of a provider load balancer — a real, billed, internet-facing resource that lives outside the cluster. That load balancer forwards to the ingress controller, which is an ordinary set of pods running a reverse proxy. The controller reads the routing rules you declared, terminates TLS, picks the right internal Service, and the Service delivers the connection to a Ready pod.

The important structural point is that the ingress controller is *just a workload*. It is a Deployment of proxy pods, exposed by one LoadBalancer Service, that happens to watch the cluster API for routing objects and reconfigure itself. That is why it scales, fails and is upgraded like anything else you run — and why "the ingress is down" is usually a pod problem rather than a networking mystery.

What this buys you is arithmetic. Without it, every publicly reachable Service needs its own LoadBalancer Service, which means its own provider load balancer, its own public IP, its own certificate and its own hourly charge. With it, twelve services share one entry point. That consolidation — not the YAML — is the reason ingress exists.

One public entry point; everything behind it is internal.PROVIDER-NEUTRAL
Browserpublic
DNS: api.example.compublic
Virtual network
Public subnetpublic
Provider load balancer :443public— The one billed, internet-facing resource. This exposure is the design, not a finding.
Private subnet (cluster nodes)private
Ingress controller podsprivate— A normal Deployment running a reverse proxy; terminates TLS.
Service "api" (ClusterIP)internal
Service "web" (ClusterIP)internal
api podsinternal
web podsinternal
Managed database :5432private
⚠ Private by design. A database reachable from the internet would be the finding here — the load balancer on 443 is not.
BrowserDNS: api.example.com· resolve
BrowserProvider load balancer :443· HTTPS 443crosses boundary
Provider load balancer :443Ingress controller pods· forward
Ingress controller podsService "api" (ClusterIP)· Host: api.example.com
Ingress controller podsService "web" (ClusterIP)· Host: example.com
Service "api" (ClusterIP)api pods
Service "web" (ClusterIP)web pods
api podsManaged database :5432· private only

The rules, declared

Kubernetes· Kubernetes 1.29 networking.k8s.io/v1. Annotation keys shown are specific to one controller and do not transfer.

Routing rules are objects like anything else. The controller watches them and rewrites its own proxy configuration. Below is the classic three-rule case: two hosts, one path prefix, and TLS backed by a certificate in a Secret that a certificate controller renews on a schedule.

Two honest limitations of the older Ingress object are worth knowing before you build on it. First, it only really models HTTP — TCP and UDP routing are done through controller-specific annotations, which is to say not portably. Second, everything beyond basic host and path matching is an annotation, and annotations are controller-specific strings with no schema and no validation. A manifest full of nginx.ingress.kubernetes.io/... keys is not portable to a different controller, and typos in them fail silently.

The Gateway API is the successor designed around those problems: typed resources rather than annotations, protocols beyond HTTP, and a deliberate split of responsibility so that a platform team owns the listeners and certificates while application teams own their own routes without editing a shared object. If you are choosing today and your controller supports it, it is the better foundation — see also Load Balancers as Infrastructure for the layer underneath.

1apiVersion: networking.k8s.io/v1
2kind: Ingress
3metadata:
4 name: public
5 annotations:
6 cert-manager.io/cluster-issuer: letsencrypt # controller-specific string, no schema
7spec:
8 ingressClassName: nginx
9 tls:
10 - hosts: [example.com, api.example.com]
11 secretName: public-tls # a Secret the cert controller writes and renews
12 rules:
13 - host: api.example.com
14 http:
15 paths:
16 - path: /
17 pathType: Prefix
18 backend: { service: { name: api, port: { number: 80 } } }
19 - host: example.com
20 http:
21 paths:
22 - path: /admin # more specific prefix must come first
23 pathType: Prefix
24 backend: { service: { name: admin, port: { number: 80 } } }
25 - path: /
26 pathType: Prefix
27 backend: { service: { name: web, port: { number: 80 } } }
28---
29# The same intent in the Gateway API: typed, and split by ownership.
30# Gateway (platform team) -> listeners, certificates, addresses
31# HTTPRoute (product team) -> "api.example.com goes to Service api"
Host and path routing with TLS. Illustration of the concepts above.

Ingress, Gateway API, or neither

The choice is not only between two Kubernetes objects. For many systems the right edge is a managed application load balancer or a CDN in front of the cluster, with the cluster exposing exactly one backend. That moves TLS, WAF rules, rate limiting and DDoS absorption to a managed service that is better at all four, and it keeps the cluster out of the path of a class of attacks entirely.

The clearest reason to route inside the cluster is that routing rules should live with the application and change with it. The clearest reason not to is that an in-cluster ingress controller is a workload you now operate: it needs capacity, its upgrades are traffic-affecting, and when it is unhealthy every service behind it is unreachable at once. That last property is the one to weigh — consolidating twelve load balancers into one entry point also consolidates twelve failure domains into one.

OptionWhat it givesWhat it costsChoose it when
LoadBalancer Service per appTotal isolation between services; no shared component.One billed load balancer, IP and certificate per service.One or two services, and you want no extra moving parts.
Ingress object + controllerHost/path routing and TLS in one place; rules live with the app.A proxy Deployment you operate; annotations that are not portable.Several HTTP services and a team comfortable running the controller.
Gateway API + controllerTyped routing, non-HTTP protocols, platform/app ownership split.Newer, controller support varies, more concepts to learn.Starting fresh, multiple teams sharing one edge.
Managed ALB or CDN in frontTLS, WAF, rate limiting and DDoS absorption handled outside the cluster.Routing config lives away from the app; a provider-shaped dependency.Public traffic where edge security matters more than config locality.
No ingress at allNothing to run or debug.Nothing.Internal-only workloads. Not everything in a cluster needs a door.
Where the edge should live

Key points

  • The path is internet → provider load balancer → ingress controller pods → Service → pod, and only the first hop is a billed external resource.
  • The ingress controller is an ordinary Deployment running a reverse proxy that reconfigures itself from cluster objects.
  • The reason it exists is consolidation: one entry point and one certificate instead of one load balancer per service.
  • The Ingress object models HTTP only; everything beyond host and path routing is controller-specific annotations with no schema.
  • Consolidating twelve entry points also consolidates twelve failure domains — an unhealthy controller makes every service behind it unreachable.

The loop, answered

Every field is required, which is why no lesson here can recommend something without saying what it costs and what simpler thing to consider first.

How it works
  • DNS resolves the public hostname to a provider load balancer created for the ingress controller's Service.
  • The load balancer forwards connections to the controller pods on the cluster nodes.
  • The controller watches routing objects through the API server and rewrites its proxy configuration whenever they change.
  • It terminates TLS using a certificate stored in a Secret, then matches the request by host and path.
  • It forwards to the selected Service, which delivers the connection to one Ready pod.
What you still own
  • The controller Deployment: its replica count, its resource requests, its upgrades — all of which affect every public request.
  • Certificate lifecycle: issuance, renewal and the alert that fires well before expiry rather than on the day — see Key Management and Encryption at Rest.
  • Routing rule conflicts, especially overlapping path prefixes where the more specific rule must be ordered first.
  • Timeouts and body-size limits at three layers — provider load balancer, controller, application — which must be consistent or the shortest one wins invisibly.
  • Client IP preservation, which requires deliberate configuration and which your rate limiting and audit logs depend on.
How it fails
  • Certificate expiry: every request fails with a TLS error at once, and the fix is minutes of panic for something a calendar alert would have prevented.
  • The controller pods being evicted or under-provisioned: all public traffic fails while the internal cluster is perfectly healthy.
  • A path prefix ordered wrongly so /admin is swallowed by /, silently exposing or hiding a route.
  • Mismatched timeouts: the load balancer cuts the connection at 60s while the application is configured for 120s, producing 504s that never appear in application logs.
  • A backend Service with no endpoints: the controller returns 503 for a route that looks correctly configured.
How it scales
  • The controller is a proxy, so it scales with concurrent connections and TLS handshakes rather than with the number of routing rules.
  • TLS termination is CPU-bound; a traffic spike on a controller with a low CPU limit produces throttling and latency, not errors — see OOM Kills and CPU Throttling.
  • Very large numbers of routing objects slow reconfiguration, so rule changes propagate more slowly as the platform grows.
Security
  • This is the deliberate public exposure of the system. A public load balancer on 443 in front of an ingress is the design; the finding would be a database or admin port reachable the same way — see Public Exposure, Read With Context.
  • TLS terminates at the controller, so traffic from there to pods is plaintext on the cluster network unless you add encryption in transit — decide this explicitly for regulated data.
  • The controller can read TLS Secrets across namespaces, which makes it one of the higher-privilege workloads in the cluster.
  • Source-address restrictions and authentication belong here for admin routes, but an ingress rule is a filter, not authentication — the application still authenticates for itself.
Cost shape
  • One provider load balancer instead of N is the direct saving, and it is the main financial argument for ingress.
  • The controller consumes cluster CPU and memory continuously, and TLS termination is the dominant driver of that.
  • Data processed through the external load balancer is metered, and egress to the internet is usually the largest edge line item — see Egress: Moving Data Costs Money, Not Just Storing It.
What to watch
  • Request rate, error rate and latency at the controller, split by host and route — the highest-value edge dashboard there is.
  • Certificate expiry dates as a monitored metric with alerts weeks ahead, not a task someone remembers.
  • Upstream 502 and 503 rates, which distinguish "no healthy backend" from "backend returned an error".
  • Controller pod CPU throttling, which is the quiet cause of edge latency that looks like a network problem.
  • The signal that lies: backend pod health. Every pod can be healthy while the controller cannot route to them because a Service selector is wrong.
Simpler alternatives
  • A managed application load balancer or CDN in front of the cluster, terminating TLS and applying WAF rules outside your failure domain.
  • A single LoadBalancer Service, when there is exactly one public service — an ingress controller for one route is pure overhead.
  • No public entry at all for internal workloads, which is the correct answer more often than the number of ingress objects in the average cluster suggests.
  • A PaaS or managed container service, which gives you routing and managed certificates without a controller to run.
What adopting this costs
  • Buys consolidation of entry points and certificates; costs a shared failure domain in front of every public service.
  • Buys routing rules that live and version with the application; costs a proxy workload you operate and upgrade under live traffic.
  • Buys a portable-looking abstraction; costs real portability, since the useful configuration is in controller-specific annotations.

What people believe, and what is true

Claim

Ingress is a component Kubernetes provides.

Reality

The object is; the controller is not. Without a controller installed and running, an Ingress object does absolutely nothing.

Claim

Ingress replaces the load balancer.

Reality

It sits behind one. There is still a provider load balancer holding the public address — ingress reduces how many of them you need to one.

Claim

Terminating TLS at the ingress means traffic is encrypted end to end.

Reality

It is encrypted to the controller. From there to the pod it is plaintext on the cluster network unless you configured otherwise.

Apply it