Kubernetes: ClusterIP vs NodePort vs LoadBalancer, Services и Ingress — обзор, примеры

Автор: | 06/04/2020
 

Для сетевого взаимодействия Kubernetes предоставляет четыре типа Service-ресурсов — ClusterIP (дефолтный), NodePort, LoadBalancer и ExternalName, плюс ресурс Ingress.

В этом посте разберём каждый из них, и попробуем их в работе.

Документация по типам — Publishing Services (ServiceTypes).

Рассматривается в основном AWS, потому учитываем, что сеть у нас — AWS VPC, балансировщики нагрузки — AWS ALB/CLB, кластер Kubernetes — AWS EKS.

Сеть и Kubernetes

В Kubernetes нам могут потребоваться такие варианты работы по сети:

  • прямая связь контейнеров с контейнерами — обеспечивается абстракцией pod и доступом контейнеров друг к другу через localhost в рамках пода — см. pods
  • коммуникация Pod to Pod в рамках кластера — обепечивается сетевыми плагинами, в случае с AWS EKS см. AWS VPC CNI for Kubernetes
  • связь Pod to Service — обеспечивается абстракциями Service, например — ClusterIP
  • связь из мира к подам в кластере — обеспечивается абстракцией Service, к которому может быть добавлен внешний ресурс, например — AWS Load Balancer

Основная цель Services в kuberenetes — обеспечить постоянный доступ к подам, без необходимости после каждого пересоздания пода искать его IP. Кроме того, Services обеспечивают минимальный load balancing, распределяя трафик между несколькими однотипными подами, см. Service.

Подготовка

Для тестов создадим деплоймент с подом, в котором запустим контейнер с NGINX, который будет принимать подключения на порт 80:

kubectl create deployment nginx --image=nginx
deployment.apps/nginx created

Проверяем:

kk get deploy nginx
NAME    READY   UP-TO-DATE   AVAILABLE   AGE
nginx   1/1     1            1           53s

Т.к. Service будут искать поды по их лейблам — проверим, какие теги созданы для подов этого деплоймента:

kubectl get deploy nginx -o jsonpath='{.metadata.labels}'
map[app:nginx]

Окей — тег app, значение nginx — запомним его.

kubectl port-forward

Что бы убедиться, что наш под принимает подключения на порт 80 — используем kubectl port-forward. После того, как будем точно знать, что у самого пода с сетью всё в порядке — можно будет настраивать сеть со стороны Kubernetes.

Находим имя пода:

kubectl get pod
NAME                                        READY   STATUS    RESTARTS   AGE
nginx-554b9c67f9-rwbp7                      1/1     Running   0          40m

Передаём его в kubectl port-forward, затем локальный порт (8080), и порт в поде (80):

kubectl port-forward nginx-554b9c67f9-rwbp7 8080:80
Forwarding from [::1]:8080 -> 80

С локальной машины проверяем доступ к NGINX в Kubernetes:

curl localhost:8080
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

Окей, теперь, когда у нас есть работающий под — посмотрим, как к нему можно получить доступ через Service объекты Kubernetes.

Типы Service — обзор

Рассмотрим типы кратко, а потом перейдём к примерам:

  1. ClusterIP: дефолтный тип, создаёт сервис с IP из пула внутренних адресов кластера, такой сервис будет доступен только внутри кластера (либо через kube-proxy)
  2. NodePort: открывает TCP порт на каждой WorkerNode EС2, “за ним” автоматом создаёт ClusterIP Service, и роутит трафик с порта ЕС2 на этот ClusterIP — такой сервис будет доступен из мира (если EC2 имеют публичные адреса), либо только внутри VPC
  3. LoadBalancer: создаёт внешний Load Balancer (AWS Classic LB), “за ним” автоматом создаёт NodePort, за ним автоматом ClusterIP, и таким образом роутит трафик от Load Balancer к поду в кластере
  4. ExternalName: что-то вроде “DNS-прокси” — в ответ на обращение к такому сервису через CNAME вернёт значение, заданное в externalName

ClusterIP

Самый простой тип, используется по-умолчанию.

Открывает доступ к приложению внутри кластера, без доступа из мира.

