1 - Підключення застосунків за допомогою Service

Модель Kubernetes для підключення контейнерів

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

Kubernetes передбачає, що кожен з Podʼів може спілкуватися з іншими, незалежно від того, на якому хості вони опиняються. Kubernetes надає кожному Podʼу власну IP-адресу, що є приватною адресою кластера, тому вам не потрібно явно створювати посилання між Podʼами або перенаправляти порти контейнера на порти хосту. Це означає, що контейнери всередині Podʼів можуть звертатися до портів один одного на localhost, і всі Podʼи в кластері можуть бачити один одного без NAT. У решті цього документа розглянуто, як ви можете запустити надійні сервіси на такій мережевій моделі.

У цьому посібнику використовується простий вебсервер nginx для демонстрації концепції.

Експонування Podʼів в кластер

Ми робили це в попередньому прикладі, але зробимо це ще раз і зосередимося на перспективі мережі. Створіть Pod nginx, і зверніть увагу, що він має специфікацію порту контейнера:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
spec:
  selector:
    matchLabels:
      run: my-nginx
  replicas: 2
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      containers:
      - name: my-nginx
        image: nginx
        ports:
        - containerPort: 80

Це робить його доступним з будь-якого вузла у вашому кластері. Перевірте вузли, на яких працює Pod:

kubectl apply -f ./run-my-nginx.yaml
kubectl get pods -l run=my-nginx -o wide
NAME                        READY     STATUS    RESTARTS   AGE       IP            NODE
my-nginx-3800858182-jr4a2   1/1       Running   0          13s       10.244.3.4    kubernetes-minion-905m
my-nginx-3800858182-kna2y   1/1       Running   0          13s       10.244.2.5    kubernetes-minion-ljyd

Перевірте IP-адреси ваших Podʼів:

kubectl get pods -l run=my-nginx -o custom-columns=POD_IP:.status.podIPs
    POD_IP
    [map[ip:10.244.3.4]]
    [map[ip:10.244.2.5]]

Ви повинні мати можливість увійти у будь-який вузол у своєму кластері за допомогою SSH і використовувати інструмент, такий як curl, щоб робити запити до обох IP-адрес. Зверніть увагу, що контейнери не використовують порт 80 на вузлі, і немає жодних спеціальних правил NAT для маршрутизації трафіку до Podʼів. Це означає, що ви можете запустити кілька Podʼів nginx на одному й тому ж вузлі, використовуючи той же containerPort, і отримувати доступ до них з будь-якого іншого Podʼа або вузла у вашому кластері, використовуючи призначену IP адресу Podʼа. Якщо ви хочете організувати перенаправлення певного порту на хості Вузла на резервні Podʼи, ви можете це зробити — але модель мережі повинна передбачати, що вам не потрібно цього робити.

Ви можете прочитати більше про Модель мережі Kubernetes якщо вас це цікавить.

Створення Service

Отже, у нас є Podʼи, що працюють з nginx у плоскому адресному просторі, доступному для всього кластера. Теоретично, ви можете звертатися до цих Podʼів безпосередньо, але що станеться, коли вузол вийде з ладу? Podʼи загинуть разом з ним, і ReplicaSet всередині Deployment створить нові, з іншими IP-адресами. Це проблема, яку вирішує Service.

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

Ви можете створити Service для ваших двох реплік nginx за допомогою kubectl expose:

kubectl expose deployment/my-nginx
service/my-nginx exposed

Це еквівалентно kubectl apply -f наступного yaml:

apiVersion: v1
kind: Service
metadata:
  name: my-nginx
  labels:
    run: my-nginx
spec:
  ports:
  - port: 80
    protocol: TCP
  selector:
    run: my-nginx

Ця специфікація створить Service, який буде спрямований на TCP порт 80 на будь-якому Podʼі з міткою run: my-nginx і відкриє його на абстрактному порті Service (targetPort: це порт, на якому контейнер приймає трафік, port — це абстрактний порт Service, який може бути будь-яким портом, що використовується іншими Podʼами для доступу до Service). Перегляньте Service API обʼєкт, щоб побачити список підтримуваних полів у визначенні Service. Перевірте ваш Service:

kubectl get svc my-nginx
NAME       TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
my-nginx   ClusterIP   10.0.162.149   <none>        80/TCP    21s

Як згадувалося раніше, Service підтримується групою Podʼів. Ці Podʼи експонуються через EndpointSlices. Селектор Service буде оцінюватися безперервно, і результати будуть надсилатися у EndpointSlice, який підключений до Service за допомогою мітки. Коли Pod припиняє існування, він автоматично видаляється з EndpointSlices, який містить його як точку доступу. Нові Podʼи, що відповідають селектору Service, автоматично додаються до EndpointSlice для цього Service. Перевірте точки доступу та зауважте, що IP-адреси збігаються з Podʼами, створеними на першому кроці:

kubectl describe svc my-nginx
Name:                my-nginx
Namespace:           default
Labels:              run=my-nginx
Annotations:         <none>
Selector:            run=my-nginx
Type:                ClusterIP
IP Family Policy:    SingleStack
IP Families:         IPv4
IP:                  10.0.162.149
IPs:                 10.0.162.149
Port:                <unset> 80/TCP
TargetPort:          80/TCP
Endpoints:           10.244.2.5:80,10.244.3.4:80
Session Affinity:    None
Events:              <none>
kubectl get endpointslices -l kubernetes.io/service-name=my-nginx
NAME             ADDRESSTYPE   PORTS   ENDPOINTS               AGE
my-nginx-7vzhx   IPv4          80      10.244.2.5,10.244.3.4   21s

