Repository: Kong/kubernetes-ingress-controller
Pull request #2402 (2022-04-07 15:58:02)

feat: support multiple backendRefs for HTTPRoute load-balancing
**What this PR does / why we need it**:

This adds support for multiple `backendRefs` in `HTTPRoute` objects, enabling weighted load-balancing between multiple services for any specific routing rule.

**Which issue this PR fixes**

Resolves https://github.com/Kong/kubernetes-ingress-controller/issues/2166

**Notes**:

Also running E2E tests for this: https://github.com/Kong/kubernetes-ingress-controller/actions/runs/2110253994 :heavy_check_mark: 

**PR Readiness Checklist**:

- [x] the `CHANGELOG.md` release notes have been updated


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a4abab1d5..bcacc76b2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
 # Table of Contents
 
+ - [2.4.0](#240)
  - [2.3.1](#231)
  - [2.3.0](#230)
  - [2.2.1](#221)
@@ -46,6 +47,19 @@
  - [0.0.5](#005)
  - [0.0.4 and prior](#004-and-prior)
 
+## [2.4.0]
+
+> Release date: TBD
+
+#### Added
+
+- `HTTPRoute` resources now support multiple `backendRefs` with a round-robin
+  load-balancing strategy applied by default across the `Endpoints` or the
+  `Services` (if the `ingress.kubernetes.io/service-upstream`
+  annotation is set). They also now support weights to enable more
+  fine-tuning of the load-balancing between those backend services.
+  [#2166](https://github.com/Kong/kubernetes-ingress-controller/issues/2166)
+
 ## [2.3.1]
 
 > Release date: 2022-04-07
@@ -1658,7 +1672,8 @@ Please read the changelog and test in your environment.
  - The initial versions  were rapildy iterated to deliver
    a working ingress controller.
 
-[2.3.0]: https://github.com/kong/kubernetes-ingress-controller/compare/v2.3.0...v2.3.1
+[2.4.0]: https://github.com/kong/kubernetes-ingress-controller/compare/v2.3.1...v2.4.0
+[2.3.1]: https://github.com/kong/kubernetes-ingress-controller/compare/v2.3.0...v2.3.1
 [2.3.0]: https://github.com/kong/kubernetes-ingress-controller/compare/v2.2.1...v2.3.0
 [2.2.1]: https://github.com/kong/kubernetes-ingress-controller/compare/v2.2.0...v2.2.1
 [2.2.0]: https://github.com/kong/kubernetes-ingress-controller/compare/v2.1.1...v2.2.0
diff --git a/examples/gateway-httproute.yaml b/examples/gateway-httproute.yaml
index 8b972a545..c486b806d 100644
--- a/examples/gateway-httproute.yaml
+++ b/examples/gateway-httproute.yaml
@@ -41,6 +41,42 @@ spec:
     app: httpbin
   type: ClusterIP
 ---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: nginx
+  labels:
+    app: nginx
+spec:
+  selector:
+    matchLabels:
+      app: nginx
+  template:
+    metadata:
+      labels:
+        app: nginx
+    spec:
+      containers:
+      - name: nginx
+        image: nginx
+        ports:
+        - containerPort: 80
+---
+apiVersion: v1
+kind: Service
+metadata:
+  labels:
+    app: nginx
+  name: nginx
+spec:
+  ports:
+  - port: 8080
+    protocol: TCP
+    targetPort: 80
+  selector:
+    app: nginx
+  type: ClusterIP
+---
 kind: GatewayClass
 apiVersion: gateway.networking.k8s.io/v1alpha2
 metadata:
@@ -62,7 +98,7 @@ spec:
 apiVersion: gateway.networking.k8s.io/v1alpha2
 kind: HTTPRoute
 metadata:
-  name: httpbin
+  name: httproute-testing
 spec:
   parentRefs:
   - name: kong
@@ -70,7 +106,11 @@ spec:
   - matches:
     - path:
         type: PathPrefix
-        value: /httpbin
+        value: /httproute-testing
     backendRefs:
     - name: httpbin
       port: 80
+      weight: 75
+    - name: nginx
+      port: 8080
+      weight: 25
diff --git a/internal/dataplane/kongstate/kongstate.go b/internal/dataplane/kongstate/kongstate.go
index d88b0f896..f11f8c413 100644
--- a/internal/dataplane/kongstate/kongstate.go
+++ b/internal/dataplane/kongstate/kongstate.go
@@ -13,7 +13,6 @@ import (
 	"github.com/kong/kubernetes-ingress-controller/v2/internal/store"
 	"github.com/kong/kubernetes-ingress-controller/v2/internal/util"
 	"github.com/kong/kubernetes-ingress-controller/v2/internal/validation/consumers/credentials"
-	configurationv1 "github.com/kong/kubernetes-ingress-controller/v2/pkg/apis/configuration/v1"
 )
 
 // KongState holds the configuration that should be applied to Kong.
@@ -134,21 +133,19 @@ func (ks *KongState) FillConsumersAndCredentials(log logrus.FieldLogger, s store
 func (ks *KongState) FillOverrides(log logrus.FieldLogger, s store.Storer) {
 	for i := 0; i < len(ks.Services); i++ {
 		// Services
-		anns := ks.Services[i].K8sService.Annotations
-		kongIngress, err := getKongIngressForService(s, ks.Services[i].K8sService)
+		kongIngress, err := getKongIngressForServices(s, ks.Services[i].K8sServices)
 		if err != nil {
-			log.WithFields(logrus.Fields{
-				"service_name":      ks.Services[i].K8sService.Name,
-				"service_namespace": ks.Services[i].K8sService.Namespace,
-			}).Errorf("failed to fetch KongIngress resource for Service: %v", err)
+			log.Errorf("failed to fetch KongIngress resource for Services %s: %v", PrettyPrintServiceList(ks.Services[i].K8sServices), err)
+			continue
+		}
+
+		for _, svc := range ks.Services[i].K8sServices {
+			ks.Services[i].override(kongIngress, svc.Annotations)
 		}
-		ks.Services[i].override(kongIngress, anns)
 
 		// Routes
 		for j := 0; j < len(ks.Services[i].Routes); j++ {
-			var kongIngress *configurationv1.KongIngress
-			var err error
-			kongIngress, err = getKongIngressFromObjectMeta(s, &ks.Services[i].Routes[j].Ingress)
+			kongIngress, err := getKongIngressFromObjectMeta(s, &ks.Services[i].Routes[j].Ingress)
 			if err != nil {
 				log.WithFields(logrus.Fields{
 					"resource_name":      ks.Services[i].Routes[j].Ingress.Name,
@@ -162,17 +159,15 @@ func (ks *KongState) FillOverrides(log logrus.FieldLogger, s store.Storer) {
 
 	// Upstreams
 	for i := 0; i < len(ks.Upstreams); i++ {
-		kongIngress, err := getKongIngressForService(s,
-			ks.Upstreams[i].Service.K8sService)
-		anns := ks.Upstreams[i].Service.K8sService.Annotations
+		kongIngress, err := getKongIngressForServices(s, ks.Upstreams[i].Service.K8sServices)
 		if err != nil {
-			log.WithFields(logrus.Fields{
-				"service_name":      ks.Upstreams[i].Service.K8sService.Name,
-				"service_namespace": ks.Upstreams[i].Service.K8sService.Namespace,
-			}).Errorf("failed to fetch KongIngress resource for Service: %v", err)
+			log.Errorf("failed to fetch KongIngress resource for Services %s: %v", PrettyPrintServiceList(ks.Upstreams[i].Service.K8sServices), err)
 			continue
 		}
-		ks.Upstreams[i].override(kongIngress, anns)
+
+		for _, svc := range ks.Upstreams[i].Service.K8sServices {
+			ks.Upstreams[i].override(kongIngress, svc.Annotations)
+		}
 	}
 }
 
@@ -209,12 +204,11 @@ func (ks *KongState) getPluginRelations() map[string]util.ForeignRelations {
 
 	for i := range ks.Services {
 		// service
-		svc := ks.Services[i].K8sService
-		pluginList := annotations.ExtractKongPluginsFromAnnotations(
-			svc.GetAnnotations())
-		for _, pluginName := range pluginList {
-			addServiceRelation(svc.Namespace, pluginName,
-				*ks.Services[i].Name)
+		for _, svc := range ks.Services[i].K8sServices {
+			pluginList := annotations.ExtractKongPluginsFromAnnotations(svc.GetAnnotations())
+			for _, pluginName := range pluginList {
+				addServiceRelation(svc.Namespace, pluginName, *ks.Services[i].Name)
+			}
 		}
 		// route
 		for j := range ks.Services[i].Routes {
diff --git a/internal/dataplane/kongstate/service.go b/internal/dataplane/kongstate/service.go
index 64179c674..05ba96842 100644
--- a/internal/dataplane/kongstate/service.go
+++ b/internal/dataplane/kongstate/service.go
@@ -15,11 +15,12 @@ import (
 // service and other k8s metadata.
 type Service struct {
 	kong.Service
-	Backend    ServiceBackend
-	Namespace  string
-	Routes     []Route
-	Plugins    []kong.Plugin
-	K8sService corev1.Service
+	Namespace string
+	Routes    []Route
+	Plugins   []kong.Plugin
+
+	Backends    []ServiceBackend
+	K8sServices map[string]*corev1.Service
 }
 
 // overrideByKongIngress sets Service fields by KongIngress
diff --git a/internal/dataplane/kongstate/types.go b/internal/dataplane/kongstate/types.go
index 761dac81f..1b51117af 100644
--- a/internal/dataplane/kongstate/types.go
+++ b/internal/dataplane/kongstate/types.go
@@ -23,6 +23,7 @@ type PortDef struct {
 
 	// Name is the port name as stated in the Kubernetes service. Must be set iff Mode == PortModeName.
 	Name string
+
 	// Number is the port number. Must be set iff PortMode == PortModeNumber.
 	Number int32
 }
@@ -42,10 +43,13 @@ func (p *PortDef) CanonicalString() string {
 }
 
 type ServiceBackend struct {
-	Name string
-	Port PortDef
+	Name    string
+	PortDef PortDef
+	Weight  *int32
 }
 
+type ServiceBackends []ServiceBackend
+
 // Target is a wrapper around Target object in Kong.
 type Target struct {
 	kong.Target
diff --git a/internal/dataplane/kongstate/util.go b/internal/dataplane/kongstate/util.go
index 4e2994c4b..08cbe3ef3 100644
--- a/internal/dataplane/kongstate/util.go
+++ b/internal/dataplane/kongstate/util.go
@@ -16,13 +16,36 @@ import (
 	configurationv1 "github.com/kong/kubernetes-ingress-controller/v2/pkg/apis/configuration/v1"
 )
 
-func getKongIngressForService(s store.Storer, service corev1.Service) (
-	*configurationv1.KongIngress, error) {
-	confName := annotations.ExtractConfigurationName(service.Annotations)
-	if confName == "" {
-		return nil, nil
+func getKongIngressForServices(
+	s store.Storer,
+	services map[string]*corev1.Service,
+) (*configurationv1.KongIngress, error) {
+	// loop through each service and retrieve the attached KongIngress resources.
+	// there can only be one KongIngress for a group of services: either one of
+	// them is configured with a KongIngress and this configures the Kong Service
+	// or Upstream OR all of them can be configured but they must be configured
+	// with the same KongIngress.
+	for _, svc := range services {
+		// check if the service is even configured with a KongIngress
+		confName := annotations.ExtractConfigurationName(svc.Annotations)
+		if confName == "" {
+			continue // some other service in the group may yet have a KongIngress attachment
+		}
+
+		// retrieve the attached KongIngress for the service
+		kongIngress, err := s.GetKongIngress(svc.Namespace, confName)
+		if err != nil {
+			return nil, err
+		}
+
+		// we found the KongIngress for these services. We don't have to check any
+		// further services as validation is expected to ensure all these Services
+		// already are annotated with the exact same overrides.
+		return kongIngress, nil
 	}
-	return s.GetKongIngress(service.Namespace, confName)
+
+	// there are no KongIngress resources for these services.
+	return nil, nil
 }
 
 func getKongIngressFromObjectMeta(s store.Storer, obj *util.K8sObjectInfo) (
@@ -221,6 +244,23 @@ func SecretToConfiguration(
 	return config, nil
 }
 
+// PrettyPrintServiceList makes a clean printable list of a map of Kubernetes
+// services for the purpose of logging (errors, info, e.t.c.).
+func PrettyPrintServiceList(services map[string]*corev1.Service) string {
+	var serviceList string
+	first := true
+	for _, svc := range services {
+		if !first {
+			serviceList = serviceList + ", "
+		}
+		serviceList = serviceList + svc.Namespace + "/" + svc.Name
+		if first {
+			first = false
+		}
+	}
+	return serviceList
+}
+
 // plugin is a intermediate type to hold plugin related configuration
 type plugin struct {
 	Name   string
diff --git a/internal/dataplane/parser/ingressrules.go b/internal/dataplane/parser/ingressrules.go
index ca0533743..0735edfde 100644
--- a/internal/dataplane/parser/ingressrules.go
+++ b/internal/dataplane/parser/ingressrules.go
@@ -1,8 +1,12 @@
 package parser
 
 import (
+	"fmt"
+	"strings"
+
 	"github.com/kong/go-kong/kong"
 	"github.com/sirupsen/logrus"
+	corev1 "k8s.io/api/core/v1"
 	networkingv1 "k8s.io/api/networking/v1"
 	networkingv1beta1 "k8s.io/api/networking/v1beta1"
 
@@ -37,42 +41,59 @@ func mergeIngressRules(objs ...ingressRules) ingressRules {
 	return result
 }
 
-func (ir *ingressRules) populateServices(log logrus.FieldLogger, s store.Storer) {
+func (ir *ingressRules) populateServices(log logrus.FieldLogger, s store.Storer) error {
 	// populate Kubernetes Service
 	for key, service := range ir.ServiceNameToServices {
-		k8sSvc, err := s.GetService(service.Namespace, service.Backend.Name)
-		if err != nil {
-			log.WithFields(logrus.Fields{
-				"service_name":      service.Backend.Name,
-				"service_namespace": service.Namespace,
-			}).Errorf("failed to fetch service: %v", err)
+		if service.K8sServices == nil {
+			service.K8sServices = make(map[string]*corev1.Service)
 		}
-		if k8sSvc != nil {
-			service.K8sService = *k8sSvc
+
+		// collect all the Kubernetes services configured for the service backends,
+		// and all the annotations in use across all services (when applicable).
+		k8sServices, seenAnnotations := getK8sServicesForBackends(log, s, service.Namespace, service.Backends)
+
+		// if the Kubernetes services have been deemed invalid, no need to continue
+		// they will all be dropped until the problem has been rectified.
+		if !servicesAllUseTheSameKongAnnotations(log, k8sServices, seenAnnotations) {
+			return fmt.Errorf("the following Kubernetes services were all configured together as the backends "+
+				"for a Kong Service named %s: %+v. These services had disparate KongIngress overrides which is not allowed: "+
+				" when configuring multiple Kubernetes Services as backends (e.g. backendRefs in HTTPRoutes) it is required "+
+				" that all of them have matching KongIngress override annotations", *service.Name, k8sServices)
 		}
-		secretName := annotations.ExtractClientCertificate(
-			service.K8sService.GetAnnotations())
-		if secretName != "" {
-			secret, err := s.GetSecret(service.K8sService.Namespace,
-				secretName)
-			secretKey := service.K8sService.Namespace + "/" + secretName
-			// ensure that the cert is loaded into Kong
-			if _, ok := ir.SecretNameToSNIs[secretKey]; !ok {
-				ir.SecretNameToSNIs[secretKey] = []string{}
-			}
-			if err == nil {
-				service.ClientCertificate = &kong.Certificate{
-					ID: kong.String(string(secret.UID)),
+
+		for _, k8sService := range k8sServices {
+			// at this point we know the Kubernetes service itself is valid and can be
+			// used for traffic, so cache it amongst the kong Services k8s services.
+			service.K8sServices[k8sService.Name] = k8sService
+
+			// extract client certificates intended for use by the service
+			secretName := annotations.ExtractClientCertificate(k8sService.Annotations)
+			if secretName != "" {
+				secret, err := s.GetSecret(k8sService.Namespace, secretName)
+				secretKey := k8sService.Namespace + "/" + secretName
+				// ensure that the cert is loaded into Kong
+				if _, ok := ir.SecretNameToSNIs[secretKey]; !ok {
+					ir.SecretNameToSNIs[secretKey] = []string{}
+				}
+				if err == nil {
+					service.ClientCertificate = &kong.Certificate{
+						ID: kong.String(string(secret.UID)),
+					}
+				} else {
+					log.WithFields(logrus.Fields{
+						"secret_name":      secretName,
+						"secret_namespace": k8sService.Namespace,
+					}).Errorf("failed to fetch secret: %v", err)
 				}
-			} else {
-				log.WithFields(logrus.Fields{
-					"secret_name":      secretName,
-					"secret_namespace": service.K8sService.Namespace,
-				}).Errorf("failed to fetch secret: %v", err)
 			}
 		}
+
+		// Kubernetes Services have been populated for this Kong Service, so it can
+		// now be cached.
 		ir.ServiceNameToServices[key] = service
 	}
+
+	return nil
 }
 
 type SecretNameToSNIs map[string][]string
@@ -123,3 +144,83 @@ func (m SecretNameToSNIs) filterHosts(hosts []string) []string {
 	}
 	return hostsToAdd
 }
+
+func getK8sServicesForBackends(
+	log logrus.FieldLogger,
+	storer store.Storer,
+	namespace string,
+	backends kongstate.ServiceBackends,
+) ([]*corev1.Service, map[string]string) {
+	// we collect all annotations seen for this group of services so that these
+	// can be later validated.
+	seenAnnotationsForK8sServices := make(map[string]string)
+
+	// for each backend (which is a reference to a Kubernetes Service object)
+	// retreieve that backend and capture any Kong annotations its using.
+	k8sServices := make([]*corev1.Service, 0, len(backends))
+	for _, backend := range backends {
+		k8sService, err := storer.GetService(namespace, backend.Name)
+		if err != nil {
+			log.WithFields(logrus.Fields{
+				"service_name":      backend.PortDef.Name,
+				"service_namespace": namespace,
+			}).Errorf("failed to fetch service: %v", err)
+			continue
+		}
+		if k8sService != nil {
+			// record all Kong annotations in use by the service
+			for k, v := range k8sService.GetAnnotations() {
+				if strings.HasPrefix(k, annotations.AnnotationPrefix) {
+					seenAnnotationsForK8sServices[k] = v
+				}
+			}
+
+			// add the service to the list of backend services
+			k8sServices = append(k8sServices, k8sService)
+		}
+	}
+
+	return k8sServices, seenAnnotationsForK8sServices
+}
+
+func servicesAllUseTheSameKongAnnotations(
+	log logrus.FieldLogger,
+	services []*corev1.Service,
+	annotations map[string]string,
+) bool {
+	match := true
+	for _, service := range services {
+		// all services grouped together via backends must have identical annotations
+		// to avoid unexpected routing behaviors.
+		//
+		// TODO: ultimately we should be able to do this validation in our normal
+		// validation layer, but we're limited at present on where and how that
+		// validation can work. We should be able to move this validation there
+		// once https://github.com/Kong/kubernetes-ingress-controller/issues/2195
+		// is resolved.
+		for k, v := range annotations {
+			valueForThisObject, ok := service.Annotations[k]
+			if !ok {
+				log.WithFields(logrus.Fields{
+					"service_name":      service.Name,
+					"service_namespace": service.Namespace,
+				}).Errorf("in the backend group of %d kubernetes services some have the %s annotation while others don't. "+
+					"this is not supported: when multiple services comprise a backend all kong annotations "+
+					"between them must be set to the same value", len(services), k)
+				match = false
+			}
+
+			if valueForThisObject != v {
+				log.WithFields(logrus.Fields{
+					"service_name":      service.Name,
+					"service_namespace": service.Namespace,
+				}).Errorf("the value of annotation %s is different between the %d services which comprise this backend. "+
+					"this is not supported: when multiple services comprise a backend all kong annotations "+
+					"between them must be set to the same value", k, len(services))
+				match = false
+			}
+		}
+	}
+
+	return match
+}
diff --git a/internal/dataplane/parser/parser.go b/internal/dataplane/parser/parser.go
index 69bf672a9..ce71cdb3c 100644
--- a/internal/dataplane/parser/parser.go
+++ b/internal/dataplane/parser/parser.go
@@ -72,7 +72,9 @@ func (p *Parser) Build() (*kongstate.KongState, error) {
 	)
 
 	// populate any Kubernetes Service objects relevant objects
-	ingressRules.populateServices(p.logger, p.storer)
+	if err := ingressRules.populateServices(p.logger, p.storer); err != nil {
+		return nil, err
+	}
 
 	// add the routes and services to the state
 	var result kongstate.KongState
@@ -258,21 +260,81 @@ func findPort(svc *corev1.Service, wantPort kongstate.PortDef) (*corev1.ServiceP
 }
 
 func getUpstreams(
-	log logrus.FieldLogger, s store.Storer, serviceMap map[string]kongstate.Service) []kongstate.Upstream {
+	log logrus.FieldLogger,
+	s store.Storer,
+	serviceMap map[string]kongstate.Service,
+) []kongstate.Upstream {
 	upstreamDedup := make(map[string]struct{}, len(serviceMap))
 	var empty struct{}
 	upstreams := make([]kongstate.Upstream, 0, len(serviceMap))
 	for _, service := range serviceMap {
-		name := fmt.Sprintf("%s.%s.%s.svc", service.Backend.Name, service.Namespace, service.Backend.Port.CanonicalString())
+		// the name of the Upstream for a service must match the service.Host
+		// as the Gateway's internal DNS resolve mechanisms will fail to properly
+		// resolve the host otherwise.
+		name := *service.Host
+
 		if _, exists := upstreamDedup[name]; !exists {
+			// populate all the kong targets for the upstream given all the backends
 			var targets []kongstate.Target
-			port, err := findPort(&service.K8sService, service.Backend.Port)
-			if err == nil {
-				targets = getServiceEndpoints(log, s, service.K8sService, port)
-			} else {
-				log.WithField("service_name", *service.Name).Warnf("skipping service - getServiceEndpoints failed: %v", err)
+			for _, backend := range service.Backends {
+				// gather the Kubernetes service for the backend
+				k8sService, ok := service.K8sServices[backend.Name]
+				if !ok {
+					log.WithField("service_name", *service.Name).Errorf("can't add target for backend %s: no kubernetes service found", backend.Name)
+					continue
+				}
+
+				// determine the port for the backend
+				port, err := findPort(k8sService, backend.PortDef)
+				if err != nil {
+					log.WithField("service_name", *service.Name).Errorf("can't find port for backend kubernetes service %s/%s: %v", k8sService.Namespace, k8sService.Name, err)
+					continue
+				}
+
+				// get the new targets for this backend service
+				newTargets := getServiceEndpoints(log, s, k8sService, port)
+
+				if len(newTargets) == 0 {
+					log.WithField("service_name", *service.Name).Errorf("no targets could be found for kubernetes service %s/%s", k8sService.Namespace, k8sService.Name)
+				}
+
+				// if weights were set for the backend then that weight needs to be
+				// distributed equally among all the targets.
+				if backend.Weight != nil && len(newTargets) != 0 {
+					// initialize the weight of the target based on the weight of the backend
+					// which governs that target (and potentially more). If the weight of the
+					// backend is 0 then this indicates an intention to drop all targets from
+					// this backend from the load-balancer and is a special situation where
+					// all derived targets will receive a weight of 0.
+					targetWeight := int(*backend.Weight)
+
+					// if the backend governing this target is not set to a weight of 0,
+					// all targets derived from the backend split the weight, therefore
+					// equally splitting the traffic load.
+					if *backend.Weight != 0 {
+						targetWeight = int(*backend.Weight) / len(newTargets)
+						// minimum weight of 1 if weight zero was not specifically set.
+						if targetWeight == 0 {
+							targetWeight = 1
+						}
+					}
+
+					for i := range newTargets {
+						newTargets[i].Weight = &targetWeight
+					}
+				}
+
+				// add the new targets to the existing pool of targets for the Upstream.
+				targets = append(targets, newTargets...)
+			}
+
+			// warn if an upstream was created with 0 targets
+			if len(targets) == 0 {
+				log.WithField("service_name", *service.Name).Warnf("no targets found to create upstream")
 			}
 
+			// define the upstream including all the newly populated targets
+			// to load-balance traffic to.
 			upstream := kongstate.Upstream{
 				Upstream: kong.Upstream{
 					Name: kong.String(name),
@@ -374,8 +436,12 @@ func getCerts(log logrus.FieldLogger, s store.Storer, secretsToSNIs map[string][
 	return res
 }
 
-func getServiceEndpoints(log logrus.FieldLogger, s store.Storer, svc corev1.Service,
-	servicePort *corev1.ServicePort) []kongstate.Target {
+func getServiceEndpoints(
+	log logrus.FieldLogger,
+	s store.Storer,
+	svc *corev1.Service,
+	servicePort *corev1.ServicePort,
+) []kongstate.Target {
 
 	log = log.WithFields(logrus.Fields{
 		"service_name":      svc.Name,
@@ -391,7 +457,7 @@ func getServiceEndpoints(log logrus.FieldLogger, s store.Storer, svc corev1.Serv
 	// check all protocols for associated endpoints
 	endpoints := []util.Endpoint{}
 	for protocol := range protocols {
-		newEndpoints := getEndpoints(log, &svc, servicePort, protocol, s.GetEndpointsForService)
+		newEndpoints := getEndpoints(log, svc, servicePort, protocol, s.GetEndpointsForService)
 		if len(newEndpoints) > 0 {
 			endpoints = append(endpoints, newEndpoints...)
 		}
@@ -507,7 +573,7 @@ func getEndpoints(
 //       is valid for the Service and its endpoints, however we need to follow up
 //       on this as this is not technically correct and causes waste.
 //       See: https://github.com/Kong/kubernetes-ingress-controller/issues/1429
-func listProtocols(svc corev1.Service) map[corev1.Protocol]bool {
+func listProtocols(svc *corev1.Service) map[corev1.Protocol]bool {
 	protocols := map[corev1.Protocol]bool{corev1.ProtocolTCP: true}
 	for _, port := range svc.Spec.Ports {
 		if port.Protocol != "" {
diff --git a/internal/dataplane/parser/service_names.go b/internal/dataplane/parser/service_names.go
new file mode 100644
index 000000000..d042fac91
--- /dev/null
+++ b/internal/dataplane/parser/service_names.go
@@ -0,0 +1,16 @@
+package parser
+
+import (
+	"fmt"
+	"strings"
+
+	"sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+// getUniqueKongServiceName provides a deterministic unique string name to be
+// used as the name of a Kong Service given details of the Kubernetes object
+// which that Service is generated for.
+func getUniqueKongServiceNameForObject(obj client.Object) (serviceName string) {
+	kind := strings.ToLower(obj.GetObjectKind().GroupVersionKind().Kind)
+	return fmt.Sprintf("%s.%s.%s", kind, obj.GetNamespace(), obj.GetName())
+}
diff --git a/internal/dataplane/parser/translate_httproute.go b/internal/dataplane/parser/translate_httproute.go
index cce3627aa..96d94dbc1 100644
--- a/internal/dataplane/parser/translate_httproute.go
+++ b/internal/dataplane/parser/translate_httproute.go
@@ -68,11 +68,6 @@ func ingressRulesFromHTTPRoute(result *ingressRules, httproute *gatewayv1alpha2.
 			return fmt.Errorf("missing backendRef in rule")
 		}
 
-		// TODO: support multiple backend refs
-		if len(rule.BackendRefs) > 1 {
-			return fmt.Errorf("multiple backendRefs are not yet supported")
-		}
-
 		// determine the routes needed to route traffic to services for this rule
 		routes, err := generateKongRoutesFromHTTPRouteRule(httproute, ruleNumber, rule)
 		if err != nil {
@@ -80,7 +75,10 @@ func ingressRulesFromHTTPRoute(result *ingressRules, httproute *gatewayv1alpha2.
 		}
 
 		// create a service and attach the routes to it
-		service := generateKongServiceFromHTTPRouteBackendRef(result, httproute, rule.BackendRefs[0])
+		service, err := generateKongServiceFromHTTPRouteBackendRef(result, httproute, ruleNumber, rule.BackendRefs...)
+		if err != nil {
+			return err
+		}
 		service.Routes = append(service.Routes, routes...)
 
 		// cache the service to avoid duplicates in further loop iterations
@@ -215,31 +213,46 @@ func generateKongRoutesFromHTTPRouteRule(httproute *gatewayv1alpha2.HTTPRoute, r
 
 // generateKongServiceFromHTTPRouteBackendRef converts a provided backendRef for an HTTPRoute
 // into a kong.Service so that routes for that object can be attached to the Service.
-func generateKongServiceFromHTTPRouteBackendRef(result *ingressRules, httproute *gatewayv1alpha2.HTTPRoute, backendRef gatewayv1alpha2.HTTPBackendRef) kongstate.Service {
-	// determine the service namespace
-	// TODO: need to add validation to restrict namespaces in backendRefs
-	namespace := httproute.Namespace
-	if backendRef.Namespace != nil {
-		namespace = string(*backendRef.Namespace)
+func generateKongServiceFromHTTPRouteBackendRef(
+	result *ingressRules,
+	httproute *gatewayv1alpha2.HTTPRoute,
+	ruleNumber int,
+	backendRefs ...gatewayv1alpha2.HTTPBackendRef,
+) (kongstate.Service, error) {
+	// at least one backendRef must be present
+	if len(backendRefs) == 0 {
+		return kongstate.Service{}, fmt.Errorf("no backendRefs present for HTTPRoute %s/%s", httproute.Namespace, httproute.Name)
 	}
 
-	// determine the name of the Service
-	serviceName := fmt.Sprintf("%s.%s.%d", namespace, backendRef.Name, *backendRef.Port)
-
-	// determine the Service port
-	port := kongstate.PortDef{
-		Mode:   kongstate.PortModeByNumber,
-		Number: int32(*backendRef.Port),
+	// create a kongstate backend for each HTTPRoute backendRef
+	backends := make(kongstate.ServiceBackends, 0, len(backendRefs))
+	for _, backendRef := range backendRefs {
+		// convert each backendRef into a kongstate.ServiceBackend
+		backends = append(backends, kongstate.ServiceBackend{
+			Name: string(backendRef.Name),
+			PortDef: kongstate.PortDef{
+				Mode:   kongstate.PortModeByNumber,
+				Number: int32(*backendRef.Port),
+			},
+			Weight: backendRef.Weight,
+		})
 	}
 
+	// the service name needs to uniquely identify this service given it's list of
+	// one or more backends.
+	serviceName := fmt.Sprintf("%s.%d", getUniqueKongServiceNameForObject(httproute), ruleNumber)
+
+	// the service host needs to be a resolvable name due to legacy logic so we'll
+	// use the anchor backendRef as the basis for the name
+	serviceHost := serviceName
+
 	// check if the service is already known, and if not create it
 	service, ok := result.ServiceNameToServices[serviceName]
 	if !ok {
 		service = kongstate.Service{
 			Service: kong.Service{
 				Name:           kong.String(serviceName),
-				Host:           kong.String(fmt.Sprintf("%s.%s.%s.svc", backendRef.Name, namespace, port.CanonicalString())),
-				Port:           kong.Int(int(*backendRef.Port)),
+				Host:           kong.String(serviceHost),
 				Protocol:       kong.String("http"),
 				Path:           kong.String("/"),
 				ConnectTimeout: kong.Int(DefaultServiceTimeout),
@@ -248,12 +261,9 @@ func generateKongServiceFromHTTPRouteBackendRef(result *ingressRules, httproute
 				Retries:        kong.Int(DefaultRetries),
 			},
 			Namespace: httproute.Namespace,
-			Backend: kongstate.ServiceBackend{
-				Name: string(backendRef.Name),
-				Port: port,
-			},
+			Backends:  backends,
 		}
 	}
 
-	return service
+	return service, nil
 }
diff --git a/internal/dataplane/parser/translate_ingress.go b/internal/dataplane/parser/translate_ingress.go
index 2d7dfa42c..a5a6c09ed 100644
--- a/internal/dataplane/parser/translate_ingress.go
+++ b/internal/dataplane/parser/translate_ingress.go
@@ -92,10 +92,10 @@ func (p *Parser) ingressRulesFromIngressV1beta1() ingressRules {
 							Retries:        kong.Int(DefaultRetries),
 						},
 						Namespace: ingress.Namespace,
-						Backend: kongstate.ServiceBackend{
-							Name: rule.Backend.ServiceName,
-							Port: PortDefFromIntStr(rule.Backend.ServicePort),
-						},
+						Backends: []kongstate.ServiceBackend{{
+							Name:    rule.Backend.ServiceName,
+							PortDef: PortDefFromIntStr(rule.Backend.ServicePort),
+						}},
 					}
 				}
 				service.Routes = append(service.Routes, r)
@@ -136,10 +136,10 @@ func (p *Parser) ingressRulesFromIngressV1beta1() ingressRules {
 					Retries:        kong.Int(DefaultRetries),
 				},
 				Namespace: ingress.Namespace,
-				Backend: kongstate.ServiceBackend{
-					Name: defaultBackend.ServiceName,
-					Port: PortDefFromIntStr(defaultBackend.ServicePort),
-				},
+				Backends: []kongstate.ServiceBackend{{
+					Name:    defaultBackend.ServiceName,
+					PortDef: PortDefFromIntStr(defaultBackend.ServicePort),
+				}},
 			}
 		}
 		r := kongstate.Route{
@@ -244,10 +244,10 @@ func (p *Parser) ingressRulesFromIngressV1() ingressRules {
 							Retries:        kong.Int(DefaultRetries),
 						},
 						Namespace: ingress.Namespace,
-						Backend: kongstate.ServiceBackend{
-							Name: rulePath.Backend.Service.Name,
-							Port: port,
-						},
+						Backends: []kongstate.ServiceBackend{{
+							Name:    rulePath.Backend.Service.Name,
+							PortDef: port,
+						}},
 					}
 				}
 				service.Routes = append(service.Routes, r)
@@ -287,10 +287,10 @@ func (p *Parser) ingressRulesFromIngressV1() ingressRules {
 					Retries:        kong.Int(DefaultRetries),
 				},
 				Namespace: ingress.Namespace,
-				Backend: kongstate.ServiceBackend{
-					Name: defaultBackend.Service.Name,
-					Port: PortDefFromServiceBackendPort(&defaultBackend.Service.Port),
-				},
+				Backends: []kongstate.ServiceBackend{{
+					Name:    defaultBackend.Service.Name,
+					PortDef: PortDefFromServiceBackendPort(&defaultBackend.Service.Port),
+				}},
 			}
 		}
 		r := kongstate.Route{
diff --git a/internal/dataplane/parser/translate_knative.go b/internal/dataplane/parser/translate_knative.go
index 89e1f2701..57d153cf8 100644
--- a/internal/dataplane/parser/translate_knative.go
+++ b/internal/dataplane/parser/translate_knative.go
@@ -89,10 +89,10 @@ func (p *Parser) ingressRulesFromKnativeIngress() ingressRules {
 							Retries:        kong.Int(DefaultRetries),
 						},
 						Namespace: ingress.Namespace,
-						Backend: kongstate.ServiceBackend{
-							Name: knativeBackend.ServiceName,
-							Port: PortDefFromIntStr(knativeBackend.ServicePort),
-						},
+						Backends: []kongstate.ServiceBackend{{
+							Name:    knativeBackend.ServiceName,
+							PortDef: PortDefFromIntStr(knativeBackend.ServicePort),
+						}},
 					}
 					if len(headers) > 0 {
 						service.Plugins = append(service.Plugins, kong.Plugin{
diff --git a/internal/dataplane/parser/translate_kong_l4.go b/internal/dataplane/parser/translate_kong_l4.go
index 930700f27..3fc66487e 100644
--- a/internal/dataplane/parser/translate_kong_l4.go
+++ b/internal/dataplane/parser/translate_kong_l4.go
@@ -83,10 +83,10 @@ func (p *Parser) ingressRulesFromTCPIngressV1beta1() ingressRules {
 						Retries:        kong.Int(DefaultRetries),
 					},
 					Namespace: ingress.Namespace,
-					Backend: kongstate.ServiceBackend{
-						Name: rule.Backend.ServiceName,
-						Port: kongstate.PortDef{Mode: kongstate.PortModeByNumber, Number: int32(rule.Backend.ServicePort)},
-					},
+					Backends: []kongstate.ServiceBackend{{
+						Name:    rule.Backend.ServiceName,
+						PortDef: kongstate.PortDef{Mode: kongstate.PortModeByNumber, Number: int32(rule.Backend.ServicePort)},
+					}},
 				}
 			}
 			service.Routes = append(service.Routes, r)
@@ -162,10 +162,10 @@ func (p *Parser) ingressRulesFromUDPIngressV1beta1() ingressRules {
 						Host:     kong.String(host),
 						Port:     kong.Int(rule.Backend.ServicePort),
 					},
-					Backend: kongstate.ServiceBackend{
-						Name: rule.Backend.ServiceName,
-						Port: kongstate.PortDef{Mode: kongstate.PortModeByNumber, Number: int32(rule.Backend.ServicePort)},
-					},
+					Backends: []kongstate.ServiceBackend{{
+						Name:    rule.Backend.ServiceName,
+						PortDef: kongstate.PortDef{Mode: kongstate.PortModeByNumber, Number: int32(rule.Backend.ServicePort)},
+					}},
 				}
 			}
 			service.Routes = append(service.Routes, route)
diff --git a/internal/dataplane/parser/translate_udproute.go b/internal/dataplane/parser/translate_udproute.go
index f40e884fa..4c1d07ac5 100644
--- a/internal/dataplane/parser/translate_udproute.go
+++ b/internal/dataplane/parser/translate_udproute.go
@@ -179,10 +179,10 @@ func generateKongServiceFromUDPRouteBackendRef(result *ingressRules, udproute *g
 				Retries:        kong.Int(DefaultRetries),
 			},
 			Namespace: udproute.Namespace,
-			Backend: kongstate.ServiceBackend{
-				Name: string(backendRef.Name),
-				Port: port,
-			},
+			Backends: []kongstate.ServiceBackend{{
+				Name:    string(backendRef.Name),
+				PortDef: port,
+			}},
 		}
 	}
 
diff --git a/internal/validation/gateway/httproute.go b/internal/validation/gateway/httproute.go
index bbe6e8fda..d73723287 100644
--- a/internal/validation/gateway/httproute.go
+++ b/internal/validation/gateway/httproute.go
@@ -101,12 +101,6 @@ func validateHTTPRouteFeatures(httproute *gatewayv1alpha2.HTTPRoute) error {
 			}
 		}
 
-		// we don't currently support multiple backendRefs
-		// See: https://github.com/Kong/kubernetes-ingress-controller/issues/2166
-		if len(rule.BackendRefs) > 1 {
-			return fmt.Errorf("multiple backendRefs is not yet supported for httproute")
-		}
-
 		// we don't support any backendRef types except Kubernetes Services
 		for _, ref := range rule.BackendRefs {
 			if ref.BackendRef.Group != nil && *ref.BackendRef.Group != "core" && *ref.BackendRef.Group != "" {


Test patch:
diff --git a/internal/dataplane/kongstate/kongstate_test.go b/internal/dataplane/kongstate/kongstate_test.go
index 1e16ccdc8..15b1289e1 100644
--- a/internal/dataplane/kongstate/kongstate_test.go
+++ b/internal/dataplane/kongstate/kongstate_test.go
@@ -102,11 +102,13 @@ func Test_getPluginRelations(t *testing.T) {
 							Service: kong.Service{
 								Name: kong.String("foo-service"),
 							},
-							K8sService: corev1.Service{
-								ObjectMeta: metav1.ObjectMeta{
-									Namespace: "ns1",
-									Annotations: map[string]string{
-										annotations.AnnotationPrefix + annotations.PluginsKey: "foo,bar",
+							K8sServices: map[string]*corev1.Service{
+								"foo-service": {
+									ObjectMeta: metav1.ObjectMeta{
+										Namespace: "ns1",
+										Annotations: map[string]string{
+											annotations.AnnotationPrefix + annotations.PluginsKey: "foo,bar",
+										},
 									},
 								},
 							},
@@ -246,11 +248,13 @@ func Test_getPluginRelations(t *testing.T) {
 							Service: kong.Service{
 								Name: kong.String("foo-service"),
 							},
-							K8sService: corev1.Service{
-								ObjectMeta: metav1.ObjectMeta{
-									Namespace: "ns1",
-									Annotations: map[string]string{
-										annotations.AnnotationPrefix + annotations.PluginsKey: "foo,bar",
+							K8sServices: map[string]*corev1.Service{
+								"foo-service": {
+									ObjectMeta: metav1.ObjectMeta{
+										Namespace: "ns1",
+										Annotations: map[string]string{
+											annotations.AnnotationPrefix + annotations.PluginsKey: "foo,bar",
+										},
 									},
 								},
 							},
diff --git a/internal/dataplane/kongstate/util_test.go b/internal/dataplane/kongstate/util_test.go
index 55b067ec8..e951d73fd 100644
--- a/internal/dataplane/kongstate/util_test.go
+++ b/internal/dataplane/kongstate/util_test.go
@@ -1,14 +1,17 @@
 package kongstate
 
 import (
+	"regexp"
 	"testing"
 
 	"github.com/kong/go-kong/kong"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 	corev1 "k8s.io/api/core/v1"
 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
+	"github.com/kong/kubernetes-ingress-controller/v2/internal/annotations"
 	"github.com/kong/kubernetes-ingress-controller/v2/internal/store"
 	configurationv1 "github.com/kong/kubernetes-ingress-controller/v2/pkg/apis/configuration/v1"
 )
@@ -285,3 +288,213 @@ func TestKongPluginFromK8SPlugin(t *testing.T) {
 		})
 	}
 }
+
+func Test_getKongIngressForServices(t *testing.T) {
+	for _, tt := range []struct {
+		name                string
+		services            map[string]*corev1.Service
+		kongIngresses       []*configurationv1.KongIngress
+		expectedKongIngress *configurationv1.KongIngress
+		expectedError       error
+	}{
+		{
+			name: "when no services are provided, no KongIngress will be provided",
+			kongIngresses: []*configurationv1.KongIngress{{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-kongingress1",
+					Namespace: corev1.NamespaceDefault,
+				},
+			}},
+		},
+		{
+			name: "when none of the provided services have attached KongIngress resources, no KongIngress resources will be provided",
+			services: map[string]*corev1.Service{
+				"test-service1": {
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test-service1",
+						Namespace: corev1.NamespaceDefault,
+					},
+				},
+				"test-service2": {
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test-service2",
+						Namespace: corev1.NamespaceDefault,
+					},
+				},
+			},
+			kongIngresses: []*configurationv1.KongIngress{{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-kongingress1",
+					Namespace: corev1.NamespaceDefault,
+				},
+			}},
+		},
+		{
+			name: "if at least one KongIngress resource is attached to a Service, it will be returned",
+			services: map[string]*corev1.Service{
+				"test-service1": {
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test-service1",
+						Namespace: corev1.NamespaceDefault,
+						Annotations: map[string]string{
+							annotations.AnnotationPrefix + annotations.ConfigurationKey: "test-kongingress2",
+						},
+					},
+				},
+				"test-service2": {
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test-service2",
+						Namespace: corev1.NamespaceDefault,
+					},
+				},
+			},
+			kongIngresses: []*configurationv1.KongIngress{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test-kongingress1",
+						Namespace: corev1.NamespaceDefault,
+					},
+				},
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test-kongingress2",
+						Namespace: corev1.NamespaceDefault,
+					},
+				},
+			},
+			expectedKongIngress: &configurationv1.KongIngress{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-kongingress2",
+					Namespace: corev1.NamespaceDefault,
+				},
+			},
+		},
+		{
+			name: "if multiple services have KongIngress resources this is accepted only if they're all attached to the same KongIngress",
+			services: map[string]*corev1.Service{
+				"test-service1": {
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test-service1",
+						Namespace: corev1.NamespaceDefault,
+						Annotations: map[string]string{
+							annotations.AnnotationPrefix + annotations.ConfigurationKey: "test-kongingress2",
+						},
+					},
+				},
+				"test-service2": {
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test-service2",
+						Namespace: corev1.NamespaceDefault,
+						Annotations: map[string]string{
+							annotations.AnnotationPrefix + annotations.ConfigurationKey: "test-kongingress2",
+						},
+					},
+				},
+				"test-service3": {
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test-service3",
+						Namespace: corev1.NamespaceDefault,
+						Annotations: map[string]string{
+							annotations.AnnotationPrefix + annotations.ConfigurationKey: "test-kongingress2",
+						},
+					},
+				},
+			},
+			kongIngresses: []*configurationv1.KongIngress{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test-kongingress1",
+						Namespace: corev1.NamespaceDefault,
+					},
+				},
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test-kongingress2",
+						Namespace: corev1.NamespaceDefault,
+					},
+				},
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test-kongingress3",
+						Namespace: corev1.NamespaceDefault,
+					},
+				},
+			},
+			expectedKongIngress: &configurationv1.KongIngress{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-kongingress2",
+					Namespace: corev1.NamespaceDefault,
+				},
+			},
+		},
+	} {
+		t.Run(tt.name, func(t *testing.T) {
+			storer, err := store.NewFakeStore(store.FakeObjects{
+				KongIngresses: tt.kongIngresses,
+			})
+			require.NoError(t, err)
+
+			kongIngress, err := getKongIngressForServices(storer, tt.services)
+			if tt.expectedError == nil {
+				assert.Equal(t, tt.expectedKongIngress, kongIngress)
+			} else {
+				assert.Error(t, err)
+				assert.Contains(t, err.Error(), tt.expectedError.Error())
+			}
+		})
+	}
+}
+
+func Test_prettyPrintServiceList(t *testing.T) {
+	for _, tt := range []struct {
+		name     string
+		services map[string]*corev1.Service
+		expected string
+	}{
+		{
+			name:     "an empty list of services produces an empty string",
+			expected: "",
+		},
+		{
+			name: "a single service should just return the <namespace>/<name>",
+			services: map[string]*corev1.Service{
+				"test-service1": {
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test-service1",
+						Namespace: corev1.NamespaceDefault,
+					},
+				},
+			},
+			expected: "default/test-service1",
+		},
+		{
+			name: "multiple services should be comma deliniated",
+			services: map[string]*corev1.Service{
+				"test-service1": {
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test-service1",
+						Namespace: corev1.NamespaceDefault,
+					},
+				},
+				"test-service2": {
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test-service2",
+						Namespace: corev1.NamespaceDefault,
+					},
+				},
+				"test-service3": {
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test-service3",
+						Namespace: corev1.NamespaceDefault,
+					},
+				},
+			},
+			expected: "default/test-service[0-9], default/test-service[0-9], default/test-service[0-9]",
+		},
+	} {
+		t.Run(tt.name, func(t *testing.T) {
+			re := regexp.MustCompile(tt.expected)
+			assert.True(t, re.MatchString(PrettyPrintServiceList(tt.services)))
+		})
+	}
+}
diff --git a/internal/dataplane/parser/ingressrules_test.go b/internal/dataplane/parser/ingressrules_test.go
index 822143d10..9435b5e1e 100644
--- a/internal/dataplane/parser/ingressrules_test.go
+++ b/internal/dataplane/parser/ingressrules_test.go
@@ -1,12 +1,18 @@
 package parser
 
 import (
+	"bytes"
 	"testing"
 
+	"github.com/sirupsen/logrus"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	corev1 "k8s.io/api/core/v1"
 	networking "k8s.io/api/networking/v1beta1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 	"github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/kongstate"
+	"github.com/kong/kubernetes-ingress-controller/v2/internal/store"
 )
 
 func TestMergeIngressRules(t *testing.T) {
@@ -171,3 +177,293 @@ func Test_addFromIngressV1beta1TLS(t *testing.T) {
 		})
 	}
 }
+
+func Test_getK8sServicesForBackends(t *testing.T) {
+	for _, tt := range []struct {
+		name                string
+		namespace           string
+		backends            kongstate.ServiceBackends
+		services            []*corev1.Service
+		expectedServices    []*corev1.Service
+		expectedAnnotations map[string]string
+		expectedLogEntries  []string
+	}{
+		{
+			name:      "if all backends have a service then all services will be returned and their annotations recorded",
+			namespace: corev1.NamespaceDefault,
+			backends: kongstate.ServiceBackends{
+				{
+					Name: "test-service1",
+				},
+				{
+					Name: "test-service2",
+				},
+			},
+			services: []*corev1.Service{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test-service1",
+						Namespace: corev1.NamespaceDefault,
+						Annotations: map[string]string{
+							"konghq.com/foo": "bar",
+						},
+					},
+				},
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test-service2",
+						Namespace: corev1.NamespaceDefault,
+						Annotations: map[string]string{
+							"konghq.com/foo": "baz",
+						},
+					},
+				},
+			},
+			expectedServices: []*corev1.Service{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test-service1",
+						Namespace: corev1.NamespaceDefault,
+						Annotations: map[string]string{
+							"konghq.com/foo": "bar",
+						},
+					},
+				},
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "test-service2",
+						Namespace: corev1.NamespaceDefault,
+						Annotations: map[string]string{
+							"konghq.com/foo": "baz",
+						},
+					},
+				},
+			},
+			expectedAnnotations: map[string]string{
+				"konghq.com/foo": "baz",
+			},
+		},
+		{
+			name:      "backends which have no corresponding services will fail to fetch",
+			namespace: corev1.NamespaceDefault,
+			backends: kongstate.ServiceBackends{
+				{
+					Name: "test-service1",
+				},
+				{
+					Name: "test-service2",
+				},
+			},
+			services: []*corev1.Service{{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-service1",
+					Namespace: corev1.NamespaceDefault,
+				},
+			}},
+			expectedServices: []*corev1.Service{{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-service1",
+					Namespace: corev1.NamespaceDefault,
+				},
+			}},
+			expectedAnnotations: map[string]string{},
+			expectedLogEntries: []string{
+				"failed to fetch service: Service default/test-service2 not found",
+			},
+		},
+	} {
+		t.Run(tt.name, func(t *testing.T) {
+			storer, err := store.NewFakeStore(store.FakeObjects{Services: tt.services})
+			require.NoError(t, err)
+
+			stdout := new(bytes.Buffer)
+			logger := logrus.New()
+			logger.SetOutput(stdout)
+
+			services, annotations := getK8sServicesForBackends(logger, storer, tt.namespace, tt.backends)
+			assert.Equal(t, tt.expectedServices, services)
+			assert.Equal(t, tt.expectedAnnotations, annotations)
+			for _, expectedLogEntry := range tt.expectedLogEntries {
+				assert.Contains(t, stdout.String(), expectedLogEntry)
+			}
+		})
+	}
+}
+
+func Test_doK8sServicesMatchAnnotations(t *testing.T) {
+	for _, tt := range []struct {
+		name               string
+		services           []*corev1.Service
+		annotations        map[string]string
+		expected           bool
+		expectedLogEntries []string
+	}{
+		{
+			name:        "if no services are provided, then there's no validation failure",
+			annotations: map[string]string{"foo": "bar"},
+			expected:    true,
+		},
+		{
+			name: "validation passes for a group of services with no annotations expected, even if they all have different annotations",
+			services: []*corev1.Service{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-service1",
+						Annotations: map[string]string{
+							"konghq.com/foo": "bar",
+						},
+					},
+				},
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-service2",
+						Annotations: map[string]string{
+							"konghq.com/bar": "foo",
+						},
+					},
+				},
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-service3",
+						Annotations: map[string]string{
+							"konghq.com/baz": "foo",
+						},
+					},
+				},
+			},
+			expected: true,
+		},
+		{
+			name: "validation passes for a group of services all have the expected annotations",
+			services: []*corev1.Service{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-service1",
+						Annotations: map[string]string{
+							"konghq.com/foo": "bar",
+							"konghq.com/bar": "foo",
+							"konghq.com/baz": "foo",
+							"example.com":    "foo",
+						},
+					},
+				},
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-service2",
+						Annotations: map[string]string{
+							"konghq.com/baz": "foo",
+							"konghq.com/foo": "bar",
+							"konghq.com/bar": "foo",
+							"example.com":    "bar",
+						},
+					},
+				},
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-service3",
+						Annotations: map[string]string{
+							"konghq.com/bar": "foo",
+							"konghq.com/foo": "bar",
+							"konghq.com/baz": "foo",
+						},
+					},
+				},
+			},
+			annotations: map[string]string{
+				"konghq.com/foo": "bar",
+				"konghq.com/bar": "foo",
+				"konghq.com/baz": "foo",
+			},
+			expected: true,
+		},
+		{
+			name: "validation fails if one service does not have all expected annotations",
+			services: []*corev1.Service{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-service1",
+						Annotations: map[string]string{
+							"konghq.com/bar": "foo",
+							"konghq.com/baz": "foo",
+						},
+					},
+				},
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-service2",
+						Annotations: map[string]string{
+							"konghq.com/baz": "foo",
+							"konghq.com/foo": "bar",
+							"konghq.com/bar": "foo",
+						},
+					},
+				},
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-service3",
+						Annotations: map[string]string{
+							"konghq.com/bar": "foo",
+							"konghq.com/foo": "bar",
+							"konghq.com/baz": "foo",
+						},
+					},
+				},
+			},
+			annotations: map[string]string{
+				"konghq.com/foo": "bar",
+				"konghq.com/bar": "foo",
+				"konghq.com/baz": "foo",
+			},
+			expected: false,
+			expectedLogEntries: []string{
+				"in the backend group of 3 kubernetes services some have the konghq.com/foo annotation while others don't",
+			},
+		},
+		{
+			name: "validation fails if all services have the same annotations, but not the same value",
+			services: []*corev1.Service{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-service1",
+						Annotations: map[string]string{
+							"konghq.com/foo": "bar",
+						},
+					},
+				},
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-service2",
+						Annotations: map[string]string{
+							"konghq.com/foo": "baz",
+						},
+					},
+				},
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-service3",
+						Annotations: map[string]string{
+							"konghq.com/foo": "buzz",
+						},
+					},
+				},
+			},
+			annotations: map[string]string{
+				"konghq.com/foo": "bar",
+			},
+			expected: false,
+			expectedLogEntries: []string{
+				"the value of annotation konghq.com/foo is different between the 3 services which comprise this backend.",
+			},
+		},
+	} {
+		t.Run(tt.name, func(t *testing.T) {
+			stdout := new(bytes.Buffer)
+			logger := logrus.New()
+			logger.SetOutput(stdout)
+			assert.Equal(t, tt.expected, servicesAllUseTheSameKongAnnotations(logger, tt.services, tt.annotations))
+			for _, expectedLogEntry := range tt.expectedLogEntries {
+				assert.Contains(t, stdout.String(), expectedLogEntry)
+			}
+		})
+	}
+}
diff --git a/internal/dataplane/parser/translate_httproute_test.go b/internal/dataplane/parser/translate_httproute_test.go
index 1eca6f44d..590c85770 100644
--- a/internal/dataplane/parser/translate_httproute_test.go
+++ b/internal/dataplane/parser/translate_httproute_test.go
@@ -8,12 +8,22 @@ import (
 	"github.com/stretchr/testify/assert"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime/schema"
 	gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
 
 	"github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/kongstate"
 	"github.com/kong/kubernetes-ingress-controller/v2/internal/util"
 )
 