Можно использовать для, например, для доступа к системе кеширования, которая будет доступна другим подам в неймспейсе кластера.

Используем такой манифест:

---
apiVersion: v1
kind: Service
metadata:
  name: "nginx-service"
  namespace: "default"
spec:
  ports:
    - port: 80
  type: ClusterIP
  selector:
    app: "nginx"

Создаём сервис:

kubectl apply -f nginx-svc.yaml
service/nginx-service created

Проверяем:

kk get svc nginx-service
NAME            TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
nginx-service   ClusterIP   172.20.54.138   <none>        80/TCP    38s

kubectl proxy и Service DNS

Так как при ClusterIP сервис доступен только изнутри кластера — то для проверки его работы можно использовать kubectl proxy, который пробросит локальный TCP-порт с рабочей машины к нашему API-серверу, а через него мы сможем обратиться к созданному сервису.

Запускаем прокси:

kubectl proxy --port=8080
Starting to serve on 127.0.0.1:8080

Зная имя сервиса — мы его указали в metadata: name — можем обратиться к localhost:8080 и через имя неймспейса — к самому сервису:

curl -L localhost:8080/api/v1/namespaces/default/services/nginx-service/proxy
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

Или просто получить информацию об этом сервисе:

curl -L localhost:8080/api/v1/namespaces/default/services/nginx-service/
{
"kind": "Service",
"apiVersion": "v1",
"metadata": {
"name": "nginx-service",
"namespace": "default",
"selfLink": "/api/v1/namespaces/default/services/nginx-service",
...

Итак, ClusterIP:

  • обеспечивает доступ к приложению в пределах кластера, но без доступа из мира
  • получает IP из CIDR кластера, доступен через DNS-имя в пределах кластера, см. DNS for Services and Pods

NodePort

Теперь попробуем NodePort.

При этом типе Kubernetes откроет TCP-порт на всех WorkerNodes, а затем через kube-proxy, работающий на всех хостах  кластера, будет проксировать запросы с этого TCP-порта к поду на этой ноде.

Обновляем манифест:

---
apiVersion: v1
kind: Service
metadata:
  name: "nginx-service"
  namespace: "default"
spec:
  ports:
    - port: 80
      nodePort: 30001
  type: NodePort
  selector:
    app: "nginx"

Параметр nodePort тут опционален, добавлен для примера. Если его не указать — Kubernetes выделит порт из диапазона 30000-32767.

Обновляем сервис:

kubectl apply -f nginx-svc.yaml
service/nginx-service configured

Проверяем:

kubectl get svc nginx-service
NAME            TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
nginx-service   NodePort   172.20.54.138   <none>        80:30001/TCP   20h

И проверяем порт на самом ЕС2-инстансе:

[root@ip-10-3-49-200 ec2-user]# netstat -anp | grep 30001
tcp6       0      0 :::30001                :::*                    LISTEN      5332/kube-proxy

Очевидно, что если WorkerNodes живут в приватных сетях, и к ним нет доступа из мира — то вы не сможете использовать такой сервис для работы.

Но вы можете получить доступ к нашему NGINX из этой подсети, например с Bastion-хоста:

[ec2-user@ip-10-3-49-200 ~]$ curl 10.3.49.200:30001
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

Итак, NodePort:

  • привязан к конкретному серверу, например ЕС2
  • если сервер не доступен из мира — очевидно, что не обеспечит доступ к подам из мира
  • получает IP из блока адресов, выделенных провайдером, например — VPC CIDR
  • обеспечивает доступ к подам только на конкретной ноде

LoadBalancer

Наиболее часто используемый тип сервиса.

В случае с AWS — создаст AWS Load Balancer, по-умолчанию Classic, который будет проксировать запросы на ЕС2-инстансы TargetGroup, и на них, через NodePort Service, в поды.

На таком Load Balancer вы можете использовать TLS, менять его тип — Internal/External, и т.д, см. Other ELB annotations.

Обновляем манифест сервиса:

---
apiVersion: v1
kind: Service
metadata:
  name: "nginx-service"
  namespace: "default"
spec:
  ports:
    - port: 80
  type: LoadBalancer
  selector:
    app: "nginx"

Применяем:

kubectl apply -f nginx-svc.yaml
service/nginx-service configured

Проверяем:

kubectl get svc nginx-service
NAME            TYPE           CLUSTER-IP      EXTERNAL-IP                                                              PORT(S)        AGE
nginx-service   LoadBalancer   172.20.54.138   ac8415de24f6c4db9b5019f789792e45-443260761.us-east-2.elb.amazonaws.com   80:30968/TCP   21h

Ждём пару минут, пока обновится ДНС — и проверяем URL балансировщика:

curl ac8415de24f6c4db9b5019f789792e45-443260761.us-east-2.elb.amazonaws.com
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

Чего не позволяет такой тип сервиса — так это использовать какие-то правила роутинга, см. Application Load Balancer vs. Classic Load Balancer.

Собственно для того, что бы получить все возможности AWS Application Load Balancer — используем ещё один тип ресурса Kubernetes — Ingress, о нём — в Ingress.

Итак, LoadBalancer:

  • обеспечивает постоянный доступ к Сервису из мира
  • обеспечивает балансировку запросов к подам на разных EC2
  • даёт возможность использования TLS/SSL
  • не поддерживает Level-7 роутинг

ExternalName

Ещё один тип сервиса — ExternalName, который при обращении к нему перенаправит запрос на домен, указанный в параметре externalName:

---
apiVersion: v1
kind: Service
metadata:
  name: "google-service"
  namespace: "default"
spec:
  ports:
    - port: 80
  type: ExternalName
  externalName: google.com

Создаём:

kubectl apply -f nginx-svc.yaml
service/google-service created

Проверяем сервис:

kubectl get svc google-service
NAME             TYPE           CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
google-service   ExternalName   <none>       google.com    80/TCP    33s

И проверим его работу — зайдём в наш под с NGINX, и выполним dig:

root@nginx-554b9c67f9-rwbp7:/# dig google-service.default.svc.cluster.local +short
google.com.
172.217.8.206

Тут мы обращаемся к локальному DNS-имени сервиса google-service, которое разрезолвилось в IP домена google.com, которое мы указали в нашем externalName.

Ingress

По сути, Ingress не явлется отдельным сервисом — он просто описывает набор правил, по которым Ingress Controller выполняет действия по созданию Load Balancer, Listeners, и правила роутинга для них.

Официальная доументация — тут>>>.

В случае с AWS это будет ALB Ingress Controller — см. ALB Ingress Controller on Amazon EKS и AWS Elastic Kubernetes Service: запуск ALB Ingress controller.

При этом для Ingress требуется связанный с ним Service, на который Ingress будет перенаправлять трафик — его backend.

Для ALB Ingress Controller манифест с Ingress и связанный с ним Service будут выглядеть так:

---
apiVersion: v1
kind: Service
metadata:
  name: "nginx-service"
  namespace: "default"
spec:
  ports:
    - port: 80
  type: NodePort
  selector:
    app: "nginx"
--- 
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: "nginx-ingress"
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internet-facing
  labels:
    app: "nginx"
spec:
  backend:
    serviceName: "nginx-service"
    servicePort: 80

Тут мы создаём Service типа NodePort, и Ingress с типом ALB.

Kubernetes создаст Ingress объект, его увидит alb-ingress-controller, запустит создание AWS ALB с правилами роутинга, описанными в spec нашего Ingress, создаст Service объект типа NodePort, откроет на WorkeNodes TCP-порты, и начнёт редиректить трафик от клиентов — через балансировщик — на NodePort EC2 — через Service — к подам.

Проверим.

Сервис:

kubectl get svc nginx-service
NAME            TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
nginx-service   NodePort   172.20.54.138   <none>        80:30968/TCP   21h

Ingress:

kubectl get ingress nginx-ingress
NAME            HOSTS   ADDRESS                                                                  PORTS   AGE
nginx-ingress   *       e172ad3e-default-nginxingr-29e9-1405936870.us-east-2.elb.amazonaws.com   80      5m22s

И URL этого балансировщика:

curl e172ad3e-default-nginxingr-29e9-1405936870.us-east-2.elb.amazonaws.com
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

«It works!» (c)

Path-based routing

В примере выше мы роутим весь трафик с нашего ALB на единый Service и его поды.

Используя Ingress и его правила — мы можем описать поведение определяющее на какой бекенд-Service перенаправлять трафик в зависимости от, например, URI запроса.

Запустим два NGINX:

kubectl create deployment nginx-1 --image=nginx
deployment.apps/nginx-1 created
kubectl create deployment nginx-2 --image=nginx
deployment.apps/nginx-2 created

Создадим в них файлы:

kubectl exec nginx-1-75969c956f-gnzwv -- bash -c "echo svc-1 > /usr/share/nginx/html/sv1.html"
kubectl exec nginx-2-db55bc45b-lssc8 -- bash -c "echo svc-2 > /usr/share/nginx/html/svc2.html"

Обновим манифест — добавим ещё один сервис, а для Ingress — опишем два бекенда:

---
apiVersion: v1
kind: Service
metadata:
  name: "nginx-1-service"
  namespace: "default"
spec:
  ports:
    - port: 80
  type: NodePort
  selector:
    app: "nginx-1"
---
apiVersion: v1
kind: Service
metadata:
  name: "nginx-2-service"
  namespace: "default"
spec:
  ports:
    - port: 80
  type: NodePort
  selector:
    app: "nginx-2"    
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: "nginx-ingress"
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internet-facing
  labels:
    app: "nginx"
spec:
  rules:
  - http:
      paths:
      - path: /svc1.html
        backend:
          serviceName: "nginx-1-service"
          servicePort: 80
      - path: /svc2.html
        backend:
          serviceName: "nginx-2-service"
          servicePort: 80

Тут мы создаём два правила — при обращении по URI svc1.html или svc2.html — отправлять на nginx-1 или nginx-2 соотвественно.

Деплоим:

kubectl apply -f nginx-svc.yaml
service/nginx-1-service created
service/nginx-2-service created
ingress.extensions/nginx-ingress configured

Посмотрим правила:

kubectl describe ingress nginx-ingress
...
Rules:
Host  Path  Backends
----  ----  --------
*
/svc1.html   nginx-1-service:80 (<none>)
/svc2.html   nginx-2-service:80 (<none>)
...

Проверяем — запросим URI svc1.html и svc2.html:

curl e172ad3e-default-nginxingr-29e9-1405936870.us-east-2.elb.amazonaws.com/svc1.html
svc-1
curl e172ad3e-default-nginxingr-29e9-1405936870.us-east-2.elb.amazonaws.com/svc2.html
svc-2

Name-based routing

Другой пример — роутинг на основе имени хоста.

Создадим три имени — svc1.example.com, svc2.example.com, и просто svc.example.com, и через CNAME-записи направим их на наш URL балансировщика, созданного из нашего Ingress-ресурса.

Обновим манифест:

---
apiVersion: v1
kind: Service
metadata:
  name: "nginx-1-service"
  namespace: "default"
spec:
  ports:
    - port: 80
  type: NodePort
  selector:
    app: "nginx-1"
---
apiVersion: v1
kind: Service
metadata:
  name: "nginx-2-service"
  namespace: "default"
spec:
  ports:
    - port: 80
  type: NodePort
  selector:
    app: "nginx-2"    
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: "nginx-ingress"
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internet-facing
  labels:
    app: "nginx"
spec:
  rules:
  - host: "svc1.example.com"
    http:
      paths:
      - backend:
          serviceName: "nginx-1-service"
          servicePort: 80
  - host: "svc2.example.com"
    http:
      paths:
      - backend:
          serviceName: "nginx-2-service"
          servicePort: 80
  - http:
      paths:      
      - backend:
          serviceName: "nginx-1-service"
          servicePort: 80

Тут Service остаются без изменений, при обращении к svc1.example.com Ingress должен направить нас на Service-1, svc2.example.com — на Service-2, svc.example.com — на дефолтный бекенд, Service-1.

Проверяем:

curl svc1.example.com
svc-1
curl svc2.example.com
svc-2
curl svc.example.com
svc-1

И правила в Listener нашего Load Balancer в панели управления AWS:

В целом — на это всё.