Kubernetes

Service — мережева абстракція для Pod

Від ефемерних IP-адрес Pod до стабільних Service endpoints — service discovery, балансування навантаження та мережева архітектура Kubernetes

Service — мережева абстракція для Pod

Проблема: ефемерні IP-адреси Pod

У попередніх статтях ми навчилися створювати Deployment з кількома репліками Pod. Але залишилося критично важливе питання: як інші компоненти застосунку можуть звертатись до цих Pod?

Сценарій: frontend потребує доступу до API

Уявіть типову архітектуру веб-застосунку:

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

package "Kubernetes Cluster" {
    package "Frontend Deployment" #e3f2fd {
        component "Frontend Pod 1" as fe1
        component "Frontend Pod 2" as fe2
    }
    
    package "API Deployment" #fff3e0 {
        component "API Pod 1\n10.244.1.10" as api1
        component "API Pod 2\n10.244.1.11" as api2
        component "API Pod 3\n10.244.1.12" as api3
    }
}

fe1 -[dashed]-> api1 : HTTP GET /todos?
fe1 -[dashed]-> api2
fe1 -[dashed]-> api3

fe2 -[dashed]-> api1
fe2 -[dashed]-> api2
fe2 -[dashed]-> api3

note right of api1
    Проблема: IP-адреси Pod
    змінюються при кожному
    перезапуску або rolling update
end note

note left of fe1
    Питання: Як frontend
    знає IP-адреси всіх
    API Pod?
end note

@enduml

Проблеми прямого звернення до Pod за IP:

Ефемерність IP-адрес

Кожен Pod отримує унікальну IP-адресу при створенні. Але ця адреса не стабільна:

  • Pod перезапустився → нова IP-адреса
  • Rolling update → старі Pod видалені, нові мають інші IP
  • Масштабування → нові Pod з новими IP

Приклад: API Pod мав IP 10.244.1.10. Після rolling update він має 10.244.2.15. Frontend продовжує звертатись до старої адреси → помилки.

Відсутність балансування навантаження

Якщо у вас 3 репліки API, frontend має самостійно розподіляти запити між ними. Це означає:

  • Вручну підтримувати список IP-адрес
  • Реалізувати логіку балансування (round-robin, least connections)
  • Відстежувати здоров'я кожного Pod

Це складно, схильно до помилок та дублює функціональність, яку має надавати платформа.

Немає service discovery

У Docker Compose ви використовували DNS-імена сервісів:

services:
  frontend:
    environment:
      - API_URL=http://api:8080

api автоматично резолвився у IP-адресу контейнера. У Kubernetes немає автоматичного DNS для Pod — потрібен механізм service discovery.

Складність конфігурації

Якщо frontend має знати IP кожного API Pod, конфігурація стає кошмаром:

env:
  - name: API_ENDPOINTS
    value: "10.244.1.10,10.244.1.11,10.244.1.12"

Після кожного rolling update потрібно оновлювати цю конфігурацію. Це не масштабується.

Що потрібно замість прямого доступу

Нам потрібен механізм, який:

  1. Надає стабільну точку доступу — одна IP-адреса або DNS-ім'я, яке не змінюється
  2. Автоматично балансує навантаження — розподіляє запити між усіма здоровими Pod
  3. Відстежує здоров'я Pod — не надсилає трафік на Pod, які не готові
  4. Підтримує service discovery — DNS-ім'я автоматично резолвиться у IP
  5. Оновлюється автоматично — при додаванні/видаленні Pod список endpoints оновлюється

Саме це і робить Service.


Що таке Service: формальне визначення

Service — це абстракція Kubernetes, яка визначає логічний набір Pod та політику доступу до них. Service надає стабільну мережеву точку доступу (IP-адресу та DNS-ім'я) для групи ефемерних Pod.

Ключова ідея: Service — це не Pod і не контейнер. Це мережевий об'єкт, який діє як стабільний проксі перед групою Pod. Коли ви звертаєтесь до Service, Kubernetes автоматично перенаправляє запит на один з Pod, які відповідають селектору Service.Аналогія з Docker Compose:У Compose ви використовували DNS-імена сервісів:
services:
  api:
    image: myapi:1.0
    deploy:
      replicas: 3
api автоматично резолвився у IP одного з 3 контейнерів. Kubernetes Service робить те саме, але з більшим контролем та гнучкістю.

Основні компоненти Service

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

package "Service Architecture" {
    component "Service\n(api-service)" as svc #e3f2fd {
        [ClusterIP: 10.96.0.10]
        [DNS: api-service.default.svc.cluster.local]
        [Port: 80]
    }
    
    component "Selector" as sel #fff3e0 {
        [app: api]
        [version: v1]
    }
    
    component "Endpoints" as ep #e8f5e9 {
        [10.244.1.10:8080]
        [10.244.1.11:8080]
        [10.244.1.12:8080]
    }
    
    package "Pods" {
        component "Pod 1\n10.244.1.10" as p1
        component "Pod 2\n10.244.1.11" as p2
        component "Pod 3\n10.244.1.12" as p3
    }
}

svc --> sel : uses
sel --> p1 : matches
sel --> p2 : matches
sel --> p3 : matches
svc --> ep : maintains
ep --> p1
ep --> p2
ep --> p3

note right of svc
    Service надає:
    - Стабільну ClusterIP
    - DNS-ім'я
    - Балансування навантаження
end note

note right of sel
    Selector визначає,
    які Pod належать
    до цього Service
end note

note right of ep
    Endpoints — список
    IP:Port всіх Pod,
    які відповідають селектору
end note

@enduml

Компоненти:

  1. ClusterIP — стабільна внутрішня IP-адреса Service (не змінюється)
  2. DNS-ім'я — автоматично створюється CoreDNS (формат: <service-name>.<namespace>.svc.cluster.local)
  3. Selector — мітки для вибору Pod (як у Deployment)
  4. Endpoints — список IP:Port всіх Pod, які відповідають селектору
  5. Port mapping — маппінг портів Service → Pod

Анатомія Service: структура YAML

Розглянемо базовий приклад Service:

apiVersion: v1
kind: Service
metadata:
  name: api-service
  namespace: default
spec:
  selector:
    app: api
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: 8080
  type: ClusterIP

Розберемо кожне поле детально.

apiVersion
string required
Для Service використовується v1 (core API group). На відміну від Deployment (apps/v1), Service — це базовий ресурс Kubernetes, який існує з самого початку.
kind
string required
Тип ресурсу — Service. Це вказує Kubernetes, що ви створюєте мережевий об'єкт для доступу до Pod.
metadata.name
string required
Унікальне ім'я Service у межах namespace. Це ім'я буде використовуватись у DNS: <name>.<namespace>.svc.cluster.local.Важливо: Ім'я має відповідати DNS-стандарту (малі літери, цифри, дефіси). Максимум 63 символи.
metadata.namespace
string
Namespace, у якому буде створено Service. Service може звертатись лише до Pod у тому самому namespace (якщо не використовується ExternalName).
spec.selector
map required
Мітки для вибору Pod, які належать до цього Service. Kubernetes автоматично знаходить всі Pod з цими мітками та додає їх до Endpoints.Критично важливо: Мітки у selector мають збігатись з мітками у template.metadata.labels Deployment.Приклад:
# Service
selector:
  app: api

# Deployment
template:
  metadata:
    labels:
      app: api  # ← Має збігатись!
spec.ports
array required
Список портів, які експонує Service. Кожен порт має наступні поля:
  • name — ім'я порту (опціонально, але рекомендується для багатопортових Service)
  • protocol — протокол (TCP або UDP, за замовчуванням TCP)
  • port — порт Service (на якому Service слухає)
  • targetPort — порт Pod (на який перенаправляється трафік)
Приклад:
ports:
  - name: http
    port: 80        # Service слухає на порту 80
    targetPort: 8080  # Трафік йде на порт 8080 Pod
Це означає: запит до api-service:80 перенаправляється на <pod-ip>:8080.
spec.type
string
Тип Service, який визначає, як Service експонується. Доступні типи:
  • ClusterIP — внутрішній IP, доступний лише всередині кластера (за замовчуванням)
  • NodePort — експонує Service на статичному порту кожного вузла
  • LoadBalancer — створює зовнішній load balancer (у хмарних провайдерів)
  • ExternalName — маппінг на зовнішнє DNS-ім'я
Детально розглянемо кожен тип далі.

Як працює маппінг портів

Розберемо детально, як працює port та targetPort:

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

actor "Client" as client
component "Service\napi-service" as svc #e3f2fd {
    [ClusterIP: 10.96.0.10]
    [Port: 80]
}

component "Pod 1" as p1 #e8f5e9 {
    [IP: 10.244.1.10]
    [Container Port: 8080]
}

component "Pod 2" as p2 #e8f5e9 {
    [IP: 10.244.1.11]
    [Container Port: 8080]
}

