Repository: kubernetes-sigs/aws-alb-ingress-controller
Pull request #1404 (2020-09-11 00:11:33)

add eni & securityGroup utilities
Add ENIResolver & SecurityGroup handling utilities.
1. PodENIInfoResolver is responsible for resolve eni for pods.
    * It first resolve eni from cache. (indexed with podKey & podUID)
    * then it tries to resolve via annotations on pod setup by branch eni support
    * then it tries to resolve via ipAddress in vpc.
2. NodeENIInfoResolver is responsible for resolve eni for nodes.
    * It first resolve eni from cache. (indexed with nodeKey & nodeUID)
    * then it tries to resolve via instanceID.
3. SecurityGroupInfo is a wrapper around securityGroup settings.
    * it breaks grouped IPPermission.
    * it computes a abstract *labels* from descriptions, which allow us to specify the scope of rule to manage.

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/1404#" title="Author self-approved">M00nF1sh</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign 
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/v2_ga/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":[]} -->

Patch:
diff --git a/controllers/service/service_controller.go b/controllers/service/service_controller.go
index afb11d30d..2eb497b18 100644
--- a/controllers/service/service_controller.go
+++ b/controllers/service/service_controller.go
@@ -5,63 +5,128 @@ import (
 	"github.com/go-logr/logr"
 	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
-	k8serr "k8s.io/apimachinery/pkg/api/errors"
 	"sigs.k8s.io/aws-alb-ingress-controller/controllers/service/eventhandlers"
 	"sigs.k8s.io/aws-alb-ingress-controller/pkg/annotations"
+	"sigs.k8s.io/aws-alb-ingress-controller/pkg/aws/services"
+	"sigs.k8s.io/aws-alb-ingress-controller/pkg/deploy"
 	"sigs.k8s.io/aws-alb-ingress-controller/pkg/k8s"
+	"sigs.k8s.io/aws-alb-ingress-controller/pkg/model/core"
+	elbv2model "sigs.k8s.io/aws-alb-ingress-controller/pkg/model/elbv2"
+	"sigs.k8s.io/aws-alb-ingress-controller/pkg/networking"
+	"sigs.k8s.io/aws-alb-ingress-controller/pkg/runtime"
+	"sigs.k8s.io/aws-alb-ingress-controller/pkg/service/nlb"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/controller"
-	"sigs.k8s.io/controller-runtime/pkg/reconcile"
 	"sigs.k8s.io/controller-runtime/pkg/source"
 )
 
 const (
 	LoadBalancerFinalizer   = "service.k8s.aws/load-balancer-finalizer"
+	DefaultTagPrefix        = "service.k8s.aws"
 	ServiceAnnotationPrefix = "service.beta.kubernetes.io"
 	controllerName          = "service"
 )
 
