Translate

середа, 9 березня 2022 р.

Kubernetes. Part V: Configure And Use Ingress. MetalLB

Ця стаття вже п'ята з серії статей про Kubernetes. Перед її прочитанням рекомендую ознайомитись хоча б з базовими об'єктами кластеру та їх використанням для вирішення задач.

Сервіси Kubernetes надають можливість створення постійних точок входу до контейнерів додатків, що працюють в подах, проте IP адреси сервісів обираються з діапазону оверлейної мережі, і тому є видимими лише в межах кластеру. Тож у разі необхідності доступу до таких додатків ззовні існують наступні варіанти:

hostNetwork: true. Под, створений із такою опцією, матиме можливість бачити мережеві інтерфейси Kubernetes хосту, де його було запущено.

apiVersion: v1
kind: Pod
metadata:
  name: influxdb
spec:
  hostNetwork: true
  containers:
  - name: influxdb
    image: influxdb

Порт такого додатку буде прослуховуватись на всіх інтерфейсах вузла. Використання цієї опції не рекомендовано, адже в цьому випадку вичерпується простір портів хост-машини, що зі зростанням кількості працюючих додатків може з легкістю призвести до конфліктів. Більш того, у разі перестворення поду, він може "переселитись" на інший вузол, що додасть складності в його пошуках. Виключенням можуть слугувати поди утиліт, котрим дійсно необхідний вищезгаданий доступ задля управлінням мережею і т.п.

Задеплоїмо вищезгаданий поді та знайдемо на якому вузлі він оселився:

$ kubectl get pod -o wide
NAME      READY   STATUS    RESTARTS   AGE   IP            NODE   
influxdb   1/1     Running   0          38h   10.30.0.141   k8s-s-a


$ kubectl get nodes -o wide
NAME      STATUS   ROLES   AGE     VERSION   INTERNAL-IP
...
k8s-s-a   Ready    <none>  4d16h   v1.23.4   192.168.1.48


І перевіримо чи порт справді відкритий:

$ ssh 192.168.1.48
$ netstat -tulpn | grep influx
tcp        0      0 127.0.0.1:4222   0.0.0.0:*   LISTEN      949214/influxd
tcp6       0      0 :::8086          :::*        LISTEN      949214/influxd


hostPort: integer. Ця опція дозволяє поду активувати лише один порт на всіх інтерфейсах вузла Kubernetes. Yaml для створення такого поду буде виглядати наступним чином:

apiVersion: v1
kind: Pod
metadata:
  name: influxdb
spec:
  containers:
    - name: influxdb
      image: influxdb
      ports:
        - containerPort: 8086
          hostPort: 8086

Його, як і попередній варіант, дуже не рекомендовано використовувати по тим же причинам.

nodePort. Сервіс, створений з даною опцією, активує порт з діапазону 30000-32767 на всіх вузлах Kubernetes, який в свою чергу проксується на порт додатку. Такий сервіс і под виглядатимуть так:

kind: Service
apiVersion: v1
metadata:
  name: influxdb
spec:
  type: NodePort
  ports:
    - port: 8086
      nodePort: 30000
  selector:
    name: influxdb
---

apiVersion: v1
kind: Pod
metadata:
  name: influxdb
  labels:
    name: influxdb
spec:
  containers:
    - name: influxdb
      image: influxdb
      ports:
        - containerPort: 8086


Після створення цих об’єктів, порт 30000 вузла Kubernetes буде переадресований в 8086 порт поду.

Цього варіанту могло би бути достатньо у разі використання окремого балансувальника попереду кластера, але це зовсім не зручно: у разі, наприклад, додавання нового вузла Kuberenetes чи додатка списки балансувальника необхідно оновити власноруч. Було б чудово звести таку роботу до мінімуму.

LoadBalancer. Опція для сервісів Kubernetes, що має сенс на cloud-платформах AWS, Azure, CloudStack, GCE та OpenStack. Якщо описати сервіс наступним чином:

kind: Service
apiVersion: v1
metadata:
  name: influxdb
spec:
  type: LoadBalancer
  ports:
    - port: 8086
  selector:
    name: influxdb


Kubernetes створить випадковий NodePort на внутрішній адресі оверлейної мережі (ClusterIP), потім запитає створення балансувальника cloud-платформи із зовнішньою IP-адресою і вказаним портом. Це буде виглядати так:

$ kubectl get svc influxdb
NAME       CLUSTER-IP     EXTERNAL-IP     PORT(S)          AGE
influxdb   10.97.121.42   10.13.242.236   8086:30051/TCP   39s


Отже запит на IP-адресу балансувальника 10.13.242.236 і порт 8086 переадресується на внутрішню адресу кластера 10.97.121.42 і порт 30051, котрий вже після переадресується на адресу поду і порт додатка.

Мінусом цього способу є те, що, по-перше, bare-metal рішення та менш популярні cloud-провайдери не мають вбудованої підтримки функціональності LoadBalancer, а, по-друге, виділення окремої балансувальника на кожен додаток може коштувати не дешево.

• Ingress. Це ресурс кластеру Kubernetes, додатковий об’єкт, що є дещо більшим ніж попередні варіанти. Окрім адресації трафіку на кінцеві поди, Ingress також має функціонал virtual-хостингу (доступ до додатків Kubernetes по доменним іменам), балансування трафіку між подами, SSL-termination/sticky sessions та ін. Зупинимось детальніше на цьому варіанті.

Для роботи Ingress ресурсів необхідний відповідний контролер, який у разі bare-metal інсталяцій (така як наша з Kubespray чи Kubeadm) не встановлений по-замовчуванню. Є велика множина таких контролерів, серед яких хотілося б відзначити їх реалізації HAProxy, Traefik та Nginx (2 реалізації: одна від F5 Networks, а інша офіційна). У цій статті ми розглянемо лише останній варіант.

Як я вже згадував, по-замовчуванню bare-metal інсталяції не мають можливості створювати сервіси із типом LoadBalancer, адже Kubernetes її не надає. Але із цим може допомогти проект MetalLB. У такому разі LoadBalancer/ExternalIP ти сервісу забезпечить прямий доступ до додатку, не треба буде використовувати проброс порту (nodePort) на хост-ноду.

MetalLB буде також корисний для Ingress-інсталяції, адже це власне такий самий сервіс,що потребує точки входу. Кожен додаток, доступ до якого буде надаватись через Ingress, в кінцевому рахунку буде використовувати лише один LoadBalancer сервіс. Скористуймося Helm пакет для установки MetalLB:

$ curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
$ chmod 700 get_helm.sh
$ ./get_helm.sh


$ helm version
version.BuildInfo{Version:"v3.8.0", GitCommit:"d14138609b01886f544b2025f5000351c9eb092e", GitTreeState:"clean", GoVersion:"go1.17.5"}


Звісно, Helm можна установити багатьма способами.

$ helm upgrade --install metallb metallb \
               --repo https://metallb.github.io/metallb \
               --namespace metallb-system \
               --create-namespace

Опишемо конфігураціний файл для MetalLB та застосуємо його:

$ cat metallb_pool.yaml

apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: first-pool
  namespace: metallb-system
spec:
  addresses:
  - 192.168.1.70-192.168.1.80
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: example
  namespace: metallb-system

$ kubectl apply -f metallb_pool.yaml
ipaddresspool.metallb.io/first-pool created
l2advertisement.metallb.io/example created

Основне на що варто звернути увагу - це перелік адрес, що будуть використовуватись у якості LoadBalancer-ів для сервісів. Я його обрав із того ж діапазону, що використовував для установки вузлів Kubernetes, але можна описувати і декілька діапазонів. Кожна із цих адрес може бути виділена для нового сервісу.

Також доречно було б зупинитись на режимах роботи MetalLB, котрих є 2: Layer 2 та BGP (потребує роутера, що підтримує цей протокол).

Принцип роботи Layer 2 режиму MetalLB наступний. MetalLB у разі зміни вузла, на котрому лежать IP-адреси LoadBalancer-ів (чи створення нового LoadBalancer-а) буде надсилати широкомовні ARP анонси (gratuitous ARP messages), що відбулись зміни і всі вузли мережі (не лише Kubernetes-вузли), виходячи із отриманих анонсів, будуть змінювати власний локальний ARP-кеш (таблиця відповідністі MAC/Ethernet та IP адрес). Тобто ARP таблиця кожного вузла буде динамічно перебудовуватись, у разі міграції адреси на інший воркер, наприклад, через падіння попереднього. Одна із адрес вказаного діапазону для LoadBalancer сервісу буде призначена на один із вузлів/воркерів Kubernetes, але не у якості IP-адреси на додатковий інтерфейс (як у випадку Corosync/Pacemaker/Heartbeat), а вузол буде просто відповідати на ARP-запити із необхідною MAC-адресою (за допомогою MetalLB speaker, що присутній на кожній ноді).

Для всіх наступних інструкцій я буду використовував вже існуючий Kubernetes кластер версії 1.23.4, котрий я установив за допомогою kubeadm:

$ kubectl get nodes
NAME      STATUS   ROLES                  AGE     VERSION
k8s-m-a   Ready    control-plane,master   4d15h   v1.23.4
k8s-s-a   Ready    <none>                 4d14h   v1.23.4
k8s-s-b   Ready    <none>                 4d13h   v1.23.4

Скористаймося офіційним helm-пакетом для установки MetalLB:

$ kubectl get all -n metallb-system
NAME                                      READY   STATUS    RESTARTS   AGE
pod/metallb-controller-777cbcf64f-m76qh   1/1     Running   0          43s
pod/metallb-speaker-6k6vq                 1/1     Running   0          43s
pod/metallb-speaker-f5t76                 1/1     Running   0          43s
pod/metallb-speaker-gqxkj                 1/1     Running   0          43s

NAME                             DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR            AGE
daemonset.apps/metallb-speaker   3         3         3       3            3           kubernetes.io/os=linux   43s

NAME                                 READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/metallb-controller   1/1     1            1           43s

NAME                                            DESIRED   CURRENT   READY   AGE
replicaset.apps/metallb-controller-777cbcf64f   1         1         1       43s


Тепер проінсталюємо Ingress:

$ helm upgrade --install ingress-nginx ingress-nginx \
               --repo https://kubernetes.github.io/ingress-nginx \
               --namespace ingress-nginx \
               --create-namespace


Перевіримо чи необхідні ресурси створились:

# kubectl get all -n ingress-nginx
NAME                                            READY   STATUS    RESTARTS   AGE
pod/ingress-nginx-controller-5b6f946f99-62hq2   1/1     Running   0          50s

NAME                                         TYPE           CLUSTER-IP       EXTERNAL-IP    PORT(S)                      AGE
service/ingress-nginx-controller             LoadBalancer   10.109.253.43    192.168.1.70   80:31543/TCP,443:30438/TCP   50s
service/ingress-nginx-controller-admission   ClusterIP      10.105.215.196   <none>         443/TCP                      50s

NAME                                       READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/ingress-nginx-controller   1/1     1            1           50s

NAME                                                  DESIRED   CURRENT   READY   AGE
replicaset.apps/ingress-nginx-controller-5b6f946f99   1         1         1       50s


Тут особливо має зацікавити адреса Ingress контролера 192.168.1.70, котру забезпечив MetalLB. Важливо одразу зрозуміти, що MetalLB - це не балансувальник в класичному розумінні цього слова, адже трафік буде надходити лише на один із вузлів кластеру, котрий буде відповідати за цю адресу, і лише потім він буде переадресований на сервіс, де і буде відбуватись його розподіл по кінцевим подам.

Наразі створимо якийсь сервіс із використанням Ingress:


$ vim app1.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app1
spec:
  replicas: 2
  selector:
    matchLabels:
      app: app1
  template:
    metadata:
      labels:
        app: app1
    spec:
      containers:
      - name: app1
        image: dockersamples/static-site
        env:
        - name: AUTHOR
          value: app1
        ports:
        - containerPort: 80
---

apiVersion: v1
kind: Service
metadata:
  name: appsvc1
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: app1

---

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app1-ingress
spec:
  rules:
  - host: app1.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: appsvc1
            port:
              number: 80
  ingressClassName: nginx


$ kubectl apply -f app1.yaml

$ kubectl get ing

NAME             CLASS   HOSTS              ADDRESS        PORTS   AGE
app1-ingress     nginx   app1.example.com   192.168.1.70   80      86s


Тепер якщо прив'язати домен app1.example.com до 192.168.1.70, наприклад через hosts, в браузері відкриється відповідна сторінка:



Із TLS-з'єднанням все приблизно так само. Всі ресурси окрім ingress виглядатимуть аналогічно:

$ vim app2.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app2
spec:
  replicas: 2
  selector:
    matchLabels:
      app: app2
  template:
    metadata:
      labels:
        app: app2
    spec:
      containers:
      - name: app2
        image: dockersamples/static-site
        env:
        - name: AUTHOR
          value: app2
        ports:
        - containerPort: 80
---

apiVersion: v1
kind: Service
metadata:
  name: appsvc2
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: app2

---

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app2-ingress
spec:
  tls:
    - hosts:
      - app2.example.com
      # This assumes tls-secret exists and the SSL
      # certificate contains a CN for app2.example.com
      secretName: tls-secret
  ingressClassName: nginx

  rules:
    - host: app2.example.com
      http:
        paths:
        - path: /
          pathType: Prefix
          backend:
            # This assumes appsvc2 exists and routes to healthy endpoints
            service:
              name: appsvc2
              port:
                number: 80


Але також треба завантажити сертифікат-ключ, на який ми щойно зробили посилання:

$ kubectl create secret tls tls-secret --key tls.key --cert tls.crt

Для генерації самопідписного сертифікату можна використати наступну команду (якщо це необхідно):

$ openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -keyout tls.key -out tls.crt -subj "/CN=*.example.com/O=nginxsvc"

Власне все, можна запускати створення нового сервісу і інгреса для нього:

$ kubectl apply -f app2.yaml

$ kubectl get ing

NAME             CLASS   HOSTS              ADDRESS        PORTS     AGE
...
app2-ingress     nginx   app2.example.com   192.168.1.70   80, 443   3h12m


Як і минулого разу новий домен також треба прив'язати до ExternalIP Ingress-контролера, після чого можна спостерігати роботу додатка:




Перевіривши локальний arp-кеш, можна дійсно пересвідчитись у тому на який вузол вказує 192.168.1.70:

$ arp -n
Address                  HWtype  HWaddress           Flags Mask            Iface
192.168.1.1              ether   cc:5d:4e:4e:4a:78   C                     wlp0s20f3
192.168.1.70             ether   08:00:27:38:70:31   C                     wlp0s20f3
192.168.1.45             ether   08:00:27:38:70:31   C                     wlp0s20f3
192.168.1.41             ether   b8:70:f4:ad:28:f1   C                     wlp0s20f3


Отже, виявляється цей IP обслуговує мережева карта вузла, що у моєму випадку виступає як control plane.

Також можемо переглянути nginx-конфігураційний файл, котрим оперує Ingress:

$ kubectl get pods -n ingress-nginx
NAME                                        READY   STATUS    RESTARTS   AGE
ingress-nginx-controller-5b6f946f99-62hq2   1/1     Running   0          3d

$ kubectl exec ingress-nginx-controller-5b6f946f99-62hq2 -n ingress-nginx -- cat /etc/nginx/nginx.conf


Так виглядає секція для нашого домену app2.example.com:

server {
        server_name app2.example.com ;
        
        listen 80  ;
        listen 443  ssl http2 ;
        
        set $proxy_upstream_name "-";
        
        ssl_certificate_by_lua_block {
                certificate.call()
        }
        
        location / {
                
                set $namespace      "default";
                set $ingress_name   "app2-ingress";
                set $service_name   "appsvc2";
                set $service_port   "80";
                set $location_path  "/";
                set $global_rate_limit_exceeding n;
                
                rewrite_by_lua_block {
                        lua_ingress.rewrite({
                                force_ssl_redirect = false,
                                ssl_redirect = true,
                                force_no_ssl_redirect = false,
                                preserve_trailing_slash = false,
                                use_port_in_redirects = false,
                                ...
                        })
                        balancer.rewrite()
                        plugins.run()
                }
                
                ...
                proxy_pass http://upstream_balancer;
                
                proxy_redirect                          off;
                
        }
}


Це власне все, про що хотілось написати. Надіюсь Ingress в Kubernetes уже набув певної стабільності і необхідності знову переписувати всю статтю не буде.

Посилання:
https://habr.com/en/company/southbridge/blog/358824/
https://kubernetes.io/docs/concepts/services-networking/ingress/
https://alesnosek.com/blog/2017/02/14/accessing-kubernetes-pods-from-outside-of-the-cluster/
https://kubernetes.github.io/ingress-nginx/
https://kubernetes.github.io/ingress-nginx/examples/tls-termination/
https://kubernetes.github.io/ingress-nginx/deploy/
https://kubernetes.github.io/ingress-nginx/deploy/baremetal/
https://itnext.io/configuring-routing-for-metallb-in-l2-mode-7ea26e19219e
https://metallb.universe.tf/
https://metallb.universe.tf/installation/

Немає коментарів:

Дописати коментар