Kubernetes Services, Pod Networking, and CoreDNS for CKA
Introduction
Kubernetes Services and CoreDNS turn changing Pod addresses into reliable application endpoints. This lesson covers Pod connectivity, Service types, EndpointSlices and DNS in the Services & Networking domain (20% of the CKA). Original practice exercises expose a Deployment on a fixed NodePort, trace a failing Service connection and forward a corporate DNS zone. The next lesson explains NetworkPolicies, Ingress, and the Gateway API.
The official exam page currently lists Kubernetes v1.37; confirm the version for your sitting. Examples here assume a Linux IPv4 lab, default cluster domain cluster.local, installed CNI and CoreDNS, and namespaces created as needed. Real clusters may use dual-stack networking, a different domain or NodeLocal DNSCache.
After this lesson you will be able to explain the Kubernetes network model, create each Service type imperatively and declaratively, read endpoints and EndpointSlices, resolve names with the cluster DNS, and edit the CoreDNS configuration.
Learning objectives
- Explain Pod addressing, cross-node reachability and shared network namespaces, and locate the CNI configuration.
- Create ClusterIP, NodePort, LoadBalancer, headless and ExternalName Services and choose between them.
- Explain how selectors, Endpoints and EndpointSlices connect a Service to Pods, and what
kube-proxydoes. - Predict the DNS name of Services and Pods, and use
nslookupordigfrom a test Pod. - Locate and modify the CoreDNS
Corefile.
The Kubernetes network model
Kubernetes imposes requirements but not an implementation:
- Ordinary, non-hostNetwork Pods get unique Pod IP addresses in the configured IP family or families; hostNetwork Pods share the node network namespace.
- The network model provides direct Pod-IP reachability without NAT across nodes; NetworkPolicies, firewalls and application listeners can still restrict actual communication.
- Agents on a node (kubelet, system daemons) can reach all Pods on that node.
- Containers within a Pod share a network namespace: they talk over
localhostand must use different ports.
A CNI implementation such as Calico, Cilium or Flannel configures Pod networking through the runtime. IP allocation may use node CIDRs or another plugin-specific IPAM scheme. Missing or broken CNI configuration commonly leaves nodes NotReady and ordinary Pods unable to create network sandboxes; hostNetwork components can still run. Pod IPs change when Pods are replaced, so applications normally use Service discovery rather than hard-coded addresses.
Useful commands:
kubectl get pods -o wide # Pod IP and node
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name} {.spec.podCIDR}{"\n"}{end}'
kubectl cluster-info dump | grep -m1 -E 'cluster-cidr|service-cluster-ip-range'On typical kubeadm clusters, inspect --service-cluster-ip-range in the kube-apiserver manifest and, when used, --cluster-cidr in kube-controller-manager under /etc/kubernetes/manifests/. Also inspect the CNI/IPAM configuration: an empty node podCIDR does not prove that the plugin has no Pod address pool.
Worked example: prove two Pods can talk
Task. Pods client (node worker-1) and server (node worker-2) must communicate, and you must show it.
Steps.
- Get the server IP:
kubectl get pod server -o jsonpath='{.status.podIP}'returns for instance10.244.2.15. - Test from the client:
kubectl exec client -- wget -qO- --timeout=3 http://10.244.2.15:8080. - A reply demonstrates connectivity to that application across nodes. It does not by itself prove source-IP preservation; inspect server connection information or packets if that is the requirement. A timeout can indicate routing, policy, firewall or application-listener problems.
Why not just ping from a node? The node-to-Pod path is a different path from Pod-to-Pod across nodes, so it proves less.
Services
A normal ClusterIP Service gives clients a stable virtual IP, DNS name and forwarding to selected backends. Headless and ExternalName Services work differently. A selector matches Pods in the Service's namespace. kube-proxy programs kernel forwarding (iptables, nftables or legacy IPVS); some CNI dataplanes replace it with eBPF. Whether the virtual IP appears on an interface depends on the implementation, so interface presence is not a universal health check.
kubectl create deployment web -n demo --image=nginx:1.27 --replicas=3
kubectl expose deployment web -n demo --name=web-svc --port=80 --target-port=80 # ClusterIP
kubectl expose deployment web -n demo --name=web-np --type=NodePort --port=80
kubectl get svc web-svc -n demo
kubectl get endpointslices -n demo -l kubernetes.io/service-name=web-svcEquivalent ClusterIP manifest for this nginx example (the image listens on port 80; the fixed tag is for a lab, not a production version recommendation):
apiVersion: v1
kind: Service
metadata:
name: web-svc
namespace: demo
spec:
type: ClusterIP
selector:
app: web
ports:
- name: http
port: 80 # port of the Service
targetPort: 80 # port on which this nginx example listens
protocol: TCPService types
| Type | Reachable from | How it works |
|---|---|---|
ClusterIP (default) |
Inside the cluster | Virtual IP from the service CIDR |
NodePort |
Routable node addresses allowed by the dataplane and firewall | Allocates a port (default 30000–32767), normally alongside a ClusterIP |
LoadBalancer |
Via an integrated external load balancer | A cloud or other controller supplies an IP/hostname; NodePorts are allocated by default but can be disabled for compatible implementations |
ExternalName |
Inside | DNS CNAME to an external hostname; no proxying, no selector |
Headless (clusterIP: None) |
Clients able to reach its endpoints | No virtual IP; selector-based DNS returns eligible endpoint addresses directly |
Key fields and behaviours:
portis what clients use,targetPortis where the container listens,nodePort(NodePort and LoadBalancer only) is the node port.kubectl exposesetstargetPortequal toportunless you pass--target-port.externalTrafficPolicy: Localsends external traffic only to Pods on the receiving node and keeps the client source IP; the defaultClustercan hop between nodes and SNAT the source.sessionAffinity: ClientIPpins a client to one Pod for a timeout (default 10800 seconds).trafficDistributionexpresses a routing preference. Current documentation supportsPreferSameNodeandPreferSameZone;PreferCloseremains a deprecated alias for same-zone preference, not a removed value. Check support on your cluster version.- NodePort reachability depends on node address selection, firewall rules and traffic policy.
externalTrafficPolicy: Localmay drop traffic on nodes without a usable local backend. allocateLoadBalancerNodePorts: falseis for load-balancer implementations that route directly to backends; it is not a substitute for a load-balancer controller.- With no controller implementing LoadBalancer Services,
EXTERNAL-IPnormally stays<pending>until that integration is provided. A bare-metal controller can fulfil the request without a cloud provider.
Endpoints and EndpointSlices
The control plane records matching Pod addresses in EndpointSlices with conditions, including endpoints that are not ready. The older Endpoints object is deprecated since v1.33 in favour of EndpointSlices (discovery.k8s.io/v1), which scale better because a Service's addresses are split into slices of up to 100 endpoints by default (the controller setting is configurable, up to 1000). Both can still be read, but build your understanding on EndpointSlices.
kubectl get endpointslices -n demo -l kubernetes.io/service-name=web-svc
kubectl get endpointslices -n demo -l kubernetes.io/service-name=web-svc -o yaml
kubectl get endpoints web-svc -n demo # legacy API; deprecated since v1.33Distinguish no addresses from addresses with conditions.ready: false. No addresses can mean a selector mismatch, wrong namespace or no matching Pods with IPs; failed readiness normally leaves addresses present but unready. A Service without a selector needs separately managed EndpointSlices (except ExternalName, which is a DNS alias).
For ordinary selector-based Services, readiness controls eligibility for normal routing, not presence in the slice. publishNotReadyAddresses: true overrides the usual ready flag for discovery use cases; terminating endpoints also have separate serving and terminating conditions. Use Deployment readiness and startup probes to diagnose unready backends.
Worked example: expose an app on a fixed NodePort
Task. Deployment shop in namespace store listens on 8080. Expose it on NodePort 30080 at node addresses reachable from the lab, with default Cluster external traffic policy and suitable firewall rules.
Solution.
- Generate the Service:
kubectl expose deployment shop -n store --name=shop-np --type=NodePort --port=80 --target-port=8080 --dry-run=client -o yaml > svc.yaml. - Edit
svc.yamland addnodePort: 30080under the port entry (the imperative command cannot set it). kubectl apply -f svc.yaml, thenkubectl get svc shop-np -n storeshows80:30080/TCP.- Test:
curl http://<node-ip>:30080.
Why not the alternatives. A LoadBalancer needs a controller integration the cluster may not have. A ClusterIP is normally for internal access; external reachability would require additional routing or access infrastructure. Without editing the file, Kubernetes allocates a free node port from the configured range.
Comparison: which Service for which need?
| Need | Choice | Reason |
|---|---|---|
| Internal microservice call | ClusterIP | Simplest, stable name and IP |
| Quick external test on a bare-metal lab | NodePort | No external components |
| Production public endpoint on a cloud | LoadBalancer (or Ingress or Gateway) | Gets a managed external IP |
| Per-Pod DNS for a database cluster | Headless with a StatefulSet | Each Pod resolves individually |
| Alias for an external SaaS database | ExternalName | CNAME without proxy |
DNS and CoreDNS
Cluster DNS is provided by CoreDNS, deployed in kube-system as a Deployment (coredns) behind the Service kube-dns (name kept for compatibility). For ordinary ClusterFirst Pods, kubelet supplies the cluster DNS address and search paths in /etc/resolv.conf; NodeLocal DNSCache or custom DNS settings can change that address.
Names you must know:
| Object | DNS name |
|---|---|
| Service | <service>.<namespace>.svc.cluster.local |
| Legacy IPv4 Pod record, when enabled by the DNS implementation | 10-244-2-15.<namespace>.pod.cluster.local |
| Pod with matching hostname/subdomain and a headless Service (such as StatefulSet members) | <hostname>.<service>.<namespace>.svc.cluster.local |
| SRV record | _http._tcp.<service>.<namespace>.svc.cluster.local |
Because of the search path (<ns>.svc.cluster.local svc.cluster.local cluster.local) and options ndots:5, a Pod in namespace store can call a Service in the same namespace as shop, and one in another namespace as shop.store or the full name. Short names do not cross namespaces without the namespace part.
kubectl run dns-test --image=busybox:1.36 --restart=Never --rm -it -- nslookup web-svc.demo.svc.cluster.local
kubectl exec client -- cat /etc/resolv.confIn this IPv4 lab, a selector-based headless Service returns eligible endpoint A records; a normal Service returns its ClusterIP. Dual-stack clusters can also return AAAA records. dnsPolicy: ClusterFirst is the default; hostNetwork Pods on Linux need ClusterFirstWithHostNet for cluster DNS. Use dnsConfig for explicit per-Pod additions. cluster.local is configurable, not universal; legacy Pod-IP records depend on CoreDNS pods mode.
Inspecting and changing CoreDNS
kubectl -n kube-system get deploy coredns
kubectl -n kube-system get cm coredns -o yaml
kubectl -n kube-system logs -l k8s-app=kube-dnsThe behaviour lives in the Corefile inside the coredns ConfigMap:
.:53 {
errors
health {
lameduck 5s
}
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
prometheus :9153
forward . /etc/resolv.conf
cache 30
loop
reload
loadbalance
}kubernetesanswers names in the cluster domain.forward . /etc/resolv.confuses the resolver file visible inside the CoreDNS container. Check its contents and kubelet resolver configuration; do not assume it always matches the host file.cache 30caps cached response TTLs at 30 seconds; actual TTL and cache configuration affect expiry.loopdetects forwarding loops and stops CoreDNS;reloadre-reads the file automatically, typically within about two minutes.
To forward only one internal domain to a corporate DNS server, add a server block:
corp.example.com:53 {
errors
cache 30
forward . 10.20.0.53
}Edit with kubectl -n kube-system edit cm coredns, allow ConfigMap projection and reload to propagate, then check logs and resolution from a test Pod. Normally no restart is needed. On a reload parse error, CoreDNS generally keeps serving the previous configuration and logs the failure; starting or restarting a Pod with an invalid Corefile can instead crash-loop. Keep a backup, correct errors and confirm projection before any deliberate rollout restart.
Worked example: send one domain to a corporate resolver
Task. Pods must resolve *.corp.example.com via 10.20.0.53; everything else keeps working as before.
Reasoning and steps.
- Changing
forward .would redirect all non-cluster queries, breaking public names if that server cannot resolve them. - Add a separate server block for
corp.example.com:53withforward . 10.20.0.53. - Wait for reload and test from one temporary Pod so both commands actually run inside the cluster:
kubectl run dns-check --image=busybox:1.36 --restart=Never --rm -it -- \
sh -c 'nslookup app.corp.example.com && nslookup kubernetes.default.svc.cluster.local'Why the other choices are worse. Editing each Pod's dnsConfig does not scale. Editing the node /etc/resolv.conf affects only the default forwarder and every domain at once.
How kube-proxy implements Services
kube-proxy runs on every node (usually as a DaemonSet in kube-system) and watches Services and EndpointSlices. It does not forward packets itself in the common modes; it programs the kernel:
| Mode | Mechanism | Notes |
|---|---|---|
iptables |
NAT rules, one chain per Service and endpoint, random endpoint selection | Long-standing default on Linux |
ipvs |
Kernel IP Virtual Server with supporting iptables rules | Deprecated since Kubernetes v1.35; inspect legacy installations rather than choosing it for new designs |
nftables |
nftables rules | Newer alternative that replaces iptables on supporting kernels |
Some CNIs (for example Cilium) can replace kube-proxy completely with eBPF. To find the mode, read the kube-proxy ConfigMap (kubectl -n kube-system get cm kube-proxy -o yaml, field mode) or the logs of a kube-proxy Pod. If kube-proxy stops, existing kernel rules can keep forwarding, but subsequent Service/endpoint updates may not be applied. Confirm whether the cluster uses a replacement dataplane before treating a missing kube-proxy Pod as an error. Working Pod IPs with a failing Service IP suggest Service forwarding, policy or port configuration, not conclusive proof of one failed component.
Worked example: stable names for a database cluster
Scenario. A StatefulSet db with 3 replicas in namespace data needs each member reachable by a fixed name (db-0, db-1, db-2) for replication, while applications use one entry point.
Reasoning and steps.
- Create a headless Service for member identity:
clusterIP: None, selectorapp=db, port 5432, and setserviceName: dbin the StatefulSet. Members then resolve asdb-0.db.data.svc.cluster.local. - Create a second, normal ClusterIP Service
db-readwith the same selector for applications that only need any ready member. - With all three members ready in this IPv4 lab, the headless query returns their addresses, while
db-read.data.svc.cluster.localreturns one virtual IP. Some databases needpublishNotReadyAddresses: trueon the governing Service for bootstrap discovery; client-facing Services should still select suitable healthy members.
Why a single ClusterIP is not enough. Replication needs to address a specific member; the Service can select different backends according to its dataplane, affinity and routing policy. Why not NodePort. It exposes the database outside the cluster for no benefit.
A quick method for Service problems
When a client cannot reach a Service, test the chain from the bottom up and stop at the first failure:
- Pod works directly? From a test Pod with curl installed:
kubectl exec test -- curl -s <podIP>:<targetPort>. If not, check the application listener, routing, firewall and NetworkPolicy. - Addresses and conditions?
kubectl get endpointslices -n <ns> -l kubernetes.io/service-name=<svc> -o yaml. Missing addresses suggest selectors or namespace; present but unready endpoints suggest Pod readiness. - ClusterIP works?
curl <clusterIP>:<port>from a Pod. If Pod IPs work but the ClusterIP does not, suspect kube-proxy or the ServicetargetPort. - DNS works?
nslookup <svc>.<ns>. If the IP works but the name does not, check CoreDNS Pods, thekube-dnsService endpoints and the Pod's/etc/resolv.conf. - External access? For NodePort, check the node firewall and
externalTrafficPolicy; for LoadBalancer, check that a cloud controller or MetalLB exists.
Follow Kubernetes Service and network troubleshooting for a fuller diagnostic sequence and failure cases.
Common pitfalls and exam tips
- Selector and readiness are different checks. Compare Service selectors with Pod labels, then inspect EndpointSlice address conditions; addresses can exist while readiness is false.
portversustargetPort. Connection refused despite endpoints usually means targetPort does not match the container port.- NodePort range. Values outside 30000 to 32767 are rejected unless the API server flag
--service-node-port-rangewas changed. - Cross-namespace names. Use
service.namespace, not justservice. - ExternalName takes no selector and has no ClusterIP; do not try to connect to its "port mapping".
- Do not restart CoreDNS blindly. An invalid reload normally retains the old configuration; restarting with that invalid file can turn a logged reload error into a DNS outage. Restore valid configuration and inspect logs before restarting.
kubectl exposefails on Pods without labels because it needs a selector.- Exam tip: use
kubectl exposewith--dry-run=client -o yamlfor non-default settings, test with a throwawaybusyboxorcurlimages/curlPod, and always state the namespace. Usekubectl explain service.specfor field names.
Mini case study: Contoso's checkout service
Contoso runs checkout (3 Pods, container port 8080, label app=checkout) in namespace shop and a payments Service in namespace billing. Requests:
- Expose
checkoutinside the cluster on port 80. - Make it reachable from the lab network on node port 30090.
- The checkout code calls
http://payments:8443and fails with "no such host".
Solution.
kubectl expose deployment checkout -n shop --name=checkout --port=80 --target-port=8080.- Create a second Service (or change the type) to NodePort. Generate YAML with
--type=NodePort --dry-run=client -o yaml, addnodePort: 30090, apply it, and test withcurl http://<node-ip>:30090. Verify withkubectl get endpointslices -n shop -l kubernetes.io/service-name=checkoutthat three addresses are listed, then use-o yamlto confirm their readiness conditions. paymentslives in another namespace, so the short name only resolves insidebilling. Change the application configuration tohttp://payments.billing:8443(or the fullpayments.billing.svc.cluster.local). If the application image has no diagnostic tools, test from an approved temporary Pod inshopusingnslookup payments.billing.svc.cluster.local; do not assume nslookup is installed in the application container.
Commentary. All three issues came from one of the three links in the chain: selector to endpoints, Service type to reachability, and DNS name to namespace. Checking those in that order finds most Service problems quickly.
Key terms
- CNI: plugin that implements Pod networking.
- Service / ClusterIP: stable virtual IP and name for a set of Pods.
- NodePort / LoadBalancer: Service types exposing a node port / an external load balancer IP.
- Headless Service: Service with
clusterIP: None; DNS returns Pod IPs. - EndpointSlice: addresses and readiness/serving/terminating conditions used for Service discovery and routing.
- kube-proxy: node agent that programs Service forwarding rules.
- CoreDNS / Corefile: cluster DNS server and its configuration.
Summary checklist
- Ordinary Pods have routable IPs; CNI implements the network, with policy and hostNetwork exceptions understood.
-
kubectl exposecreates Services;port,targetPortandnodePortare different things. - Service types: ClusterIP, NodePort (30000 to 32767), LoadBalancer, ExternalName, headless.
- EndpointSlices can include unready addresses; inspect conditions and publishNotReadyAddresses before predicting routing.
- Names:
svc.ns.svc.cluster.local; short names are namespace-local. - CoreDNS runs in
kube-system, configured by thecorednsConfigMap (Corefile). - Add a separate server block to forward a single domain.
Official sources
- Cluster networking: https://kubernetes.io/docs/concepts/cluster-administration/networking/ (opens in a new tab)
- Service: https://kubernetes.io/docs/concepts/services-networking/service/ (opens in a new tab)
- EndpointSlices: https://kubernetes.io/docs/concepts/services-networking/endpoint-slices/ (opens in a new tab)
- DNS for Services and Pods: https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/ (opens in a new tab)
- Customizing DNS Service: https://kubernetes.io/docs/tasks/administer-cluster/dns-custom-nameservers/ (opens in a new tab)
- Virtual IPs and Service proxies: https://kubernetes.io/docs/reference/networking/virtual-ips/ (opens in a new tab)
- CoreDNS reload behaviour: https://coredns.io/plugins/reload/ (opens in a new tab)
- CKA domains and current exam version: https://training.linuxfoundation.org/certification/certified-kubernetes-administrator-cka/ (opens in a new tab)
- CKA curriculum: https://github.com/cncf/curriculum (opens in a new tab)
