Skip to content

Робота з сервісами

Кожен под у Kubernetes має свою унікальну IP-адресу в межах кластера, яка дозволяє йому взаємодіяти з іншими подами. Але такий підхід не є дуже зручним, бо тоді потрібно знати точні IP-адреси всіх подів. Тут у нагоді нам стануть сервіси, які розв'язують задачу взаємодії подів, а також зовнішнього світу з подами.

DNS-ім'я сервісу та поду

Щоб використовувати сервіси, у його маніфесті потрібно вказати, на які поди він має балансувати трафік. Далі вся комунікація з цими подами буде відбуватись не по IP-адресі подів, а по DNS-імені сервісу. Кожен сервіс у Kubernetes має унікальне DNS-ім'я, що дозволяє подам легко взаємодіяти один з одним. Ці імена створюються в Kubernetes автоматично при створенні сервісу. Доменним іменем верхнього рівня за замовчуванням є cluster.local.

DNS-ім'я сервісу має формат ім'я сервісу, простір імен (namespace), тип ресурсу (у цьому випадку це сервіс), і далі доменне ім'я Kubernetes :

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

А DNS-ім'я пода має формат IP поду, простір імен (namespace), тип ресурсу (тепер це под), і далі доменне ім'я Kubernetes :

<pod-ip>.<namespace>.pod.cluster.local.

💡 Знати DNS-ім'я пода не дуже корисно, оскільки поди можуть бути перезапущені або переміщені між вузлами. З цієї причини більшість взаємодій відбувається через сервіси, які надають стабільний ендпоінт для доступу до групи подів.

Типи сервісів

Існують такі типи сервісів ⬇️

ClusterIP

Цей тип створює віртуальну внутрішню IP-адресу всередині кластера, яку можна використовувати для з'єднання з подами. Ця адреса доступна лише зсередини. ClusterIP може бути корисним для забезпечення комунікації між різними сервісами або компонентами додатка, які працюють у межах одного кластера.

Завдяки ClusterIP поди мають стабільний та надійний спосіб для звернення до інших сервісів без необхідності відстежувати зміни IP-адрес. Це зменшує складність мережевої конфігурації та збільшує надійність взаємодії сервісів. Цей тип також забезпечує ізоляцію мережевого трафіку всередині кластера, що є важливим з погляду безпеки. Таким чином внутрішні сервіси залишаються захищеними від небажаного доступу ззовні, що допомагає управляти доступом до чутливих компонентів.

Розглянемо приклад. У тебе є вебдодаток, розгорнутий у Kubernetes, який включає фронтенд (наприклад, React) та бекенд (наприклад, RESTful API на Node.js). Бекенд потребує бази даних, яка також розгорнута в Kubernetes окремими подами. Проблема: потрібно, щоб фронтенд міг взаємодіяти з бекендом, але ти не хочеш, щоб база даних була доступна ззовні кластера. Рішенням буде використати ClusterIP-сервіси для бекенду та бази даних. Оскільки ClusterIP не доступний ззовні кластера, це забезпечить безпечну внутрішню взаємодію компонентів.

NodePort

Цей сервіс розширює можливості ClusterIP, дозволяючи доступ до сервісів ззовні кластера. При створенні NodePort, Kubernetes автоматично виділяє порт у певному діапазоні портів (30000 — 32767 ) на кожному вузлі (node) кластера. Після цього, викликаючи ІР ноди та вказуючи вибраний порт, ми можемо отримати доступ до сервісу та його подів. Тобто коли зовнішній запит надходить на виділений NodePort на будь-якому з вузлів кластера, Kubernetes маршрутизує цей запит до відповідного пода. А оскільки NodePort містить функціонал ClusterIP, то сервіс також доступний і всередині кластера.

NodePort часто використовується для надання доступу до вебдодатків, розгорнутих у кластері з зовнішнього інтернету. Та найчастіше NodePort корисний під час тестування та розробки, коли потрібен швидкий та легкий доступ до додатків із зовнішніх мереж.

У NodePort є і недоліки. Порти NodePort обмежені діапазоном 30000 — 32767 , що може бути обмеженням у деяких випадках. Також використання NodePort не завжди є безпечним, оскільки він відкриває доступ до додатків на всіх вузлах кластера.

Load Balancer

