Repository: kubernetes-sigs/aws-alb-ingress-controller
Pull request #1527 (2020-10-21 00:32:20)

Allow patch for events

k8s-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/aws-alb-ingress-controller/pull/1527#" title="Author self-approved">kishorj</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign  after the PR has been reviewed.
You can assign the PR to them by writing `/assign ` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Faws-alb-ingress-controller).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubernetes-sigs/aws-alb-ingress-controller/blob/main/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
k8s-ci-robot: Hi @kishorj. Thanks for your PR.

I'm waiting for a [kubernetes-sigs](https://github.com/orgs/kubernetes-sigs/people) member to verify that this patch is reasonable to test. If it is, they should reply with `/ok-to-test` on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work. Regular contributors should [join the org](https://git.k8s.io/community/community-membership.md#member) to skip this step.

Once the patch is verified, the new status will be reflected by the `ok-to-test` label.

I understand the commands that are listed [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Faws-alb-ingress-controller).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>


Patch:
diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml
index 4d602957d..6edc2f40a 100644
--- a/config/rbac/role.yaml
+++ b/config/rbac/role.yaml
@@ -20,6 +20,7 @@ rules:
   - events
   verbs:
   - create
+  - patch
 - apiGroups:
   - ""
   resources:
diff --git a/controllers/elbv2/targetgroupbinding_controller.go b/controllers/elbv2/targetgroupbinding_controller.go
index d09a4c6db..206aedfe1 100644
--- a/controllers/elbv2/targetgroupbinding_controller.go
+++ b/controllers/elbv2/targetgroupbinding_controller.go
@@ -74,7 +74,7 @@ type targetGroupBindingReconciler struct {
 // +kubebuilder:rbac:groups="",resources=endpoints,verbs=get;list;watch
 // +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch
 // +kubebuilder:rbac:groups="",resources=namespaces,verbs=get;list;watch
-// +kubebuilder:rbac:groups="",resources=events,verbs=create
+// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch
 
 func (r *targetGroupBindingReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
 	return runtime.HandleReconcileError(r.reconcile(req), r.logger)
diff --git a/controllers/ingress/group_controller.go b/controllers/ingress/group_controller.go
index 0debe608f..b85aba063 100644
--- a/controllers/ingress/group_controller.go
+++ b/controllers/ingress/group_controller.go
@@ -92,7 +92,7 @@ type groupReconciler struct {
 // +kubebuilder:rbac:groups=extensions,resources=ingresses/status,verbs=update;patch
 // +kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;update;patch
 // +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch
-// +kubebuilder:rbac:groups="",resources=events,verbs=create
+// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch
 
 // Reconcile
 func (r *groupReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
diff --git a/controllers/service/service_controller.go b/controllers/service/service_controller.go
index ca29ddcf4..6a54a8815 100644
--- a/controllers/service/service_controller.go
+++ b/controllers/service/service_controller.go
@@ -70,7 +70,7 @@ type serviceReconciler struct {
 
 // +kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;update;patch
 // +kubebuilder:rbac:groups="",resources=services/status,verbs=update;patch
-// +kubebuilder:rbac:groups="",resources=events,verbs=create
+// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch
 
 func (r *serviceReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
 	return runtime.HandleReconcileError(r.reconcile(req), r.logger)
diff --git a/main.go b/main.go
index b56878b75..eae89fccb 100644
--- a/main.go
+++ b/main.go
@@ -22,6 +22,7 @@ import (
 	"github.com/spf13/pflag"
 	zapraw "go.uber.org/zap"
 	k8sruntime "k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/client-go/kubernetes"
 	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
 	_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
 	"os"
@@ -38,9 +39,9 @@ import (
 	"sigs.k8s.io/aws-load-balancer-controller/pkg/runtime"
 	"sigs.k8s.io/aws-load-balancer-controller/pkg/targetgroupbinding"
 	"sigs.k8s.io/aws-load-balancer-controller/pkg/version"
+	ctrl "sigs.k8s.io/controller-runtime"
 	corewebhook "sigs.k8s.io/aws-load-balancer-controller/webhooks/core"
 	elbv2webhook "sigs.k8s.io/aws-load-balancer-controller/webhooks/elbv2"
-	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/log/zap"
 	"sigs.k8s.io/controller-runtime/pkg/metrics"
 	// +kubebuilder:scaffold:imports
@@ -88,7 +89,12 @@ func main() {
 		setupLog.Error(err, "unable to start manager")
 		os.Exit(1)
 	}
-
+	clientSet, err := kubernetes.NewForConfig(mgr.GetConfig())
+	if err != nil {
+		setupLog.Error(err, "unable to obtain clientSet")
+		os.Exit(1)
+	}
+	podInfoRepo := k8s.NewDefaultPodInfoRepo(clientSet.CoreV1().RESTClient(), rtOpts.Namespace, ctrl.Log)
 	finalizerManager := k8s.NewDefaultFinalizerManager(mgr.GetClient(), ctrl.Log)
 	podENIResolver := networking.NewDefaultPodENIInfoResolver(cloud.EC2(), cloud.VpcID(), ctrl.Log)
 	nodeENIResolver := networking.NewDefaultNodeENIInfoResolver(cloud.EC2(), ctrl.Log)
@@ -96,7 +102,7 @@ func main() {
 	sgReconciler := networking.NewDefaultSecurityGroupReconciler(sgManager, ctrl.Log)
 	subnetResolver := networking.NewSubnetsResolver(cloud.EC2(), cloud.VpcID(), controllerCFG.ClusterName, ctrl.Log.WithName("subnets-resolver"))
 	tgbResManager := targetgroupbinding.NewDefaultResourceManager(mgr.GetClient(), cloud.ELBV2(),
-		podENIResolver, nodeENIResolver, sgManager, sgReconciler, cloud.VpcID(), controllerCFG.ClusterName, ctrl.Log)
+		podInfoRepo, podENIResolver, nodeENIResolver, sgManager, sgReconciler, cloud.VpcID(), controllerCFG.ClusterName, ctrl.Log)
 
 	ingGroupReconciler := ingress.NewGroupReconciler(cloud, mgr.GetClient(), mgr.GetEventRecorderFor("ingress"),
 		finalizerManager, sgManager, sgReconciler, subnetResolver,
@@ -125,10 +131,21 @@ func main() {
 	corewebhook.NewPodMutator(podReadinessGateInjector).SetupWithManager(mgr)
 	elbv2webhook.NewTargetGroupBindingMutator(cloud.ELBV2(), ctrl.Log).SetupWithManager(mgr)
 	elbv2webhook.NewTargetGroupBindingValidator(ctrl.Log).SetupWithManager(mgr)
-	// +kubebuilder:scaffold:builder
-
-	setupLog.Info("starting manager")
-	if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
+	//+kubebuilder:scaffold:builder
+
+	stopChan := ctrl.SetupSignalHandler()
+	go func() {
+		setupLog.Info("starting podInfo repo")
+		if err := podInfoRepo.Start(stopChan); err != nil {
+			setupLog.Error(err, "problem running podInfo repo")
+			os.Exit(1)
+		}
+	}()
+	if err := podInfoRepo.WaitForCacheSync(stopChan); err != nil {
+		setupLog.Error(err, "problem wait for podInfo repo sync")
+		os.Exit(1)
+	}
+	if err := mgr.Start(stopChan); err != nil {
 		setupLog.Error(err, "problem running manager")
 		os.Exit(1)
 	}
diff --git a/mocks/k8s/mock_repo_info_repo.go b/mocks/k8s/mock_repo_info_repo.go
new file mode 100644
index 000000000..ed0249338
--- /dev/null
+++ b/mocks/k8s/mock_repo_info_repo.go
@@ -0,0 +1,66 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: sigs.k8s.io/aws-load-balancer-controller/pkg/k8s (interfaces: PodInfoRepo)
+
+// Package mock_k8s is a generated GoMock package.
+package mock_k8s
+
+import (
+	context "context"
+	gomock "github.com/golang/mock/gomock"
+	types "k8s.io/apimachinery/pkg/types"
+	reflect "reflect"
+	k8s "sigs.k8s.io/aws-load-balancer-controller/pkg/k8s"
+)
+
+// MockPodInfoRepo is a mock of PodInfoRepo interface
+type MockPodInfoRepo struct {
+	ctrl     *gomock.Controller
+	recorder *MockPodInfoRepoMockRecorder
+}
+
+// MockPodInfoRepoMockRecorder is the mock recorder for MockPodInfoRepo
+type MockPodInfoRepoMockRecorder struct {
+	mock *MockPodInfoRepo
+}
+
+// NewMockPodInfoRepo creates a new mock instance
+func NewMockPodInfoRepo(ctrl *gomock.Controller) *MockPodInfoRepo {
+	mock := &MockPodInfoRepo{ctrl: ctrl}
+	mock.recorder = &MockPodInfoRepoMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use
+func (m *MockPodInfoRepo) EXPECT() *MockPodInfoRepoMockRecorder {
+	return m.recorder
+}
+
+// Get mocks base method
+func (m *MockPodInfoRepo) Get(arg0 context.Context, arg1 types.NamespacedName) (k8s.PodInfo, bool, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Get", arg0, arg1)
+	ret0, _ := ret[0].(k8s.PodInfo)
+	ret1, _ := ret[1].(bool)
+	ret2, _ := ret[2].(error)
+	return ret0, ret1, ret2
+}
+
+// Get indicates an expected call of Get
+func (mr *MockPodInfoRepoMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockPodInfoRepo)(nil).Get), arg0, arg1)
+}
+
+// ListKeys mocks base method
+func (m *MockPodInfoRepo) ListKeys(arg0 context.Context) []types.NamespacedName {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ListKeys", arg0)
+	ret0, _ := ret[0].([]types.NamespacedName)
+	return ret0
+}
+
+// ListKeys indicates an expected call of ListKeys
+func (mr *MockPodInfoRepoMockRecorder) ListKeys(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListKeys", reflect.TypeOf((*MockPodInfoRepo)(nil).ListKeys), arg0)
+}
diff --git a/pkg/backend/endpoint_resolver.go b/pkg/backend/endpoint_resolver.go
index a4e933a1c..de606cd52 100644
--- a/pkg/backend/endpoint_resolver.go
+++ b/pkg/backend/endpoint_resolver.go
@@ -5,27 +5,35 @@ import (
 	"github.com/go-logr/logr"
 	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/util/intstr"
 	"sigs.k8s.io/aws-load-balancer-controller/pkg/k8s"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
-// EndpointResolver resolves the endpoints for specific service & service Port.
+// TODO: for pod endpoints, we currently rely on endpoints events, we might change to use pod events directly in the future.
+// under current implementation with pod readinessGate enabled, an unready endpoint but not match our inclusionCriteria won't be registered,
+// and it won't turn ready due to blocked by readinessGate, and no future endpoint events will trigger.
+// We solve this by requeue the TGB if unready endpoints have the potential to be ready if reconcile in later time.
+
+//  EndpointResolver resolves the endpoints for specific service & service Port.
 type EndpointResolver interface {
 	// ResolvePodEndpoints will resolve endpoints backed by pods directly.
-	ResolvePodEndpoints(ctx context.Context, svcKey types.NamespacedName, port intstr.IntOrString, opts ...EndpointResolveOption) ([]PodEndpoint, error)
+	// returns resolved podEndpoints and whether there are unready endpoints that can potentially turn ready in future reconciles.
+	ResolvePodEndpoints(ctx context.Context, svcKey types.NamespacedName, port intstr.IntOrString,
+		opts ...EndpointResolveOption) ([]PodEndpoint, bool, error)
 
 	// ResolveNodePortEndpoints will resolve endpoints backed by nodePort.
-	ResolveNodePortEndpoints(ctx context.Context, svcKey types.NamespacedName, port intstr.IntOrString, opts ...EndpointResolveOption) ([]NodePortEndpoint, error)
+	ResolveNodePortEndpoints(ctx context.Context, svcKey types.NamespacedName, port intstr.IntOrString,
+		opts ...EndpointResolveOption) ([]NodePortEndpoint, error)
 }
 
 // NewDefaultEndpointResolver constructs new defaultEndpointResolver
-func NewDefaultEndpointResolver(k8sClient client.Client, logger logr.Logger) *defaultEndpointResolver {
+func NewDefaultEndpointResolver(k8sClient client.Client, podInfoRepo k8s.PodInfoRepo, logger logr.Logger) *defaultEndpointResolver {
 	return &defaultEndpointResolver{
-		k8sClient: k8sClient,
-		logger:    logger,
+		k8sClient:   k8sClient,
+		podInfoRepo: podInfoRepo,
+		logger:      logger,
 	}
 }
 
@@ -33,26 +41,27 @@ var _ EndpointResolver = &defaultEndpointResolver{}
 
 // default implementation for EndpointResolver
 type defaultEndpointResolver struct {
-	k8sClient client.Client
-	logger    logr.Logger
+	k8sClient   client.Client
+	podInfoRepo k8s.PodInfoRepo
+	logger      logr.Logger
 }
 
-func (r *defaultEndpointResolver) ResolvePodEndpoints(ctx context.Context, svcKey types.NamespacedName, port intstr.IntOrString, opts ...EndpointResolveOption) ([]PodEndpoint, error) {
-	resolveOpts := EndpointResolveOptions{
-		NodeSelector: labels.Nothing(),
-	}
+func (r *defaultEndpointResolver) ResolvePodEndpoints(ctx context.Context, svcKey types.NamespacedName, port intstr.IntOrString,
+	opts ...EndpointResolveOption) ([]PodEndpoint, bool, error) {
+	resolveOpts := defaultEndpointResolveOptions()
 	resolveOpts.ApplyOptions(opts)
+
 	svc, svcPort, err := r.findServiceAndServicePort(ctx, svcKey, port)
 	if err != nil {
-		return nil, err
+		return nil, false, err
 	}
-
 	epsKey := k8s.NamespacedName(svc) // k8s Endpoints have same name as k8s Service
 	eps := &corev1.Endpoints{}
 	if err := r.k8sClient.Get(ctx, epsKey, eps); err != nil {
-		return nil, err
+		return nil, false, err
 	}
 
+	containsPotentialReadyEndpoints := false
 	var endpoints []PodEndpoint
 	for _, epSubset := range eps.Subsets {
 		for _, epPort := range epSubset.Ports {
@@ -65,43 +74,53 @@ func (r *defaultEndpointResolver) ResolvePodEndpoints(ctx context.Context, svcKe
 				if epAddr.TargetRef == nil || epAddr.TargetRef.Kind != "Pod" {
 					continue
 				}
-				epPod, err := r.findPodByReference(ctx, svc.Namespace, *epAddr.TargetRef)
+				pod, exists, err := r.findPodByReference(ctx, svc.Namespace, *epAddr.TargetRef)
 				if err != nil {
-					return nil, err
+					return nil, false, err
 				}
-				endpoints = append(endpoints, buildPodEndpoint(epPod, epAddr, epPort))
+				if !exists {
+					return nil, false, errors.New("couldn't find podInfo for ready endpoint")
+				}
+				endpoints = append(endpoints, buildPodEndpoint(pod, epAddr, epPort))
 			}
 
-			if len(resolveOpts.UnreadyPodInclusionCriteria) != 0 {
+			if len(resolveOpts.PodReadinessGates) != 0 {
 				for _, epAddr := range epSubset.NotReadyAddresses {
 					if epAddr.TargetRef == nil || epAddr.TargetRef.Kind != "Pod" {
 						continue
 					}
-					epPod, err := r.findPodByReference(ctx, svc.Namespace, *epAddr.TargetRef)
+					pod, exists, err := r.findPodByReference(ctx, svc.Namespace, *epAddr.TargetRef)
 					if err != nil {
-						return nil, err
+						return nil, false, err
+					}
+					if !exists {
+						containsPotentialReadyEndpoints = true
+						continue
+					}
+					if !pod.HasAnyOfReadinessGates(resolveOpts.PodReadinessGates) {
+						continue
 					}
-					if isPodMeetCriteria(epPod, resolveOpts.UnreadyPodInclusionCriteria) {
-						endpoints = append(endpoints, buildPodEndpoint(epPod, epAddr, epPort))
+					if !pod.IsContainersReady() {
+						containsPotentialReadyEndpoints = true
+						continue
 					}
+					endpoints = append(endpoints, buildPodEndpoint(pod, epAddr, epPort))
 				}
 			}
 		}
 	}
 
-	return endpoints, nil
+	return endpoints, containsPotentialReadyEndpoints, nil
 }
 
 func (r *defaultEndpointResolver) ResolveNodePortEndpoints(ctx context.Context, svcKey types.NamespacedName, port intstr.IntOrString, opts ...EndpointResolveOption) ([]NodePortEndpoint, error) {
-	resolveOpts := EndpointResolveOptions{
-		NodeSelector: labels.Nothing(),
-	}
+	resolveOpts := defaultEndpointResolveOptions()
 	resolveOpts.ApplyOptions(opts)
+
 	svc, svcPort, err := r.findServiceAndServicePort(ctx, svcKey, port)
 	if err != nil {
 		return nil, err
 	}
-
 	if svc.Spec.Type != corev1.ServiceTypeNodePort && svc.Spec.Type != corev1.ServiceTypeLoadBalancer {
 		return nil, errors.Errorf("service type must be either 'NodePort' or 'LoadBalancer': %v", svcKey)
 	}
@@ -140,25 +159,12 @@ func (r *defaultEndpointResolver) findServiceAndServicePort(ctx context.Context,
 	return svc, svcPort, nil
 }
 
-func (r *defaultEndpointResolver) findPodByReference(ctx context.Context, namespace string, podRef corev1.ObjectReference) (*corev1.Pod, error) {
-	pod := &corev1.Pod{}
+func (r *defaultEndpointResolver) findPodByReference(ctx context.Context, namespace string, podRef corev1.ObjectReference) (k8s.PodInfo, bool, error) {
 	podKey := types.NamespacedName{Namespace: namespace, Name: podRef.Name}
-	if err := r.k8sClient.Get(ctx, podKey, pod); err != nil {
-		return nil, err
-	}
-	return pod, nil
-}
-
-func isPodMeetCriteria(pod *corev1.Pod, criteria []PodPredicate) bool {
-	for _, criterion := range criteria {
-		if !criterion(pod) {
-			return false
-		}
-	}
-	return true
+	return r.podInfoRepo.Get(ctx, podKey)
 }
 
-func buildPodEndpoint(pod *corev1.Pod, epAddr corev1.EndpointAddress, epPort corev1.EndpointPort) PodEndpoint {
+func buildPodEndpoint(pod k8s.PodInfo, epAddr corev1.EndpointAddress, epPort corev1.EndpointPort) PodEndpoint {
 	return PodEndpoint{
 		IP:   epAddr.IP,
 		Port: int64(epPort.Port),
diff --git a/pkg/backend/endpoint_types.go b/pkg/backend/endpoint_types.go
index b561e3537..ee8d58716 100644
--- a/pkg/backend/endpoint_types.go
+++ b/pkg/backend/endpoint_types.go
@@ -3,6 +3,7 @@ package backend
 import (
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/labels"
+	"sigs.k8s.io/aws-load-balancer-controller/pkg/k8s"
 )
 
 // An endpoint provided by pod directly.
@@ -12,10 +13,10 @@ type PodEndpoint struct {
 	// Pod's container port.
 	Port int64
 	// Pod that provides this endpoint.
-	Pod *corev1.Pod
+	Pod k8s.PodInfo
 }
 
-// An endpoint provided by nodePort as trafficProxy.
+// An endpoint provided by nodePort as traffic proxy.
 type NodePortEndpoint struct {
 	// Node's instanceID.
 	InstanceID string
@@ -25,19 +26,15 @@ type NodePortEndpoint struct {
 	Node *corev1.Node
 }
 
-type PodPredicate func(pod *corev1.Pod) bool
-
 // options for Endpoints resolve APIs
 type EndpointResolveOptions struct {
-	// [Pod Endpoint] unready pods that have passed all these inclusion criteria will be included as well. (ready pods will always be included)
-	// If it's empty, unready pods won't be included.
-	// If it's non-empty, unready pods passed *all* criteria will be included.
-	// By default, it's empty.
-	UnreadyPodInclusionCriteria []PodPredicate
-
 	// [NodePort Endpoint] only nodes that are ready and matched by nodeSelector will be included.
 	// By default, no node will be selected.
 	NodeSelector labels.Selector
+
+	// [Pod Endpoint] If pod readinessGates is defined, then pods from unready addresses with any of these readinessGates and containersReady condition will be included as well.
+	// By default, no readinessGate is specified.
+	PodReadinessGates []corev1.PodConditionType
 }
 
 func (opts *EndpointResolveOptions) ApplyOptions(options []EndpointResolveOption) {
@@ -55,9 +52,17 @@ func WithNodeSelector(nodeSelector labels.Selector) EndpointResolveOption {
 	}
 }
 
-// WithUnreadyPodInclusionCriterion is a option that appends additional criterion into UnreadyPodInclusionCriteria.
-func WithUnreadyPodInclusionCriterion(predicate PodPredicate) EndpointResolveOption {
+// WithPodReadinessGate is a option that appends podReadinessGate into EndpointResolveOptions.
+func WithPodReadinessGate(cond corev1.PodConditionType) EndpointResolveOption {
 	return func(opts *EndpointResolveOptions) {
-		opts.UnreadyPodInclusionCriteria = append(opts.UnreadyPodInclusionCriteria, predicate)
+		opts.PodReadinessGates = append(opts.PodReadinessGates, cond)
+	}
+}
+
+// defaultEndpointResolveOptions returns the default value for EndpointResolveOptions.
+func defaultEndpointResolveOptions() EndpointResolveOptions {
+	return EndpointResolveOptions{
+		NodeSelector:      labels.Nothing(),
+		PodReadinessGates: nil,
 	}
 }
diff --git a/pkg/ingress/reference_indexer.go b/pkg/ingress/reference_indexer.go
index 9d47829da..94ed829d1 100644
--- a/pkg/ingress/reference_indexer.go
+++ b/pkg/ingress/reference_indexer.go
@@ -2,7 +2,6 @@ package ingress
 
 import (
 	"context"
-	"fmt"
 	"github.com/go-logr/logr"
 	networking "k8s.io/api/networking/v1beta1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -60,7 +59,6 @@ func (i *defaultReferenceIndexer) BuildServiceRefIndexes(ctx context.Context, in
 		if err != nil {
 			i.logger.Error(err, "failed to build Ingress indexes",
 				"indexKey", IndexKeyServiceRefName)
-			fmt.Println(err)
 			return nil
 		}
 		serviceNamesFromBackend := extractServiceNamesFromAction(enhancedBackend.Action)
diff --git a/pkg/k8s/conversion_store.go b/pkg/k8s/conversion_store.go
new file mode 100644
index 000000000..34deeb431
--- /dev/null
+++ b/pkg/k8s/conversion_store.go
@@ -0,0 +1,90 @@
+package k8s
+
+import (
+	"k8s.io/client-go/tools/cache"
+)
+
+type ConversionFunc func(obj interface{}) (interface{}, error)
+
+// NewConversionStore constructs new conversionStore
+func NewConversionStore(conversionFunc ConversionFunc, keyFunc cache.KeyFunc) *ConversionStore {
+	return &ConversionStore{
+		conversionFunc: conversionFunc,
+		store:          cache.NewStore(keyFunc),
+	}
+}
+
+var _ cache.Store = &ConversionStore{}
+
+// ConversionStore implements cache.Store.
+// It converts objects to another type using specified conversionFunc.
+// In store's term, the accumulator for this store is a convert that converts object's latest state into another object.
+type ConversionStore struct {
+	conversionFunc ConversionFunc
+	store          cache.Store
+}
+
+// Add adds the given object to the accumulator associated with the given object's key
+func (s *ConversionStore) Add(obj interface{}) error {
+	converted, err := s.conversionFunc(obj)
+	if err != nil {
+		return err
+	}
+	return s.store.Add(converted)
+}
+
+// Update updates the given object in the accumulator associated with the given object's key
+func (s *ConversionStore) Update(obj interface{}) error {
+	converted, err := s.conversionFunc(obj)
+	if err != nil {
+		return err
+	}
+	return s.store.Update(converted)
+}
+
+// Delete deletes the given object from the accumulator associated with the given object's key
+func (s *ConversionStore) Delete(obj interface{}) error {
+	converted, err := s.conversionFunc(obj)
+	if err != nil {
+		return err
+	}
+	return s.store.Delete(converted)
+}
+
+// Replace will delete the contents of the store, using instead the given list.
+func (s *ConversionStore) Replace(list []interface{}, resourceVersion string) error {
+	items := make([]interface{}, 0, len(list))
+	for _, item := range list {
+		converted, err := s.conversionFunc(item)
+		if err != nil {
+			return err
+		}
+		items = append(items, converted)
+	}
+	return s.store.Replace(items, resourceVersion)
+}
+
+// Resync is meaningless in the terms appearing here
+func (s *ConversionStore) Resync() error {
+	return s.store.Resync()
+}
+
+// List returns a list of all the currently non-empty accumulators
+func (s *ConversionStore) List() []interface{} {
+	return s.store.List()
+}
+
+// ListKeys returns a list of all the keys currently associated with non-empty accumulators
+func (s *ConversionStore) ListKeys() []string {
+	return s.store.ListKeys()
+}
+
+// Get returns the accumulator associated with the given object's key
+func (s *ConversionStore) Get(obj interface{}) (item interface{}, exists bool, err error) {
+	return s.store.Get(obj)
+}
+
+// GetByKey returns the accumulator associated with the given key
+func (s *ConversionStore) GetByKey(key string) (item interface{}, exists bool, err error) {
+	return s.store.GetByKey(key)
+}
diff --git a/pkg/k8s/pod_info.go b/pkg/k8s/pod_info.go
new file mode 100644
index 000000000..7507c8fa0
--- /dev/null
+++ b/pkg/k8s/pod_info.go
@@ -0,0 +1,123 @@
+package k8s
+
+import (
+	"encoding/json"
+	"github.com/pkg/errors"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/intstr"
+)
+
+const (
+	annotationKeyPodENIInfo = "vpc.amazonaws.com/pod-eni"
+)
+
+// PodInfo contains simplified pod information we cares about.
+// We do so to minimize memory usage.
+type PodInfo struct {
+	Key types.NamespacedName
+	UID types.UID
+
+	ContainerPorts []corev1.ContainerPort
+	ReadinessGates []corev1.PodReadinessGate
+	Conditions     []corev1.PodCondition
+	PodIP          string
+
+	ENIInfos []PodENIInfo
+}
+
+// PodENIInfo is a json convertible structure that stores the Branch ENI details that can be
+// used by the CNI plugin or the component consuming the resource
+type PodENIInfo struct {
+	// ENIID is the network interface id of the branch interface
+	ENIID string `json:"eniId"`
+
+	// PrivateIP is the primary IP of the branch Network interface
+	PrivateIP string `json:"privateIp"`
+}
+
+// HasAnyOfReadinessGates returns whether podInfo has any of these readinessGates
+func (i *PodInfo) HasAnyOfReadinessGates(conditionTypes []corev1.PodConditionType) bool {
+	for _, rg := range i.ReadinessGates {
+		for _, conditionType := range conditionTypes {
+			if rg.ConditionType == conditionType {
+				return true
+			}
+		}
+	}
+	return false
+}
+
+// IsContainersReady returns whether podInfo is ContainersReady.
+func (i *PodInfo) IsContainersReady() bool {
+	containersReadyCond, exists := i.GetPodCondition(corev1.ContainersReady)
+	return exists && containersReadyCond.Status == corev1.ConditionTrue
+}
+
+// GetPodCondition will get Pod's condition.
+func (i *PodInfo) GetPodCondition(conditionType corev1.PodConditionType) (corev1.PodCondition, bool) {
+	for _, cond := range i.Conditions {
+		if cond.Type == conditionType {
+			return cond, true
+		}
+	}
+
+	return corev1.PodCondition{}, false
+}
+
+// LookupContainerPort returns the numerical containerPort for specific port on Pod.
+func (i *PodInfo) LookupContainerPort(port intstr.IntOrString) (int64, error) {
+	switch port.Type {
+	case intstr.String:
+		for _, podPort := range i.ContainerPorts {
+			if podPort.Name == port.StrVal {
+				return int64(podPort.ContainerPort), nil
+			}
+		}
+	case intstr.Int:
+		return int64(port.IntVal), nil
+	}
+	return 0, errors.Errorf("unable to find port %s on pod %s", port.String(), i.Key)
+}
+
+// buildPodInfo will construct PodInfo for given pod.
+func buildPodInfo(pod *corev1.Pod) PodInfo {
+	podKey := NamespacedName(pod)
+
+	var podENIInfos []PodENIInfo
+	// we kept podENIInfo as nil if the eniInfo via annotation is malformed.
+	if eniInfo, err := buildPodENIInfos(pod); err == nil {
+		podENIInfos = eniInfo
+	}
+
+	var containerPorts []corev1.ContainerPort
+	for _, podContainer := range pod.Spec.Containers {
+		containerPorts = append(containerPorts, podContainer.Ports...)
+	}
+	return PodInfo{
+		Key: podKey,
+		UID: pod.UID,
+
+		ContainerPorts: containerPorts,
+		ReadinessGates: pod.Spec.ReadinessGates,
+		Conditions:     pod.Status.Conditions,
+		PodIP:          pod.Status.PodIP,
+
+		ENIInfos: podENIInfos,
+	}
+}
+
+// buildPodENIInfo will construct PodENIInfo for given pod if any.
+func buildPodENIInfos(pod *corev1.Pod) ([]PodENIInfo, error) {
+	rawAnnotation, ok := pod.Annotations[annotationKeyPodENIInfo]
+	if !ok {
+		return nil, nil
+	}
+
+	var podENIInfos []PodENIInfo
+	if err := json.Unmarshal([]byte(rawAnnotation), &podENIInfos); err != nil {
+		return nil, err
+	}
+
+	return podENIInfos, nil
+}
diff --git a/pkg/k8s/pod_info_repo.go b/pkg/k8s/pod_info_repo.go
new file mode 100644
index 000000000..e096d69eb
--- /dev/null
+++ b/pkg/k8s/pod_info_repo.go
@@ -0,0 +1,119 @@
+package k8s
+
+import (
+	"context"
+	"errors"
+	"github.com/go-logr/logr"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/fields"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/wait"
+	"k8s.io/client-go/tools/cache"
+	"sigs.k8s.io/controller-runtime/pkg/manager"
+	"time"
+)
+
+const (
+	resourceTypePods        = "pods"
+	waitCacheSyncPollPeriod = 2 * time.Second
+)
+
+// PodInfoRepo provides access to pod information within cluster.
+// We only store necessary pod information related to our controller to reduce memory usage.
+type PodInfoRepo interface {
+	// Get returns PodInfo specified with specific podKey, and whether it exists.
+	Get(ctx context.Context, key types.NamespacedName) (PodInfo, bool, error)
+
+	// ListKeys will list the pod keys in this repo.
+	ListKeys(ctx context.Context) []types.NamespacedName
+}
+
+// NewDefaultPodInfoRepo constructs new defaultPodInfoRepo.
+// * watchNamespace is the namespace to monitor pod spec.
+// 		* if watchNamespace is "", this repo monitors pods in all namespaces
+// 		* if watchNamespace is not "", this repo monitors pods in specific namespace
+func NewDefaultPodInfoRepo(getter cache.Getter, watchNamespace string, logger logr.Logger) *defaultPodInfoRepo {
+	store := NewConversionStore(podInfoConversionFunc, podInfoKeyFunc)
+	lw := cache.NewListWatchFromClient(getter, resourceTypePods, watchNamespace, fields.Everything())
+	rt := cache.NewReflector(lw, &corev1.Pod{}, store, 0)
+
+	repo := &defaultPodInfoRepo{
+		store:  store,
+		rt:     rt,
+		logger: logger,
+	}
+	return repo
+}
+
+var _ PodInfoRepo = &defaultPodInfoRepo{}
+var _ manager.Runnable = &defaultPodInfoRepo{}
+
+// default implementation for PodInfoRepo
+type defaultPodInfoRepo struct {
+	store  *ConversionStore
+	rt     *cache.Reflector
+	logger logr.Logger
+}
+
+// Get returns PodInfo specified with specific podKey, and whether it exists.
+func (r *defaultPodInfoRepo) Get(_ context.Context, key types.NamespacedName) (PodInfo, bool, error) {
+	pInfo := PodInfo{Key: key}
+	raw, exists, err := r.store.Get(&pInfo)
+	if err != nil {
+		return PodInfo{}, false, err
+	}
+	if !exists {
+		return PodInfo{}, false, nil
+	}
+	pInfo = *raw.(*PodInfo)
+	return pInfo, true, nil
+}
+
+// ListKeys will list the pod keys in this repo.
+func (r *defaultPodInfoRepo) ListKeys(_ context.Context) []types.NamespacedName {
+	storeKeys := r.store.ListKeys()
+	keys := make([]types.NamespacedName, 0, len(storeKeys))
+	for _, storeKey := range storeKeys {
+		namespace, name, _ := cache.SplitMetaNamespaceKey(storeKey)
+		key := types.NamespacedName{
+			Namespace: namespace,
+			Name:      name,
+		}
+		keys = append(keys, key)
+	}
+	return keys
+}
+
+// Start will start the repo.
+// It leverages ListWatch to keep pod info stored locally to be in-sync with Kubernetes.
+func (r *defaultPodInfoRepo) Start(stopChan <-chan struct{}) error {
+	r.rt.Run(stopChan)
+	return nil
+}
+
+// WaitForCacheSync waits for the initial sync of pod information repository.
+func (r *defaultPodInfoRepo) WaitForCacheSync(stopCh <-chan struct{}) error {
+	return wait.PollImmediateUntil(waitCacheSyncPollPeriod, func() (bool, error) {
+		lastSyncResourceVersion := r.rt.LastSyncResourceVersion()
+		return lastSyncResourceVersion != "", nil
+	}, stopCh)
+}
+
+// podInfoKeyFunc computes the store key per PodInfo object.
+func podInfoKeyFunc(obj interface{}) (string, error) {
+	info, ok := obj.(*PodInfo)
+	if !ok {
+		return "", errors.New("expect PodInfo object")
+	}
+	return info.Key.String(), nil
+}
+
+// podInfoConversionFunc computes the converted PodInfo per pod object.
+func podInfoConversionFunc(obj interface{}) (interface{}, error) {
+	pod, ok := obj.(*corev1.Pod)
+	if !ok {
+		return nil, errors.New("expect pod object")
+	}
+	podInfo := buildPodInfo(pod)
+	return &podInfo, nil
+}
diff --git a/pkg/networking/node_eni_info_resolver.go b/pkg/networking/node_eni_info_resolver.go
index a871eb798..622e657b3 100644
--- a/pkg/networking/node_eni_info_resolver.go
+++ b/pkg/networking/node_eni_info_resolver.go
@@ -52,22 +52,22 @@ type defaultNodeENIInfoResolver struct {
 
 func (r *defaultNodeENIInfoResolver) Resolve(ctx context.Context, nodes []*corev1.Node) (map[types.NamespacedName]ENIInfo, error) {
 	eniInfoByNodeKey := r.fetchENIInfosFromCache(nodes)
-	unresolvedNodes := computeNodesWithUnresolvedENIInfo(nodes, eniInfoByNodeKey)
-	eniInfoByNodeKeyViaLookup, err := r.resolveViaInstanceID(ctx, unresolvedNodes)
+	nodesWithoutENIInfo := computeNodesWithoutENIInfo(nodes, eniInfoByNodeKey)
+	eniInfoByNodeKeyViaLookup, err := r.resolveViaInstanceID(ctx, nodesWithoutENIInfo)
 	if err != nil {
 		return nil, err
 	}
 	if len(eniInfoByNodeKeyViaLookup) > 0 {
-		r.saveENIInfosToCache(unresolvedNodes, eniInfoByNodeKeyViaLookup)
+		r.saveENIInfosToCache(nodesWithoutENIInfo, eniInfoByNodeKeyViaLookup)
 		for nodeKey, eniInfo := range eniInfoByNodeKeyViaLookup {
 			eniInfoByNodeKey[nodeKey] = eniInfo
 		}
-		unresolvedNodes = computeNodesWithUnresolvedENIInfo(unresolvedNodes, eniInfoByNodeKeyViaLookup)
+		nodesWithoutENIInfo = computeNodesWithoutENIInfo(nodesWithoutENIInfo, eniInfoByNodeKeyViaLookup)
 	}
 
-	if len(unresolvedNodes) > 0 {
-		unresolvedNodeKeys := make([]types.NamespacedName, 0, len(unresolvedNodes))
-		for _, node := range unresolvedNodes {
+	if len(nodesWithoutENIInfo) > 0 {
+		unresolvedNodeKeys := make([]types.NamespacedName, 0, len(nodesWithoutENIInfo))
+		for _, node := range nodesWithoutENIInfo {
 			unresolvedNodeKeys = append(unresolvedNodeKeys, k8s.NamespacedName(node))
 		}
 		return nil, errors.Errorf("cannot resolve node ENI for nodes: %v", unresolvedNodeKeys)
@@ -80,7 +80,7 @@ type nodeENIInfoCacheKey struct {
 	nodeKey types.NamespacedName
 	// Node's UID.
 	// Note: we assume node's eni haven't changed as long as node UID is same.
-	nodeUID string
+	nodeUID types.UID
 }
 
 func (r *defaultNodeENIInfoResolver) fetchENIInfosFromCache(nodes []*corev1.Node) map[types.NamespacedName]ENIInfo {
@@ -164,12 +164,12 @@ func findInstancePrimaryENI(enis []*ec2sdk.InstanceNetworkInterface) (*ec2sdk.In
 func computeNodeENIInfoCacheKey(node *corev1.Node) nodeENIInfoCacheKey {
 	return nodeENIInfoCacheKey{
 		nodeKey: k8s.NamespacedName(node),
-		nodeUID: string(node.UID),
+		nodeUID: node.UID,
 	}
 }
 
-// computeNodesWithUnresolvedENIInfo computes nodes that don't have resolvedENIInfo.
-func computeNodesWithUnresolvedENIInfo(nodes []*corev1.Node, eniInfoByNodeKey map[types.NamespacedName]ENIInfo) []*corev1.Node {
+// computeNodesWithoutENIInfo computes nodes that don't have resolvedENIInfo.
+func computeNodesWithoutENIInfo(nodes []*corev1.Node, eniInfoByNodeKey map[types.NamespacedName]ENIInfo) []*corev1.Node {
 	unresolvedNodes := make([]*corev1.Node, 0, len(nodes)-len(eniInfoByNodeKey))
 	for _, node := range nodes {
 		nodeKey := k8s.NamespacedName(node)
diff --git a/pkg/networking/pod_eni_info_resolver.go b/pkg/networking/pod_eni_info_resolver.go
index 4332e144a..662fd99bb 100644
--- a/pkg/networking/pod_eni_info_resolver.go
+++ b/pkg/networking/pod_eni_info_resolver.go
@@ -2,12 +2,10 @@ package networking
 
 import (
 	"context"
-	"encoding/json"
 	awssdk "github.com/aws/aws-sdk-go/aws"
 	ec2sdk "github.com/aws/aws-sdk-go/service/ec2"
 	"github.com/go-logr/logr"
 	"github.com/pkg/errors"
-	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/util/cache"
 	"k8s.io/apimachinery/pkg/util/sets"
@@ -19,8 +17,6 @@ import (
 )
 
 const (
-	annotationPodENI = "vpc.amazonaws.com/pod-eni"
-
 	defaultPodENIInfoCacheTTL = 10 * time.Minute
 	// EC2:DescribeNetworkInterface supports up to 200 filters per call.
 	describeNetworkInterfacesFiltersLimit = 200
@@ -29,7 +25,7 @@ const (
 // PodENIInfoResolver is responsible for resolve the AWS VPC ENI that supports pod network.
 type PodENIInfoResolver interface {
 	// Resolve resolves eniInfo for pods.
-	Resolve(ctx context.Context, pods []*corev1.Pod) (map[types.NamespacedName]ENIInfo, error)
+	Resolve(ctx context.Context, pods []k8s.PodInfo) (map[types.NamespacedName]ENIInfo, error)
 }
 
 // NewDefaultPodENIResolver constructs new defaultPodENIResolver.
@@ -68,28 +64,27 @@ type defaultPodENIInfoResolver struct {
 	describeNetworkInterfacesIPChunkSize int
 }
 
-func (r *defaultPodENIInfoResolver) Resolve(ctx context.Context, pods []*corev1.Pod) (map[types.NamespacedName]ENIInfo, error) {
+func (r *defaultPodENIInfoResolver) Resolve(ctx context.Context, pods []k8s.PodInfo) (map[types.NamespacedName]ENIInfo, error) {
 	eniInfoByPodKey := r.fetchENIInfosFromCache(pods)
-
-	unresolvedPods := computePodsWithUnresolvedENIInfo(pods, eniInfoByPodKey)
-	eniInfoByPodKeyViaLookup, err := r.resolveViaCascadedLookup(ctx, unresolvedPods)
+	podsWithoutENIInfo := computePodsWithoutENIInfo(pods, eniInfoByPodKey)
+	eniInfoByPodKeyViaLookup, err := r.resolveViaCascadedLookup(ctx, podsWithoutENIInfo)
 	if err != nil {
 		return nil, err
 	}
 	if len(eniInfoByPodKeyViaLookup) > 0 {
-		r.saveENIInfosToCache(unresolvedPods, eniInfoByPodKeyViaLookup)
+		r.saveENIInfosToCache(podsWithoutENIInfo, eniInfoByPodKeyViaLookup)
 		for podKey, eniInfo := range eniInfoByPodKeyViaLookup {
 			eniInfoByPodKey[podKey] = eniInfo
 		}
-		unresolvedPods = computePodsWithUnresolvedENIInfo(unresolvedPods, eniInfoByPodKeyViaLookup)
+		podsWithoutENIInfo = computePodsWithoutENIInfo(podsWithoutENIInfo, eniInfoByPodKeyViaLookup)
 	}
 
-	if len(unresolvedPods) > 0 {
-		unresolvedPodKeys := make([]types.NamespacedName, 0, len(unresolvedPods))
-		for _, pod := range unresolvedPods {
-			unresolvedPodKeys = append(unresolvedPodKeys, k8s.NamespacedName(pod))
+	if len(podsWithoutENIInfo) > 0 {
+		podKeysWithoutENIInfo := make([]types.NamespacedName, 0, len(podsWithoutENIInfo))
+		for _, pod := range podsWithoutENIInfo {
+			podKeysWithoutENIInfo = append(podKeysWithoutENIInfo, pod.Key)
 		}
-		return nil, errors.Errorf("cannot resolve pod ENI for pods: %v", unresolvedPodKeys)
+		return nil, errors.Errorf("cannot resolve pod ENI for pods: %v", podKeysWithoutENIInfo)
 	}
 	return eniInfoByPodKey, nil
 }
@@ -99,10 +94,10 @@ type podENIInfoCacheKey struct {
 	podKey types.NamespacedName
 	// Pod's UID.
 	// Note: we assume pod's eni haven't changed as long as pod UID is same.
-	podUID string
+	podUID types.UID
 }
 
-func (r *defaultPodENIInfoResolver) fetchENIInfosFromCache(pods []*corev1.Pod) map[types.NamespacedName]ENIInfo {
+func (r *defaultPodENIInfoResolver) fetchENIInfosFromCache(pods []k8s.PodInfo) map[types.NamespacedName]ENIInfo {
 	r.podENIInfoCacheMutex.RLock()
 	defer r.podENIInfoCacheMutex.RUnlock()
 
@@ -111,19 +106,19 @@ func (r *defaultPodENIInfoResolver) fetchENIInfosFromCache(pods []*corev1.Pod) m
 		cacheKey := computePodENIInfoCacheKey(pod)
 		if rawCacheItem, exists := r.podENIInfoCache.Get(cacheKey); exists {
 			eniInfo := rawCacheItem.(ENIInfo)
-			podKey := k8s.NamespacedName(pod)
+			podKey := pod.Key
 			eniInfoByPodKey[podKey] = eniInfo
 		}
 	}
 	return eniInfoByPodKey
 }
 
-func (r *defaultPodENIInfoResolver) saveENIInfosToCache(pods []*corev1.Pod, eniInfoByPodKey map[types.NamespacedName]ENIInfo) {
+func (r *defaultPodENIInfoResolver) saveENIInfosToCache(pods []k8s.PodInfo, eniInfoByPodKey map[types.NamespacedName]ENIInfo) {
 	r.podENIInfoCacheMutex.Lock()
 	defer r.podENIInfoCacheMutex.Unlock()
 
 	for _, pod := range pods {
-		podKey := k8s.NamespacedName(pod)
+		podKey := pod.Key
 		if eniInfo, exists := eniInfoByPodKey[podKey]; exists {
 			cacheKey := computePodENIInfoCacheKey(pod)
 			r.podENIInfoCache.Set(cacheKey, eniInfo, r.podENIInfoCacheTTL)
@@ -131,8 +126,8 @@ func (r *defaultPodENIInfoResolver) saveENIInfosToCache(pods []*corev1.Pod, eniI
 	}
 }
 
-func (r *defaultPodENIInfoResolver) resolveViaCascadedLookup(ctx context.Context, pods []*corev1.Pod) (map[types.NamespacedName]ENIInfo, error) {
-	resolveFuncs := []func(ctx context.Context, pods []*corev1.Pod) (map[types.NamespacedName]ENIInfo, error){
+func (r *defaultPodENIInfoResolver) resolveViaCascadedLookup(ctx context.Context, pods []k8s.PodInfo) (map[types.NamespacedName]ENIInfo, error) {
+	resolveFuncs := []func(ctx context.Context, pods []k8s.PodInfo) (map[types.NamespacedName]ENIInfo, error){
 		r.resolveViaPodENIAnnotation,
 		r.resolveViaVPCIPAddress,
 		// TODO, add support for kubenet CNI plugin(kops) by resolve via routeTable.
@@ -143,6 +138,7 @@ func (r *defaultPodENIInfoResolver) resolveViaCascadedLookup(ctx context.Context
 		if len(pods) == 0 {
 			break
 		}
+
 		resolvedENIInfoByPodKey, err := resolveFunc(ctx, pods)
 		if err != nil {
 			return nil, err
@@ -150,36 +146,26 @@ func (r *defaultPodENIInfoResolver) resolveViaCascadedLookup(ctx context.Context
 		for podKey, eniInfo := range resolvedENIInfoByPodKey {
 			eniInfoByPodKey[podKey] = eniInfo
 		}
-		pods = computePodsWithUnresolvedENIInfo(pods, resolvedENIInfoByPodKey)
+		pods = computePodsWithoutENIInfo(pods, resolvedENIInfoByPodKey)
 	}
 	return eniInfoByPodKey, nil
 }
 
-// annotationSchemaPodENI is a json convertible structure that stores the Branch ENI details that can be
-// used by the CNI plugin or the component consuming the resource
-type annotationSchemaPodENI struct {
-	// ENIID is the network interface id of the branch interface
-	ENIID string `json:"eniId"`
-	// PrivateIP is the primary IP of the branch Network interface
-	PrivateIP string `json:"privateIp"`
-	// SubnetCIDR is the CIDR block of the subnet
-	SubnetCIDR string `json:"subnetCidr"`
-}
-
 // resolveViaPodENIAnnotation tries to resolve a pod ENI via the branch ENI annotation.
-func (r *defaultPodENIInfoResolver) resolveViaPodENIAnnotation(ctx context.Context, pods []*corev1.Pod) (map[types.NamespacedName]ENIInfo, error) {
+func (r *defaultPodENIInfoResolver) resolveViaPodENIAnnotation(ctx context.Context, pods []k8s.PodInfo) (map[types.NamespacedName]ENIInfo, error) {
 	podKeysByENIID := make(map[string][]types.NamespacedName)
 	for _, pod := range pods {
-		podENIAnnotation, ok := pod.Annotations[annotationPodENI]
-		if !ok {
-			continue
+		var eniID string
+		for _, podENIInfo := range pod.ENIInfos {
+			if podENIInfo.PrivateIP == pod.PodIP {
+				eniID = podENIInfo.ENIID
+			}
 		}
-		var schema annotationSchemaPodENI
-		if err := json.Unmarshal([]byte(podENIAnnotation), &schema); err != nil {
-			return nil, err
+		if len(eniID) == 0 {
+			continue
 		}
-		eniID := schema.ENIID
-		podKey := k8s.NamespacedName(pod)
+
+		podKey := pod.Key
 		podKeysByENIID[eniID] = append(podKeysByENIID[eniID], podKey)
 	}
 	if len(podKeysByENIID) == 0 {
@@ -206,10 +192,10 @@ func (r *defaultPodENIInfoResolver) resolveViaPodENIAnnotation(ctx context.Conte
 }
 
 // resolveViaVPCIPAddress tries to resolve pod ENI via the IPAddress within VPC.
-func (r *defaultPodENIInfoResolver) resolveViaVPCIPAddress(ctx context.Context, pods []*corev1.Pod) (map[types.NamespacedName]ENIInfo, error) {
+func (r *defaultPodENIInfoResolver) resolveViaVPCIPAddress(ctx context.Context, pods []k8s.PodInfo) (map[types.NamespacedName]ENIInfo, error) {
 	podKeysByIP := make(map[string][]types.NamespacedName, len(pods))
 	for _, pod := range pods {
-		podKeysByIP[pod.Status.PodIP] = append(podKeysByIP[pod.Status.PodIP], k8s.NamespacedName(pod))
+		podKeysByIP[pod.PodIP] = append(podKeysByIP[pod.PodIP], pod.Key)
 	}
 	if len(podKeysByIP) == 0 {
 		return nil, nil
@@ -255,21 +241,21 @@ func (r *defaultPodENIInfoResolver) resolveViaVPCIPAddress(ctx context.Context,
 }
 
 // computePodENIInfoCacheKey computes the cacheKey for pod's ENIInfo cache.
-func computePodENIInfoCacheKey(pod *corev1.Pod) podENIInfoCacheKey {
+func computePodENIInfoCacheKey(podInfo k8s.PodInfo) podENIInfoCacheKey {
 	return podENIInfoCacheKey{
-		podKey: k8s.NamespacedName(pod),
-		podUID: string(pod.UID),
+		podKey: podInfo.Key,
+		podUID: podInfo.UID,
 	}
 }
 
-// computePodsWithUnresolvedENIInfo computes pods that don't have resolvedENIInfo.
-func computePodsWithUnresolvedENIInfo(pods []*corev1.Pod, eniInfoByPodKey map[types.NamespacedName]ENIInfo) []*corev1.Pod {
-	unresolvedPods := make([]*corev1.Pod, 0, len(pods)-len(eniInfoByPodKey))
+// computePodsWithoutENIInfo computes pods that don't have a ENIInfo.
+func computePodsWithoutENIInfo(pods []k8s.PodInfo, eniInfoByPodKey map[types.NamespacedName]ENIInfo) []k8s.PodInfo {
+	podsWithoutENIInfo := make([]k8s.PodInfo, 0, len(pods)-len(eniInfoByPodKey))
 	for _, pod := range pods {
-		podKey := k8s.NamespacedName(pod)
+		podKey := pod.Key
 		if _, ok := eniInfoByPodKey[podKey]; !ok {
-			unresolvedPods = append(unresolvedPods, pod)
+			podsWithoutENIInfo = append(podsWithoutENIInfo, pod)
 		}
 	}
-	return unresolvedPods
+	return podsWithoutENIInfo
 }
diff --git a/pkg/runtime/errors.go b/pkg/runtime/errors.go
index 1dda68e6e..04dbba3fc 100644
--- a/pkg/runtime/errors.go
+++ b/pkg/runtime/errors.go
@@ -1,68 +1,63 @@
 package runtime
 
 import (
+	"fmt"
 	"time"
 )
 
-// NewRequeueError constructs new RequeueError to
+// NewRequeueNeeded constructs new RequeueError to
 // instruct controller-runtime to requeue the processing item without been logged as error.
-func NewRequeueError(err error) *RequeueError {
-	return &RequeueError{
-		err: err,
+func NewRequeueNeeded(reason string) *RequeueNeeded {
+	return &RequeueNeeded{
+		reason: reason,
 	}
 }
 
-// NewRequeueAfterError constructs new RequeueAfterError to
+// NewRequeueNeededAfter constructs new RequeueNeededAfter to
 // instruct controller-runtime to requeue the processing item after specified duration without been logged as error.
-func NewRequeueAfterError(err error, duration time.Duration) *RequeueAfterError {
-	return &RequeueAfterError{
-		err:      err,
+func NewRequeueNeededAfter(reason string, duration time.Duration) *RequeueNeededAfter {
+	return &RequeueNeededAfter{
+		reason:   reason,
 		duration: duration,
 	}
 }
 
-var _ error = &RequeueError{}
+var _ error = &RequeueNeeded{}
 
 // An error to instruct controller-runtime to requeue the processing item without been logged as error.
 // This should be used when a "error condition" occurrence is sort of expected and can be resolved by retry.
 // e.g. a dependency haven't been fulfilled yet.
-type RequeueError struct {
-	err error
+type RequeueNeeded struct {
+	reason string
 }
 
-func (e *RequeueError) Error() string {
-	if e.err == nil {
-		return ""
-	}
-	return e.err.Error()
+func (e *RequeueNeeded) Reason() string {
+	return e.reason
 }
 
-func (e *RequeueError) Unwrap() error {
-	return e.err
+func (e *RequeueNeeded) Error() string {
+	return fmt.Sprintf("requeue needed: %v", e.reason)
 }
 
-var _ error = &RequeueAfterError{}
+var _ error = &RequeueNeededAfter{}
 
 // An error to instruct controller-runtime to requeue the processing item after specified duration without been logged as error.
 // This should be used when a "error condition" occurrence is sort of expected and can be resolved by retry.
 // e.g. a dependency haven't been fulfilled yet, and expected it to be fulfilled after duration.
 // Note: use this with care,a simple wait might suits your use case better.
-type RequeueAfterError struct {
-	err      error
+type RequeueNeededAfter struct {
+	reason   string
 	duration time.Duration
 }
 
-func (e *RequeueAfterError) Error() string {
-	if e.err == nil {
-		return ""
-	}
-	return e.err.Error()
+func (e *RequeueNeededAfter) Reason() string {
+	return e.reason
 }
 
-func (e *RequeueAfterError) Duration() time.Duration {
+func (e *RequeueNeededAfter) Duration() time.Duration {
 	return e.duration
 }
 
-func (e *RequeueAfterError) Unwrap() error {
-	return e.err
+func (e *RequeueNeededAfter) Error() string {
+	return fmt.Sprintf("requeue needed after %v: %v", e.duration, e.reason)
 }
diff --git a/pkg/runtime/reconcile.go b/pkg/runtime/reconcile.go
index d9332e5c0..d37412072 100644
--- a/pkg/runtime/reconcile.go
+++ b/pkg/runtime/reconcile.go
@@ -12,15 +12,15 @@ func HandleReconcileError(err error, log logr.Logger) (ctrl.Result, error) {
 		return ctrl.Result{}, nil
 	}
 
-	var requeueAfterErr *RequeueAfterError
-	if errors.As(err, &requeueAfterErr) {
-		log.V(1).Info("requeue after due to error", "duration", requeueAfterErr.Duration(), "error", requeueAfterErr.Unwrap())
-		return ctrl.Result{RequeueAfter: requeueAfterErr.Duration()}, nil
+	var requeueNeededAfter *RequeueNeededAfter
+	if errors.As(err, &requeueNeededAfter) {
+		log.V(1).Info("requeue after", "duration", requeueNeededAfter.Duration(), "reason", requeueNeededAfter.Reason())
+		return ctrl.Result{RequeueAfter: requeueNeededAfter.Duration()}, nil
 	}
 
-	var requeueError *RequeueError
-	if errors.As(err, &requeueError) {
-		log.V(1).Info("requeue due to error", "error", requeueError.Unwrap())
+	var requeueNeeded *RequeueNeeded
+	if errors.As(err, &requeueNeeded) {
+		log.V(1).Info("requeue", "reason", requeueNeeded.Reason())
 		return ctrl.Result{Requeue: true}, nil
 	}
 
diff --git a/pkg/targetgroupbinding/networking_manager.go b/pkg/targetgroupbinding/networking_manager.go
index 2e0420012..a4e6aca38 100644
--- a/pkg/targetgroupbinding/networking_manager.go
+++ b/pkg/targetgroupbinding/networking_manager.go
@@ -115,18 +115,18 @@ func (m *defaultNetworkingManager) Cleanup(ctx context.Context, tgb *elbv2api.Ta
 }
 
 func (m *defaultNetworkingManager) computeIngressPermissionsPerSGWithPodEndpoints(ctx context.Context, tgbNetworking elbv2api.TargetGroupBindingNetworking, endpoints []backend.PodEndpoint) (map[string][]networking.IPPermissionInfo, error) {
-	pods := make([]*corev1.Pod, 0, len(endpoints))
-	podByPodKey := make(map[types.NamespacedName]*corev1.Pod, len(endpoints))
+	pods := make([]k8s.PodInfo, 0, len(endpoints))
+	podByPodKey := make(map[types.NamespacedName]k8s.PodInfo, len(endpoints))
 	for _, endpoint := range endpoints {
 		pods = append(pods, endpoint.Pod)
-		podByPodKey[k8s.NamespacedName(endpoint.Pod)] = endpoint.Pod
+		podByPodKey[endpoint.Pod.Key] = endpoint.Pod
 	}
 	eniInfoByPodKey, err := m.podENIResolver.Resolve(ctx, pods)
 	if err != nil {
 		return nil, err
 	}
 
-	podsBySG := make(map[string][]*corev1.Pod)
+	podsBySG := make(map[string][]k8s.PodInfo)
 	for podKey, eniInfo := range eniInfoByPodKey {
 		sgID, err := m.resolveEndpointSGForENI(ctx, eniInfo)
 		if err != nil {
@@ -250,7 +250,7 @@ func (m *defaultNetworkingManager) computeAggregatedIngressPermissionsPerSG(_ co
 
 // computeIngressPermissionsForTGBNetworking computes the needed Inbound IPPermissions for specified TargetGroupBinding.
 // an optional list of pods if provided if pod endpoints are used, and named ports will be resolved to the pod port.
-func (m *defaultNetworkingManager) computeIngressPermissionsForTGBNetworking(ctx context.Context, tgbNetworking elbv2api.TargetGroupBindingNetworking, pods []*corev1.Pod) ([]networking.IPPermissionInfo, error) {
+func (m *defaultNetworkingManager) computeIngressPermissionsForTGBNetworking(ctx context.Context, tgbNetworking elbv2api.TargetGroupBindingNetworking, pods []k8s.PodInfo) ([]networking.IPPermissionInfo, error) {
 	var permissions []networking.IPPermissionInfo
 	protocolTCP := elbv2api.NetworkingProtocolTCP
 	for _, rule := range tgbNetworking.Ingress {
@@ -285,7 +285,7 @@ type sdkFromToPortPair struct {
 
 // computePermissionsForPeerPort computes the needed Inbound IPPermissions for specified peer and port.
 // an optional list of pods if provided if pod endpoints are used, and named ports will be resolved to the pod port.
-func (m *defaultNetworkingManager) computePermissionsForPeerPort(ctx context.Context, peer elbv2api.NetworkingPeer, port elbv2api.NetworkingPort, pods []*corev1.Pod) ([]networking.IPPermissionInfo, error) {
+func (m *defaultNetworkingManager) computePermissionsForPeerPort(ctx context.Context, peer elbv2api.NetworkingPeer, port elbv2api.NetworkingPort, pods []k8s.PodInfo) ([]networking.IPPermissionInfo, error) {
 	sdkProtocol := "tcp"
 	if port.Protocol != nil {
 		switch *port.Protocol {
@@ -350,7 +350,7 @@ func (m *defaultNetworkingManager) computePermissionsForPeerPort(ctx context.Con
 
 // computeNumericalPorts computes the numerical ports if a named is used.
 // Note: multiple numerical ports can be returned since same named port might corresponding to different numerical ports on different pods.
-func (m *defaultNetworkingManager) computeNumericalPorts(_ context.Context, port intstr.IntOrString, pods []*corev1.Pod) ([]int64, error) {
+func (m *defaultNetworkingManager) computeNumericalPorts(_ context.Context, port intstr.IntOrString, pods []k8s.PodInfo) ([]int64, error) {
 	if port.Type == intstr.Int {
 		return []int64{int64(port.IntVal)}, nil
 	}
@@ -360,7 +360,7 @@ func (m *defaultNetworkingManager) computeNumericalPorts(_ context.Context, port
 
 	containerPorts := sets.NewInt64()
 	for _, pod := range pods {
-		containerPort, err := k8s.LookupContainerPort(pod, port)
+		containerPort, err := pod.LookupContainerPort(port)
 		if err != nil {
 			return nil, err
 		}
diff --git a/pkg/targetgroupbinding/resource_manager.go b/pkg/targetgroupbinding/resource_manager.go
index dd7c97bfa..9f38b5a9f 100644
--- a/pkg/targetgroupbinding/resource_manager.go
+++ b/pkg/targetgroupbinding/resource_manager.go
@@ -35,11 +35,11 @@ type ResourceManager interface {
 
 // NewDefaultResourceManager constructs new defaultResourceManager.
 func NewDefaultResourceManager(k8sClient client.Client, elbv2Client services.ELBV2,
-	podENIResolver networking.PodENIInfoResolver, nodeENIResolver networking.NodeENIInfoResolver,
+	podInfoRepo k8s.PodInfoRepo, podENIResolver networking.PodENIInfoResolver, nodeENIResolver networking.NodeENIInfoResolver,
 	sgManager networking.SecurityGroupManager, sgReconciler networking.SecurityGroupReconciler,
 	vpcID string, clusterName string, logger logr.Logger) *defaultResourceManager {
 	targetsManager := NewCachedTargetsManager(elbv2Client, logger)
-	endpointResolver := backend.NewDefaultEndpointResolver(k8sClient, logger)
+	endpointResolver := backend.NewDefaultEndpointResolver(k8sClient, podInfoRepo, logger)
 	networkingManager := NewDefaultNetworkingManager(k8sClient, podENIResolver, nodeENIResolver, sgManager, sgReconciler, vpcID, clusterName, logger)
 	return &defaultResourceManager{
 		k8sClient:         k8sClient,
@@ -87,13 +87,16 @@ func (m *defaultResourceManager) Cleanup(ctx context.Context, tgb *elbv2api.Targ
 
 func (m *defaultResourceManager) reconcileWithIPTargetType(ctx context.Context, tgb *elbv2api.TargetGroupBinding) error {
 	svcKey := buildServiceReferenceKey(tgb, tgb.Spec.ServiceRef)
+
+	targetHealthCondType := BuildTargetHealthPodConditionType(tgb)
 	resolveOpts := []backend.EndpointResolveOption{
-		backend.WithUnreadyPodInclusionCriterion(k8s.IsPodContainersReady),
+		backend.WithPodReadinessGate(targetHealthCondType),
 	}
-	endpoints, err := m.endpointResolver.ResolvePodEndpoints(ctx, svcKey, tgb.Spec.ServiceRef.Port, resolveOpts...)
+	endpoints, containsPotentialReadyEndpoints, err := m.endpointResolver.ResolvePodEndpoints(ctx, svcKey, tgb.Spec.ServiceRef.Port, resolveOpts...)
 	if err != nil {
 		return err
 	}
+
 	tgARN := tgb.Spec.TargetGroupARN
 	targets, err := m.targetsManager.ListTargets(ctx, tgARN)
 	if err != nil {
@@ -112,16 +115,22 @@ func (m *defaultResourceManager) reconcileWithIPTargetType(ctx context.Context,
 		return err
 	}
 
-	anyPodNeedFurtherProbe, err := m.updateTargetHealthPodCondition(ctx, tgb, matchedEndpointAndTargets, unmatchedEndpoints)
+	anyPodNeedFurtherProbe, err := m.updateTargetHealthPodCondition(ctx, targetHealthCondType, matchedEndpointAndTargets, unmatchedEndpoints)
 	if err != nil {
 		return err
 	}
+
 	if anyPodNeedFurtherProbe {
 		if containsTargetsInInitialState(matchedEndpointAndTargets) || len(unmatchedEndpoints) != 0 {
-			return runtime.NewRequeueAfterError(nil, m.targetHealthRequeueDuration)
+			return runtime.NewRequeueNeededAfter("monitor targetHealth", m.targetHealthRequeueDuration)
 		}
-		return runtime.NewRequeueError(nil)
+		return runtime.NewRequeueNeeded("monitor targetHealth")
 	}
+
+	if containsPotentialReadyEndpoints {
+		return runtime.NewRequeueNeeded("monitor potential ready endpoints")
+	}
+
 	_ = drainingTargets
 	return nil
 }
@@ -174,10 +183,8 @@ func (m *defaultResourceManager) cleanupTargets(ctx context.Context, tgb *elbv2a
 
 // updateTargetHealthPodCondition will updates pod's targetHealth condition for matchedEndpointAndTargets and unmatchedEndpoints.
 // returns whether further probe is needed or not
-func (m *defaultResourceManager) updateTargetHealthPodCondition(ctx context.Context, tgb *elbv2api.TargetGroupBinding,
+func (m *defaultResourceManager) updateTargetHealthPodCondition(ctx context.Context, targetHealthCondType corev1.PodConditionType,
 	matchedEndpointAndTargets []podEndpointAndTargetPair, unmatchedEndpoints []backend.PodEndpoint) (bool, error) {
-
-	targetHealthCondType := BuildTargetHealthPodConditionType(tgb)
 	anyPodNeedFurtherProbe := false
 
 	for _, endpointAndTarget := range matchedEndpointAndTargets {
@@ -212,12 +219,12 @@ func (m *defaultResourceManager) updateTargetHealthPodCondition(ctx context.Cont
 
 // updateTargetHealthPodConditionForPod updates pod's targetHealth condition for a single pod and its matched target.
 // returns whether further probe is needed or not.
-func (m *defaultResourceManager) updateTargetHealthPodConditionForPod(ctx context.Context, pod *corev1.Pod,
+func (m *defaultResourceManager) updateTargetHealthPodConditionForPod(ctx context.Context, pod k8s.PodInfo,
 	targetHealth *elbv2sdk.TargetHealth, targetHealthCondType corev1.PodConditionType) (bool, error) {
-
-	if !k8s.IsPodHasReadinessGate(pod, targetHealthCondType) {
+	if !pod.HasAnyOfReadinessGates([]corev1.PodConditionType{targetHealthCondType}) {
 		return false, nil
 	}
+
 	targetHealthCondStatus := corev1.ConditionUnknown
 	var reason, message string
 	if targetHealth != nil {
@@ -231,9 +238,10 @@ func (m *defaultResourceManager) updateTargetHealthPodConditionForPod(ctx contex
 		message = awssdk.StringValue(targetHealth.Description)
 	}
 	needFurtherProbe := targetHealthCondStatus != corev1.ConditionTrue
-	existingTargetHealthCond := k8s.GetPodCondition(pod, targetHealthCondType)
+
+	existingTargetHealthCond, exists := pod.GetPodCondition(targetHealthCondType)
 	// we skip patch pod if it matches current computed status/reason/message.
-	if existingTargetHealthCond != nil &&
+	if exists &&
 		existingTargetHealthCond.Status == targetHealthCondStatus &&
 		existingTargetHealthCond.Reason == reason &&
 		existingTargetHealthCond.Message == message {
@@ -246,17 +254,22 @@ func (m *defaultResourceManager) updateTargetHealthPodConditionForPod(ctx contex
 		Reason:  reason,
 		Message: message,
 	}
-	if existingTargetHealthCond == nil || existingTargetHealthCond.Status != targetHealthCondStatus {
+	if !exists || existingTargetHealthCond.Status != targetHealthCondStatus {
 		newTargetHealthCond.LastTransitionTime = metav1.Now()
 	}
 
-	oldPod := pod.DeepCopy()
-	k8s.UpdatePodCondition(pod, newTargetHealthCond)
-	patch, err := buildPodStrategicMergePatch(oldPod, pod)
+	patch, err := buildPodConditionPatch(pod, newTargetHealthCond)
 	if err != nil {
 		return false, err
 	}
-	if err := m.k8sClient.Status().Patch(ctx, pod, patch); err != nil {
+	k8sPod := &corev1.Pod{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: pod.Key.Namespace,
+			Name:      pod.Key.Name,
+			UID:       pod.UID,
+		},
+	}
+	if err := m.k8sClient.Status().Patch(ctx, k8sPod, patch); err != nil {
 		if apierrors.IsNotFound(err) {
 			return false, nil
 		}
@@ -396,13 +409,21 @@ func matchNodePortEndpointWithTargets(endpoints []backend.NodePortEndpoint, targ
 	return matchedEndpointAndTargets, unmatchedEndpoints, unmatchedTargets
 }
 
-func buildPodStrategicMergePatch(oldPod *corev1.Pod, newPod *corev1.Pod) (client.Patch, error) {
-	oldData, err := json.Marshal(oldPod)
+func buildPodConditionPatch(pod k8s.PodInfo, condition corev1.PodCondition) (client.Patch, error) {
+	oldData, err := json.Marshal(corev1.Pod{
+		Status: corev1.PodStatus{
+			Conditions: nil,
+		},
+	})
 	if err != nil {
 		return nil, err
 	}
-
-	newData, err := json.Marshal(newPod)
+	newData, err := json.Marshal(corev1.Pod{
+		ObjectMeta: metav1.ObjectMeta{UID: pod.UID}, // only put the uid in the new object to ensure it appears in the patch as a precondition
+		Status: corev1.PodStatus{
+			Conditions: []corev1.PodCondition{condition},
+		},
+	})
 	if err != nil {
 		return nil, err
 	}
diff --git a/scripts/gen_mocks.sh b/scripts/gen_mocks.sh
index 57126b958..f81debec7 100644
--- a/scripts/gen_mocks.sh
+++ b/scripts/gen_mocks.sh
@@ -4,6 +4,7 @@ mockgen -destination=./mocks/aws/services/mock_ec2.go sigs.k8s.io/aws-load-balan
 mockgen -destination=./mocks/webhook/mock_mutator.go sigs.k8s.io/aws-load-balancer-controller/pkg/webhook Mutator
 mockgen -destination=./mocks/webhook/mock_validator.go sigs.k8s.io/aws-load-balancer-controller/pkg/webhook Validator
 mockgen -destination=./mocks/k8s/mock_finalizer.go sigs.k8s.io/aws-load-balancer-controller/pkg/k8s FinalizerManager
+mockgen -destination=./mocks/k8s/mock_repo_info_repo.go sigs.k8s.io/aws-load-balancer-controller/pkg/k8s PodInfoRepo
 mockgen -destination=./mocks/networking/mock_security_group_manager.go sigs.k8s.io/aws-load-balancer-controller/pkg/networking SecurityGroupManager
 mockgen -destination=./mocks/networking/mock_subnet_resolver.go sigs.k8s.io/aws-load-balancer-controller/pkg/networking SubnetsResolver
 mockgen -destination=./mocks/ingress/mock_cert_discovery.go sigs.k8s.io/aws-load-balancer-controller/pkg/ingress CertDiscovery


Test patch:
diff --git a/pkg/backend/endpoint_resolver_test.go b/pkg/backend/endpoint_resolver_test.go
index 46e8d8acc..05b5bb174 100644
--- a/pkg/backend/endpoint_resolver_test.go
+++ b/pkg/backend/endpoint_resolver_test.go
@@ -3,6 +3,7 @@ package backend
 import (
 	"context"
 	"errors"
+	"github.com/golang/mock/gomock"
 	"github.com/google/go-cmp/cmp"
 	"github.com/google/go-cmp/cmp/cmpopts"
 	"github.com/stretchr/testify/assert"
@@ -13,106 +14,103 @@ import (
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/util/intstr"
 	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
+	mock_k8s "sigs.k8s.io/aws-load-balancer-controller/mocks/k8s"
 	"sigs.k8s.io/aws-load-balancer-controller/pkg/equality"
 	"sigs.k8s.io/aws-load-balancer-controller/pkg/k8s"
 	ctrl "sigs.k8s.io/controller-runtime"
 	testclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
+	"sigs.k8s.io/controller-runtime/pkg/log"
 	"testing"
 )
 
 func Test_defaultEndpointResolver_ResolvePodEndpoints(t *testing.T) {
 	testNS := "test-ns"
-	pod1 := &corev1.Pod{
-		ObjectMeta: metav1.ObjectMeta{
-			Namespace: testNS,
-			Name:      "pod-1",
-		},
-		Spec: corev1.PodSpec{
-			ReadinessGates: nil,
-		},
-		Status: corev1.PodStatus{
-			PodIP: "192.168.1.1",
-			Conditions: []corev1.PodCondition{
-				{
-					Type:   corev1.PodReady,
-					Status: corev1.ConditionTrue,
-				},
-				{
-					Type:   corev1.ContainersReady,
-					Status: corev1.ConditionTrue,
-				},
+	pod1 := k8s.PodInfo{
+		Key:            types.NamespacedName{Namespace: testNS, Name: "pod-1"},
+		UID:            "pod-uuid-1",
+		ReadinessGates: nil,
+		Conditions: []corev1.PodCondition{
+			{
+				Type:   corev1.PodReady,
+				Status: corev1.ConditionTrue,
+			},
+			{
+				Type:   corev1.ContainersReady,
+				Status: corev1.ConditionTrue,
 			},
 		},
+		PodIP: "192.168.1.1",
 	}
-	pod2 := &corev1.Pod{
-		ObjectMeta: metav1.ObjectMeta{
-			Namespace: testNS,
-			Name:      "pod-2",
-		},
-		Spec: corev1.PodSpec{
-			ReadinessGates: nil,
-		},
-		Status: corev1.PodStatus{
-			PodIP: "192.168.1.2",
-			Conditions: []corev1.PodCondition{
-				{
-					Type:   corev1.PodReady,
-					Status: corev1.ConditionTrue,
-				},
-				{
-					Type:   corev1.ContainersReady,
-					Status: corev1.ConditionTrue,
-				},
+	pod2 := k8s.PodInfo{
+		Key:            types.NamespacedName{Namespace: testNS, Name: "pod-2"},
+		UID:            "pod-uuid-2",
+		ReadinessGates: nil,
+		Conditions: []corev1.PodCondition{
+			{
+				Type:   corev1.PodReady,
+				Status: corev1.ConditionTrue,
+			},
+			{
+				Type:   corev1.ContainersReady,
+				Status: corev1.ConditionTrue,
 			},
 		},
+		PodIP: "192.168.1.2",
 	}
-	pod3 := &corev1.Pod{
-		ObjectMeta: metav1.ObjectMeta{
-			Namespace: testNS,
-			Name:      "pod-3",
+	pod3 := k8s.PodInfo{
+		Key: types.NamespacedName{Namespace: testNS, Name: "pod-3"},
+		UID: "pod-uuid-3",
+		Conditions: []corev1.PodCondition{
+			{
+				Type:   corev1.PodReady,
+				Status: corev1.ConditionFalse,
+			},
+			{
+				Type:   corev1.ContainersReady,
+				Status: corev1.ConditionTrue,
+			},
 		},
-		Spec: corev1.PodSpec{
-			ReadinessGates: nil,
+		PodIP: "192.168.1.3",
+	}
+	pod4 := k8s.PodInfo{
+		Key: types.NamespacedName{Namespace: testNS, Name: "pod-4"},
+		UID: "pod-uuid-4",
+		ReadinessGates: []corev1.PodReadinessGate{
+			{
+				ConditionType: "custom-condition",
+			},
 		},
-		Status: corev1.PodStatus{
-			PodIP: "192.168.1.3",
-			Conditions: []corev1.PodCondition{
-				{
-					Type:   corev1.PodReady,
-					Status: corev1.ConditionFalse,
-				},
-				{
-					Type:   corev1.ContainersReady,
-					Status: corev1.ConditionTrue,
-				},
+		Conditions: []corev1.PodCondition{
+			{
+				Type:   corev1.PodReady,
+				Status: corev1.ConditionFalse,
+			},
+			{
+				Type:   corev1.ContainersReady,
+				Status: corev1.ConditionTrue,
 			},
 		},
+		PodIP: "192.168.1.4",
 	}
-	pod4 := &corev1.Pod{
-		ObjectMeta: metav1.ObjectMeta{
-			Namespace: testNS,
-			Name:      "pod-4",
-		},
-		Spec: corev1.PodSpec{
-			ReadinessGates: []corev1.PodReadinessGate{
-				{
-					ConditionType: "custom-condition",
-				},
+	pod5 := k8s.PodInfo{
+		Key: types.NamespacedName{Namespace: testNS, Name: "pod-5"},
+		UID: "pod-uuid-5",
+		ReadinessGates: []corev1.PodReadinessGate{
+			{
+				ConditionType: "custom-condition",
 			},
 		},
-		Status: corev1.PodStatus{
-			PodIP: "192.168.1.4",
-			Conditions: []corev1.PodCondition{
-				{
-					Type:   corev1.PodReady,
-					Status: corev1.ConditionFalse,
-				},
-				{
-					Type:   corev1.ContainersReady,
-					Status: corev1.ConditionTrue,
-				},
+		Conditions: []corev1.PodCondition{
+			{
+				Type:   corev1.PodReady,
+				Status: corev1.ConditionFalse,
+			},
+			{
+				Type:   corev1.ContainersReady,
+				Status: corev1.ConditionFalse,
 			},
 		},
+		PodIP: "192.168.1.5",
 	}
 	svc1 := &corev1.Service{
 		ObjectMeta: metav1.ObjectMeta{
@@ -148,37 +146,37 @@ func Test_defaultEndpointResolver_ResolvePodEndpoints(t *testing.T) {
 				},
 				Addresses: []corev1.EndpointAddress{
 					{
-						IP: pod1.Status.PodIP,
+						IP: pod1.PodIP,
 						TargetRef: &corev1.ObjectReference{
 							Kind:      "Pod",
-							Namespace: pod1.Namespace,
-							Name:      pod1.Name,
+							Namespace: pod1.Key.Namespace,
+							Name:      pod1.Key.Name,
 						},
 					},
 					{
-						IP: pod2.Status.PodIP,
+						IP: pod2.PodIP,
 						TargetRef: &corev1.ObjectReference{
 							Kind:      "Pod",
-							Namespace: pod2.Namespace,
-							Name:      pod2.Name,
+							Namespace: pod2.Key.Namespace,
+							Name:      pod2.Key.Name,
 						},
 					},
 				},
 				NotReadyAddresses: []corev1.EndpointAddress{
 					{
-						IP: pod3.Status.PodIP,
+						IP: pod3.PodIP,
 						TargetRef: &corev1.ObjectReference{
 							Kind:      "Pod",
-							Namespace: pod3.Namespace,
-							Name:      pod3.Name,
+							Namespace: pod3.Key.Namespace,
+							Name:      pod3.Key.Name,
 						},
 					},
 					{
-						IP: pod4.Status.PodIP,
+						IP: pod4.PodIP,
 						TargetRef: &corev1.ObjectReference{
 							Kind:      "Pod",
-							Namespace: pod4.Namespace,
-							Name:      pod4.Name,
+							Namespace: pod4.Key.Namespace,
+							Name:      pod4.Key.Name,
 						},
 					},
 				},
@@ -200,21 +198,21 @@ func Test_defaultEndpointResolver_ResolvePodEndpoints(t *testing.T) {
 				},
 				Addresses: []corev1.EndpointAddress{
 					{
-						IP: pod1.Status.PodIP,
+						IP: pod1.PodIP,
 						TargetRef: &corev1.ObjectReference{
 							Kind:      "Pod",
-							Namespace: pod1.Namespace,
-							Name:      pod1.Name,
+							Namespace: pod1.Key.Namespace,
+							Name:      pod1.Key.Name,
 						},
 					},
 				},
 				NotReadyAddresses: []corev1.EndpointAddress{
 					{
-						IP: pod3.Status.PodIP,
+						IP: pod3.PodIP,
 						TargetRef: &corev1.ObjectReference{
 							Kind:      "Pod",
-							Namespace: pod3.Namespace,
-							Name:      pod3.Name,
+							Namespace: pod3.Key.Namespace,
+							Name:      pod3.Key.Name,
 						},
 					},
 				},
@@ -228,21 +226,21 @@ func Test_defaultEndpointResolver_ResolvePodEndpoints(t *testing.T) {
 				},
 				Addresses: []corev1.EndpointAddress{
 					{
-						IP: pod2.Status.PodIP,
+						IP: pod2.PodIP,
 						TargetRef: &corev1.ObjectReference{
 							Kind:      "Pod",
-							Namespace: pod2.Namespace,
-							Name:      pod2.Name,
+							Namespace: pod2.Key.Namespace,
+							Name:      pod2.Key.Name,
 						},
 					},
 				},
 				NotReadyAddresses: []corev1.EndpointAddress{
 					{
-						IP: pod4.Status.PodIP,
+						IP: pod4.PodIP,
 						TargetRef: &corev1.ObjectReference{
 							Kind:      "Pod",
-							Namespace: pod4.Namespace,
-							Name:      pod4.Name,
+							Namespace: pod4.Key.Namespace,
+							Name:      pod4.Key.Name,
 						},
 					},
 				},
@@ -268,37 +266,97 @@ func Test_defaultEndpointResolver_ResolvePodEndpoints(t *testing.T) {
 				},
 				Addresses: []corev1.EndpointAddress{
 					{
-						IP: pod1.Status.PodIP,
+						IP: pod1.PodIP,
 						TargetRef: &corev1.ObjectReference{
 							Kind:      "Pod",
-							Namespace: pod1.Namespace,
-							Name:      pod1.Name,
+							Namespace: pod1.Key.Namespace,
+							Name:      pod1.Key.Name,
 						},
 					},
 					{
-						IP: pod2.Status.PodIP,
+						IP: pod2.PodIP,
 						TargetRef: &corev1.ObjectReference{
 							Kind:      "Pod",
-							Namespace: pod2.Namespace,
-							Name:      pod2.Name,
+							Namespace: pod2.Key.Namespace,
+							Name:      pod2.Key.Name,
 						},
 					},
 				},
 				NotReadyAddresses: []corev1.EndpointAddress{
 					{
-						IP: pod3.Status.PodIP,
+						IP: pod3.PodIP,
 						TargetRef: &corev1.ObjectReference{
 							Kind:      "Pod",
-							Namespace: pod3.Namespace,
-							Name:      pod3.Name,
+							Namespace: pod3.Key.Namespace,
+							Name:      pod3.Key.Name,
 						},
 					},
 					{
-						IP: pod4.Status.PodIP,
+						IP: pod4.PodIP,
 						TargetRef: &corev1.ObjectReference{
 							Kind:      "Pod",
-							Namespace: pod4.Namespace,
-							Name:      pod4.Name,
+							Namespace: pod4.Key.Namespace,
+							Name:      pod4.Key.Name,
+						},
+					},
+				},
+			},
+		},
+	}
+	ep1D := &corev1.Endpoints{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: testNS,
+			Name:      "svc-1",
+		},
+		Subsets: []corev1.EndpointSubset{
+			{
+				Ports: []corev1.EndpointPort{
+					{
+						Name: "http",
+						Port: 8080,
+					},
+				},
+				Addresses: []corev1.EndpointAddress{
+					{
+						IP: pod1.PodIP,
+						TargetRef: &corev1.ObjectReference{
+							Kind:      "Pod",
+							Namespace: pod1.Key.Namespace,
+							Name:      pod1.Key.Name,
+						},
+					},
+					{
+						IP: pod2.PodIP,
+						TargetRef: &corev1.ObjectReference{
+							Kind:      "Pod",
+							Namespace: pod2.Key.Namespace,
+							Name:      pod2.Key.Name,
+						},
+					},
+				},
+				NotReadyAddresses: []corev1.EndpointAddress{
+					{
+						IP: pod3.PodIP,
+						TargetRef: &corev1.ObjectReference{
+							Kind:      "Pod",
+							Namespace: pod3.Key.Namespace,
+							Name:      pod3.Key.Name,
+						},
+					},
+					{
+						IP: pod4.PodIP,
+						TargetRef: &corev1.ObjectReference{
+							Kind:      "Pod",
+							Namespace: pod4.Key.Namespace,
+							Name:      pod4.Key.Name,
+						},
+					},
+					{
+						IP: pod5.PodIP,
+						TargetRef: &corev1.ObjectReference{
+							Kind:      "Pod",
+							Namespace: pod5.Key.Namespace,
+							Name:      pod5.Key.Name,
 						},
 					},
 				},
@@ -306,30 +364,53 @@ func Test_defaultEndpointResolver_ResolvePodEndpoints(t *testing.T) {
 		},
 	}
 
+	type podInfoRepoGetCall struct {
+		key    types.NamespacedName
+		pod    k8s.PodInfo
+		exists bool
+		err    error
+	}
 	type env struct {
-		pods          []*corev1.Pod
 		services      []*corev1.Service
 		endpointsList []*corev1.Endpoints
 	}
+	type fields struct {
+		podInfoRepoGetCalls []podInfoRepoGetCall
+	}
 	type args struct {
 		svcKey types.NamespacedName
 		port   intstr.IntOrString
 		opts   []EndpointResolveOption
 	}
 	tests := []struct {
-		name    string
-		env     env
-		args    args
-		want    []PodEndpoint
-		wantErr error
+		name                                string
+		env                                 env
+		fields                              fields
+		args                                args
+		want                                []PodEndpoint
+		wantContainsPotentialReadyEndpoints bool
+		wantErr                             error
 	}{
 		{
 			name: "only ready pods will be included by default",
 			env: env{
-				pods:          []*corev1.Pod{pod1, pod2, pod3, pod4},
 				services:      []*corev1.Service{svc1},
 				endpointsList: []*corev1.Endpoints{ep1A},
 			},
+			fields: fields{
+				podInfoRepoGetCalls: []podInfoRepoGetCall{
+					{
+						key:    pod1.Key,
+						pod:    pod1,
+						exists: true,
+					},
+					{
+						key:    pod2.Key,
+						pod:    pod2,
+						exists: true,
+					},
+				},
+			},
 			args: args{
 				svcKey: k8s.NamespacedName(svc1),
 				port:   intstr.FromString("http"),
@@ -347,18 +428,42 @@ func Test_defaultEndpointResolver_ResolvePodEndpoints(t *testing.T) {
 					Pod:  pod2,
 				},
 			},
+			wantContainsPotentialReadyEndpoints: false,
 		},
 		{
-			name: "unready but containerReady pod will also be included",
+			name: "unready only be included if it have readinessGate and containerReady",
 			env: env{
-				pods:          []*corev1.Pod{pod1, pod2, pod3, pod4},
 				services:      []*corev1.Service{svc1},
 				endpointsList: []*corev1.Endpoints{ep1A},
 			},
+			fields: fields{
+				podInfoRepoGetCalls: []podInfoRepoGetCall{
+					{
+						key:    pod1.Key,
+						pod:    pod1,
+						exists: true,
+					},
+					{
+						key:    pod2.Key,
+						pod:    pod2,
+						exists: true,
+					},
+					{
+						key:    pod3.Key,
+						pod:    pod3,
+						exists: true,
+					},
+					{
+						key:    pod4.Key,
+						pod:    pod4,
+						exists: true,
+					},
+				},
+			},
 			args: args{
 				svcKey: k8s.NamespacedName(svc1),
 				port:   intstr.FromString("http"),
-				opts:   []EndpointResolveOption{WithUnreadyPodInclusionCriterion(k8s.IsPodContainersReady)},
+				opts:   []EndpointResolveOption{WithPodReadinessGate("custom-condition")},
 			},
 			want: []PodEndpoint{
 				{
@@ -371,34 +476,48 @@ func Test_defaultEndpointResolver_ResolvePodEndpoints(t *testing.T) {
 					Port: 8080,
 					Pod:  pod2,
 				},
-				{
-					IP:   "192.168.1.3",
-					Port: 8080,
-					Pod:  pod3,
-				},
 				{
 					IP:   "192.168.1.4",
 					Port: 8080,
 					Pod:  pod4,
 				},
 			},
+			wantContainsPotentialReadyEndpoints: false,
 		},
 		{
-			name: "unready but containerReady pod will also be included when contains specific readinessGate",
+			name: "endpoints with multiple subsets should work as expected",
 			env: env{
-				pods:          []*corev1.Pod{pod1, pod2, pod3, pod4},
 				services:      []*corev1.Service{svc1},
-				endpointsList: []*corev1.Endpoints{ep1A},
+				endpointsList: []*corev1.Endpoints{ep1B},
+			},
+			fields: fields{
+				podInfoRepoGetCalls: []podInfoRepoGetCall{
+					{
+						key:    pod1.Key,
+						pod:    pod1,
+						exists: true,
+					},
+					{
+						key:    pod2.Key,
+						pod:    pod2,
+						exists: true,
+					},
+					{
+						key:    pod3.Key,
+						pod:    pod3,
+						exists: true,
+					},
+					{
+						key:    pod4.Key,
+						pod:    pod4,
+						exists: true,
+					},
+				},
 			},
 			args: args{
 				svcKey: k8s.NamespacedName(svc1),
 				port:   intstr.FromString("http"),
-				opts: []EndpointResolveOption{
-					WithUnreadyPodInclusionCriterion(k8s.IsPodContainersReady),
-					WithUnreadyPodInclusionCriterion(func(pod *corev1.Pod) bool {
-						return k8s.IsPodHasReadinessGate(pod, "custom-condition")
-					}),
-				},
+				opts:   []EndpointResolveOption{WithPodReadinessGate("custom-condition")},
 			},
 			want: []PodEndpoint{
 				{
@@ -417,18 +536,42 @@ func Test_defaultEndpointResolver_ResolvePodEndpoints(t *testing.T) {
 					Pod:  pod4,
 				},
 			},
+			wantContainsPotentialReadyEndpoints: false,
 		},
 		{
-			name: "endpoints with multiple subsets should work as expected",
+			name: "endpoints with multiple ports should work as expected",
 			env: env{
-				pods:          []*corev1.Pod{pod1, pod2, pod3, pod4},
 				services:      []*corev1.Service{svc1},
-				endpointsList: []*corev1.Endpoints{ep1B},
+				endpointsList: []*corev1.Endpoints{ep1C},
+			},
+			fields: fields{
+				podInfoRepoGetCalls: []podInfoRepoGetCall{
+					{
+						key:    pod1.Key,
+						pod:    pod1,
+						exists: true,
+					},
+					{
+						key:    pod2.Key,
+						pod:    pod2,
+						exists: true,
+					},
+					{
+						key:    pod3.Key,
+						pod:    pod3,
+						exists: true,
+					},
+					{
+						key:    pod4.Key,
+						pod:    pod4,
+						exists: true,
+					},
+				},
 			},
 			args: args{
 				svcKey: k8s.NamespacedName(svc1),
 				port:   intstr.FromString("http"),
-				opts:   []EndpointResolveOption{WithUnreadyPodInclusionCriterion(k8s.IsPodContainersReady)},
+				opts:   []EndpointResolveOption{WithPodReadinessGate("custom-condition")},
 			},
 			want: []PodEndpoint{
 				{
@@ -441,29 +584,47 @@ func Test_defaultEndpointResolver_ResolvePodEndpoints(t *testing.T) {
 					Port: 8080,
 					Pod:  pod2,
 				},
-				{
-					IP:   "192.168.1.3",
-					Port: 8080,
-					Pod:  pod3,
-				},
 				{
 					IP:   "192.168.1.4",
 					Port: 8080,
 					Pod:  pod4,
 				},
 			},
+			wantContainsPotentialReadyEndpoints: false,
 		},
 		{
-			name: "endpoints with multiple ports should work as expected",
+			name: "unready but not found pod will be ignored, but signal potentialReadyEndpoints",
 			env: env{
-				pods:          []*corev1.Pod{pod1, pod2, pod3, pod4},
 				services:      []*corev1.Service{svc1},
-				endpointsList: []*corev1.Endpoints{ep1C},
+				endpointsList: []*corev1.Endpoints{ep1A},
+			},
+			fields: fields{
+				podInfoRepoGetCalls: []podInfoRepoGetCall{
+					{
+						key:    pod1.Key,
+						pod:    pod1,
+						exists: true,
+					},
+					{
+						key:    pod2.Key,
+						pod:    pod2,
+						exists: true,
+					},
+					{
+						key:    pod3.Key,
+						exists: false,
+					},
+					{
+						key:    pod4.Key,
+						pod:    pod4,
+						exists: true,
+					},
+				},
 			},
 			args: args{
 				svcKey: k8s.NamespacedName(svc1),
 				port:   intstr.FromString("http"),
-				opts:   []EndpointResolveOption{WithUnreadyPodInclusionCriterion(k8s.IsPodContainersReady)},
+				opts:   []EndpointResolveOption{WithPodReadinessGate("custom-condition")},
 			},
 			want: []PodEndpoint{
 				{
@@ -477,9 +638,63 @@ func Test_defaultEndpointResolver_ResolvePodEndpoints(t *testing.T) {
 					Pod:  pod2,
 				},
 				{
-					IP:   "192.168.1.3",
+					IP:   "192.168.1.4",
 					Port: 8080,
-					Pod:  pod3,
+					Pod:  pod4,
+				},
+			},
+			wantContainsPotentialReadyEndpoints: true,
+		},
+		{
+			name: "unready only be included if it have readinessGate and containerReady - not containerReady will signal containsPotentialReadyEndpoints",
+			env: env{
+				services:      []*corev1.Service{svc1},
+				endpointsList: []*corev1.Endpoints{ep1D},
+			},
+			fields: fields{
+				podInfoRepoGetCalls: []podInfoRepoGetCall{
+					{
+						key:    pod1.Key,
+						pod:    pod1,
+						exists: true,
+					},
+					{
+						key:    pod2.Key,
+						pod:    pod2,
+						exists: true,
+					},
+					{
+						key:    pod3.Key,
+						pod:    pod3,
+						exists: true,
+					},
+					{
+						key:    pod4.Key,
+						pod:    pod4,
+						exists: true,
+					},
+					{
+						key:    pod5.Key,
+						pod:    pod5,
+						exists: true,
+					},
+				},
+			},
+			args: args{
+				svcKey: k8s.NamespacedName(svc1),
+				port:   intstr.FromString("http"),
+				opts:   []EndpointResolveOption{WithPodReadinessGate("custom-condition")},
+			},
+			want: []PodEndpoint{
+				{
+					IP:   "192.168.1.1",
+					Port: 8080,
+					Pod:  pod1,
+				},
+				{
+					IP:   "192.168.1.2",
+					Port: 8080,
+					Pod:  pod2,
 				},
 				{
 					IP:   "192.168.1.4",
@@ -487,18 +702,24 @@ func Test_defaultEndpointResolver_ResolvePodEndpoints(t *testing.T) {
 					Pod:  pod4,
 				},
 			},
+			wantContainsPotentialReadyEndpoints: true,
 		},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			ctx := context.Background()
+			ctrl := gomock.NewController(t)
+			defer ctrl.Finish()
+			podInfoRepo := mock_k8s.NewMockPodInfoRepo(ctrl)
+			for _, call := range tt.fields.podInfoRepoGetCalls {
+				podInfoRepo.EXPECT().Get(gomock.Any(), call.key).Return(call.pod, call.exists, call.err)
+			}
+
 			k8sSchema := runtime.NewScheme()
 			clientgoscheme.AddToScheme(k8sSchema)
 			k8sClient := testclient.NewFakeClientWithScheme(k8sSchema)
-			for _, pod := range tt.env.pods {
-				assert.NoError(t, k8sClient.Create(ctx, pod.DeepCopy()))
-			}
+
+			ctx := context.Background()
 			for _, svc := range tt.env.services {
 				assert.NoError(t, k8sClient.Create(ctx, svc.DeepCopy()))
 			}
@@ -507,10 +728,11 @@ func Test_defaultEndpointResolver_ResolvePodEndpoints(t *testing.T) {
 			}
 
 			r := &defaultEndpointResolver{
-				k8sClient: k8sClient,
-				logger:    ctrl.Log,
+				k8sClient:   k8sClient,
+				podInfoRepo: podInfoRepo,
+				logger:      &log.NullLogger{},
 			}
-			got, err := r.ResolvePodEndpoints(ctx, tt.args.svcKey, tt.args.port, tt.args.opts...)
+			got, gotContainsPotentialReadyEndpoints, err := r.ResolvePodEndpoints(ctx, tt.args.svcKey, tt.args.port, tt.args.opts...)
 			if tt.wantErr != nil {
 				assert.EqualError(t, err, tt.wantErr.Error())
 			} else {
@@ -523,6 +745,7 @@ func Test_defaultEndpointResolver_ResolvePodEndpoints(t *testing.T) {
 				}
 				assert.True(t, cmp.Equal(tt.want, got, opt),
 					"diff: %v", cmp.Diff(tt.want, got, opt))
+				assert.Equal(t, tt.wantContainsPotentialReadyEndpoints, gotContainsPotentialReadyEndpoints)
 			}
 		})
 	}
@@ -648,7 +871,6 @@ func Test_defaultEndpointResolver_ResolveNodePortEndpoints(t *testing.T) {
 			},
 		},
 	}
-	_ = svc2
 
 	type env struct {
 		nodes    []*corev1.Node
@@ -771,86 +993,3 @@ func Test_defaultEndpointResolver_ResolveNodePortEndpoints(t *testing.T) {
 		})
 	}
 }
-
-func Test_isPodMeetCriteria(t *testing.T) {
-	type args struct {
-		pod      *corev1.Pod
-		criteria []PodPredicate
-	}
-	tests := []struct {
-		name string
-		args args
-		want bool
-	}{
-		{
-			name: "pod is containerReady and with no readinessGate - meet criteria",
-			args: args{
-				pod: &corev1.Pod{
-					Spec: corev1.PodSpec{},
-					Status: corev1.PodStatus{
-						Conditions: []corev1.PodCondition{
-							{
-								Type:   corev1.ContainersReady,
-								Status: corev1.ConditionTrue,
-							},
-						},
-					},
-				},
-				criteria: []PodPredicate{k8s.IsPodContainersReady},
-			},
-			want: true,
-		},
-		{
-			name: "pod is containerReady and with no readinessGate - doesn't meet criteria",
-			args: args{
-				pod: &corev1.Pod{
-					Spec: corev1.PodSpec{},
-					Status: corev1.PodStatus{
-						Conditions: []corev1.PodCondition{
-							{
-								Type:   corev1.ContainersReady,
-								Status: corev1.ConditionTrue,
-							},
-						},
-					},
-				},
-				criteria: []PodPredicate{k8s.IsPodContainersReady, func(pod *corev1.Pod) bool {
-					return k8s.IsPodHasReadinessGate(pod, "alb.ingress.k8s.aws/condition")
-				}},
-			},
-			want: false,
-		},
-		{
-			name: "pod is containerReady and with readinessGate - meet criteria",
-			args: args{
-				pod: &corev1.Pod{
-					Spec: corev1.PodSpec{
-						ReadinessGates: []corev1.PodReadinessGate{
-							{
-								ConditionType: "alb.ingress.k8s.aws/condition",
-							},
-						},
-					},
-					Status: corev1.PodStatus{
-						Conditions: []corev1.PodCondition{
-							{
-								Type:   corev1.ContainersReady,
-								Status: corev1.ConditionTrue,
-							},
-						},
-					},
-				},
-				criteria: []PodPredicate{k8s.IsPodContainersReady, func(pod *corev1.Pod) bool {
-					return k8s.IsPodHasReadinessGate(pod, "alb.ingress.k8s.aws/condition")
-				}},
-			},
-			want: true,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			got := isPodMeetCriteria(tt.args.pod, tt.args.criteria)
-			assert.Equal(t, tt.want, got)
-		})
-	}
-}
diff --git a/pkg/backend/endpoint_types_test.go b/pkg/backend/endpoint_types_test.go
index 4928a9c04..4e2466a7a 100644
--- a/pkg/backend/endpoint_types_test.go
+++ b/pkg/backend/endpoint_types_test.go
@@ -7,7 +7,7 @@ import (
 	"testing"
 )
 
-func TestWithNodeSelector(t *testing.T) {
+func TestEndpointResolveOptions_ApplyOptions(t *testing.T) {
 	tests := []struct {
 		name string
 		opts []EndpointResolveOption
@@ -20,58 +20,37 @@ func TestWithNodeSelector(t *testing.T) {
 				NodeSelector: labels.Everything(),
 			},
 		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			opts := EndpointResolveOptions{
-				NodeSelector: labels.Nothing(),
-			}
-			opts.ApplyOptions(tt.opts)
-			assert.Equal(t, tt.want.NodeSelector, opts.NodeSelector)
-		})
-	}
-}
-
-func TestWithUnreadyPodInclusionCriterion(t *testing.T) {
-	podPredicateTrue := func(pod *corev1.Pod) bool { return true }
-	podPredicateFalse := func(pod *corev1.Pod) bool { return false }
-	tests := []struct {
-		name string
-		opts []EndpointResolveOption
-		want EndpointResolveOptions
-	}{
 		{
-			name: "zero pod predict",
-			opts: []EndpointResolveOption{},
-			want: EndpointResolveOptions{
-				NodeSelector:                labels.Nothing(),
-				UnreadyPodInclusionCriteria: nil,
+			name: "set two readinessGate",
+			opts: []EndpointResolveOption{
+				WithPodReadinessGate("target-health.ingress.k8s.aws/some-tgb-1"),
+				WithPodReadinessGate("target-health.ingress.k8s.aws/some-tgb-2"),
 			},
-		},
-		{
-			name: "one pod predict",
-			opts: []EndpointResolveOption{WithUnreadyPodInclusionCriterion(podPredicateTrue)},
 			want: EndpointResolveOptions{
-				NodeSelector:                labels.Nothing(),
-				UnreadyPodInclusionCriteria: []PodPredicate{podPredicateTrue},
+				NodeSelector: labels.Nothing(),
+				PodReadinessGates: []corev1.PodConditionType{
+					"target-health.ingress.k8s.aws/some-tgb-1",
+					"target-health.ingress.k8s.aws/some-tgb-2",
+				},
 			},
 		},
 		{
-			name: "two pod predict",
-			opts: []EndpointResolveOption{WithUnreadyPodInclusionCriterion(podPredicateTrue), WithUnreadyPodInclusionCriterion(podPredicateFalse)},
+			name: "set labelSelector & readinessGate",
+			opts: []EndpointResolveOption{
+				WithNodeSelector(labels.Everything()),
+				WithPodReadinessGate("target-health.ingress.k8s.aws/some-tgb"),
+			},
 			want: EndpointResolveOptions{
-				NodeSelector:                labels.Nothing(),
-				UnreadyPodInclusionCriteria: []PodPredicate{podPredicateTrue, podPredicateFalse},
+				NodeSelector:      labels.Everything(),
+				PodReadinessGates: []corev1.PodConditionType{"target-health.ingress.k8s.aws/some-tgb"},
 			},
 		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			opts := EndpointResolveOptions{
-				NodeSelector: labels.Nothing(),
-			}
+			opts := defaultEndpointResolveOptions()
 			opts.ApplyOptions(tt.opts)
-			assert.Equal(t, len(tt.want.UnreadyPodInclusionCriteria), len(opts.UnreadyPodInclusionCriteria))
+			assert.Equal(t, tt.want.NodeSelector, opts.NodeSelector)
 		})
 	}
 }
diff --git a/pkg/k8s/pod_info_repo_test.go b/pkg/k8s/pod_info_repo_test.go
new file mode 100644
index 000000000..a0e62884b
--- /dev/null
+++ b/pkg/k8s/pod_info_repo_test.go
@@ -0,0 +1,105 @@
+package k8s
+
+import (
+	"errors"
+	"github.com/stretchr/testify/assert"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"testing"
+)
+
+func Test_podInfoKeyFunc(t *testing.T) {
+	type args struct {
+		obj interface{}
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    string
+		wantErr error
+	}{
+		{
+			name: "normal case",
+			args: args{
+				obj: &PodInfo{
+					Key: types.NamespacedName{Namespace: "ns-1", Name: "pod-a"},
+				},
+			},
+			want: "ns-1/pod-a",
+		},
+		{
+			name: "invalid type",
+			args: args{
+				obj: PodInfo{
+					Key: types.NamespacedName{Namespace: "ns-1", Name: "pod-a"},
+				},
+			},
+			wantErr: errors.New("expect PodInfo object"),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := podInfoKeyFunc(tt.args.obj)
+			if tt.wantErr != nil {
+				assert.EqualError(t, err, tt.wantErr.Error())
+			} else {
+				assert.NoError(t, err)
+				assert.Equal(t, tt.want, got)
+			}
+		})
+	}
+}
+
+func Test_podInfoConversionFunc(t *testing.T) {
+	type args struct {
+		obj interface{}
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    interface{}
+		wantErr error
+	}{
+		{
+			name: "normal case",
+			args: args{
+				obj: &corev1.Pod{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "ns-1",
+						Name:      "pod-a",
+						UID:       "pod-uuid",
+					},
+				},
+			},
+			want: &PodInfo{
+				Key: types.NamespacedName{Namespace: "ns-1", Name: "pod-a"},
+				UID: "pod-uuid",
+			},
+		},
+		{
+			name: "invalid type",
+			args: args{
+				obj: corev1.Pod{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "ns-1",
+						Name:      "pod-a",
+						UID:       "pod-uuid",
+					},
+				},
+			},
+			wantErr: errors.New("expect pod object"),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := podInfoConversionFunc(tt.args.obj)
+			if tt.wantErr != nil {
+				assert.EqualError(t, err, tt.wantErr.Error())
+			} else {
+				assert.NoError(t, err)
+				assert.Equal(t, tt.want, got)
+			}
+		})
+	}
+}
diff --git a/pkg/k8s/pod_info_test.go b/pkg/k8s/pod_info_test.go
new file mode 100644
index 000000000..470685e25
--- /dev/null
+++ b/pkg/k8s/pod_info_test.go
@@ -0,0 +1,590 @@
+package k8s
+
+import (
+	"errors"
+	"github.com/stretchr/testify/assert"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/intstr"
+	"testing"
+)
+
+func TestPodInfo_HasAnyOfReadinessGates(t *testing.T) {
+	type args struct {
+		conditionTypes []corev1.PodConditionType
+	}
+	tests := []struct {
+		name string
+		pod  PodInfo
+		args args
+		want bool
+	}{
+		{
+			name: "empty readinessGates passed-in",
+			pod: PodInfo{
+				Key: types.NamespacedName{Namespace: "ns-1", Name: "pod-1"},
+				ReadinessGates: []corev1.PodReadinessGate{
+					{
+						ConditionType: "ingress.k8s.aws/cond-1",
+					},
+				},
+			},
+			args: args{
+				conditionTypes: []corev1.PodConditionType{},
+			},
+			want: false,
+		},
+		{
+			name: "contains the readinessGates passed-in",
+			pod: PodInfo{
+				Key: types.NamespacedName{Namespace: "ns-1", Name: "pod-1"},
+				ReadinessGates: []corev1.PodReadinessGate{
+					{
+						ConditionType: "ingress.k8s.aws/cond-1",
+					},
+				},
+			},
+			args: args{
+				conditionTypes: []corev1.PodConditionType{
+					"ingress.k8s.aws/cond-1",
+				},
+			},
+			want: true,
+		},
+		{
+			name: "doesn't contain the readinessGates passed-in",
+			pod: PodInfo{
+				Key: types.NamespacedName{Namespace: "ns-1", Name: "pod-1"},
+				ReadinessGates: []corev1.PodReadinessGate{
+					{
+						ConditionType: "ingress.k8s.aws/cond-1",
+					},
+				},
+			},
+			args: args{
+				conditionTypes: []corev1.PodConditionType{
+					"ingress.k8s.aws/cond-2",
+				},
+			},
+			want: false,
+		},
+		{
+			name: "contains one of the readinessGates passed-in",
+			pod: PodInfo{
+				Key: types.NamespacedName{Namespace: "ns-1", Name: "pod-1"},
+				ReadinessGates: []corev1.PodReadinessGate{
+					{
+						ConditionType: "ingress.k8s.aws/cond-1",
+					},
+				},
+			},
+			args: args{
+				conditionTypes: []corev1.PodConditionType{
+					"ingress.k8s.aws/cond-1",
+					"ingress.k8s.aws/cond-2",
+				},
+			},
+			want: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := tt.pod.HasAnyOfReadinessGates(tt.args.conditionTypes)
+			assert.Equal(t, tt.want, got)
+		})
+	}
+}
+
+func TestPodInfo_IsContainersReady(t *testing.T) {
+	tests := []struct {
+		name string
+		pod  PodInfo
+		want bool
+	}{
+		{
+			name: "pod have true containers ready condition",
+			pod: PodInfo{
+				Key: types.NamespacedName{Namespace: "ns-1", Name: "pod-1"},
+				Conditions: []corev1.PodCondition{
+					{
+						Type:   corev1.ContainersReady,
+						Status: corev1.ConditionTrue,
+					},
+				},
+			},
+			want: true,
+		},
+		{
+			name: "pod have false containers ready condition",
+			pod: PodInfo{
+				Key: types.NamespacedName{Namespace: "ns-1", Name: "pod-1"},
+				Conditions: []corev1.PodCondition{
+					{
+						Type:   corev1.ContainersReady,
+						Status: corev1.ConditionFalse,
+					},
+				},
+			},
+			want: false,
+		},
+		{
+			name: "pod don't have containers ready condition",
+			pod: PodInfo{
+				Key:        types.NamespacedName{Namespace: "ns-1", Name: "pod-1"},
+				Conditions: []corev1.PodCondition{},
+			},
+			want: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := tt.pod.IsContainersReady()
+			assert.Equal(t, tt.want, got)
+		})
+	}
+}
+
+func TestPodInfo_GetPodCondition(t *testing.T) {
+	type args struct {
+		conditionType corev1.PodConditionType
+	}
+	tests := []struct {
+		name       string
+		pod        PodInfo
+		args       args
+		want       corev1.PodCondition
+		wantExists bool
+	}{
+		{
+			name: "condition exists",
+			pod: PodInfo{
+				Key: types.NamespacedName{Namespace: "ns-1", Name: "pod-1"},
+				Conditions: []corev1.PodCondition{
+					{
+						Type:   corev1.ContainersReady,
+						Status: corev1.ConditionFalse,
+					},
+				},
+			},
+			args: args{
+				conditionType: corev1.ContainersReady,
+			},
+			want: corev1.PodCondition{
+				Type:   corev1.ContainersReady,
+				Status: corev1.ConditionFalse,
+			},
+			wantExists: true,
+		},
+		{
+			name: "condition doesn't exists",
+			pod: PodInfo{
+				Key: types.NamespacedName{Namespace: "ns-1", Name: "pod-1"},
+				Conditions: []corev1.PodCondition{
+					{
+						Type:   corev1.ContainersReady,
+						Status: corev1.ConditionFalse,
+					},
+				},
+			},
+			args: args{
+				conditionType: corev1.PodReady,
+			},
+			want:       corev1.PodCondition{},
+			wantExists: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, gotExists := tt.pod.GetPodCondition(tt.args.conditionType)
+			assert.Equal(t, tt.wantExists, gotExists)
+			assert.Equal(t, tt.want, got)
+		})
+	}
+}
+
+func TestPodInfo_LookupContainerPort(t *testing.T) {
+	type args struct {
+		port intstr.IntOrString
+	}
+	tests := []struct {
+		name    string
+		pod     PodInfo
+		args    args
+		want    int64
+		wantErr error
+	}{
+		{
+			name: "str container port exists",
+			pod: PodInfo{
+				Key: types.NamespacedName{Namespace: "ns-1", Name: "pod-1"},
+				ContainerPorts: []corev1.ContainerPort{
+					{
+						Name:          "http",
+						ContainerPort: 8080,
+					},
+					{
+						Name:          "https",
+						ContainerPort: 8443,
+					},
+				},
+			},
+			args: args{
+				port: intstr.FromString("https"),
+			},
+			want: 8443,
+		},
+		{
+			name: "str container port don't exists",
+			pod: PodInfo{
+				Key: types.NamespacedName{Namespace: "ns-1", Name: "pod-1"},
+				ContainerPorts: []corev1.ContainerPort{
+					{
+						Name:          "http",
+						ContainerPort: 8080,
+					},
+					{
+						Name:          "https",
+						ContainerPort: 8443,
+					},
+				},
+			},
+			args: args{
+				port: intstr.FromString("ssh"),
+			},
+			wantErr: errors.New("unable to find port ssh on pod ns-1/pod-1"),
+		},
+		{
+			name: "int container port exists",
+			pod: PodInfo{
+				Key: types.NamespacedName{Namespace: "ns-1", Name: "pod-1"},
+				ContainerPorts: []corev1.ContainerPort{
+					{
+						Name:          "http",
+						ContainerPort: 8080,
+					},
+					{
+						Name:          "https",
+						ContainerPort: 8443,
+					},
+				},
+			},
+			args: args{
+				port: intstr.FromInt(8080),
+			},
+			want: 8080,
+		},
+		{
+			name: "int container port don't exists",
+			pod: PodInfo{
+				Key: types.NamespacedName{Namespace: "ns-1", Name: "pod-1"},
+				ContainerPorts: []corev1.ContainerPort{
+					{
+						Name:          "http",
+						ContainerPort: 8080,
+					},
+					{
+						Name:          "https",
+						ContainerPort: 8443,
+					},
+				},
+			},
+			args: args{
+				port: intstr.FromInt(9090),
+			},
+			want: 9090,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := tt.pod.LookupContainerPort(tt.args.port)
+			if tt.wantErr != nil {
+				assert.EqualError(t, err, tt.wantErr.Error())
+			} else {
+				assert.NoError(t, err)
+				assert.Equal(t, tt.want, got)
+			}
+		})
+	}
+}
+
+func Test_buildPodInfo(t *testing.T) {
+	type args struct {
+		pod *corev1.Pod
+	}
+	tests := []struct {
+		name string
+		args args
+		want PodInfo
+	}{
+		{
+			name: "standard case",
+			args: args{
+				pod: &corev1.Pod{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "my-ns",
+						Name:      "pod-1",
+						UID:       "pod-uuid",
+					},
+					Spec: corev1.PodSpec{
+						Containers: []corev1.Container{
+							{
+								Ports: []corev1.ContainerPort{
+									{
+										Name:          "ssh",
+										ContainerPort: 22,
+									},
+									{
+										Name:          "http",
+										ContainerPort: 8080,
+									},
+								},
+							},
+							{
+								Ports: []corev1.ContainerPort{
+									{
+										Name:          "https",
+										ContainerPort: 8443,
+									},
+								},
+							},
+						},
+						ReadinessGates: []corev1.PodReadinessGate{
+							{
+								ConditionType: "ingress.k8s.aws/cond-1",
+							},
+							{
+								ConditionType: "ingress.k8s.aws/cond-2",
+							},
+						},
+					},
+					Status: corev1.PodStatus{
+						Conditions: []corev1.PodCondition{
+							{
+								Type:   corev1.ContainersReady,
+								Status: corev1.ConditionFalse,
+							},
+							{
+								Type:   "ingress.k8s.aws/cond-2",
+								Status: corev1.ConditionTrue,
+							},
+						},
+						PodIP: "192.168.1.1",
+					},
+				},
+			},
+			want: PodInfo{
+				Key: types.NamespacedName{Namespace: "my-ns", Name: "pod-1"},
+				UID: "pod-uuid",
+				ContainerPorts: []corev1.ContainerPort{
+					{
+						Name:          "ssh",
+						ContainerPort: 22,
+					},
+					{
+						Name:          "http",
+						ContainerPort: 8080,
+					},
+					{
+						Name:          "https",
+						ContainerPort: 8443,
+					},
+				},
+				ReadinessGates: []corev1.PodReadinessGate{
+					{
+						ConditionType: "ingress.k8s.aws/cond-1",
+					},
+					{
+						ConditionType: "ingress.k8s.aws/cond-2",
+					},
+				},
+				Conditions: []corev1.PodCondition{
+					{
+						Type:   corev1.ContainersReady,
+						Status: corev1.ConditionFalse,
+					},
+					{
+						Type:   "ingress.k8s.aws/cond-2",
+						Status: corev1.ConditionTrue,
+					},
+				},
+				PodIP: "192.168.1.1",
+			},
+		},
+		{
+			name: "standard case - with ENIInfo",
+			args: args{
+				pod: &corev1.Pod{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "my-ns",
+						Name:      "pod-1",
+						UID:       "pod-uuid",
+						Annotations: map[string]string{
+							"vpc.amazonaws.com/pod-eni": `[{"eniId":"eni-06a712e1622fda4a0","ifAddress":"02:34:a5:25:0b:63","privateIp":"192.168.219.103","vlanId":3,"subnetCidr":"192.168.192.0/19"}]`,
+						},
+					},
+					Spec: corev1.PodSpec{
+						Containers: []corev1.Container{
+							{
+								Ports: []corev1.ContainerPort{
+									{
+										Name:          "ssh",
+										ContainerPort: 22,
+									},
+									{
+										Name:          "http",
+										ContainerPort: 8080,
+									},
+								},
+							},
+							{
+								Ports: []corev1.ContainerPort{
+									{
+										Name:          "https",
+										ContainerPort: 8443,
+									},
+								},
+							},
+						},
+						ReadinessGates: []corev1.PodReadinessGate{
+							{
+								ConditionType: "ingress.k8s.aws/cond-1",
+							},
+							{
+								ConditionType: "ingress.k8s.aws/cond-2",
+							},
+						},
+					},
+					Status: corev1.PodStatus{
+						Conditions: []corev1.PodCondition{
+							{
+								Type:   corev1.ContainersReady,
+								Status: corev1.ConditionFalse,
+							},
+							{
+								Type:   "ingress.k8s.aws/cond-2",
+								Status: corev1.ConditionTrue,
+							},
+						},
+						PodIP: "192.168.1.1",
+					},
+				},
+			},
+			want: PodInfo{
+				Key: types.NamespacedName{Namespace: "my-ns", Name: "pod-1"},
+				UID: "pod-uuid",
+				ContainerPorts: []corev1.ContainerPort{
+					{
+						Name:          "ssh",
+						ContainerPort: 22,
+					},
+					{
+						Name:          "http",
+						ContainerPort: 8080,
+					},
+					{
+						Name:          "https",
+						ContainerPort: 8443,
+					},
+				},
+				ReadinessGates: []corev1.PodReadinessGate{
+					{
+						ConditionType: "ingress.k8s.aws/cond-1",
+					},
+					{
+						ConditionType: "ingress.k8s.aws/cond-2",
+					},
+				},
+				Conditions: []corev1.PodCondition{
+					{
+						Type:   corev1.ContainersReady,
+						Status: corev1.ConditionFalse,
+					},
+					{
+						Type:   "ingress.k8s.aws/cond-2",
+						Status: corev1.ConditionTrue,
+					},
+				},
+				PodIP: "192.168.1.1",
+				ENIInfos: []PodENIInfo{
+					{
+						ENIID:     "eni-06a712e1622fda4a0",
+						PrivateIP: "192.168.219.103",
+					},
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := buildPodInfo(tt.args.pod)
+			assert.Equal(t, tt.want, got)
+		})
+	}
+}
+
+func Test_buildPodENIInfo(t *testing.T) {
+	type args struct {
+		pod *corev1.Pod
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    []PodENIInfo
+		wantErr error
+	}{
+		{
+			name: "pod-eni annotation exists and valid",
+			args: args{
+				pod: &corev1.Pod{
+					ObjectMeta: metav1.ObjectMeta{
+						Annotations: map[string]string{
+							"vpc.amazonaws.com/pod-eni": `[{"eniId":"eni-06a712e1622fda4a0","ifAddress":"02:34:a5:25:0b:63","privateIp":"192.168.219.103","vlanId":3,"subnetCidr":"192.168.192.0/19"}]`,
+						},
+					},
+				},
+			},
+			want: []PodENIInfo{
+				{
+					ENIID:     "eni-06a712e1622fda4a0",
+					PrivateIP: "192.168.219.103",
+				},
+			},
+		},
+		{
+			name: "pod-eni annotation didn't exist",
+			args: args{
+				pod: &corev1.Pod{
+					ObjectMeta: metav1.ObjectMeta{
+						Annotations: map[string]string{},
+					},
+				},
+			},
+			want: nil,
+		},
+		{
+			name: "pod-eni annotation invalid",
+			args: args{
+				pod: &corev1.Pod{
+					ObjectMeta: metav1.ObjectMeta{
+						Annotations: map[string]string{
+							"vpc.amazonaws.com/pod-eni": ``,
+						},
+					},
+				},
+			},
+			wantErr: errors.New("unexpected end of JSON input"),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := buildPodENIInfos(tt.args.pod)
+			if tt.wantErr != nil {
+				assert.EqualError(t, err, tt.wantErr.Error())
+			} else {
+				assert.NoError(t, err)
+				assert.Equal(t, tt.want, got)
+			}
+		})
+	}
+}
diff --git a/pkg/networking/node_eni_info_resolver_test.go b/pkg/networking/node_eni_info_resolver_test.go
new file mode 100644
index 000000000..04499c0f3
--- /dev/null
+++ b/pkg/networking/node_eni_info_resolver_test.go
@@ -0,0 +1,189 @@
+package networking
+
+import (
+	"github.com/stretchr/testify/assert"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"testing"
+)
+
+func Test_computeNodeENIInfoCacheKey(t *testing.T) {
+	type args struct {
+		node *corev1.Node
+	}
+	tests := []struct {
+		name string
+		args args
+		want nodeENIInfoCacheKey
+	}{
+		{
+			name: "node UID should be included as cacheKey",
+			args: args{
+				node: &corev1.Node{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "ns-1",
+						Name:      "node-1",
+						UID:       "uuid",
+					},
+				},
+			},
+			want: nodeENIInfoCacheKey{
+				nodeKey: types.NamespacedName{Namespace: "ns-1", Name: "node-1"},
+				nodeUID: "uuid",
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := computeNodeENIInfoCacheKey(tt.args.node)
+			assert.Equal(t, tt.want, got)
+		})
+	}
+}
+
+func Test_computeNodesWithoutENIInfo(t *testing.T) {
+	type args struct {
+		nodes            []*corev1.Node
+		eniInfoByNodeKey map[types.NamespacedName]ENIInfo
+	}
+	tests := []struct {
+		name string
+		args args
+		want []*corev1.Node
+	}{
+		{
+			name: "all nodes are resolved",
+			args: args{
+				nodes: []*corev1.Node{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Namespace: "ns-1",
+							Name:      "node-1",
+						},
+					},
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Namespace: "ns-1",
+							Name:      "node-2",
+						},
+					},
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Namespace: "ns-2",
+							Name:      "node-1",
+						},
+					},
+				},
+				eniInfoByNodeKey: map[types.NamespacedName]ENIInfo{
+					types.NamespacedName{Namespace: "ns-1", Name: "node-1"}: {
+						NetworkInterfaceID: "eni-xx",
+					},
+					types.NamespacedName{Namespace: "ns-1", Name: "node-2"}: {
+						NetworkInterfaceID: "eni-xx",
+					},
+					types.NamespacedName{Namespace: "ns-2", Name: "node-1"}: {
+						NetworkInterfaceID: "eni-xx",
+					},
+				},
+			},
+			want: []*corev1.Node{},
+		},
+		{
+			name: "some nodes are resolved",
+			args: args{
+				nodes: []*corev1.Node{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Namespace: "ns-1",
+							Name:      "node-1",
+						},
+					},
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Namespace: "ns-1",
+							Name:      "node-3",
+						},
+					},
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Namespace: "ns-2",
+							Name:      "node-1",
+						},
+					},
+				},
+				eniInfoByNodeKey: map[types.NamespacedName]ENIInfo{
+					types.NamespacedName{Namespace: "ns-1", Name: "node-1"}: {
+						NetworkInterfaceID: "eni-xx",
+					},
+					types.NamespacedName{Namespace: "ns-1", Name: "node-2"}: {
+						NetworkInterfaceID: "eni-xx",
+					},
+					types.NamespacedName{Namespace: "ns-2", Name: "node-1"}: {
+						NetworkInterfaceID: "eni-xx",
+					},
+				},
+			},
+			want: []*corev1.Node{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "ns-1",
+						Name:      "node-3",
+					},
+				},
+			},
+		},
+		{
+			name: "no nodes are resolved",
+			args: args{
+				nodes: []*corev1.Node{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Namespace: "ns-1",
+							Name:      "node-1",
+						},
+					},
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Namespace: "ns-1",
+							Name:      "node-3",
+						},
+					},
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Namespace: "ns-2",
+							Name:      "node-1",
+						},
+					},
+				},
+				eniInfoByNodeKey: map[types.NamespacedName]ENIInfo{},
+			},
+			want: []*corev1.Node{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "ns-1",
+						Name:      "node-1",
+					},
+				},
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "ns-1",
+						Name:      "node-3",
+					},
+				},
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "ns-2",
+						Name:      "node-1",
+					},
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := computeNodesWithoutENIInfo(tt.args.nodes, tt.args.eniInfoByNodeKey)
+			assert.Equal(t, tt.want, got)
+		})
+	}
+}
diff --git a/pkg/networking/pod_eni_info_resolver_test.go b/pkg/networking/pod_eni_info_resolver_test.go
new file mode 100644
index 000000000..9100b3e55
--- /dev/null
+++ b/pkg/networking/pod_eni_info_resolver_test.go
@@ -0,0 +1,146 @@
+package networking
+
+import (
+	"github.com/stretchr/testify/assert"
+	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/aws-load-balancer-controller/pkg/k8s"
+	"testing"
+)
+
+func Test_computePodENIInfoCacheKey(t *testing.T) {
+	type args struct {
+		pod k8s.PodInfo
+	}
+	tests := []struct {
+		name string
+		args args
+		want podENIInfoCacheKey
+	}{
+		{
+			name: "pods UID should be included as cacheKey",
+			args: args{
+				pod: k8s.PodInfo{
+					Key: types.NamespacedName{Namespace: "ns-1", Name: "pod-1"},
+					UID: types.UID("uuid"),
+				},
+			},
+			want: podENIInfoCacheKey{
+				podKey: types.NamespacedName{Namespace: "ns-1", Name: "pod-1"},
+				podUID: "uuid",
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := computePodENIInfoCacheKey(tt.args.pod)
+			assert.Equal(t, tt.want, got)
+		})
+	}
+}
+
+func Test_computePodsWithoutENIInfo(t *testing.T) {
+	type args struct {
+		pods            []k8s.PodInfo
+		eniInfoByPodKey map[types.NamespacedName]ENIInfo
+	}
+	tests := []struct {
+		name string
+		args args
+		want []k8s.PodInfo
+	}{
+		{
+			name: "all pods are resolved",
+			args: args{
+				pods: []k8s.PodInfo{
+					{
+						Key: types.NamespacedName{Namespace: "ns-1", Name: "pod-1"},
+					},
+					{
+						Key: types.NamespacedName{Namespace: "ns-1", Name: "pod-2"},
+					},
+					{
+						Key: types.NamespacedName{Namespace: "ns-2", Name: "pod-1"},
+					},
+				},
+				eniInfoByPodKey: map[types.NamespacedName]ENIInfo{
+					types.NamespacedName{Namespace: "ns-1", Name: "pod-1"}: {
+						NetworkInterfaceID: "eni-xx",
+					},
+					types.NamespacedName{Namespace: "ns-1", Name: "pod-2"}: {
+						NetworkInterfaceID: "eni-xx",
+					},
+					types.NamespacedName{Namespace: "ns-2", Name: "pod-1"}: {
+						NetworkInterfaceID: "eni-xx",
+					},
+				},
+			},
+			want: []k8s.PodInfo{},
+		},
+		{
+			name: "some pods are resolved",
+			args: args{
+				pods: []k8s.PodInfo{
+					{
+						Key: types.NamespacedName{Namespace: "ns-1", Name: "pod-1"},
+					},
+					{
+						Key: types.NamespacedName{Namespace: "ns-1", Name: "pod-3"},
+					},
+					{
+						Key: types.NamespacedName{Namespace: "ns-2", Name: "pod-1"},
+					},
+				},
+				eniInfoByPodKey: map[types.NamespacedName]ENIInfo{
+					types.NamespacedName{Namespace: "ns-1", Name: "pod-1"}: {
+						NetworkInterfaceID: "eni-xx",
+					},
+					types.NamespacedName{Namespace: "ns-1", Name: "pod-2"}: {
+						NetworkInterfaceID: "eni-xx",
+					},
+					types.NamespacedName{Namespace: "ns-2", Name: "pod-1"}: {
+						NetworkInterfaceID: "eni-xx",
+					},
+				},
+			},
+			want: []k8s.PodInfo{
+				{
+					Key: types.NamespacedName{Namespace: "ns-1", Name: "pod-3"},
+				},
+			},
+		},
+		{
+			name: "no pods are resolved",
+			args: args{
+				pods: []k8s.PodInfo{
+					{
+						Key: types.NamespacedName{Namespace: "ns-1", Name: "pod-1"},
+					},
+					{
+						Key: types.NamespacedName{Namespace: "ns-1", Name: "pod-3"},
+					},
+					{
+						Key: types.NamespacedName{Namespace: "ns-2", Name: "pod-1"},
+					},
+				},
+				eniInfoByPodKey: map[types.NamespacedName]ENIInfo{},
+			},
+			want: []k8s.PodInfo{
+				{
+					Key: types.NamespacedName{Namespace: "ns-1", Name: "pod-1"},
+				},
+				{
+					Key: types.NamespacedName{Namespace: "ns-1", Name: "pod-3"},
+				},
+				{
+					Key: types.NamespacedName{Namespace: "ns-2", Name: "pod-1"},
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := computePodsWithoutENIInfo(tt.args.pods, tt.args.eniInfoByPodKey)
+			assert.Equal(t, tt.want, got)
+		})
+	}
+}
diff --git a/pkg/runtime/errors_test.go b/pkg/runtime/errors_test.go
index 1aa032d57..b8c034bd4 100644
--- a/pkg/runtime/errors_test.go
+++ b/pkg/runtime/errors_test.go
@@ -1,94 +1,61 @@
 package runtime
 
 import (
-	"github.com/pkg/errors"
 	"github.com/stretchr/testify/assert"
 	"testing"
 	"time"
 )
 
-func TestNewRequeueError(t *testing.T) {
+func TestNewRequeueNeeded(t *testing.T) {
 	type args struct {
-		err error
+		msg string
 	}
 	tests := []struct {
 		name       string
 		args       args
-		wantErr    string
-		wantUnwrap error
+		wantReason string
 	}{
 		{
-			name: "wraps non-nil error",
+			name: "standard case",
 			args: args{
-				err: errors.New("some error"),
+				msg: "some message",
 			},
-			wantErr:    "some error",
-			wantUnwrap: errors.New("some error"),
-		},
-		{
-			name: "wraps nil error",
-			args: args{
-				err: nil,
-			},
-			wantErr:    "",
-			wantUnwrap: nil,
+			wantReason: "some message",
 		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			got := NewRequeueError(tt.args.err)
-			assert.Equal(t, tt.wantErr, got.Error())
-			if tt.wantUnwrap != nil {
-				assert.EqualError(t, got.Unwrap(), tt.wantUnwrap.Error())
-			} else {
-				assert.NoError(t, got.Unwrap())
-			}
+			got := NewRequeueNeeded(tt.args.msg)
+			assert.Equal(t, tt.wantReason, got.Reason())
 		})
 	}
 }
 
-func TestNewRequeueAfterError(t *testing.T) {
+func TestNewRequeueNeededAfter(t *testing.T) {
 	type args struct {
-		err      error
+		msg      string
 		duration time.Duration
 	}
 	tests := []struct {
 		name         string
 		args         args
-		wantErr      string
-		wantUnwrap   error
+		wantReason   string
 		wantDuration time.Duration
 	}{
 		{
-			name: "wraps non-nil error",
-			args: args{
-				err:      errors.New("some error"),
-				duration: 3 * time.Second,
-			},
-			wantErr:      "some error",
-			wantUnwrap:   errors.New("some error"),
-			wantDuration: 3 * time.Second,
-		},
-		{
-			name: "wraps nil error",
+			name: "standard case",
 			args: args{
-				err:      nil,
+				msg:      "some message",
 				duration: 3 * time.Second,
 			},
-			wantErr:      "",
-			wantUnwrap:   nil,
+			wantReason:   "some message",
 			wantDuration: 3 * time.Second,
 		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			got := NewRequeueAfterError(tt.args.err, tt.args.duration)
-			assert.Equal(t, tt.wantErr, got.Error())
-			if tt.wantUnwrap != nil {
-				assert.EqualError(t, got.Unwrap(), tt.wantUnwrap.Error())
-			} else {
-				assert.NoError(t, got.Unwrap())
-			}
+			got := NewRequeueNeededAfter(tt.args.msg, tt.args.duration)
+			assert.Equal(t, tt.wantReason, got.Reason())
 			assert.Equal(t, 3*time.Second, got.Duration())
 		})
 	}
diff --git a/pkg/runtime/reconcile_test.go b/pkg/runtime/reconcile_test.go
index 09ec35e22..3b1767302 100644
--- a/pkg/runtime/reconcile_test.go
+++ b/pkg/runtime/reconcile_test.go
@@ -28,9 +28,9 @@ func TestHandleReconcileError(t *testing.T) {
 			wantErr: nil,
 		},
 		{
-			name: "input err is RequeueAfterError",
+			name: "input err is RequeueNeededAfter",
 			args: args{
-				err: NewRequeueAfterError(errors.New("some error"), 3*time.Second),
+				err: NewRequeueNeededAfter("some error", 3*time.Second),
 			},
 			want: ctrl.Result{
 				RequeueAfter: 3 * time.Second,
@@ -38,9 +38,9 @@ func TestHandleReconcileError(t *testing.T) {
 			wantErr: nil,
 		},
 		{
-			name: "input err is RequeueError",
+			name: "input err is RequeueNeeded",
 			args: args{
-				err: NewRequeueError(errors.New("some error")),
+				err: NewRequeueNeeded("some error"),
 			},
 			want: ctrl.Result{
 				Requeue: true,
diff --git a/pkg/targetgroupbinding/networking_manager_test.go b/pkg/targetgroupbinding/networking_manager_test.go
index 6c895052e..9536b7024 100644
--- a/pkg/targetgroupbinding/networking_manager_test.go
+++ b/pkg/targetgroupbinding/networking_manager_test.go
@@ -2,14 +2,15 @@ package targetgroupbinding
 
 import (
 	"context"
+	"errors"
 	awssdk "github.com/aws/aws-sdk-go/aws"
 	ec2sdk "github.com/aws/aws-sdk-go/service/ec2"
-	"github.com/pkg/errors"
 	"github.com/stretchr/testify/assert"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/util/intstr"
 	elbv2api "sigs.k8s.io/aws-load-balancer-controller/apis/elbv2/v1beta1"
+	"sigs.k8s.io/aws-load-balancer-controller/pkg/k8s"
 	"sigs.k8s.io/aws-load-balancer-controller/pkg/networking"
 	"testing"
 )
@@ -19,7 +20,7 @@ func Test_defaultNetworkingManager_computeIngressPermissionsForTGBNetworking(t *
 	port8443 := intstr.FromInt(8443)
 	type args struct {
 		tgbNetworking elbv2api.TargetGroupBindingNetworking
-		pods          []*corev1.Pod
+		pods          []k8s.PodInfo
 	}
 	tests := []struct {
 		name    string
@@ -244,7 +245,7 @@ func Test_defaultNetworkingManager_computePermissionsForPeerPort(t *testing.T) {
 	type args struct {
 		peer elbv2api.NetworkingPeer
 		port elbv2api.NetworkingPort
-		pods []*corev1.Pod
+		pods []k8s.PodInfo
 	}
 	tests := []struct {
 		name    string
@@ -357,32 +358,22 @@ func Test_defaultNetworkingManager_computePermissionsForPeerPort(t *testing.T) {
 					Protocol: &protocolUDP,
 					Port:     &portHTTP,
 				},
-				pods: []*corev1.Pod{
+				pods: []k8s.PodInfo{
 					{
-						Spec: corev1.PodSpec{
-							Containers: []corev1.Container{
-								{
-									Ports: []corev1.ContainerPort{
-										{
-											Name:          "http",
-											ContainerPort: 80,
-										},
-									},
-								},
+						Key: types.NamespacedName{Namespace: "ns-1", Name: "pod-1"},
+						ContainerPorts: []corev1.ContainerPort{
+							{
+								Name:          "http",
+								ContainerPort: 80,
 							},
 						},
 					},
 					{
-						Spec: corev1.PodSpec{
-							Containers: []corev1.Container{
-								{
-									Ports: []corev1.ContainerPort{
-										{
-											Name:          "http",
-											ContainerPort: 8080,
-										},
-									},
-								},
+						Key: types.NamespacedName{Namespace: "ns-1", Name: "pod-2"},
+						ContainerPorts: []corev1.ContainerPort{
+							{
+								Name:          "http",
+								ContainerPort: 8080,
 							},
 						},
 					},
@@ -498,7 +489,7 @@ func Test_defaultNetworkingManager_computePermissionsForPeerPort(t *testing.T) {
 func Test_defaultNetworkingManager_computeNumericalPorts(t *testing.T) {
 	type args struct {
 		port intstr.IntOrString
-		pods []*corev1.Pod
+		pods []k8s.PodInfo
 	}
 	tests := []struct {
 		name    string
@@ -518,32 +509,22 @@ func Test_defaultNetworkingManager_computeNumericalPorts(t *testing.T) {
 			name: "named port resolves to same numerical port",
 			args: args{
 				port: intstr.FromString("http"),
-				pods: []*corev1.Pod{
+				pods: []k8s.PodInfo{
 					{
-						Spec: corev1.PodSpec{
-							Containers: []corev1.Container{
-								{
-									Ports: []corev1.ContainerPort{
-										{
-											Name:          "http",
-											ContainerPort: 80,
-										},
-									},
-								},
+						Key: types.NamespacedName{Namespace: "ns-1", Name: "pod-1"},
+						ContainerPorts: []corev1.ContainerPort{
+							{
+								Name:          "http",
+								ContainerPort: 80,
 							},
 						},
 					},
 					{
-						Spec: corev1.PodSpec{
-							Containers: []corev1.Container{
-								{
-									Ports: []corev1.ContainerPort{
-										{
-											Name:          "http",
-											ContainerPort: 80,
-										},
-									},
-								},
+						Key: types.NamespacedName{Namespace: "ns-1", Name: "pod-2"},
+						ContainerPorts: []corev1.ContainerPort{
+							{
+								Name:          "http",
+								ContainerPort: 80,
 							},
 						},
 					},
@@ -555,32 +536,22 @@ func Test_defaultNetworkingManager_computeNumericalPorts(t *testing.T) {
 			name: "named port resolves to different numerical port",
 			args: args{
 				port: intstr.FromString("http"),
-				pods: []*corev1.Pod{
+				pods: []k8s.PodInfo{
 					{
-						Spec: corev1.PodSpec{
-							Containers: []corev1.Container{
-								{
-									Ports: []corev1.ContainerPort{
-										{
-											Name:          "http",
-											ContainerPort: 80,
-										},
-									},
-								},
+						Key: types.NamespacedName{Namespace: "ns-1", Name: "pod-1"},
+						ContainerPorts: []corev1.ContainerPort{
+							{
+								Name:          "http",
+								ContainerPort: 80,
 							},
 						},
 					},
 					{
-						Spec: corev1.PodSpec{
-							Containers: []corev1.Container{
-								{
-									Ports: []corev1.ContainerPort{
-										{
-											Name:          "http",
-											ContainerPort: 8080,
-										},
-									},
-								},
+						Key: types.NamespacedName{Namespace: "ns-1", Name: "pod-2"},
+						ContainerPorts: []corev1.ContainerPort{
+							{
+								Name:          "http",
+								ContainerPort: 8080,
 							},
 						},
 					},
diff --git a/pkg/targetgroupbinding/resource_manager_test.go b/pkg/targetgroupbinding/resource_manager_test.go
index bb51a1a69..c7de3581f 100644
--- a/pkg/targetgroupbinding/resource_manager_test.go
+++ b/pkg/targetgroupbinding/resource_manager_test.go
@@ -10,6 +10,7 @@ import (
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/types"
 	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
 	"sigs.k8s.io/aws-load-balancer-controller/pkg/equality"
 	"sigs.k8s.io/aws-load-balancer-controller/pkg/k8s"
@@ -19,14 +20,19 @@ import (
 )
 
 func Test_defaultResourceManager_updateTargetHealthPodConditionForPod(t *testing.T) {
+	type env struct {
+		pods []*corev1.Pod
+	}
+
 	type args struct {
-		pod                  *corev1.Pod
+		pod                  k8s.PodInfo
 		targetHealth         *elbv2sdk.TargetHealth
 		targetHealthCondType corev1.PodConditionType
 	}
 
 	tests := []struct {
 		name    string
+		env     env
 		args    args
 		want    bool
 		wantPod *corev1.Pod
@@ -34,28 +40,48 @@ func Test_defaultResourceManager_updateTargetHealthPodConditionForPod(t *testing
 	}{
 		{
 			name: "pod contains readinessGate and targetHealth is healthy - add pod condition",
-			args: args{
-				pod: &corev1.Pod{
-					ObjectMeta: metav1.ObjectMeta{
-						Namespace: "default",
-						Name:      "my-pod",
-					},
-					Spec: corev1.PodSpec{
-						ReadinessGates: []corev1.PodReadinessGate{
-							{
-								ConditionType: "target-health.elbv2.k8s.aws/my-tgb",
+			env: env{
+				pods: []*corev1.Pod{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Namespace: "default",
+							Name:      "my-pod",
+							UID:       "my-pod-uuid",
+						},
+						Spec: corev1.PodSpec{
+							ReadinessGates: []corev1.PodReadinessGate{
+								{
+									ConditionType: "target-health.elbv2.k8s.aws/my-tgb",
+								},
 							},
 						},
-					},
-					Status: corev1.PodStatus{
-						Conditions: []corev1.PodCondition{
-							{
-								Type:   corev1.ContainersReady,
-								Status: corev1.ConditionTrue,
+						Status: corev1.PodStatus{
+							Conditions: []corev1.PodCondition{
+								{
+									Type:   corev1.ContainersReady,
+									Status: corev1.ConditionTrue,
+								},
 							},
 						},
 					},
 				},
+			},
+			args: args{
+				pod: k8s.PodInfo{
+					Key: types.NamespacedName{Namespace: "default", Name: "my-pod"},
+					UID: "my-pod-uuid",
+					ReadinessGates: []corev1.PodReadinessGate{
+						{
+							ConditionType: "target-health.elbv2.k8s.aws/my-tgb",
+						},
+					},
+					Conditions: []corev1.PodCondition{
+						{
+							Type:   corev1.ContainersReady,
+							Status: corev1.ConditionTrue,
+						},
+					},
+				},
 				targetHealth: &elbv2sdk.TargetHealth{
 					State: awssdk.String(elbv2sdk.TargetHealthStateEnumHealthy),
 				},
@@ -66,6 +92,7 @@ func Test_defaultResourceManager_updateTargetHealthPodConditionForPod(t *testing
 				ObjectMeta: metav1.ObjectMeta{
 					Namespace: "default",
 					Name:      "my-pod",
+					UID:       "my-pod-uuid",
 				},
 				Spec: corev1.PodSpec{
 					ReadinessGates: []corev1.PodReadinessGate{
@@ -77,11 +104,11 @@ func Test_defaultResourceManager_updateTargetHealthPodConditionForPod(t *testing
 				Status: corev1.PodStatus{
 					Conditions: []corev1.PodCondition{
 						{
-							Type:   corev1.ContainersReady,
+							Type:   "target-health.elbv2.k8s.aws/my-tgb",
 							Status: corev1.ConditionTrue,
 						},
 						{
-							Type:   "target-health.elbv2.k8s.aws/my-tgb",
+							Type:   corev1.ContainersReady,
 							Status: corev1.ConditionTrue,
 						},
 					},
@@ -90,34 +117,56 @@ func Test_defaultResourceManager_updateTargetHealthPodConditionForPod(t *testing
 		},
 		{
 			name: "pod contains readinessGate and targetHealth is healthy - update pod condition",
-			args: args{
-				pod: &corev1.Pod{
-					ObjectMeta: metav1.ObjectMeta{
-						Namespace: "default",
-						Name:      "my-pod",
-					},
-					Spec: corev1.PodSpec{
-						ReadinessGates: []corev1.PodReadinessGate{
-							{
-								ConditionType: "target-health.elbv2.k8s.aws/my-tgb",
-							},
+			env: env{
+				pods: []*corev1.Pod{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Namespace: "default",
+							Name:      "my-pod",
+							UID:       "my-pod-uuid",
 						},
-					},
-					Status: corev1.PodStatus{
-						Conditions: []corev1.PodCondition{
-							{
-								Type:   corev1.ContainersReady,
-								Status: corev1.ConditionTrue,
+						Spec: corev1.PodSpec{
+							ReadinessGates: []corev1.PodReadinessGate{
+								{
+									ConditionType: "target-health.elbv2.k8s.aws/my-tgb",
+								},
 							},
-							{
-								Type:    "target-health.elbv2.k8s.aws/my-tgb",
-								Status:  corev1.ConditionFalse,
-								Reason:  elbv2sdk.TargetHealthReasonEnumElbRegistrationInProgress,
-								Message: "Target registration is in progress",
+						},
+						Status: corev1.PodStatus{
+							Conditions: []corev1.PodCondition{
+								{
+									Type:   "target-health.elbv2.k8s.aws/my-tgb",
+									Status: corev1.ConditionFalse,
+								},
+								{
+									Type:   corev1.ContainersReady,
+									Status: corev1.ConditionTrue,
+								},
 							},
 						},
 					},
 				},
+			},
+			args: args{
+				pod: k8s.PodInfo{
+					Key: types.NamespacedName{Namespace: "default", Name: "my-pod"},
+					UID: "my-pod-uuid",
+					ReadinessGates: []corev1.PodReadinessGate{
+						{
+							ConditionType: "target-health.elbv2.k8s.aws/my-tgb",
+						},
+					},
+					Conditions: []corev1.PodCondition{
+						{
+							Type:   "target-health.elbv2.k8s.aws/my-tgb",
+							Status: corev1.ConditionFalse,
+						},
+						{
+							Type:   corev1.ContainersReady,
+							Status: corev1.ConditionTrue,
+						},
+					},
+				},
 				targetHealth: &elbv2sdk.TargetHealth{
 					State: awssdk.String(elbv2sdk.TargetHealthStateEnumHealthy),
 				},
@@ -128,6 +177,7 @@ func Test_defaultResourceManager_updateTargetHealthPodConditionForPod(t *testing
 				ObjectMeta: metav1.ObjectMeta{
 					Namespace: "default",
 					Name:      "my-pod",
+					UID:       "my-pod-uuid",
 				},
 				Spec: corev1.PodSpec{
 					ReadinessGates: []corev1.PodReadinessGate{
@@ -139,11 +189,11 @@ func Test_defaultResourceManager_updateTargetHealthPodConditionForPod(t *testing
 				Status: corev1.PodStatus{
 					Conditions: []corev1.PodCondition{
 						{
-							Type:   corev1.ContainersReady,
+							Type:   "target-health.elbv2.k8s.aws/my-tgb",
 							Status: corev1.ConditionTrue,
 						},
 						{
-							Type:   "target-health.elbv2.k8s.aws/my-tgb",
+							Type:   corev1.ContainersReady,
 							Status: corev1.ConditionTrue,
 						},
 					},
@@ -151,35 +201,61 @@ func Test_defaultResourceManager_updateTargetHealthPodConditionForPod(t *testing
 			},
 		},
 		{
-			name: "pod contains readinessGate and targetHealth is unhealthy - add pod condition",
-			args: args{
-				pod: &corev1.Pod{
-					ObjectMeta: metav1.ObjectMeta{
-						Namespace: "default",
-						Name:      "my-pod",
-					},
-					Spec: corev1.PodSpec{
-						ReadinessGates: []corev1.PodReadinessGate{
-							{
-								ConditionType: "target-health.elbv2.k8s.aws/my-tgb",
-							},
+			name: "pod contains readinessGate and targetHealth is unhealthy - update pod condition",
+			env: env{
+				pods: []*corev1.Pod{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Namespace: "default",
+							Name:      "my-pod",
+							UID:       "my-pod-uuid",
 						},
-					},
-					Status: corev1.PodStatus{
-						Conditions: []corev1.PodCondition{
-							{
-								Type:   corev1.ContainersReady,
-								Status: corev1.ConditionTrue,
+						Spec: corev1.PodSpec{
+							ReadinessGates: []corev1.PodReadinessGate{
+								{
+									ConditionType: "target-health.elbv2.k8s.aws/my-tgb",
+								},
 							},
-							{
-								Type:    "target-health.elbv2.k8s.aws/my-tgb",
-								Status:  corev1.ConditionFalse,
-								Reason:  elbv2sdk.TargetHealthReasonEnumElbRegistrationInProgress,
-								Message: "Target registration is in progress",
+						},
+						Status: corev1.PodStatus{
+							Conditions: []corev1.PodCondition{
+								{
+									Type:    "target-health.elbv2.k8s.aws/my-tgb",
+									Status:  corev1.ConditionFalse,
+									Reason:  elbv2sdk.TargetHealthReasonEnumElbRegistrationInProgress,
+									Message: "Target registration is in progress",
+								},
+								{
+									Type:   corev1.ContainersReady,
+									Status: corev1.ConditionTrue,
+								},
 							},
 						},
 					},
 				},
+			},
+			args: args{
+				pod: k8s.PodInfo{
+					Key: types.NamespacedName{Namespace: "default", Name: "my-pod"},
+					UID: "my-pod-uuid",
+					ReadinessGates: []corev1.PodReadinessGate{
+						{
+							ConditionType: "target-health.elbv2.k8s.aws/my-tgb",
+						},
+					},
+					Conditions: []corev1.PodCondition{
+						{
+							Type:    "target-health.elbv2.k8s.aws/my-tgb",
+							Status:  corev1.ConditionFalse,
+							Reason:  elbv2sdk.TargetHealthReasonEnumElbRegistrationInProgress,
+							Message: "Target registration is in progress",
+						},
+						{
+							Type:   corev1.ContainersReady,
+							Status: corev1.ConditionTrue,
+						},
+					},
+				},
 				targetHealth: &elbv2sdk.TargetHealth{
 					State:       awssdk.String(elbv2sdk.TargetHealthStateEnumUnhealthy),
 					Reason:      awssdk.String(elbv2sdk.TargetHealthReasonEnumTargetFailedHealthChecks),
@@ -192,6 +268,7 @@ func Test_defaultResourceManager_updateTargetHealthPodConditionForPod(t *testing
 				ObjectMeta: metav1.ObjectMeta{
 					Namespace: "default",
 					Name:      "my-pod",
+					UID:       "my-pod-uuid",
 				},
 				Spec: corev1.PodSpec{
 					ReadinessGates: []corev1.PodReadinessGate{
@@ -202,44 +279,64 @@ func Test_defaultResourceManager_updateTargetHealthPodConditionForPod(t *testing
 				},
 				Status: corev1.PodStatus{
 					Conditions: []corev1.PodCondition{
-						{
-							Type:   corev1.ContainersReady,
-							Status: corev1.ConditionTrue,
-						},
 						{
 							Type:    "target-health.elbv2.k8s.aws/my-tgb",
 							Status:  corev1.ConditionFalse,
 							Reason:  elbv2sdk.TargetHealthReasonEnumTargetFailedHealthChecks,
 							Message: "Health checks failed",
 						},
+						{
+							Type:   corev1.ContainersReady,
+							Status: corev1.ConditionTrue,
+						},
 					},
 				},
 			},
 		},
 		{
-			name: "pod contains readinessGate and targetHealth is nil - currentPod has no condition",
-			args: args{
-				pod: &corev1.Pod{
-					ObjectMeta: metav1.ObjectMeta{
-						Namespace: "default",
-						Name:      "my-pod",
-					},
-					Spec: corev1.PodSpec{
-						ReadinessGates: []corev1.PodReadinessGate{
-							{
-								ConditionType: "target-health.elbv2.k8s.aws/my-tgb",
+			name: "pod contains readinessGate and targetHealth is nil",
+			env: env{
+				pods: []*corev1.Pod{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Namespace: "default",
+							Name:      "my-pod",
+							UID:       "my-pod-uuid",
+						},
+						Spec: corev1.PodSpec{
+							ReadinessGates: []corev1.PodReadinessGate{
+								{
+									ConditionType: "target-health.elbv2.k8s.aws/my-tgb",
+								},
 							},
 						},
-					},
-					Status: corev1.PodStatus{
-						Conditions: []corev1.PodCondition{
-							{
-								Type:   corev1.ContainersReady,
-								Status: corev1.ConditionTrue,
+						Status: corev1.PodStatus{
+							Conditions: []corev1.PodCondition{
+								{
+									Type:   corev1.ContainersReady,
+									Status: corev1.ConditionTrue,
+								},
 							},
 						},
 					},
 				},
+			},
+			args: args{
+				pod: k8s.PodInfo{
+					Key: types.NamespacedName{Namespace: "default", Name: "my-pod"},
+					UID: "my-pod-uuid",
+					ReadinessGates: []corev1.PodReadinessGate{
+						{
+							ConditionType: "target-health.elbv2.k8s.aws/my-tgb",
+						},
+					},
+					Conditions: []corev1.PodCondition{
+						{
+							Type:   corev1.ContainersReady,
+							Status: corev1.ConditionTrue,
+						},
+					},
+				},
 				targetHealth:         nil,
 				targetHealthCondType: "target-health.elbv2.k8s.aws/my-tgb",
 			},
@@ -248,6 +345,7 @@ func Test_defaultResourceManager_updateTargetHealthPodConditionForPod(t *testing
 				ObjectMeta: metav1.ObjectMeta{
 					Namespace: "default",
 					Name:      "my-pod",
+					UID:       "my-pod-uuid",
 				},
 				Spec: corev1.PodSpec{
 					ReadinessGates: []corev1.PodReadinessGate{
@@ -258,14 +356,14 @@ func Test_defaultResourceManager_updateTargetHealthPodConditionForPod(t *testing
 				},
 				Status: corev1.PodStatus{
 					Conditions: []corev1.PodCondition{
-						{
-							Type:   corev1.ContainersReady,
-							Status: corev1.ConditionTrue,
-						},
 						{
 							Type:   "target-health.elbv2.k8s.aws/my-tgb",
 							Status: corev1.ConditionUnknown,
 						},
+						{
+							Type:   corev1.ContainersReady,
+							Status: corev1.ConditionTrue,
+						},
 					},
 				},
 			},
@@ -283,12 +381,13 @@ func Test_defaultResourceManager_updateTargetHealthPodConditionForPod(t *testing
 			}
 
 			ctx := context.Background()
-			pod := tt.args.pod.DeepCopy()
-			err := k8sClient.Create(ctx, pod)
-			assert.NoError(t, err)
+			for _, pod := range tt.env.pods {
+				err := k8sClient.Create(ctx, pod.DeepCopy())
+				assert.NoError(t, err)
+			}
 
 			got, err := m.updateTargetHealthPodConditionForPod(context.Background(),
-				pod, tt.args.targetHealth, tt.args.targetHealthCondType)
+				tt.args.pod, tt.args.targetHealth, tt.args.targetHealthCondType)
 			if tt.wantErr != nil {
 				assert.EqualError(t, err, tt.wantErr.Error())
 			} else {
@@ -296,7 +395,7 @@ func Test_defaultResourceManager_updateTargetHealthPodConditionForPod(t *testing
 				assert.Equal(t, tt.want, got)
 
 				updatedPod := &corev1.Pod{}
-				err := m.k8sClient.Get(ctx, k8s.NamespacedName(pod), updatedPod)
+				err := m.k8sClient.Get(ctx, tt.args.pod.Key, updatedPod)
 				assert.NoError(t, err)
 
 				opts := cmp.Options{
@@ -358,3 +457,46 @@ func Test_containsTargetsInInitialState(t *testing.T) {
 		})
 	}
 }
+
+func Test_buildPodConditionPatch(t *testing.T) {
+	type args struct {
+		pod       k8s.PodInfo
+		condition corev1.PodCondition
+	}
+	tests := []struct {
+		name      string
+		args      args
+		wantPatch []byte
+		wantErr   error
+	}{
+		{
+			name: "standard case",
+			args: args{
+				pod: k8s.PodInfo{
+					Key: types.NamespacedName{Namespace: "ns-1", Name: "pod-1"},
+					UID: "pod-uuid",
+				},
+				condition: corev1.PodCondition{
+					Type:    "custom-condition",
+					Status:  corev1.ConditionTrue,
+					Reason:  "some-reason",
+					Message: "some-msg",
+				},
+			},
+			wantPatch: []byte(`{"metadata":{"uid":"pod-uuid"},"status":{"conditions":[{"lastProbeTime":null,"lastTransitionTime":null,"message":"some-msg","reason":"some-reason","status":"True","type":"custom-condition"}]}}`),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := buildPodConditionPatch(tt.args.pod, tt.args.condition)
+			if tt.wantErr != nil {
+				assert.EqualError(t, err, tt.wantErr.Error())
+			} else {
+				assert.NoError(t, err)
+				gotPatch, _ := got.Data(nil)
+				assert.Equal(t, tt.wantPatch, gotPatch)
+				assert.Equal(t, types.StrategicMergePatchType, got.Type())
+			}
+		})
+	}
+}