Тепер ви повинні бути в змозі звертатися до nginx Service на <CLUSTER-IP>:<PORT> з будь-якого вузла у вашому кластері. Зауважте, що IP-адреса Service є повністю віртуальною, вона ніколи не передається мережею. Якщо вам цікаво, як це працює, ви можете прочитати більше про сервіс-проксі.

Доступ до Service

Kubernetes підтримує два основні режими знаходження Service — змінні середовища та DNS. Перший режим працює "з коробки", тоді як другий вимагає додаткового модуля CoreDNS cluster addon.

Змінні середовища

Коли Pod запускається на вузлі, kubelet додає набір змінних середовища для кожного активного Service. Це створює проблему з порядком. Щоб зрозуміти чому, перевірте середовище вашого працюючого Pod nginx (назва вашого Pod буде іншою):

kubectl exec my-nginx-3800858182-jr4a2 -- printenv | grep SERVICE
KUBERNETES_SERVICE_HOST=10.0.0.1
KUBERNETES_SERVICE_PORT=443
KUBERNETES_SERVICE_PORT_HTTPS=443

Зверніть увагу, що ваш Service не згадується. Це тому, що ви створили репліки перед створенням Service. Іншим недоліком цього є те, що планувальник може розмістити обидва Podʼи на одній машині, що призведе до припинення роботи всього Service, якщо він вийде з ладу. Ми можемо зробити це прям зараз, видаливши 2 Podʼи та очікуючи, поки Deployment їх відновить. Цього разу Service існує до реплік. Це забезпечить рівномірний розподіл ваших Podʼів на рівні планувальника (за умови рівної потужності всіх вузлів), а також правильні змінні середовища:

kubectl scale deployment my-nginx --replicas=0; kubectl scale deployment my-nginx --replicas=2;

kubectl get pods -l run=my-nginx -o wide
NAME                        READY     STATUS    RESTARTS   AGE     IP            NODE
my-nginx-3800858182-e9ihh   1/1       Running   0          5s      10.244.2.7    kubernetes-minion-ljyd
my-nginx-3800858182-j4rm4   1/1       Running   0          5s      10.244.3.8    kubernetes-minion-905m

Ви можете помітити, що Podʼи мають різні імена, оскільки вони були видалені та відтворені.

kubectl exec my-nginx-3800858182-e9ihh -- printenv | grep SERVICE
KUBERNETES_SERVICE_PORT=443
MY_NGINX_SERVICE_HOST=10.0.162.149
KUBERNETES_SERVICE_HOST=10.0.0.1
MY_NGINX_SERVICE_PORT=80
KUBERNETES_SERVICE_PORT_HTTPS=443

DNS

Kubernetes пропонує DNS-сервіс, який автоматично призначає DNS-імена іншим Serviceʼам. Ви можете перевірити, чи він працює у вашому кластері:

kubectl get services kube-dns --namespace=kube-system
NAME       TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)         AGE
kube-dns   ClusterIP   10.0.0.10    <none>        53/UDP,53/TCP   8m

Далі в цьому розділі ми будемо вважати, що у вас є Service із довготривалою IP-адресою (my-nginx), і DNS-сервер, який присвоїв цій IP-адресі імʼя. Ми використовуємо надбудову CoreDNS (назва застосунку kube-dns), тому ви можете звертатися до Service з будь-якого Pod у вашому кластері, використовуючи стандартні методи (наприклад, gethostbyname()). Якщо CoreDNS не працює, ви можете увімкнути його, звірившись з README CoreDNS або інформацією з Встановлення CoreDNS. Запустімо ще один застосунок curl для перевірки цього:

kubectl run curl --image=radial/busyboxplus:curl -i --tty --rm
Waiting for pod default/curl-131556218-9fnch to be running, status is Pending, pod ready: false
Hit enter for command prompt

Потім натисніть Enter і запустіть nslookup my-nginx:

[ root@curl-131556218-9fnch:/ ]$ nslookup my-nginx
Server:    10.0.0.10
Address 1: 10.0.0.10

Name:      my-nginx
Address 1: 10.0.162.149

Захист Service

До цього моменту ми отримували доступ до сервера nginx лише всередині кластеру. Перед тим як виставити Service в Інтернет, вам потрібно переконатися, що канал звʼязку захищений. Для цього вам знадобиться:

  • Самопідписні сертифікати для https (якщо у вас немає ідентифікаційного сертифіката)
  • Сервер nginx, налаштований для використання сертифікатів
  • Secret, що робить сертифікати доступними для Pod

Ви можете отримати все це з прикладу nginx https. Це вимагає встановлення інструментів go та make. Якщо ви не хочете їх встановлювати, тоді дотримуйтесь ручних кроків, описаних нижче. Коротко:

make keys KEY=/tmp/nginx.key CERT=/tmp/nginx.crt
kubectl create secret tls nginxsecret --key /tmp/nginx.key --cert /tmp/nginx.crt
secret/nginxsecret created
kubectl get secrets
NAME                  TYPE                                  DATA      AGE
nginxsecret           kubernetes.io/tls                     2         1m

А також configmap:

kubectl create configmap nginxconfigmap --from-file=default.conf

Ви можете знайти приклад default.conf у репозиторії прикладів Kubernetes.

configmap/nginxconfigmap created
kubectl get configmaps
NAME             DATA   AGE
nginxconfigmap   1      114s

Ви можете переглянути деталі ConfigMap nginxconfigmap, використовуючи наступну команду:

kubectl describe configmap nginxconfigmap

Вихід буде схожим на:

Name:         nginxconfigmap
Namespace:    default
Labels:       <none>
Annotations:  <none>

Data
====
default.conf:
----
server {
        listen 80 default_server;
        listen [::]:80 default_server ipv6only=on;

        listen 443 ssl;

        root /usr/share/nginx/html;
        index index.html;

        server_name localhost;
        ssl_certificate /etc/nginx/ssl/tls.crt;
        ssl_certificate_key /etc/nginx/ssl/tls.key;

        location / {
                try_files $uri $uri/ =404;
        }
}

BinaryData
====

Events:  <none>

Ось ручні кроки, яких слід дотримуватись у випадку, якщо у вас виникли проблеми з запуском make (наприклад, у Windows):

# Створіть пару відкритий/закритий ключ
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /d/tmp/nginx.key -out /d/tmp/nginx.crt -subj "/CN=my-nginx/O=my-nginx"
# Перетворіть ключі у кодування base64
cat /d/tmp/nginx.key | base64
cat /d/tmp/nginx.crt | base64

Скористайтесь виводом з попередньої команди для створення yaml-файлу. Закодовані у base64 значення мають бути в одному рядку.

apiVersion: "v1"
kind: "Secret"
metadata:
  name: "nginxsecret"
  namespace: "default"
type: kubernetes.io/tls
data:
  tls.crt: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURIekNDQWdlZ0F3SUJBZ0lKQUp5M3lQK0pzMlpJTUEwR0NTcUdTSWIzRFFFQkJRVUFNQ1l4RVRBUEJnTlYKQkFNVENHNW5hVzU0YzNaak1SRXdEd1lEVlFRS0V3aHVaMmx1ZUhOMll6QWVGdzB4TnpFd01qWXdOekEzTVRKYQpGdzB4T0RFd01qWXdOekEzTVRKYU1DWXhFVEFQQmdOVkJBTVRDRzVuYVc1NGMzWmpNUkV3RHdZRFZRUUtFd2h1CloybHVlSE4yWXpDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBSjFxSU1SOVdWM0IKMlZIQlRMRmtobDRONXljMEJxYUhIQktMSnJMcy8vdzZhU3hRS29GbHlJSU94NGUrMlN5ajBFcndCLzlYTnBwbQppeW1CL3JkRldkOXg5UWhBQUxCZkVaTmNiV3NsTVFVcnhBZW50VWt1dk1vLzgvMHRpbGhjc3paenJEYVJ4NEo5Ci82UVRtVVI3a0ZTWUpOWTVQZkR3cGc3dlVvaDZmZ1Voam92VG42eHNVR0M2QURVODBpNXFlZWhNeVI1N2lmU2YKNHZpaXdIY3hnL3lZR1JBRS9mRTRqakxCdmdONjc2SU90S01rZXV3R0ljNDFhd05tNnNTSzRqYUNGeGpYSnZaZQp2by9kTlEybHhHWCtKT2l3SEhXbXNhdGp4WTRaNVk3R1ZoK0QrWnYvcW1mMFgvbVY0Rmo1NzV3ajFMWVBocWtsCmdhSXZYRyt4U1FVQ0F3RUFBYU5RTUU0d0hRWURWUjBPQkJZRUZPNG9OWkI3YXc1OUlsYkROMzhIYkduYnhFVjcKTUI4R0ExVWRJd1FZTUJhQUZPNG9OWkI3YXc1OUlsYkROMzhIYkduYnhFVjdNQXdHQTFVZEV3UUZNQU1CQWY4dwpEUVlKS29aSWh2Y05BUUVGQlFBRGdnRUJBRVhTMW9FU0lFaXdyMDhWcVA0K2NwTHI3TW5FMTducDBvMm14alFvCjRGb0RvRjdRZnZqeE04Tzd2TjB0clcxb2pGSW0vWDE4ZnZaL3k4ZzVaWG40Vm8zc3hKVmRBcStNZC9jTStzUGEKNmJjTkNUekZqeFpUV0UrKzE5NS9zb2dmOUZ3VDVDK3U2Q3B5N0M3MTZvUXRUakViV05VdEt4cXI0Nk1OZWNCMApwRFhWZmdWQTRadkR4NFo3S2RiZDY5eXM3OVFHYmg5ZW1PZ05NZFlsSUswSGt0ejF5WU4vbVpmK3FqTkJqbWZjCkNnMnlwbGQ0Wi8rUUNQZjl3SkoybFIrY2FnT0R4elBWcGxNSEcybzgvTHFDdnh6elZPUDUxeXdLZEtxaUMwSVEKQ0I5T2wwWW5scE9UNEh1b2hSUzBPOStlMm9KdFZsNUIyczRpbDlhZ3RTVXFxUlU9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"
  tls.key: "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRQ2RhaURFZlZsZHdkbFIKd1V5eFpJWmVEZWNuTkFhbWh4d1NpeWF5N1AvOE9ta3NVQ3FCWmNpQ0RzZUh2dGtzbzlCSzhBZi9WemFhWm9zcApnZjYzUlZuZmNmVUlRQUN3WHhHVFhHMXJKVEVGSzhRSHA3VkpMcnpLUC9QOUxZcFlYTE0yYzZ3MmtjZUNmZitrCkU1bEVlNUJVbUNUV09UM3c4S1lPNzFLSWVuNEZJWTZMMDUrc2JGQmd1Z0ExUE5JdWFubm9UTWtlZTRuMG4rTDQKb3NCM01ZUDhtQmtRQlAzeE9JNHl3YjREZXUraURyU2pKSHJzQmlIT05Xc0RadXJFaXVJMmdoY1kxeWIyWHI2UAozVFVOcGNSbC9pVG9zQngxcHJHclk4V09HZVdPeGxZZmcvbWIvNnBuOUYvNWxlQlkrZStjSTlTMkQ0YXBKWUdpCkwxeHZzVWtGQWdNQkFBRUNnZ0VBZFhCK0xkbk8ySElOTGo5bWRsb25IUGlHWWVzZ294RGQwci9hQ1Zkank4dlEKTjIwL3FQWkUxek1yall6Ry9kVGhTMmMwc0QxaTBXSjdwR1lGb0xtdXlWTjltY0FXUTM5SjM0VHZaU2FFSWZWNgo5TE1jUHhNTmFsNjRLMFRVbUFQZytGam9QSFlhUUxLOERLOUtnNXNrSE5pOWNzMlY5ckd6VWlVZWtBL0RBUlBTClI3L2ZjUFBacDRuRWVBZmI3WTk1R1llb1p5V21SU3VKdlNyblBESGtUdW1vVlVWdkxMRHRzaG9reUxiTWVtN3oKMmJzVmpwSW1GTHJqbGtmQXlpNHg0WjJrV3YyMFRrdWtsZU1jaVlMbjk4QWxiRi9DSmRLM3QraTRoMTVlR2ZQegpoTnh3bk9QdlVTaDR2Q0o3c2Q5TmtEUGJvS2JneVVHOXBYamZhRGR2UVFLQmdRRFFLM01nUkhkQ1pKNVFqZWFKClFGdXF4cHdnNzhZTjQyL1NwenlUYmtGcVFoQWtyczJxWGx1MDZBRzhrZzIzQkswaHkzaE9zSGgxcXRVK3NHZVAKOWRERHBsUWV0ODZsY2FlR3hoc0V0L1R6cEdtNGFKSm5oNzVVaTVGZk9QTDhPTm1FZ3MxMVRhUldhNzZxelRyMgphRlpjQ2pWV1g0YnRSTHVwSkgrMjZnY0FhUUtCZ1FEQmxVSUUzTnNVOFBBZEYvL25sQVB5VWs1T3lDdWc3dmVyClUycXlrdXFzYnBkSi9hODViT1JhM05IVmpVM25uRGpHVHBWaE9JeXg5TEFrc2RwZEFjVmxvcG9HODhXYk9lMTAKMUdqbnkySmdDK3JVWUZiRGtpUGx1K09IYnRnOXFYcGJMSHBzUVpsMGhucDBYSFNYVm9CMUliQndnMGEyOFVadApCbFBtWmc2d1BRS0JnRHVIUVV2SDZHYTNDVUsxNFdmOFhIcFFnMU16M2VvWTBPQm5iSDRvZUZKZmcraEppSXlnCm9RN3hqWldVR3BIc3AyblRtcHErQWlSNzdyRVhsdlhtOElVU2FsbkNiRGlKY01Pc29RdFBZNS9NczJMRm5LQTQKaENmL0pWb2FtZm1nZEN0ZGtFMXNINE9MR2lJVHdEbTRpb0dWZGIwMllnbzFyb2htNUpLMUI3MkpBb0dBUW01UQpHNDhXOTVhL0w1eSt5dCsyZ3YvUHM2VnBvMjZlTzRNQ3lJazJVem9ZWE9IYnNkODJkaC8xT2sybGdHZlI2K3VuCnc1YytZUXRSTHlhQmd3MUtpbGhFZDBKTWU3cGpUSVpnQWJ0LzVPbnlDak9OVXN2aDJjS2lrQ1Z2dTZsZlBjNkQKckliT2ZIaHhxV0RZK2Q1TGN1YSt2NzJ0RkxhenJsSlBsRzlOZHhrQ2dZRUF5elIzT3UyMDNRVVV6bUlCRkwzZAp4Wm5XZ0JLSEo3TnNxcGFWb2RjL0d5aGVycjFDZzE2MmJaSjJDV2RsZkI0VEdtUjZZdmxTZEFOOFRwUWhFbUtKCnFBLzVzdHdxNWd0WGVLOVJmMWxXK29xNThRNTBxMmk1NVdUTThoSDZhTjlaMTltZ0FGdE5VdGNqQUx2dFYxdEYKWSs4WFJkSHJaRnBIWll2NWkwVW1VbGc9Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K"