component "Pod 3" as p3 #e8f5e9 {
    [IP: 10.244.1.12]
    [Container Port: 8080]
}

client --> svc : GET http://api-service:80/todos
svc --> p1 : GET http://10.244.1.10:8080/todos
svc --> p2 : GET http://10.244.1.11:8080/todos
svc --> p3 : GET http://10.244.1.12:8080/todos

note right of svc
    Service слухає на порту 80
    (port: 80)
    
    Перенаправляє на порт 8080 Pod
    (targetPort: 8080)
end note

note right of p1
    Контейнер слухає на порту 8080
    (containerPort: 8080 у Pod spec)
end note

@enduml

Важливі моменти:

  1. port — це порт, на якому Service слухає. Клієнти звертаються до Service на цьому порту.
  2. targetPort — це порт, на якому Pod слухає. Service перенаправляє трафік на цей порт.
  3. containerPort (у Pod spec) — це порт, який контейнер експонує. Має збігатись з targetPort.

Типова помилка новачків:

# Service
ports:
  - port: 80
    targetPort: 8080

# Pod (у Deployment)
containers:
  - name: api
    ports:
      - containerPort: 80  # ← ПОМИЛКА! Має бути 8080

У цьому випадку Service перенаправляє трафік на порт 8080 Pod, але контейнер слухає на порту 80. Запити не доходять до застосунку.

Критично важливо:targetPort у Service має збігатись з containerPort у Pod spec. Інакше трафік не дійде до застосунку.Правильна конфігурація:
# Service
spec:
  ports:
    - port: 80
      targetPort: 8080

# Deployment
spec:
  template:
    spec:
      containers:
        - name: api
          ports:
            - containerPort: 8080  # ← Збігається з targetPort

targetPort як ім'я порту

Замість числа можна використовувати ім'я порту:

# Deployment
spec:
  template:
    spec:
      containers:
        - name: api
          ports:
            - name: http  # ← Ім'я порту
              containerPort: 8080

# Service
spec:
  ports:
    - port: 80
      targetPort: http  # ← Посилання на ім'я

Переваги:

  • Якщо змінюється номер порту у Pod (8080 → 9090), не потрібно оновлювати Service
  • Більш читабельно — зрозуміло, що це HTTP-порт

Типи Service: ClusterIP, NodePort, LoadBalancer, ExternalName

Kubernetes підтримує чотири типи Service, кожен для різних сценаріїв.

1. ClusterIP (за замовчуванням)

Тип сервісу, який виділяє для вашого набору Pod'ів єдину внутрішню IP-адресу (ClusterIP), доступну виключно всередині самого Kubernetes-кластера.

  • Простими словами (з іншого ракурсу): Це приватна внутрішня телефонна лінія вашої компанії. Хтось зовні не може на неї набрати напрямую, але колеги з інших кабінетів (інші Pod'и) можуть дзвонить за цим коротким номером без перешкод.
  • Конкретний приклад: Уявіть, що у вас є мікросервісний додаток: веб-фронтенд та база даних PostgreSQL. Ви не хочете, щоб будь-хто з інтернету міг напряму достукатися до вашої бази даних. Тому ви створюєте для PostgreSQL сервіс із типом ClusterIP. Тепер ваш фронтенд-контейнер, що працює всередині кластера, може спокійно звертатися до бази за адресою postgres-service, тоді як будь-які зовнішні запити з інтернету будуть заблоковані на рівні мережі кластера. Це стандарт де-факто для безпечної внутрішньої взаємодії.

YAML:

apiVersion: v1
kind: Service
metadata:
  name: api-service
spec:
  type: ClusterIP  # За замовчуванням, можна не вказувати
  selector:
    app: api
  ports:
    - port: 80
      targetPort: 8080

Що відбувається:

  1. Kubernetes виділяє IP-адресу з внутрішнього діапазону (наприклад, 10.96.0.10)
  2. CoreDNS створює DNS-запис: api-service.default.svc.cluster.local10.96.0.10
  3. Будь-який Pod у кластері може звертатись до Service за DNS-іменем або IP

Візуалізація:

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

package "Kubernetes Cluster" {
    component "Frontend Pod" as fe #e3f2fd
    
    component "Service\napi-service" as svc #fff3e0 {
        [ClusterIP: 10.96.0.10]
        [DNS: api-service.default.svc.cluster.local]
    }
    
    package "API Pods" #e8f5e9 {
        component "Pod 1\n10.244.1.10:8080" as p1
        component "Pod 2\n10.244.1.11:8080" as p2
        component "Pod 3\n10.244.1.12:8080" as p3
    }
}

actor "Зовнішній користувач" as user #ffebee

fe --> svc : GET http://api-service/todos
svc --> p1
svc --> p2
svc --> p3

user -[dashed]-> svc : ❌ Недоступний ззовні

note right of svc
    ClusterIP доступний
    ЛИШЕ всередині кластера
end note

note bottom of user
    Зовнішні користувачі
    не можуть звертатись
    до ClusterIP Service
end note

@enduml

Коли використовувати:

  • Внутрішня комунікація між сервісами (frontend → API, API → database)
  • Сервіси, які не мають бути доступні ззовні (бази даних, черги повідомлень)
  • Більшість Service у production — це ClusterIP

Як працює DNS-імя в Kubernetes (CoreDNS)

Коли ви створюєте Service у Kubernetes, вбудована служба DNS (зазвичай CoreDNS) автоматично створює для нього внутрішній DNS-запис. Це дозволяє Pod'ам спілкуватися між собою не за IP-адресами (які постійно змінюються при перезапуску контейнерів), а за постійними, зрозумілими іменами.

Повне доменне ім'я (FQDN) для будь-якого сервісу формується за шаблоном:

[назва-сервісу].[namespace].svc.cluster.local

Для нашого сервісу повна адреса буде: api-service.default.svc.cluster.local.

Чому в коді ми пишемо просто http://api-service?

  1. Суфікси пошуку (DNS Search Paths): Коли Kubernetes створює будь-який Pod, він автоматично прописує у його конфігурацію DNS (файл /etc/resolv.conf) суфікси пошуку. Вони включають поточний namespace (наприклад, default.svc.cluster.local). Тому, якщо ваші контейнери знаходяться в одному namespace, DNS-клієнт автоматично підставить суфікс. Вам достатньо написати лише коротку назву сервісу — api-service — і CoreDNS успішно розпізнає повну адресу.
  2. Стандартний порт HTTP: У нашому YAML-файлі для api-service ми вказали зовнішній порт сервісу port: 80. Оскільки порт 80 є стандартним для протоколу http://, його не потрібно вказувати явно в URL. Рядок http://api-service під капотом резолвиться в запит до IP-адреси сервісу на 80-й порт (наприклад, http://10.96.0.10:80).
Це фундаментальна перевага Kubernetes: вам абсолютно не потрібно знати динамічні IP-адреси Pod'ів чи сервісів. Ви просто звертаєтесь до імені сервісу, а Kubernetes автоматично бере на себе всю роботу з пошуку контейнерів, перевірки їхньої готовності та балансування навантаження!

Приклад використання у .NET:

var builder = WebApplication.CreateBuilder(args);

// Frontend звертається до API через Service DNS
var apiUrl = builder.Configuration["ApiUrl"] ?? "http://api-service";

builder.Services.AddHttpClient("ApiClient", client =>
{
    client.BaseAddress = new Uri(apiUrl);
});

var app = builder.Build();
app.Run();

ConfigMap для frontend:

apiVersion: v1
kind: ConfigMap
metadata:
  name: frontend-config
data:
  ApiUrl: "http://api-service"  # DNS-ім'я Service

2. NodePort

Тип сервісу, який відкриває певний фіксований порт (у діапазоні 30000-32767) на абсолютно кожному сервері (вузлі/node) вашого Kubernetes-кластера. Будь-який зовнішній трафік, що приходить на цей порт будь-якого сервера, автоматично перенаправляється на ваші Pod'и.

  • Простими словами (з іншого ракурсу): Це схоже на виділений домофонний код у кожному під'їзді одного великого житлового комплексу. У який би під'їзд ви не підійшли (Node IP), якщо ви наберете цей спеціальний код (NodePort), дзвінок піде в ту саму конкретну квартиру (ваш Pod).
  • Конкретний приклад: Уявіть, що ви запустили веб-сайт у локальному кластері Minikube на вашому комп'ютері і хочете показати його колезі в офісі. Оскільки IP-адреси Pod'ів є внутрішніми для кластера, колега не зможе зайти на сайт. Якщо ви створите сервіс із типом NodePort та вкажете nodePort: 32000, ваш сайт стане доступним за адресою комп'ютера у локальній мережі: наприклад, http://192.168.1.150:32000. Будь-яка Node кластера візьме цей запит і перенаправить його до потрібного контейнера.

YAML:

apiVersion: v1
kind: Service
metadata:
  name: api-service
spec:
  type: NodePort
  selector:
    app: api
  ports:
    - port: 80
      targetPort: 8080
      nodePort: 30080  # Опціонально, Kubernetes виділить автоматично (30000-32767)

Що відбувається:

  1. Kubernetes виділяє ClusterIP (як у ClusterIP Service)
  2. Kubernetes відкриває порт на кожному вузлі кластера (наприклад, 30080)
  3. Трафік на <NodeIP>:30080 перенаправляється на Service, який перенаправляє на Pod

Візуалізація:

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

actor "Зовнішній користувач" as user

package "Kubernetes Cluster" {
    component "Node 1\n192.168.1.10" as node1 #e3f2fd {
        [NodePort: 30080]
    }
    
    component "Node 2\n192.168.1.11" as node2 #e3f2fd {
        [NodePort: 30080]
    }
    
    component "Service\napi-service" as svc #fff3e0 {
        [ClusterIP: 10.96.0.10]
        [NodePort: 30080]
    }
    
    package "API Pods" #e8f5e9 {
        component "Pod 1\n10.244.1.10:8080" as p1
        component "Pod 2\n10.244.1.11:8080" as p2
    }
}

user --> node1 : GET http://192.168.1.10:30080/todos
user --> node2 : GET http://192.168.1.11:30080/todos

node1 --> svc
node2 --> svc

svc --> p1
svc --> p2

note right of node1
    Кожен вузол слухає
    на порту 30080
    та перенаправляє
    на Service
end note

note bottom of user
    Користувач може звертатись
    до будь-якого вузла на порту 30080
end note

@enduml

Діапазон NodePort:

За замовчуванням Kubernetes виділяє порти з діапазону 30000-32767. Це можна змінити у конфігурації API Server, але не рекомендується.

Коли використовувати:

  • Локальна розробка (Minikube, kind) — швидкий доступ до сервісу ззовні
  • Тестування — не потрібен зовнішній load balancer
  • On-premise кластери без підтримки LoadBalancer
  • Debugging — тимчасовий доступ до сервісу

Коли НЕ використовувати:

  • Production у хмарі — використовуйте LoadBalancer або Ingress
  • Багато сервісів — NodePort займає порти на всіх вузлах
  • Потрібен HTTPS — NodePort не підтримує TLS termination

Приклад використання:

Доступ до NodePort Service
# Створення NodePort Service
$ kubectl apply -f api-nodeport-service.yaml
service/api-service created
# Перегляд Service
$ kubectl get service api-service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
api-service NodePort 10.96.0.10 <none> 80:30080/TCP 10s
# Отримання IP вузла (Minikube)
$ minikube ip
192.168.49.2
# Доступ до Service ззовні
$ curl http://192.168.49.2:30080/todos
{"todos":[],"count":0}
Minikube service команда:Minikube надає зручну команду для відкриття NodePort Service у браузері:
minikube service api-service
Це автоматично відкриє браузер з правильною URL (http://<minikube-ip>:<node-port>).

3. LoadBalancer

Тип сервісу, який інтегрується з хмарним провайдером (наприклад AWS, Google Cloud, Azure) і автоматично замовляє у нього реальний, зовнішній мережевий балансувальник навантаження. Провайдер виділяє публічну статичну IP-адресу, і весь трафік з інтернету через цю IP надходить безпосередньо у ваш Kubernetes-кластер.

  • Простими словами (з іншого ракурсу): Це як найняти професійного швейцара чи хостес на вході до ресторану. Клієнтам (користувачам в інтернеті) не потрібно шукати службові входи чи дзвонити на внутрішні номери — вони просто приходять на центральний вхід (публічний IP), а швейцар (Load Balancer) сам бережно проводить їх за потрібний вільний столик (Pod) всередині залу.
  • Конкретний приклад: Ви розгорнули свій інтернет-магазин у хмарі AWS. Якщо ви створите сервіс типу LoadBalancer, AWS автоматично запустить для вас сервіс Classic/Network Load Balancer та надасть публічну адресу (наприклад, http://a84729...us-east-1.elb.amazonaws.com або статичний IP 1.2.3.4). Тепер будь-який користувач у світі може зайти на цей сайт. Балансувальник прийме запит, рівномірно розподілить його між працюючими репліками вашого застосунку і забезпечить відмовостійкість: якщо один із серверів (Node) раптово вимкнеться, балансувальник миттєво перенаправить трафік на інші робочі машини.

YAML:

apiVersion: v1
kind: Service
metadata:
  name: api-service
spec:
  type: LoadBalancer
  selector:
    app: api
  ports:
    - port: 80
      targetPort: 8080

Що відбувається:

  1. Kubernetes створює ClusterIP Service (внутрішній доступ)
  2. Kubernetes створює NodePort (доступ через вузли)
  3. Kubernetes запитує у хмарного провайдера створення зовнішнього load balancer
  4. Load balancer отримує публічну IP-адресу та перенаправляє трафік на NodePort

Візуалізація:

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

actor "Зовнішній користувач" as user

cloud "Cloud Provider" {
    component "Load Balancer\n34.123.45.67" as lb #e3f2fd
}

package "Kubernetes Cluster" {
    component "Node 1\n10.0.1.10" as node1 #fff3e0 {
        [NodePort: 31234]
    }
    
    component "Node 2\n10.0.1.11" as node2 #fff3e0 {
        [NodePort: 31234]
    }
    
    component "Service\napi-service" as svc #e8f5e9 {
        [ClusterIP: 10.96.0.10]
        [Type: LoadBalancer]
    }
    
    package "API Pods" {
        component "Pod 1" as p1
        component "Pod 2" as p2
        component "Pod 3" as p3
    }
}

user --> lb : GET http://34.123.45.67/todos
lb --> node1 : Балансування
lb --> node2 : Балансування

node1 --> svc
node2 --> svc

svc --> p1
svc --> p2
svc --> p3

note right of lb
    Load Balancer:
    - Публічна IP-адреса
    - Health checks
    - SSL termination (опціонально)
    - DDoS protection
end note

note right of svc
    Service автоматично
    отримує EXTERNAL-IP
    від load balancer
end note

@enduml

Коли використовувати:

  • Production у хмарі (AWS, GCP, Azure)
  • Потрібен публічний доступ до сервісу
  • Один сервіс на один load balancer (для багатьох сервісів використовуйте Ingress)

Коли НЕ використовувати:

  • Локальна розробка (Minikube не підтримує LoadBalancer, використовуйте NodePort)
  • Багато сервісів — кожен LoadBalancer коштує грошей (використовуйте Ingress)
  • On-premise кластери без підтримки LoadBalancer

Приклад використання:

LoadBalancer Service
$ kubectl apply -f api-loadbalancer-service.yaml
service/api-service created
# Перегляд Service (EXTERNAL-IP спочатку <pending>)
$ kubectl get service api-service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
api-service LoadBalancer 10.96.0.10 <pending> 80:31234/TCP 5s
# Через 1-2 хвилини load balancer готовий
$ kubectl get service api-service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
api-service LoadBalancer 10.96.0.10 34.123.45.67 80:31234/TCP 2m
# Доступ через публічну IP
$ curl http://34.123.45.67/todos
{"todos":[],"count":0}
LoadBalancer у Minikube:Minikube не підтримує LoadBalancer Service нативно. Service залишається у стані <pending>. Для локальної розробки використовуйте:
  1. NodePort — найпростіший спосіб
  2. minikube tunnel — емулює LoadBalancer:
minikube tunnel
Це створює мережевий тунель та призначає EXTERNAL-IP з діапазону 127.0.0.1/8. Команда має працювати постійно у фоні.

Вартість LoadBalancer:

Кожен LoadBalancer Service створює окремий load balancer у хмарі, що коштує грошей:

  • AWS ELB: ~$16-25/місяць + трафік
  • GCP Load Balancer: ~$18/місяць + трафік
  • Azure Load Balancer: ~$18/місяць + трафік

Якщо у вас 10 сервісів, це $180-250/місяць лише за load balancers. Для економії використовуйте Ingress (один load balancer для багатьох сервісів).


4. ExternalName

Особливий тип сервісу, який виступає в ролі внутрішнього DNS-аліасу (псевдоніма) і перенаправляє запити на якесь зовнішнє доменне ім'я (поза межами вашого кластера). Він не виділяє жодних IP-адрес і не проксує трафік самостійно — замість цього він просто повертає стандартний DNS CNAME-запис.

  • Простими словами (з іншого ракурсу): Це внутрішня переадресація чи закладка в телефонній книзі вашої компанії. Замість того, щоб вчити кожного співробітника набирати довгий складний номер зовнішнього партнера, ви створюєте для нього внутрішній короткий псевдонім. Якщо партнер зміниться, ви просто оновить запис в одному місці (у маніфесті сервісу), а код ваших додатків залишиться незмінним.
  • Конкретний приклад: Ваша програма працює в кластері Kubernetes, але використовує сторонню базу даних MongoDB Atlas, розміщену на зовнішньому хостингу за адресою prod-db-948a.mongodb.net. Замість того, щоб зашивати це довге і нестабільне ім'я прямо в конфігураційні файли вашого коду, ви створюєте сервіс типу ExternalName із назвою my-database та externalName: prod-db-948a.mongodb.net. Тепер у вашому .NET-коді рядок підключення буде виглядати максимально просто: mongodb://my-database. Якщо ви вирішите змінити базу даних на інший сервер або переїдете на іншого провайдера, ви просто зміните значення externalName в одному YAML-маніфесті, не перезбираючи та не перезапускаючи сам додаток.

YAML:

apiVersion: v1
kind: Service
metadata:
  name: external-api
spec:
  type: ExternalName
  externalName: api.example.com

Що відбувається:

  1. Kubernetes НЕ створює ClusterIP
  2. CoreDNS створює CNAME-запис: external-api.default.svc.cluster.localapi.example.com
  3. Запити до external-api резолвяться у api.example.com та йдуть напряму до зовнішнього сервісу

Візуалізація:

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

package "Kubernetes Cluster" {
    component "Frontend Pod" as fe #e3f2fd
    
    component "Service\nexternal-api" as svc #fff3e0 {
        [Type: ExternalName]
        [externalName: api.example.com]
        [NO ClusterIP]
    }
}

cloud "Зовнішній світ" {
    component "api.example.com\n34.56.78.90" as external #e8f5e9
}

fe --> svc : GET http://external-api/data
svc --> external : DNS CNAME redirect

note right of svc
    ExternalName Service
    не має ClusterIP
    
    Це просто DNS alias
    для зовнішнього сервісу
end note

note right of external
    Трафік йде напряму
    до зовнішнього сервісу,
    без проксування через
    Kubernetes
end note

@enduml

Коли використовувати:

  • Міграція з зовнішнього сервісу у Kubernetes — спочатку ExternalName, потім заміна на ClusterIP
  • Доступ до зовнішніх API (наприклад, AWS RDS, зовнішній Redis)
  • Абстракція зовнішніх залежностей — код звертається до database-service, а не до prod-db.us-east-1.rds.amazonaws.com

Приклад використання:

# ExternalName для зовнішньої бази даних
apiVersion: v1
kind: Service
metadata:
  name: postgres-service
spec:
  type: ExternalName
  externalName: prod-db.us-east-1.rds.amazonaws.com

У .NET коді:

var builder = WebApplication.CreateBuilder(args);

// Код звертається до Service, а не до зовнішнього DNS
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
// "Host=postgres-service;Database=mydb;Username=user;Password=pass"

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(connectionString));

Переваги:

  • Легко змінити зовнішній endpoint — просто оновити externalName у Service
  • Код не залежить від конкретного DNS-імені зовнішнього сервісу
  • Можна легко мігрувати: ExternalName → ClusterIP (коли база даних переїде у Kubernetes)
Обмеження ExternalName:
  1. Немає балансування навантаження — Kubernetes не проксує трафік, лише резолвить DNS
  2. Немає health checks — Kubernetes не перевіряє доступність зовнішнього сервісу
  3. Немає TLS termination — трафік йде напряму, без можливості додати TLS
  4. Працює лише з DNS — не можна використовувати IP-адресу
Якщо потрібен більший контроль, використовуйте Service без selector + Endpoints (розглянемо далі).

Порівняння типів Service

ClusterIP

Використання: Внутрішня комунікація між сервісами

Доступність: Лише всередині кластера

IP-адреса: Внутрішня ClusterIP

Вартість: Безкоштовно

Приклад: API → Database, Frontend → API

NodePort

Використання: Локальна розробка, debugging, on-premise

Доступність: Через <NodeIP>:<NodePort>

IP-адреса: ClusterIP + порт на кожному вузлі

Вартість: Безкоштовно

Приклад: Доступ до API з локальної машини

LoadBalancer

Використання: Production у хмарі, публічний доступ

Доступність: Через публічну IP load balancer

IP-адреса: ClusterIP + NodePort + зовнішня IP

Вартість: ~$18-25/місяць за load balancer

Приклад: Публічний API, веб-сайт

ExternalName

Використання: Доступ до зовнішніх сервісів

Доступність: DNS CNAME до зовнішнього сервісу

IP-адреса: Немає (лише DNS alias)

Вартість: Безкоштовно

Приклад: AWS RDS, зовнішній API

Таблиця порівняння:

ХарактеристикаClusterIPNodePortLoadBalancerExternalName
ClusterIP✅ Так✅ Так✅ Так❌ Ні
NodePort❌ Ні✅ Так (30000-32767)✅ Так (автоматично)❌ Ні
Зовнішня IP❌ Ні❌ Ні✅ Так❌ Ні
Балансування✅ Так✅ Так✅ Так❌ Ні
Health checks✅ Так✅ Так✅ Так❌ Ні
Доступ ззовні❌ Ні✅ Так✅ ТакN/A
ВартістьБезкоштовноБезкоштовно$18-25/місБезкоштовно

Як працює Service: kube-proxy та iptables

Тепер розберемо, як саме Kubernetes перенаправляє трафік від Service до Pod. За це відповідає компонент kube-proxy.

Архітектура kube-proxy

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

package "Kubernetes Node" {
    component "kube-proxy" as proxy #e3f2fd {
        [Watches API Server]
        [Updates iptables/IPVS]
    }
    
    component "iptables/IPVS" as ipt #fff3e0 {
        [Routing rules]
        [Load balancing]
    }
    
    component "Pod 1\n10.244.1.10" as p1 #e8f5e9
    component "Pod 2\n10.244.1.11" as p2 #e8f5e9
}

component "API Server" as api
component "Service\napi-service\n10.96.0.10" as svc

api --> proxy : Watch Services/Endpoints
proxy --> ipt : Update rules

actor "Client" as client

client --> ipt : GET http://10.96.0.10/todos
ipt --> p1 : DNAT to 10.244.1.10:8080
ipt --> p2 : DNAT to 10.244.1.11:8080

note right of proxy
    kube-proxy працює
    на кожному вузлі
    
    Не проксує трафік,
    а налаштовує iptables
end note

note right of ipt
    iptables виконує:
    - DNAT (зміна IP:Port)
    - Load balancing (random)
    - Health checks (через Endpoints)
end note

@enduml

Режими роботи kube-proxy

kube-proxy підтримує три режими:

iptables (за замовчуванням)
Як працює:
  1. kube-proxy стежить за Service та Endpoints через API Server
  2. Для кожного Service створюються правила iptables
  3. Коли пакет надходить на ClusterIP, iptables виконує DNAT (Destination NAT) — змінює IP:Port на IP:Port одного з Pod
  4. Балансування: random (випадковий вибір Pod)
Переваги:
  • Стабільний, перевірений часом
  • Низьке споживання ресурсів
  • Працює на рівні ядра Linux (швидко)
Недоліки:
  • Погана масштабованість (багато правил iptables при тисячах Service)
  • Балансування не ідеальне (random, а не round-robin)
  • Складно діагностувати проблеми
IPVS (IP Virtual Server)
Як працює:
  1. kube-proxy створює віртуальний сервер IPVS для кожного Service
  2. IPVS виконує балансування навантаження на рівні ядра
  3. Підтримує різні алгоритми: round-robin, least connections, source hashing
Переваги:
  • Краща масштабованість (тисячі Service)
  • Кращі алгоритми балансування
  • Вища продуктивність при великій кількості Service
Недоліки:
  • Потребує модуль ядра IPVS (не завжди доступний)
  • Складніше налаштування
Увімкнення IPVS:
# kube-proxy ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
  name: kube-proxy
  namespace: kube-system
data:
  config.conf: |
    mode: "ipvs"
    ipvs:
      scheduler: "rr"  # round-robin
userspace (застарілий)
Як працює:
  1. kube-proxy сам проксує трафік (працює як reverse proxy)
  2. Трафік йде через user space, а не kernel space
Недоліки:
  • Повільний (трафік проходить через user space)
  • Застарілий, не рекомендується
Статус: Deprecated, не використовується у production

Детальний розбір iptables режиму

Давайте подивимося, які саме правила створює kube-proxy:

iptables правила для Service
# Перегляд iptables правил для Service
$ sudo iptables -t nat -L KUBE-SERVICES | grep api-service
KUBE-SVC-XXXXX tcp -- anywhere 10.96.0.10 /* default/api-service cluster IP */ tcp dpt:http
# Детальний перегляд правил для конкретного Service
$ sudo iptables -t nat -L KUBE-SVC-XXXXX
Chain KUBE-SVC-XXXXX (1 references)
target prot opt source destination
KUBE-SEP-AAAA all -- anywhere anywhere /* default/api-service */ statistic mode random probability 0.33333
KUBE-SEP-BBBB all -- anywhere anywhere /* default/api-service */ statistic mode random probability 0.50000
KUBE-SEP-CCCC all -- anywhere anywhere /* default/api-service */

Що означають ці правила:

  1. KUBE-SERVICES — головний ланцюжок для всіх Service
  2. KUBE-SVC-XXXXX — ланцюжок для конкретного Service (api-service)
  3. KUBE-SEP-AAAA/BBBB/CCCC — ланцюжки для кожного Pod (Service Endpoint)

Балансування:

  • Перший Pod: ймовірність 33.33% (1/3)
  • Другий Pod: ймовірність 50% з решти (1/2 від 66.67%)
  • Третій Pod: решта 33.33%

Це дає рівномірний розподіл 33.33% / 33.33% / 33.33%.

DNAT правило для Pod:

DNAT правило
$ sudo iptables -t nat -L KUBE-SEP-AAAA
Chain KUBE-SEP-AAAA (1 references)
target prot opt source destination
DNAT tcp -- anywhere anywhere /* default/api-service */ tcp to:10.244.1.10:8080

Це правило змінює destination IP:Port з 10.96.0.10:80 (Service) на 10.244.1.10:8080 (Pod).

Візуалізація потоку трафіку

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

participant "Client Pod\n10.244.1.5" as client
participant "iptables" as ipt
participant "Service\n10.96.0.10:80" as svc
participant "Pod 1\n10.244.1.10:8080" as p1
participant "Pod 2\n10.244.1.11:8080" as p2
participant "Pod 3\n10.244.1.12:8080" as p3

client -> ipt : SYN to 10.96.0.10:80
activate ipt

ipt -> ipt : Match KUBE-SERVICES rule
ipt -> ipt : Jump to KUBE-SVC-XXXXX
ipt -> ipt : Random selection (33.33% each)
ipt -> ipt : Jump to KUBE-SEP-AAAA (Pod 1 selected)
ipt -> ipt : DNAT: 10.96.0.10:80 → 10.244.1.10:8080

deactivate ipt

ipt -> p1 : SYN to 10.244.1.10:8080
p1 -> ipt : SYN-ACK from 10.244.1.10:8080

activate ipt
ipt -> ipt : SNAT: 10.244.1.10:8080 → 10.96.0.10:80
deactivate ipt

ipt -> client : SYN-ACK from 10.96.0.10:80

client -> ipt : GET /todos
ipt -> p1 : GET /todos (DNAT)
p1 -> ipt : 200 OK
ipt -> client : 200 OK (SNAT)

note right of ipt
    iptables виконує:
    1. DNAT (вхідний трафік)
    2. SNAT (вихідний трафік)
    
    Клієнт не знає про Pod,
    бачить лише Service IP
end note

@enduml

Ключові моменти:

  1. DNAT (Destination NAT) — зміна destination IP:Port з Service на Pod
  2. SNAT (Source NAT) — зміна source IP:Port у відповіді з Pod на Service
  3. Прозорість — клієнт не знає про існування Pod, бачить лише Service
  4. Stateful — iptables відстежує з'єднання (connection tracking), тому відповіді йдуть до того самого клієнта

CoreDNS та Service Discovery

Одна з найважливіших можливостей Service — автоматичний service discovery через DNS. За це відповідає CoreDNS.

Що таке CoreDNS

CoreDNS — це DNS-сервер, який працює у Kubernetes кластері та автоматично створює DNS-записи для всіх Service.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

package "Kubernetes Cluster" {
    component "CoreDNS Pod" as dns #e3f2fd {
        [DNS Server]
        [Watches API Server]
    }
    
    component "API Server" as api #fff3e0
    
    component "Client Pod" as client #e8f5e9
    
    component "Service\napi-service" as svc {
        [ClusterIP: 10.96.0.10]
    }
}

api --> dns : Watch Services
dns --> dns : Create DNS records

client --> dns : DNS query: api-service.default.svc.cluster.local
dns --> client : Response: 10.96.0.10

client --> svc : HTTP GET http://10.96.0.10/todos

note right of dns
    CoreDNS автоматично
    створює DNS-записи
    для всіх Service
end note

note right of client
    Pod налаштовані
    використовувати CoreDNS
    як DNS-сервер
    (через /etc/resolv.conf)
end note

@enduml

Формат DNS-імен Service

Kubernetes створює DNS-записи у наступному форматі:

<service-name>.<namespace>.svc.<cluster-domain>

Компоненти:

  • service-name — ім'я Service (з metadata.name)
  • namespace — namespace, у якому створено Service
  • svc — константа (означає "service")
  • cluster-domain — домен кластера (за замовчуванням cluster.local)

Приклади:

ServiceNamespaceПовне DNS-ім'я
api-servicedefaultapi-service.default.svc.cluster.local
postgresdatabasepostgres.database.svc.cluster.local
rediscacheredis.cache.svc.cluster.local

Скорочені форми DNS-імен

Kubernetes підтримує скорочені форми для зручності:

<service-name> (найкоротша форма)
Працює: Лише у тому самому namespaceПриклад:
// Pod у namespace "default" звертається до Service "api-service" у "default"
var apiUrl = "http://api-service";
DNS resolution:
  1. Спроба: api-service.default.svc.cluster.local
  2. Якщо не знайдено: помилка
Коли використовувати: Більшість випадків (якщо Service у тому самому namespace)
<service-name>.<namespace>
Працює: З будь-якого namespaceПриклад:
// Pod у namespace "frontend" звертається до Service "postgres" у "database"
var dbHost = "postgres.database";
DNS resolution:
  1. Спроба: postgres.database.svc.cluster.local
Коли використовувати: Міжнамespace комунікація (frontend → database)
<service-name>.<namespace>.svc.cluster.local (повна форма)
Працює: Завжди, з будь-якого місцяПриклад:
var apiUrl = "http://api-service.default.svc.cluster.local";
Коли використовувати:
  • Явна специфікація (для документації)
  • Уникнення конфліктів імен
  • Зовнішні системи (якщо вони мають доступ до CoreDNS)

Як Pod налаштовані використовувати CoreDNS

Кожен Pod автоматично налаштовується використовувати CoreDNS як DNS-сервер:

/etc/resolv.conf у Pod
$ kubectl exec -it api-pod-xxx -- cat /etc/resolv.conf
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5

Що означають ці рядки:

  • nameserver 10.96.0.10 — IP-адреса CoreDNS Service (kube-dns)
  • search default.svc.cluster.local svc.cluster.local cluster.local — список доменів для автоматичного додавання
  • options ndots:5 — якщо у DNS-запиті менше 5 крапок, додавати search domains

Як працює search:

Коли Pod робить DNS-запит api-service, резолвер пробує:

  1. api-service.default.svc.cluster.local ✅ (знайдено)
  2. Якщо не знайдено: api-service.svc.cluster.local
  3. Якщо не знайдено: api-service.cluster.local
  4. Якщо не знайдено: api-service (як є)

Це дозволяє використовувати короткі імена (api-service замість повного DNS-імені).

DNS-записи для різних типів Service

ClusterIP Service

DNS-запис: A-запис (IP-адреса)

Приклад:

nslookup api-service.default.svc.cluster.local
# Name:    api-service.default.svc.cluster.local
# Address: 10.96.0.10

Що повертається: ClusterIP Service

Headless Service (ClusterIP: None)

DNS-запис: A-записи для кожного Pod

Приклад:

nslookup postgres.database.svc.cluster.local
# Name:    postgres.database.svc.cluster.local
# Address: 10.244.1.10  (Pod 1)
# Address: 10.244.1.11  (Pod 2)
# Address: 10.244.1.12  (Pod 3)

Що повертається: IP-адреси всіх Pod (детально розглянемо далі)

ExternalName Service

DNS-запис: CNAME-запис

Приклад:

nslookup external-api.default.svc.cluster.local
# external-api.default.svc.cluster.local canonical name = api.example.com
# Name:    api.example.com
# Address: 34.56.78.90

Що повертається: CNAME на зовнішнє DNS-ім'я

Тестування DNS resolution

Давайте протестуємо DNS resolution у реальному кластері:

Тестування DNS
# Створення тестового Pod з curl та nslookup
$ kubectl run test-dns --image=curlimages/curl:latest --rm -it --restart=Never -- sh
# Тест 1: Коротка форма (той самий namespace)
$ nslookup api-service
Server: 10.96.0.10
Address: 10.96.0.10:53
Name: api-service.default.svc.cluster.local
Address: 10.96.0.15
# Тест 2: З namespace (міжнамespace)
$ nslookup postgres.database
Server: 10.96.0.10
Address: 10.96.0.10:53
Name: postgres.database.svc.cluster.local
Address: 10.96.0.20
# Тест 3: Повна форма
$ nslookup api-service.default.svc.cluster.local
Server: 10.96.0.10
Address: 10.96.0.10:53
Name: api-service.default.svc.cluster.local
Address: 10.96.0.15
# Тест 4: HTTP-запит через DNS-ім'я
$ curl http://api-service/health
{"status":"healthy","version":"1.0.0"}

