Service: типы и базовые свойства
PodIP — ephemeral. При пересоздании Pod (rolling update, restart, eviction) IP меняется. Нельзя харкодить PodIP в конфигах клиентов. Нельзя ходить на конкретный Pod, потому что Pods приходят и уходят.
Нужна абстракция со стабильным адресом, которая прячет конкретные Pod-ы и распределяет нагрузку между ними. Это и есть Service. На уровне API — это объект kind: Service, на уровне сети — виртуальный IP (или DNS-имя) + правила kube-proxy, реализующие load balancing.
Bridge network и публикация портов в Docker
Что такое Service
Service — это API-объект группы v1 (core, не apps/v1). Минимальный пример:
apiVersion: v1
kind: Service
metadata:
name: web
spec:
selector:
app: web
ports:
- port: 80
targetPort: 8080
Что произойдёт:
- K8s выделит из service CIDR диапазона ClusterIP (например,
10.96.0.10). - Создаст объект Endpoints (или EndpointSlices) с IP всех Pods, у которых label
app=webи они Ready. - На каждой ноде kube-proxy подхватит изменения и пропишет правила в iptables/IPVS/nftables: пакеты на
10.96.0.10:80будут DNAT-ться на один из endpoint IP:8080. - CoreDNS добавит запись
web.default.svc.cluster.local → 10.96.0.10.
С этого момента любой Pod в кластере может ходить на http://web:80 — и попадёт на один из живых backend-ов.
Selector — связь Service ↔ Pods
spec.selector — это label selector, который выбирает Pods для роли backend. Это не запрос к Deployment — Service знает только о Pods. Если labels Deployment-овских Pods матчатся — они становятся endpoints.
spec:
selector:
app: web
tier: frontend
K8s контроллер EndpointSlice controller в kube-controller-manager:
- Watch-ит Services и Pods.
- Для каждого Service фильтрует Pods по
spec.selector. - Из этих Pods берёт только Ready (по
.status.conditions.Ready == True). - Складывает их IP:port в объекты EndpointSlice.
Старый объект Endpoints (один на Service) всё ещё создаётся для backward compatibility. С v1.21 default — EndpointSlices (несколько slice-ов по 100 endpoints в каждом). Об этом подробнее в уроке 4.
Только Ready Pods попадают в endpoints. Если readinessProbe возвращает fail — kube-proxy исключит этот Pod из ротации load balancing. Это критичный механизм graceful rollout.
ports: port, targetPort, nodePort
В spec.ports три разных порта. Их часто путают.
spec:
ports:
- name: http
port: 80 # порт самого Service (на ClusterIP)
targetPort: 8080 # порт внутри backend Pod
nodePort: 30080 # порт на каждой ноде (для типа NodePort)
protocol: TCP
port— на каком порту слушает Service ClusterIP. Когда клиент пишетhttp://web:80— он попадает сюда. Это “frontend” порт Service.targetPort— куда Service форвардит трафик внутри backend Pod. Это containerPort из Pod-а. Может быть числом (8080) или именем (http— если в Pod описанports: [{name: http, containerPort: 8080}]).nodePort— порт на каждой ноде кластера, открытый для типа NodePort/LoadBalancer. Только в диапазоне30000-32767(configurable, но по умолчанию).
Client → web:80 (Service port)
→ DNAT → 10.244.1.5:8080 (Pod targetPort)
Один Service может exposить несколько портов (для приложений на разных портах, например metrics + main app):
ports:
- name: http
port: 80
targetPort: 8080
- name: metrics
port: 9090
targetPort: 9090
name обязательно если портов больше одного.
Headless Service: clusterIP None
Иногда ClusterIP не нужен — клиенту нужны IPs всех Pods (например, для StatefulSet или клиента, который сам делает load balancing). Тогда используют headless service: spec.clusterIP: None.
apiVersion: v1
kind: Service
metadata:
name: db
spec:
clusterIP: None # headless
selector:
app: db
ports:
- port: 5432
Что происходит:
- K8s не выделяет ClusterIP.
- kube-proxy не создаёт iptables/IPVS правила для этого Service.
- Но CoreDNS всё равно создаёт DNS-записи — только теперь это несколько A-записей, по одной на каждый Pod в endpoints.
# Обычный (ClusterIP) Service:
$ dig +short web.default.svc.cluster.local
10.96.0.10 # один IP, ClusterIP
# Headless:
$ dig +short db.default.svc.cluster.local
10.244.1.5 # Pod IPs напрямую
10.244.2.7
10.244.3.9
Клиент получает список реальных Pod-ов, может балансировать сам, выбрать конкретный, держать stable connection к одному (например, для gRPC).
Для StatefulSet headless service обязательный: он обеспечивает каждому Pod-у stable DNS-имя <pod-name>.<service-name>.<namespace>.svc.cluster.local (например, db-0.db.default.svc.cluster.local).
Headless service — это “DNS as service discovery, without virtual IP”. Полезно когда клиент сам умнее kube-proxy. Главные случаи: StatefulSets (Cassandra, Elasticsearch, Kafka), gRPC clients (нужны Pod IPs, чтобы балансировать на L7), peer-to-peer apps.
Service types: ClusterIP
Default тип. ClusterIP — виртуальный IP, доступный только внутри кластера.
spec:
type: ClusterIP # можно не указывать — default
Это базовая форма: 99% internal services именно ClusterIP. Извне кластера такой Service недоступен.
Service types: NodePort
type: NodePort — открывает порт на КАЖДОЙ ноде кластера (включая control plane, если не disabled), форвардящий на Service.
spec:
type: NodePort
selector:
app: web
ports:
- port: 80
targetPort: 8080
nodePort: 30080 # если не указать — K8s выделит из 30000-32767
Доступен извне через <любой-node-ip>:30080.
NodePort включает ClusterIP under the hood — то есть Service одновременно доступен и через ClusterIP (изнутри), и через NodePort (снаружи).
Подводные камни NodePort:
- Порты 30000-32767 — это ограниченный диапазон. На больших кластерах может закончиться.
- Открывать порты на всех нодах — security risk. Нужен firewall.
externalTrafficPolicy: Cluster(default) — пакет с одной ноды может уехать на Pod на другой, теряя client IP.externalTrafficPolicy: Local— обрабатывается только на той же ноде где Pod (сохраняет src IP, но даёт неравномерную нагрузку).
Service types: LoadBalancer
type: LoadBalancer — для cloud-инсталляций (AWS, GCP, Azure). K8s через cloud-controller-manager просит у cloud-провайдера внешний LB (ELB, GCLB, Azure LB), который терминирует external трафик и форвардит на NodePort всех нод.
spec:
type: LoadBalancer
selector:
app: web
ports:
- port: 80
targetPort: 8080
K8s сам выделяет nodePort, заполняет .status.loadBalancer.ingress адресом LB:
$ kubectl get svc web
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web LoadBalancer 10.96.0.42 34.123.45.67 80:30080/TCP 2m
LoadBalancer = ClusterIP + NodePort + external LB. Все три слоя работают.
В bare-metal кластерах (без cloud) LoadBalancer не создастся автоматически — нужны решения как MetalLB, kube-vip, или Ingress поверх NodePort.
Service types: ExternalName
type: ExternalName — необычный тип. Без selector, без endpoints, без ClusterIP. Просто DNS CNAME alias.
apiVersion: v1
kind: Service
metadata:
name: prod-db
spec:
type: ExternalName
externalName: my-db.example.com
Что произойдёт:
- CoreDNS добавит CNAME:
prod-db.default.svc.cluster.local → my-db.example.com. - Никаких ClusterIP/iptables правил — это чисто DNS-уровень.
- Pod пишет
prod-db:5432→ DNS возвращаетmy-db.example.com→ дальше идёт через обычную DNS-цепочку к external адресу.
Полезно для:
- Миграции: пока DB живёт извне кластера, потом её перевозят внутрь — меняется только
externalName. - Отделение конфига от кода: код знает имя
prod-db, Service-объект указывает на реальный адрес.
ExternalName не делает port translation, не проксирует TCP, не имеет ничего кроме DNS. Это просто alias. Если нужен реальный proxy к external endpoint — используйте Service без selector + ручной Endpoints объект с IP внешнего хоста.
Service без selector
Иногда нужен Service, который указывает на не-Kubernetes backend (legacy сервер, external DB, что-то в другом кластере). Можно создать Service без selector и вручную создать Endpoints с нужными IP:
apiVersion: v1
kind: Service
metadata:
name: legacy-api
spec:
ports:
- port: 80
targetPort: 8080
---
apiVersion: v1
kind: Endpoints
metadata:
name: legacy-api # ИМЯ должно совпадать с Service
subsets:
- addresses:
- ip: 192.168.50.10
- ip: 192.168.50.11
ports:
- port: 8080
K8s не будет автоматически менять этот Endpoints — управляет им человек/external оператор. Pod-ы внутри кластера могут писать http://legacy-api:80 и попадать на эти IP.
Killer момент: ClusterIP — это призрак
Давайте чётко: что такое 10.96.0.10 физически?
ClusterIP не принадлежит ни одному network interface. Ни на одной ноде нет интерфейса с этим IP. Никакой Pod не отвечает на ARP-запрос с этим IP. Если запустить ping 10.96.0.10 с ноды — никто не ответит.
ClusterIP существует только в правилах netfilter (или эквиваленте в IPVS/nftables/eBPF). Это правило вида:
iptables -t nat -A KUBE-SERVICES -d 10.96.0.10/32 -p tcp --dport 80 \
-j KUBE-SVC-XXXXXX
Когда пакет с dst=10.96.0.10:80 проходит через kernel netfilter — это правило перехватывает его и отправляет в цепочку KUBE-SVC-XXXXXX, которая делает DNAT на один из endpoint IPs.
Что это значит на практике:
- ClusterIP работает только для протоколов, которые обрабатывает netfilter для NAT — TCP, UDP, SCTP.
- ICMP не работает (ping не пройдёт).
tcpdumpна интерфейсе ноды покажет, что пакет приходит с dst=ClusterIP, но сразу же DNAT-ится (видно в conntrack), и на eth0 уже уходит с dst=PodIP.- Если kube-proxy упал на ноде — Service перестаёт работать на этой ноде (старые правила могут остаться в iptables, но новые changes не применятся).
# Посмотреть, какой DNAT произошёл
ssh node1
conntrack -L | grep 10.96.0.10
# tcp 6 110 ESTABLISHED src=10.244.1.5 dst=10.96.0.10 sport=42124 dport=80
# src=10.244.2.7 dst=10.244.1.5 sport=8080 dport=42124 [ASSURED]
# ↑ reverse direction — обратный пакет приходит от PodIP, conntrack восстанавливает ClusterIP
В следующем уроке мы препарируем сами iptables-правила kube-proxy.
Жизненный цикл Service
# Создать
kubectl apply -f svc.yaml
# Посмотреть
kubectl get svc
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# web ClusterIP 10.96.0.10 <none> 80/TCP 2m
# Endpoints
kubectl get endpoints web
# NAME ENDPOINTS AGE
# web 10.244.1.5:8080,10.244.2.7:8080,10.244.3.9:8080 2m
# EndpointSlices (modern)
kubectl get endpointslices -l kubernetes.io/service-name=web
# Describe — selector, ports, endpoints, sessionAffinity
kubectl describe svc web
# Кратко: создать ClusterIP Service для уже работающего Deployment
kubectl expose deployment web --port=80 --target-port=8080
kubectl expose — императивная команда, удобная для экзамена.
Session affinity
По умолчанию kube-proxy балансирует случайно — каждый новый TCP-connect может попасть на другой Pod. Если приложение не stateless — это проблема. Можно включить sticky sessions:
spec:
sessionAffinity: ClientIP
sessionAffinityConfig:
clientIP:
timeoutSeconds: 10800 # 3 часа, default
С ClientIP kube-proxy запоминает (через conntrack или IPVS hash table): один клиент IP → один backend, пока не истечёт timeout. Это L4 affinity, не cookie-based — не работает за NAT (когда много клиентов с одного IP).
Для L7 sticky sessions (cookie) — нужен Ingress controller.