Kubernetes Service types, Ingress and Gateway API
Pods come and go, and every new Pod gets a new IP address. Anything that talks to a Pod by its IP breaks the next time a Deployment rolls or a node drains. A Service fixes that by giving a group of Pods one stable name, and depending on its type, one stable virtual IP, a port on every node or an external load balancer as well.
Both CKA and CKAD test this hard, in two ways. Multiple-choice style questions ask which type fits a requirement: one address per replica, one external IP for three apps, a database that lives outside the cluster. Hands-on tasks give you a Service that "doesn't work" and expect you to find out why within a few minutes. Both come down to knowing what each type actually creates and how a Service decides which Pods to send traffic to.
How a Service finds its Pods
A Service with a selector does not point at Pods directly. The control plane keeps watching for Pods whose labels match the selector and writes their IPs into EndpointSlice objects that belong to the Service. kube-proxy on each node (or the CNI plugin's replacement for it) reads those slices and programs the node to forward traffic sent to the Service's virtual IP to one of the listed Pod IPs.
Two details matter in debugging:
- Matching and readiness are separate. A Pod whose labels match is listed in the slice even when it is failing its readiness probe. Its endpoint is marked not ready, and the proxy skips it. So "three Pods in the slice, no traffic" points at readiness, while "no Pods in the slice" points at the selector.
- `port` and `targetPort` are different numbers.
portis what clients connect to on the Service.targetPortis what the container listens on. If you leavetargetPortout it defaults to the same value asport, which is wrong whenever the app listens on something else.targetPortcan also be the name of a container port, which survives a port number change.
A Service without a selector gets no automatic endpoints. You create the EndpointSlice yourself, which is how you put a Kubernetes name in front of something the cluster doesn't manage, such as a database on a fixed IP.
The Service types
The type field is designed so each level builds on the one before it: a NodePort Service also gets a cluster IP, and a LoadBalancer Service also gets a node port (unless you turn that off).
ClusterIP
The default. Kubernetes assigns a virtual IP from the cluster's service range, and the Service is reachable only from inside the cluster at that IP and at the DNS name <service>.<namespace>.svc.cluster.local. Connections are spread across the ready endpoints. This is the right type for almost all internal traffic, and it is also what sits behind an Ingress or a Gateway route.
NodePort
Kubernetes picks a port from a reserved range (30000-32767 by default, set by the API server's --service-node-port-range flag) and every node forwards that port to the Service. Clients reach it at <any-node-ip>:<nodePort>. You can request a specific port in the range with nodePort. It is useful for labs, bare-metal clusters and putting your own load balancer in front, but it exposes high, awkward port numbers and does no HTTP routing or TLS.
LoadBalancer
On a cloud provider (or with an add-on such as a bare-metal load balancer controller), Kubernetes asks the provider for an external load balancer and writes its address into .status.loadBalancer. Creation is asynchronous, so EXTERNAL-IP shows <pending> until the provider responds, and stays that way forever on a cluster with nothing to fulfil it. Each LoadBalancer Service normally means one external address and one billed load balancer.
ExternalName
No selector, no endpoints, no proxying. Cluster DNS returns a CNAME record pointing at the hostname in externalName, so db.prod.svc.cluster.local can resolve to orders.example-db.com. It is a DNS alias and nothing more. Because the client still sends the original name, HTTP Host headers and TLS certificate names can fail to match on the far side. Putting an IP address in externalName does not work; for a fixed IP, use a selectorless Service with a manual EndpointSlice.
Headless Services
Setting clusterIP: None (not the same as leaving the field empty) makes a Service headless. No virtual IP is allocated and kube-proxy ignores it. Instead, DNS answers with the IPs of the ready Pods directly, so the client chooses which one to call. Combined with a StatefulSet, each replica also gets its own stable name, <pod>.<service>.<namespace>.svc.cluster.local, which is how database peers find each other.
Ingress and Gateway API
Services work at layer 4: they forward TCP, UDP or SCTP to a port and know nothing about URLs or hostnames. When several HTTP apps should share one external address, you add a layer 7 router.
- Ingress is an API object holding host and path rules that point at Services, plus TLS settings from a Secret. It does nothing on its own: an Ingress controller has to be running to read it. It handles HTTP and HTTPS only. Path matching uses a
pathTypeofExact,PrefixorImplementationSpecific. The Ingress API is stable but frozen, so it will get no new features. - Gateway API is the project's recommended successor. It splits the job into roles: a
GatewayClassnames the controller, aGatewayis one listening entry point owned by the cluster operator, and routes such asHTTPRouteorGRPCRouteare owned by app teams and attach to a Gateway. Header matching and weighted traffic splitting are part of the spec instead of controller-specific annotations. It is an add-on installed as CRDs, not built into every cluster.
For the exams, the rule is simple: one external address, many HTTP apps, routing by host or path means Ingress or a Gateway in front of ordinary ClusterIP Services. A non-HTTP protocol exposed externally means NodePort or LoadBalancer.
How to choose
- Pods inside the cluster calling each other: ClusterIP.
- Clients need to reach a specific replica, or do their own discovery: headless Service, usually with a StatefulSet.
- A stable in-cluster name for an external DNS name: ExternalName. For an external IP, a selectorless Service plus an EndpointSlice.
- Quick external access in a lab or on bare metal without a load balancer: NodePort.
- One external address for one TCP or UDP service on a cloud: LoadBalancer.
- Several HTTP apps behind one hostname or IP, with path or host routing and TLS: Ingress or Gateway API routes to ClusterIP Services.
Debugging a Service that doesn't route
Work from the client towards the Pods, and stop at the first thing that is wrong.
- Check the Pods:
kubectl get pods -l <selector> -o wide. Are theyRunningand1/1ready? Can you reach a Pod IP on its container port from a debug Pod? - Check the Service:
kubectl get svc <name>andkubectl describe svc <name>. Note the selector,portandtargetPort. - Check the endpoints:
kubectl get endpointslices -l kubernetes.io/service-name=<name>. No endpoints means the selector doesn't match any Pod labels, or is in the wrong namespace. Endpoints that are not ready means a readiness probe is failing. - Check DNS from a Pod: the short name works only from the same namespace; from elsewhere use
<service>.<namespace>. - Check the port mapping: is
targetPortthe port the container really listens on? - Check for a NetworkPolicy that isolates the backend Pods and doesn't allow the client.
- Only then look at
kube-proxyor the CNI on the nodes.
Common exam traps
- Assuming a
0/1Pod has fallen out of the Service. It is still in the EndpointSlice, marked not ready, which proves the selector is fine. - Leaving out
targetPortand assuming it follows the container port. It copiesport. - Choosing ExternalName to reach a Service in another namespace. Cluster DNS already resolves
<service>.<namespace>from anywhere; ExternalName is for names outside the cluster. - Choosing one LoadBalancer per app when the question asks for one external IP for several HTTP apps. That is an Ingress or Gateway job.
- Thinking a headless Service still load-balances. It has no virtual IP at all; DNS returns Pod IPs.
- Expecting an Ingress to work without a controller, or to route raw TCP.
A worked example
A team deploys orders with three replicas labelled app: orders-api and creates a Service named orders with selector: {app: orders}, port: 80 and no targetPort. The container listens on 8080. Requests from the web namespace to http://orders time out.
There are three problems stacked on top of each other, and the debugging order finds them one at a time. The EndpointSlice for orders is empty, because the selector says app: orders and the Pods say app: orders-api. Fixing the selector fills the slice, but traffic still fails: with no targetPort, the Service forwards to port 80 on the Pods, where nothing listens, so targetPort must be set to 8080. Finally, the client is in web and the Service in another namespace, so the short name orders never resolves to it; the client needs orders.<namespace>. None of this needs a different Service type. ClusterIP was right all along.
Practice questions
- Giving each StatefulSet replica its own DNS name
- Resolving a Service in another namespace
- Three HTTP apps behind one hostname
- Pods running but the Service gets no traffic
- An instant cutover between two versions