-func NewServiceReconciler(k8sClient client.Client, log logr.Logger) *ServiceReconciler {
+func NewServiceReconciler(k8sClient client.Client, elbv2Client services.ELBV2, vpcId string, clusterName string, resolver networking.SubnetsResolver, log logr.Logger) *ServiceReconciler {
 	return &ServiceReconciler{
 		k8sClient:        k8sClient,
+		elbv2Client:      elbv2Client,
+		vpcID:            vpcId,
+		clusterName:      clusterName,
 		log:              log,
 		annotationParser: annotations.NewSuffixAnnotationParser(ServiceAnnotationPrefix),
 		finalizerManager: k8s.NewDefaultFinalizerManager(k8sClient, log),
+		subnetsResolver:  resolver,
 	}
 }
 
 type ServiceReconciler struct {
 	k8sClient        client.Client
+	elbv2Client      services.ELBV2
+	vpcID            string
+	clusterName      string
 	log              logr.Logger
 	annotationParser annotations.Parser
 	finalizerManager k8s.FinalizerManager
+	subnetsResolver  networking.SubnetsResolver
 }
 
 // +kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;create;update;patch;delete
 // +kubebuilder:rbac:groups="",resources=services/status,verbs=get;update;patch
 func (r *ServiceReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
+	return runtime.HandleReconcileError(r.reconcile(req), r.log)
+}
+
+func (r *ServiceReconciler) reconcile(req ctrl.Request) error {
 	ctx := context.Background()
-	_ = r.log.WithValues("service", req.NamespacedName)
 	svc := &corev1.Service{}
 	if err := r.k8sClient.Get(ctx, req.NamespacedName, svc); err != nil {
-		if k8serr.IsNotFound(err) {
-			return reconcile.Result{}, nil
-		}
-		return reconcile.Result{}, err
+		return client.IgnoreNotFound(err)
+	}
+	if !svc.DeletionTimestamp.IsZero() {
+		return r.cleanupLoadBalancerResources(ctx, svc)
+	}
+	return r.reconcileLoadBalancerResources(ctx, svc)
+}
+
+func (r *ServiceReconciler) buildAndDeployModel(ctx context.Context, svc *corev1.Service) (core.Stack, error) {
+	nlbBuilder := nlb.NewServiceBuilder(svc, r.subnetsResolver, k8s.NamespacedName(svc), r.annotationParser)
+	stack, err := nlbBuilder.Build(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	d := deploy.NewDefaultStackMarshaller()
+	jsonString, err := d.Marshal(stack)
+	r.log.Info("Built service model", "stack", jsonString)
+
+	deployer := deploy.NewDefaultStackDeployer(r.k8sClient, r.elbv2Client, r.vpcID, r.clusterName, DefaultTagPrefix, r.log)
+	err = deployer.Deploy(ctx, stack)
+	if err != nil {
+		return nil, err
 	}
+	r.log.Info("Successfully deployed service resources")
+	return stack, nil
+}
+
+func (r *ServiceReconciler) reconcileLoadBalancerResources(ctx context.Context, svc *corev1.Service) error {
 	if err := r.finalizerManager.AddFinalizers(ctx, svc, LoadBalancerFinalizer); err != nil {
-		return reconcile.Result{}, err
+		return err
 	}
-	// TODO: Build & Deploy model
-	// TODO: Update status
+	stack, err := r.buildAndDeployModel(ctx, svc)
+	if err != nil {
+		return err
+	}
+	var resLBs []*elbv2model.LoadBalancer
+	stack.ListResources(&resLBs)
+	if len(resLBs) == 0 {
+		r.log.Info("Unable to list LoadBalancer resource")
+	} else {
+		var resTGs []*elbv2model.TargetGroup
+		resLB := resLBs[0]
+		dnsName, _ := resLB.DNSName().Resolve(ctx)
+		stack.ListResources(&resTGs)
+		r.log.Info("Deployed LoadBalancer", "dnsname", dnsName, "#target groups", len(resTGs))
+		err = r.updateServiceStatus(ctx, svc, dnsName)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
 
-	if !svc.DeletionTimestamp.IsZero() {
+func (r *ServiceReconciler) cleanupLoadBalancerResources(ctx context.Context, svc *corev1.Service) error {
+	if k8s.HasFinalizer(svc, LoadBalancerFinalizer) {
+		_, err := r.buildAndDeployModel(ctx, svc)
+		if err != nil {
+			return err
+		}
 		if err := r.finalizerManager.RemoveFinalizers(ctx, svc, LoadBalancerFinalizer); err != nil {
-			return reconcile.Result{}, err
+			return err
 		}
 	}
-	return ctrl.Result{}, nil
+	return nil
 }
 
 func (r *ServiceReconciler) updateServiceStatus(ctx context.Context, svc *corev1.Service, lbDNS string) error {
diff --git a/main.go b/main.go
index ba9abe516..c88052c18 100644
--- a/main.go
+++ b/main.go
@@ -29,6 +29,7 @@ import (
 	"sigs.k8s.io/aws-alb-ingress-controller/pkg/aws"
 	"sigs.k8s.io/aws-alb-ingress-controller/pkg/aws/throttle"
 	"sigs.k8s.io/aws-alb-ingress-controller/pkg/k8s"
+	"sigs.k8s.io/aws-alb-ingress-controller/pkg/networking"
 	"sigs.k8s.io/aws-alb-ingress-controller/pkg/targetgroupbinding"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/log/zap"
@@ -118,6 +119,10 @@ func main() {
 	}
 	if err = (service.NewServiceReconciler(
 		mgr.GetClient(),
+		cloud.ELBV2(),
+		cloud.VpcID(),
+		k8sClusterName,
+		networking.NewSubnetsResolver(cloud.EC2(), cloud.VpcID(), k8sClusterName, ctrl.Log.WithName("subnets-resolver")),
 		ctrl.Log.WithName("controllers").WithName("Service"),
 	)).SetupWithManager(mgr); err != nil {
 		setupLog.Error(err, "Unable to create controller", "controller", "Service")
diff --git a/mocks/aws/services/mock_ec2.go b/mocks/aws/services/mock_ec2.go
index 89ff7520f..8ec46d31c 100644
--- a/mocks/aws/services/mock_ec2.go
+++ b/mocks/aws/services/mock_ec2.go
@@ -10508,6 +10508,21 @@ func (mr *MockEC2MockRecorder) DescribeInstances(arg0 interface{}) *gomock.Call
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeInstances", reflect.TypeOf((*MockEC2)(nil).DescribeInstances), arg0)
 }
 
+// DescribeInstancesAsList mocks base method
+func (m *MockEC2) DescribeInstancesAsList(arg0 context.Context, arg1 *ec2.DescribeInstancesInput) ([]*ec2.Instance, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "DescribeInstancesAsList", arg0, arg1)
+	ret0, _ := ret[0].([]*ec2.Instance)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// DescribeInstancesAsList indicates an expected call of DescribeInstancesAsList
+func (mr *MockEC2MockRecorder) DescribeInstancesAsList(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeInstancesAsList", reflect.TypeOf((*MockEC2)(nil).DescribeInstancesAsList), arg0, arg1)
+}
+
 // DescribeInstancesPages mocks base method
 func (m *MockEC2) DescribeInstancesPages(arg0 *ec2.DescribeInstancesInput, arg1 func(*ec2.DescribeInstancesOutput, bool) bool) error {
 	m.ctrl.T.Helper()
@@ -11936,6 +11951,21 @@ func (mr *MockEC2MockRecorder) DescribeNetworkInterfaces(arg0 interface{}) *gomo
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeNetworkInterfaces", reflect.TypeOf((*MockEC2)(nil).DescribeNetworkInterfaces), arg0)
 }
 
+// DescribeNetworkInterfacesAsList mocks base method
+func (m *MockEC2) DescribeNetworkInterfacesAsList(arg0 context.Context, arg1 *ec2.DescribeNetworkInterfacesInput) ([]*ec2.NetworkInterface, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "DescribeNetworkInterfacesAsList", arg0, arg1)
+	ret0, _ := ret[0].([]*ec2.NetworkInterface)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// DescribeNetworkInterfacesAsList indicates an expected call of DescribeNetworkInterfacesAsList
+func (mr *MockEC2MockRecorder) DescribeNetworkInterfacesAsList(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeNetworkInterfacesAsList", reflect.TypeOf((*MockEC2)(nil).DescribeNetworkInterfacesAsList), arg0, arg1)
+}
+
 // DescribeNetworkInterfacesPages mocks base method
 func (m *MockEC2) DescribeNetworkInterfacesPages(arg0 *ec2.DescribeNetworkInterfacesInput, arg1 func(*ec2.DescribeNetworkInterfacesOutput, bool) bool) error {
 	m.ctrl.T.Helper()
@@ -12933,6 +12963,21 @@ func (mr *MockEC2MockRecorder) DescribeSecurityGroups(arg0 interface{}) *gomock.
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeSecurityGroups", reflect.TypeOf((*MockEC2)(nil).DescribeSecurityGroups), arg0)
 }
 
+// DescribeSecurityGroupsAsList mocks base method
+func (m *MockEC2) DescribeSecurityGroupsAsList(arg0 context.Context, arg1 *ec2.DescribeSecurityGroupsInput) ([]*ec2.SecurityGroup, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "DescribeSecurityGroupsAsList", arg0, arg1)
+	ret0, _ := ret[0].([]*ec2.SecurityGroup)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// DescribeSecurityGroupsAsList indicates an expected call of DescribeSecurityGroupsAsList
+func (mr *MockEC2MockRecorder) DescribeSecurityGroupsAsList(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeSecurityGroupsAsList", reflect.TypeOf((*MockEC2)(nil).DescribeSecurityGroupsAsList), arg0, arg1)
+}
+
 // DescribeSecurityGroupsPages mocks base method
 func (m *MockEC2) DescribeSecurityGroupsPages(arg0 *ec2.DescribeSecurityGroupsInput, arg1 func(*ec2.DescribeSecurityGroupsOutput, bool) bool) error {
 	m.ctrl.T.Helper()
diff --git a/pkg/aws/services/ec2.go b/pkg/aws/services/ec2.go
index 8186eb478..3a6eb3562 100644
--- a/pkg/aws/services/ec2.go
+++ b/pkg/aws/services/ec2.go
@@ -1,6 +1,7 @@
 package services
 
 import (
+	"context"
 	"github.com/aws/aws-sdk-go/aws/session"
 	"github.com/aws/aws-sdk-go/service/ec2"
 	"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
@@ -8,6 +9,15 @@ import (
 
 type EC2 interface {
 	ec2iface.EC2API
+
+	// wrapper to DescribeInstancesPagesWithContext API, which aggregates paged results into list.
+	DescribeInstancesAsList(ctx context.Context, input *ec2.DescribeInstancesInput) ([]*ec2.Instance, error)
+
+	// wrapper to DescribeNetworkInterfacesPagesWithContext API, which aggregates paged results into list.
+	DescribeNetworkInterfacesAsList(ctx context.Context, input *ec2.DescribeNetworkInterfacesInput) ([]*ec2.NetworkInterface, error)
+
+	// wrapper to DescribeSecurityGroupsPagesWithContext API, which aggregates paged results into list.
+	DescribeSecurityGroupsAsList(ctx context.Context, input *ec2.DescribeSecurityGroupsInput) ([]*ec2.SecurityGroup, error)
 }
 
 // NewEC2 constructs new EC2 implementation.
@@ -20,3 +30,38 @@ func NewEC2(session *session.Session) EC2 {
 type defaultEC2 struct {
 	ec2iface.EC2API
 }
+
+func (c *defaultEC2) DescribeInstancesAsList(ctx context.Context, input *ec2.DescribeInstancesInput) ([]*ec2.Instance, error) {
+	var result []*ec2.Instance
+	if err := c.DescribeInstancesPagesWithContext(ctx, input, func(output *ec2.DescribeInstancesOutput, _ bool) bool {
+		for _, reservation := range output.Reservations {
+			result = append(result, reservation.Instances...)
+		}
+		return true
+	}); err != nil {
+		return nil, err
+	}
+	return result, nil
+}
+
+func (c *defaultEC2) DescribeNetworkInterfacesAsList(ctx context.Context, input *ec2.DescribeNetworkInterfacesInput) ([]*ec2.NetworkInterface, error) {
+	var result []*ec2.NetworkInterface
+	if err := c.DescribeNetworkInterfacesPagesWithContext(ctx, input, func(output *ec2.DescribeNetworkInterfacesOutput, _ bool) bool {
+		result = append(result, output.NetworkInterfaces...)
+		return true
+	}); err != nil {
+		return nil, err
+	}
+	return result, nil
+}
+
+func (c *defaultEC2) DescribeSecurityGroupsAsList(ctx context.Context, input *ec2.DescribeSecurityGroupsInput) ([]*ec2.SecurityGroup, error) {
+	var result []*ec2.SecurityGroup
+	if err := c.DescribeSecurityGroupsPagesWithContext(ctx, input, func(output *ec2.DescribeSecurityGroupsOutput, _ bool) bool {
+		result = append(result, output.SecurityGroups...)
+		return true
+	}); err != nil {
+		return nil, err
+	}
+	return result, nil
+}
diff --git a/pkg/backend/endpoint_resolver.go b/pkg/backend/endpoint_resolver.go
index b1904a67f..460353ec2 100644
--- a/pkg/backend/endpoint_resolver.go
+++ b/pkg/backend/endpoint_resolver.go
@@ -10,7 +10,6 @@ import (
 	"k8s.io/apimachinery/pkg/util/intstr"
 	"sigs.k8s.io/aws-alb-ingress-controller/pkg/k8s"
 	"sigs.k8s.io/controller-runtime/pkg/client"
-	"strings"
 )
 
 // EndpointResolver resolves the endpoints for specific service & service Port.
@@ -118,7 +117,7 @@ func (r *defaultEndpointResolver) ResolveNodePortEndpoints(ctx context.Context,
 		if !k8s.IsNodeReady(node) {
 			continue
 		}
-		instanceID, err := extractNodeInstanceID(node)
+		instanceID, err := k8s.ExtractNodeInstanceID(node)
 		if err != nil {
 			return nil, err
 		}
@@ -159,16 +158,6 @@ func isPodMeetCriteria(pod *corev1.Pod, criteria []PodPredicate) bool {
 	return true
 }
 
-func extractNodeInstanceID(node *corev1.Node) (string, error) {
-	providerID := node.Spec.ProviderID
-	if providerID == "" {
-		return "", errors.Errorf("providerID is not specified for node: %s", node.Name)
-	}
-
-	p := strings.Split(providerID, "/")
-	return p[len(p)-1], nil
-}
-
 func buildPodEndpoint(pod *corev1.Pod, epAddr corev1.EndpointAddress, epPort corev1.EndpointPort) PodEndpoint {
 	return PodEndpoint{
 		IP:   epAddr.IP,
diff --git a/pkg/deploy/elbv2/listener_manager.go b/pkg/deploy/elbv2/listener_manager.go
index 202b486e7..36d7e26c4 100644
--- a/pkg/deploy/elbv2/listener_manager.go
+++ b/pkg/deploy/elbv2/listener_manager.go
@@ -2,9 +2,7 @@ package elbv2
 
 import (
 	"context"
-	"fmt"
 	awssdk "github.com/aws/aws-sdk-go/aws"
-	"github.com/aws/aws-sdk-go/aws/awsutil"
 	elbv2sdk "github.com/aws/aws-sdk-go/service/elbv2"
 	"github.com/go-logr/logr"
 	"github.com/google/go-cmp/cmp"
@@ -208,19 +206,15 @@ func isSDKListenerSettingsDrifted(lsSpec elbv2model.ListenerSpec, sdkLS *elbv2sd
 		return true
 	}
 	if !cmp.Equal(desiredDefaultActions, sdkLS.DefaultActions, elbv2equality.CompareOptionForActions()) {
-		fmt.Println("case 3", awsutil.Prettify(desiredDefaultActions), awsutil.Prettify(sdkLS.DefaultActions))
 		return true
 	}
 	if !cmp.Equal(desiredDefaultCerts, sdkLS.Certificates, elbv2equality.CompareOptionForCertificates()) {
-		fmt.Println("case 3")
 		return true
 	}
 	if lsSpec.SSLPolicy != nil && awssdk.StringValue(lsSpec.SSLPolicy) != awssdk.StringValue(sdkLS.SslPolicy) {
-		fmt.Println("case 4")
 		return true
 	}
 	if !cmp.Equal(lsSpec.ALPNPolicy, awssdk.StringValueSlice(sdkLS.AlpnPolicy), cmpopts.EquateEmpty()) {
-		fmt.Println("case 5")
 		return true
 	}
 
diff --git a/pkg/deploy/elbv2/listener_rule_manager.go b/pkg/deploy/elbv2/listener_rule_manager.go
new file mode 100644
index 000000000..6829a9e83
--- /dev/null
+++ b/pkg/deploy/elbv2/listener_rule_manager.go
@@ -0,0 +1,148 @@
+package elbv2
+
+import (
+	"context"
+	awssdk "github.com/aws/aws-sdk-go/aws"
+	elbv2sdk "github.com/aws/aws-sdk-go/service/elbv2"
+	"github.com/go-logr/logr"
+	"github.com/google/go-cmp/cmp"
+	"sigs.k8s.io/aws-alb-ingress-controller/pkg/aws/services"
+	elbv2equality "sigs.k8s.io/aws-alb-ingress-controller/pkg/equality/elbv2"
+	elbv2model "sigs.k8s.io/aws-alb-ingress-controller/pkg/model/elbv2"
+)
+
+// ListenerRuleManager is responsible for create/update/delete ListenerRule resources.
+type ListenerRuleManager interface {
+	Create(ctx context.Context, resLR *elbv2model.ListenerRule) (elbv2model.ListenerRuleStatus, error)
+
+	Update(ctx context.Context, resLR *elbv2model.ListenerRule, sdkLR *elbv2sdk.Rule) (elbv2model.ListenerRuleStatus, error)
+
+	Delete(ctx context.Context, sdkLR *elbv2sdk.Rule) error
+}
+
+// NewDefaultListenerRuleManager constructs new defaultListenerRuleManager.
+func NewDefaultListenerRuleManager(elbv2Client services.ELBV2, logger logr.Logger) *defaultListenerRuleManager {
+	return &defaultListenerRuleManager{
+		elbv2Client: elbv2Client,
+		logger:      logger,
+	}
+}
+
+// default implementation for ListenerRuleManager.
+type defaultListenerRuleManager struct {
+	elbv2Client services.ELBV2
+	logger      logr.Logger
+}
+
+func (m *defaultListenerRuleManager) Create(ctx context.Context, resLR *elbv2model.ListenerRule) (elbv2model.ListenerRuleStatus, error) {
+	req, err := buildSDKCreateListenerRuleInput(resLR.Spec)
+	if err != nil {
+		return elbv2model.ListenerRuleStatus{}, err
+	}
+
+	m.logger.Info("creating listener rule",
+		"stackID", resLR.Stack().StackID(),
+		"resourceID", resLR.ID())
+	resp, err := m.elbv2Client.CreateRuleWithContext(ctx, req)
+	if err != nil {
+		return elbv2model.ListenerRuleStatus{}, err
+	}
+	sdkLR := resp.Rules[0]
+	m.logger.Info("created listener rule",
+		"stackID", resLR.Stack().StackID(),
+		"resourceID", resLR.ID(),
+		"arn", awssdk.StringValue(sdkLR.RuleArn))
+
+	return buildResListenerRuleStatus(sdkLR), nil
+}
+
+func (m *defaultListenerRuleManager) Update(ctx context.Context, resLR *elbv2model.ListenerRule, sdkLR *elbv2sdk.Rule) (elbv2model.ListenerRuleStatus, error) {
+	if err := m.updateSDKListenerRuleWithSettings(ctx, resLR, sdkLR); err != nil {
+		return elbv2model.ListenerRuleStatus{}, err
+	}
+	return buildResListenerRuleStatus(sdkLR), nil
+}
+
+func (m *defaultListenerRuleManager) Delete(ctx context.Context, sdkLR *elbv2sdk.Rule) error {
+	req := &elbv2sdk.DeleteRuleInput{
+		RuleArn: sdkLR.RuleArn,
+	}
+	m.logger.Info("deleting listener rule",
+		"arn", awssdk.StringValue(req.RuleArn))
+	if _, err := m.elbv2Client.DeleteRuleWithContext(ctx, req); err != nil {
+		return err
+	}
+	m.logger.Info("deleted listener rule",
+		"arn", awssdk.StringValue(req.RuleArn))
+	return nil
+}
+
+func (m *defaultListenerRuleManager) updateSDKListenerRuleWithSettings(ctx context.Context, resLR *elbv2model.ListenerRule, sdkLR *elbv2sdk.Rule) error {
+	desiredActions, err := buildSDKActions(resLR.Spec.Actions)
+	if err != nil {
+		return err
+	}
+	desiredConditions := buildSDKRuleConditions(resLR.Spec.Conditions)
+	if !isSDKListenerRuleSettingsDrifted(resLR.Spec, sdkLR, desiredActions, desiredConditions) {
+		return nil
+	}
+
+	req := buildSDKModifyListenerRuleInput(resLR.Spec, desiredActions, desiredConditions)
+	req.RuleArn = sdkLR.RuleArn
+	m.logger.Info("modifying listener rule",
+		"stackID", resLR.Stack().StackID(),
+		"resourceID", resLR.ID(),
+		"arn", awssdk.StringValue(sdkLR.RuleArn))
+	if _, err := m.elbv2Client.ModifyRuleWithContext(ctx, req); err != nil {
+		return err
+	}
+	m.logger.Info("modified listener rule",
+		"stackID", resLR.Stack().StackID(),
+		"resourceID", resLR.ID(),
+		"arn", awssdk.StringValue(sdkLR.RuleArn))
+	return nil
+}
+
+func isSDKListenerRuleSettingsDrifted(lrSpec elbv2model.ListenerRuleSpec, sdkLR *elbv2sdk.Rule,
+	desiredActions []*elbv2sdk.Action, desiredConditions []*elbv2sdk.RuleCondition) bool {
+
+	if !cmp.Equal(desiredActions, sdkLR.Actions, elbv2equality.CompareOptionForActions()) {
+		return true
+	}
+	if !cmp.Equal(desiredConditions, sdkLR.Conditions, elbv2equality.CompareOptionForRuleConditions()) {
+		return true
+	}
+
+	return false
+}
+
+func buildSDKCreateListenerRuleInput(lrSpec elbv2model.ListenerRuleSpec) (*elbv2sdk.CreateRuleInput, error) {
+	ctx := context.Background()
+	lsARN, err := lrSpec.ListenerARN.Resolve(ctx)
+	if err != nil {
+		return nil, err
+	}
+	sdkObj := &elbv2sdk.CreateRuleInput{}
+	sdkObj.ListenerArn = awssdk.String(lsARN)
+	sdkObj.Priority = awssdk.Int64(lrSpec.Priority)
+	actions, err := buildSDKActions(lrSpec.Actions)
+	if err != nil {
+		return nil, err
+	}
+	sdkObj.Actions = actions
+	sdkObj.Conditions = buildSDKRuleConditions(lrSpec.Conditions)
+	return sdkObj, nil
+}
+
+func buildSDKModifyListenerRuleInput(_ elbv2model.ListenerRuleSpec, desiredActions []*elbv2sdk.Action, desiredConditions []*elbv2sdk.RuleCondition) *elbv2sdk.ModifyRuleInput {
+	sdkObj := &elbv2sdk.ModifyRuleInput{}
+	sdkObj.Actions = desiredActions
+	sdkObj.Conditions = desiredConditions
+	return sdkObj
+}
+
+func buildResListenerRuleStatus(sdkLR *elbv2sdk.Rule) elbv2model.ListenerRuleStatus {
+	return elbv2model.ListenerRuleStatus{
+		RuleARN: awssdk.StringValue(sdkLR.RuleArn),
+	}
+}
diff --git a/pkg/deploy/elbv2/listener_rule_synthesizer.go b/pkg/deploy/elbv2/listener_rule_synthesizer.go
new file mode 100644
index 000000000..cb499bc55
--- /dev/null
+++ b/pkg/deploy/elbv2/listener_rule_synthesizer.go
@@ -0,0 +1,169 @@
+package elbv2
+
+import (
+	"context"
+	awssdk "github.com/aws/aws-sdk-go/aws"
+	elbv2sdk "github.com/aws/aws-sdk-go/service/elbv2"
+	"github.com/go-logr/logr"
+	"k8s.io/apimachinery/pkg/util/sets"
+	"sigs.k8s.io/aws-alb-ingress-controller/pkg/aws/services"
+	"sigs.k8s.io/aws-alb-ingress-controller/pkg/model/core"
+	elbv2model "sigs.k8s.io/aws-alb-ingress-controller/pkg/model/elbv2"
+	"strconv"
+)
+
+// NewListenerRuleSynthesizer constructs new listenerRuleSynthesizer.
+func NewListenerRuleSynthesizer(elbv2Client services.ELBV2, lrManager ListenerRuleManager, logger logr.Logger, stack core.Stack) *listenerRuleSynthesizer {
+	return &listenerRuleSynthesizer{
+		elbv2Client: elbv2Client,
+		lrManager:   lrManager,
+		logger:      logger,
+		stack:       stack,
+	}
+}
+
+type listenerRuleSynthesizer struct {
+	elbv2Client services.ELBV2
+	lrManager   ListenerRuleManager
+	logger      logr.Logger
+
+	stack core.Stack
+}
+
+func (s *listenerRuleSynthesizer) Synthesize(ctx context.Context) error {
+	var resLRs []*elbv2model.ListenerRule
+	s.stack.ListResources(&resLRs)
+	resLRsByLSARN, err := mapResListenerRuleByListenerARN(resLRs)
+	if err != nil {
+		return err
+	}
+
+	var resLSs []*elbv2model.Listener
+	s.stack.ListResources(&resLSs)
+	for _, resLS := range resLSs {
+		lsARN, err := resLS.ListenerARN().Resolve(ctx)
+		if err != nil {
+			return err
+		}
+		resLRs := resLRsByLSARN[lsARN]
+		if err := s.synthesizeListenerRulesOnListener(ctx, lsARN, resLRs); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (s *listenerRuleSynthesizer) PostSynthesize(ctx context.Context) error {
+	// nothing to do here.
+	return nil
+}
+
+func (s *listenerRuleSynthesizer) synthesizeListenerRulesOnListener(ctx context.Context, lsARN string, resLRs []*elbv2model.ListenerRule) error {
+	sdkLRs, err := s.findSDKListenersRulesOnLS(ctx, lsARN)
+	if err != nil {
+		return err
+	}
+
+	matchedResAndSDKLRs, unmatchedResLRs, unmatchedSDKLRs := matchResAndSDKListenerRules(resLRs, sdkLRs)
+	for _, sdkLR := range unmatchedSDKLRs {
+		if err := s.lrManager.Delete(ctx, sdkLR); err != nil {
+			return err
+		}
+	}
+	for _, resLR := range unmatchedResLRs {
+		lrStatus, err := s.lrManager.Create(ctx, resLR)
+		if err != nil {
+			return err
+		}
+		resLR.SetStatus(lrStatus)
+	}
+	for _, resAndSDKLR := range matchedResAndSDKLRs {
+		lsStatus, err := s.lrManager.Update(ctx, resAndSDKLR.resLR, resAndSDKLR.sdkLR)
+		if err != nil {
+			return err
+		}
+		resAndSDKLR.resLR.SetStatus(lsStatus)
+	}
+	return nil
+}
+
+// findSDKListenersRulesOnLS returns the listenerRules configured on Listener.
+func (s *listenerRuleSynthesizer) findSDKListenersRulesOnLS(ctx context.Context, lsARN string) ([]*elbv2sdk.Rule, error) {
+	req := &elbv2sdk.DescribeRulesInput{
+		ListenerArn: awssdk.String(lsARN),
+	}
+	rules, err := s.elbv2Client.DescribeRulesAsList(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+	nonDefaultRules := make([]*elbv2sdk.Rule, 0, len(rules))
+	for _, rule := range rules {
+		if awssdk.BoolValue(rule.IsDefault) {
+			continue
+		}
+		nonDefaultRules = append(nonDefaultRules, rule)
+	}
+	return nonDefaultRules, nil
+}
+
+type resAndSDKListenerRulePair struct {
+	resLR *elbv2model.ListenerRule
+	sdkLR *elbv2sdk.Rule
+}
+
+func matchResAndSDKListenerRules(resLRs []*elbv2model.ListenerRule, sdkLRs []*elbv2sdk.Rule) ([]resAndSDKListenerRulePair, []*elbv2model.ListenerRule, []*elbv2sdk.Rule) {
+	var matchedResAndSDKLRs []resAndSDKListenerRulePair
+	var unmatchedResLRs []*elbv2model.ListenerRule
+	var unmatchedSDKLRs []*elbv2sdk.Rule
+
+	resLRByPriority := mapResListenerRuleByPriority(resLRs)
+	sdkLRByPriority := mapSDKListenerRuleByPriority(sdkLRs)
+	resLRPriorities := sets.Int64KeySet(resLRByPriority)
+	sdkLRPriorities := sets.Int64KeySet(sdkLRByPriority)
+	for _, priority := range resLRPriorities.Intersection(sdkLRPriorities).List() {
+		resLR := resLRByPriority[priority]
+		sdkLR := sdkLRByPriority[priority]
+		matchedResAndSDKLRs = append(matchedResAndSDKLRs, resAndSDKListenerRulePair{
+			resLR: resLR,
+			sdkLR: sdkLR,
+		})
+	}
+	for _, priority := range resLRPriorities.Difference(sdkLRPriorities).List() {
+		unmatchedResLRs = append(unmatchedResLRs, resLRByPriority[priority])
+	}
+	for _, priority := range sdkLRPriorities.Difference(resLRPriorities).List() {
+		unmatchedSDKLRs = append(unmatchedSDKLRs, sdkLRByPriority[priority])
+	}
+
+	return matchedResAndSDKLRs, unmatchedResLRs, unmatchedSDKLRs
+}
+
+func mapResListenerRuleByPriority(resLRs []*elbv2model.ListenerRule) map[int64]*elbv2model.ListenerRule {
+	resLRByPriority := make(map[int64]*elbv2model.ListenerRule, len(resLRs))
+	for _, resLR := range resLRs {
+		resLRByPriority[resLR.Spec.Priority] = resLR
+	}
+	return resLRByPriority
+}
+
+func mapSDKListenerRuleByPriority(sdkLRs []*elbv2sdk.Rule) map[int64]*elbv2sdk.Rule {
+	sdkLRByPriority := make(map[int64]*elbv2sdk.Rule, len(sdkLRs))
+	for _, sdkLR := range sdkLRs {
+		priority, _ := strconv.ParseInt(awssdk.StringValue(sdkLR.Priority), 10, 64)
+		sdkLRByPriority[priority] = sdkLR
+	}
+	return sdkLRByPriority
+}
+
+func mapResListenerRuleByListenerARN(resLRs []*elbv2model.ListenerRule) (map[string][]*elbv2model.ListenerRule, error) {
+	resLRsByLSARN := make(map[string][]*elbv2model.ListenerRule, len(resLRs))
+	ctx := context.Background()
+	for _, lr := range resLRs {
+		lsARN, err := lr.Spec.ListenerARN.Resolve(ctx)
+		if err != nil {
+			return nil, err
+		}
+		resLRsByLSARN[lsARN] = append(resLRsByLSARN[lsARN], lr)
+	}
+	return resLRsByLSARN, nil
+}
diff --git a/pkg/deploy/elbv2/listener_synthesizer.go b/pkg/deploy/elbv2/listener_synthesizer.go
index 3fec924d7..bd44e3924 100644
--- a/pkg/deploy/elbv2/listener_synthesizer.go
+++ b/pkg/deploy/elbv2/listener_synthesizer.go
@@ -35,12 +35,12 @@ func (s *listenerSynthesizer) Synthesize(ctx context.Context) error {
 	if err != nil {
 		return err
 	}
+
 	for lbARN, resLSs := range resLSsByLBARN {
 		if err := s.synthesizeListenersOnLB(ctx, lbARN, resLSs); err != nil {
 			return err
 		}
 	}
-
 	return nil
 }
 
diff --git a/pkg/deploy/elbv2/listener_utils.go b/pkg/deploy/elbv2/listener_utils.go
index 4f61d5bfe..7652e9fd4 100644
--- a/pkg/deploy/elbv2/listener_utils.go
+++ b/pkg/deploy/elbv2/listener_utils.go
@@ -120,3 +120,83 @@ func buildSDKForwardActionConfig(modelCfg elbv2model.ForwardActionConfig) (*elbv
 
 	return sdkObj, nil
 }
+
+func buildSDKRuleConditions(modelConditions []elbv2model.RuleCondition) []*elbv2sdk.RuleCondition {
+	var sdkConditions []*elbv2sdk.RuleCondition
+	if len(modelConditions) != 0 {
+		sdkConditions = make([]*elbv2sdk.RuleCondition, 0, len(modelConditions))
+		for _, modelCondition := range modelConditions {
+			sdkCondition := buildSDKRuleCondition(modelCondition)
+			sdkConditions = append(sdkConditions, sdkCondition)
+		}
+	}
+	return sdkConditions
+}
+
+func buildSDKRuleCondition(modelCondition elbv2model.RuleCondition) *elbv2sdk.RuleCondition {
+	sdkObj := &elbv2sdk.RuleCondition{}
+	sdkObj.Field = awssdk.String(string(modelCondition.Field))
+	if modelCondition.HostHeaderConfig != nil {
+		sdkObj.HostHeaderConfig = buildSDKHostHeaderConditionConfig(*modelCondition.HostHeaderConfig)
+	}
+	if modelCondition.HTTPHeaderConfig != nil {
+		sdkObj.HttpHeaderConfig = buildSDKHTTPHeaderConditionConfig(*modelCondition.HTTPHeaderConfig)
+	}
+	if modelCondition.HTTPRequestMethodConfig != nil {
+		sdkObj.HttpRequestMethodConfig = buildSDKHTTPRequestMethodConditionConfig(*modelCondition.HTTPRequestMethodConfig)
+	}
+	if modelCondition.PathPatternConfig != nil {
+		sdkObj.PathPatternConfig = buildSDKPathPatternConditionConfig(*modelCondition.PathPatternConfig)
+	}
+	if modelCondition.QueryStringConfig != nil {
+		sdkObj.QueryStringConfig = buildSDKQueryStringConditionConfig(*modelCondition.QueryStringConfig)
+	}
+	if modelCondition.SourceIPConfig != nil {
+		sdkObj.SourceIpConfig = buildSDKSourceIpConditionConfig(*modelCondition.SourceIPConfig)
+	}
+	return sdkObj
+}
+
+func buildSDKHostHeaderConditionConfig(modelCfg elbv2model.HostHeaderConditionConfig) *elbv2sdk.HostHeaderConditionConfig {
+	return &elbv2sdk.HostHeaderConditionConfig{
+		Values: awssdk.StringSlice(modelCfg.Values),
+	}
+}
+
+func buildSDKHTTPHeaderConditionConfig(modelCfg elbv2model.HTTPHeaderConditionConfig) *elbv2sdk.HttpHeaderConditionConfig {
+	return &elbv2sdk.HttpHeaderConditionConfig{
+		HttpHeaderName: awssdk.String(modelCfg.HTTPHeaderName),
+		Values:         awssdk.StringSlice(modelCfg.Values),
+	}
+}
+
+func buildSDKHTTPRequestMethodConditionConfig(modelCfg elbv2model.HTTPRequestMethodConditionConfig) *elbv2sdk.HttpRequestMethodConditionConfig {
+	return &elbv2sdk.HttpRequestMethodConditionConfig{
+		Values: awssdk.StringSlice(modelCfg.Values),
+	}
+}
+
+func buildSDKPathPatternConditionConfig(modelCfg elbv2model.PathPatternConditionConfig) *elbv2sdk.PathPatternConditionConfig {
+	return &elbv2sdk.PathPatternConditionConfig{
+		Values: awssdk.StringSlice(modelCfg.Values),
+	}
+}
+
+func buildSDKQueryStringConditionConfig(modelCfg elbv2model.QueryStringConditionConfig) *elbv2sdk.QueryStringConditionConfig {
+	kvPairs := make([]*elbv2sdk.QueryStringKeyValuePair, 0, len(modelCfg.Values))
+	for _, value := range modelCfg.Values {
+		kvPairs = append(kvPairs, &elbv2sdk.QueryStringKeyValuePair{
+			Key:   value.Key,
+			Value: awssdk.String(value.Value),
+		})
+	}
+	return &elbv2sdk.QueryStringConditionConfig{
+		Values: kvPairs,
+	}
+}
+
+func buildSDKSourceIpConditionConfig(modelCfg elbv2model.SourceIPConditionConfig) *elbv2sdk.SourceIpConditionConfig {
+	return &elbv2sdk.SourceIpConditionConfig{
+		Values: awssdk.StringSlice(modelCfg.Values),
+	}
+}
diff --git a/pkg/deploy/stack_deployer.go b/pkg/deploy/stack_deployer.go
index ab244f1c2..a4e79c106 100644
--- a/pkg/deploy/stack_deployer.go
+++ b/pkg/deploy/stack_deployer.go
@@ -27,6 +27,7 @@ func NewDefaultStackDeployer(k8sClient client.Client, elbv2Client services.ELBV2
 		elbv2TaggingManager: elbv2TaggingManager,
 		elbv2LBManager:      elbv2.NewDefaultLoadBalancerManager(elbv2Client, taggingProvider, elbv2TaggingManager, logger),
 		elbv2LSManager:      elbv2.NewDefaultListenerManager(elbv2Client, logger),
+		elbv2LRManager:      elbv2.NewDefaultListenerRuleManager(elbv2Client, logger),
 		elbv2TGManager:      elbv2.NewDefaultTargetGroupManager(elbv2Client, taggingProvider, elbv2TaggingManager, vpcID, logger),
 		elbv2TGBManager:     elbv2.NewDefaultTargetGroupBindingManager(k8sClient, taggingProvider, logger),
 		logger:              logger,
@@ -43,6 +44,7 @@ type defaultStackDeployer struct {
 	elbv2TaggingManager elbv2.TaggingManager
 	elbv2LBManager      elbv2.LoadBalancerManager
 	elbv2LSManager      elbv2.ListenerManager
+	elbv2LRManager      elbv2.ListenerRuleManager
 	elbv2TGManager      elbv2.TargetGroupManager
 	elbv2TGBManager     elbv2.TargetGroupBindingManager
 
@@ -61,6 +63,7 @@ func (d *defaultStackDeployer) Deploy(ctx context.Context, stack core.Stack) err
 		elbv2.NewTargetGroupBindingSynthesizer(d.k8sClient, d.taggingProvider, d.elbv2TGBManager, d.logger, stack),
 		elbv2.NewLoadBalancerSynthesizer(d.elbv2Client, d.taggingProvider, d.elbv2TaggingManager, d.elbv2LBManager, d.logger, stack),
 		elbv2.NewListenerSynthesizer(d.elbv2Client, d.elbv2LSManager, d.logger, stack),
+		elbv2.NewListenerRuleSynthesizer(d.elbv2Client, d.elbv2LRManager, d.logger, stack),
 	}
 	for _, synthesizer := range synthesizers {
 		if err := synthesizer.Synthesize(ctx); err != nil {
diff --git a/pkg/equality/elbv2/compare_option_for_rule_conditions.go b/pkg/equality/elbv2/compare_option_for_rule_conditions.go
new file mode 100644
index 000000000..788980913
--- /dev/null
+++ b/pkg/equality/elbv2/compare_option_for_rule_conditions.go
@@ -0,0 +1,25 @@
+package elbv2
+
+import (
+	awssdk "github.com/aws/aws-sdk-go/aws"
+	elbv2sdk "github.com/aws/aws-sdk-go/service/elbv2"
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+)
+
+func CompareOptionForRuleCondition() cmp.Option {
+	return cmp.Options{
+		cmpopts.IgnoreFields(elbv2sdk.RuleCondition{}, "Values"),
+	}
+}
+
+// CompareOptionForRuleConditions returns the compare option for rule conditions slice.
+func CompareOptionForRuleConditions() cmp.Option {
+	return cmp.Options{
+		cmpopts.EquateEmpty(),
+		cmpopts.SortSlices(func(lhs *elbv2sdk.RuleCondition, rhs *elbv2sdk.RuleCondition) bool {
+			return awssdk.StringValue(lhs.Field) < awssdk.StringValue(rhs.Field)
+		}),
+		CompareOptionForRuleCondition(),
+	}
+}
diff --git a/pkg/k8s/node_utils.go b/pkg/k8s/node_utils.go
index 42002bd65..efbda4968 100644
--- a/pkg/k8s/node_utils.go
+++ b/pkg/k8s/node_utils.go
@@ -1,6 +1,10 @@
 package k8s
 
-import corev1 "k8s.io/api/core/v1"
+import (
+	"github.com/pkg/errors"
+	corev1 "k8s.io/api/core/v1"
+	"strings"
+)
 
 // IsNodeReady returns whether node is ready.
 func IsNodeReady(node *corev1.Node) bool {
@@ -18,3 +22,13 @@ func GetNodeCondition(node *corev1.Node, conditionType corev1.NodeConditionType)
 	}
 	return nil
 }
+
+func ExtractNodeInstanceID(node *corev1.Node) (string, error) {
+	providerID := node.Spec.ProviderID
+	if providerID == "" {
+		return "", errors.Errorf("providerID is not specified for node: %s", node.Name)
+	}
+
+	p := strings.Split(providerID, "/")
+	return p[len(p)-1], nil
+}
diff --git a/pkg/model/elbv2/listener.go b/pkg/model/elbv2/listener.go
index fed02eee0..2a4fa2129 100644
--- a/pkg/model/elbv2/listener.go
+++ b/pkg/model/elbv2/listener.go
@@ -1,6 +1,8 @@
 package elbv2
 
 import (
+	"context"
+	"github.com/pkg/errors"
 	"sigs.k8s.io/aws-alb-ingress-controller/pkg/model/core"
 )
 
@@ -35,6 +37,19 @@ func (ls *Listener) SetStatus(status ListenerStatus) {
 	ls.Status = &status
 }
 
+// ListenerARN returns The Amazon Resource Name (ARN) of the Listener
+func (ls *Listener) ListenerARN() core.StringToken {
+	return core.NewResourceFieldStringToken(ls, "status/listenerARN",
+		func(ctx context.Context, res core.Resource, fieldPath string) (s string, err error) {
+			ls := res.(*Listener)
+			if ls.Status == nil {
+				return "", errors.Errorf("Listener is not fulfilled yet: %v", ls.ID())
+			}
+			return ls.Status.ListenerARN, nil
+		},
+	)
+}
+
 // register dependencies for Listener.
 func (ls *Listener) registerDependencies(stack core.Stack) {
 	for _, dep := range ls.Spec.LoadBalancerARN.Dependencies() {
diff --git a/pkg/model/elbv2/listener_rule.go b/pkg/model/elbv2/listener_rule.go
new file mode 100644
index 000000000..aa7da84e5
--- /dev/null
+++ b/pkg/model/elbv2/listener_rule.go
@@ -0,0 +1,138 @@
+package elbv2
+
+import (
+	"sigs.k8s.io/aws-alb-ingress-controller/pkg/model/core"
+)
+
+var _ core.Resource = &ListenerRule{}
+
+// ListenerRule represents a ELBV2 ListenerRule
+type ListenerRule struct {
+	core.ResourceMeta `json:"-"`
+
+	// desired state of ListenerRule
+	Spec ListenerRuleSpec `json:"spec"`
+
+	// observed state of ListenerRule
+	// +optional
+	Status *ListenerRuleStatus `json:"status,omitempty"`
+}
+
+// NewListenerRule constructs new ListenerRule resource.
+func NewListenerRule(stack core.Stack, id string, spec ListenerRuleSpec) *ListenerRule {
+	lr := &ListenerRule{
+		ResourceMeta: core.NewResourceMeta(stack, "AWS::ElasticLoadBalancingV2::ListenerRule", id),
+		Spec:         spec,
+		Status:       nil,
+	}
+	stack.AddResource(lr)
+	lr.registerDependencies(stack)
+	return lr
+}
+
+// SetStatus sets the ListenerRule's status
+func (lr *ListenerRule) SetStatus(status ListenerRuleStatus) {
+	lr.Status = &status
+}
+
+// register dependencies for ListenerRule.
+func (lr *ListenerRule) registerDependencies(stack core.Stack) {
+	for _, dep := range lr.Spec.ListenerARN.Dependencies() {
+		stack.AddDependency(dep, lr)
+	}
+}
+
+type RuleConditionField string
+
+const (
+	RuleConditionFieldHTTPHeader        RuleConditionField = "http-header"
+	RuleConditionFieldHTTPRequestMethod RuleConditionField = "http-request-method"
+	RuleConditionFieldHostHeader        RuleConditionField = "host-header"
+	RuleConditionFieldPathPattern       RuleConditionField = "path-pattern"
+	RuleConditionFieldQueryString       RuleConditionField = "query-string"
+	RuleConditionFieldSourceIP          RuleConditionField = "source-ip"
+)
+
+// Information for a host header condition.
+type HostHeaderConditionConfig struct {
+	// One or more host names.
+	Values []string `json:"values"`
+}
+
+// Information for an HTTP header condition.
+type HTTPHeaderConditionConfig struct {
+	// The name of the HTTP header field.
+	HTTPHeaderName string `json:"httpHeaderName"`
+	// One or more strings to compare against the value of the HTTP header.
+	Values []string `json:"values"`
+}
+
+// Information for an HTTP method condition.
+type HTTPRequestMethodConditionConfig struct {
+	// The name of the request method.
+	Values []string `json:"values"`
+}
+
+// Information about a path pattern condition.
+type PathPatternConditionConfig struct {
+	// One or more path patterns to compare against the request URL.
+	Values []string `json:"values"`
+}
+
+// Information about a key/value pair.
+type QueryStringKeyValuePair struct {
+	// The key.
+	// +optional
+	Key *string `json:"key,omitempty"`
+
+	// The value.
+	Value string `json:"value"`
+}
+
+// Information about a query string condition.
+type QueryStringConditionConfig struct {
+	// One or more key/value pairs or values to find in the query string.
+	Values []*QueryStringKeyValuePair `json:"values"`
+}
+
+// Information about a source IP condition.
+type SourceIPConditionConfig struct {
+	// One or more source IP addresses, in CIDR format.
+	Values []string `json:"value"`
+}
+
+// Information about a condition for a rule.
+type RuleCondition struct {
+	// The field in the HTTP request.
+	Field RuleConditionField `json:"field"`
+	// Information for a host header condition.
+	HostHeaderConfig *HostHeaderConditionConfig `json:"hostHeaderConfig"`
+	// Information for an HTTP header condition.
+	HTTPHeaderConfig *HTTPHeaderConditionConfig `json:"httpHeaderConfig"`
+	// Information for an HTTP method condition.
+	HTTPRequestMethodConfig *HTTPRequestMethodConditionConfig `json:"httpRequestMethodConfig"`
+	// Information for a path pattern condition.
+	PathPatternConfig *PathPatternConditionConfig `json:"pathPatternConfig"`
+	// Information for a query string condition.
+	QueryStringConfig *QueryStringConditionConfig `json:"queryStringConfig"`
+	// Information for a source IP condition.
+	SourceIPConfig *SourceIPConditionConfig `json:"sourceIPConfig"`
+}
+
+// ListenerRuleSpec defines the desired state of ListenerRule
+type ListenerRuleSpec struct {
+	// The Amazon Resource Name (ARN) of the listener.
+	ListenerARN core.StringToken `json:"listenerARN"`
+	// The rule priority.
+	Priority int64 `json:"priority"`
+	// The actions.
+	Actions []Action `json:"actions"`
+	// The conditions.
+	Conditions []RuleCondition `json:"conditions"`
+}
+
+// ListenerRuleStatus defines the observed state of ListenerRule
+type ListenerRuleStatus struct {
+	// The Amazon Resource Name (ARN) of the rule.
+	RuleARN string `json:"ruleARN"`
+}
diff --git a/pkg/networking/eni_info.go b/pkg/networking/eni_info.go
new file mode 100644
index 000000000..685da6f42
--- /dev/null
+++ b/pkg/networking/eni_info.go
@@ -0,0 +1,37 @@
+package networking
+
+import (
+	awssdk "github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/service/ec2"
+)
+
+// ENIInfo wraps necessary information about a ENI.
+type ENIInfo struct {
+	// ENI's ID
+	NetworkInterfaceID string
+
+	// SecurityGroups on ENI
+	SecurityGroups []string
+}
+
+func buildENIInfoViaENI(eni *ec2.NetworkInterface) ENIInfo {
+	sgIDs := make([]string, 0, len(eni.Groups))
+	for _, group := range eni.Groups {
+		sgIDs = append(sgIDs, awssdk.StringValue(group.GroupId))
+	}
+	return ENIInfo{
+		NetworkInterfaceID: awssdk.StringValue(eni.NetworkInterfaceId),
+		SecurityGroups:     sgIDs,
+	}
+}
+
+func buildENIInfoViaInstanceENI(eni *ec2.InstanceNetworkInterface) ENIInfo {
+	sgIDs := make([]string, 0, len(eni.Groups))
+	for _, group := range eni.Groups {
+		sgIDs = append(sgIDs, awssdk.StringValue(group.GroupId))
+	}
+	return ENIInfo{
+		NetworkInterfaceID: awssdk.StringValue(eni.NetworkInterfaceId),
+		SecurityGroups:     sgIDs,
+	}
+}
diff --git a/pkg/networking/node_eni_info_resolver.go b/pkg/networking/node_eni_info_resolver.go
new file mode 100644
index 000000000..6f7244491
--- /dev/null
+++ b/pkg/networking/node_eni_info_resolver.go
@@ -0,0 +1,181 @@
+package networking
+
+import (
+	"context"
+	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"
+	"sigs.k8s.io/aws-alb-ingress-controller/pkg/aws/services"
+	"sigs.k8s.io/aws-alb-ingress-controller/pkg/k8s"
+	"sync"
+	"time"
+)
+
+const (
+	defaultNodeENIInfoCacheTTL = 10 * time.Minute
+)
+
+// NodeENIInfoResolver is responsible for resolve the AWS VPC ENI that supports node network.
+type NodeENIInfoResolver interface {
+	Resolve(ctx context.Context, nodes []*corev1.Node) (map[types.NamespacedName]ENIInfo, error)
+}
+
+// NewDefaultNodeENIInfoResolver constructs new defaultNodeENIInfoResolver.
+func NewDefaultNodeENIInfoResolver(ec2Client services.EC2, logger logr.Logger) *defaultNodeENIInfoResolver {
+	return &defaultNodeENIInfoResolver{
+		ec2Client:             ec2Client,
+		logger:                logger,
+		nodeENIInfoCache:      cache.NewExpiring(),
+		nodeENIInfoCacheMutex: sync.RWMutex{},
+		nodeENIInfoCacheTTL:   defaultNodeENIInfoCacheTTL,
+	}
+}
+
+var _ NodeENIInfoResolver = &defaultNodeENIInfoResolver{}
+
+// default implementation for NodeENIInfoResolver.
+type defaultNodeENIInfoResolver struct {
+	// ec2 client
+	ec2Client services.EC2
+	// logger
+	logger logr.Logger
+
+	nodeENIInfoCache      *cache.Expiring
+	nodeENIInfoCacheMutex sync.RWMutex
+	nodeENIInfoCacheTTL   time.Duration
+}
+
+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)
+	if err != nil {
+		return nil, err
+	}
+	if len(eniInfoByNodeKeyViaLookup) > 0 {
+		r.saveENIInfosToCache(unresolvedNodes, eniInfoByNodeKeyViaLookup)
+		for nodeKey, eniInfo := range eniInfoByNodeKeyViaLookup {
+			eniInfoByNodeKey[nodeKey] = eniInfo
+		}
+		unresolvedNodes = computeNodesWithUnresolvedENIInfo(unresolvedNodes, eniInfoByNodeKeyViaLookup)
+	}
+
+	if len(unresolvedNodes) > 0 {
+		unresolvedNodeKeys := make([]types.NamespacedName, 0, len(unresolvedNodes))
+		for _, node := range unresolvedNodes {
+			unresolvedNodeKeys = append(unresolvedNodeKeys, k8s.NamespacedName(node))
+		}
+		return nil, errors.Errorf("cannot resolve node ENI for nodes: %v", unresolvedNodeKeys)
+	}
+	return eniInfoByNodeKey, nil
+}
+
+type nodeENIInfoCacheKey struct {
+	// Node's key
+	nodeKey types.NamespacedName
+	// Node's UID.
+	// Note: we assume node's eni haven't changed as long as node UID is same.
+	nodeUID string
+}
+
+func (r *defaultNodeENIInfoResolver) fetchENIInfosFromCache(nodes []*corev1.Node) map[types.NamespacedName]ENIInfo {
+	r.nodeENIInfoCacheMutex.RLock()
+	defer r.nodeENIInfoCacheMutex.RUnlock()
+
+	eniInfoByNodeKey := make(map[types.NamespacedName]ENIInfo)
+	for _, node := range nodes {
+		cacheKey := computeNodeENIInfoCacheKey(node)
+		if rawCacheItem, exists := r.nodeENIInfoCache.Get(cacheKey); exists {
+			eniInfo := rawCacheItem.(ENIInfo)
+			nodeKey := k8s.NamespacedName(node)
+			eniInfoByNodeKey[nodeKey] = eniInfo
+		}
+	}
+	return eniInfoByNodeKey
+}
+
+func (r *defaultNodeENIInfoResolver) saveENIInfosToCache(nodes []*corev1.Node, eniInfoByNodeKey map[types.NamespacedName]ENIInfo) {
+	r.nodeENIInfoCacheMutex.Lock()
+	defer r.nodeENIInfoCacheMutex.Unlock()
+
+	for _, node := range nodes {
+		nodeKey := k8s.NamespacedName(node)
+		if eniInfo, exists := eniInfoByNodeKey[nodeKey]; exists {
+			cacheKey := computeNodeENIInfoCacheKey(node)
+			r.nodeENIInfoCache.Set(cacheKey, eniInfo, r.nodeENIInfoCacheTTL)
+		}
+	}
+}
+
+func (r *defaultNodeENIInfoResolver) resolveViaInstanceID(ctx context.Context, nodes []*corev1.Node) (map[types.NamespacedName]ENIInfo, error) {
+	nodeKeysByInstanceID := make(map[string][]types.NamespacedName)
+	for _, node := range nodes {
+		instanceID, err := k8s.ExtractNodeInstanceID(node)
+		if err != nil {
+			return nil, err
+		}
+		nodeKey := k8s.NamespacedName(node)
+		nodeKeysByInstanceID[instanceID] = append(nodeKeysByInstanceID[instanceID], nodeKey)
+	}
+	if len(nodeKeysByInstanceID) == 0 {
+		return nil, nil
+	}
+
+	instanceIDs := sets.StringKeySet(nodeKeysByInstanceID).List()
+	req := &ec2sdk.DescribeInstancesInput{
+		InstanceIds: awssdk.StringSlice(instanceIDs),
+	}
+	instances, err := r.ec2Client.DescribeInstancesAsList(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+
+	eniInfoByNodeKey := make(map[types.NamespacedName]ENIInfo)
+	for _, instance := range instances {
+		instanceID := awssdk.StringValue(instance.InstanceId)
+		primaryENI, err := findInstancePrimaryENI(instance.NetworkInterfaces)
+		if err != nil {
+			return nil, err
+		}
+		eniInfo := buildENIInfoViaInstanceENI(primaryENI)
+		for _, nodeKey := range nodeKeysByInstanceID[instanceID] {
+			eniInfoByNodeKey[nodeKey] = eniInfo
+		}
+	}
+	return eniInfoByNodeKey, nil
+}
+
+// findInstancePrimaryENI returns the primary ENI among list of eni on an EC2 instance
+func findInstancePrimaryENI(enis []*ec2sdk.InstanceNetworkInterface) (*ec2sdk.InstanceNetworkInterface, error) {
+	for _, eni := range enis {
+		if awssdk.Int64Value(eni.Attachment.DeviceIndex) == 0 {
+			return eni, nil
+		}
+	}
+	return nil, errors.Errorf("[this should never happen] no primary ENI found")
+}
+
+// computeNodeENIInfoCacheKey computes the cacheKey for node's ENIInfo cache.
+func computeNodeENIInfoCacheKey(node *corev1.Node) nodeENIInfoCacheKey {
+	return nodeENIInfoCacheKey{
+		nodeKey: k8s.NamespacedName(node),
+		nodeUID: string(node.UID),
+	}
+}
+
+// computeNodesWithUnresolvedENIInfo computes nodes that don't have resolvedENIInfo.
+func computeNodesWithUnresolvedENIInfo(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)
+		if _, ok := eniInfoByNodeKey[nodeKey]; !ok {
+			unresolvedNodes = append(unresolvedNodes, node)
+		}
+	}
+	return unresolvedNodes
+}
diff --git a/pkg/networking/pod_eni_info_resolver.go b/pkg/networking/pod_eni_info_resolver.go
new file mode 100644
index 000000000..ff53dd52b
--- /dev/null
+++ b/pkg/networking/pod_eni_info_resolver.go
@@ -0,0 +1,275 @@
+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"
+	"sigs.k8s.io/aws-alb-ingress-controller/pkg/algorithm"
+	"sigs.k8s.io/aws-alb-ingress-controller/pkg/aws/services"
+	"sigs.k8s.io/aws-alb-ingress-controller/pkg/k8s"
+	"sync"
+	"time"
+)
+
+const (
+	annotationPodENI = "vpc.amazonaws.com/pod-eni"
+
+	defaultPodENIInfoCacheTTL = 10 * time.Minute
+	// EC2:DescribeNetworkInterface supports up to 200 filters per call.
+	describeNetworkInterfacesFiltersLimit = 200
+)
+
+// 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)
+}
+
+// NewDefaultPodENIResolver constructs new defaultPodENIResolver.
+func NewDefaultPodENIInfoResolver(ec2Client services.EC2, vpcID string, logger logr.Logger) *defaultPodENIInfoResolver {
+	return &defaultPodENIInfoResolver{
+		ec2Client:                            ec2Client,
+		vpcID:                                vpcID,
+		logger:                               logger,
+		podENIInfoCache:                      cache.NewExpiring(),
+		podENIInfoCacheMutex:                 sync.RWMutex{},
+		podENIInfoCacheTTL:                   defaultPodENIInfoCacheTTL,
+		describeNetworkInterfacesIPChunkSize: describeNetworkInterfacesFiltersLimit - 1, // we used 1 filter for VPC.
+	}
+}
+
+var _ PodENIInfoResolver = &defaultPodENIInfoResolver{}
+
+// default implementation for PodENIResolver
+type defaultPodENIInfoResolver struct {
+	// ec2 client
+	ec2Client services.EC2
+	// our vpcID.
+	vpcID string
+	// logger
+	logger logr.Logger
+
+	// cache of ENIInfo by podUID(podKey + UID).
+	// NOTE: since this cache implementation will automatically GC expired entries, we don't need to GC entries.
+	podENIInfoCache *cache.Expiring
+	// podENICacheMutex protects podENICache
+	podENIInfoCacheMutex sync.RWMutex
+	// TTL for each cache entries.
+	// Note: we assume pod's ENI information(e.g. securityGroups) haven't changed per podENICacheTTL.
+	podENIInfoCacheTTL time.Duration
+
+	describeNetworkInterfacesIPChunkSize int
+}
+
+func (r *defaultPodENIInfoResolver) Resolve(ctx context.Context, pods []*corev1.Pod) (map[types.NamespacedName]ENIInfo, error) {
+	eniInfoByPodKey := r.fetchENIInfosFromCache(pods)
+
+	unresolvedPods := computePodsWithUnresolvedENIInfo(pods, eniInfoByPodKey)
+	eniInfoByPodKeyViaLookup, err := r.resolveViaCascadedLookup(ctx, unresolvedPods)
+	if err != nil {
+		return nil, err
+	}
+	if len(eniInfoByPodKeyViaLookup) > 0 {
+		r.saveENIInfosToCache(unresolvedPods, eniInfoByPodKeyViaLookup)
+		for podKey, eniInfo := range eniInfoByPodKeyViaLookup {
+			eniInfoByPodKey[podKey] = eniInfo
+		}
+		unresolvedPods = computePodsWithUnresolvedENIInfo(unresolvedPods, eniInfoByPodKeyViaLookup)
+	}
+
+	if len(unresolvedPods) > 0 {
+		unresolvedPodKeys := make([]types.NamespacedName, 0, len(unresolvedPods))
+		for _, pod := range unresolvedPods {
+			unresolvedPodKeys = append(unresolvedPodKeys, k8s.NamespacedName(pod))
+		}
+		return nil, errors.Errorf("cannot resolve pod ENI for pods: %v", unresolvedPodKeys)
+	}
+	return eniInfoByPodKey, nil
+}
+
+type podENIInfoCacheKey struct {
+	// Pod's key
+	podKey types.NamespacedName
+	// Pod's UID.
+	// Note: we assume pod's eni haven't changed as long as pod UID is same.
+	podUID string
+}
+
+func (r *defaultPodENIInfoResolver) fetchENIInfosFromCache(pods []*corev1.Pod) map[types.NamespacedName]ENIInfo {
+	r.podENIInfoCacheMutex.RLock()
+	defer r.podENIInfoCacheMutex.RUnlock()
+
+	eniInfoByPodKey := make(map[types.NamespacedName]ENIInfo)
+	for _, pod := range pods {
+		cacheKey := computePodENIInfoCacheKey(pod)
+		if rawCacheItem, exists := r.podENIInfoCache.Get(cacheKey); exists {
+			eniInfo := rawCacheItem.(ENIInfo)
+			podKey := k8s.NamespacedName(pod)
+			eniInfoByPodKey[podKey] = eniInfo
+		}
+	}
+	return eniInfoByPodKey
+}
+
+func (r *defaultPodENIInfoResolver) saveENIInfosToCache(pods []*corev1.Pod, eniInfoByPodKey map[types.NamespacedName]ENIInfo) {
+	r.podENIInfoCacheMutex.Lock()
+	defer r.podENIInfoCacheMutex.Unlock()
+
+	for _, pod := range pods {
+		podKey := k8s.NamespacedName(pod)
+		if eniInfo, exists := eniInfoByPodKey[podKey]; exists {
+			cacheKey := computePodENIInfoCacheKey(pod)
+			r.podENIInfoCache.Set(cacheKey, eniInfo, r.podENIInfoCacheTTL)
+		}
+	}
+}
+
+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){
+		r.resolveViaPodENIAnnotation,
+		r.resolveViaVPCIPAddress,
+		// TODO, add support for kubenet CNI plugin(kops) by resolve via routeTable.
+	}
+
+	eniInfoByPodKey := make(map[types.NamespacedName]ENIInfo)
+	for _, resolveFunc := range resolveFuncs {
+		if len(pods) == 0 {
+			break
+		}
+		resolvedENIInfoByPodKey, err := resolveFunc(ctx, pods)
+		if err != nil {
+			return nil, err
+		}
+		for podKey, eniInfo := range resolvedENIInfoByPodKey {
+			eniInfoByPodKey[podKey] = eniInfo
+		}
+		pods = computePodsWithUnresolvedENIInfo(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) {
+	podKeysByENIID := make(map[string][]types.NamespacedName)
+	for _, pod := range pods {
+		podENIAnnotation, ok := pod.Annotations[annotationPodENI]
+		if !ok {
+			continue
+		}
+		var schema annotationSchemaPodENI
+		if err := json.Unmarshal([]byte(podENIAnnotation), &schema); err != nil {
+			return nil, err
+		}
+		eniID := schema.ENIID
+		podKey := k8s.NamespacedName(pod)
+		podKeysByENIID[eniID] = append(podKeysByENIID[eniID], podKey)
+	}
+	if len(podKeysByENIID) == 0 {
+		return nil, nil
+	}
+
+	eniIDs := sets.StringKeySet(podKeysByENIID).List()
+	req := &ec2sdk.DescribeNetworkInterfacesInput{
+		NetworkInterfaceIds: awssdk.StringSlice(eniIDs),
+	}
+	enis, err := r.ec2Client.DescribeNetworkInterfacesAsList(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+	eniInfoByPodKey := make(map[types.NamespacedName]ENIInfo)
+	for _, eni := range enis {
+		eniID := awssdk.StringValue(eni.NetworkInterfaceId)
+		eniInfo := buildENIInfoViaENI(eni)
+		for _, podKey := range podKeysByENIID[eniID] {
+			eniInfoByPodKey[podKey] = eniInfo
+		}
+	}
+	return eniInfoByPodKey, nil
+}
+
+// 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) {
+	podKeysByIP := make(map[string][]types.NamespacedName, len(pods))
+	for _, pod := range pods {
+		podKeysByIP[pod.Status.PodIP] = append(podKeysByIP[pod.Status.PodIP], k8s.NamespacedName(pod))
+	}
+	if len(podKeysByIP) == 0 {
+		return nil, nil
+	}
+
+	podIPs := sets.StringKeySet(podKeysByIP).List()
+	podIPChunks := algorithm.ChunkStrings(podIPs, r.describeNetworkInterfacesIPChunkSize)
+	eniByID := make(map[string]*ec2sdk.NetworkInterface)
+	for _, podIPChunk := range podIPChunks {
+		req := &ec2sdk.DescribeNetworkInterfacesInput{
+			Filters: []*ec2sdk.Filter{
+				{
+					Name:   awssdk.String("vpc-id"),
+					Values: awssdk.StringSlice([]string{r.vpcID}),
+				},
+				{
+					Name:   awssdk.String("addresses.private-ip-address"),
+					Values: awssdk.StringSlice(podIPChunk),
+				},
+			},
+		}
+		enis, err := r.ec2Client.DescribeNetworkInterfacesAsList(ctx, req)
+		if err != nil {
+			return nil, err
+		}
+		for _, eni := range enis {
+			eniID := awssdk.StringValue(eni.NetworkInterfaceId)
+			eniByID[eniID] = eni
+		}
+	}
+
+	eniInfoByPodKey := make(map[types.NamespacedName]ENIInfo)
+	for _, eni := range eniByID {
+		eniInfo := buildENIInfoViaENI(eni)
+		for _, addr := range eni.PrivateIpAddresses {
+			eniIP := awssdk.StringValue(addr.PrivateIpAddress)
+			for _, podKey := range podKeysByIP[eniIP] {
+				eniInfoByPodKey[podKey] = eniInfo
+			}
+		}
+	}
+	return eniInfoByPodKey, nil
+}
+
+// computePodENIInfoCacheKey computes the cacheKey for pod's ENIInfo cache.
+func computePodENIInfoCacheKey(pod *corev1.Pod) podENIInfoCacheKey {
+	return podENIInfoCacheKey{
+		podKey: k8s.NamespacedName(pod),
+		podUID: string(pod.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))
+	for _, pod := range pods {
+		podKey := k8s.NamespacedName(pod)
+		if _, ok := eniInfoByPodKey[podKey]; !ok {
+			unresolvedPods = append(unresolvedPods, pod)
+		}
+	}
+	return unresolvedPods
+}
diff --git a/pkg/networking/security_group_info.go b/pkg/networking/security_group_info.go
new file mode 100644
index 000000000..82e6b1688
--- /dev/null
+++ b/pkg/networking/security_group_info.go
@@ -0,0 +1,125 @@
+package networking
+
+import (
+	awssdk "github.com/aws/aws-sdk-go/aws"
+	ec2sdk "github.com/aws/aws-sdk-go/service/ec2"
+	"regexp"
+)
+
+const (
+	// the raw permission description
+	labelKeyRawDescription = "raw/description"
+)
+
+// SecurityGroupInfo wraps necessary information about a SecurityGroup.
+type SecurityGroupInfo struct {
+	// securityGroup's ID.
+	SecurityGroupID string
+
+	// Ingress permission for securityGroup.
+	IngressPermissions []IPPermissionInfo
+}
+
+type IPPermissionInfo struct {
+	// the aws sdk permission
+	Permission ec2sdk.IpPermission
+
+	// a set of computed labels for IPPermission.
+	// we can use labels to select the rules we want to manage.
+	Labels map[string]string
+}
+
+func buildSecurityGroupInfo(sdkSG *ec2sdk.SecurityGroup) SecurityGroupInfo {
+	sgID := awssdk.StringValue(sdkSG.GroupId)
+	var ingressPermissions []IPPermissionInfo
+	for _, sdkPermission := range sdkSG.IpPermissions {
+		for _, expandedPermission := range expandSDKIPPermission(*sdkPermission) {
+			ingressPermissions = append(ingressPermissions, buildIPPermissionInfo(expandedPermission))
+		}
+	}
+	return SecurityGroupInfo{
+		SecurityGroupID:    sgID,
+		IngressPermissions: ingressPermissions,
+	}
+}
+
+func buildIPPermissionInfo(sdkPermission ec2sdk.IpPermission) IPPermissionInfo {
+	if len(sdkPermission.IpRanges) == 1 {
+		return IPPermissionInfo{
+			Permission: sdkPermission,
+			Labels:     buildIPPermissionLabelForDescription(awssdk.StringValue(sdkPermission.IpRanges[0].Description)),
+		}
+	}
+	if len(sdkPermission.Ipv6Ranges) == 1 {
+		return IPPermissionInfo{
+			Permission: sdkPermission,
+			Labels:     buildIPPermissionLabelForDescription(awssdk.StringValue(sdkPermission.Ipv6Ranges[0].Description)),
+		}
+	}
+	if len(sdkPermission.PrefixListIds) == 1 {
+		return IPPermissionInfo{
+			Permission: sdkPermission,
+			Labels:     buildIPPermissionLabelForDescription(awssdk.StringValue(sdkPermission.PrefixListIds[0].Description)),
+		}
+	}
+	if len(sdkPermission.UserIdGroupPairs) == 1 {
+		return IPPermissionInfo{
+			Permission: sdkPermission,
+			Labels:     buildIPPermissionLabelForDescription(awssdk.StringValue(sdkPermission.UserIdGroupPairs[0].Description)),
+		}
+	}
+	return IPPermissionInfo{
+		Permission: sdkPermission,
+		Labels:     nil,
+	}
+}
+
+// expandSDKIPPermission will expand the IPPermission so that each permission only contain single entry.
+// EC2 api automatically group IPPermissions, so we need to expand first before further processing.
+func expandSDKIPPermission(sdkPermission ec2sdk.IpPermission) []ec2sdk.IpPermission {
+	var expandedPermissions []ec2sdk.IpPermission
+	base := ec2sdk.IpPermission{
+		FromPort:   sdkPermission.FromPort,
+		ToPort:     sdkPermission.ToPort,
+		IpProtocol: sdkPermission.IpProtocol,
+	}
+
+	for _, ipRange := range sdkPermission.IpRanges {
+		perm := base
+		perm.IpRanges = []*ec2sdk.IpRange{ipRange}
+		expandedPermissions = append(expandedPermissions, perm)
+	}
+
+	for _, ipRange := range sdkPermission.Ipv6Ranges {
+		perm := base
+		perm.Ipv6Ranges = []*ec2sdk.Ipv6Range{ipRange}
+		expandedPermissions = append(expandedPermissions, perm)
+	}
+	for _, prefixListID := range sdkPermission.PrefixListIds {
+		perm := base
+		perm.PrefixListIds = []*ec2sdk.PrefixListId{prefixListID}
+		expandedPermissions = append(expandedPermissions, perm)
+	}
+
+	for _, ug := range sdkPermission.UserIdGroupPairs {
+		perm := base
+		perm.UserIdGroupPairs = []*ec2sdk.UserIdGroupPair{ug}
+		expandedPermissions = append(expandedPermissions, perm)
+	}
+
+	if len(expandedPermissions) == 0 {
+		expandedPermissions = append(expandedPermissions, sdkPermission)
+	}
+	return expandedPermissions
+}
+
+var commaSeparatedKVPairPattern = regexp.MustCompile(`(?P<key>[^\s,=]+)=(?P<value>[^\s,=]+)(?:,|$)`)
+
+// buildIPPermissionLabelForDescription constructs a set of labels parsed from IPPermission description
+func buildIPPermissionLabelForDescription(description string) map[string]string {
+	labels := map[string]string{labelKeyRawDescription: description}
+	for _, groups := range commaSeparatedKVPairPattern.FindAllStringSubmatch(description, -1) {
+		labels[groups[1]] = groups[2]
+	}
+	return labels
+}
diff --git a/pkg/networking/security_group_reconciler.go b/pkg/networking/security_group_reconciler.go
new file mode 100644
index 000000000..9cb9232ac
--- /dev/null
+++ b/pkg/networking/security_group_reconciler.go
@@ -0,0 +1,75 @@
+package networking
+
+import (
+	"context"
+	awssdk "github.com/aws/aws-sdk-go/aws"
+	ec2sdk "github.com/aws/aws-sdk-go/service/ec2"
+	"github.com/go-logr/logr"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/apimachinery/pkg/util/cache"
+	"sigs.k8s.io/aws-alb-ingress-controller/pkg/aws/services"
+	"sync"
+	"time"
+)
+
+const (
+	defaultSGInfoCacheTTL = 10 * time.Minute
+)
+
+type SecurityGroupReconcileOptions struct {
+	// PermissionSelector defines the selector to identify permissions that should be managed.
+	// By default, it selects every permission.
+	PermissionSelector labels.Selector
+}
+
+type SecurityGroupReconcileOption func(opts *SecurityGroupReconcileOptions)
+
+// SecurityGroupReconciler manages securityGroup rules on securityGroup.
+type SecurityGroupReconciler interface {
+	ReconcileIngress(ctx context.Context, sgID string, permissions []IPPermissionInfo, opts ...SecurityGroupReconcileOption) error
+}
+
+// NewDefaultSecurityGroupReconciler constructs new defaultSecurityGroupReconciler.
+func NewDefaultSecurityGroupReconciler(ec2Client services.EC2, logger logr.Logger) *defaultSecurityGroupReconciler {
+	return &defaultSecurityGroupReconciler{
+		ec2Client:        ec2Client,
+		logger:           logger,
+		sgInfoCache:      cache.NewExpiring(),
+		sgInfoCacheMutex: sync.RWMutex{},
+		sgInfoCacheTTL:   defaultSGInfoCacheTTL,
+	}
+}
+
+var _ SecurityGroupReconciler = &defaultSecurityGroupReconciler{}
+
+// default implementation for SecurityGroupReconciler.
+type defaultSecurityGroupReconciler struct {
+	ec2Client services.EC2
+	logger    logr.Logger
+
+	sgInfoCache      *cache.Expiring
+	sgInfoCacheMutex sync.RWMutex
+	sgInfoCacheTTL   time.Duration
+}
+
+func (r *defaultSecurityGroupReconciler) ReconcileIngress(ctx context.Context, sgID string, permissions []IPPermissionInfo, opts ...SecurityGroupReconcileOption) error {
+	// TODO
+	return nil
+}
+
+func (r *defaultSecurityGroupReconciler) fetchSGInfosFromAWS(ctx context.Context, sgIDs []string) (map[string]SecurityGroupInfo, error) {
+	req := &ec2sdk.DescribeSecurityGroupsInput{
+		GroupIds: awssdk.StringSlice(sgIDs),
+	}
+	sgs, err := r.ec2Client.DescribeSecurityGroupsAsList(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+	sgInfoByID := make(map[string]SecurityGroupInfo, len(sgs))
+	for _, sg := range sgs {
+		sgID := awssdk.StringValue(sg.GroupId)
+		sgInfo := buildSecurityGroupInfo(sg)
+		sgInfoByID[sgID] = sgInfo
+	}
+	return sgInfoByID, nil
+}
diff --git a/pkg/service/nlb/builder.go b/pkg/service/nlb/builder.go
index 8746f8111..eb67ce8a9 100644
--- a/pkg/service/nlb/builder.go
+++ b/pkg/service/nlb/builder.go
@@ -16,6 +16,7 @@ import (
 	"sigs.k8s.io/aws-alb-ingress-controller/pkg/k8s"
 	"sigs.k8s.io/aws-alb-ingress-controller/pkg/model/core"
 	"sigs.k8s.io/aws-alb-ingress-controller/pkg/model/elbv2"
+	"sigs.k8s.io/aws-alb-ingress-controller/pkg/networking"
 	"strconv"
 	"strings"
 )
@@ -49,20 +50,25 @@ type nlbBuilder struct {
 	service          *corev1.Service
 	key              types.NamespacedName
 	annotationParser annotations.Parser
+	subnetsResolver  networking.SubnetsResolver
 }
 
-func NewServiceBuilder(service *corev1.Service, key types.NamespacedName, annotationParser annotations.Parser) Builder {
+func NewServiceBuilder(service *corev1.Service, subnetsResolver networking.SubnetsResolver, key types.NamespacedName, annotationParser annotations.Parser) Builder {
 	return &nlbBuilder{
 		service:          service,
 		key:              key,
 		annotationParser: annotationParser,
+		subnetsResolver:  subnetsResolver,
 	}
 }
 
 func (b *nlbBuilder) Build(ctx context.Context) (core.Stack, error) {
 	stack := core.NewDefaultStack(k8s.NamespacedName(b.service).String())
-	b.buildModel(ctx, stack)
-	return stack, nil
+	if !b.service.DeletionTimestamp.IsZero() {
+		return stack, nil
+	}
+	err := b.buildModel(ctx, stack)
+	return stack, err
 }
 
 func (b *nlbBuilder) buildModel(ctx context.Context, stack core.Stack) error {
@@ -97,18 +103,39 @@ func (b *nlbBuilder) loadBalancerSpec(ctx context.Context) (elbv2.LoadBalancerSp
 	}
 	tags := map[string]string{}
 	b.annotationParser.ParseStringMapAnnotation(annotations.SvcLBSuffixAdditionalTags, &tags, b.service.Annotations)
+	subnets, err := b.subnetsResolver.DiscoverSubnets(ctx, scheme)
+	if err != nil {
+		return elbv2.LoadBalancerSpec{}, err
+	}
+	subnetMappings, err := b.getSubnetMappings(subnets)
+	if err != nil {
+		return elbv2.LoadBalancerSpec{}, err
+	}
 	spec := elbv2.LoadBalancerSpec{
 		Name:                   b.loadbalancerName(b.service),
 		Type:                   elbv2.LoadBalancerTypeNetwork,
 		Scheme:                 &scheme,
 		IPAddressType:          &ipAddressType,
-		SubnetMappings:         nil,
+		SubnetMappings:         subnetMappings,
 		LoadBalancerAttributes: lbAttributes,
 		Tags:                   tags,
 	}
 	return spec, nil
 }
 
+func (b *nlbBuilder) getSubnetMappings(subnets []string) ([]elbv2.SubnetMapping, error) {
+	if len(subnets) == 0 {
+		return []elbv2.SubnetMapping{}, errors.Errorf("Unable to discover at least 1 subnet across availability zones")
+	}
+	subnetMappings := make([]elbv2.SubnetMapping, 0, len(subnets))
+	for _, subnet := range subnets {
+		subnetMappings = append(subnetMappings, elbv2.SubnetMapping{
+			SubnetID: subnet,
+		})
+	}
+	return subnetMappings, nil
+}
+
 func (b *nlbBuilder) buildLBAttributes(ctx context.Context) ([]elbv2.LoadBalancerAttribute, error) {
 	attrs := []elbv2.LoadBalancerAttribute{}
 	accessLogEnabled := DefaultAccessLogS3Enabled
@@ -267,7 +294,7 @@ func (b *nlbBuilder) buildListeners(ctx context.Context, stack core.Stack, lb *e
 				Template: elbv2.TargetGroupBindingTemplate{
 					ObjectMeta: metav1.ObjectMeta{
 						Namespace:    b.service.Namespace,
-						GenerateName: tgName,
+						Name: tgName,
 					},
 					Spec: elbv2api.TargetGroupBindingSpec{
 						TargetType: &targetType,
diff --git a/pkg/service/nlb/mock_subnet_resolver.go b/pkg/service/nlb/mock_subnet_resolver.go
new file mode 100644
index 000000000..8db9933d9
--- /dev/null
+++ b/pkg/service/nlb/mock_subnet_resolver.go
@@ -0,0 +1,21 @@
+package nlb
+
+import (
+	"context"
+	"sigs.k8s.io/aws-alb-ingress-controller/pkg/model/elbv2"
+	"sigs.k8s.io/aws-alb-ingress-controller/pkg/networking"
+)
+
+type mockSubnetsReoslver struct {
+	subnets []string
+}
+
+var _ networking.SubnetsResolver = &mockSubnetsReoslver{}
+
+func (m *mockSubnetsReoslver) DiscoverSubnets(ctx context.Context, scheme elbv2.LoadBalancerScheme) ([]string, error) {
+	return m.subnets, nil
+}
+
+func NewMockSubnetsResolver(subnets []string) networking.SubnetsResolver {
+	return &mockSubnetsReoslver{subnets: subnets}
+}
diff --git a/pkg/targetgroupbinding/resource_manager.go b/pkg/targetgroupbinding/resource_manager.go
index 81b01ccec..5890b567b 100644
--- a/pkg/targetgroupbinding/resource_manager.go
+++ b/pkg/targetgroupbinding/resource_manager.go
@@ -73,7 +73,8 @@ func (m *defaultResourceManager) reconcileWithIPTargetType(ctx context.Context,
 	if err != nil {
 		return err
 	}
-	matchedEndpointAndTargets, unmatchedEndpoints, unmatchedTargets := matchPodEndpointWithTargets(endpoints, targets)
+	notDrainingTargets, drainingTargets := partitionTargetsByDrainingStatus(targets)
+	matchedEndpointAndTargets, unmatchedEndpoints, unmatchedTargets := matchPodEndpointWithTargets(endpoints, notDrainingTargets)
 	if err := m.deregisterTargets(ctx, tgARN, unmatchedTargets); err != nil {
 		return err
 	}
@@ -81,6 +82,7 @@ func (m *defaultResourceManager) reconcileWithIPTargetType(ctx context.Context,
 		return err
 	}
 	_ = matchedEndpointAndTargets
+	_ = drainingTargets
 	return nil
 }
 
@@ -142,6 +144,19 @@ type podEndpointAndTargetPair struct {
 	target   TargetInfo
 }
 
+func partitionTargetsByDrainingStatus(targets []TargetInfo) ([]TargetInfo, []TargetInfo) {
+	var notDrainingTargets []TargetInfo
+	var drainingTargets []TargetInfo
+	for _, target := range targets {
+		if target.IsDraining() {
+			drainingTargets = append(drainingTargets, target)
+		} else {
+			notDrainingTargets = append(notDrainingTargets, target)
+		}
+	}
+	return notDrainingTargets, drainingTargets
+}
+
 func matchPodEndpointWithTargets(endpoints []backend.PodEndpoint, targets []TargetInfo) ([]podEndpointAndTargetPair, []backend.PodEndpoint, []TargetInfo) {
 	var matchedEndpointAndTargets []podEndpointAndTargetPair
 	var unmatchedEndpoints []backend.PodEndpoint
diff --git a/pkg/targetgroupbinding/targets_manager_types.go b/pkg/targetgroupbinding/targets_manager_types.go
index 7809acaa0..56e4ae94b 100644
--- a/pkg/targetgroupbinding/targets_manager_types.go
+++ b/pkg/targetgroupbinding/targets_manager_types.go
@@ -33,6 +33,13 @@ func (t *TargetInfo) IsNotRegistered() bool {
 		awssdk.StringValue(t.TargetHealth.Reason) == elbv2sdk.TargetHealthReasonEnumTargetNotRegistered
 }
 
+func (t *TargetInfo) IsDraining() bool {
+	if t.TargetHealth == nil {
+		return false
+	}
+	return awssdk.StringValue(t.TargetHealth.State) == elbv2sdk.TargetHealthStateEnumDraining
+}
+
 // UniqueIDForTargetDescription generates a unique ID to differentiate targets.
 func UniqueIDForTargetDescription(target elbv2sdk.TargetDescription) string {
 	return fmt.Sprintf("%v:%v", awssdk.StringValue(target.Id), awssdk.Int64Value(target.Port))


Test patch:
diff --git a/pkg/backend/endpoint_resolver_test.go b/pkg/backend/endpoint_resolver_test.go
index fd66d1568..280fff99c 100644
--- a/pkg/backend/endpoint_resolver_test.go
+++ b/pkg/backend/endpoint_resolver_test.go
@@ -854,55 +854,3 @@ func Test_isPodMeetCriteria(t *testing.T) {
 		})
 	}
 }
-
-func Test_extractNodeInstanceID(t *testing.T) {
-	type args struct {
-		node *corev1.Node
-	}
-	tests := []struct {
-		name    string
-		args    args
-		want    string
-		wantErr error
-	}{
-		{
-			name: "node without providerID",
-			args: args{
-				node: &corev1.Node{
-					ObjectMeta: metav1.ObjectMeta{
-						Name: "my-node-name",
-					},
-					Spec: corev1.NodeSpec{
-						ProviderID: "",
-					},
-				},
-			},
-			wantErr: errors.New("providerID is not specified for node: my-node-name"),
-		},
-		{
-			name: "node with providerID",
-			args: args{
-				node: &corev1.Node{
-					ObjectMeta: metav1.ObjectMeta{
-						Name: "my-node-name",
-					},
-					Spec: corev1.NodeSpec{
-						ProviderID: "aws:///us-west-2b/i-abcdefg0",
-					},
-				},
-			},
-			want: "i-abcdefg0",
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			got, err := extractNodeInstanceID(tt.args.node)
-			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/node_utils_test.go b/pkg/k8s/node_utils_test.go
index 39fb6411a..ab0f68280 100644
--- a/pkg/k8s/node_utils_test.go
+++ b/pkg/k8s/node_utils_test.go
@@ -1,8 +1,10 @@
 package k8s
 
 import (
+	"github.com/pkg/errors"
 	"github.com/stretchr/testify/assert"
 	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"testing"
 )
 
@@ -122,3 +124,55 @@ func TestGetNodeCondition(t *testing.T) {
 		})
 	}
 }
+
+func TestExtractNodeInstanceID(t *testing.T) {
+	type args struct {
+		node *corev1.Node
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    string
+		wantErr error
+	}{
+		{
+			name: "node without providerID",
+			args: args{
+				node: &corev1.Node{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "my-node-name",
+					},
+					Spec: corev1.NodeSpec{
+						ProviderID: "",
+					},
+				},
+			},
+			wantErr: errors.New("providerID is not specified for node: my-node-name"),
+		},
+		{
+			name: "node with providerID",
+			args: args{
+				node: &corev1.Node{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "my-node-name",
+					},
+					Spec: corev1.NodeSpec{
+						ProviderID: "aws:///us-west-2b/i-abcdefg0",
+					},
+				},
+			},
+			want: "i-abcdefg0",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := ExtractNodeInstanceID(tt.args.node)
+			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/security_group_info_test.go b/pkg/networking/security_group_info_test.go
new file mode 100644
index 000000000..2b7e1ae62
--- /dev/null
+++ b/pkg/networking/security_group_info_test.go
@@ -0,0 +1,63 @@
+package networking
+
+import (
+	"github.com/stretchr/testify/assert"
+	"testing"
+)
+
+func Test_buildIPPermissionLabelForDescription(t *testing.T) {
+	type args struct {
+		description string
+	}
+	tests := []struct {
+		name string
+		args args
+		want map[string]string
+	}{
+		{
+			name: "empty description",
+			args: args{
+				description: "",
+			},
+			want: map[string]string{
+				labelKeyRawDescription: "",
+			},
+		},
+		{
+			name: "non-empty description",
+			args: args{
+				description: "some-description",
+			},
+			want: map[string]string{
+				labelKeyRawDescription: "some-description",
+			},
+		},
+		{
+			name: "single k-v pair description",
+			args: args{
+				description: "key1=value1",
+			},
+			want: map[string]string{
+				labelKeyRawDescription: "key1=value1",
+				"key1":                 "value1",
+			},
+		},
+		{
+			name: "multiple k-v pair description",
+			args: args{
+				description: "key1=value1,key2=value2",
+			},
+			want: map[string]string{
+				labelKeyRawDescription: "key1=value1,key2=value2",
+				"key1":                 "value1",
+				"key2":                 "value2",
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := buildIPPermissionLabelForDescription(tt.args.description)
+			assert.Equal(t, tt.want, got)
+		})
+	}
+}
diff --git a/pkg/service/nlb/builder_test.go b/pkg/service/nlb/builder_test.go
index c7590d0b0..725d3e803 100644
--- a/pkg/service/nlb/builder_test.go
+++ b/pkg/service/nlb/builder_test.go
@@ -2,6 +2,7 @@ package nlb
 
 import (
 	"context"
+	"errors"
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/stretchr/testify/assert"
 	corev1 "k8s.io/api/core/v1"
@@ -13,12 +14,14 @@ import (
 	"sigs.k8s.io/aws-alb-ingress-controller/pkg/k8s"
 	"sigs.k8s.io/aws-alb-ingress-controller/pkg/model/elbv2"
 	"testing"
+	"time"
 )
 
-func Test_buildNLB(t *testing.T) {
+func Test_nlbBuilder_buildNLB(t *testing.T) {
 	tests := []struct {
 		testName         string
 		svc              *corev1.Service
+		subnets          []string
 		wantError        bool
 		wantValue        string
 		wantNumResources int
@@ -45,6 +48,7 @@ func Test_buildNLB(t *testing.T) {
 					},
 				},
 			},
+			subnets:   []string{"subnet-1"},
 			wantError: false,
 			wantValue: `
 {
@@ -81,6 +85,11 @@ func Test_buildNLB(t *testing.T) {
           "name": "a",
           "type": "network",
           "scheme": "internet-facing",
+		  "subnetMapping": [
+			{
+			  "subnetID": "subnet-1"
+			}
+          ],
           "ipAddressType": "ipv4",
           "loadBalancerAttributes": [
             {
@@ -135,7 +144,7 @@ func Test_buildNLB(t *testing.T) {
           },
           "template": {
             "metadata": {
-              "generateName": "k8s-nlb-ip-s-default-507734fb10",
+              "name": "k8s-nlb-ip-s-default-507734fb10",
               "namespace": "default",
               "creationTimestamp": null
             },
@@ -193,6 +202,7 @@ func Test_buildNLB(t *testing.T) {
 					},
 				},
 			},
+			subnets:   []string{"subnet-abc", "test-subnet"},
 			wantError: false,
 			wantValue: `
 {
@@ -262,7 +272,7 @@ func Test_buildNLB(t *testing.T) {
               }
             },
             "metadata": {
-              "generateName": "k8s-nlb-ip-s-default-03582c76a7",
+              "name": "k8s-nlb-ip-s-default-03582c76a7",
               "namespace": "default",
               "creationTimestamp": null
             }
@@ -276,6 +286,14 @@ func Test_buildNLB(t *testing.T) {
           "ipAddressType": "ipv4",
           "type": "network",
           "scheme": "internet-facing",
+		  "subnetMapping": [
+			{
+			  "subnetID": "subnet-abc"
+			},
+			{
+			  "subnetID": "test-subnet"
+			}
+          ],
           "name": "a7ab4be3311c24a7bb6557add8affab3",
           "loadBalancerAttributes": [
             {
@@ -371,6 +389,7 @@ func Test_buildNLB(t *testing.T) {
 					},
 				},
 			},
+			subnets:   []string{"s-1", "s-2", "s-3"},
 			wantError: false,
 			wantValue: `
 {
@@ -448,7 +467,7 @@ func Test_buildNLB(t *testing.T) {
               }
             },
             "metadata": {
-              "generateName": "k8s-nlb-ip-s-default-03582c76a7",
+              "name": "k8s-nlb-ip-s-default-03582c76a7",
               "namespace": "default",
               "creationTimestamp": null
             }
@@ -470,7 +489,7 @@ func Test_buildNLB(t *testing.T) {
               }
             },
             "metadata": {
-              "generateName": "k8s-nlb-ip-s-default-f4577ac8db",
+              "name": "k8s-nlb-ip-s-default-f4577ac8db",
               "namespace": "default",
               "creationTimestamp": null
             }
@@ -484,6 +503,17 @@ func Test_buildNLB(t *testing.T) {
           "ipAddressType": "ipv4",
           "type": "network",
           "scheme": "internet-facing",
+		  "subnetMapping": [
+			{
+			  "subnetID": "s-1"
+			},
+			{
+			  "subnetID": "s-2"
+			},
+			{
+			  "subnetID": "s-3"
+			}
+          ],
           "name": "a7ab4be3311c24a7bb6557add8affab3",
           "loadBalancerAttributes": [
             {
@@ -559,12 +589,30 @@ func Test_buildNLB(t *testing.T) {
 `,
 			wantNumResources: 7,
 		},
+		{
+			testName: "Service being deleted",
+			svc: &corev1.Service{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "service-deleted",
+					Namespace: "doesnt-exist",
+					DeletionTimestamp: &metav1.Time{
+						Time: time.Now(),
+					},
+				},
+			},
+			wantValue: `
+{
+  "id": "doesnt-exist/service-deleted",
+  "resources": {} 
+}
+`,
+		},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.testName, func(t *testing.T) {
 			parser := annotations.NewSuffixAnnotationParser("service.beta.kubernetes.io")
-			builder := NewServiceBuilder(tt.svc, k8s.NamespacedName(tt.svc), parser)
+			builder := NewServiceBuilder(tt.svc, NewMockSubnetsResolver(tt.subnets), k8s.NamespacedName(tt.svc), parser)
 			ctx := context.Background()
 			stack, err := builder.Build(ctx)
 			if tt.wantError {
@@ -583,7 +631,7 @@ func Test_buildNLB(t *testing.T) {
 	}
 }
 
-func Test_buildLBAttributes(t *testing.T) {
+func Test_nlbBuilder_buildLBAttributes(t *testing.T) {
 	tests := []struct {
 		testName  string
 		svc       *corev1.Service
@@ -686,7 +734,7 @@ func Test_buildLBAttributes(t *testing.T) {
 	}
 }
 
-func Test_targetGroupAttrs(t *testing.T) {
+func Test_nlbBuilder_targetGroupAttrs(t *testing.T) {
 	tests := []struct {
 		testName  string
 		svc       *corev1.Service
@@ -755,7 +803,7 @@ func Test_targetGroupAttrs(t *testing.T) {
 	}
 }
 
-func Test_buildTargetHealthCheck(t *testing.T) {
+func Test_nlbBuilder_buildTargetHealthCheck(t *testing.T) {
 	trafficPort := intstr.FromString("traffic-port")
 	port8888 := intstr.FromInt(8888)
 	tests := []struct {
@@ -861,3 +909,42 @@ func Test_buildTargetHealthCheck(t *testing.T) {
 		})
 	}
 }
+
+func Test_nlbBuilder_getSubnetMappings(t *testing.T) {
+	tests := []struct {
+		name    string
+		subnets []string
+		want    []elbv2.SubnetMapping
+		wantErr error
+	}{
+		{
+			name:    "Empty subnets",
+			subnets: []string{},
+			wantErr: errors.New("Unable to discover at least 1 subnet across availability zones"),
+		},
+		{
+			name:    "Multiple subnets",
+			subnets: []string{"s-1", "s-2"},
+			want: []elbv2.SubnetMapping{
+				{
+					SubnetID: "s-1",
+				},
+				{
+					SubnetID: "s-2",
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			builder := &nlbBuilder{}
+			got, err := builder.getSubnetMappings(tt.subnets)
+			if tt.wantErr != nil {
+				assert.EqualError(t, err, tt.wantErr.Error())
+			} else {
+				assert.Equal(t, tt.want, got)
+			}
+		})
+	}
+}