Headless Service — прямий доступ до Pod

Headless Service — це Service без ClusterIP (clusterIP: None). Замість балансування навантаження через Service IP, DNS повертає IP-адреси всіх Pod напряму.

Навіщо потрібен Headless Service

StatefulSet

Для stateful застосунків (бази даних, черги) потрібен доступ до конкретного Pod за стабільним DNS-іменем. Headless Service надає DNS-запис для кожного Pod: <pod-name>.<service-name>.<namespace>.svc.cluster.local.

Приклад: PostgreSQL primary/replica — клієнт має звертатись до primary для запису, до replica для читання.

Service Discovery

Застосунок сам хоче керувати балансуванням навантаження або вибором Pod. Headless Service надає список всіх IP-адрес Pod, а застосунок сам вирішує, до якого звертатись.

Приклад: Elasticsearch cluster — клієнт отримує список всіх вузлів та сам розподіляє запити.

Peer Discovery

Застосунки, які потребують знати про всіх інших членів кластера (наприклад, для формування quorum або gossip protocol).

Приклад: Kafka, Cassandra, etcd — кожен вузол має знати про інших.

Створення Headless Service

apiVersion: v1
kind: Service
metadata:
  name: postgres-headless
spec:
  clusterIP: None  # ← Це робить Service headless
  selector:
    app: postgres
  ports:
    - port: 5432
      targetPort: 5432

Що відбувається:

  1. Kubernetes НЕ виділяє ClusterIP
  2. CoreDNS створює A-записи для кожного Pod
  3. DNS-запит повертає список IP-адрес всіх Pod

DNS-записи Headless Service

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

component "CoreDNS" as dns #e3f2fd

component "Headless Service\npostgres-headless" as svc #fff3e0 {
    [clusterIP: None]
}

package "StatefulSet Pods" {
    component "postgres-0\n10.244.1.10" as p0 #e8f5e9
    component "postgres-1\n10.244.1.11" as p1 #e8f5e9
    component "postgres-2\n10.244.1.12" as p2 #e8f5e9
}

dns --> svc : Create DNS records

note right of dns
    DNS-записи:
    
    postgres-headless.default.svc.cluster.local
    → 10.244.1.10, 10.244.1.11, 10.244.1.12
    
    postgres-0.postgres-headless.default.svc.cluster.local
    → 10.244.1.10
    
    postgres-1.postgres-headless.default.svc.cluster.local
    → 10.244.1.11
    
    postgres-2.postgres-headless.default.svc.cluster.local
    → 10.244.1.12
end note

@enduml

DNS-записи:

  1. Service DNS — повертає всі Pod:
    postgres-headless.default.svc.cluster.local
    → 10.244.1.10, 10.244.1.11, 10.244.1.12
    
  2. Pod DNS (лише для StatefulSet) — повертає конкретний Pod:
    postgres-0.postgres-headless.default.svc.cluster.local → 10.244.1.10
    postgres-1.postgres-headless.default.svc.cluster.local → 10.244.1.11
    postgres-2.postgres-headless.default.svc.cluster.local → 10.244.1.12
    

Тестування Headless Service

DNS resolution для Headless Service
$ kubectl run test-dns --image=curlimages/curl:latest --rm -it --restart=Never -- sh
# DNS-запит до Headless Service (повертає всі Pod)
$ nslookup postgres-headless.default.svc.cluster.local
Server: 10.96.0.10
Address: 10.96.0.10:53
Name: postgres-headless.default.svc.cluster.local
Address: 10.244.1.10
Address: 10.244.1.11
Address: 10.244.1.12
# DNS-запит до конкретного Pod (StatefulSet)
$ nslookup postgres-0.postgres-headless.default.svc.cluster.local
Server: 10.96.0.10
Address: 10.96.0.10:53
Name: postgres-0.postgres-headless.default.svc.cluster.local
Address: 10.244.1.10

Використання у .NET

Приклад: PostgreSQL primary/replica:

var builder = WebApplication.CreateBuilder(args);

// Primary для запису
var primaryHost = "postgres-0.postgres-headless.default.svc.cluster.local";
var primaryConnectionString = $"Host={primaryHost};Database=mydb;Username=user;Password=pass";

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(primaryConnectionString));

// Replica для читання
var replicaHost = "postgres-1.postgres-headless.default.svc.cluster.local";
var replicaConnectionString = $"Host={replicaHost};Database=mydb;Username=user;Password=pass";

builder.Services.AddDbContext<ReadOnlyDbContext>(options =>
    options.UseNpgsql(replicaConnectionString));

var app = builder.Build();
app.Run();

Приклад: Service Discovery (отримання всіх Pod):

using System.Net;

var serviceName = "postgres-headless.default.svc.cluster.local";
var addresses = await Dns.GetHostAddressesAsync(serviceName);

foreach (var address in addresses)
{
    Console.WriteLine($"Pod IP: {address}");
}

// Output:
// Pod IP: 10.244.1.10
// Pod IP: 10.244.1.11
// Pod IP: 10.244.1.12

Endpoints та EndpointSlices

Endpoints — це об'єкт Kubernetes, який містить список IP:Port всіх Pod, які відповідають селектору Service.

Що таке Endpoints

Коли ви створюєте Service, Kubernetes автоматично створює об'єкт Endpoints з тим самим іменем:

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

component "Service\napi-service" as svc #e3f2fd {
    [selector: app=api]
}

component "Endpoints\napi-service" as ep #fff3e0 {
    [10.244.1.10:8080]
    [10.244.1.11:8080]
    [10.244.1.12:8080]
}

package "Pods" {
    component "Pod 1\napp=api\n10.244.1.10" as p1 #e8f5e9
    component "Pod 2\napp=api\n10.244.1.11" as p2 #e8f5e9
    component "Pod 3\napp=api\n10.244.1.12" as p3 #e8f5e9
}

component "Endpoints Controller" as ctrl

ctrl --> svc : Watch
ctrl --> p1 : Watch
ctrl --> p2 : Watch
ctrl --> p3 : Watch
ctrl --> ep : Update

note right of ctrl
    Endpoints Controller
    стежить за Pod та Service
    
    Автоматично оновлює
    Endpoints при зміні Pod
end note

note right of ep
    Endpoints містить
    список IP:Port
    всіх готових Pod
end note

@enduml

Перегляд Endpoints

kubectl get endpoints
$ kubectl get endpoints api-service
NAME ENDPOINTS AGE
api-service 10.244.1.10:8080,10.244.1.11:8080,10.244.1.12:8080 5m

Детальна інформація:

kubectl describe endpoints
$ kubectl describe endpoints api-service
Name: api-service
Namespace: default
Labels:
Annotations: endpoints.kubernetes.io/last-change-trigger-time: 2026-05-10T17:30:00Z
Subsets:
Addresses: 10.244.1.10,10.244.1.11,10.244.1.12
NotReadyAddresses:
Ports:
Name Port Protocol
---- ---- --------
http 8080 TCP

Важливі поля:

  • Addresses — список IP-адрес готових Pod (пройшли readiness probe)
  • NotReadyAddresses — список IP-адрес Pod, які ще не готові
  • Ports — порти, на яких слухають Pod

Як Endpoints оновлюються

Endpoints Controller постійно стежить за Pod та автоматично оновлює Endpoints:

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

participant "Endpoints\nController" as ctrl
participant "API Server" as api
participant "Endpoints\napi-service" as ep

== Pod стає Ready ==

api -> ctrl : Event: Pod api-xxx Ready=True
activate ctrl
ctrl -> ctrl : Pod має мітку app=api\nта пройшов readiness probe
ctrl -> api : PATCH Endpoints/api-service\nAdd: 10.244.1.10:8080
deactivate ctrl

api -> ep : Update
ep -> ep : Addresses: [10.244.1.10:8080]

== Pod стає NotReady ==

api -> ctrl : Event: Pod api-xxx Ready=False
activate ctrl
ctrl -> ctrl : Pod не пройшов readiness probe
ctrl -> api : PATCH Endpoints/api-service\nMove to NotReadyAddresses
deactivate ctrl

api -> ep : Update
ep -> ep : Addresses: []\nNotReadyAddresses: [10.244.1.10:8080]

== Pod видалено ==

api -> ctrl : Event: Pod api-xxx Deleted
activate ctrl
ctrl -> ctrl : Видалити Pod з Endpoints
ctrl -> api : PATCH Endpoints/api-service\nRemove: 10.244.1.10:8080
deactivate ctrl

