Привет всем,

Как уже стало понятным из описания, в этой заметке разбираемся с предоставлением доступа для внешних клиентов на сервисы запущенные в кластере.

В качестве тестового стенда у меня поднят кластер из 3х нод. Две ноды в роли воркеров, и одна нода в роли мастер-ноды.

Создаем деплоймент

Для тестирования доступов, предварительно развернем тестовое приложение. В нашем случаи это будем обычный деплоймент, который поднимает 3 экземпляра nginx. При запросах на веб-сервера будет отдаваться ответ с текущем именем пода.

Для начала пишем конфиг для nginx:

[root@k8s-node1 nginx-app]# cat nginx-app.conf
---
server {
    listen 80 default_server;
    server_name _;

    default_type text/plain;
    location / {
        return 200 'Hi from pod: $hostname.\n';
    }
}

Как видно из описания, у нас есть единственный локейшен, при попадании на который будет отдаваться 200 http-код с текстовым приветом.

Сохраняемся, и создаем новый конфигмапу из файла:

[root@k8s-node1 nginx-app]# kubectl create cm nginx-conf --from-file=./nginx-app.conf

Далее напишем новый деплоймент:

[root@k8s-node1 nginx-app]# vim nginx-app-dp.yml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-ing
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx-ing
  strategy:
    rollingUpdate:
      maxSurge: 30%
      maxUnavailable: 30%
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: nginx-ing
    spec:
      containers:
      - image: nginx:1.20
        name: nginx
        ports:
        - containerPort: 80
        readinessProbe:
          failureThreshold: 3
          successThreshold: 1
          periodSeconds: 10
          timeoutSeconds: 3
          httpGet:
            port: 80
            path: /
        livenessProbe:
          failureThreshold: 3
          successThreshold: 1
          periodSeconds: 10
          timeoutSeconds: 3
          httpGet:
            port: 80
            path: /
        resources:
          requests:
            cpu: 200m
            memory: 250Mi
          limits:
            cpu: 200m
            memory: 250Mi
        volumeMounts:
          - name: nginx-conf
            mountPath: /etc/nginx/conf.d/
      volumes:
      - name: nginx-conf
        configMap:
          name: nginx-conf

Как можно понять из деплоймента, поднимается 3 экземпляра с nginx версией 1.20. В контексте volumes, подключаем ранее созданный конфигмап. Затем монтируем его во внутрь пода.

Применяем наш деплоймент к кластеру:

[root@k8s-node1 nginx-app]# kubectl apply -f nginx-app-dp.yml
deployment.apps/nginx-ing created 

Проверяем статус нашего деплоймента:

[root@k8s-node1 nginx-app]# kubectl get po
NAME                         READY   STATUS    RESTARTS   AGE
nginx-ing-7944c5499c-46529   1/1     Running   0          11s
nginx-ing-7944c5499c-7dmdd   1/1     Running   0          11s
nginx-ing-7944c5499c-7m2s6   1/1     Running   0          11s

Отлично, наше импровизированное приложение поднято.

Доступ через NodePort

Самый простой и быстрый способ открыть доступ на приложение, можно реализовать через NodePort сервис. При создании такого типа сервиса на всех нодах открывается рандомный порт, в диапазоне 30000-32767. За счет NAT-преобразований, наши запросы на выделенные порт пробрасываются на внутренний порт нашего сервиса.

Давайте напишем манифест для NodePort:

[root@k8s-node1 nginx-app]# vim nginx-app-nodeport.yml
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-svc-front
spec:
  selector:
    app: nginx-ing
  ports:
  - port: 80
    nodePort: 30080
    name: http
  type: NodePort

Из основного, в спецификации сервиса мы указываем селектр, по которому будут определены наши end-поинты. Следом описываем порт, в данном случаи мы биндим статический порт 30080. Запросы на этот порт будут проксироваться на 80 порт подов.

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

[root@k8s-node1 nginx-app]# kubectl apply -f nginx-app-nodeport.yml
service/nginx-svc-front created

Пролистим все сервисы:

[root@k8s-node1 nginx-app]# kubectl get svc
NAME              TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
kubernetes        ClusterIP   10.233.0.1      <none>        443/TCP        2d3h
nginx-svc-front   NodePort    10.233.51.104   <none>        80:30080/TCP   18s

Курлом проверим работу нашего сервиса:

[root@k8s-node1 nginx-app]# kubectl get po
NAME                         READY   STATUS    RESTARTS   AGE
nginx-ing-7944c5499c-46529   1/1     Running   0          130m
nginx-ing-7944c5499c-7dmdd   1/1     Running   0          130m
nginx-ing-7944c5499c-7m2s6   1/1     Running   0          130m
---
[root@k8s-node1 nginx-app]# curl http://10.200.70.11:30080
Hi from pod: nginx-ing-7944c5499c-7dmdd.
[root@k8s-node1 nginx-app]# curl http://10.200.70.11:30080
Hi from pod: nginx-ing-7944c5499c-7m2s6.
[root@k8s-node1 nginx-app]# curl http://10.200.70.11:30080
Hi from pod: nginx-ing-7944c5499c-46529.

В curl-запросе я указываю ip воркера вместе с портом 30080.

На схеме все будет устроено так: k8s-cluster2.png Все же не рекомендуется использовать в продакшене такой способ доступа к нашим приложением. Так как на один порт мы можем повесить только один сервис, и в случаи если мы открывать доступ на множество наших сервисов, получается вагон и маленькая тележка множества портов, которые дают нам много потенциальных уязвимостей.

Хотя как временное решение или для тестов такой вариант годится. Также хочу отметить, что некоторые таким способом открывают доступ к кластеру монги или к кластеру ДБ.

Доступ через Ingress

Подробно про ингрессы мы разбирались в прошлых заметках, все эксперименты там проделывались в разрезе minicube. В этой итерации будет также использовать nginx ингресс контроллер, давайте его заинсталим:

[root@k8s-node1 nginx-app]# kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.8.1/deploy/static/provider/baremetal/deploy.yaml

И проверяем состояние подов:

[root@k8s-node1 nginx-app]# kubectl get po -n ingress-nginx
NAME                                        READY   STATUS      RESTARTS   AGE
ingress-nginx-admission-create-s2w5m        0/1     Completed   0          2m19s
ingress-nginx-admission-patch-k46dr         0/1     Completed   0          2m19s
ingress-nginx-controller-5c778bffff-7gkmw   1/1     Running     0          2m19s

В документации описывается несколько способов использовая nginx-контроллера, рассмотрим распространненые случаи это через использование NodePort сервис или же дополнительный компонент Metallb.

Ingress через NodePort

Для доступа к нашему деплойменту со стороны ingress, создадим новый сервис типа - ClusterIP. Который используется как правило, по умолчанию. Когда одному нашему сервису нужно дать доступ к другому сервису, в разрезе только одного кластера.

Пишем деплоймент сервиса:

[root@k8s-node1 nginx-app]# vim nginx-app-clusterip.yml
---
apiVersion: v1
kind: Service
metadata:
  name:
    nginx-app-svc
spec:
  selector:
    app: nginx-ing
  ports:
  - port: 80
    targetPort: 80
    name: http
  type: ClusterIP

Затем применяем манифест:

[root@k8s-node1 nginx-app]# kubectl apply -f nginx-app-clusterip.yml

Думаю из этого манифеста понятно, что мы также выбираем по селектору наши поды и пробрасываем внешний 80 порт во внутрь пода.

Теперь создадим манифест ингресс контроллера:

[root@k8s-node1 nginx-app]# vim nginx-app-ingress.yml
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-nginx-app
spec:
  rules:
  - host: nginx-service.local
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: nginx-app-svc
            port:
              number: 80
  ingressClassName: nginx

В данном манифесте мы описываем правило при обращении на хост - nginx-service.local. Все обращения будут отправлены на ClusterIP сервис - nginx-app-svc.

Листим объекты ингресов:

[root@k8s-node1 nginx-app]# kubectl get ing
NAME            CLASS    HOSTS                 ADDRESS   PORTS   AGE
nginx-ingress   nginx   nginx-service.local             80      30s

На этом все, можем проверять доступность нашего сервиса. При установке ingress-контролера, сервис NodePort будет создан из манифеста автоматом.

Смотрим на какие порты можем подключиться:

[root@k8s-node1 nginx-app]# kubectl get svc -n ingress-nginx
NAME                                 TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)                      AGE
ingress-nginx-controller             NodePort    10.233.33.49   <none>        80:30210/TCP,443:30416/TCP   157m
ingress-nginx-controller-admission   ClusterIP   10.233.24.42   <none>        443/TCP                      157m