Цей тип сервісу розширює функціонал NodePort та ClusterIP та дозволяє більш ефективне розподілення вхідного трафіку між подами. Load Balancer забезпечує інтеграцію з балансувальниками навантаження, які надаються хмарними провайдерами, такими як AWS, Google Cloud, Azure тощо. У результаті виходить більш ефективне управління навантаженням та краща доступність і надійність сервісу. Сервіс LoadBalancer забезпечує доступність і через зовнішній балансувальник навантаження, і через внутрішні механізми маршрутизації Kubernetes.

Розглянемо приклад. Коли ти створюєш сервіс типу LoadBalancer у Kubernetes, кластер автоматично взаємодіє з хмарним провайдером для створення зовнішнього балансувальника навантаження. Цей балансувальник отримує власну публічну IP-адресу, яку можна використовувати для доступу до сервісів ззовні кластера. LoadBalancer автоматично розподіляє вхідний трафік між подами, які належать до сервісу. Це забезпечує більш ефективне управління навантаженням і покращує загальну доступність і надійність сервісу.

ExternalName

На відміну від інших сервісів, ExternalName не налаштовує IP-адреси та не балансує навантаження. Замість цього він створює DNS-ім’я для зовнішнього сервісу. ExternalName не має IP-адреси, асоційованої з сервісом у Kubernetes, а просто використовує DNS-ім'я.

Якщо кластер потребує взаємодії із зовнішнім API, ти можеш використовувати ExternalName, щоб створити легкодоступний DNS-псевдонім для цього API всередині кластера. Але варто подумати про безпеку, оскільки зловмисники можуть використовувати DNS-маніпуляції для перенаправлення трафіку.

Далі ми створимо ClusterIP, NodePort та ExternalName, а до LoadBalancer ми повернемось далі у курсі.

Створення сервісу ClusterIP

У попередньому топіку ми написали маніфест для нашого додатка на Python, а тепер додамо йому ClusterIP. Цей сервіс створить статичну IP-адресу всередині кластера, яка буде доступна лише всередині самого кластера. Також сервіс отримає DNS-імʼя всередині кластера. ClusterIP нам потрібний, щоб балансувати навантаження на декілька подів і щоб мати стабільну комунікацію з подами.

Перед тим як ми напишемо маніфест сервісу, створимо ще один под додатка. Для цього відкриємо маніфест пода та змінимо назву пода з kube2py на kube2py-1. Потім виконаємо команду для створення пода:

kubectl apply -f app-pod.yml -n mateapp  

Перевіримо статуси подів:

1
2
3
4
5
kubectl get pods -n mateapp 

NAME        READY   STATUS    RESTARTS        AGE
busybox     1/1     Running   237 (13m ago)   10d
kube2py     1/1     Running   1 (13m ago)     2d4h

Тепер вкажемо неймспейс, який буде використовуватися за замовчуванням:

kubectl config set-context --current --namespace=mateapp

Ми модифікували контекст — конфігурацію, яка описує як конектитись до конкретного кластера. Контекстів може бути багато і між ними можна перемикатись. Та поки ми просто змінили поточний контекст і вказали там неймспейс за замовчуванням.

Тепер перевіримо поди:

kubectl get pods

Та логи:

kubectl logs kube2py
kubectl logs kube2py-1

Далі створимо сервіс, який дозволить нам балансувати трафік між цими двома подами та через який ми зможемо звертатись до додатка. У Visual Studio Code створи новий файл clusterIp.yml та вкажи версію API і тип об'єкта:

apiVersion: v1
kind: Service

Далі вкажемо назву і неймспейс. За цим імʼям ми зможемо звернутися до сервісу всередині кластера:

metadata:
 name: kube2py-service
 namespace: mateapp

Далі йде специфікація — селектор і мепінг портів. Завдяки селектору сервіс визначає, якими подами він має опікуватись. У нашому випадку ми вказуємо поди з лейблом app і значенням kube2py:

spec:
 selector:
   app: kube2py

Далі додамо мепінг портів: протокол — TCP; порт, на якому буде слухат сам сервіс — 80, порт на подах, на який треба цей трафік переспрямомувати — 8080:

 ports:
   - protocol: TCP
     port: 80
     targetPort: 8080

Наостанок вказуємо тип сервісу:

type: ClusterIP

Тепер передамо цей маніфест на Kube-API Server для створення:

kubectl apply -f clusterIp.yml
service/kube2py-service created

Подивимось на цей сервіс у кластері:

kubectl get services

NAME              TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
kube2py-service   ClusterIP   10.102.81.235   <none>        80/TCP    37s

