Подивились ми на наші витрати на AWS Load Balancers, і подумали, що треба трохи це діло привести в порядок.
Чого хочеться: мати один LoadBalancer, і через нього роутити запити на різні Kubernetes Ingresses та Services в різних Namespaces.
Перше, що спало на думку – це або додавати в Kubernetes кластер якийсь Service Mesh типу Istio або Linkerd, або додавати Nginx Ingress Controller, а перед ним – AWS ALB.
Але в UkrOps Slack мені нагадали, що AWS Load Balancer Controller, який ми використовуємо в нашому кластері AWS Elastic Kubernetes Service, вже давно вміє таке робити за допомогою IngressGroup
.
Тож давайте подивимось як це працює, і як таку схему можна додати на існуючі Ingress ресурси.
Зміст
Тест Load Balancer Controller IngressGroup
Отже, ідея доволі проста: в маніфесті Kubernetes Ingress ми задаємо ще один атрибут – group.name
, і по ньому Load Balancer Controller визначає до якого AWS LoadBalancer цей Ingress належить.
Потім він використовуючи spec.hosts
в Ingress визначає hostnames і на LoadBalancer будує роутинг до необхідних Target Groups.
Давайте спробуємо на простому прикладі.
Спочатку створюємо звичайну схему з окремими Ingress/ALB – описуємо маніфест з Namespace, Deployment, Service та Ingress:
apiVersion: v1 kind: Namespace metadata: name: test-app-1-ns --- apiVersion: apps/v1 kind: Deployment metadata: name: app-1-deploy namespace: test-app-1-ns spec: replicas: 1 selector: matchLabels: app: app-1-pod template: metadata: labels: app: app-1-pod spec: containers: - name: app-1-container image: nginx ports: - containerPort: 80 --- apiVersion: v1 kind: Service metadata: name: app-1-service namespace: test-app-1-ns spec: selector: app: app-1-pod ports: - protocol: TCP port: 80 targetPort: 80 type: ClusterIP --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: app-1-ingress namespace: test-app-1-ns annotations: alb.ingress.kubernetes.io/scheme: internet-facing alb.ingress.kubernetes.io/target-type: ip alb.ingress.kubernetes.io/listen-ports: '[{"HTTP":80}]' spec: ingressClassName: alb rules: - host: app-1.ops.example.com http: paths: - path: / pathType: Prefix backend: service: name: app-1-service port: number: 80
І такий же, тільки з app-2.
Деплоїмо:
$ kk apply -f . namespace/test-app-1-ns created deployment.apps/app-1-deploy created service/app-1-service created ingress.networking.k8s.io/app-1-ingress created namespace/test-app-2-ns created deployment.apps/app-2-deploy created service/app-2-service created ingress.networking.k8s.io/app-2-ingress created
Перевіряємо Ingress та його LoadBalancer для app-1:
$ kk -n test-app-1-ns get ingress NAME CLASS HOSTS ADDRESS PORTS AGE app-1-ingress alb app-1.ops.example.com k8s-testapp1-app1ingr-9375bc68bc-376038977.us-east-1.elb.amazonaws.com 80 33s
Тут ADDRESS
– “k8s-testapp1-app1ingr-9375bc68bc-376038977“.
Перевіряємо для app-2:
$ kk -n test-app-2-ns get ingress NAME CLASS HOSTS ADDRESS PORTS AGE app-2-ingress alb app-2.ops.example.com k8s-testapp2-app2ingr-0277bbb198-1743964934.us-east-1.elb.amazonaws.com 80 64s
Тут ADDRESS
– “k8s-testapp2-app2ingr-0277bbb198-1743964934“.
Відповідно, в AWS маємо два Load Balancers:
Тепер до обох Ingress додаємо анотацію alb.ingress.kubernetes.io/group.name: test-app-alb
:
... apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: app-1-ingress namespace: test-app-1-ns annotations: alb.ingress.kubernetes.io/group.name: test-app-alb alb.ingress.kubernetes.io/scheme: internet-facing ...
... apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: app-2-ingress namespace: test-app-2-ns annotations: alb.ingress.kubernetes.io/group.name: test-app-alb alb.ingress.kubernetes.io/scheme: internet-facing ...
Деплоїмо ще раз:
$ kk apply -f . namespace/test-app-1-ns unchanged deployment.apps/app-1-deploy unchanged service/app-1-service unchanged ingress.networking.k8s.io/app-1-ingress configured namespace/test-app-2-ns unchanged deployment.apps/app-2-deploy unchanged service/app-2-service unchanged ingress.networking.k8s.io/app-2-ingress configured
Та перевіряємо Ingresses та їхні адреси тепер.
У app-1 – це “k8s-testappalb-95eaaef0c8-2109819642“:
$ kk -n test-app-1-ns get ingress NAME CLASS HOSTS ADDRESS PORTS AGE app-1-ingress alb app-1.ops.example.com k8s-testappalb-95eaaef0c8-2109819642.us-east-1.elb.amazonaws.com 80 6m19s
У app-2 – теж “k8s-testappalb-95eaaef0c8-2109819642“:
$ kk -n test-app-2-ns get ingress NAME CLASS HOSTS ADDRESS PORTS AGE app-2-ingress alb app-2.ops.example.com k8s-testappalb-95eaaef0c8-2109819642.us-east-1.elb.amazonaws.com 80 6m48s
І в AWS у нас тепер один Load Balancer:
Який має два Listerner Rules, які в залежності від hostname в Ingress будуть редіректити запити до потрібних Target Groups:
IngressGroups – ліміти та реалізація в Production
При використанні такої схеми треба мати на увазі, що деякі параметри LoadBalancer не можуть задаватись в різних Ingress.
Наприклад, якщо один Ingress має анотацію alb.ingress.kubernetes.io/tags: "component=devops"
, а другий Ingress намагається задати тег component=backend
, то Load Balancer Controller не задеплоїть такі зміни, і повідомить про конфлікт, наприклад:
aws-load-balancer-controller-7647c5cbc7-2stvx:aws-load-balancer-controller {"level":"error","ts":"2024-09-25T10:50:23Z","msg":"Reconciler error","controller":"ingress","object":{"name":"ops-1-30-external-alb"},"namespace":"","name":"ops-1-30-external-alb","reconcileID":"1091979f-f349-4b96-850f-9e7203bfb8be","error":"conflicting tag component: devops | backend"} aws-load-balancer-controller-7647c5cbc7-2stvx:aws-load-balancer-controller {"level":"error","ts":"2024-09-25T10:50:44Z","msg":"Reconciler error","controller":"ingress","object":{"name":"ops-1-30-external-alb"},"namespace":"","name":"ops-1-30-external-alb","reconcileID":"19851b0c-ea82-424c-8534-d3324f4c5e60","error":"conflicting tag environment: ops | prod"}
Аналогічно до параметрів на кшталт alb.ingress.kubernetes.io/load-balancer-attributes: access_logs.s3.enabled=true,access_logs.s3.bucket=some-bucket-name
, або параметри SecurityGroups.
А от з TLS все простіше: для кожного Ingress в його annotations з alb.ingress.kubernetes.io/certificate-arn
можна передати ARN сертифікату з AWS Certificates Manager, і вони будуть налаштовані у Listener certificates for SNI:
Тому я принаймні поки що зробив так:
- створив окремий GitHub репозиторій
- в ньому Helm- чарт
- в цьому чарті два маніфести для двох Ingress – один з типом
internal
, другий –internet-facing
, і задав там всякі дефолтні параметри
Наприклад:
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: ops-external-ingress annotations: kubernetes.io/ingress.class: alb alb.ingress.kubernetes.io/group.name: ops-1-30-external-alb alb.ingress.kubernetes.io/scheme: internet-facing alb.ingress.kubernetes.io/target-type: ip alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}]' alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-east-1:492***148:certificate/88a5ccc0-e729-4fdb-818c-c41c411a3e3e alb.ingress.kubernetes.io/tags: "environment=ops,component=devops,Name=ops-1-30-external-alb" alb.ingress.kubernetes.io/actions.ssl-redirect: '{"Type": "redirect", "RedirectConfig": { "Protocol": "HTTPS", "Port": "443", "StatusCode": "HTTP_301"}}' alb.ingress.kubernetes.io/load-balancer-attributes: access_logs.s3.enabled=true,access_logs.s3.bucket=ops-1-30-devops-ingress-ops-alb-logs alb.ingress.kubernetes.io/actions.default-action: > {"Type":"fixed-response","FixedResponseConfig":{"ContentType":"text/plain","StatusCode":"200","MessageBody":"It works!"}} spec: ingressClassName: alb defaultBackend: service: name: default-action port: name: use-annotation
В defaultBackend
задаємо дію, коли запит приходить на hostname
, для якого нема окремого Listener – тут просто відповідаємо “It works!” з кодом 200.
А далі вже в Ingress проектів налаштовуються їхні параметри, наприклад Grafana:
$ kk -n ops-monitoring-ns get ingress atlas-victoriametrics-grafana -o yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: annotations: # the Common ALB group name alb.ingress.kubernetes.io/group.name: ops-1-30-external-alb ## TLS certificate from AWS ACM alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-east-1:492***148:certificate/88a5ccc0-e729-4fdb-818c-c41c411a3e3e # sucess codes for Target Group health checks alb.ingress.kubernetes.io/success-codes: "302" alb.ingress.kubernetes.io/target-type: ip kubernetes.io/ingress.class: alb ... name: atlas-victoriametrics-grafana namespace: ops-monitoring-ns ... spec: rules: - host: monitoring.1-30.ops.example.com http: paths: - backend: service: name: atlas-victoriametrics-grafana port: number: 80 path: / pathType: Prefix
І через alb.ingress.kubernetes.io/group.name: ops-1-30-external-alb
вони “підключаються” до нашого “дефолтного” LoadBalancer.
Працюємо на такій схемі вже кілька тижнів – поки політ нормальний.
Моніторинг
Ще з важливих нюансів – це моніторинг, бо дефолтні метрики CloudWatch, наприклад по помилкам 502/503/504 створюються на весь LoadBalancer.
Але в нашому випадку ми взагалі відмовились від метрик CloudWatch (ще й платити за кожен запит GetData на отримання метрик в CloudWatch Exporter або Yet Another Cloudwatch Exporter).
Натомість ми всі Access логи лоад-балансерів збираємо до Loki, а далі вже з її Recording Rules генеруємо метрики, де в лейблах маємо ім’я домену при запиті на який помилка виникла:
... - record: aws:alb:requests:sum_by:elb_http_codes_by_uri_path:5xx:by_pod_ip:rate:1m expr: | sum by (pod_ip, domain, elb_code, uri_path, user_agent) ( rate( {logtype="alb"} | pattern `<type> <date> <elb_id> <client_ip> <pod_ip> <request_processing_time> <target_processing_time> <response_processing_time> <elb_code> <target_code> <received_bytes> <sent_bytes> "<request>" "<user_agent>" <ssl_cipher> <ssl_protocol> <target_group_arn> "<trace_id>" "<domain>" "<chosen_cert_arn>" <matched_rule_priority> <request_creation_time> "<actions_executed>" "<redirect_url>" "<error_reason>" "<target>" "<target_status_code>" "<_>" "<_>"` | domain=~"(^.*api.challenge.example.co|lightdash.example.co)" | elb_code=~"50[2-4]" | regexp `.*:443(?P<uri_path>/[^/?]+).* HTTP` [1m] offset 5m ) ) ...
Див. Grafana Loki: збираємо логи AWS LoadBalancer з S3 за допомогою Promtail Lambda та Grafana Loki: LogQL та Recoding Rules для метрик з логів AWS Load Balancer.
Ще з цікавого почитати можна тут – A deeper look at Ingress Sharing and Target Group Binding in AWS Load Balancer Controller.