Для сетевого взаимодействия 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 – обзор
Рассмотрим типы кратко, а потом перейдём к примерам:
ClusterIP
: дефолтный тип, создаёт сервис с IP из пула внутренних адресов кластера, такой сервис будет доступен только внутри кластера (либо черезkube-proxy
)NodePort
: открывает TCP порт на каждой WorkerNode EС2, “за ним” автоматом создаётClusterIP
Service, и роутит трафик с порта ЕС2 на этотClusterIP
– такой сервис будет доступен из мира (если EC2 имеют публичные адреса), либо только внутри VPCLoadBalancer
: создаёт внешний Load Balancer (AWS Classic LB), “за ним” автоматом создаётNodePort
, за ним автоматомClusterIP
, и таким образом роутит трафик от Load Balancer к поду в кластере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: