Ingress and Cluster DNS: One Entrance In, One Name to Recognise Each Other

· tech

#kubernetes#networking

📑 Contents

The fourth post gave short-lived Pods a fixed address, the Service, but left two loose ends: first, “how does the outside come in through one entrance and get routed by URL to different services?” — the Ingress at the top of that exposure-types diagram; second, when services inside the cluster call each other, who actually resolves the name http://web? This post closes both: outward via Ingress, inward via cluster DNS. They’re two sides of one coin — both solve “how to find a service by name rather than by IP”.

The outward half first; the key is that Service and Ingress divide the work across different network layers:

user one domain one public IP GET shop.com/api Ingress L7: reads the HTTP Host + path shop.com/ shop.com/api img.shop.com terminates TLS here too (https → http) web-svc ClusterIP → web Pods api-svc ClusterIP → api Pods img-svc ClusterIP → img Pods one IP, one entrance, routed by URL to many internal Services — something an L4 Service can't do
Ingress is L7: it understands the HTTP Host and path, so one IP can hand different URLs to different Services. By contrast a Service (LoadBalancer) is L4 — it knows only IP:port and can't see URLs, so every external service needs its own IP

Ingress: one entrance, routed by URL

Recall the LoadBalancer type of Service: every external service you open gets its own public IP from the cloud. Ten services means ten IPs and ten bills, and it’s L4 — it looks only at destination IP and port, can’t see the HTTP URL, so there’s no “same domain, /api goes here, /img goes there”.

Ingress fills exactly that layer. It’s the L7 (HTTP layer) entrance, and an Ingress rule looks like this:

apiVersion: networking.k8s.io/v1
kind: Ingress
spec:
  rules:
    - host: shop.com
      http:
        paths:
          - path: /
            backend: { service: { name: web-svc, port: { number: 80 } } }
          - path: /api
            backend: { service: { name: api-svc, port: { number: 80 } } }
  tls:                       # https is terminated at the Ingress layer; backends receive plain http
    - hosts: [shop.com]
      secretName: shop-tls

One public IP, splitting traffic by Host + path across a crowd of ClusterIP Services behind it, and doing TLS termination at this layer as a bonus (decrypt https to http before forwarding, so backend Pods don’t each handle certificates). This is exactly the most common combination the fourth post described: “an Ingress in front + a bunch of internal ClusterIP Services”.

A pit: an Ingress is only “rules”; a Controller has to execute them

This is where most people get stuck: you kubectl apply an Ingress and nothing happens. Because the Ingress object is only a rule table — it needs an actually running Ingress Controller (commonly ingress-nginx or Traefik) to read those rules and configure its internal reverse proxy before any traffic flows.

Ingress object just a rule table (in etcd) watches Ingress Controller a Pod that's actually running configures the reverse proxy per the rules backend Services the various ClusterIPs external traffic (usually a LoadBalancer in front) the reconcile loop again: the Controller converges "proxy config" onto "what the rules say"
The Ingress object is the "desired routing rules"; the Ingress Controller is the Pod that actually watches the rules and converges the reverse-proxy configuration onto them. Without a Controller installed, Ingress rules are just a sheet of paper lying in etcd that nobody executes

A cluster can run several kinds of Controller at once, with IngressClass deciding which one a given Ingress belongs to. It’s the familiar pattern again: you declare a desired state (routing rules), and a controller keeps converging reality (proxy config) onto it — exactly the same loop as Deployment and Endpoints, only this time what converges is nginx’s config file.

Cluster DNS: how services recognise each other by “name”

Now the inward half. The fourth post said services call each other by names like http://web, but who translates the name into a ClusterIP? The answer is the cluster’s built-in DNS — CoreDNS (running as a Deployment in kube-system, exposed through a Service). When each Pod is created, its /etc/resolv.conf is pointed at CoreDNS, so every DNS query from inside a Pod goes there.

Every Service automatically gets a fixed DNS name, following <service>.<namespace>.svc.cluster.local:

web . default . svc . cluster.local Service name namespace it's a Service cluster domain Pod asks for "web" short name, same namespace complete it CoreDNS the DNS in kube-system ClusterIP 10.96.0.10 across namespaces write web.other-ns; the short name only works within the same namespace
Each of the four parts of a Service's DNS name means something. Within the same namespace a Pod can just use the short name web — the search domain in resolv.conf completes it to the full name and CoreDNS turns it into the ClusterIP; only across namespaces do you need to write web.other-ns

In practice you’ll almost only use short names: web within the namespace, web.payments across namespaces — that trailing .svc.cluster.local is filled in automatically by the search domain. One exception worth remembering: a headless Service (clusterIP: None) doesn’t return a single virtual IP; it returns the IP of every Pod, and gives each Pod its own DNS name (pod-0.web.default.svc...) — needed only by stateful services like a StatefulSet that have to “address one specific member”; ordinary stateless services use a normal Service.

Reflections

Separate L4 from L7, and “which one” stops being a dilemma

When I started, LoadBalancer and Ingress were easy to confuse — both look like “external”. The watershed is actually crisp: does it understand HTTP. A Service stops at L4, knows only IP:port, and suits “I just need this port opened up” (databases, gRPC, non-HTTP things); Ingress stands at L7, reads Host and path, and suits “a pile of HTTP services under one domain, TLS collected at one entrance, routed by URL”. Once that line was clear, my default became: external HTTP always goes through Ingress, one IP for everything; only non-HTTP, or things needing their own IP, get a separate LoadBalancer. Saves IPs, saves bills, and certificates are managed in one place.

”The rules” and “the thing that executes the rules” are two different things — this idea saves a lot of debugging

An Ingress applied with no effect is the most common newbie ghost story, and behind it is a more general truth: many objects in K8s are only “desired state”; something has to happen only if a controller is running. Ingress needs an Ingress Controller, NetworkPolicy needs CNI support, a CRD needs its operator. I’ve built a habit: when a resource “applied but nothing moved”, my first reaction isn’t to suspect a typo but to ask — “is the controller that executes this rule actually running?” That question often hits the mark directly. Rules never move on their own; what moves is always the loop watching them.

Service discovery is K8s’s most underrated free gift

Coming from the era of self-built services, I have scars from “service discovery” — Consul, Eureka, hand-assembled etcd; just keeping “who is where” correct was exhausting. K8s made it built in, zero config: create a Service and it automatically has a DNS name, CoreDNS resolves it automatically, and the name stays the same when Pods change IP. It’s the underlying support for the fourth post‘s “depend on names, not locations” — precisely because this DNS exists, “recognising each other by name” went from a wish to the default behaviour. I now deliberately remind teams not to build their own service-discovery wheel: K8s has handed you the hardest piece for free; treasure it.