Тепер створіть Secret, застосувавши файл:

kubectl apply -f nginxsecret.yaml
kubectl get secrets
NAME                  TYPE                                  DATA      AGE
nginxsecret           kubernetes.io/tls                     2         1m

Тепер змініть кількість реплік nginx для запуску сервера https використовуючи сертифікати з Secret та Service для експонування портів (80 та 443):

apiVersion: v1
kind: Service
metadata:
  name: my-nginx
  labels:
    run: my-nginx
spec:
  type: NodePort
  ports:
  - port: 8080
    targetPort: 80
    protocol: TCP
    name: http
  - port: 443
    protocol: TCP
    name: https
  selector:
    run: my-nginx
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
spec:
  selector:
    matchLabels:
      run: my-nginx
  replicas: 1
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      volumes:
      - name: secret-volume
        secret:
          secretName: nginxsecret
      - name: configmap-volume
        configMap:
          name: nginxconfigmap
      containers:
      - name: nginxhttps
        image: bprashanth/nginxhttps:1.0
        ports:
        - containerPort: 443
        - containerPort: 80
        volumeMounts:
        - mountPath: /etc/nginx/ssl
          name: secret-volume
        - mountPath: /etc/nginx/conf.d
          name: configmap-volume

Примітні моменти про маніфест nginx-secure-app:

  • Він містить як специфікацію Deployment, так і Service в одному файлі.
  • nginx сервер обслуговує HTTP трафік на порту 80 та HTTPS трафік на порту 443, і Service nginx відкриває доступ до обох портів.
  • Кожен контейнер має доступ до ключів через том, змонтовану в /etc/nginx/ssl. Це налаштовується до запуску nginx сервера.
kubectl delete deployments,svc my-nginx; kubectl create -f ./nginx-secure-app.yaml

На цьому етапі ви можете отримати доступ до nginx сервера з будь-якого вузла.

kubectl get pods -l run=my-nginx -o custom-columns=POD_IP:.status.podIPs
    POD_IP
    [map[ip:10.244.3.5]]
node $ curl -k https://10.244.3.5
...
<h1>Welcome to nginx!</h1>

Зверніть увагу, що ми використовували параметр -k для curl на останньому етапі, тому що ми нічого не знаємо про Podʼи, що виконують nginx під час генерації сертифіката, тому ми повинні сказати curl ігнорувати невідповідність CName. Створюючи Service, ми повʼязали CName, який використовується в сертифікаті, з фактичним DNS-імʼям, що використовується Podʼами під час пошуку Service. Перевірмо це з Podʼа (для простоти використовується той же Secret, Podʼу потрібен лише nginx.crt для доступу до Service):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: curl-deployment
spec:
  selector:
    matchLabels:
      app: curlpod
  replicas: 1
  template:
    metadata:
      labels:
        app: curlpod
    spec:
      volumes:
      - name: secret-volume
        secret:
          secretName: nginxsecret
      containers:
      - name: curlpod
        command:
        - sh
        - -c
        - while true; do sleep 1; done
        image: radial/busyboxplus:curl
        volumeMounts:
        - mountPath: /etc/nginx/ssl
          name: secret-volume
kubectl apply -f ./curlpod.yaml
kubectl get pods -l app=curlpod
NAME                               READY     STATUS    RESTARTS   AGE
curl-deployment-1515033274-1410r   1/1       Running   0          1m
kubectl exec curl-deployment-1515033274-1410r -- curl https://my-nginx --cacert /etc/nginx/ssl/tls.crt
...
<title>Welcome to nginx!</title>
...

Експонування Service

Для деяких частин ваших застосунків ви можете захотіти експонувати Service на зовнішню IP-адресу. Kubernetes підтримує два способи: NodePorts та LoadBalancers. Service, створений у попередньому розділі, вже використовував NodePort, тому ваша репліка nginx HTTPS готова обслуговувати трафік з інтернету, якщо ваш вузол має публічну IP-адресу.

kubectl get svc my-nginx -o yaml | grep nodePort -C 5
  uid: 07191fb3-f61a-11e5-8ae5-42010af00002
spec:
  clusterIP: 10.0.162.149
  ports:
  - name: http
    nodePort: 31704
    port: 8080
    protocol: TCP
    targetPort: 80
  - name: https
    nodePort: 32453
    port: 443
    protocol: TCP
    targetPort: 443
  selector:
    run: my-nginx
kubectl get nodes -o yaml | grep ExternalIP -C 1
    - address: 104.197.41.11
      type: ExternalIP
    allocatable:
--
    - address: 23.251.152.56
      type: ExternalIP
    allocatable:
...

$ curl https://<EXTERNAL-IP>:<NODE-PORT> -k
...
<h1>Welcome to nginx!</h1>

Тепер створімо Service знову, використовуючи хмарний балансувальник навантаження. Змініть Type Service my-nginx з NodePort на LoadBalancer:

kubectl edit svc my-nginx
kubectl get svc my-nginx
NAME       TYPE           CLUSTER-IP     EXTERNAL-IP        PORT(S)               AGE
my-nginx   LoadBalancer   10.0.162.149   xx.xxx.xxx.xxx     8080:30163/TCP        21s
curl https://<EXTERNAL-IP> -k
...
<title>Welcome to nginx!</title>

IP-адреса в колонці EXTERNAL-IP доступна в інтернеті. CLUSTER-IP доступна тільки всередині вашого кластера/приватної хмарної мережі.

Зверніть увагу, що у AWS тип LoadBalancer створює ELB, який використовує (довге) імʼя хосту, а не IP. Воно занадто довге, щоб поміститися в стандартному виводі kubectl get svc, тому вам потрібно виконати kubectl describe service my-nginx, щоб побачити його. Ви побачите щось на кшталт:

kubectl describe service my-nginx
...
LoadBalancer Ingress:   a320587ffd19711e5a37606cf4a74574-1142138393.us-east-1.elb.amazonaws.com
...

Що далі

2 - Використання Source IP

Застосунки, що працюють у кластері Kubernetes, знаходять і взаємодіють один з одним та з зовнішнім світом через абстракцію Service. У цьому документі пояснюється, що відбувається з Source IP пакетів, надісланих на різні типи Service, і як ви можете змінити цю поведінку відповідно до ваших потреб.

Перш ніж ви розпочнете

Терміни

У цьому документі використовуються наступні терміни:

NAT
Трансляція мережевих адрес
Source NAT
Заміна Source IP в пакеті; у цьому документі це зазвичай означає заміну на IP-адресу вузла.
Destination NAT
Заміна Destination IP в пакеті; у цьому документі це зазвичай означає заміну на IP-адресу Pod
VIP
Віртуальна IP-адреса, така як та, що призначається кожному Service у Kubernetes
kube-proxy
Мережевий демон, який оркеструє управління віртуальними IP Service на кожному вузлі

Необхідні умови

Вам треба мати кластер Kubernetes, а також інструмент командного рядка kubectl має бути налаштований для роботи з вашим кластером. Рекомендується виконувати ці настанови у кластері, що має щонайменше два вузли, які не виконують роль вузлів управління. Якщо у вас немає кластера, ви можете створити його, за допомогою minikube або використовувати одну з цих пісочниць:

У прикладах використовується невеликий вебсервер nginx, який повертає Source IP запитів, які він отримує через HTTP-заголовок. Ви можете створити його наступним чином:

kubectl create deployment source-ip-app --image=registry.k8s.io/echoserver:1.10

Результат:

deployment.apps/source-ip-app created

Цілі

  • Експонувати простий застосунок через різні типи Serviceʼів
  • Зрозуміти, як кожен тип Services обробляє Source IP NAT
  • Зрозуміти компроміси, повʼязані зі збереженням Source IP

Source IP для Service з Type=ClusterIP

Пакети, надіслані на ClusterIP зсередини кластера, ніколи не обробляються Source NAT, якщо ви використовуєте kube-proxy в iptables mode, (станартно). Ви можете запитати режим kube-proxy, отримавши http://localhost:10249/proxyMode на вузлі, де працює kube-proxy.

kubectl get nodes

Результат схожий на цей:

NAME                           STATUS     ROLES    AGE     VERSION
kubernetes-node-6jst   Ready      <none>   2h      v1.13.0
kubernetes-node-cx31   Ready      <none>   2h      v1.13.0
kubernetes-node-jj1t   Ready      <none>   2h      v1.13.0

Отримати режим проксі на одному з вузлів (kube-proxy слухає на порту 10249):

# Запустіть це в оболонці на вузлі, який хочете запитати.
curl http://localhost:10249/proxyMode

Результат:

iptables

Ви можете протестувати збереження Source IP, створивши Service над source-ip застосунку:

kubectl expose deployment source-ip-app --name=clusterip --port=80 --target-port=8080

Результат:

service/clusterip exposed
kubectl get svc clusterip

Результат схожий на:

NAME         TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)   AGE
clusterip    ClusterIP   10.0.170.92   <none>        80/TCP    51s

І звернутися до ClusterIP з Pod в тому ж кластері:

kubectl run busybox -it --image=busybox:1.28 --restart=Never --rm

Результат схожий на цей:

Waiting for pod default/busybox to be running, status is Pending, pod ready: false
If you don't see a command prompt, try pressing enter.

Ви можете виконати команду всередині цього Pod:

# Запустіть це всередині терміналу "kubectl run"
ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
3: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1460 qdisc noqueue
    link/ether 0a:58:0a:f4:03:08 brd ff:ff:ff:ff:ff:ff
    inet 10.244.3.8/24 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::188a:84ff:feb0:26a5/64 scope link
       valid_lft forever preferred_lft forever

…потім використовувати wget для надсилання запиту до локального вебсервера

# Замініть "10.0.170.92" на IPv4-адресу сервісу "clusterip"
wget -qO - 10.0.170.92
CLIENT VALUES:
client_address=10.244.3.8
command=GET
...

client_address завжди є IP-адресою Podʼа-клієнта, незалежно від того, знаходяться Pod-клієнт і Pod-сервер на одному вузлі чи на різних вузлах.

Source IP для Service з Type=NodePort

Пакети, надіслані на Serviceʼи з Type=NodePort, типово обробляються Source NAT. Ви можете протестувати це, створивши Service NodePort:

kubectl expose deployment source-ip-app --name=nodeport --port=80 --target-port=8080 --type=NodePort

Результат:

service/nodeport exposed
NODEPORT=$(kubectl get -o jsonpath="{.spec.ports[0].nodePort}" services nodeport)
NODES=$(kubectl get nodes -o jsonpath='{ $.items[*].status.addresses[?(@.type=="InternalIP")].address }')

Якщо ви працюєте на хмарному провайдері, можливо, вам доведеться відкрити правило брандмауера для nodes:nodeport, зазначеного вище. Тепер ви можете спробувати звернутися до сервісу ззовні кластера через вказаний вище порт вузла.

for node in $NODES; do curl -s $node:$NODEPORT | grep -i client_address; done

Результат схожий на:

client_address=10.180.1.1
client_address=10.240.0.5
client_address=10.240.0.3

Зверніть увагу, що це не правильні IP клієнтів, це внутрішні IP кластера. Це відбувається так:

  • Клієнт надсилає пакет до node2:nodePort
  • node2 замінює Source IP-адресу (SNAT) у пакеті своєю власною IP-адресою
  • node2 замінює Destination IP-адресу пакета на IP Podʼа
  • пакет спрямовується до node 1, а потім до точки доступу
  • відповідь Podʼа спрямовується назад до node2
  • відповідь Podʼа надсилається клієнту

Візуально це виглядає так:

graph LR; client(client)-->node2[Node 2]; node2-->client; node2-. SNAT .->node1[Node 1]; node1-. SNAT .->node2; node1-->endpoint(Endpoint); classDef plain fill:#ddd,stroke:#fff,stroke-width:4px,color:#000; classDef k8s fill:#326ce5,stroke:#fff,stroke-width:4px,color:#fff; class node1,node2,endpoint k8s; class client plain;

Схема. Source IP Type=NodePort з використанням SNAT

Щоб уникнути цього, Kubernetes має функцію зберігати Source IP-адресу клієнта. Якщо ви встановите для service.spec.externalTrafficPolicy значення Local, kube-proxy надсилає проксі-запити лише до локальних точок доступу та не пересилає трафік до інших вузлів. Цей підхід зберігає Source IP-адресу. Якщо немає локальних точок доступу, пакети, надіслані до вузла, відкидаються, тому ви можете покладатися на правильну Source IP-адресу в будь-яких правилах обробки пакетів, які ви можете застосувати до пакета, який проходить до точки доступу.

