Ранее для доступа к нашему поду, мы через утилиту kubectl реализовывали проброс портов во внутрь пода. Эта история хоть и работает, но вообще не годиться для ежедневной эксплуатации. Так как состояние пода эфемерно, то есть под может сломаться или перезапуститься, изчезнуть или появиться на другой ноде. Соответствено, kubernetes назначает поду новый ip, и клиенты нашего приложения даже не догадываются об изменениях. В дополнение у нашего приложения может быть несколько его реплик.

В данном топике, мы рассмотрим решение этих проблем через использование ресурса - Service.

Сервис объединяет несколько подов в единую группу, при помощи уже известного нам механизма - селекторов и меток, и образует так называемую точку входа (endpoint). Проще говоря сервис выбирает на какие поды перенаправлять трафик, используя селекторы и метки. Клиенты спокойно ходят на выделенный ip и порт, при этому даже не догадываясь о колличестве реплик и адресации подов.

k8s-svc-schemes.png (Схема взаимосвязи подов и сервиса по меткам.)

Важно отметить, что назначение сервисов это предоставление доступа к группе подов, для других подов. Созданная точка входа будет доступна только изнутри кластера.

Перейдем к практике, давайте создадим деплоймент состоящий из трех реплик nginx. В контейнер будет монтированится конфигмепа с конфигурацией nginx. Начнем с создание конфигурации nginx. У нас будет простенький веб-сервер, который на все запросы будет отвечать своим hostname.

server {
	listen 8080;
	server_name _;

	default_type text/plain;

	location / {
		return 200 'Hello, I am $hostname\n';
	}
}

Создадим новую конфигмапу через kubectl:

$ kubectl create cm nginx-svc-conf --from-file=./nginx-svc.conf

Теперь напишем новый деплоймент на запуск трех реплик c nginx:

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-svc-dp
spec:
  replicas: 3
  selector:
    matchLabels:
      front: nginx-svc
  strategy:
    rollingUpdate:
      maxSurge: 3
      maxUnavailable: 3
    type: RollingUpdate
  template: 
    metadata: 
      labels:
        front: nginx-svc
    spec:
      containers:
      - image: nginx:1.22
        name: nginx
        ports:
        - containerPort: 8080
        readinessProbe:
          failureThreshold: 3
          successThreshold: 1
          httpGet:
            port: 8080
            path: /
          periodSeconds: 15
          timeoutSeconds: 5
        livenessProbe:
          failureThreshold: 3
          successThreshold: 1
          httpGet:
            port: 8080
            path: /
          periodSeconds: 15
          timeoutSeconds: 5
        resources:
          requests:
            cpu: 250m
            memory: 250Mi
          limits:
            cpu: 250m
            memory: 250Mi
        volumeMounts:
          - name: nginx-conf
            mountPath: /etc/nginx/conf.d/
      volumes:
      - name: nginx-conf
        configMap:
          name: nginx-svc-conf

Применим манифест к кластеру, и смотрим состояние подов.

$ kubectl apply -f nginx-deployment.yml
$ kubectl get po

Теперь напишем манифест для сервиса, откроем доступ к нашему деплойменту:

apiVersion: v1
kind: Service
metadata:
  name: nginx-front-service
spec:
  selector:
    front: nginx-svc
  ports:
  - port: 80
    targetPort: 8080
  type: ClusterIP

Отметим что версия api изменилась, так же тип используемого объекта изменился на Service. В поле спецификации (spec), как говорилось ранее, указываем selector по которому сервис будет определять поды относящиеся к его группе. Далее описываются порты, указывается порт нашего сервиса (на каком порту принимает подключения) и таргет порт который слушается нашими подами. И в конце указывается тип сервиса, в нашем случаи это ClusterIP, как раз таки этот тип открывает доступ к сервису внутри кластера, по внетреннему IP-адресу.

Применяем манифест к кластеру:

$ kubectl apply -f nginx-svc.yml
--
service/nginx-front-service created

Отлично, теперь мы можем пролистить все созданные объекты и посмотрим их зависимость друг от друга. Листим сервис:

$ kubectl get svc nginx-front-service -o wide
NAME                  TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE   SELECTOR
nginx-front-service   ClusterIP   10.100.95.219   <none>        80/TCP    10m   front=nginx-svc

Как видим, наш сервис слушает кластерный ip - 10.100.95.219 на порту 80. И балансит весь трафик на поды с меткой - front=nginx-svc.

После добалении нового сервиса, кубернетес создает еще один объект - endpoint. Этот объект является некой абстрактной прослойкой между сервисами и подами. Далее куб, находит все поды по лейблу, и включает или исключает их из некого списка (Все зависит от состояния пода, и результата выполнения Readiness пробы). В случаи, если мы создаем сервис без указания селектора, endpoint не будет создан автоматически.

Если пролистим созданный endpoint, то увидем адреса наших подов

$ kubectl get ep 
NAME                  ENDPOINTS                                         AGE
nginx-front-service   172.17.0.3:8080,172.17.0.4:8080,172.17.0.7:8080   11h

Теперь выведем список подов с этим лейблом.:

$ kubectl get po --selector front=nginx-svc -o wide
NAME                            READY   STATUS    RESTARTS      AGE     IP           NODE       NOMINATED NODE   READINESS GATES
nginx-svc-dp-687bd7956b-qlbkk   1/1     Running   1 (92s ago)   11h     172.17.0.7   minikube   <none>           <none>
nginx-svc-dp-687bd7956b-vjlgc   1/1     Running   1 (92s ago)   11h     172.17.0.4   minikube   <none>           <none>
nginx-svc-dp-687bd7956b-w9jfn   1/1     Running   1 (92s ago)   11h     172.17.0.3   minikube   <none>           <none>

Как видно адрес каждого пода соответствует списку в endpoints.

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

$ kubectl run -ti --rm --image centosadmin/utils test bash
--
bash-5.0# curl http://10.100.95.219
I am nginx-svc-dp-687bd7956b-w9jfn
bash-5.0# curl http://10.100.95.219
I am nginx-svc-dp-687bd7956b-qlbkk
bash-5.0# curl http://10.100.95.219
I am nginx-svc-dp-687bd7956b-w9jfn
bash-5.0# curl http://10.100.95.219
I am nginx-svc-dp-687bd7956b-vjlgc

На мои запросы ответ прилетает каждый раз от нового пода.

В случаи если мы создаем новый сервис, без указания селекторов, то объект endpoints не будет создан автоматом. Нам придется в ручным спобосом запускать точку входа. Такой поинт может быть полезен, когда мы хотим открыть доступ на ресурс вне кластера. Запустить еще один сервис без указания меток:

---    
apiVersion: v1
kind: Service
metadata: 
  name: proxy-example
spec:
  ports:
  - port: 80
    targetPort: 80 

Применяем манифест.

Если же мы пролистим список endpoints, то не найдет точку с аналогичным именем нашего сервиса. Как ранее отмечалось, сервисы без селекторов не создают endpoint. Добавим новый endpoint, в качестве target-адреса укажим сторонний сайт:

---
apiVersion: v1
kind: Endpoints
metadata:
  name: proxy-example
subsets:
  - addresses:
    - ip: 176.126.166.193
    ports:
      - port: 80

Поднимаем тестовый под и стучимся на сервис:

$ kubectl run -ti --rm --image centosadmin/utils test bash
---
bash-5.0# curl http://176.126.166.193
<html>
<head><title>301 Moved Permanently</title></head>
<body>
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx/1.20.1</center>
</body>
</html>

Ссылки на ресурсы, дополнительно: