AWS: миграция AWS ALB Ingress Controller (v1) на AWS Load Balancer Controller (v2)

Автор: | 07/13/2021
 

Мы пользуемся AWS ALB Ingress Controller для создания AWS Application LoadBalancer (ALB) при создании Ingress в Kubernetes.

Последняя доступная версия — v2.2.1, тогда как у нас версия 1.14.

См. changelog версии 2.0 тут>>> и документацию по обновлению тут>>>.

Из интересных обновлений:

  • изменение имени: вместо AWS ALB Ingress Controller он теперь называется AWS LoadBalancer Controller
  • поддержка Network Load Balancers
  • IngressGroup: возможность использовать один ALB для нескольких Ingress
  • TargetGroupBinding: новый Kubernetes Custom Resource (CR), позволяющий открывать доступ подам через существующие TargetGroups

Кроме того, хочется переделать его деплой: сейчас он устанавливается из голого манифест-файла, а для новой версии используем установку из Helm-шаблона AWS Load Balancer Controller.

Что сделаем:

  1. создадим тестовый кластер с AWS ALB Ingress Controller v1
  2. задеплоим туда Ingress, проверим, что AWS LoadBalancer (ALB) создан
  3. удалим старый Контроллер AWS ALB Ingress Controller v1
  4. задеплоим новый, AWS Load Balancer Controller (он же AWS ALB Ingress Controller v2)
    • посмотрим различные варианты подключения IAM-политик и/или роли
  5. попробуем удалить Ingress и ALB, что бы убедиться в том, что контроллер версии 2 может работать с ресурсами, созданными версией 1
  6. столкнёмся с проблемами при удалении Ingress и AWS SecurityGroups, и посмотрим причины и варианты решений

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

Описываем Deployment, Service, Ingress:

---
apiVersion: v1
kind: Namespace
metadata:
  name: test-namespace
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-deployment
  namespace: test-namespace
  labels:
    app: test
    version: v1
spec:
  replicas: 1
  selector:
    matchLabels:
      app: test
  template:
    metadata:
      labels:
        app: test
        version: v1
    spec:
      containers:
      - name: web
        image: nginxdemos/hello
        ports:
        - containerPort: 80
        resources:
          requests:
            cpu: 100m
            memory: 100Mi
        readinessProbe:
          httpGet:
            path: /
            port: 80
---
apiVersion: v1
kind: Service
metadata:
  name: test-svc
  namespace: test-namespace
spec:
  type: NodePort
  selector:
    app: test
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: 80
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: test-ingress
  namespace: test-namespace
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}]'
    alb.ingress.kubernetes.io/inbound-cidrs: 0.0.0.0/0
spec:
  rules:
  - http:
      paths:
      - backend:
          serviceName: test-svc
          servicePort: 80

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

kubectl -n test-namespace get ingress
NAME           CLASS    HOSTS   ADDRESS                                                                 PORTS   AGE
test-ingress   <none>   *       1c00ce34-testnamespace-tes-5874-459061000.eu-west-3.elb.amazonaws.com   80      12s
curl -I 1c00ce31c00ce34-testnamespace-tes-5874-459061000.eu-west-3.elb.amazonaws.com
HTTP/1.1 200 OK

Всё работает.

Удаление AWS ALB Ingress Controller (v1)

Проверяем текущую версию:

kubectl -n kube-system describe deployment  alb-ingress-controller | grep Image
Image:      docker.io/amazon/aws-alb-ingress-controller:v1.1.4

Удаляем Deployment, ClusterRole, ClusterRoleBinding, ServiceAccount:

kubectl -n kube-system delete deploy alb-ingress-controller
deployment.apps "alb-ingress-controller" deleted
kubectl -n kube-system delete serviceaccount alb-ingress-controller
serviceaccount "alb-ingress-controller" deleted
kubectl delete clusterrole alb-ingress-controller
clusterrole.rbac.authorization.k8s.io "alb-ingress-controller" deleted
kubectl delete clusterrolebinding alb-ingress-controller
clusterrolebinding.rbac.authorization.k8s.io "alb-ingress-controller" deleted

Установка AWS Load Balancer Controller (v2)

Сама установка достаточно тривиальна, а вот с IAM (внезапно 🙂 ) пришлось повозиться.

Правда, не столько с самим AWS IAM — тут всё просто, сколько с тем, как именно выполнять аутентификацию и авторизацию контроллера.

IAM и ServiceAccount

Что у нас есть: есть одна новая IAM-политика, которую надо подключить к контроллеру для его работы с AWS API, и есть одна дополнительная, которая используется для работы с ресурсами, созданными контроллером первой версии.

Создать политики и/или роли можно вручную, можно описать в CloudFormation, как у нас это уже сделано для Cluster Autoscaler, см. AWS Elastic Kubernetes Service: автоматизация создания кластера, часть 1 — CloudFormation.

А для использования политик есть несколько вариантов:

  1. использовать ServiceAccount, в annotations которого будет задана IAM-роль, которую сможет Assume наш контроллер (см. AWS: IAM AssumeRole — описание, примеры).
  2. подключить IAM-роль или политики напрямую к WorkerNodes.

И для первого, и для второго варианта тоже есть варианты:

  • при использовании ServiceAccount:
    • создать ServiceAccount, используя values Helm-чарт AWS Load Balancer Controller, и в его serviceAccount.annotations указать AIM-роль
    • можно создать ServiceAccount используя eskctl и его файлы параметров для кластера, см. IAM Roles for Service Accounts
    • можно создать IAM ServiceAccount, используя прямой вызов eksctl create iamserviceaccount, передав аргументами политики через --attach-policy-arn
  • при использовании IAM для WorkerNodes:
    • можно подключить IAM-роль с политиками или просто две политики (+дефолтные, см. документацию) через файл параметров для WorkerNode, см. Attaching policies by ARN
    • можно и создать, и подключить политики через iam.withAddonPolicies.albIngress  самого eksctl

Учитывая факт, что у нас кластера уже есть, и обновлять их параметры лишний раз не хочется — то поступим самым костыльным простым образом:

  1. сами политики создадим вручную через вызов  aws iam create-policy
  2. используем Kubernetes ServiceAccount, который создадим и настроим прямым вызовом eksctl create iamserviceaccount
  3. в values чарта укажем serviceAccount.create == false и зададим serviceAccount.name, в котором передадим имя аккаунта, с которым этот ServiceAccount будет создан в п.1

Создание IAM-политик

Загружаем основную политику:

curl -o iam-policy.json https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/main/docs/install/iam_policy.json

Создаём её в AWS IAM с именем AWSLoadBalancerControllerIV2AMPolicy, сохраняем её ARN:

aws iam create-policy --policy-name AWSLoadBalancerControllerIV2AMPolicy --policy-document file://iam-policy.json
...
Arn": "arn:aws:iam::534***385:policy/AWSLoadBalancerControllerIV2AMPolicy",
...

Загружаем дополнительную политику:

wget https:/https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.0/install/iam_policy_v1_to_v2_additional.json

Создаём её с именем AWSLoadBalancerControllerIV2AdditionalAMPolicy, и тоже записываем ARN:

aws iam create-policy --policy-name AWSLoadBalancerControllerIV2AdditionalAMPolicy --policy-document file://iam_policy_v1_to_v2_additional.json

Ansible role

Далее, обновим Absible-роль, в которой выполняется установка всяких контроллеров.

Сейчас ALB Contoller устанавливается напрямую из манифеста, скачанного больше года тому:

...
- name: "Creating ALB Ingress ServiceAccount"
  command: "kubectl --kubeconfig={{ kube_config_path }} apply -f roles/controllers/templates/alb-ingress-rbac-role.yml.j2"

- name: "Generate ALB Ingress Controller deployment"
  template: 
    src: "alb-ingress-controller.yml.j2"
    dest: "/tmp/alb-ingress-controller.yml"

- name: "Install ALB Ingress Controller"
  command: "kubectl --kubeconfig={{ kube_config_path }} apply -f /tmp/alb-ingress-controller.yml"
...

Вместо этого — используем прямые вызовы AWS CLI и eksctl, а затем — установим AWS Load Balancer Controller из его Helm-чарта.

OIDC, IAM и ServiceAccount

Удаляем или комментируем предыдущую установку, и добавляем подключение OIDC к кластеру:

...
- name: "Create IAM OIDC provider"
  command: "eksctl --region={{ region }} --cluster={{ eks_cluster_name }} utils associate-iam-oidc-provider --approve"
...

Добавляем создание IAM ServiceAccount через прямой вызов eksctl, которому передаём ARN-ы политик, которые создали ранее:

...
- name: "Running eksctl create iamserviceaccount for the AWS ALB Controller"
  command: "eksctl --region={{ region }} create iamserviceaccount --cluster={{ eks_cluster_name }} --namespace=kube-system --name=aws-load-balancer-controller --attach-policy-arn=arn:aws:iam::534***385:policy/AWSLoadBalancerControllerIV2AMPolicy --attach-policy-arn=arn:aws:iam::534***385:policy/AWSLoadBalancerControllerIV2AdditionalAMPolicy --approve"
...

При использовании ClusterConfig для eksctl — он выглядит так:

apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
  name: "{{ eks_cluster_name }}"
  region: "{{ region }}"
  version: "{{ k8s_version }}" 
      
iam:  
  withOIDC: true
  serviceAccounts:
  - metadata:
      name: aws-load-balancer-controller
      namespace: kube-system
    attachPolicyARNs:
    - "arn:aws:iam::534***385:policy/AWSLoadBalancerControllerIV2AMPolicy"
    - "arn:aws:iam::534***385:policy/AWSLoadBalancerControllerIV2AdditionalAMPolicy"

...

Будем использовать его при создании будущих новых кластеров.

AWS LoadBalancer Controller Helm-чарт

Используя модуль community.kubernetes.helm описываем установку контроллера из чарта:

...
- name: "Install Ansible community.kubernetes plugin"
  command: "ansible-galaxy collection install community.kubernetes"

- name: "Add AWS EKS chart repo"
  community.kubernetes.helm_repository:
    name: "eks"
    repo_url: "https://aws.github.io/eks-charts"
      
- name: "Deploy AWS ALB Ingress Controller chart"
  community.kubernetes.helm:
    kubeconfig: "{{ kube_config_path }}"
    name: "aws-load-balancer-controller"
    chart_ref: "eks/aws-load-balancer-controller"
    release_namespace: "kube-system"
    create_namespace: false
    values: 
      clusterName: "{{ eks_cluster_name }}"
      enableWafv2: true
      enableShield: true
      logLevel: "debug"
      replicaCount: 1
      vpcId: "{{ vpc_id }}"
      region: "{{ region  }}"
      serviceAccount:
        create: false
        name: "aws-load-balancer-controller"
...

Указываем на то, что создавать ServiceAccount не надо, а надо использовать уже готовый.

Деплоим:

Проверяем CloudFormation стек, созданный при вызове eksctl create iamserviceaccount:

И политики, подключенные к этой роли:

Проверяем версию AWS LoadBalancer Controller:

kubectl -n kube-system describe deployment aws-load-balancer-controller | grep Image
Image:       602401143452.dkr.ecr.us-west-2.amazonaws.com/amazon/aws-load-balancer-controller:v2.2.1

Проверяем, используется ли созданный ServiceAccount:

kubectlkubectl -n kube-system get deploy aws-load-balancer-controller -o yaml | grep -w serviceAccount
f:serviceAccount: {}
serviceAccount: aws-load-balancer-controller

И аннотации этого ServiceAccount:

kubectl -n kube-system get sa aws-load-balancer-controller -o jsonpath='{.metadata.annotations}'
map[eks.amazonaws.com/role-arn:arn:aws:iam::534***385:role/eksctl-bttrm-eks-test-1-18-addon-iamservicea-Role1-NM3BLAGO21RR]

Вот и наша роль.

Ошибка «failed to delete securityGroup: timed out waiting for the condition» и обновление SecurityGroup

Теперь пробуем удалить Ingress и AWS Application Loadbalancer, созданный в самом начале с использованием AWSALBIngressController версии 1:

kubectl delete -f ~/Work/devops-kubernetes/tests/test-deployment.yaml
namespace "test-namespace" deleted
deployment.apps "test-deployment" deleted
service "test-svc" deleted
ingress.extensions "test-ingress" deleted

И тут kubectl зависает.

Проверяем логи контроллера:

kubectl -n kube-system logs -f deployment/aws-load-balancer-controller
...
{"level":"error","ts":1625487068.8594606,"logger":"controller","msg":"Reconciler error","controller":"ingress","name":"test-ingress","namespace":"test-namespace","error":"failed to delete securityGroup: timed out waiting for the condition"}
{"level":"info","ts":1625487069.7219825,"logger":"controllers.ingress","msg":"deleting securityGroup","securityGroupID":"sg-0b70595044826c9ac"}
...

Сейчас контроллер не может удалить SecrityGroup, которая была создана старым контроллером.

Case #1 Ручное добавление Description в SecurityGroup Rules

Одна из возможных причин описана в документации по миграции, и она вполне ожидаема:

SecurityGroup sg-0b70595044826c9ac — это группа, созданная контроллером при создании ALB.

Проверяем SecurityGroup нашей WorkerNode eksctl-bttrm-eks-test-1-18-2-nodegroup-common-eu-west-3a-v2021-02-SG-NGRWCUG2UJDT — находим Allow правило из группы sg-0b70595044826c9ac:

Добавляем Description со значением «elbv2.k8s.aws/targetGroupBinding=shared«:

Повторяем для всех WorkerNodes, и через 5 минут старые Ingress и AWS LoadBalancer удалены.

Case #2 Ошибки в Kubernetes Service

Второй случай заставил задуматься на пару дней и даже открыть Issue на Github.

Казалось бы — всё верно: Description в правилах есть, IAM настроен, нигде никаких ошибок нет.

Но при этом при удалении Ingress происходит странное:

  1. удаляются все ресурсы, кроме Kubernetes Namespace и SecurityGroup, которая была создана для ALB
  2. в SecurityGroup-ах, которые подключены к WorkerNodes не удаляется Rule — нет вызова RevokeSecurityGroupIngress, а CloudTrail сообщает об ошибке Client.DependencyViolation

Пока копал — приходилось вычищать их вручную.

Причина оказалась очень (ожидаемо) простой: на Dev-кластере оставался один «заброшенный» Ingress и LoadBalancer, у которого был криво настроен Service (ошибка «service type must be either ‘nodeport’ or ‘loadbalancer'»).

Из-за этого Controller и не выполнял очистку SecurityGroup во всех других Ingress/ALB, хотя разработчики вроде меняют логику его работы, что бы избежать таких зависимостей.