Встановіть значення поля service.spec.externalTrafficPolicy наступним чином:

kubectl patch svc nodeport -p '{"spec":{"externalTrafficPolicy":"Local"}}'

Вивід має бути наступним:

service/nodeport patched

Повторіть тест:

for node in $NODES; do curl --connect-timeout 1 -s $node:$NODEPORT | grep -i client_address; done

Вивід подібний до:

client_address=198.51.100.79

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

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

  • клієнт надсилає пакет на node2:nodePort, який не має кінцевих точок
  • пакет відкидається
  • клієнт надсилає пакет на node1:nodePort, який має точки доступу
  • node1 направляє пакет до точки доступу з правильною Source IP-адресою

Візуально:

graph TD; client(client)-->node1[Node 1]; client(client)-->node2[Node 2]; node1-->endpoint(Endpoint); endpoint(Endpoint)-->node1; classDef plain fill:#ddd,stroke:#fff,stroke-width:4px,color:#000; classDef k8s fill:#326ce5,stroke:#fff,stroke-width:4px,color:#fff; class node1,node2,endpoint k8s; class client plain;
Схема. Source IP Type=NodePort зберігає Source IP клієнта

Source IP для Service з Type=LoadBalancer

Пакети, які надсилаються на Service з Type=LoadBalancer, стандартно мають Source NAT адресацію, оскільки всі можливі для планування вузли Kubernetes у стані Ready мають право на навантаження рівномірного розподілу трафіку. Якщо пакети надходять на вузол без точки доступу, система передає їх на вузол з точкою доступу, замінюючи адресу Source IP на пакеті на IP-адресу вузла (як описано у попередньому розділі).

Ви можете перевірити це, експонувавши застосунок source-ip-app через балансувальник навантаження:

kubectl expose deployment source-ip-app --name=loadbalancer --port=80 --target-port=8080 --type=LoadBalancer

Результат виглядає наступним чином:

service/loadbalancer exposed

Виведіть IP-адреси Service:

kubectl get svc loadbalancer

Вивід подібний наступному:

NAME           TYPE           CLUSTER-IP    EXTERNAL-IP       PORT(S)   AGE
loadbalancer   LoadBalancer   10.0.65.118   203.0.113.140     80/TCP    5m

Потім надішліть запит на зовнішню IP-адресу цього Service:

curl 203.0.113.140

Вивід подібний наступному:

CLIENT VALUES:
client_address=10.240.0.5
...

Однак, якщо ви працюєте на середовищі Google Kubernetes Engine/GCE, встановлення поля service.spec.externalTrafficPolicy на значення Local змушує вузли без точок доступу Service видаляти себе зі списку вузлів, які мають право на рівномірно розподілений трафік, шляхом умисної невдачі перевірок стану.

Візуально:

IP-адреса джерела з externalTrafficPolicy

Ви можете перевірити це, встановивши анотацію:

kubectl patch svc loadbalancer -p '{"spec":{"externalTrafficPolicy":"Local"}}'

Ви повинні негайно побачити, що поле service.spec.healthCheckNodePort виділене Kubernetes:

kubectl get svc loadbalancer -o yaml | grep -i healthCheckNodePort

Вивід подібний наступному:

  healthCheckNodePort: 32122

Поле service.spec.healthCheckNodePort вказує на порт на кожному вузлі, на якому надається перевірка стану за адресою /healthz. Ви можете перевірити це:

kubectl get pod -o wide -l app=source-ip-app

Вивід подібний наступному:

NAME                            READY     STATUS    RESTARTS   AGE       IP             NODE
source-ip-app-826191075-qehz4   1/1       Running   0          20h       10.180.1.136   kubernetes-node-6jst

Використовуйте curl, щоб отримати доступ до точки доступу /healthz на різних вузлах:

# Виконайте це локально на вузлі, який ви вибрали
curl localhost:32122/healthz
1 Service Endpoints found

На іншому вузлі ви можете отримати інший результат:

# Виконайте це локально на вузлі, який ви вибрали
curl localhost:32122/healthz
No Service Endpoints Found

Контролер, який працює на панелі управління відповідає за доступ до хмарного балансувальника навантаження. Той самий контролер також виділяє HTTP перевірки стану, що вказують на цей порт/шлях на кожному вузлі. Зачекайте приблизно 10 секунд, щоб вузли без точок доступу мають невдалі перевірки стану, а потім використовуйте curl, щоб запитати IPv4-адресу балансувальника навантаження:

curl 203.0.113.140

Вивід подібний наступному:

CLIENT VALUES:
client_address=198.51.100.79
...

Міжплатформна підтримка

Тільки деякі хмарні постачальники пропонують підтримку збереження Source IP-адреси через Serviceʼи з Type=LoadBalancer. Хмарний постачальник, на якому ви працюєте, може виконати запит на надання балансувальника навантаження кількома різними способами:

  1. З проксі, що завершує зʼєднання клієнта та відкриває нове зʼєднання з вашими вузлами/точками доступу. У таких випадках Source IP-адреса завжди буде адресою хмарного балансувальника, а не адресою клієнта.

  2. З сервісом пересилання пакетів (forwarder), таким чином, що запити від клієнта, надісліна до VIP балансувальника, потрапляють на вузол з Source IP-адресою клієнта, а не на проміжний проксі.

Балансувальники навантаження у першій категорії повинні використовувати узгоджений протокол між балансувальником навантаження та бекендом для передачі реальної IP-адреси клієнта, такі як HTTP Forwarded або X-FORWARDED-FOR заголовки, або протокол проксі. Балансувальники навантаження у другій категорії можуть використовувати функціонал, описаний вище, створюючи HTTP перевірку стану, що вказує на порт, збережений у полі service.spec.healthCheckNodePort Serviceʼа.

