Istio: интеграция Ingress Gateway с AWS Application LoadBalancer

Автор: | 26/03/2021
 

В предыдущем посте – Istio: обзор и запуск service mesh в Kubernetes – запустили Istio в AWS Elastic Kubernetes Service, познакомились с основными компонентами.

Следующая задача – добавить AWS Application Load Balancer (ALB) перед Istio Inrgress Gateway, так как Istio Gateway Service с типом LoadBalancer создаёт AWS Classic LoadBalancer, к которому можно подключить только один SSL-сертификат из Amazon Certificate Manager.

Сейчас наша схема прохождения трафика следующая:

  • в Helm-чарте каждого приложения описываются Ingress и Service, этот Ingress создаёт AWS Application LoadBalancer с SSL
  • пакет с ALB попадает на Service приложения
  • через Service приложения – в под с приложением

Теперь добавим сюда Istio.

Идея такая:

  • устанавливаем Istio, он создаёт Istio Ingress Gateway – Service и Pod
  • в Helm-чарте приложения описываются Ingress, Service плюс Gateway и VirtualService
  • Ingress приложения создаёт ALB, тут продолжаем выполнять SSL termination – дальше трафик внутри кластера идёт обычным HTTP
  • пакет с ALB попадает на под с Istio Ingress Gateway
  • с Istio Ingress Gateway в соответствии с настройками Envoy, описанными в Gateway и VirtualService приложения, пакет идёт на Service приложения
  • через Service приложения – попадает в под с приложением

Для этого нам надо:

  1. описать Ingress, который создаст ALB
  2. обновить Service для Istio Ingress Gateway – вместо типа LoadBalancer задать ему тип NodePort, что бы AWS ALB Ingress Controller создал TargetGroup, на которую будет слать трафик ALB
  3. задеплоить тестовое приложение с обычным Service
  4. для тестового приложения описать Gateway и VirtualService, которые настроят Envoy нашего Istio Ingress Gateway на роутинг трафика к Сервису приложения

Поехали.

Обновление Istio Ingress Gateway

Istio у нас уже установлен в предыдущей статье – тут уже есть Istio Ingress Gateway сервисом с типом LoadBalancer:

[simterm]

$ kubectl -n istio-system get svc istio-ingressgateway
NAME                   TYPE           CLUSTER-IP       EXTERNAL-IP                             PORT(S)                                                                      AGE
istio-ingressgateway   LoadBalancer   172.20.112.213   a6f***599.eu-west-3.elb.amazonaws.com   15021:30218/TCP,80:31246/TCP,443:30486/TCP,15012:32445/TCP,15443:30154/TCP   25h

[/simterm]

Теперь надо его перенастроить – изменить тип Service на NodePort – обновляем через istioctl и --set:

[simterm]

$ istioctl install --set profile=default --set values.gateways.istio-ingressgateway.type=NodePort -y
✔ Istio core installed
✔ Istiod installed
✔ Ingress gateways installed
✔ Installation complete

[/simterm]

Проверяем Service:

[simterm]

$ kubectl -n istio-system get svc istio-ingressgateway
NAME                   TYPE       CLUSTER-IP       EXTERNAL-IP   PORT(S)                                                                      AGE
istio-ingressgateway   NodePort   172.20.112.213   <none>        15021:30218/TCP,80:31246/TCP,443:30486/TCP,15012:32445/TCP,15443:30154/TCP   25h

[/simterm]

NodePort, хорошо – тут готово.

Istio Ingress Gateway и AWS Application LoadBalancer healthchecks

Дальше возникает вопрос – как выполнять Health checks на AWS Application LoadBalancer, ведь у Istio Ingress Gateway используются разные порты – 80 для входящего трафика, и 15021 для проверки статуса?

Если в Ingress указать аннотацию alb.ingress.kubernetes.io/healthcheck-port – то ALB Ingress Controller вообще игнорирует этот Ingress, и хоть  бы что-то в логах написал! Нет, ничего – полная тишина и игнор. Ingress создаётся, а AWS LoadBalancer – нет.

Решение нагуглилось на Github – Health Checks do not work if using multiple pods on routes: выносим аннотации, связанные с healthcheck, в Service самого Istio Gateway.

Редактируем Service istio-ingressgateway:

[simterm]

$ kubectl -n istio-system edit svc istio-ingressgateway

[/simterm]

В spec.ports находим status-port и его nodePort:

...
spec:
  clusterIP: 172.20.112.213
  externalTrafficPolicy: Cluster
  ports:
  - name: status-port
    nodePort: 30218
    port: 15021
    protocol: TCP
    targetPort: 15021
...

Для указания alb.ingress.kubernetes.io/alb.ingress.kubernetes.io/healthcheck-path – берём readinessProbe из  Deployment-а, который создаёт поды с istio-ingressgateway:

[simterm]

$ kubectl -n istio-system get deploy istio-ingressgateway -o yaml
...
        readinessProbe:
          failureThreshold: 30
          httpGet:
            path: /healthz/ready
...

[/simterm]

Задаём аннотации для istio-ingressgateway Service – в healthchek-port указываем nodePort из status-port, а в healthcheck-path – путь из readinessProbe:

...
    alb.ingress.kubernetes.io/healthcheck-path: /healthz/ready
    alb.ingress.kubernetes.io/healthcheck-port: "30218"
...

При создании Ingress – ALB Ingress контроллер найдёт сервис, указанный в backend.serviceName Ingress-манифеста, оттуда считает аннотации, и применит их к TargetGroup, которая будет создана для ALB.

При деплое с Helm – аннотации можно будет указать через values.gateways.istio-ingressgateway.serviceAnnotations.

Создание Ingress и AWS Application LoadBalancer

Добавляем Ingress – это будет основной LoadBalancer приложения, на котором мы выполняем SSL termination.

В нём указываем ARN SSL-сертификата из AWS Certificate Manager. Ingress создаём в istio-system namespace, так как ему нужен доступ к istio-ingressgateway Service.

---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: test-alb
  namespace: istio-system
  annotations:
    # create AWS Application LoadBalancer
    kubernetes.io/ingress.class: alb
    # external type
    alb.ingress.kubernetes.io/scheme: internet-facing
    # AWS Certificate Manager certificate's ARN
    alb.ingress.kubernetes.io/certificate-arn: "arn:aws:acm:eu-west-3:***:certificate/fcaa9fd2-1b55-48d7-92f2-e829f7bafafd"
    # open ports 80 and 443 
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]'
    # redirect all HTTP to HTTPS
    alb.ingress.kubernetes.io/actions.ssl-redirect: '{"Type": "redirect", "RedirectConfig": { "Protocol": "HTTPS", "Port": "443", "StatusCode": "HTTP_301"}}'
    # ExternalDNS settings: https://rtfm.co.ua/en/kubernetes-update-aws-route53-dns-from-an-ingress/
    external-dns.alpha.kubernetes.io/hostname: "istio-test-alb.example.com"
spec:
  rules:
  - http:
      paths:
        - path: /*
          backend:
            serviceName: ssl-redirect
            servicePort: use-annotation
        - path: /*
          backend:
            serviceName: istio-ingressgateway
            servicePort: 80

Деплоим его:

[simterm]

$ kubectl apply -f test-ingress.yaml 
ingress.extensions/test-alb created

[/simterm]

Проверяем Ingress в неймспейсе istio-system:

[simterm]

$ kubectl -n istio-system get ingress
NAME       CLASS    HOSTS   ADDRESS                                 PORTS   AGE
test-alb   <none>   *       cc2***514.eu-west-3.elb.amazonaws.com   80      2m49s

[/simterm]

AWS ALB создан:

В Health checks его TargetGroup есть наш порт и URI:

И таргеты здоровы:

Проверяем по домену – он был создан автоматически по аннотации external-dns.alpha.kubernetes.io/hostname в Ingress, см. Kubernetes: обновление DNS в Route53 при создании Ingress:

[simterm]

$ curl -I https://istio-test-alb.example.com
HTTP/2 502
server: awselb/2.0

[/simterm]

Отлично – за Istio пока нет приложения, потому роутить трафик некуда, более того – на поде Ingress Gateway нет даже листенера на порту 80, т.е. Envoy-прокси его не слушает:

[simterm]

$ istioctl proxy-config listeners -n istio-system  istio-ingressgateway-d45fb4b48-jsz9z
ADDRESS PORT  MATCH DESTINATION
0.0.0.0 15021 ALL   Inline Route: /healthz/ready*
0.0.0.0 15090 ALL   Inline Route: /stats/prometheus*

[/simterm]

А вот 15021 открыт, поэтому healh-checks с ALB проходят.

Тестовое приложение

Описываем стандартное тестовое приложение – отдельный неймспейс, два пода с nginxdemos/hello, и перед ними Service:

---         
apiVersion: v1
kind: Namespace
metadata:
  name: test-ns
  labels:
    istio-injection:
      enabled
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-deploy
  namespace: test-ns
  labels:
    app: test-app
    version: v1
spec: 
  replicas: 2
  selector:
    matchLabels:
      app: test-app
  template:
    metadata:
      labels:
        app: test-app
        version: v1
    spec: 
      containers:
      - name: web
        image: nginxdemos/hello
        ports:
        - containerPort: 80
        resources:
          requests:
            cpu: 100m
            memory: 100Mi
        readinessProbe:
          httpGet:
            path: /
            port: 80
      nodeSelector:
        role: common-workers
---
apiVersion: v1
kind: Service
metadata:
  name: test-svc
  namespace: test-namespace
spec:   
  selector:
    app: test-app
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: 80

Деплоим:

[simterm]

$ kubectl apply -f test-ingress.yaml 
ingress.extensions/test-alb configured
namespace/test-ns created
deployment.apps/test-deploy created
service/test-svc created

[/simterm]

Но ALB всё-ещё отдаёт ошибку 502, т.к. Istio Ingress Gateway пока не настроен.

Istio Gateway

Описываем Gateway и VirtualService.

В Gateway указываем порт, который будем слушать – 80, и Istio Ingress, который настраиваем – ingressgateway. В hosts описываем наш тестовый домен:

---
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: test-gateway
  namespace: test-ns
spec: 
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "istio-test-alb.example.com"

Деплоим:

[simterm]

$ kubectl apply -f test-ingress.yaml 
ingress.extensions/test-alb configured
namespace/test-ns unchanged
deployment.apps/test-deploy configured
service/test-svc unchanged
gateway.networking.istio.io/test-gateway created

[/simterm]

Проверяем listeners нашего Istio Ingress Gateway ещё раз:

[simterm]

$ istioctl proxy-config listeners -n istio-system  istio-ingressgateway-d45fb4b48-jsz9z
ADDRESS PORT  MATCH DESTINATION
0.0.0.0 8080  ALL   Route: http.80
0.0.0.0 15021 ALL   Inline Route: /healthz/ready*
0.0.0.0 15090 ALL   Inline Route: /stats/prometheus*

[/simterm]

Порт 80 появился, но маршрут ведёт в никуда:

[simterm]

$ istioctl proxy-config routes -n istio-system  istio-ingressgateway-d45fb4b48-jsz9z
NOTE: This output only contains routes loaded via RDS.
NAME        DOMAINS     MATCH                  VIRTUAL SERVICE
http.80     *           /*                     404
            *           /healthz/ready*        
            *           /stats/prometheus*

[/simterm]

И обращение по домену нам сейчас выдаёт 404, но уже не от awselb/2.0, а от istio-envoy, так как пакет теперь доходит до пода с Ingress Gateway:

[simterm]

$ curl -I https://istio-test-alb.example.com
HTTP/2 404 
date: Fri, 26 Mar 2021 11:02:57 GMT
server: istio-envoy

[/simterm]

Istio VirtualService

В VirtualService описываем Gateway, к корому роуты будут применены, и сам роут – шлём трафик на Service нашего тестового приложения:

---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: test-virtualservice
  namespace: test-ns
spec: 
  hosts:
  - "istio-test-alb.example.com"
  gateways:
  - test-gateway
  http:
  - match: 
    - uri:   
        prefix: /
    route:
    - destination:
        host: test-svc
        port:
          number: 80

Деплоим, проверяем роуты Istio Ingress Gateway ещё раз:

[simterm]

$ istioctl proxy-config routes -n istio-system  istio-ingressgateway-d45fb4b48-jsz9z
NOTE: This output only contains routes loaded via RDS.
NAME        DOMAINS                           MATCH                  VIRTUAL SERVICE
http.80     istio-test-alb.example.com     /*                     test-virtualservice.test-ns

[/simterm]

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

[simterm]

$ curl -I https://istio-test-alb.example.com
HTTP/2 200
date: Fri, 26 Mar 2021 11:06:52 GMT
content-type: text/html
server: istio-envoy

[/simterm]

Готово