Из вывода понятно, что http поднят на порту 30210.

Теперь мы можем постучаться на любую ноду:

[root@k8s-node1 nginx-app]# curl http://10.200.70.13:30210/ -H 'Host: nginx-service.local'
Hi from pod: nginx-ing-7944c5499c-7m2s6.

[root@k8s-node1 nginx-app]# curl http://10.200.70.12:30210/ -H 'Host: nginx-service.local'
Hi from pod: nginx-ing-7944c5499c-46529.

Отлично все работает.

Использование подобной схемы, подразумевает что у вас есть какие-то внешние балансировщики. И имеет место быть такое мнение, что использование ingress поверх nodeport привносит дополнительный оверхед за счет NAT-преобразований.

На обновленной схеме, взаимодействие объектов будет таким: k8s-cluster3.png

Ingress + Metallb

Доступ к сервису можно предоставить за счет установки встроенного в кластер балансировщика - MetalLB. В режиме работы L2, Metallb решает проблему предоставления доступа к сервисам из локальной сети, путем назначения ноде дополнительного ip-адреса и привязки его к сервису.

В нашем текущем кейсе, мы будем предоставлять доступ к ingress-y. Для его нужно:

  • Установить в кластер Metallb,
  • Создать ресурсы - IPAddressPool и L2Advertisement, которые определяют пулл ip-адресов, и режим работы Metallb.
  • Ну и создать сервис типа - Loadbalancer.

Если же у вас kube-proxy работает в режиме ipvs, нужно в его настройках разрешить ARP.

[root@k8s-node1]# kubectl get configmap kube-proxy -n kube-system -o yaml | sed -e "s/strictARP: false/strictARP: true/" | kubectl apply -f - -n kube-system

Далее применяем манифест:

[root@k8s-node1]# kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.13.10/config/manifests/metallb-native.yaml

Создаем объекты, описывающие режим работы MetalLB и адресный пул:

[root@k8s-node1]# vim metall-lb.yml
---
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: default
  namespace: metallb-system
spec:
  addresses:
  - 10.200.70.211-10.200.70.213
  autoAssign: true
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: default
  namespace: metallb-system
spec:
  ipAddressPools:
  - default

В объекте IPAddressPool, я определяю пулл с именем default и маленьким адресным пулом с тремя IP (10.200.70.211-10.200.70.213). А объект L2Advertisement опреляет режим работы MetalLB, и можете обратить внимание что в спецификации указывается какой использовать адресный пулл. Применяем манифест:

[root@k8s-node1]# kubectl apply -f metall-lb.yml

Теперь остается создать новый сервис типа - LoadBalancer, с указанием на ingress:

[root@k8s-node1]# vim loadbalancer.yml
---
kind: Service
apiVersion: v1
metadata:
  name: ingress-nginx-lb
  namespace: ingress-nginx
  annotations:
    metallb.universe.tf/address-pool: default
spec:
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: 80
    - name: https
      protocol: TCP
      port: 443
      targetPort: 443
  selector:
    app.kubernetes.io/component: controller
    app.kubernetes.io/instance: ingress-nginx
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
  type: LoadBalancer

Из особеностей здесь в аннотациях указываем какой адресный пулл использоваться - metallb.universe.tf/address-pool: default. Далее уже знакомые нам спецификации, описывающие порты. В селекторе описываем объекты ingress-nginx.

Применяем манифест, и смотрим:

[root@k8s-node1]# kubectl apply -f loadbalancer.yml
[root@k8s-node1]# kubectl -n ingress-nginx get svc
NAME                                 TYPE           CLUSTER-IP     EXTERNAL-IP     PORT(S)                      AGE
ingress-nginx-controller             NodePort       10.233.33.49   <none>          80:30210/TCP,443:30416/TCP   25h
ingress-nginx-controller-admission   ClusterIP      10.233.24.42   <none>          443/TCP                      25h
ingress-nginx-lb                     LoadBalancer   10.233.31.1    10.200.70.211   80:31518/TCP,443:31822/TCP   19s

Как можете понять из вывода, на сервис с именем ingress-nginx-lb присвоился ip - 10.200.70.211. По мимо основных портов, на этот сервис вещаются nodePort порты. (Одна из особеностей)

На обновленной схеме взаимосвязь отобразится так: k8s-cluster4.png

Для проверки можно также курлой постучатся на этот ip, c указанием поля host. И собственно задача решена.