Очищення

Видаліть Serviceʼи:

kubectl delete svc -l app=source-ip-app

Видаліть Deployment, ReplicaSet та Pod:

kubectl delete deployment source-ip-app

Що далі

3 - Дослідження поведінки завершення роботи для Podʼів та їх точок доступу

Коли ви підʼєднали свій застосунок до Service, дотримуючись кроків, схожих на ті, що описані в Підключення застосунків за допомогою Service, у вас є постійно працюючий, реплікований застосунок, який викритий в мережі. Цей посібник допоможе вам розглянути процес завершення роботи для Podʼів та дослідити способи впровадження належного припинення зʼєднань.

Процес завершення роботи для Podʼів та їх точок доступу

Часто виникають випадки, коли потрібно завершити роботу Podʼа — чи то для оновлення, чи то для зменшення масштабу. Для поліпшення доступності застосунку може бути важливо реалізувати правильне завершення активних зʼєднань.

У цьому посібнику пояснюється процес завершення роботи для Podʼів у звʼязку з відповідним станом точки доступу та видаленням за допомогою простого вебсервера nginx для демонстрації концепції.

Приклад процесу з завершенням роботи точки доступу

Наведений нижче приклад показує описаний у документі Завершення роботи Podʼів процес.

Допустимо, у вас є Deployment, яка складається з одної репліки nginx (для демонстраційних цілей) та Service:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      terminationGracePeriodSeconds: 120 # extra long grace period
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80
        lifecycle:
          preStop:
            exec:
              # Real life termination may take any time up to terminationGracePeriodSeconds.
              # In this example - just hang around for at least the duration of terminationGracePeriodSeconds,
              # at 120 seconds container will be forcibly terminated.
              # Note, all this time nginx will keep processing requests.
              command: [
                "/bin/sh", "-c", "sleep 180"
              ]
apiVersion: v1
kind: Service
metadata:
  name: nginx-service
spec:
  selector:
    app: nginx
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80

Тепер створіть Pod Deployment та Service, використовуючи вищезазначені файли:

kubectl apply -f pod-with-graceful-termination.yaml
kubectl apply -f explore-graceful-termination-nginx.yaml

Після запуску Podʼа та Service ви можете отримати імʼя будь-яких повʼязаних точок доступу:

kubectl get endpointslice

Вивід подібний до цього:

NAME                  ADDRESSTYPE   PORTS   ENDPOINTS                 AGE
nginx-service-6tjbr   IPv4          80      10.12.1.199,10.12.1.201   22m

Ви можете перевірити їх статус та підтвердити, що зареєстровано одну точку доступу:

kubectl get endpointslices -o json -l kubernetes.io/service-name=nginx-service

Вивід подібний до цього:

{
    "addressType": "IPv4",
    "apiVersion": "discovery.k8s.io/v1",
    "endpoints": [
        {
            "addresses": [
                "10.12.1.201"
            ],
            "conditions": {
                "ready": true,
                "serving": true,
                "terminating": false

Тепер завершімо роботу Podʼа та перевіримо, що Pod завершується, дотримуючись налаштувань відповідного періоду завершення роботи:

kubectl delete pod nginx-deployment-7768647bf9-b4b9s

Усі Podʼи:

kubectl get pods

Вивід подібний до цього:

NAME                                READY   STATUS        RESTARTS      AGE
nginx-deployment-7768647bf9-b4b9s   1/1     Terminating   0             4m1s
nginx-deployment-7768647bf9-rkxlw   1/1     Running       0             8s

Ви бачите, що новий Pod був запланований.

Поки створюється нова точка доступу для нового Podʼа, стара точка доступу все ще знаходиться у стані завершення:

kubectl get endpointslice -o json nginx-service-6tjbr

Вивід подібний до цього:

{
    "addressType": "IPv4",
    "apiVersion": "discovery.k8s.io/v1",
    "endpoints": [
        {
            "addresses": [
                "10.12.1.201"
            ],
            "conditions": {
                "ready": false,
                "serving": true,
                "terminating": true
            },
            "nodeName": "gke-main-default-pool-dca1511c-d17b",
            "targetRef": {
                "kind": "Pod",
                "name": "nginx-deployment-7768647bf9-b4b9s",
                "namespace": "default",
                "uid": "66fa831c-7eb2-407f-bd2c-f96dfe841478"
            },
            "zone": "us-central1-c"
        },
        {
            "addresses": [
                "10.12.1.202"
            ],
            "conditions": {
                "ready": true,
                "serving": true,
                "terminating": false
            },
            "nodeName": "gke-main-default-pool-dca1511c-d17b",
            "targetRef": {
                "kind": "Pod",
                "name": "nginx-deployment-7768647bf9-rkxlw",
                "namespace": "default",
                "uid": "722b1cbe-dcd7-4ed4-8928-4a4d0e2bbe35"
            },
            "zone": "us-central1-c"

Це дозволяє застосункам передавати свій стан під час завершення, а клієнтам (таким як балансувальники навантаження) реалізувати функціональність завершення зʼєднань. Ці клієнти можуть виявляти завершальні точки доступу та реалізувати спеціальну логіку для них.

У Kubernetes точки доступу, які завершуються, завжди мають свій статус ready встановлений як false. Це потрібно для забезпечення зворотної сумісності, щоб наявні балансувальники навантаження не використовували їх для звичайного трафіку. Якщо потрібно припинення обробки трафіку на Podʼі, що завершує роботу, фактична готовність може бути перевірені як умова serving.

Після видалення Podʼа, стару точку доступу також буде видалено.

Що далі