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

Автор: | 04/06/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:

[simterm]

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

[/simterm]

Проверяем:

[simterm]

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

[/simterm]

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

[simterm]

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

[/simterm]

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

kubectl port-forward

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

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

[simterm]

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

[/simterm]

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

[simterm]

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

[/simterm]

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

[simterm]

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

[/simterm]

Окей, теперь, когда у нас есть работающий под – посмотрим, как к нему можно получить доступ через 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"

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

[simterm]

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

[/simterm]

Проверяем:

[simterm]

$ 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

[/simterm]

kubectl proxy и Service DNS

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

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

[simterm]

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

[/simterm]

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

[simterm]

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

[/simterm]

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

[simterm]

$ 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",
...

[/simterm]

Итак, 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.

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

[simterm]

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

[/simterm]

Проверяем:

[simterm]

$ 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

[/simterm]

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

[simterm]

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

[/simterm]

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

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

[simterm]

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

[/simterm]

Итак, 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"

Применяем:

[simterm]

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

[/simterm]

Проверяем:

[simterm]

$ 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

[/simterm]

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

[simterm]

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

[/simterm]

Чего не позволяет такой тип сервиса – так это использовать какие-то правила роутинга, см. 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

Создаём:

[simterm]

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

[/simterm]

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

[simterm]

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

[/simterm]

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

[simterm]

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

[/simterm]

Тут мы обращаемся к локальному 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 – к подам.

Проверим.

Сервис:

[simterm]

$ 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

[/simterm]

Ingress:

[simterm]

$ 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

[/simterm]

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

[simterm]

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

[/simterm]

“It works!” (c)

Path-based routing

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

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

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

[simterm]

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

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

[/simterm]

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

[simterm]

$ 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"

[/simterm]

Обновим манифест – добавим ещё один сервис, а для 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 соотвественно.

Деплоим:

[simterm]

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

[/simterm]

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

[simterm]

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

[/simterm]

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

[simterm]

$ 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

[/simterm]

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.

Проверяем:

[simterm]

$ curl svc1.example.com
svc-1

$ curl svc2.example.com
svc-2

$ curl svc.example.com
svc-1

[/simterm]

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

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