Istio: общий Ingress/AWS ALB, Helm-чарт с условиями, Istio и ExternalDNS

Автор: | 14/04/2021

Продолжаем баловаться с Istio.

Предыдущие части:

  1. Istio: обзор и запуск service mesh в Kubernetes

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

Кроме Istio, мы будем настраивать ExternalDNS, см. Kubernetes: обновление DNS в Route53 при создании Ingress.

Всё описанное ниже пока в статусе Proof of Concept, и деплоится на единый Dev-кластер AWS Elastic Kubernetes Service.

Цели

Сейчас есть три задачи:

  1. протестировать, как будет работать один AWS Application LoadBalancer (AWS ALB) и Istio Ingress Gateway с приложениями в различных неймспейсах
  2. создать Helm-чарт, в шаблонах которого будет возможность выборочной установки и настройки Ingress, Istio Gateway и Istio VirtualService
  3. настроить ExternalDNS на создание записей в Route53 при создании/обновлении Istio Gateway или VirtualService

Сначала посмотрим, как Istio Ingress Gateway будет работать с приложениями из разный неймспейсов – создадим Ingress, который создаст AWS Application LoadBalancer, плюс два приложения – каждое со своим Service, Gateway и VirtualService.

В Ingress/AWS ALB описываем хосты, и создаём домены через ExternalDNS, плюс выполняем SSL termination – сюда будем подключать сертификаты.

В Gateway каждого приложения – открываем порты на Istio Ingress Gateway, плюс описываем хосты, для которых принимает трафик этот Gateway.

Общий Ingress создаём в istio-system namespace, так как ему нужен доступ к Istio Ingress Service.

Gateway можно было бы создать один общий для всех приложений, но в конце дискуссии тут>>> говорят, что более правильным подходом будет создавать отдельный Gateway на каждое приложение, т.е. в отдельных неймспейсах. Выглядит логично – так и сделаем.

Давайте попробуем – потом посмотрим, как это взлетит.

Значит, делаем:

  1. отдельный Ingress/AWS ALB с двумя тестовыми доменами в неймспейсе istio-system
  2. app1 и app2 в неймспейсах backend-app-1-ns и backend-app-2-ns соответственно: у каждого свой Deployment, Service, Gateway и VirtualService

Вторая задача будет интереснее – создать шаблоны Helm для деплоя приложений на разные окружения с разными настройками Ingress.

В третьей задаче – настроим ExternalDNS на работу с Istio Gateway и VirtualService.

Местами получилось достаточно запутано, особенно во второй и третьей частях, про Helm и ExternalDNS, но постарался описать всё максимально доступно (хотя пока это всё сетапилось – поломать голову пришлось).

Общий Ingress/AWS ALB

Создаём файл манифеста для Ingress/ALB – common-ingress-gateway.yaml:

---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: backend-common-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:us-east-2:534***385:certificate/db886018-c173-43d0-b1be-b940de71f2a2"
    # 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: "app1-test-common-ingress.example.com, app2-test-common-ingress.example.com"
spec:
  rules:
  - http:
      paths:
        - path: /*
          backend:
            serviceName: ssl-redirect
            servicePort: use-annotation
        - path: /*
          backend:
            serviceName: istio-ingressgateway
            servicePort: 80

Тут в аннотациях задаём имена доменов, которые создаст ExternalDNS и привяжет их к URL создаваемого AWS ALB, а в backend – шлём весь трафик на Service istio-ingressgateway.

Деплоим:

[simterm]

$ kubectl apply -f common-ingress-gateway.yaml 
ingress.extensions/backend-common-alb created

[/simterm]

Проверяем Ingress/ALB:

[simterm]

$ kubectl -n istio-system get ingress backend-common-alb
NAME                 CLASS    HOSTS   ADDRESS                                              PORTS   AGE
backend-common-alb   <none>   *       aadca942-***1826302788.us-east-2.elb.amazonaws.com   80      72s

[/simterm]

Проверяем ExternalDNS:

[simterm]

...
time="2021-04-12T09:45:02Z" level=info msg="Desired change: CREATE app-1-test-common-ingress.example.com A [Id: /hostedzone/Z30KLN6M3D0LB6]"
time="2021-04-12T09:45:02Z" level=info msg="Desired change: CREATE app-1-test-common-ingress.example.com TXT [Id: /hostedzone/Z30KLN6M3D0LB6]"
time="2021-04-12T09:45:02Z" level=info msg="Desired change: CREATE app-2-test-common-ingress.example.com A [Id: /hostedzone/Z30KLN6M3D0LB6]"
time="2021-04-12T09:45:02Z" level=info msg="Desired change: CREATE app-2-test-common-ingress.example.com TXT [Id: /hostedzone/Z30KLN6M3D0LB6]"
time="2021-04-12T09:45:03Z" level=info msg="4 record(s) in zone example.com. [Id: /hostedzone/Z30KLN6M3D0LB6] were successfully updated"
...

[/simterm]

Пробуем URL – должен быть ответ с ошибкой, так как сам Istio Ingress Gateway ещё не настроен:

[simterm]

$ curl -I https://app-1-test-common-ingress.example.com
HTTP/2 502 
date: Mon, 12 Apr 2021 09:46:11 GMT
server: istio-envoy

[/simterm]

Всё верно.

На Istio Ingress Gateway у нас пока нет роутов:

[simterm]

$ istioctl proxy-config routes -n istio-system istio-ingressgateway-8459df68cb-bh76b
NOTE: This output only contains routes loaded via RDS.
NAME     DOMAINS     MATCH                  VIRTUAL SERVICE
         *           /healthz/ready*        
         *           /stats/prometheus*

[/simterm]

Добавим тестовые приложения, в которых опишем настройки для Istio Ingress Gateway.

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

Тестовые приложения одинаковы кроме неймспейсов и имён приложений/сервисов.

Тут создаём ресурсы:

  • Namespace
  • Deployment
  • Service
  • Gateway
  • VirtualService

Получается такой манифест:

---
apiVersion: v1
kind: Namespace
metadata:
  name: backend-app-1-ns
  labels:
    istio-injection:
      enabled
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend-app-1-deploy
  namespace: backend-app-1-ns
  labels:
    app: backend-app-1
    version: v1
spec:
  replicas: 2
  selector:
    matchLabels:
      app: backend-app-1
  template:
    metadata:
      labels:
        app: backend-app-1
        version: v1
    spec: 
      containers:
      - name: app1
        image: nginxdemos/hello
        ports:
        - containerPort: 80
        resources:
          requests:
            cpu: 100m
            memory: 100Mi
        readinessProbe:
          httpGet:
            path: /
            port: 80
---
apiVersion: v1
kind: Service
metadata:
  name: backend-app-1-servcie
  namespace: backend-app-1-ns
spec:       
  selector:
    app: backend-app-1
  ports:    
    - name: http
      protocol: TCP
      port: 80
---
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: backend-app-1-gateway
  namespace: backend-app-1-ns
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "app-1-test-common-ingress.example.com"
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: backend-app-1-virtualservice
  namespace: backend-app-1-ns
spec:
  hosts:
  - "app-1-test-common-ingress.example.com"
  gateways:
  - backend-app-1-gateway
  http:
  - match:
    - uri:
        prefix: /
    route:
    - destination:
        host: backend-app-1-servcie
        port:
          number: 80

Применяем:

[simterm]

$ kubectl apply -f app1.yaml 
namespace/backend-app-1-ns created
deployment.apps/backend-app-1-deploy created
service/backend-app-1-servcie created
gateway.networking.istio.io/backend-app-1-gateway created
virtualservice.networking.istio.io/backend-app-1-virtualservice created

[/simterm]

Копируем манифест, меняем app-1 на app-2, деплоим:

[simterm]

$ kubectl apply -f app2.yaml 
namespace/backend-app-2-ns created
deployment.apps/backend-app-2-deploy created
service/backend-app-2-servcie created
gateway.networking.istio.io/backend-app-2-gateway created
virtualservice.networking.istio.io/backend-app-2-virtualservice created

[/simterm]

Проверяем роуты Istio ещё раз:

[simterm]

$ istioctl proxy-config routes -n istio-system istio-ingressgateway-8459df68cb-bh76b
NOTE: This output only contains routes loaded via RDS.
NAME        DOMAINS                                      MATCH                  VIRTUAL SERVICE
http.80     app-1-test-common-ingress.example.com     /*                     backend-app-1-virtualservice.backend-app-1-ns
http.80     app-2-test-common-ingress.example.com     /*                     backend-app-2-virtualservice.backend-app-2-ns

[/simterm]

И пробуем обратиться по имени хоста приложения app-1:

[simterm]

$ curl -I https://app-1-test-common-ingress.example.com
HTTP/2 200 
date: Mon, 12 Apr 2021 09:52:13 GMT
content-type: text/html
server: istio-envoy

[/simterm]

Отлично – тут всё работает. Теперь у нас есть один общий AWS ALB, который через Istio Ingress Gateway шлёт трафик на два приложения в разных неймспейсах.

Удаляем ресурсы из неймспейсов, Ingress с общим LoadBalancer оставляем для следующих тестов:

[simterm]

$ kubectl delete -f app1.yaml
$ kubectl delete -f app2.yaml

[/simterm]

Переходим к Helm.

Istio: общий Ingress/AWS ALB и Helm

Планирование: условия

Следующая задача несколько нетривиальна: у нас есть Dev EKS кластер, есть Production EKS кластер.

На Dev-кластере Istio уже есть, тестируется, но не для всех приложений. На Production – Istio ещё нет, но будет.

Кроме того – сейчас все приложения, каждое в своём Namespace, создают свой отдельный Ingress/AWS ALB.

На Dev-кластере от этого подхода хочется избавиться, и трафик ко всем приложениям слать через Istio Ingress Gateway, а на Production – оставить, как есть, т.е. у каждого приложения свой ALB, но все Ingress/ALB в будущем будут слать трафик через Istio Ingress Gateway.

Но при этом и на Dev, и на Production приложение может уже использовать Istio – или ещё нет, так как всё ещё в процессе внедрения-тестирования, и какое-то время настройки приложений будут отличаться, а потому надо создать такой шаблон для Helm, в котором можно будет переопределять настройки Ingress.

Т.е. во-первых – надо определить, создаёт ли чарт свой собственный Ingress – или использует общий? Во-вторых, если создаёт, то какой бекенд использует – Istio Ingress Gateway, или обычный Service самого приложния? И если не создаёт, а использует Istio Ingress Gateway – то из чарта приложения создавать Gateway и VirtualService.

Следовательно, наш шаблон должен поддерживать три схемы деплоя приложения:

  1. используется общий LoadBalancer и Istio: Istio Ingress Gateway LoadBalancer и бекенд Istio Ingress Gateway Service
  2. отдельный LoadBalancer приложения и Istio Ingress Gateway: Ingress приложения и бекенд Istio Ingress Gateway Service
  3. отдельный LoadBalancer приложения  и Istio не используется вообще: отдельный Ingress приложения и бекенд Service приложения

Значит:

  1. Ingress общий – или свой?
    1. если общий – то свой Ingress не создаём
    2. если свой – то создаём, но с возможностью выбора бекенда – Istio Ingress Service, или обычный Service приложения
  2. бекенд для Ингресса – Istio, или Service приложения?
    1. если Istio – то указываем serviceName: istio-ingressgateway, плюс создаём Gateway и VirtualService
    2. если Service приложения – то указываем serviceName: <APPNAME>-service, а Gateway и VirtualService не создаём

Для решения используем values.yaml – у нас он у Dev свой, у Prod – свой.

В них определим два параметра – istio.enabled и ingress.enabled.

Это даст возможность для Dev-кластера, например, указать ingress.enabled=false, и не создавать Ingress, но указать istio.enabled – и создать Gateway и VirtualService, на которые будет слать трафик наш общий Ingress/ALB из нейспейса istio-system. А на Production можно будет указать ingress.enabled=true и istio.enabled=false, и тогда деплой будет выполняться по старой схеме. А когда там появится Istio – зададим значения ingress.enabled=true и istio.enabled=true, и создадим отдельный LoadBalancer, который будет слать трафик через Istio Ingress Gateway.

Попробуем.

Создание Helm шаблона

Генерируем тестовый чарт:

[simterm]

$ helm create app-1
Creating app-1

[/simterm]

Создаём каталоги для values.yaml Dev-а и Production-а:

[simterm]

$ mkdir -p app-1/env/{dev,prod}

[/simterm]

Удаляем шаблоны и дефолтные values – напишем свой велосипед:

[simterm]

$ rm -r app-1/templates/* app-1/values.yaml

[/simterm]

Пишем файлы конфигов:

[simterm]

$ vim -p app-1/env/dev/values.yaml app-1/env/prod/values.yaml

[/simterm]

Dev – app-1/env/dev/values.yaml:

appConfig:
  name: "backend-app-1"
  version: "v1"
  url: "dev-app-1-test-common-ingress.example.com"
  
istio:
  enabled: true
    
ingress:
  enabled: false

Ingress не создаём, Gateway и VirtualService создаём.

Production – app-1/env/prod/values.yaml:

appConfig:
  name: "backend-app-1"
  version: "v1"
  url: "prod-app-1-test-common-ingress.example.com"
  
istio:
  enabled: false
  
ingress:
  enabled: true

Тут Ingress создаём в виде отдельного ALB, но ресурсы Istio не создаём – Ingress будет слать трафик прямо на Service приложения.

Пишем файлы шаблонов:

[simterm]

$ vim -p app-1/templates/ingress.yaml app-1/templates/service.yaml app-1/templates/deployment.yaml

[/simterm]

Тут все ресурсы (кроме Ingress, см. ниже) описываем без Namespace – он будет создан Helm во время деплоя.

Deployment

Без изменений, только часть параметров берём из values:

--- 
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Values.appConfig.name }}-deploy
  labels:
    app: {{ .Values.appConfig.name }}
    version: {{ .Values.appConfig.version }}
spec:
  replicas: 2
  selector:
    matchLabels:
      app: {{ .Values.appConfig.name }}
  template: 
    metadata:
      labels:
        app: {{ .Values.appConfig.name }}
        version: {{ .Values.appConfig.version }}
    spec: 
      containers:
      - name: web-app
        image: nginxdemos/hello
        ports:
        - containerPort: 80
        resources:
          requests:
            cpu: 100m
            memory: 100Mi
        readinessProbe:
          httpGet:
            path: /
            port: 80

Service, VirtualService, Gateway

Тут мы всегда создаём обычный Service, в котором выполняем проверку: если ingress.enabled – то задаём тип NodePort, что бы LoadBalancer мог слать трафик на WorkerNode. Если нет – то используем дефолтный ClusterIP, что бы пакет не проходил через дополнительный роутинг, а шёл прямо на WorkerNode, на которой расположен под (категорически советую почитать Kubernetes: Service, балансировка нагрузки, kube-proxy и iptables):

--- 
apiVersion: v1
kind: Service
metadata:
  name: {{ .Values.appConfig.name }}-service
spec:
  {{- if .Values.ingress.enabled }}
  type: NodePort
  {{- end }}
  selector:
    app: {{ .Values.appConfig.name }}
  ports:
    - name: http
      protocol: TCP
      port: 80

Далее, проверяем условие istio.enabled – если true, то добавляем создание ресурсов Gateway и VirtualService:

{{- if .Values.istio.enabled }}
---
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: {{ .Values.appConfig.name }}-gateway
spec:     
  selector: 
    istio: ingressgateway
  servers:
  - port: 
      number: 80
      name: http
      protocol: HTTP
    hosts:  
    - {{ .Values.appConfig.url }}
---         
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: {{ .Values.appConfig.name }}-virtualservice
spec:
  hosts:
  - {{ .Values.appConfig.url }}
  gateways:
  - {{ .Values.appConfig.name }}-gateway
  http:
  - match:
    - uri:
        prefix: /
    route:
    - destination:
        host: {{ .Values.appConfig.name }}-service
        port:
          number: 80
{{- end }}

Ingress

Для Ingress в условии ingress.enabled сначала проверяем – создавать ли его, и если создаётся – то проверяем в каком неймспейсе создавать Ingress, так как при использовании Istio этот Ingress должен создаваться в неймспейсе istio-system, а если это “обычный” Ingress – то в неймспейсе приложения.

Ниже в условии istio.enabled определяем куда он будет слать трафик – на Istio, или обычный Service приложения:

{{- if .Values.ingress.enabled }}
--- 
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: {{ .Values.appConfig.name }}-alb
  {{- if .Values.istio.enabled }}
  namespace: istio-system
  {{- end }}
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/certificate-arn: "arn:aws:acm:us-east-2:534***385:certificate/db886018-c173-43d0-b1be-b940de71f2a2"
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]'
    alb.ingress.kubernetes.io/actions.ssl-redirect: '{"Type": "redirect", "RedirectConfig": { "Protocol": "HTTPS", "Port": "443", "StatusCode": "HTTP_301"}}'
    external-dns.alpha.kubernetes.io/hostname: {{ .Values.appConfig.url }}
spec:   
  rules:
  - http:
      paths:
        - path: /*
          backend:
            serviceName: ssl-redirect
            servicePort: use-annotation
        - path: /*
          backend:
          {{- if .Values.istio.enabled }}
            serviceName: istio-ingressgateway
          {{ else }}
            serviceName: {{ .Values.appConfig.name }}-service
          {{- end }}
            servicePort: 80
{{- end }}

Проверяем Dev с --debug и --dry-run – Ingress быть не должно, но Gateway и VirtualService – должны:

[simterm]

$ helm upgrade --install backend-app-1 --namespace dev-backend-app-1-ns --create-namespace -f app-1/env/dev/values.yaml app-1/ --debug --dry-run

[/simterm]

Если ошибок нет – деплоим:

[simterm]

$ helm upgrade --install backend-app-1 --namespace dev-backend-app-1-ns --create-namespace -f app-1/env/dev/values.yaml app-1/
Release "backend-app-1" does not exist. Installing it now.
NAME: backend-app-1
LAST DEPLOYED: Mon Apr 12 18:03:08 2021
NAMESPACE: dev-backend-app-1-ns
STATUS: deployed
REVISION: 1
TEST SUITE: None

[/simterm]

Проверяем Ingress в namespace dev-backend-app-1-ns:

[simterm]

$ kubectl -n dev-backend-app-1-ns get ingress
No resources found.

[/simterm]

А Gateway и VirtualServce – есть:

[simterm]

$ kubectl -n dev-backend-app-1-ns get gateway
NAME                    AGE
backend-app-1-gateway   38s

$ kubectl -n dev-backend-app-1-ns get virtualservice
NAME                           GATEWAYS                  HOSTS                                            AGE
backend-app-1-virtualservice   [backend-app-1-gateway]   [dev-app-1-test-common-ingress.example.com]   65s

[/simterm]

Деплоим Production:

[simterm]

$ helm upgrade --install backend-app-1 --namespace prod-backend-app-1-ns --create-namespace -f app-1/env/prod/values.yaml app-1/

[/simterm]

Проверяем Ingress в неймспейсе prod-backend-app-1-ns, так как Istio мы тут не используем:

[simterm]

$ kubectl -n prod-backend-app-1-ns get ingress
NAME                CLASS    HOSTS   ADDRESS                                             PORTS   AGE
backend-app-1-alb   <none>   *       aadca942-***-49478225.us-east-2.elb.amazonaws.com   80      4m54s

[/simterm]

Для Dev-окружения обновляем Ingress, созданный в самом начале – добавляем external-dns.alpha.kubernetes.io/hostname, что бы ExternalDNS его создал и направил на этот LoadBalancer:

...
    # ExternalDNS settings: https://rtfm.co.ua/en/kubernetes-update-aws-route53-dns-from-an-ingress/
    external-dns.alpha.kubernetes.io/hostname: "dev-app-1-test-common-ingress.example.com"
...

Применяем:

[simterm]

$ kubectl apply -f common-ingress-gateway.yaml 
ingress.extensions/backend-common-alb configured

[/simterm]

Посмотрим роуты Istio Ingress Gateway – сейчас тут должен быть только Dev:

[simterm]

$ istioctl proxy-config routes -n istio-system istio-ingressgateway-8459df68cb-bh76b --name http.80
NOTE: This output only contains routes loaded via RDS.
NAME        DOMAINS                                           MATCH     VIRTUAL SERVICE
http.80     dev-app-1-test-common-ingress.example.com      /*        backend-app-1-virtualservice.dev-backend-app-1-ns

[/simterm]

Или так:

[simterm]

$ istioctl proxy-config routes -n istio-system istio-ingressgateway-8459df68cb-bh76b --name http.80 -o json | jq '.[].virtualHosts[].domains[0], .[].virtualHosts[].routes[].route.cluster'
"dev-app-1-test-common-ingress.example.com"
"outbound|80||backend-app-1-service.dev-backend-app-1-ns.svc.cluster.local"

[/simterm]

И пробуем обратиться к приложениям.

Прод:

[simterm]

$ curl -I https://prod-app-1-test-common-ingress.example.com
HTTP/2 200 
date: Tue, 13 Apr 2021 12:47:15 GMT
content-type: text/html
server: nginx/1.13.8

[/simterm]

server: nginx/1.13.8 – ответ от NGINX, трафик прошёл через LoadBalancer напрямую на Service приложения и к его поду.

Dev:

[simterm]

$ curl -I https://dev-app-1-test-common-ingress.example.com
HTTP/2 200 
date: Tue, 13 Apr 2021 12:47:18 GMT
content-type: text/html
server: istio-envoy

[/simterm]

server: istio-envoy – трафик пошёл через общий LoadBalancer, потом на Istio Ingress Gateway, потом на sidecar-контейнер с Envoy в поде с приложением.

И проверяем третью схему – оставляем отдельный Ingress, но включаем работу через Istio.

В app-1/env/prod/values.yaml меняем istio.enabled на true, ingress.enabled у нас и так в true:

appConfig:
  name: "backend-app-1"
  version: "v1"
  url: "prod-app-1-test-common-ingress.example.com"
  
istio:
  enabled: true
    
ingress:
  enabled: true

Обновляем:

[simterm]

$ helm upgrade --install backend-app-1 --namespace prod-backend-app-1-ns --create-namespace -f app-1/env/prod/values.yaml app-1/

[/simterm]

Проверяем routes Istio Ingress Gateway ещё раз:

[simterm]

$ istioctl proxy-config routes -n istio-system istio-ingressgateway-8459df68cb-bh76b --name http.80
NOTE: This output only contains routes loaded via RDS.
NAME        DOMAINS                                           MATCH     VIRTUAL SERVICE
http.80     dev-app-1-test-common-ingress.example.com      /*        backend-app-1-virtualservice.dev-backend-app-1-ns
http.80     prod-app-1-test-common-ingress.example.com     /*        backend-app-1-virtualservice.prod-backend-app-1-ns

[/simterm]

Да – появился маршрут к бекенду Production.

Проверяем Ingress в неймспейсе prod-backend-app-1-ns:

[simterm]

$ kubectl -n prod-backend-app-1-ns get ingress backend-app-1-alb
Error from server (NotFound): ingresses.extensions "backend-app-1-alb" not found

[/simterm]

И в istio-system:

[simterm]

$ kubectl -n istio-system get ingress backend-app-1-alb
NAME                CLASS    HOSTS   ADDRESS                                               PORTS   AGE
backend-app-1-alb   <none>   *       aadca942-***-1554475105.us-east-2.elb.amazonaws.com   80      8m52s

[/simterm]

Пробуем curl:

[simterm]

$ curl -I https://prod-app-1-test-common-ingress.example.com
HTTP/2 200 
date: Tue, 13 Apr 2021 13:14:34 GMT
content-type: text/html
server: istio-envoy

[/simterm]

server: istio-envoy – трафик пошёл через Istio.

Istio и ExternalDNS

И последний нюанс – ExternalDNS.

Сейчас при использовании общего LoadBalancer мы указываем URL в аннотациях этого Ingress, при этом делой приложения из Helm-чарта не будет затрагивать этот Ingress – общий Ingress у нас создаётся из отдельного файла common-ingress-gateway.yaml.

Соответственно, что бы создавать DNS-записи при деплое новых приложений – придётся обновлять ещё и аннотации общего Ingress/ALB – двойная работа, дополнительная автоматизация.

Вместо этого, можем настроить ExternalDNS так, что бы он получал информацию не только с ресурсов Ingress, но и с самого Istio.

Обновляем его деплой, добавляем --source=istio-gateway и/или --source=istio-virtualservice, см. документацию тут>>>:

...
      containers:
      - args:
        - --log-level=info
        - --log-format=text
        - --events
        - --policy=upsert-only
        - --provider=aws
        - --registry=txt
        - --interval=2m
        - --source=service
        - --source=ingress
        - --source=istio-gateway
        - --source=istio-virtualservice
...

Из шаблона общего Ingress common-ingress-gateway.yaml удаляем:

...
external-dns.alpha.kubernetes.io/hostname: "dev-app-1-test-common-ingress.example.com"
...

Теперь имя хоста у нас будет задаваться только в Gateway и VirtualService из их полей spec.servers.hosts для Gateway или spec.hosts для VirtualService.

Что бы ExternalDNS мог получать данные от Istio – проверяем ClusterRole external-dns:

...
- apiGroups:
  - networking.istio.io
  resources:
  - virtualservices
  - gateways
  verbs:
  - get
  - list
  - watch
...

ExternalDNS не создаёт записи для Istio Gateway и VirtualService: “No endpoints could be generated”

Но тут возникла проблема.

Включаем --debug в ExternalDNS, смотрим за логами:

[simterm]

...
time="2021-04-14T12:53:05Z" level=debug msg="Adding event handler for Istio VirtualService"                                                                                                                                                  
time="2021-04-14T12:53:05Z" level=debug msg="Adding event handler for Istio Gateway"                                                                                                                                                         
time="2021-04-14T12:53:05Z" level=debug msg="Adding event handler for service"                                                                                                                                                               
time="2021-04-14T12:53:05Z" level=debug msg="Adding event handler for ingress" 
...
level=debug msg="No endpoints could be generated from ingress istio-system/backend-common-alb"
...

[/simterm]

Хенделы добавлены, т.е. ExternalDNS обновления Istio видит, но создавать записи не хочет.

Происходит это потому, что Istio Ingress Gateway Service создаётся с типом NodePort, а не дефолтным LoadBalancer, что бы сделать возможной работу с внешним AWS ALB, и ExternalDNS при парсинге VirtualService не может корректно определить ендпоинты для нашего внешнего Ingress, создаваемого из файла common-ingress-gateway.yaml.

Поэтому, пришлось делать небольшой костыль – в аннотациях создаваемого VirtualService указываем URL ALB явно.

При этом снова-таки помним, что иногда VirtualService создаётся вместе с отдельным Ingress/ALB – тогда костылить в виде аннотиаций непосредственно в VirtualService не надо.

Следовательно, в VirtualService добавляем ещё одно условие – if not .Values.ingress.enabled:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: {{ .Values.appConfig.name }}-virtualservice
  {{- if not .Values.ingress.enabled }}
  annotations:
    external-dns.alpha.kubernetes.io/target: {{ .Values.istio.externalURL }}
  {{- end }}
...

А значение istio.externalURL определяем в app-1/env/dev/values.yaml – оно у нас будет достаточно постоянным, и использоваться будет только на Dev-кластере:

...
istio:
  enabled: true
  externalURL: "aadca942-istiosystem-backe-3ee2-700661912.us-east-2.elb.amazonaws.com"
...

Полностью Service, Gateway и VirtualService теперь выглядят так:

--- 
apiVersion: v1
kind: Service
metadata:
  name: {{ .Values.appConfig.name }}-service
spec:
  {{- if .Values.ingress.enabled }}
  type: NodePort
  {{- end }}
  selector:
    app: {{ .Values.appConfig.name }}
  ports:
    - name: http
      protocol: TCP
      port: 80
    
{{- if .Values.istio.enabled }}
--- 
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: {{ .Values.appConfig.name }}-gateway
spec:
  selector:
    istio: ingressgateway
  servers:  
  - port:
      number: 80
      name: http 
      protocol: HTTP
    hosts:
    - "*"
---       
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: {{ .Values.appConfig.name }}-virtualservice
  {{- if not .Values.ingress.enabled }}
  annotations:
    external-dns.alpha.kubernetes.io/target: {{ .Values.istio.externalURL }}
  {{- end }}
spec:
  hosts:
  - {{ .Values.appConfig.url | quote }}
  gateways:
  - {{ .Values.appConfig.name }}-gateway
  http:
  - match:
    - uri:
        prefix: /
    route:
    - destination:
        host: {{ .Values.appConfig.name }}-service
        port:
          number: 80
{{- end }}

Деплоим, проверяем логи ExternalDNS:

[simterm]

...
time="2021-04-14T13:05:00Z" level=info msg="Desired change: CREATE dev-app-1-test-common-ingress.example.com A [Id: /hostedzone/Z30KLN6M3D0LB6]"                                                                                          
time="2021-04-14T13:05:00Z" level=info msg="Desired change: CREATE dev-app-1-test-common-ingress.example.com TXT [Id: /hostedzone/Z30KLN6M3D0LB6]" 
...

[/simterm]

И пробуем снова curl:

[simterm]

$ curl -I https://dev-app-1-test-common-ingress.example.com
HTTP/2 200 
date: Wed, 14 Apr 2021 13:21:11 GMT
content-type: text/html
server: istio-envoy

[/simterm]

Готово.