Або:

kubectl get svc   

NAME              TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
kube2py-service   ClusterIP   10.102.81.235   <none>        80/TCP    32s

Якщо виконати команду kubectl get services -o wide, то ми зможемо побачити і селектор:

NAME              TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE    SELECTOR
kube2py-service   ClusterIP   10.102.81.235   <none>        80/TCP    108s   app=kube2py

Тепер нам треба протестувати чи все працює, і зробимо ми це двома способами. Почнемо вже зі знайомого нам порт форварду, тільки цього разу змінимо тип:

kubectl port-forward service/kube2py-service 8081:80

Відкриємо браузер:

Другий спосіб: підʼєднаємось до пода з busybox-контейнером і виконаємо інтерактивну shell-команду:

kubectl -n mateapp exec -it busybox -- sh

Тепер виконаємо HTTP GET виклик за допомогою інструмента CURL:

curl http://kube2py-service.mateapp.svc.cluster.local

    Docker is Awesome!
<pre>                   ##        .</pre>
<pre>             ## ## ##       ==</pre>
<pre>          ## ## ## ##      ===</pre>
<pre>      /""""""""""""""""\___/ ===</pre>
<pre> ~~~ (~~ ~~~~ ~~~ ~~~~ ~~ ~ /  ===-- ~~~</pre>
<pre>      \______ o          __/</pre>
<pre>        \    \        __/</pre>
<pre>         \____\______/</pre>

Спробуємо знайти ці виклики у логах подів:

kubectl logs kube2py

kubectl logs kube2py-1

Можна побачити, що трафік дійсно балансується.

Створення сервісу NodePort

Тепер створимо і протестуємо сервіс типу NodePort, який зробить наші додатки доступними ззовні кластера Kubernetes, використовуючи IP-адресу кожного вузла на певному порті. Створимо новий файл маніфесту в Visual Studio Code, назвемо його nodeport.yml. У цьому файлі ми опишемо наш сервіс:

Kubernetes service NodePort
apiVersion: v1
kind: Service
metadata:
 name: kube2py-nodeport-service
 namespace: mateapp
spec:
 type: NodePort
 selector:
   app: kube2py
 ports:
   - protocol: TCP
     port: 80  #(1)!
     targetPort: 8080 #(2)!
     nodePort: 30007 #(3)!
  1. Оскільки NodePort створює і ClusterIP то цей параметр потрібен для внутрішньої взаємодії
  2. Це порт на який перенаправляється в в контейнері додатку
  3. А це порт для зовнішнього доступу

Ми вказали тип сервісу, вибрали селектор для визначення подів, які будуть обслуговуватися цим сервісом, і визначили мепінг портів. nodePort — це порт, доступний на кожному вузлі кластера, через який можна звертатися до нашого додатка. Застосуємо наш маніфест до кластера за допомогою команди:

kubectl apply -f nodeport.yml

Перевіримо створений сервіс:

kubectl get svc 

Тепер наш додаток доступний ззовні кластера на порту, який ми вказали у nodePort. Це означає, що до додатка можна звернутись, використавши IP-адресу будь-якого вузла кластера.

Для тестування відкриємо браузер і введемо адресу у вигляді http://<NodeIP>:<NodePort>, замінивши <NodeIP> на реальну IP-адресу одного з вузлів кластера і <NodePort> на реальний порт. У нашому випадку локальний кластер не має публічно доступних IP-адрес, тому потрібно використовувати http://localhost:<NodePort>.

Цей метод легко надає доступ до додатків для зовнішніх користувачів, хоча для production-середовища рекомендується використовувати складніші методи, такі як Ingress (розглянемо далі у курсі).

Створення сервісу ExternalName

Сервіс ExternalName у Kubernetes дозволяє мапити сервіс у кластері на DNS-ім'я зовнішнього сервісу. Це зручно, коли потрібно надати подам у кластері доступ до зовнішнього сервісу та локальне ім'я для сервісу. Використання ExternalName спрощує конфігурацію, оскільки не потрібно вказувати зовнішні адреси у додатку.

Практичну реалізацію ми почнемо з модифікації додатка Python. Додамо нову залежність:

import requests

Далі допишемо нову функцію (external-call-handler) яка буде обробляти зовнішній запит (/external-call). Ця функція зчитує значення зовнішнього ендпоінта зі змінної середовища. Якщо значення у змінній середовища є, то робимо HTTP GET запит на цей URL, а якщо немає — повертаємо статус 500:

app.py
@app.route('/external-call')
def external_call():
    external_url = os.getenv('EXTERNAL_ENDPOINT')
    if not external_url:
        return Response("EXTERNAL_ENDPOINT environment variable is not defined.", status=500)

    try:
        response = requests.get(external_url)
        return Response(f"Response from external service: {response.text}", status=response.status_code)
    except requests.exceptions.RequestException as e:
        return Response(f"Failed to call external service: {str(e)}", status=500)

Додамо встановлення нової залежності в Dockerfile:

RUN pip install Flask requests

Тепер напишемо маніфест нового сервісу:

Creating ExternalName
1
2
3
4
5
6
7
8
apiVersion: v1
kind: Service
metadata:
   name: httpbin-api
   namespace: mateapp
spec:
   type: ExternalName
   externalName: httpbin.org

Додамо змінну оточення зі значенням API, яке треба викликати — httpbin-api. За допомогою цієї змінної ми будемо робити запити на сайт httpbin. Також вкажемо неймспейс mateapp, тип ExternalService, DNS-імʼя зовнішнього ресурсу, до якого ми будемо звертатись через цей сервіс — externalName: httpbin.org:

Creating ExternalName
apiVersion: v1
kind: Pod
metadata:
 name: kube2py
 namespace: mateapp
 labels:
   app: kube2py
spec:
 containers:
 - name: kube2py
   image: ikulyk404/kub2py:1.1.0
   ports:
   - containerPort: 8080
   env:
   - name: EXTERNAL_ENDPOINT
     value: "http://httpbi-api.mateapp.svc.cluster.local"
   livenessProbe:
     httpGet:
       path: /health
       port: 8080
     initialDelaySeconds: 60
     periodSeconds: 5
   readinessProbe:
     httpGet:
       path: /ready
       port: 8080
     initialDelaySeconds: 5
     periodSeconds: 5

Тепер до тестування! Створимо новий імедж:

docker build . -t ikulyk404/kub2py:1.2.0

Зробимо push у репозиторій:

docker push ikulyk404/kub2py:1.2.0

Поміняємо версію імеджа у маніфесті пода. Через те, що ми об'єднали опис двох подів у один маніфест (так можна робити, тільки не забудь розділити описи різних ресурсів трьома дефісами ), то міняємо версію імеджа в обох подах:

Creating ExternalName
apiVersion: v1
kind: Pod
metadata:
 name: kube2py
 namespace: mateapp
 labels:
   app: kube2py
spec:
 containers:
   - name: kube2py
     image: ikulyk404/kube2py:1.2.0
     imagePullPolicy: Always
     env:
     - name: EXTERNAL_ENDPOINT
       value: http://httpbin-api.mateapp.svc.cluster.local
     ports:
     - containerPort: 8080
     livenessProbe:
       httpGet:
         path: /health
         port: 8080
       initialDelaySeconds: 60
       periodSeconds: 5
     readinessProbe:
       httpGet:
         path: /ready
         port: 8080
       initialDelaySeconds: 5
       periodSeconds: 5

---

apiVersion: v1
kind: Pod
metadata:
 name: kube2py-1
 namespace: mateapp
 labels:
   app: kube2py
spec:
 containers:
   - name: kube2py
     image: ikulyk404/kube2py:1.2.0
     imagePullPolicy: Always
     env:
     - name: EXTERNAL_ENDPOINT
       value: http://httpbin-api.mateapp.svc.cluster.local
     ports:
     - containerPort: 8080
     livenessProbe:
       httpGet:
         path: /health
         port: 8080
       initialDelaySeconds: 60
       periodSeconds: 5
     readinessProbe:
       httpGet:
         path: /ready
         port: 8080
       initialDelaySeconds: 5
       periodSeconds: 5

Створимо новий сервіс

kubectl apply -f externalName.yml
service/weather-api created

Видалимо старі поди:

kubectl delete pod kube2py
pod "kube2py" deleted
kubectl delete pod kube2py-1
pod "kube2py-1" deleted

Виконаємо команду apply:

kubectl apply -f app-pod.yml
pod/kube2py created
pod/kube2py-1 created

Для тестування можна використати NodePort Service, який створили раніше. Тож перейдемо за адресою http://localhost:30007/:

А тепер спробуємо виклик на зовнішній сервіс http://localhost:30007/external-call:

І у нас частково завантажився сам сайт httpbin.org