api -> ep : Update
ep -> ep : Addresses: []\nNotReadyAddresses: []

note right of ctrl
    Endpoints Controller
    реагує на зміни Pod
    у реальному часі
    
    Це гарантує, що Service
    надсилає трафік лише
    на готові Pod
end note

@enduml

Важливо: Лише Pod, які пройшли readiness probe, додаються до Addresses. Pod, які не готові, потрапляють до NotReadyAddresses та не отримують трафік.

EndpointSlices — масштабована альтернатива

Проблема Endpoints: Якщо у Service 1000 Pod, об'єкт Endpoints містить 1000 IP-адрес. При кожній зміні (додавання/видалення Pod) весь об'єкт оновлюється, що створює навантаження на API Server та etcd.

Рішення: EndpointSlices (Kubernetes 1.21+) — розбиває Endpoints на менші частини (slices).

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

component "Service\napi-service" as svc #e3f2fd

package "EndpointSlices" {
    component "EndpointSlice 1" as es1 #fff3e0 {
        [10.244.1.10:8080]
        [10.244.1.11:8080]
        [...]
        [10.244.1.99:8080]
    }
    
    component "EndpointSlice 2" as es2 #fff3e0 {
        [10.244.1.100:8080]
        [10.244.1.101:8080]
        [...]
        [10.244.1.199:8080]
    }
    
    component "EndpointSlice N" as esn #fff3e0 {
        [10.244.1.900:8080]
        [10.244.1.901:8080]
        [...]
        [10.244.1.999:8080]
    }
}

svc --> es1
svc --> es2
svc --> esn

note right of es1
    Кожен EndpointSlice
    містить до 100 endpoints
    (за замовчуванням)
    
    При зміні Pod оновлюється
    лише один slice, а не всі
end note

@enduml

Переваги EndpointSlices:

  • Масштабованість — менше навантаження на API Server при великій кількості Pod
  • Ефективність — оновлюється лише один slice, а не весь список
  • Додаткові метадані — topology hints, zone information

Перегляд EndpointSlices:

kubectl get endpointslices
$ kubectl get endpointslices -l kubernetes.io/service-name=api-service
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
api-service-abc123 IPv4 8080 10.244.1.10,10.244.1.11,10.244.1.12 5m
Endpoints vs EndpointSlices:
  • Endpoints — legacy, один об'єкт для всіх Pod
  • EndpointSlices — сучасний підхід, розбиття на частини
Kubernetes автоматично створює обидва для зворотної сумісності. kube-proxy підтримує обидва формати.Рекомендація: Використовуйте EndpointSlices для нових кластерів (Kubernetes 1.21+). Endpoints залишається для сумісності зі старими версіями.

Service без selector — ручне управління Endpoints

Іноді потрібен Service, який не автоматично вибирає Pod за селектором. Наприклад:

  • Доступ до зовнішньої бази даних (не у Kubernetes)
  • Міграція з зовнішнього сервісу у Kubernetes
  • Проксування до legacy системи

Рішення: Створити Service без selector та вручну створити Endpoints.

Приклад: Зовнішня база даних

Service без selector:

apiVersion: v1
kind: Service
metadata:
  name: external-postgres
spec:
  # Немає selector!
  ports:
    - port: 5432
      targetPort: 5432

Endpoints (створюємо вручну):

apiVersion: v1
kind: Endpoints
metadata:
  name: external-postgres  # Має збігатись з іменем Service
subsets:
  - addresses:
      - ip: 192.168.1.100  # IP зовнішньої БД
    ports:
      - port: 5432

Що відбувається:

  1. Service створюється з ClusterIP (наприклад, 10.96.0.50)
  2. DNS-запис створюється: external-postgres.default.svc.cluster.local10.96.0.50
  3. Трафік на 10.96.0.50:5432 перенаправляється на 192.168.1.100:5432

Візуалізація:

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

package "Kubernetes Cluster" {
    component "Pod" as pod #e3f2fd
    
    component "Service\nexternal-postgres" as svc #fff3e0 {
        [ClusterIP: 10.96.0.50]
        [NO selector]
    }
    
    component "Endpoints\nexternal-postgres" as ep #e8f5e9 {
        [192.168.1.100:5432]
    }
}

cloud "Зовнішня мережа" {
    component "PostgreSQL\n192.168.1.100" as db
}

pod --> svc : psql -h external-postgres
svc --> ep : Lookup endpoints
ep --> db : Connect to 192.168.1.100:5432

note right of svc
    Service без selector
    не шукає Pod автоматично
    
    Використовує вручну
    створені Endpoints
end note

@enduml

Коли використовувати:

  • Зовнішня база даних (AWS RDS, Azure Database)
  • Legacy системи поза Kubernetes
  • Поступова міграція у Kubernetes

Переваги:

  • Код не змінюється — використовує той самий DNS-ім'я
  • Легко мігрувати: спочатку зовнішня БД, потім Pod у Kubernetes
  • Централізована конфігурація через Service

Практичний приклад: TodoApi з різними типами Service

Тепер створимо повний приклад з TodoApi та різними типами Service.

Архітектура застосунку

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

actor "Зовнішній користувач" as user

cloud "LoadBalancer" {
    component "Load Balancer\n34.123.45.67" as lb #e3f2fd
}

package "Kubernetes Cluster" {
    component "Service\nfrontend-lb\nType: LoadBalancer" as fe_svc #fff3e0
    
    package "Frontend Pods" {
        component "Frontend Pod 1" as fe1 #e8f5e9
        component "Frontend Pod 2" as fe2 #e8f5e9
    }
    
    component "Service\napi-service\nType: ClusterIP" as api_svc #fff3e0
    
    package "API Pods" {
        component "API Pod 1" as api1 #e8f5e9
        component "API Pod 2" as api2 #e8f5e9
        component "API Pod 3" as api3 #e8f5e9
    }
    
    component "Service\npostgres-service\nType: ClusterIP" as db_svc #fff3e0
    
    component "PostgreSQL Pod" as db #e8f5e9
}

user --> lb : HTTP
lb --> fe_svc
fe_svc --> fe1
fe_svc --> fe2

fe1 --> api_svc : http://api-service
fe2 --> api_svc : http://api-service

api_svc --> api1
api_svc --> api2
api_svc --> api3

api1 --> db_svc : postgres-service:5432
api2 --> db_svc : postgres-service:5432
api3 --> db_svc : postgres-service:5432

db_svc --> db

@enduml

Крок 1: PostgreSQL з ClusterIP Service

postgres-deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:16
          ports:
            - containerPort: 5432
          env:
            - name: POSTGRES_DB
              value: tododb
            - name: POSTGRES_USER
              value: todouser
            - name: POSTGRES_PASSWORD
              value: todopass
          volumeMounts:
            - name: postgres-storage
              mountPath: /var/lib/postgresql/data
      volumes:
        - name: postgres-storage
          emptyDir: {}

postgres-service.yaml:

apiVersion: v1
kind: Service
metadata:
  name: postgres-service
spec:
  type: ClusterIP  # За замовчуванням, можна не вказувати
  selector:
    app: postgres
  ports:
    - port: 5432
      targetPort: 5432

Крок 2: TodoApi з ClusterIP Service

api-deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: todoapi
spec:
  replicas: 3
  selector:
    matchLabels:
      app: todoapi
  template:
    metadata:
      labels:
        app: todoapi
    spec:
      containers:
        - name: todoapi
          image: todoapi:1.0.0
          ports:
            - containerPort: 8080
          env:
            - name: ASPNETCORE_ENVIRONMENT
              value: "Production"
            - name: ASPNETCORE_URLS
              value: "http://+:8080"
            - name: ConnectionStrings__DefaultConnection
              value: "Host=postgres-service;Database=tododb;Username=todouser;Password=todopass"
          resources:
            requests:
              memory: "128Mi"
              cpu: "100m"
            limits:
              memory: "256Mi"
              cpu: "500m"

api-service.yaml:

apiVersion: v1
kind: Service
metadata:
  name: api-service
spec:
  type: ClusterIP
  selector:
    app: todoapi
  ports:
    - name: http
      port: 80
      targetPort: 8080

Крок 3: Frontend з LoadBalancer Service (або NodePort для Minikube)

frontend-deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
spec:
  replicas: 2
  selector:
    matchLabels:
      app: frontend
  template:
    metadata:
      labels:
        app: frontend
    spec:
      containers:
        - name: nginx
          image: nginx:1.27
          ports:
            - containerPort: 80
          volumeMounts:
            - name: nginx-config
              mountPath: /etc/nginx/conf.d
      volumes:
        - name: nginx-config
          configMap:
            name: nginx-config

nginx-configmap.yaml:

apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-config
data:
  default.conf: |
    upstream api {
        server api-service:80;
    }
    
    server {
        listen 80;
        
        location /api/ {
            proxy_pass http://api/;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }
        
        location / {
            root /usr/share/nginx/html;
            index index.html;
            try_files $uri $uri/ /index.html;
        }
    }

frontend-service.yaml (для production з LoadBalancer):

apiVersion: v1
kind: Service
metadata:
  name: frontend-service
spec:
  type: LoadBalancer
  selector:
    app: frontend
  ports:
    - port: 80
      targetPort: 80

frontend-service-nodeport.yaml (для Minikube):

apiVersion: v1
kind: Service
metadata:
  name: frontend-service
spec:
  type: NodePort
  selector:
    app: frontend
  ports:
    - port: 80
      targetPort: 80
      nodePort: 30080  # Доступ через http://<minikube-ip>:30080

Розгортання

Розгортання застосунку
# PostgreSQL
$ kubectl apply -f postgres-deployment.yaml
$ kubectl apply -f postgres-service.yaml
# TodoApi
$ kubectl apply -f api-deployment.yaml
$ kubectl apply -f api-service.yaml
# Frontend
$ kubectl apply -f nginx-configmap.yaml
$ kubectl apply -f frontend-deployment.yaml
$ kubectl apply -f frontend-service-nodeport.yaml
# Перевірка
$ kubectl get all
NAME READY STATUS RESTARTS AGE
pod/postgres-xxx 1/1 Running 0 2m
pod/todoapi-xxx 1/1 Running 0 1m
pod/todoapi-yyy 1/1 Running 0 1m
pod/todoapi-zzz 1/1 Running 0 1m
pod/frontend-xxx 1/1 Running 0 30s
pod/frontend-yyy 1/1 Running 0 30s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
service/postgres-service ClusterIP 10.96.0.10 <none> 5432/TCP
service/api-service ClusterIP 10.96.0.20 <none> 80/TCP
service/frontend-service NodePort 10.96.0.30 <none> 80:30080/TCP

Тестування

Тестування застосунку
# Отримання IP Minikube
$ minikube ip
192.168.49.2
# Доступ до frontend
$ curl http://192.168.49.2:30080
<html>...</html>
# Доступ до API через frontend
$ curl http://192.168.49.2:30080/api/todos
{"todos":[],"count":0}

Практичні завдання

Тепер виконайте завдання для закріплення знань про Service та мережі у Kubernetes.

Завдання 1: Експерименти з типами Service

Мета: Зрозуміти різницю між ClusterIP, NodePort та LoadBalancer.

Завдання:

  1. Створіть Deployment з nginx (3 репліки)
  2. Створіть три Service для того самого Deployment:
    • ClusterIP Service
    • NodePort Service
    • LoadBalancer Service (або емулюйте через minikube tunnel)
  3. Протестуйте доступ до кожного Service:
    • ClusterIP — з іншого Pod
    • NodePort — з локальної машини
    • LoadBalancer — через EXTERNAL-IP
  4. Порівняйте результати

Очікуваний результат: Ви зрозумієте, коли використовувати кожен тип Service.


Завдання 2: Service Discovery через DNS

Мета: Навчитись використовувати DNS для service discovery.

Завдання:

  1. Створіть два Deployment у різних namespace:
    • api у namespace backend
    • frontend у namespace frontend
  2. Створіть ClusterIP Service для кожного
  3. З Pod frontend зробіть DNS-запит до API Service:
    • Коротка форма (не працює — різні namespace)
    • З namespace (api.backend)
    • Повна форма (api.backend.svc.cluster.local)
  4. Перевірте HTTP-запит через DNS-ім'я

Очікуваний результат: Ви зрозумієте, як працює DNS resolution між namespace.


Завдання 3: Headless Service для StatefulSet

Мета: Навчитись використовувати Headless Service для доступу до конкретних Pod.

Завдання:

  1. Створіть StatefulSet з 3 репліками nginx
  2. Створіть Headless Service (clusterIP: None)
  3. Перевірте DNS-записи:
    • Service DNS (повертає всі Pod)
    • Pod DNS (повертає конкретний Pod)
  4. Зробіть HTTP-запит до конкретного Pod через DNS

Очікуваний результат: Ви зрозумієте, як Headless Service надає стабільні DNS-імена для Pod.


Завдання 4: Service без selector для зовнішньої БД

Мета: Навчитись створювати Service для зовнішніх ресурсів.

Завдання:

  1. Створіть Service без selector
  2. Вручну створіть Endpoints з IP-адресою зовнішнього сервісу (наприклад, 8.8.8.8 — Google DNS для тесту)
  3. Перевірте DNS resolution
  4. Зробіть запит до Service (має перенаправити на зовнішній сервіс)

Очікуваний результат: Ви навчитесь інтегрувати зовнішні сервіси у Kubernetes через Service.


Резюме

У цій статті ми детально вивчили Service — мережеву абстракцію для Pod у Kubernetes. Ось що ми розглянули:

Проблема ефемерних IP-адрес

Чому прямий доступ до Pod за IP неможливий у production: IP змінюються при перезапуску, немає балансування навантаження, відсутній service discovery.

Що таке Service

Абстракція, яка надає стабільну мережеву точку доступу (IP та DNS) для групи ефемерних Pod. Service — це проксі перед Pod.

Типи Service

ClusterIP (внутрішній), NodePort (доступ через вузли), LoadBalancer (зовнішній load balancer), ExternalName (DNS alias). Кожен для різних сценаріїв.

kube-proxy та iptables

Як Kubernetes перенаправляє трафік від Service до Pod через iptables/IPVS. DNAT, SNAT, балансування навантаження на рівні ядра.

CoreDNS та Service Discovery

Автоматичне створення DNS-записів для Service. Формат DNS-імен, скорочені форми, міжнамespace комунікація.

Headless Service

Service без ClusterIP для прямого доступу до Pod. DNS повертає IP всіх Pod. Використовується для StatefulSet та peer discovery.

Endpoints та EndpointSlices

Список IP:Port всіх Pod, які відповідають селектору. Автоматичне оновлення при зміні Pod. EndpointSlices для масштабованості.

Service без selector

Ручне управління Endpoints для інтеграції зовнішніх сервісів. Міграція з зовнішніх систем у Kubernetes.

Практичний приклад

Повний застосунок з Frontend, API та PostgreSQL. Різні типи Service для різних компонентів. Nginx як reverse proxy.

Практичні завдання

4 завдання для закріплення знань: типи Service, DNS resolution, Headless Service, Service без selector.

Ключові висновки

  1. Service — це не Pod — це мережевий об'єкт, який діє як стабільний проксі перед групою Pod.
  2. ClusterIP для більшості випадків — внутрішня комунікація між сервісами. NodePort та LoadBalancer лише для зовнішнього доступу.
  3. DNS — основа service discovery — використовуйте DNS-імена замість IP-адрес. Короткі форми для того самого namespace, повні для міжнамespace.
  4. kube-proxy не проксує трафік — він налаштовує iptables/IPVS. Трафік йде напряму від клієнта до Pod через kernel.
  5. Readiness probe критично важливий — лише готові Pod додаються до Endpoints. Без readiness probe Service надсилає трафік на неготові Pod.
  6. Headless Service для stateful застосунків — коли потрібен доступ до конкретного Pod за стабільним DNS-іменем.
  7. Service без selector для зовнішніх ресурсів — інтеграція зовнішніх баз даних, API, legacy систем у Kubernetes.

Що далі?

Ви вивчили основи Service та мережі у Kubernetes. Наступні теми для поглибленого вивчення:

  • Ingress — HTTP-маршрутизація та TLS termination для багатьох Service
  • NetworkPolicy — ізоляція трафіку між Pod та namespace
  • Service Mesh — розширені можливості мережі (mTLS, traffic management, observability)
  • ConfigMap та Secret — управління конфігурацією та секретами
  • Volumes та PersistentVolume — зберігання даних

Корисні команди

Для швидкого доступу — всі команди для роботи з Service:

# Створення Service з YAML
kubectl apply -f service.yaml

# Створення Service з kubectl expose
kubectl expose deployment <name> --port=80 --target-port=8080

# Перегляд Service
kubectl get services
kubectl get svc  # скорочена форма

# Детальна інформація
kubectl describe service <name>

# Перегляд у форматі YAML
kubectl get service <name> -o yaml

Додаткові ресурси

Офіційна документація: Service

Повна документація про Service з усіма полями та прикладами.

DNS for Services and Pods

Детальний опис DNS resolution у Kubernetes.

Connecting Applications with Services

Офіційний туторіал з підключення застосунків через Service.

Service API Reference

Детальна специфікація API для Service v1.

Попередня стаття: Rolling Updates та управління життєвим циклом Deployment

Наступна стаття: ConfigMap та Secret — управління конфігурацією