Wat een Service is
Een Service is een Kubernetes API-object (apiVersion: v1, kind: Service) dat een stabiel netwerk-endpoint biedt voor een set pods die via labels worden geselecteerd. De control plane houdt bij welke pods matchen met de spec.selector van de Service, registreert hun IP's in EndpointSlice-objecten, en programmeert het dataplane zodat pakketten naar het virtuele IP (VIP) van de Service bij een gezonde pod uitkomen.
Het VIP verandert niet zolang het Service-object bestaat. CoreDNS beheert een DNS-record (<svc>.<namespace>.svc.cluster.local) dat ernaar verwijst, zodat clients services op naam kunnen aanspreken.
Waar veel mensen over struikelen: kube-proxy, dat op elke node draait, is geen traditionele proxy. In iptables- en nftables-modus programmeert het kernelregels voor NAT. Verkeer stroomt rechtstreeks van bron naar pod door de kernel; kube-proxy stelt alleen de regels in. Het is een control-plane component voor het dataplane, geen tussenstation voor verkeer.
Sinds Kubernetes v1.33 is de oudere Endpoints API deprecated ten gunste van EndpointSlice. Nieuwe tooling hoort discovery.k8s.io/v1 EndpointSlice-objecten te gebruiken in plaats van de v1 Endpoints API.
ClusterIP: alleen intern verkeer
ClusterIP is de standaard. Als je spec.type weglaat, krijg je een ClusterIP Service.
Kubernetes wijst een stabiel virtueel IP toe uit de service-CIDR, alleen bereikbaar vanuit het cluster. kube-proxy programmeert iptables/IPVS/nftables-regels zodat elke pod in het cluster dit VIP op de opgegeven poort kan bereiken. Pakketten worden via DNAT doorgestuurd naar een van de achterliggende pod-IP's.
apiVersion: v1
kind: Service
metadata:
name: backend-api
namespace: production
spec:
selector:
app: backend-api
ports:
- port: 80 # poort waar clients verbinding mee maken
targetPort: 8080 # poort waar de container op luistert
DNS lost backend-api.production.svc.cluster.local op naar het ClusterIP-adres. Sessie-affiniteit is beschikbaar via sessionAffinity: ClientIP met een configureerbare timeoutSeconds (standaard 10800), die een client-IP voor de opgegeven duur aan dezelfde pod koppelt.
ClusterIP is de juiste keuze voor alles wat geen externe toegang nodig heeft: databases, caches, interne API's, monitoring-scrape-targets. Het heeft het kleinste aanvalsoppervlak van alle Service-typen. Je kunt het nog steeds van buiten het cluster bereiken via kubectl port-forward of de API-proxy voor debugging, maar dat is een bewuste operatoractie, geen routeringpad.
NodePort: externe toegang via node-poorten
NodePort is een laag bovenop ClusterIP. Als je een NodePort Service aanmaakt, maakt Kubernetes ook een ClusterIP aan. Daarbovenop opent het een poort in het bereik 30000–32767 op elke node in het cluster (configureerbaar via --service-node-port-range op de kube-apiserver).
apiVersion: v1
kind: Service
metadata:
name: demo-app
namespace: staging
spec:
type: NodePort
selector:
app: demo-app
ports:
- port: 80
targetPort: 8080
nodePort: 31080 # optioneel; automatisch toegewezen als je het weglaat
Verkeer dat binnenkomt op <willekeurig-node-ip>:31080 wordt door kube-proxy doorgestuurd naar een matchende pod. Maar er zit geen ingebouwde load-balancing over nodes heen. De node waar de client toevallig uitkomt, handelt de forwarding af, en kube-proxy stuurt het pakket mogelijk door naar een pod op een andere node. Dat veroorzaakt een extra netwerkhop en SNAT die het echte client-IP verbergt.
Met externalTrafficPolicy: Local vermijd je die extra hop en behoud je het bron-IP, maar verkeer wordt gewoon gedropt als de node geen lokale pod voor die Service heeft. Dat is een harde trade-off, geen instelling die je even zonder nadenken omzet.
Voor productie is NodePort sterk af te raden als directe exposure. Niet-standaard poorten, geen automatische verdeling over nodes, en elke node in het cluster als potentieel toegangspunt zorgen voor operationele complexiteit en een breed aanvalsoppervlak. NodePort is handig voor development, on-prem clusters achter een aparte load balancer, of als bouwsteen waar LoadBalancer intern gebruik van maakt.
LoadBalancer: cloud provider-integratie
LoadBalancer bouwt voort op NodePort, dat weer voortbouwt op ClusterIP. Bij het aanmaken worden alle drie de lagen ingericht: een ClusterIP, een NodePort, en een externe cloud load balancer (AWS ELB/NLB, GCP LB, Azure LB, of wat je cloudcontroller ook ondersteunt).
De cloud load balancer verdeelt verkeer over de NodePorts op je clusternodes. kube-proxy op elke node stuurt dan door naar pods.
apiVersion: v1
kind: Service
metadata:
name: public-api
namespace: production
spec:
type: LoadBalancer
selector:
app: public-api
ports:
- port: 443
targetPort: 8443
De gelaagdheid heeft directe gevolgen voor kosten. Elke LoadBalancer Service krijgt zijn eigen cloud load balancer. Op AWS kost een ELB zo'n $16/maand voor datatransfer. Tien services betekent tien load balancers. Voor HTTP/HTTPS-workloads routeert Ingress of Gateway API meerdere services achter een enkele load balancer, wat veel voordeliger is.
Verkeersbeleid en bron-IP
externalTrafficPolicy: Cluster (de standaard) verdeelt verkeer over alle nodes. De cloud load balancer voert health checks uit op elke node en stuurt breed. Het nadeel: kube-proxy past SNAT toe en vervangt het client-IP met het node-IP voordat het pakket de pod bereikt. Je applicatie ziet node-IP's, geen echte client-IP's.
externalTrafficPolicy: Local zorgt ervoor dat kube-proxy alleen doorstuurt naar pods op de lokale node. Geen SNAT, dus de pod ziet het echte client-IP. De cloud load balancer moet healthCheckNodePort gebruiken om te controleren welke nodes daadwerkelijk lokale pods hebben en stopt met verkeer sturen naar lege nodes. Als je pods niet goed verdeeld zijn over nodes, wordt de verkeersverdeling ongelijk.
Sinds Kubernetes v1.26 doet internalTrafficPolicy: Local hetzelfde voor cluster-intern verkeer: alleen doorsturen naar pods op de eigen node.
De NodePort-laag overslaan
Sinds Kubernetes 1.24 (GA) kun je met allocateLoadBalancerNodePorts: false de NodePort-allocatie helemaal overslaan. Dit werkt met CNI's die directe pod-routing ondersteunen (AWS VPC CNI, Azure CNI), waarbij de cloud load balancer verkeer rechtstreeks naar routeerbare pod-IP's stuurt. Geen NodePort, geen extra NAT-hop, en het bron-IP blijft behouden zonder externalTrafficPolicy: Local.
Op bare-metal of on-prem clusters zonder cloudcontroller blijft een LoadBalancer Service in Pending-status hangen. Projecten als MetalLB vullen dit gat met een softwarematige load balancer-implementatie.
ExternalName: DNS CNAME-alias
ExternalName maakt geen VIP aan en programmeert geen kube-proxy-regels. Het is puur een DNS-mechanisme. Wanneer een pod de Service-naam resolvet, geeft CoreDNS een CNAME-record terug dat verwijst naar de hostname die je in spec.externalName hebt opgegeven.
apiVersion: v1
kind: Service
metadata:
name: billing-db
namespace: production
spec:
type: ExternalName
externalName: billing.c9xk3q.eu-west-1.rds.amazonaws.com
Pods kunnen nu verbinding maken met billing-db.production.svc.cluster.local en bereiken daarmee de RDS-instance. Handig om externe afhankelijkheden in de cluster-DNS-namespace te brengen, of om een migratie van een externe database naar een in-cluster database te faseren zonder applicatieconfiguratie aan te passen.
De TLS-valkuil. De officiele Kubernetes-documentatie waarschuwt expliciet: protocollen die hostnamen gebruiken (met name TLS) gaan stuk. De client stuurt SNI op basis van de interne servicenaam (billing-db.production.svc.cluster.local), maar het certificaat van de server is uitgegeven voor de externe hostname (billing.c9xk3q.eu-west-1.rds.amazonaws.com). De TLS-handshake faalt met een certificaatmismatch. De workaround is je client configureren om de externe hostname voor TLS-validatie te gebruiken, wat het doel van de abstractie deels tenietdoet.
Nog een valkuil: de standaard ndots:5 in de DNS-configuratie van pods zorgt ervoor dat elke naam met minder dan vijf punten eerst zoekdomein-expansie doorloopt. Een externe naam als api.example.com heeft drie punten, dus de resolver probeert eerst elk zoekdomein (drie onnodige lookups) voordat de directe resolutie slaagt.
Headless Services: directe pod-adressering
Een headless Service is een ClusterIP Service met clusterIP: None. Er wordt geen VIP toegewezen. Er worden geen kube-proxy-regels aangemaakt. In plaats daarvan geeft CoreDNS A/AAAA-records terug voor elk Ready pod-IP wanneer de servicenaam wordt opgevraagd.
apiVersion: v1
kind: Service
metadata:
name: redis
namespace: data
spec:
clusterIP: None
selector:
app: redis
ports:
- port: 6379
In combinatie met een StatefulSet krijgt elke pod een stabiel, voorspelbaar DNS-record: redis-0.redis.data.svc.cluster.local, redis-1.redis.data.svc.cluster.local, enzovoort. Dit is precies wat gedistribueerde systemen als Cassandra, etcd, Kafka en MySQL-clusters nodig hebben voor peer discovery en leader election. Elk lid moet elk ander lid direct kunnen aanspreken; een VIP dat willekeurig load-balancet werkt daar juist tegen.
Headless Services zijn niet beperkt tot StatefulSets. Sommige gRPC-clients en databasedrivers (MongoDB bijvoorbeeld) doen hun eigen DNS-gebaseerde endpoint-selectie. Voor die clients geeft een headless Service de ruwe pod-IP's, zodat de client-side logica de routing bepaalt.
Er worden ook SRV-records gepubliceerd voor named ports, wat handig is voor protocollen die SRV-gebaseerde discovery ondersteunen.
Hoe kube-proxy verkeer routeert
kube-proxy draait op elke node en kijkt via de API-server mee naar Service- en EndpointSlice-wijzigingen. Het programmeert de netwerkstack van de Linux-kernel zodat pakketten gericht aan een Service-VIP worden herschreven naar een pod-IP. De implementatie hangt af van de modus.
iptables-modus (huidige standaard)
kube-proxy maakt chains in de NAT-tabel van de kernel: PREROUTING-regels onderscheppen pakketten, KUBE-SERVICES-chains matchen op bestemmings-IP en poort, en per-endpoint-regels gebruiken --probability voor een ruwweg gelijke willekeurige verdeling over pods.
Het probleem: het doorlopen van regels is O(n) in het aantal services. Bij 10.000 services loopt de kernel door tienduizenden iptables-regels voor elke nieuwe verbinding. Met langlopende keepalive-verbindingen (de norm bij microservices) is de impact klein, want de kosten worden eenmalig per verbinding betaald. Zonder keepalive kan de CPU-overhead oplopen tot ~35% meer dan IPVS op die schaal.
iptables blijft de standaardmodus, ook nadat nftables GA bereikt.
IPVS-modus
Gebruikt de IP Virtual Server-module van de kernel, speciaal ontworpen voor load balancing. Lookup is O(1) via hashtabellen. Biedt zeven load-balancing-algoritmen in plaats van alleen random: round-robin, least connections, shortest expected delay, destination hashing, source hashing, en meer.
Bij 10.000 services zonder keepalive voegt IPVS zo'n 8% CPU-overhead toe tegenover de 35% van iptables. Met keepalive (100 requests per verbinding) krimpt het verschil naar ongeveer 2%. Voor de meeste clusters met minder dan 1.000 services is het verschil verwaarloosbaar.
IPVS-pakketten volgen andere paden door iptables-filterhooks. Als je vertrouwt op iptables-gebaseerde netwerkpolicies of andere tooling die iptables-chains inspecteert, test dan de compatibiliteit voordat je overschakelt.
nftables-modus (beta in 1.31, verwacht GA in 1.33)
nftables vervangt de sequentiele iptables-regels door verdict maps: hashtabellen op kernelniveau. Lookup is O(1). Bij 5.000–10.000 Services is de p50-latentie van nftables vergelijkbaar met de p01-latentie (bijna best-case) van iptables.
Ook de control-plane-performance is beter: nftables ondersteunt incrementele regelupdates, terwijl iptables de volledige regeltabel herschrijft bij elke sync. Vereist Linux-kernel 5.13 of nieuwer.
Service-discovery via DNS
CoreDNS is de standaard DNS-provider in Kubernetes-clusters. De kubelet injecteert een resolv.conf in elke pod:
search staging.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.96.0.10
options ndots:5
De ndots:5-instelling betekent dat elke naam met minder dan vijf punten eerst zoekdomein-expansie doorloopt. Het resolven van backend-api (nul punten) probeert backend-api.staging.svc.cluster.local als eerste en slaagt in een lookup. Het resolven van api.example.com (drie punten) probeert drie zoekdomein-permutaties voordat de directe query slaagt. Voor pods die veel externe DNS-calls doen kun je dnsConfig.options instellen met ndots: 2 om onnodige lookups te verminderen.
| Querypatroon | Recordtype | Geeft terug |
|---|---|---|
<svc>.<ns>.svc.cluster.local (ClusterIP) |
A / AAAA | ClusterIP-adres |
<svc>.<ns>.svc.cluster.local (headless) |
A / AAAA | Alle Ready pod-IP's |
_<port>._<proto>.<svc>.<ns>.svc.cluster.local |
SRV | Poortnummer + hostname |
<svc>.<ns>.svc.cluster.local (ExternalName) |
CNAME | Externe hostname |
<pod-naam>.<svc>.<ns>.svc.cluster.local (StatefulSet + headless) |
A | Specifiek pod-IP |
Kubernetes injecteert ook omgevingsvariabelen ({SVCNAME}_SERVICE_HOST, {SVCNAME}_SERVICE_PORT) in pods, maar alleen voor Services die bestonden voordat de pod startte. DNS is het aanbevolen discoverymechanisme.
Wat een Service niet is
Een Service is geen Ingress of Gateway. Services werken op L4 (TCP/UDP). Ze sturen pakketten door op basis van IP en poort. Ze inspecteren geen HTTP-headers, routeren niet op hostname of pad, termineren geen TLS, en doen geen request-retries. Ingress en Gateway API werken op L7 en staan voor ClusterIP Services om HTTP-bewuste routing te bieden.
Een Service is geen service mesh. Een service mesh (Istio, Linkerd) voegt per-request telemetrie, mutual TLS, circuit breaking en retry-policies toe. Dat zijn sidecar-concerns die onafhankelijk werken van het Kubernetes Service-object, ook al gebruikt de mesh Service-DNS-namen als routeringsdoelen.
Een Service herverdeelt geen langlopende verbindingen. kube-proxy verdeelt nieuwe verbindingen. Zodra een TCP-verbinding is opgezet (HTTP/2-stream, gRPC-channel, WebSocket, databasepool), gaan alle requests over die verbinding naar dezelfde pod. Als je het backend opschaalt, krijgen de nieuwe pods geen van de bestaande verbindingen. Strategieen hiervoor zijn kortere keepalive-timeouts, client-side load balancing met headless Services, of een service mesh die gemultiplexte streams kan opsplitsen.
Een Service is geen firewall. Een ClusterIP Service is standaard cluster-intern, maar dwingt geen toegangscontrole af. NetworkPolicy is het Kubernetes-mechanisme om te beperken welke pods met elkaar mogen communiceren.
Wanneer welk type gebruiken
| Scenario | Type | Reden |
|---|---|---|
| Interne microservice, database, cache | ClusterIP | Kleinste aanvalsoppervlak; geen externe exposure nodig |
| Snelle externe toegang tijdens development | NodePort | Simpel; geen cloud-LB-afhankelijkheid |
| TCP/UDP-service met een publiek IP nodig | LoadBalancer | Cloud-LB regelt HA en health checks |
| Meerdere HTTP/HTTPS-services achter een IP | Ingress of Gateway API | Eeen cloud-LB gedeeld over services; L7-routing; kosteneffectief |
| DNS-alias voor een externe database of API | ExternalName | Geen proxy-overhead; let op de TLS-valkuil |
| Gedistribueerd stateful systeem (Kafka, etcd, Cassandra) | Headless + StatefulSet | Per-pod stabiele DNS; peer discovery; geen willekeurige LB |
Voor de meeste productieclusters is het patroon: ClusterIP voor alles wat intern is, een of twee LoadBalancer Services voor niet-HTTP TCP-endpoints, en Gateway API of Ingress voor HTTP/HTTPS-verkeer via een gedeelde load balancer. NodePort en ExternalName vullen nichebehoeften. Headless Services zijn de standaard voor elke stateful workload die deterministische pod-adressering nodig heeft.