Repository: zalando-incubator/kubernetes-on-aws
Pull request #9279: Add the `aws-load-balancer-controller` to support ServiceType LoadBalancer on EKS
Author: login: linki
Labels: name: merged/alpha
name: merged/beta
name: merged/stable
name: minor
description: Minor changes, e.g. low risk config updates, changes that do not introduce a new API call.
name: merged/kube-1.32
Created: 2025-04-23T15:40:40Z | Merged: 2025-04-29T13:36:51Z

This adds the https://github.com/kubernetes-sigs/aws-load-balancer-controller (ALBC) to EKS-based clusters.

With it it's possible to use Services with `Type: LoadBalancer` on EKS with IPv6. It's implemented as an NLB. The controller also takes care of opening and closing the ports on the nodes via updating the security group.

A working service could look like this:

```yaml
apiVersion: v1
kind: Service
metadata:
  name: nginx
  annotations:
    # dualstack enables IPv6, there's no IPv6-only option
    # injected: this will be injected by admission-controller on EKS if not defined
    service.beta.kubernetes.io/aws-load-balancer-ip-address-type: dualstack
    # configures IPv6 Pod-addresses in the Target Group, bypassing the need to configure nodes (which doesn't work currently)
    # optional: this is the default and can be omitted or changed to `instance`
    service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
    # makes this NLB available via the Internet
    # optional: this is the default and can be omitted or changed to `internal`
    service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
    # exposes this port from the below port-list via SSL
    # optional: if not defined, all ports would be used
    service.beta.kubernetes.io/aws-load-balancer-ssl-ports: "443"
    # configure a certificate for SSL termination
    # optional: if not defined, no SSL endpoint will be available
    service.beta.kubernetes.io/aws-load-balancer-ssl-cert: arn:aws:acm:eu-central-1:1234567890:certificate/some-id
    # configure DNS to point this name to the created NLB
    # optional: if not defined, no custom DNS name will be available. Needed when using SSL.
    external-dns.alpha.kubernetes.io/hostname: nginx.playground.zalan.do
spec:
  type: LoadBalancer
  # marks this service for being processed by ALBC rather than the in-tree Service controller
  # injected: this will be injected by admission-controller on EKS if not defined
  loadBalancerClass: service.k8s.aws/nlb
  selector:
    application: nginx
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: 80
    - name: https
      protocol: TCP
      port: 443
      targetPort: 80
```

The part that triggers the new controller is:

```yaml
spec:
  type: LoadBalancer
  loadBalancerClass: service.k8s.aws/nlb
```

which marks the Service as a "load balancer" and gives it a class that's handled by ALBC. This prevents the in-tree, or cloud-provider (formerly in-tree) controllers to process it. `loadBalancerClass` is optional and will be injected by `admission-controller` unless it's already defined.

Another required part is the annotation: `service.beta.kubernetes.io/aws-load-balancer-ip-address-type: dualstack`. Since our EKS clusters run on IPv6 this is the only value that works. This annotation is optional and will be injected by `admission-controller` unless it's already defined.

All other annotations are optional but using them either overrides a default or triggers new functionality:
* `service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip`: backend can be registered to load balancers in two modes, IP-mode attaches IPv6 pod IPs, whereas instance-mode attaches instances. Currently, only ip-mode works but it could be changed here.
* `service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing`: by default load balancers are internet facing to match the old behaviour. This annotations can be used to make private load balancers.
* `service.beta.kubernetes.io/aws-load-balancer-ssl-cert: arn:aws:acm:eu-central-1:1234567890:certificate/some-id`: optionally enable SSL termination by specifying a certificate. If not set, traffic will be unencrypted.
* `service.beta.kubernetes.io/aws-load-balancer-ssl-ports`: when using SSL, all ports are exposed encrypted. If not desired (such as when doing a typical 80/443 setup) a subset can be selected
* `external-dns.alpha.kubernetes.io/hostname: nginx.playground.zalan.do`: assign a custom DNS name via ExternalDNS. Likely needed when using SSL, otherwise clients will present warnings when the NLB endpoint is used directly.


Diff:
diff --git a/cluster/cluster.yaml b/cluster/cluster.yaml
index b9c5164980..94090d9c70 100644
--- a/cluster/cluster.yaml
+++ b/cluster/cluster.yaml
@@ -3222,6 +3222,217 @@ Resources:
           PolicyName: root
       RoleName: "{{.Cluster.LocalID}}-audittrail-adapter"
     Type: 'AWS::IAM::Role'
+# {{- if and (eq .Cluster.Provider "zalando-eks") (eq .Cluster.ConfigItems.eks_ip_family "ipv6") }}
+  AWSLoadBalancerControllerIAMRole:
+    Properties:
+      AssumeRolePolicyDocument: !Sub
+        - |
+          {
+            "Version": "2012-10-17",
+            "Statement": [
+              {
+                "Effect": "Allow",
+                "Principal": {
+                  "Federated": [
+                    "arn:aws:iam::${AWS::AccountId}:oidc-provider/${OIDC}"
+                  ]
+                },
+                "Action": [
+                  "sts:AssumeRoleWithWebIdentity"
+                ],
+                "Condition": {
+                  "StringEquals": {
+                    "${OIDC}:sub": "system:serviceaccount:kube-system:aws-load-balancer-controller"
+                  }
+                }
+              }
+            ]
+          }
+        - OIDC: !Select [1, !Split ["//", !GetAtt EKSCluster.OpenIdConnectIssuerUrl]]
+      Path: /
+      Policies:
+        - PolicyDocument:
+            Version: '2012-10-17'
+            Statement:
+            - Effect: Allow
+              Action:
+              - iam:CreateServiceLinkedRole
+              Resource: "*"
+              Condition:
+                StringEquals:
+                  iam:AWSServiceName: elasticloadbalancing.amazonaws.com
+            - Effect: Allow
+              Action:
+              - ec2:DescribeAccountAttributes
+              - ec2:DescribeAddresses
+              - ec2:DescribeAvailabilityZones
+              - ec2:DescribeInternetGateways
+              - ec2:DescribeVpcs
+              - ec2:DescribeVpcPeeringConnections
+              - ec2:DescribeSubnets
+              - ec2:DescribeSecurityGroups
+              - ec2:DescribeInstances
+              - ec2:DescribeNetworkInterfaces
+              - ec2:DescribeTags
+              - ec2:GetCoipPoolUsage
+              - ec2:DescribeCoipPools
+              - ec2:GetSecurityGroupsForVpc
+              - ec2:DescribeIpamPools
+              - elasticloadbalancing:DescribeLoadBalancers
+              - elasticloadbalancing:DescribeLoadBalancerAttributes
+              - elasticloadbalancing:DescribeListeners
+              - elasticloadbalancing:DescribeListenerCertificates
+              - elasticloadbalancing:DescribeSSLPolicies
+              - elasticloadbalancing:DescribeRules
+              - elasticloadbalancing:DescribeTargetGroups
+              - elasticloadbalancing:DescribeTargetGroupAttributes
+              - elasticloadbalancing:DescribeTargetHealth
+              - elasticloadbalancing:DescribeTags
+              - elasticloadbalancing:DescribeTrustStores
+              - elasticloadbalancing:DescribeListenerAttributes
+              - elasticloadbalancing:DescribeCapacityReservation
+              Resource: "*"
+            - Effect: Allow
+              Action:
+              - cognito-idp:DescribeUserPoolClient
+              - acm:ListCertificates
+              - acm:DescribeCertificate
+              - iam:ListServerCertificates
+              - iam:GetServerCertificate
+              - waf-regional:GetWebACL
+              - waf-regional:GetWebACLForResource
+              - waf-regional:AssociateWebACL
+              - waf-regional:DisassociateWebACL
+              - wafv2:GetWebACL
+              - wafv2:GetWebACLForResource
+              - wafv2:AssociateWebACL
+              - wafv2:DisassociateWebACL
+              - shield:GetSubscriptionState
+              - shield:DescribeProtection
+              - shield:CreateProtection
+              - shield:DeleteProtection
+              Resource: "*"
+            - Effect: Allow
+              Action:
+              - ec2:AuthorizeSecurityGroupIngress
+              - ec2:RevokeSecurityGroupIngress
+              Resource: "*"
+            - Effect: Allow
+              Action:
+              - ec2:CreateSecurityGroup
+              Resource: "*"
+            - Effect: Allow
+              Action:
+              - ec2:CreateTags
+              Resource: arn:aws:ec2:*:*:security-group/*
+              Condition:
+                StringEquals:
+                  ec2:CreateAction: CreateSecurityGroup
+                'Null':
+                  aws:RequestTag/elbv2.k8s.aws/cluster: 'false'
+            - Effect: Allow
+              Action:
+              - ec2:CreateTags
+              - ec2:DeleteTags
+              Resource: arn:aws:ec2:*:*:security-group/*
+              Condition:
+                'Null':
+                  aws:RequestTag/elbv2.k8s.aws/cluster: 'true'
+                  aws:ResourceTag/elbv2.k8s.aws/cluster: 'false'
+            - Effect: Allow
+              Action:
+              - ec2:AuthorizeSecurityGroupIngress
+              - ec2:RevokeSecurityGroupIngress
+              - ec2:DeleteSecurityGroup
+              Resource: "*"
+              Condition:
+                'Null':
+                  aws:ResourceTag/elbv2.k8s.aws/cluster: 'false'
+            - Effect: Allow
+              Action:
+              - elasticloadbalancing:CreateLoadBalancer
+              - elasticloadbalancing:CreateTargetGroup
+              Resource: "*"
+              Condition:
+                'Null':
+                  aws:RequestTag/elbv2.k8s.aws/cluster: 'false'
+            - Effect: Allow
+              Action:
+              - elasticloadbalancing:CreateListener
+              - elasticloadbalancing:DeleteListener
+              - elasticloadbalancing:CreateRule
+              - elasticloadbalancing:DeleteRule
+              Resource: "*"
+            - Effect: Allow
+              Action:
+              - elasticloadbalancing:AddTags
+              - elasticloadbalancing:RemoveTags
+              Resource:
+              - arn:aws:elasticloadbalancing:*:*:targetgroup/*/*
+              - arn:aws:elasticloadbalancing:*:*:loadbalancer/net/*/*
+              - arn:aws:elasticloadbalancing:*:*:loadbalancer/app/*/*
+              Condition:
+                'Null':
+                  aws:RequestTag/elbv2.k8s.aws/cluster: 'true'
+                  aws:ResourceTag/elbv2.k8s.aws/cluster: 'false'
+            - Effect: Allow
+              Action:
+              - elasticloadbalancing:AddTags
+              - elasticloadbalancing:RemoveTags
+              Resource:
+              - arn:aws:elasticloadbalancing:*:*:listener/net/*/*/*
+              - arn:aws:elasticloadbalancing:*:*:listener/app/*/*/*
+              - arn:aws:elasticloadbalancing:*:*:listener-rule/net/*/*/*
+              - arn:aws:elasticloadbalancing:*:*:listener-rule/app/*/*/*
+            - Effect: Allow
+              Action:
+              - elasticloadbalancing:ModifyLoadBalancerAttributes
+              - elasticloadbalancing:SetIpAddressType
+              - elasticloadbalancing:SetSecurityGroups
+              - elasticloadbalancing:SetSubnets
+              - elasticloadbalancing:DeleteLoadBalancer
+              - elasticloadbalancing:ModifyTargetGroup
+              - elasticloadbalancing:ModifyTargetGroupAttributes
+              - elasticloadbalancing:DeleteTargetGroup
+              - elasticloadbalancing:ModifyListenerAttributes
+              - elasticloadbalancing:ModifyCapacityReservation
+              - elasticloadbalancing:ModifyIpPools
+              Resource: "*"
+              Condition:
+                'Null':
+                  aws:ResourceTag/elbv2.k8s.aws/cluster: 'false'
+            - Effect: Allow
+              Action:
+              - elasticloadbalancing:AddTags
+              Resource:
+              - arn:aws:elasticloadbalancing:*:*:targetgroup/*/*
+              - arn:aws:elasticloadbalancing:*:*:loadbalancer/net/*/*
+              - arn:aws:elasticloadbalancing:*:*:loadbalancer/app/*/*
+              Condition:
+                StringEquals:
+                  elasticloadbalancing:CreateAction:
+                  - CreateTargetGroup
+                  - CreateLoadBalancer
+                'Null':
+                  aws:RequestTag/elbv2.k8s.aws/cluster: 'false'
+            - Effect: Allow
+              Action:
+              - elasticloadbalancing:RegisterTargets
+              - elasticloadbalancing:DeregisterTargets
+              Resource: arn:aws:elasticloadbalancing:*:*:targetgroup/*/*
+            - Effect: Allow
+              Action:
+              - elasticloadbalancing:SetWebAcl
+              - elasticloadbalancing:ModifyListener
+              - elasticloadbalancing:AddListenerCertificates
+              - elasticloadbalancing:RemoveListenerCertificates
+              - elasticloadbalancing:ModifyRule
+              - elasticloadbalancing:SetRulePriorities
+              Resource: "*"
+          PolicyName: root
+      RoleName: "aws-load-balancer-controller-{{.Cluster.Name}}"
+    Type: 'AWS::IAM::Role'
+# {{- end }}
   RemoteFilesEncryptionKey:
     Type: "AWS::KMS::Key"
     Properties:
diff --git a/cluster/config-defaults.yaml b/cluster/config-defaults.yaml
index eb428c247b..602b0620c1 100644
--- a/cluster/config-defaults.yaml
+++ b/cluster/config-defaults.yaml
@@ -1290,3 +1290,7 @@ aws_vpc_cni_custom_networking: "false"
 aws_vpc_cni_enable_network_policy: "false"
 # specify the network policy enforcement mode.
 aws_vpc_cni_network_policy_enforcing_mode: "standard"
+
+# aws-load-balancer-controller resource settings
+aws_load_balancer_controller_cpu: "100m"
+aws_load_balancer_controller_mem_max: "4Gi"
diff --git a/cluster/manifests/02-admission-control/config.yaml b/cluster/manifests/02-admission-control/config.yaml
index e2558e5369..0c9025dde5 100644
--- a/cluster/manifests/02-admission-control/config.yaml
+++ b/cluster/manifests/02-admission-control/config.yaml
@@ -13,6 +13,9 @@ data:
   dns.default.subdomain-max-length: "{{ .Cluster.ConfigItems.subdomain_max_length }}"
 
   generic.prevent-write-operations.enable: "{{ .Cluster.ConfigItems.teapot_admission_controller_prevent_write_operations }}"
+{{- if and (eq .Cluster.Provider "zalando-eks") (eq .Cluster.ConfigItems.eks_ip_family "ipv6") }}
+  generic.inject-albc-defaults.enable: "true"
+{{- end }}
 
   pod.container-resource-control.min-memory-request: "25Mi"
   pod.container-resource-control.default-cpu-request: "{{ .Cluster.ConfigItems.teapot_admission_controller_default_cpu_request }}"
diff --git a/cluster/manifests/02-admission-control/deployment.yaml b/cluster/manifests/02-admission-control/deployment.yaml
index 379aac4277..364198a864 100644
--- a/cluster/manifests/02-admission-control/deployment.yaml
+++ b/cluster/manifests/02-admission-control/deployment.yaml
@@ -33,7 +33,7 @@ spec:
       priorityClassName: system-cluster-critical
       containers:
       - name: admission-controller
-        image: 926694233939.dkr.ecr.eu-central-1.amazonaws.com/production_namespace/teapot/admission-controller:master-252
+        image: 926694233939.dkr.ecr.eu-central-1.amazonaws.com/production_namespace/teapot/admission-controller:master-253
         lifecycle:
           preStop:
             exec:
diff --git a/cluster/manifests/aws-load-balancer-controller/deployment.yaml b/cluster/manifests/aws-load-balancer-controller/deployment.yaml
new file mode 100644
index 0000000000..58b86ca516
--- /dev/null
+++ b/cluster/manifests/aws-load-balancer-controller/deployment.yaml
@@ -0,0 +1,78 @@
+# {{- if and (eq .Cluster.Provider "zalando-eks") (eq .Cluster.ConfigItems.eks_ip_family "ipv6") }}
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: aws-load-balancer-controller
+  namespace: kube-system
+  labels:
+    application: kubernetes
+    component: aws-load-balancer-controller
+spec:
+  replicas: 2
+  selector:
+    matchLabels:
+      deployment: aws-load-balancer-controller
+  template:
+    metadata:
+      labels:
+        application: kubernetes
+        component: aws-load-balancer-controller
+        deployment: aws-load-balancer-controller
+    spec:
+      containers:
+      - args:
+        - "--aws-region={{.Cluster.Region}}"
+        - "--aws-vpc-id={{.Cluster.ConfigItems.vpc_id}}"
+        - "--cluster-name={{.Cluster.Name}}"
+        - --feature-gates=ServiceTypeLoadBalancerOnly=true
+        - --webhook-cert-file=admission-controller.pem
+        - --webhook-key-file=admission-controller-key.pem
+        - --default-load-balancer-scheme=internet-facing
+        - --default-target-type=ip
+        image: container-registry.zalando.net/teapot/aws-load-balancer-controller:v2.12.0-main-2.patched
+        livenessProbe:
+          failureThreshold: 2
+          httpGet:
+            path: /healthz
+            port: 61779
+            scheme: HTTP
+          initialDelaySeconds: 30
+          timeoutSeconds: 10
+        name: controller
+        ports:
+        - containerPort: 9443
+          name: webhook-server
+          protocol: TCP
+        resources:
+          limits:
+            cpu: {{ .Cluster.ConfigItems.aws_load_balancer_controller_cpu }}
+            memory: {{ .Cluster.ConfigItems.aws_load_balancer_controller_mem_max }}
+          requests:
+            cpu: {{ .Cluster.ConfigItems.aws_load_balancer_controller_cpu }}
+            memory: {{ .Cluster.ConfigItems.aws_load_balancer_controller_mem_max }}
+        securityContext:
+          allowPrivilegeEscalation: false
+          readOnlyRootFilesystem: true
+          runAsNonRoot: true
+        volumeMounts:
+        - mountPath: /tmp/k8s-webhook-server/serving-certs
+          name: cert
+          readOnly: true
+      priorityClassName: system-cluster-critical
+      securityContext:
+        fsGroup: 1337
+      serviceAccountName: aws-load-balancer-controller
+      terminationGracePeriodSeconds: 10
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - topologyKey: topology.kubernetes.io/zone
+            labelSelector:
+              matchLabels:
+                deployment: aws-load-balancer-controller
+      volumes:
+      - name: cert
+        secret:
+          defaultMode: 420
+          secretName: admission-controller-tls-certs
+# {{- end }}
diff --git a/cluster/manifests/aws-load-balancer-controller/ingressclassparams.yaml b/cluster/manifests/aws-load-balancer-controller/ingressclassparams.yaml
new file mode 100644
index 0000000000..17607d9224
--- /dev/null
+++ b/cluster/manifests/aws-load-balancer-controller/ingressclassparams.yaml
@@ -0,0 +1,266 @@
+# {{- if and (eq .Cluster.Provider "zalando-eks") (eq .Cluster.ConfigItems.eks_ip_family "ipv6") }}
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.14.0
+  labels:
+    application: kubernetes
+    component: aws-load-balancer-controller
+  name: ingressclassparams.elbv2.k8s.aws
+spec:
+  group: elbv2.k8s.aws
+  names:
+    kind: IngressClassParams
+    listKind: IngressClassParamsList
+    plural: ingressclassparams
+    singular: ingressclassparams
+  scope: Cluster
+  versions:
+  - additionalPrinterColumns:
+    - description: The Ingress Group name
+      jsonPath: .spec.group.name
+      name: GROUP-NAME
+      type: string
+    - description: The AWS Load Balancer scheme
+      jsonPath: .spec.scheme
+      name: SCHEME
+      type: string
+    - description: The AWS Load Balancer ipAddressType
+      jsonPath: .spec.ipAddressType
+      name: IP-ADDRESS-TYPE
+      type: string
+    - jsonPath: .metadata.creationTimestamp
+      name: AGE
+      type: date
+    name: v1beta1
+    schema:
+      openAPIV3Schema:
+        description: IngressClassParams is the Schema for the IngressClassParams API
+        properties:
+          apiVersion:
+            description: |-
+              APIVersion defines the versioned schema of this representation of an object.
+              Servers should convert recognized schemas to the latest internal value, and
+              may reject unrecognized values.
+              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+            type: string
+          kind:
+            description: |-
+              Kind is a string value representing the REST resource this object represents.
+              Servers may infer this from the endpoint the client submits requests to.
+              Cannot be updated.
+              In CamelCase.
+              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: IngressClassParamsSpec defines the desired state of IngressClassParams
+            properties:
+              certificateArn:
+                description: CertificateArn specifies the ARN of the certificates
+                  for all Ingresses that belong to IngressClass with this IngressClassParams.
+                items:
+                  type: string
+                type: array
+              group:
+                description: Group defines the IngressGroup for all Ingresses that
+                  belong to IngressClass with this IngressClassParams.
+                properties:
+                  name:
+                    description: Name is the name of IngressGroup.
+                    type: string
+                required:
+                - name
+                type: object
+              inboundCIDRs:
+                description: InboundCIDRs specifies the CIDRs that are allowed to
+                  access the Ingresses that belong to IngressClass with this IngressClassParams.
+                items:
+                  type: string
+                type: array
+              ipAddressType:
+                description: IPAddressType defines the ip address type for all Ingresses
+                  that belong to IngressClass with this IngressClassParams.
+                enum:
+                - ipv4
+                - dualstack
+                - dualstack-without-public-ipv4
+                type: string
+              ipamConfiguration:
+                description: IPAMConfiguration defines the IPAM settings for a Load
+                  Balancer.
+                properties:
+                  ipv4IPAMPoolId:
+                    description: IPv4IPAMPoolId defines the IPAM pool ID used for
+                      IPv4 Addresses on the ALB.
+                    type: string
+                type: object
+              listeners:
+                description: Listeners define a list of listeners with their protocol,
+                  port and attributes.
+                items:
+                  properties:
+                    listenerAttributes:
+                      description: The attributes of the listener
+                      items:
+                        description: Attributes defines custom attributes on resources.
+                        properties:
+                          key:
+                            description: The key of the attribute.
+                            type: string
+                          value:
+                            description: The value of the attribute.
+                            type: string
+                        required:
+                        - key
+                        - value
+                        type: object
+                      type: array
+                    port:
+                      description: The port of the listener
+                      format: int32
+                      type: integer
+                    protocol:
+                      description: The protocol of the listener
+                      type: string
+                  type: object
+                type: array
+              loadBalancerAttributes:
+                description: LoadBalancerAttributes define the custom attributes to
+                  LoadBalancers for all Ingress that that belong to IngressClass with
+                  this IngressClassParams.
+                items:
+                  description: Attributes defines custom attributes on resources.
+                  properties:
+                    key:
+                      description: The key of the attribute.
+                      type: string
+                    value:
+                      description: The value of the attribute.
+                      type: string
+                  required:
+                  - key
+                  - value
+                  type: object
+                type: array
+              minimumLoadBalancerCapacity:
+                description: MinimumLoadBalancerCapacity define the capacity reservation
+                  for LoadBalancers for all Ingress that belong to IngressClass with
+                  this IngressClassParams.
+                properties:
+                  capacityUnits:
+                    description: The Capacity Units Value.
+                    format: int32
+                    type: integer
+                required:
+                - capacityUnits
+                type: object
+              namespaceSelector:
+                description: |-
+                  NamespaceSelector restrict the namespaces of Ingresses that are allowed to specify the IngressClass with this IngressClassParams.
+                  * if absent or present but empty, it selects all namespaces.
+                properties:
+                  matchExpressions:
+                    description: matchExpressions is a list of label selector requirements.
+                      The requirements are ANDed.
+                    items:
+                      description: |-
+                        A label selector requirement is a selector that contains values, a key, and an operator that
+                        relates the key and values.
+                      properties:
+                        key:
+                          description: key is the label key that the selector applies
+                            to.
+                          type: string
+                        operator:
+                          description: |-
+                            operator represents a key's relationship to a set of values.
+                            Valid operators are In, NotIn, Exists and DoesNotExist.
+                          type: string
+                        values:
+                          description: |-
+                            values is an array of string values. If the operator is In or NotIn,
+                            the values array must be non-empty. If the operator is Exists or DoesNotExist,
+                            the values array must be empty. This array is replaced during a strategic
+                            merge patch.
+                          items:
+                            type: string
+                          type: array
+                          x-kubernetes-list-type: atomic
+                      required:
+                      - key
+                      - operator
+                      type: object
+                    type: array
+                    x-kubernetes-list-type: atomic
+                  matchLabels:
+                    additionalProperties:
+                      type: string
+                    description: |-
+                      matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
+                      map is equivalent to an element of matchExpressions, whose key field is "key", the
+                      operator is "In", and the values array contains only "value". The requirements are ANDed.
+                    type: object
+                type: object
+                x-kubernetes-map-type: atomic
+              scheme:
+                description: Scheme defines the scheme for all Ingresses that belong
+                  to IngressClass with this IngressClassParams.
+                enum:
+                - internal
+                - internet-facing
+                type: string
+              sslPolicy:
+                description: SSLPolicy specifies the SSL Policy for all Ingresses
+                  that belong to IngressClass with this IngressClassParams.
+                type: string
+              subnets:
+                description: Subnets defines the subnets for all Ingresses that belong
+                  to IngressClass with this IngressClassParams.
+                properties:
+                  ids:
+                    description: IDs specify the resource IDs of subnets. Exactly
+                      one of this or `tags` must be specified.
+                    items:
+                      description: SubnetID specifies a subnet ID.
+                      pattern: subnet-[0-9a-f]+
+                      type: string
+                    minItems: 1
+                    type: array
+                  tags:
+                    additionalProperties:
+                      items:
+                        type: string
+                      type: array
+                    description: |-
+                      Tags specifies subnets in the load balancer's VPC where each
+                      tag specified in the map key contains one of the values in the corresponding
+                      value list.
+                      Exactly one of this or `ids` must be specified.
+                    type: object
+                type: object
+              tags:
+                description: Tags defines list of Tags on AWS resources provisioned
+                  for Ingresses that belong to IngressClass with this IngressClassParams.
+                items:
+                  description: Tag defines a AWS Tag on resources.
+                  properties:
+                    key:
+                      description: The key of the tag.
+                      type: string
+                    value:
+                      description: The value of the tag.
+                      type: string
+                  required:
+                  - key
+                  - value
+                  type: object
+                type: array
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources: {}
+# {{- end }}
diff --git a/cluster/manifests/aws-load-balancer-controller/rbac.yaml b/cluster/manifests/aws-load-balancer-controller/rbac.yaml
new file mode 100644
index 0000000000..9f33a70ebe
--- /dev/null
+++ b/cluster/manifests/aws-load-balancer-controller/rbac.yaml
@@ -0,0 +1,234 @@
+# {{- if and (eq .Cluster.Provider "zalando-eks") (eq .Cluster.ConfigItems.eks_ip_family "ipv6") }}
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: aws-load-balancer-controller-leader-election-role
+  namespace: kube-system
+  labels:
+    application: kubernetes
+    component: aws-load-balancer-controller
+rules:
+- apiGroups:
+  - ""
+  resources:
+  - configmaps
+  verbs:
+  - create
+- apiGroups:
+  - ""
+  resourceNames:
+  - aws-load-balancer-controller-leader
+  resources:
+  - configmaps
+  verbs:
+  - get
+  - update
+  - patch
+- apiGroups:
+  - coordination.k8s.io
+  resources:
+  - leases
+  verbs:
+  - create
+- apiGroups:
+  - coordination.k8s.io
+  resourceNames:
+  - aws-load-balancer-controller-leader
+  resources:
+  - leases
+  verbs:
+  - get
+  - update
+  - patch
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: aws-load-balancer-controller-role
+  labels:
+    application: kubernetes
+    component: aws-load-balancer-controller
+rules:
+- apiGroups:
+  - ""
+  resources:
+  - configmaps
+  verbs:
+  - create
+  - delete
+  - get
+  - update
+- apiGroups:
+  - ""
+  resources:
+  - endpoints
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - ""
+  resources:
+  - events
+  verbs:
+  - create
+  - patch
+- apiGroups:
+  - ""
+  resources:
+  - namespaces
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - ""
+  resources:
+  - nodes
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - ""
+  resources:
+  - pods
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - ""
+  resources:
+  - pods/status
+  verbs:
+  - patch
+  - update
+- apiGroups:
+  - ""
+  resources:
+  - services
+  verbs:
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - ""
+  resources:
+  - services/status
+  verbs:
+  - patch
+  - update
+- apiGroups:
+  - discovery.k8s.io
+  resources:
+  - endpointslices
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - elbv2.k8s.aws
+  resources:
+  - ingressclassparams
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - elbv2.k8s.aws
+  resources:
+  - targetgroupbindings
+  verbs:
+  - create
+  - delete
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - elbv2.k8s.aws
+  resources:
+  - targetgroupbindings/status
+  verbs:
+  - patch
+  - update
+- apiGroups:
+  - extensions
+  resources:
+  - ingresses
+  verbs:
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - extensions
+  resources:
+  - ingresses/status
+  verbs:
+  - patch
+  - update
+- apiGroups:
+  - networking.k8s.io
+  resources:
+  - ingressclasses
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - networking.k8s.io
+  resources:
+  - ingresses
+  verbs:
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - networking.k8s.io
+  resources:
+  - ingresses/status
+  verbs:
+  - patch
+  - update
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  name: aws-load-balancer-controller-leader-election-rolebinding
+  namespace: kube-system
+  labels:
+    application: kubernetes
+    component: aws-load-balancer-controller
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: aws-load-balancer-controller-leader-election-role
+subjects:
+- kind: ServiceAccount
+  name: aws-load-balancer-controller
+  namespace: kube-system
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  name: aws-load-balancer-controller-rolebinding
+  labels:
+    application: kubernetes
+    component: aws-load-balancer-controller
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: aws-load-balancer-controller-role
+subjects:
+- kind: ServiceAccount
+  name: aws-load-balancer-controller
+  namespace: kube-system
+# {{- end }}
diff --git a/cluster/manifests/aws-load-balancer-controller/serviceaccount.yaml b/cluster/manifests/aws-load-balancer-controller/serviceaccount.yaml
new file mode 100644
index 0000000000..fd3838155c
--- /dev/null
+++ b/cluster/manifests/aws-load-balancer-controller/serviceaccount.yaml
@@ -0,0 +1,12 @@
+# {{- if and (eq .Cluster.Provider "zalando-eks") (eq .Cluster.ConfigItems.eks_ip_family "ipv6") }}
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: aws-load-balancer-controller
+  namespace: kube-system
+  labels:
+    application: kubernetes
+    component: aws-load-balancer-controller
+  annotations:
+    eks.amazonaws.com/role-arn: "arn:aws:iam::{{.Cluster.InfrastructureAccount | getAWSAccountID}}:role/aws-load-balancer-controller-{{.Cluster.Name}}"
+# {{- end }}
diff --git a/cluster/manifests/aws-load-balancer-controller/targetgroupbindings.yaml b/cluster/manifests/aws-load-balancer-controller/targetgroupbindings.yaml
new file mode 100644
index 0000000000..d83677f0c0
--- /dev/null
+++ b/cluster/manifests/aws-load-balancer-controller/targetgroupbindings.yaml
@@ -0,0 +1,454 @@
+# {{- if and (eq .Cluster.Provider "zalando-eks") (eq .Cluster.ConfigItems.eks_ip_family "ipv6") }}
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.14.0
+  labels:
+    application: kubernetes
+    component: aws-load-balancer-controller
+  name: targetgroupbindings.elbv2.k8s.aws
+spec:
+  group: elbv2.k8s.aws
+  names:
+    kind: TargetGroupBinding
+    listKind: TargetGroupBindingList
+    plural: targetgroupbindings
+    singular: targetgroupbinding
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - description: The Kubernetes Service's name
+      jsonPath: .spec.serviceRef.name
+      name: SERVICE-NAME
+      type: string
+    - description: The Kubernetes Service's port
+      jsonPath: .spec.serviceRef.port
+      name: SERVICE-PORT
+      type: string
+    - description: The AWS TargetGroup's TargetType
+      jsonPath: .spec.targetType
+      name: TARGET-TYPE
+      type: string
+    - description: The AWS TargetGroup's Amazon Resource Name
+      jsonPath: .spec.targetGroupARN
+      name: ARN
+      priority: 1
+      type: string
+    - description: The AWS TargetGroup's Name
+      jsonPath: .spec.targetGroupName
+      name: NAME
+      priority: 2
+      type: string
+    - jsonPath: .metadata.creationTimestamp
+      name: AGE
+      type: date
+    name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: TargetGroupBinding is the Schema for the TargetGroupBinding API
+        properties:
+          apiVersion:
+            description: |-
+              APIVersion defines the versioned schema of this representation of an object.
+              Servers should convert recognized schemas to the latest internal value, and
+              may reject unrecognized values.
+              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+            type: string
+          kind:
+            description: |-
+              Kind is a string value representing the REST resource this object represents.
+              Servers may infer this from the endpoint the client submits requests to.
+              Cannot be updated.
+              In CamelCase.
+              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: TargetGroupBindingSpec defines the desired state of TargetGroupBinding
+            properties:
+              assumeRoleExternalId:
+                description: IAM Role ARN to assume when calling AWS APIs. Needed
+                  to assume a role in another account and prevent the confused deputy
+                  problem. https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html
+                type: string
+              iamRoleArnToAssume:
+                description: IAM Role ARN to assume when calling AWS APIs. Useful
+                  if the target group is in a different AWS account
+                type: string
+              multiClusterTargetGroup:
+                description: MultiClusterTargetGroup Denotes if the TargetGroup is
+                  shared among multiple clusters
+                type: boolean
+              networking:
+                description: networking provides the networking setup for ELBV2 LoadBalancer
+                  to access targets in TargetGroup.
+                properties:
+                  ingress:
+                    description: List of ingress rules to allow ELBV2 LoadBalancer
+                      to access targets in TargetGroup.
+                    items:
+                      properties:
+                        from:
+                          description: |-
+                            List of peers which should be able to access the targets in TargetGroup.
+                            At least one NetworkingPeer should be specified.
+                          items:
+                            description: NetworkingPeer defines the source/destination
+                              peer for networking rules.
+                            properties:
+                              ipBlock:
+                                description: |-
+                                  IPBlock defines an IPBlock peer.
+                                  If specified, none of the other fields can be set.
+                                properties:
+                                  cidr:
+                                    description: |-
+                                      CIDR is the network CIDR.
+                                      Both IPV4 or IPV6 CIDR are accepted.
+                                    type: string
+                                required:
+                                - cidr
+                                type: object
+                              securityGroup:
+                                description: |-
+                                  SecurityGroup defines a SecurityGroup peer.
+                                  If specified, none of the other fields can be set.
+                                properties:
+                                  groupID:
+                                    description: GroupID is the EC2 SecurityGroupID.
+                                    type: string
+                                required:
+                                - groupID
+                                type: object
+                            type: object
+                          type: array
+                        ports:
+                          description: |-
+                            List of ports which should be made accessible on the targets in TargetGroup.
+                            If ports is empty or unspecified, it defaults to all ports with TCP.
+                          items:
+                            properties:
+                              port:
+                                anyOf:
+                                - type: integer
+                                - type: string
+                                description: |-
+                                  The port which traffic must match.
+                                  When NodePort endpoints(instance TargetType) is used, this must be a numerical port.
+                                  When Port endpoints(ip TargetType) is used, this can be either numerical or named port on pods.
+                                  if port is unspecified, it defaults to all ports.
+                                x-kubernetes-int-or-string: true
+                              protocol:
+                                description: |-
+                                  The protocol which traffic must match.
+                                  If protocol is unspecified, it defaults to TCP.
+                                enum:
+                                - TCP
+                                - UDP
+                                type: string
+                            type: object
+                          type: array
+                      required:
+                      - from
+                      - ports
+                      type: object
+                    type: array
+                type: object
+              serviceRef:
+                description: serviceRef is a reference to a Kubernetes Service and
+                  ServicePort.
+                properties:
+                  name:
+                    description: Name is the name of the Service.
+                    type: string
+                  port:
+                    anyOf:
+                    - type: integer
+                    - type: string
+                    description: Port is the port of the ServicePort.
+                    x-kubernetes-int-or-string: true
+                required:
+                - name
+                - port
+                type: object
+              targetGroupARN:
+                description: targetGroupARN is the Amazon Resource Name (ARN) for
+                  the TargetGroup.
+                type: string
+              targetGroupName:
+                description: targetGroupName is the Name of the TargetGroup.
+                type: string
+              targetType:
+                description: targetType is the TargetType of TargetGroup. If unspecified,
+                  it will be automatically inferred.
+                enum:
+                - instance
+                - ip
+                type: string
+            required:
+            - serviceRef
+            type: object
+          status:
+            description: TargetGroupBindingStatus defines the observed state of TargetGroupBinding
+            properties:
+              observedGeneration:
+                description: The generation observed by the TargetGroupBinding controller.
+                format: int64
+                type: integer
+            type: object
+        type: object
+    served: true
+    storage: false
+    subresources:
+      status: {}
+  - additionalPrinterColumns:
+    - description: The Kubernetes Service's name
+      jsonPath: .spec.serviceRef.name
+      name: SERVICE-NAME
+      type: string
+    - description: The Kubernetes Service's port
+      jsonPath: .spec.serviceRef.port
+      name: SERVICE-PORT
+      type: string
+    - description: The AWS TargetGroup's TargetType
+      jsonPath: .spec.targetType
+      name: TARGET-TYPE
+      type: string
+    - description: The AWS TargetGroup's Amazon Resource Name
+      jsonPath: .spec.targetGroupARN
+      name: ARN
+      priority: 1
+      type: string
+    - description: The AWS TargetGroup's Name
+      jsonPath: .spec.targetGroupName
+      name: NAME
+      priority: 2
+      type: string
+    - jsonPath: .metadata.creationTimestamp
+      name: AGE
+      type: date
+    name: v1beta1
+    schema:
+      openAPIV3Schema:
+        description: TargetGroupBinding is the Schema for the TargetGroupBinding API
+        properties:
+          apiVersion:
+            description: |-
+              APIVersion defines the versioned schema of this representation of an object.
+              Servers should convert recognized schemas to the latest internal value, and
+              may reject unrecognized values.
+              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+            type: string
+          kind:
+            description: |-
+              Kind is a string value representing the REST resource this object represents.
+              Servers may infer this from the endpoint the client submits requests to.
+              Cannot be updated.
+              In CamelCase.
+              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: TargetGroupBindingSpec defines the desired state of TargetGroupBinding
+            properties:
+              assumeRoleExternalId:
+                description: IAM Role ARN to assume when calling AWS APIs. Needed
+                  to assume a role in another account and prevent the confused deputy
+                  problem. https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html
+                type: string
+              iamRoleArnToAssume:
+                description: IAM Role ARN to assume when calling AWS APIs. Useful
+                  if the target group is in a different AWS account
+                type: string
+              ipAddressType:
+                description: ipAddressType specifies whether the target group is of
+                  type IPv4 or IPv6. If unspecified, it will be automatically inferred.
+                enum:
+                - ipv4
+                - ipv6
+                type: string
+              multiClusterTargetGroup:
+                description: MultiClusterTargetGroup Denotes if the TargetGroup is
+                  shared among multiple clusters
+                type: boolean
+              networking:
+                description: networking defines the networking rules to allow ELBV2
+                  LoadBalancer to access targets in TargetGroup.
+                properties:
+                  ingress:
+                    description: List of ingress rules to allow ELBV2 LoadBalancer
+                      to access targets in TargetGroup.
+                    items:
+                      description: NetworkingIngressRule defines a particular set
+                        of traffic that is allowed to access TargetGroup's targets.
+                      properties:
+                        from:
+                          description: |-
+                            List of peers which should be able to access the targets in TargetGroup.
+                            At least one NetworkingPeer should be specified.
+                          items:
+                            description: NetworkingPeer defines the source/destination
+                              peer for networking rules.
+                            properties:
+                              ipBlock:
+                                description: |-
+                                  IPBlock defines an IPBlock peer.
+                                  If specified, none of the other fields can be set.
+                                properties:
+                                  cidr:
+                                    description: |-
+                                      CIDR is the network CIDR.
+                                      Both IPV4 or IPV6 CIDR are accepted.
+                                    type: string
+                                required:
+                                - cidr
+                                type: object
+                              securityGroup:
+                                description: |-
+                                  SecurityGroup defines a SecurityGroup peer.
+                                  If specified, none of the other fields can be set.
+                                properties:
+                                  groupID:
+                                    description: GroupID is the EC2 SecurityGroupID.
+                                    type: string
+                                required:
+                                - groupID
+                                type: object
+                            type: object
+                          type: array
+                        ports:
+                          description: |-
+                            List of ports which should be made accessible on the targets in TargetGroup.
+                            If ports is empty or unspecified, it defaults to all ports with TCP.
+                          items:
+                            description: NetworkingPort defines the port and protocol
+                              for networking rules.
+                            properties:
+                              port:
+                                anyOf:
+                                - type: integer
+                                - type: string
+                                description: |-
+                                  The port which traffic must match.
+                                  When NodePort endpoints(instance TargetType) is used, this must be a numerical port.
+                                  When Port endpoints(ip TargetType) is used, this can be either numerical or named port on pods.
+                                  if port is unspecified, it defaults to all ports.
+                                x-kubernetes-int-or-string: true
+                              protocol:
+                                description: |-
+                                  The protocol which traffic must match.
+                                  If protocol is unspecified, it defaults to TCP.
+                                enum:
+                                - TCP
+                                - UDP
+                                type: string
+                            type: object
+                          type: array
+                      required:
+                      - from
+                      - ports
+                      type: object
+                    type: array
+                type: object
+              nodeSelector:
+                description: node selector for instance type target groups to only
+                  register certain nodes
+                properties:
+                  matchExpressions:
+                    description: matchExpressions is a list of label selector requirements.
+                      The requirements are ANDed.
+                    items:
+                      description: |-
+                        A label selector requirement is a selector that contains values, a key, and an operator that
+                        relates the key and values.
+                      properties:
+                        key:
+                          description: key is the label key that the selector applies
+                            to.
+                          type: string
+                        operator:
+                          description: |-
+                            operator represents a key's relationship to a set of values.
+                            Valid operators are In, NotIn, Exists and DoesNotExist.
+                          type: string
+                        values:
+                          description: |-
+                            values is an array of string values. If the operator is In or NotIn,
+                            the values array must be non-empty. If the operator is Exists or DoesNotExist,
+                            the values array must be empty. This array is replaced during a strategic
+                            merge patch.
+                          items:
+                            type: string
+                          type: array
+                          x-kubernetes-list-type: atomic
+                      required:
+                      - key
+                      - operator
+                      type: object
+                    type: array
+                    x-kubernetes-list-type: atomic
+                  matchLabels:
+                    additionalProperties:
+                      type: string
+                    description: |-
+                      matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
+                      map is equivalent to an element of matchExpressions, whose key field is "key", the
+                      operator is "In", and the values array contains only "value". The requirements are ANDed.
+                    type: object
+                type: object
+                x-kubernetes-map-type: atomic
+              serviceRef:
+                description: serviceRef is a reference to a Kubernetes Service and
+                  ServicePort.
+                properties:
+                  name:
+                    description: Name is the name of the Service.
+                    type: string
+                  port:
+                    anyOf:
+                    - type: integer
+                    - type: string
+                    description: Port is the port of the ServicePort.
+                    x-kubernetes-int-or-string: true
+                required:
+                - name
+                - port
+                type: object
+              targetGroupARN:
+                description: targetGroupARN is the Amazon Resource Name (ARN) for
+                  the TargetGroup.
+                type: string
+              targetGroupName:
+                description: targetGroupName is the Name of the TargetGroup.
+                type: string
+              targetType:
+                description: targetType is the TargetType of TargetGroup. If unspecified,
+                  it will be automatically inferred.
+                enum:
+                - instance
+                - ip
+                type: string
+              vpcID:
+                description: VpcID is the VPC of the TargetGroup. If unspecified,
+                  it will be automatically inferred.
+                type: string
+            required:
+            - serviceRef
+            type: object
+          status:
+            description: TargetGroupBindingStatus defines the observed state of TargetGroupBinding
+            properties:
+              observedGeneration:
+                description: The generation observed by the TargetGroupBinding controller.
+                format: int64
+                type: integer
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+# {{- end }}
diff --git a/cluster/manifests/aws-load-balancer-controller/vpa.yaml b/cluster/manifests/aws-load-balancer-controller/vpa.yaml
new file mode 100644
index 0000000000..e2d8a8dea8
--- /dev/null
+++ b/cluster/manifests/aws-load-balancer-controller/vpa.yaml
@@ -0,0 +1,22 @@
+# {{- if and (eq .Cluster.Provider "zalando-eks") (eq .Cluster.ConfigItems.eks_ip_family "ipv6") }}
+apiVersion: autoscaling.k8s.io/v1
+kind: VerticalPodAutoscaler
+metadata:
+  name: aws-load-balancer-controller
+  namespace: kube-system
+  labels:
+    application: kubernetes
+    component: aws-load-balancer-controller
+spec:
+  targetRef:
+    apiVersion: apps/v1
+    kind: Deployment
+    name: aws-load-balancer-controller
+  updatePolicy:
+    updateMode: Auto
+  resourcePolicy:
+    containerPolicies:
+    - containerName: controller
+      maxAllowed:
+        memory: {{ .Cluster.ConfigItems.aws_load_balancer_controller_mem_max }}
+# {{- end }}
diff --git a/test/e2e/run_e2e.sh b/test/e2e/run_e2e.sh
index 1535d5d0ec..c38369fe3a 100755
--- a/test/e2e/run_e2e.sh
+++ b/test/e2e/run_e2e.sh
@@ -215,7 +215,6 @@ if [ "$e2e" = true ]; then
             "Should audit API calls to create, update, patch, delete pods. \[Audit\] \[Zalando\]"
             "should validate permissions for \[Authorization\] \[RBAC\] \[Zalando\]" # TODO: Remains skipped until we remove the older RBAC setup
             "should creating a working mysql cluster" # upstream test which does not work with IPv6
-            "Should create DNS entry via Service \[Zalando\]" # Depends on service Type LoadBalancer which doesn't work with IPv6.
         )
     fi
 


Reviews:
user: login: demonCoder95
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T08:44:57Z
user: login: demonCoder95
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T08:45:48Z
user: login: demonCoder95
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T08:48:28Z
user: login: demonCoder95
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T08:57:01Z
user: login: linki
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T09:16:18Z
user: login: linki
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T09:23:22Z
user: login: mikkeloscar
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T09:25:36Z
user: login: linki
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T09:27:06Z
user: login: linki
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T09:32:53Z
user: login: demonCoder95
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T09:37:55Z
user: login: linki
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T09:56:03Z
user: login: linki
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T10:15:11Z

Review comments:
diff_hunk: @@ -3222,6 +3222,221 @@ Resources:
           PolicyName: root
       RoleName: "{{.Cluster.LocalID}}-audittrail-adapter"
     Type: 'AWS::IAM::Role'
+# {{- if eq .Cluster.Provider "zalando-eks"}}
+  AWSLoadBalancerControllerIAMRole:
+    Properties:
+      AssumeRolePolicyDocument: !Sub
+        - |
+          {
+            "Version": "2012-10-17",
+            "Statement": [
+              {
+                "Effect": "Allow",
+                "Principal": {
+                  "Federated": [
+                    "arn:aws:iam::${AWS::AccountId}:oidc-provider/${OIDC}"
+                  ]
+                },
+                "Action": [
+                  "sts:AssumeRoleWithWebIdentity"
+                ],
+                "Condition": {
+                  "StringEquals": {
+                    "${OIDC}:sub": "system:serviceaccount:kube-system:aws-load-balancer-controller"
+                  }
+                }
+              }
+            ]
+          }
+{{- if eq .Cluster.Provider "zalando-eks" }}
+        - OIDC: !Select [1, !Split ["//", !GetAtt EKSCluster.OpenIdConnectIssuerUrl]]
+{{- else }}
+        - OIDC: "{{.Cluster.LocalID}}.{{.Values.hosted_zone}}"
+{{- end }}
path: cluster/cluster.yaml
user: login: demonCoder95
body: The outer block starting on line 3225 has the condition `.Cluster.Provider == "zalando-eks"` already, won't that make it impossible for this `else` block to ever be executed?
created_at: 2025-04-29T08:44:57Z
author_association: MEMBER
diff_hunk: @@ -3222,6 +3222,221 @@ Resources:
           PolicyName: root
       RoleName: "{{.Cluster.LocalID}}-audittrail-adapter"
     Type: 'AWS::IAM::Role'
+# {{- if eq .Cluster.Provider "zalando-eks"}}
+  AWSLoadBalancerControllerIAMRole:
+    Properties:
+      AssumeRolePolicyDocument: !Sub
+        - |
+          {
+            "Version": "2012-10-17",
+            "Statement": [
+              {
+                "Effect": "Allow",
+                "Principal": {
+                  "Federated": [
+                    "arn:aws:iam::${AWS::AccountId}:oidc-provider/${OIDC}"
+                  ]
+                },
+                "Action": [
+                  "sts:AssumeRoleWithWebIdentity"
+                ],
+                "Condition": {
+                  "StringEquals": {
+                    "${OIDC}:sub": "system:serviceaccount:kube-system:aws-load-balancer-controller"
+                  }
+                }
+              }
+            ]
+          }
+{{- if eq .Cluster.Provider "zalando-eks" }}
+        - OIDC: !Select [1, !Split ["//", !GetAtt EKSCluster.OpenIdConnectIssuerUrl]]
+{{- else }}
+        - OIDC: "{{.Cluster.LocalID}}.{{.Values.hosted_zone}}"
+{{- end }}
path: cluster/cluster.yaml
user: login: demonCoder95
body: Maybe we need to terminate the outer block on line 3252?
created_at: 2025-04-29T08:45:48Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,78 @@
+# {{- if eq .Cluster.Provider "zalando-eks"}}
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: aws-load-balancer-controller
+  namespace: kube-system
+  labels:
+    application: kubernetes
+    component: aws-load-balancer-controller
+spec:
+  replicas: 2
+  selector:
+    matchLabels:
+      deployment: aws-load-balancer-controller
+  template:
+    metadata:
+      labels:
+        application: kubernetes
+        component: aws-load-balancer-controller
+        deployment: aws-load-balancer-controller
+    spec:
+      containers:
+      - args:
+        - "--aws-region={{.Cluster.Region}}"
+        - "--aws-vpc-id={{.Cluster.ConfigItems.vpc_id}}"
+        - "--cluster-name={{.Cluster.Name}}"
+        - --feature-gates=ServiceTypeLoadBalancerOnly=true
+        - --webhook-cert-file=admission-controller.pem
+        - --webhook-key-file=admission-controller-key.pem
+        - --default-load-balancer-scheme=internet-facing
+        - --default-target-type=ip
+        image: container-registry.zalando.net/teapot/aws-load-balancer-controller:v2.12.0-main-2.patched
+        livenessProbe:
+          failureThreshold: 2
+          httpGet:
+            path: /healthz
+            port: 61779
+            scheme: HTTP
+          initialDelaySeconds: 30
+          timeoutSeconds: 10
+        name: controller
+        ports:
+        - containerPort: 9443
+          name: webhook-server
+          protocol: TCP
+        resources:
+          limits:
+            cpu: 100m
path: cluster/manifests/aws-load-balancer-controller/deployment.yaml
user: login: demonCoder95
body: Would it make sense to use a config-item for CPU as well?
created_at: 2025-04-29T08:48:28Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,22 @@
+# {{- if eq .Cluster.Provider "zalando-eks"}}
+apiVersion: autoscaling.k8s.io/v1
+kind: VerticalPodAutoscaler
+metadata:
+  name: aws-load-balancer-controller
+  namespace: kube-system
+  labels:
+    application: kubernetes
+    component: aws-load-balancer-controller
+spec:
+  targetRef:
+    apiVersion: apps/v1
+    kind: Deployment
+    name: aws-load-balancer-controller
+  updatePolicy:
+    updateMode: Auto
+  resourcePolicy:
+    containerPolicies:
+    - containerName: controller
+      maxAllowed:
+        memory: {{ .Cluster.ConfigItems.aws_load_balancer_controller_mem }}
path: cluster/manifests/aws-load-balancer-controller/vpa.yaml
user: login: demonCoder95
body: This should be `minAllowed`, I think. Having this `maxAllowed` defeats the purpose of having a VPA as the memory requests will never be increased. Since we've already set the memory requests to the same value.

We should also change the config-item's name to append `_min` to clarify this.
created_at: 2025-04-29T08:57:01Z
author_association: MEMBER
diff_hunk: @@ -3222,6 +3222,221 @@ Resources:
           PolicyName: root
       RoleName: "{{.Cluster.LocalID}}-audittrail-adapter"
     Type: 'AWS::IAM::Role'
+# {{- if eq .Cluster.Provider "zalando-eks"}}
+  AWSLoadBalancerControllerIAMRole:
+    Properties:
+      AssumeRolePolicyDocument: !Sub
+        - |
+          {
+            "Version": "2012-10-17",
+            "Statement": [
+              {
+                "Effect": "Allow",
+                "Principal": {
+                  "Federated": [
+                    "arn:aws:iam::${AWS::AccountId}:oidc-provider/${OIDC}"
+                  ]
+                },
+                "Action": [
+                  "sts:AssumeRoleWithWebIdentity"
+                ],
+                "Condition": {
+                  "StringEquals": {
+                    "${OIDC}:sub": "system:serviceaccount:kube-system:aws-load-balancer-controller"
+                  }
+                }
+              }
+            ]
+          }
+{{- if eq .Cluster.Provider "zalando-eks" }}
+        - OIDC: !Select [1, !Split ["//", !GetAtt EKSCluster.OpenIdConnectIssuerUrl]]
+{{- else }}
+        - OIDC: "{{.Cluster.LocalID}}.{{.Values.hosted_zone}}"
+{{- end }}
path: cluster/cluster.yaml
user: login: linki
body: I left it just in case we removed the outer if (to run this controller on legacy clusters) in which case it would just work. Otherwise, it's likely to be missed.

However, I dropped a similar if in another place, so I'm going to remove it here, too.
created_at: 2025-04-29T09:16:18Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,78 @@
+# {{- if eq .Cluster.Provider "zalando-eks"}}
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: aws-load-balancer-controller
+  namespace: kube-system
+  labels:
+    application: kubernetes
+    component: aws-load-balancer-controller
+spec:
+  replicas: 2
+  selector:
+    matchLabels:
+      deployment: aws-load-balancer-controller
+  template:
+    metadata:
+      labels:
+        application: kubernetes
+        component: aws-load-balancer-controller
+        deployment: aws-load-balancer-controller
+    spec:
+      containers:
+      - args:
+        - "--aws-region={{.Cluster.Region}}"
+        - "--aws-vpc-id={{.Cluster.ConfigItems.vpc_id}}"
+        - "--cluster-name={{.Cluster.Name}}"
+        - --feature-gates=ServiceTypeLoadBalancerOnly=true
+        - --webhook-cert-file=admission-controller.pem
+        - --webhook-key-file=admission-controller-key.pem
+        - --default-load-balancer-scheme=internet-facing
+        - --default-target-type=ip
+        image: container-registry.zalando.net/teapot/aws-load-balancer-controller:v2.12.0-main-2.patched
+        livenessProbe:
+          failureThreshold: 2
+          httpGet:
+            path: /healthz
+            port: 61779
+            scheme: HTTP
+          initialDelaySeconds: 30
+          timeoutSeconds: 10
+        name: controller
+        ports:
+        - containerPort: 9443
+          name: webhook-server
+          protocol: TCP
+        resources:
+          limits:
+            cpu: 100m
path: cluster/manifests/aws-load-balancer-controller/deployment.yaml
user: login: linki
body: Added
created_at: 2025-04-29T09:23:22Z
author_association: MEMBER
diff_hunk: @@ -3222,6 +3222,217 @@ Resources:
           PolicyName: root
       RoleName: "{{.Cluster.LocalID}}-audittrail-adapter"
     Type: 'AWS::IAM::Role'
+# {{- if eq .Cluster.Provider "zalando-eks"}}
path: cluster/cluster.yaml
user: login: mikkeloscar
body: Should we also condition all this to `eks_ip_family: "ipv6"`? Since on IPv4 the normal "in-tree" controller works.
created_at: 2025-04-29T09:25:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,22 @@
+# {{- if eq .Cluster.Provider "zalando-eks"}}
+apiVersion: autoscaling.k8s.io/v1
+kind: VerticalPodAutoscaler
+metadata:
+  name: aws-load-balancer-controller
+  namespace: kube-system
+  labels:
+    application: kubernetes
+    component: aws-load-balancer-controller
+spec:
+  targetRef:
+    apiVersion: apps/v1
+    kind: Deployment
+    name: aws-load-balancer-controller
+  updatePolicy:
+    updateMode: Auto
+  resourcePolicy:
+    containerPolicies:
+    - containerName: controller
+      maxAllowed:
+        memory: {{ .Cluster.ConfigItems.aws_load_balancer_controller_mem }}
path: cluster/manifests/aws-load-balancer-controller/vpa.yaml
user: login: linki
body: This was indeed wrong but I left it at `maxAllowed`. I took other controller definitions as a guide, such as this one: https://github.com/zalando-incubator/kubernetes-on-aws/blob/dev/cluster/manifests/pdb-controller/vpa.yaml and others.

It initially starts it with a lot of extra memory and then lets VPA take care of adjusting it _down_ over time.
created_at: 2025-04-29T09:27:06Z
author_association: MEMBER
diff_hunk: @@ -3222,6 +3222,217 @@ Resources:
           PolicyName: root
       RoleName: "{{.Cluster.LocalID}}-audittrail-adapter"
     Type: 'AWS::IAM::Role'
+# {{- if eq .Cluster.Provider "zalando-eks"}}
path: cluster/cluster.yaml
user: login: linki
body: I didn't think about that case. We could do it, but it should also work like it is now. `admission-controller` shifts _all_ Services with LoadBalancer type to the new controller on EKS, regardless of IP version.

So, it depends on which controller we prefer. I think there's benefit in using a single controller for EKS rather than two based on IP version.

Do we intend to run IPv4 EKS clusters going forward?
created_at: 2025-04-29T09:32:52Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,22 @@
+# {{- if eq .Cluster.Provider "zalando-eks"}}
+apiVersion: autoscaling.k8s.io/v1
+kind: VerticalPodAutoscaler
+metadata:
+  name: aws-load-balancer-controller
+  namespace: kube-system
+  labels:
+    application: kubernetes
+    component: aws-load-balancer-controller
+spec:
+  targetRef:
+    apiVersion: apps/v1
+    kind: Deployment
+    name: aws-load-balancer-controller
+  updatePolicy:
+    updateMode: Auto
+  resourcePolicy:
+    containerPolicies:
+    - containerName: controller
+      maxAllowed:
+        memory: {{ .Cluster.ConfigItems.aws_load_balancer_controller_mem }}
path: cluster/manifests/aws-load-balancer-controller/vpa.yaml
user: login: demonCoder95
body: I see, you've increased the memory a bit, so I suppose that's fine this way as well :)
created_at: 2025-04-29T09:37:54Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,22 @@
+# {{- if eq .Cluster.Provider "zalando-eks"}}
+apiVersion: autoscaling.k8s.io/v1
+kind: VerticalPodAutoscaler
+metadata:
+  name: aws-load-balancer-controller
+  namespace: kube-system
+  labels:
+    application: kubernetes
+    component: aws-load-balancer-controller
+spec:
+  targetRef:
+    apiVersion: apps/v1
+    kind: Deployment
+    name: aws-load-balancer-controller
+  updatePolicy:
+    updateMode: Auto
+  resourcePolicy:
+    containerPolicies:
+    - containerName: controller
+      maxAllowed:
+        memory: {{ .Cluster.ConfigItems.aws_load_balancer_controller_mem }}
path: cluster/manifests/aws-load-balancer-controller/vpa.yaml
user: login: linki
body: `4Gi` seems to be what other components use. It's more like a placeholder than a consciously chosen value.
created_at: 2025-04-29T09:56:02Z
author_association: MEMBER
diff_hunk: @@ -3222,6 +3222,217 @@ Resources:
           PolicyName: root
       RoleName: "{{.Cluster.LocalID}}-audittrail-adapter"
     Type: 'AWS::IAM::Role'
+# {{- if eq .Cluster.Provider "zalando-eks"}}
path: cluster/cluster.yaml
user: login: linki
body: Given that we have existing EKS/IPv4 clusters it's probably better to add the check.
created_at: 2025-04-29T10:15:11Z
author_association: MEMBER

PR comments:
user: login: linki
created_at: 2025-04-28T16:22:17Z
body: 👍 
author_association: MEMBER
user: login: demonCoder95
created_at: 2025-04-29T09:41:53Z
body: 👍 
author_association: MEMBER
user: login: mikkeloscar
created_at: 2025-04-29T11:59:34Z
body: :+1: 
author_association: CONTRIBUTOR
user: login: linki
created_at: 2025-04-29T13:36:41Z
body: 👍 
author_association: MEMBER