+// httprouteGVK is the GVK for HTTPRoutes, needed in unit tests because
+// we have to manually initialize objects that aren't retrieved from the
+// Kubernetes API.
+var httprouteGVK = schema.GroupVersionKind{
+	Group:   "gateway.networking.k8s.io",
+	Version: "v1alpha2",
+	Kind:    "HTTPRoute",
+}
+
 func Test_ingressRulesFromHTTPRoutes(t *testing.T) {
 	httpPort := gatewayv1alpha2.PortNumber(80)
 	pathMatchPrefix := gatewayv1alpha2.PathMatchPathPrefix
@@ -66,25 +76,24 @@ func Test_ingressRulesFromHTTPRoutes(t *testing.T) {
 			expected: ingressRules{
 				SecretNameToSNIs: SecretNameToSNIs{},
 				ServiceNameToServices: map[string]kongstate.Service{
-					"default.fake-service.80": {
+					"httproute.default.basic-httproute.0": {
 						Service: kong.Service{ // only 1 service should be created
 							ConnectTimeout: kong.Int(60000),
-							Host:           kong.String("fake-service.default.80.svc"),
-							Name:           kong.String("default.fake-service.80"),
+							Host:           kong.String("httproute.default.basic-httproute.0"),
+							Name:           kong.String("httproute.default.basic-httproute.0"),
 							Path:           kong.String("/"),
-							Port:           kong.Int(80),
 							Protocol:       kong.String("http"),
 							ReadTimeout:    kong.Int(60000),
 							Retries:        kong.Int(5),
 							WriteTimeout:   kong.Int(60000),
 						},
-						Backend: kongstate.ServiceBackend{
+						Backends: kongstate.ServiceBackends{{
 							Name: "fake-service",
-							Port: kongstate.PortDef{
+							PortDef: kongstate.PortDef{
 								Mode:   kongstate.PortMode(1),
 								Number: 80,
 							},
-						},
+						}},
 						Namespace: "default",
 						Routes: []kongstate.Route{{ // only 1 route should be created
 							Route: kong.Route{
@@ -105,7 +114,6 @@ func Test_ingressRulesFromHTTPRoutes(t *testing.T) {
 								Annotations: make(map[string]string),
 							},
 						}},
-						K8sService: corev1.Service{},
 					},
 				},
 			},
@@ -179,25 +187,24 @@ func Test_ingressRulesFromHTTPRoutes(t *testing.T) {
 			expected: ingressRules{
 				SecretNameToSNIs: SecretNameToSNIs{},
 				ServiceNameToServices: map[string]kongstate.Service{
-					"default.fake-service.80": {
+					"httproute.default.basic-httproute.0": {
 						Service: kong.Service{ // only 1 service should be created
 							ConnectTimeout: kong.Int(60000),
-							Host:           kong.String("fake-service.default.80.svc"),
-							Name:           kong.String("default.fake-service.80"),
+							Host:           kong.String("httproute.default.basic-httproute.0"),
+							Name:           kong.String("httproute.default.basic-httproute.0"),
 							Path:           kong.String("/"),
-							Port:           kong.Int(80),
 							Protocol:       kong.String("http"),
 							ReadTimeout:    kong.Int(60000),
 							Retries:        kong.Int(5),
 							WriteTimeout:   kong.Int(60000),
 						},
-						Backend: kongstate.ServiceBackend{
+						Backends: kongstate.ServiceBackends{{
 							Name: "fake-service",
-							Port: kongstate.PortDef{
+							PortDef: kongstate.PortDef{
 								Mode:   kongstate.PortMode(1),
 								Number: 80,
 							},
-						},
+						}},
 						Namespace: "default",
 						Routes: []kongstate.Route{{ // only 1 route should be created
 							Route: kong.Route{
@@ -217,7 +224,6 @@ func Test_ingressRulesFromHTTPRoutes(t *testing.T) {
 								Annotations: make(map[string]string),
 							},
 						}},
-						K8sService: corev1.Service{},
 					},
 				},
 			},
@@ -319,25 +325,24 @@ func Test_ingressRulesFromHTTPRoutes(t *testing.T) {
 			expected: ingressRules{
 				SecretNameToSNIs: SecretNameToSNIs{},
 				ServiceNameToServices: map[string]kongstate.Service{
-					"default.fake-service.80": {
+					"httproute.default.basic-httproute.0": {
 						Service: kong.Service{ // only 1 service should be created
 							ConnectTimeout: kong.Int(60000),
-							Host:           kong.String("fake-service.default.80.svc"),
-							Name:           kong.String("default.fake-service.80"),
+							Host:           kong.String("httproute.default.basic-httproute.0"),
+							Name:           kong.String("httproute.default.basic-httproute.0"),
 							Path:           kong.String("/"),
-							Port:           kong.Int(80),
 							Protocol:       kong.String("http"),
 							ReadTimeout:    kong.Int(60000),
 							Retries:        kong.Int(5),
 							WriteTimeout:   kong.Int(60000),
 						},
-						Backend: kongstate.ServiceBackend{
+						Backends: kongstate.ServiceBackends{{
 							Name: "fake-service",
-							Port: kongstate.PortDef{
+							PortDef: kongstate.PortDef{
 								Mode:   kongstate.PortMode(1),
 								Number: 80,
 							},
-						},
+						}},
 						Namespace: "default",
 						Routes: []kongstate.Route{{ // only 1 route should be created
 							Route: kong.Route{
@@ -357,7 +362,6 @@ func Test_ingressRulesFromHTTPRoutes(t *testing.T) {
 								Annotations: make(map[string]string),
 							},
 						}},
-						K8sService: corev1.Service{},
 					},
 				},
 			},
@@ -396,25 +400,24 @@ func Test_ingressRulesFromHTTPRoutes(t *testing.T) {
 			expected: ingressRules{
 				SecretNameToSNIs: SecretNameToSNIs{},
 				ServiceNameToServices: map[string]kongstate.Service{
-					"default.fake-service.80": {
+					"httproute.default.basic-httproute.0": {
 						Service: kong.Service{ // only 1 service should be created
 							ConnectTimeout: kong.Int(60000),
-							Host:           kong.String("fake-service.default.80.svc"),
-							Name:           kong.String("default.fake-service.80"),
+							Host:           kong.String("httproute.default.basic-httproute.0"),
+							Name:           kong.String("httproute.default.basic-httproute.0"),
 							Path:           kong.String("/"),
-							Port:           kong.Int(80),
 							Protocol:       kong.String("http"),
 							ReadTimeout:    kong.Int(60000),
 							Retries:        kong.Int(5),
 							WriteTimeout:   kong.Int(60000),
 						},
-						Backend: kongstate.ServiceBackend{
+						Backends: kongstate.ServiceBackends{{
 							Name: "fake-service",
-							Port: kongstate.PortDef{
+							PortDef: kongstate.PortDef{
 								Mode:   kongstate.PortMode(1),
 								Number: 80,
 							},
-						},
+						}},
 						Namespace: "default",
 						Routes: []kongstate.Route{{ // only 1 route should be created
 							Route: kong.Route{
@@ -434,7 +437,6 @@ func Test_ingressRulesFromHTTPRoutes(t *testing.T) {
 								Annotations: make(map[string]string),
 							},
 						}},
-						K8sService: corev1.Service{},
 					},
 				},
 			},
@@ -445,6 +447,9 @@ func Test_ingressRulesFromHTTPRoutes(t *testing.T) {
 
 			var errs []error
 			for _, httproute := range tt.routes {
+				// initialize the HTTPRoute object
+				httproute.SetGroupVersionKind(httprouteGVK)
+
 				// generate the ingress rules
 				err := ingressRulesFromHTTPRoute(&ingressRules, httproute)
 				if err != nil {
diff --git a/internal/validation/gateway/httproute_test.go b/internal/validation/gateway/httproute_test.go
index 24e0d3eb7..6abedafe9 100644
--- a/internal/validation/gateway/httproute_test.go
+++ b/internal/validation/gateway/httproute_test.go
@@ -360,70 +360,6 @@ func TestValidateHTTPRoute(t *testing.T) {
 			validationMsg: "httproute spec did not pass validation",
 			err:           fmt.Errorf("regex header matching is not yet supported for httproute"),
 		},
-		{
-			msg: "if an HTTPRoute defines more than one backendref for a single rule this is unsupported",
-			route: &gatewayv1alpha2.HTTPRoute{
-				ObjectMeta: metav1.ObjectMeta{
-					Namespace: corev1.NamespaceDefault,
-					Name:      "testing-httproute",
-				},
-				Spec: gatewayv1alpha2.HTTPRouteSpec{
-					CommonRouteSpec: gatewayv1alpha2.CommonRouteSpec{
-						ParentRefs: []gatewayv1alpha2.ParentReference{{
-							Name: "testing-gateway",
-						}},
-					},
-					Rules: []gatewayv1alpha2.HTTPRouteRule{{
-						Matches: []gatewayv1alpha2.HTTPRouteMatch{{
-							Headers: []gatewayv1alpha2.HTTPHeaderMatch{{
-								Name:  "Content-Type",
-								Value: "audio/vorbis",
-							}},
-						}},
-						BackendRefs: []gatewayv1alpha2.HTTPBackendRef{
-							{
-								BackendRef: gatewayv1alpha2.BackendRef{
-									BackendObjectReference: gatewayv1alpha2.BackendObjectReference{
-										Namespace: &defaultGWNamespace,
-										Name:      "service1",
-									},
-								},
-							},
-							{
-								BackendRef: gatewayv1alpha2.BackendRef{
-									BackendObjectReference: gatewayv1alpha2.BackendObjectReference{
-										Namespace: &defaultGWNamespace,
-										Name:      "service2",
-									},
-								},
-							},
-						},
-					}},
-				},
-			},
-			gateways: []*gatewayv1alpha2.Gateway{{
-				ObjectMeta: metav1.ObjectMeta{
-					Namespace: corev1.NamespaceDefault,
-					Name:      "testing-gateway",
-				},
-				Spec: gatewayv1alpha2.GatewaySpec{
-					Listeners: []gatewayv1alpha2.Listener{{
-						Name:     "http",
-						Port:     80,
-						Protocol: gatewayv1alpha2.HTTPProtocolType,
-						AllowedRoutes: &gatewayv1alpha2.AllowedRoutes{
-							Kinds: []gatewayv1alpha2.RouteGroupKind{{
-								Group: &group,
-								Kind:  "HTTPRoute",
-							}},
-						},
-					}},
-				},
-			}},
-			valid:         false,
-			validationMsg: "httproute spec did not pass validation",
-			err:           fmt.Errorf("multiple backendRefs is not yet supported for httproute"),
-		},
 		{
 			msg: "we don't support any group except core kubernetes for backendRefs",
 			route: &gatewayv1alpha2.HTTPRoute{
diff --git a/test/e2e/helpers_test.go b/test/e2e/helpers_test.go
index 5581ba055..7a0b37332 100644
--- a/test/e2e/helpers_test.go
+++ b/test/e2e/helpers_test.go
@@ -405,7 +405,7 @@ func killKong(ctx context.Context, t *testing.T, env environments.Environment, p
 	written, err := kubeconfigFile.Write(kubeconfig)
 	require.NoError(t, err)
 	require.Equal(t, len(kubeconfig), written)
-	cmd := exec.Command("kubectl", "--kubeconfig", kubeconfigFile.Name(), "exec", "-n", pod.Namespace, pod.Name, "--", "kill", "1")
+	cmd := exec.Command("kubectl", "--kubeconfig", kubeconfigFile.Name(), "exec", "-n", pod.Namespace, pod.Name, "--", "kill", "1") //nolint:gosec
 	require.NoError(t, cmd.Run())
 	require.Eventually(t, func() bool {
 		pod, err = env.Cluster().Client().CoreV1().Pods(pod.Namespace).Get(ctx, pod.Name, metav1.GetOptions{})
diff --git a/test/integration/examples_test.go b/test/integration/examples_test.go
index ba36729df..7cc75da58 100644
--- a/test/integration/examples_test.go
+++ b/test/integration/examples_test.go
@@ -62,7 +62,7 @@ func TestHTTPRouteExample(t *testing.T) {
 
 	t.Logf("verifying that the HTTPRoute becomes routable")
 	require.Eventually(t, func() bool {
-		resp, err := httpc.Get(fmt.Sprintf("http://%s/httpbin", gatewayAddr))
+		resp, err := httpc.Get(fmt.Sprintf("http://%s/httproute-testing", gatewayAddr))
 		if err != nil {
 			return false
 		}
@@ -76,6 +76,23 @@ func TestHTTPRouteExample(t *testing.T) {
 		}
 		return false
 	}, ingressWait, waitTick)
+
+	t.Logf("verifying that the backendRefs are being loadbalanced")
+	require.Eventually(t, func() bool {
+		resp, err := httpc.Get(fmt.Sprintf("http://%s/httproute-testing", gatewayAddr))
+		if err != nil {
+			return false
+		}
+		defer resp.Body.Close()
+		if resp.StatusCode == http.StatusOK {
+			b := new(bytes.Buffer)
+			n, err := b.ReadFrom(resp.Body)
+			require.NoError(t, err)
+			require.True(t, n > 0)
+			return strings.Contains(b.String(), "<title>Welcome to nginx!</title>")
+		}
+		return false
+	}, ingressWait, waitTick)
 }
 
 var udpRouteExampleManifests = fmt.Sprintf("%s/gateway-udproute.yaml", examplesDIR)
diff --git a/test/integration/httproute_test.go b/test/integration/httproute_test.go
index 980b652a3..b695715f8 100644
--- a/test/integration/httproute_test.go
+++ b/test/integration/httproute_test.go
@@ -84,35 +84,56 @@ func TestHTTPRouteEssentials(t *testing.T) {
 	}()
 
 	t.Log("deploying a minimal HTTP container deployment to test Ingress routes")
-	container := generators.NewContainer("httpbin", httpBinImage, 80)
-	deployment := generators.NewDeploymentForContainer(container)
-	deployment, err = env.Cluster().Client().AppsV1().Deployments(ns.Name).Create(ctx, deployment, metav1.CreateOptions{})
+	container1 := generators.NewContainer("httpbin", httpBinImage, 80)
+	deployment1 := generators.NewDeploymentForContainer(container1)
+	deployment1, err = env.Cluster().Client().AppsV1().Deployments(ns.Name).Create(ctx, deployment1, metav1.CreateOptions{})
+	require.NoError(t, err)
+
+	t.Log("deploying an extra minimal HTTP container deployment to test multiple backendRefs")
+	container2 := generators.NewContainer("nginx", "nginx", 80)
+	deployment2 := generators.NewDeploymentForContainer(container2)
+	deployment2, err = env.Cluster().Client().AppsV1().Deployments(ns.Name).Create(ctx, deployment2, metav1.CreateOptions{})
 	require.NoError(t, err)
 
 	defer func() {
-		t.Logf("cleaning up the deployment %s", deployment.Name)
-		if err := env.Cluster().Client().AppsV1().Deployments(ns.Name).Delete(ctx, deployment.Name, metav1.DeleteOptions{}); err != nil {
+		t.Log("cleaning up the deployments")
+		if err := env.Cluster().Client().AppsV1().Deployments(ns.Name).Delete(ctx, deployment1.Name, metav1.DeleteOptions{}); err != nil {
+			if !errors.IsNotFound(err) {
+				assert.NoError(t, err)
+			}
+		}
+		if err := env.Cluster().Client().AppsV1().Deployments(ns.Name).Delete(ctx, deployment2.Name, metav1.DeleteOptions{}); err != nil {
 			if !errors.IsNotFound(err) {
 				assert.NoError(t, err)
 			}
 		}
 	}()
 
-	t.Logf("exposing deployment %s via service", deployment.Name)
-	service := generators.NewServiceForDeployment(deployment, corev1.ServiceTypeLoadBalancer)
-	_, err = env.Cluster().Client().CoreV1().Services(ns.Name).Create(ctx, service, metav1.CreateOptions{})
+	t.Logf("exposing deployment %s via service", deployment1.Name)
+	service1 := generators.NewServiceForDeployment(deployment1, corev1.ServiceTypeLoadBalancer)
+	_, err = env.Cluster().Client().CoreV1().Services(ns.Name).Create(ctx, service1, metav1.CreateOptions{})
+	require.NoError(t, err)
+
+	t.Logf("exposing deployment %s via service", deployment2.Name)
+	service2 := generators.NewServiceForDeployment(deployment2, corev1.ServiceTypeLoadBalancer)
+	_, err = env.Cluster().Client().CoreV1().Services(ns.Name).Create(ctx, service2, metav1.CreateOptions{})
 	require.NoError(t, err)
 
 	defer func() {
-		t.Logf("cleaning up the service %s", service.Name)
-		if err := env.Cluster().Client().CoreV1().Services(ns.Name).Delete(ctx, service.Name, metav1.DeleteOptions{}); err != nil {
+		t.Logf("cleaning up the service %s", service1.Name)
+		if err := env.Cluster().Client().CoreV1().Services(ns.Name).Delete(ctx, service1.Name, metav1.DeleteOptions{}); err != nil {
+			if !errors.IsNotFound(err) {
+				assert.NoError(t, err)
+			}
+		}
+		if err := env.Cluster().Client().CoreV1().Services(ns.Name).Delete(ctx, service2.Name, metav1.DeleteOptions{}); err != nil {
 			if !errors.IsNotFound(err) {
 				assert.NoError(t, err)
 			}
 		}
 	}()
 
-	t.Logf("creating an httproute to access deployment %s via kong", deployment.Name)
+	t.Logf("creating an httproute to access deployment %s via kong", deployment1.Name)
 	httpPort := gatewayv1alpha2.PortNumber(80)
 	pathMatchPrefix := gatewayv1alpha2.PathMatchPathPrefix
 	pathMatchRegularExpression := gatewayv1alpha2.PathMatchRegularExpression
@@ -155,7 +176,7 @@ func TestHTTPRouteEssentials(t *testing.T) {
 				BackendRefs: []gatewayv1alpha2.HTTPBackendRef{{
 					BackendRef: gatewayv1alpha2.BackendRef{
 						BackendObjectReference: gatewayv1alpha2.BackendObjectReference{
-							Name: gatewayv1alpha2.ObjectName(service.Name),
+							Name: gatewayv1alpha2.ObjectName(service1.Name),
 							Port: &httpPort,
 						},
 					},
@@ -202,6 +223,52 @@ func TestHTTPRouteEssentials(t *testing.T) {
 		eventuallyGETPath(t, "", http.StatusOK, "<title>httpbin.org</title>", map[string]string{"Content-Type": "audio/mp3"})
 	}
 
+	t.Log("adding an additional backendRef to the HTTPRoute")
+	var httpbinWeight int32 = 75
+	var nginxWeight int32 = 25
+	require.Eventually(t, func() bool {
+		httproute, err = c.GatewayV1alpha2().HTTPRoutes(httproute.Namespace).Get(ctx, httproute.Name, metav1.GetOptions{})
+		require.NoError(t, err)
+		httproute.Spec.Rules[0].BackendRefs = []gatewayv1alpha2.HTTPBackendRef{
+			{
+				BackendRef: gatewayv1alpha2.BackendRef{
+					BackendObjectReference: gatewayv1alpha2.BackendObjectReference{
+						Name: gatewayv1alpha2.ObjectName(service1.Name),
+						Port: &httpPort,
+					},
+					Weight: &httpbinWeight,
+				},
+			},
+			{
+				BackendRef: gatewayv1alpha2.BackendRef{
+					BackendObjectReference: gatewayv1alpha2.BackendObjectReference{
+						Name: gatewayv1alpha2.ObjectName(service2.Name),
+						Port: &httpPort,
+					},
+					Weight: &nginxWeight,
+				},
+			},
+		}
+
+		httproute, err = c.GatewayV1alpha2().HTTPRoutes(httproute.Namespace).Update(ctx, httproute, metav1.UpdateOptions{})
+		if err != nil {
+			t.Logf("WARNING: could not update httproute with an additional backendRef: %s (retrying)", err)
+			return false
+		}
+
+		return true
+	}, ingressWait, waitTick)
+
+	t.Log("verifying that weighted loadbalancing strategy is implemented properly and the nginx service is only getting 25% of the load")
+	eventuallyGETPath(t, "httpbin", http.StatusOK, "<title>httpbin.org</title>", emptyHeaderSet)
+	eventuallyGETPath(t, "httpbin", http.StatusOK, "<title>httpbin.org</title>", emptyHeaderSet)
+	eventuallyGETPath(t, "httpbin", http.StatusOK, "<title>httpbin.org</title>", emptyHeaderSet)
+	eventuallyGETPath(t, "httpbin", http.StatusOK, "<title>Welcome to nginx!</title>", emptyHeaderSet)
+	eventuallyGETPath(t, "httpbin", http.StatusOK, "<title>httpbin.org</title>", emptyHeaderSet)
+	eventuallyGETPath(t, "httpbin", http.StatusOK, "<title>httpbin.org</title>", emptyHeaderSet)
+	eventuallyGETPath(t, "httpbin", http.StatusOK, "<title>httpbin.org</title>", emptyHeaderSet)
+	eventuallyGETPath(t, "httpbin", http.StatusOK, "<title>Welcome to nginx!</title>", emptyHeaderSet)
+
 	t.Log("removing the parentrefs from the HTTPRoute")
 	oldParentRefs := httproute.Spec.ParentRefs
 	require.Eventually(t, func() bool {
diff --git a/test/integration/ingress_test.go b/test/integration/ingress_test.go
index ed8a721e4..31df56c91 100644
--- a/test/integration/ingress_test.go
+++ b/test/integration/ingress_test.go
@@ -676,6 +676,7 @@ func TestDefaultIngressClass(t *testing.T) {
 			t.Logf("WARNING: error while waiting for %s: %v", proxyURL, err)
 			return false
 		}
+		defer resp.Body.Close()
 		if resp.StatusCode < http.StatusBadRequest {
 			t.Logf("unexpected status when checking Ingress status: %v", resp.StatusCode)
 			return true