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

Thank you for reading this post, don't forget to subscribe!

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

Подготовка

Для тестов созда­дим деп­лой­мент с подом, в кото­ром запу­стим кон­тей­нер с 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

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

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

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

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

[codesyntax lang="php"]

[/codesyntax]

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

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-пор­та к поду на этой ноде.

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

[codesyntax lang="php"]

[/codesyntax]

Пара­метр 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.

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

[codesyntax lang="php"]

[/codesyntax]

При­ме­ня­ем:

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:

[codesyntax lang="php"]

[/codesyntax]

Созда­ём:

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 будут выгля­деть так:

[codesyntax lang="php"]

[/codesyntax]

Тут мы созда­ём 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!» ©

 

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 — опи­шем два бекенда:

[codesyntax lang="php"]

[/codesyntax]

Тут мы созда­ём два пра­ви­ла — при обра­ще­нии по 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.comsvc2.example.com, и про­сто svc.example.com, и через CNAME-запи­си напра­вим их на наш URL балан­си­ров­щи­ка, создан­но­го из наше­го Ingress-ресур­са.

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

[codesyntax lang="php"]

[/codesyntax]

Тут 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: