Repository: kubernetes-sigs/aws-alb-ingress-controller
Pull request #1507 (2020-10-15 15:35:51)

add unit tests and fix annotation-based forward
1. add unit tests for modelBuilders
2. fix annotation-based forward when numeric port is used. 
   1. new controller: both `serviceName: "80"` and `serviceName: 80` works and points to numeric 80 port.
   2. old controller: only `serviceName: "80"` works and points to numeric 80 port.
3. add the stackID into model build logs

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/1507#" 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  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/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/ingress/group_controller.go b/controllers/ingress/group_controller.go
index 23431c9d2..6daa99db9 100644
--- a/controllers/ingress/group_controller.go
+++ b/controllers/ingress/group_controller.go
@@ -148,7 +148,7 @@ func (r *groupReconciler) buildAndDeployModel(ctx context.Context, ingGroup ingr
 	if err := r.stackDeployer.Deploy(ctx, stack); err != nil {
 		return nil, nil, err
 	}
-	r.logger.Info("successfully deployed model")
+	r.logger.Info("successfully deployed model", "ingressGroup", ingGroup.ID)
 	return stack, lb, err
 }
 
diff --git a/controllers/service/service_controller.go b/controllers/service/service_controller.go
index 390ea9d65..a92c37853 100644
--- a/controllers/service/service_controller.go
+++ b/controllers/service/service_controller.go
@@ -99,7 +99,7 @@ func (r *serviceReconciler) buildAndDeployModel(ctx context.Context, svc *corev1
 	if err = r.stackDeployer.Deploy(ctx, stack); err != nil {
 		return nil, nil, err
 	}
-	r.logger.Info("successfully deployed model")
+	r.logger.Info("successfully deployed model", "service", k8s.NamespacedName(svc))
 
 	return stack, lb, nil
 }
diff --git a/docs/guide/ingress/annotations.md b/docs/guide/ingress/annotations.md
index 51c2c096f..611ae9a14 100644
--- a/docs/guide/ingress/annotations.md
+++ b/docs/guide/ingress/annotations.md
@@ -199,7 +199,7 @@ Traffic Routing can be controlled with following annotations:
             alb.ingress.kubernetes.io/actions.forward-single-tg: >
               {"type":"forward","targetGroupARN": "arn-of-your-target-group"}
             alb.ingress.kubernetes.io/actions.forward-multiple-tg: >
-              {"type":"forward","forwardConfig":{"targetGroups":[{"serviceName":"service-1","servicePort":"80","weight":20},{"serviceName":"service-2","servicePort":"80","weight":20},{"targetGroupARN":"arn-of-your-non-k8s-target-group","weight":60}],"targetGroupStickinessConfig":{"enabled":true,"durationSeconds":200}}}
+              {"type":"forward","forwardConfig":{"targetGroups":[{"serviceName":"service-1","servicePort":"http","weight":20},{"serviceName":"service-2","servicePort":80,"weight":20},{"targetGroupARN":"arn-of-your-non-k8s-target-group","weight":60}],"targetGroupStickinessConfig":{"enabled":true,"durationSeconds":200}}}
         spec:
           rules:
             - http:
diff --git a/mocks/ingress/mock_cert_discovery.go b/mocks/ingress/mock_cert_discovery.go
new file mode 100644
index 000000000..d215e75b1
--- /dev/null
+++ b/mocks/ingress/mock_cert_discovery.go
@@ -0,0 +1,49 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: sigs.k8s.io/aws-load-balancer-controller/pkg/ingress (interfaces: CertDiscovery)
+
+// Package mock_ingress is a generated GoMock package.
+package mock_ingress
+
+import (
+	context "context"
+	gomock "github.com/golang/mock/gomock"
+	reflect "reflect"
+)
+
+// MockCertDiscovery is a mock of CertDiscovery interface
+type MockCertDiscovery struct {
+	ctrl     *gomock.Controller
+	recorder *MockCertDiscoveryMockRecorder
+}
+
+// MockCertDiscoveryMockRecorder is the mock recorder for MockCertDiscovery
+type MockCertDiscoveryMockRecorder struct {
+	mock *MockCertDiscovery
+}
+
+// NewMockCertDiscovery creates a new mock instance
+func NewMockCertDiscovery(ctrl *gomock.Controller) *MockCertDiscovery {
+	mock := &MockCertDiscovery{ctrl: ctrl}
+	mock.recorder = &MockCertDiscoveryMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use
+func (m *MockCertDiscovery) EXPECT() *MockCertDiscoveryMockRecorder {
+	return m.recorder
+}
+
+// Discover mocks base method
+func (m *MockCertDiscovery) Discover(arg0 context.Context, arg1 []string) ([]string, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Discover", arg0, arg1)
+	ret0, _ := ret[0].([]string)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Discover indicates an expected call of Discover
+func (mr *MockCertDiscoveryMockRecorder) Discover(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Discover", reflect.TypeOf((*MockCertDiscovery)(nil).Discover), arg0, arg1)
+}
diff --git a/mocks/networking/mock_subnet_resolver.go b/mocks/networking/mock_subnet_resolver.go
new file mode 100644
index 000000000..605d72fc1
--- /dev/null
+++ b/mocks/networking/mock_subnet_resolver.go
@@ -0,0 +1,51 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: sigs.k8s.io/aws-load-balancer-controller/pkg/networking (interfaces: SubnetsResolver)
+
+// Package mock_networking is a generated GoMock package.
+package mock_networking
+
+import (
+	context "context"
+	ec2 "github.com/aws/aws-sdk-go/service/ec2"
+	gomock "github.com/golang/mock/gomock"
+	reflect "reflect"
+	elbv2 "sigs.k8s.io/aws-load-balancer-controller/pkg/model/elbv2"
+)
+
+// MockSubnetsResolver is a mock of SubnetsResolver interface
+type MockSubnetsResolver struct {
+	ctrl     *gomock.Controller
+	recorder *MockSubnetsResolverMockRecorder
+}
+
+// MockSubnetsResolverMockRecorder is the mock recorder for MockSubnetsResolver
+type MockSubnetsResolverMockRecorder struct {
+	mock *MockSubnetsResolver
+}
+
+// NewMockSubnetsResolver creates a new mock instance
+func NewMockSubnetsResolver(ctrl *gomock.Controller) *MockSubnetsResolver {
+	mock := &MockSubnetsResolver{ctrl: ctrl}
+	mock.recorder = &MockSubnetsResolverMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use
+func (m *MockSubnetsResolver) EXPECT() *MockSubnetsResolverMockRecorder {
+	return m.recorder
+}
+
+// DiscoverSubnets mocks base method
+func (m *MockSubnetsResolver) DiscoverSubnets(arg0 context.Context, arg1 elbv2.LoadBalancerScheme) ([]*ec2.Subnet, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "DiscoverSubnets", arg0, arg1)
+	ret0, _ := ret[0].([]*ec2.Subnet)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// DiscoverSubnets indicates an expected call of DiscoverSubnets
+func (mr *MockSubnetsResolverMockRecorder) DiscoverSubnets(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DiscoverSubnets", reflect.TypeOf((*MockSubnetsResolver)(nil).DiscoverSubnets), arg0, arg1)
+}
diff --git a/pkg/ingress/config_types.go b/pkg/ingress/config_types.go
index c53b0ba45..d90d59aa3 100644
--- a/pkg/ingress/config_types.go
+++ b/pkg/ingress/config_types.go
@@ -276,7 +276,7 @@ func (c *QueryStringKeyValuePair) validate() error {
 // 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"`
+	Values []QueryStringKeyValuePair `json:"values"`
 }
 
 func (c *QueryStringConditionConfig) validate() error {
diff --git a/pkg/ingress/enhanced_backend_builder.go b/pkg/ingress/enhanced_backend_builder.go
index fa4113bf4..f53867af9 100644
--- a/pkg/ingress/enhanced_backend_builder.go
+++ b/pkg/ingress/enhanced_backend_builder.go
@@ -102,6 +102,18 @@ func (b *defaultEnhancedBackendBuilder) buildActionViaAnnotation(_ context.Conte
 		}
 		action.TargetGroupARN = nil
 	}
+
+	// normalize servicePort to be int type if possible.
+	// this is for backwards-compatibility with old AWSALBIngressController, where ServicePort is defined as Type string.
+	if action.Type == ActionTypeForward && action.ForwardConfig != nil {
+		for _, tgt := range action.ForwardConfig.TargetGroups {
+			if tgt.ServicePort != nil {
+				normalizedSVCPort := intstr.Parse(tgt.ServicePort.String())
+				*tgt.ServicePort = normalizedSVCPort
+			}
+		}
+	}
+
 	return action, nil
 }
 
diff --git a/pkg/model/elbv2/listener_rule.go b/pkg/model/elbv2/listener_rule.go
index 6de803f08..161200ef1 100644
--- a/pkg/model/elbv2/listener_rule.go
+++ b/pkg/model/elbv2/listener_rule.go
@@ -106,17 +106,23 @@ type RuleCondition struct {
 	// The field in the HTTP request.
 	Field RuleConditionField `json:"field"`
 	// Information for a host header condition.
-	HostHeaderConfig *HostHeaderConditionConfig `json:"hostHeaderConfig"`
+	// +optional
+	HostHeaderConfig *HostHeaderConditionConfig `json:"hostHeaderConfig,omitempty"`
 	// Information for an HTTP header condition.
-	HTTPHeaderConfig *HTTPHeaderConditionConfig `json:"httpHeaderConfig"`
+	// +optional
+	HTTPHeaderConfig *HTTPHeaderConditionConfig `json:"httpHeaderConfig,omitempty"`
 	// Information for an HTTP method condition.
-	HTTPRequestMethodConfig *HTTPRequestMethodConditionConfig `json:"httpRequestMethodConfig"`
+	// +optional
+	HTTPRequestMethodConfig *HTTPRequestMethodConditionConfig `json:"httpRequestMethodConfig,omitempty"`
 	// Information for a path pattern condition.
-	PathPatternConfig *PathPatternConditionConfig `json:"pathPatternConfig"`
+	// +optional
+	PathPatternConfig *PathPatternConditionConfig `json:"pathPatternConfig,omitempty"`
 	// Information for a query string condition.
-	QueryStringConfig *QueryStringConditionConfig `json:"queryStringConfig"`
+	// +optional
+	QueryStringConfig *QueryStringConditionConfig `json:"queryStringConfig,omitempty"`
 	// Information for a source IP condition.
-	SourceIPConfig *SourceIPConditionConfig `json:"sourceIPConfig"`
+	// +optional
+	SourceIPConfig *SourceIPConditionConfig `json:"sourceIPConfig,omitempty"`
 }
 
 // ListenerRuleSpec defines the desired state of ListenerRule
diff --git a/scripts/gen_mocks.sh b/scripts/gen_mocks.sh
index 74a7f9e74..57126b958 100644
--- a/scripts/gen_mocks.sh
+++ b/scripts/gen_mocks.sh
@@ -4,4 +4,6 @@ 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/networking/mock_security_group_manager.go sigs.k8s.io/aws-load-balancer-controller/pkg/networking SecurityGroupManager
\ No newline at end of file
+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/ingress/enhanced_backend_builder_test.go b/pkg/ingress/enhanced_backend_builder_test.go
new file mode 100644
index 000000000..6812c0e95
--- /dev/null
+++ b/pkg/ingress/enhanced_backend_builder_test.go
@@ -0,0 +1,815 @@
+package ingress
+
+import (
+	"context"
+	awssdk "github.com/aws/aws-sdk-go/aws"
+	"github.com/google/go-cmp/cmp"
+	"github.com/pkg/errors"
+	"github.com/stretchr/testify/assert"
+	networking "k8s.io/api/networking/v1beta1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/intstr"
+	"sigs.k8s.io/aws-load-balancer-controller/pkg/annotations"
+	"testing"
+)
+
+func Test_defaultEnhancedBackendBuilder_Build(t *testing.T) {
+	type args struct {
+		ing     *networking.Ingress
+		backend networking.IngressBackend
+	}
+	portHTTP := intstr.FromString("http")
+	tests := []struct {
+		name    string
+		args    args
+		want    EnhancedBackend
+		wantErr error
+	}{
+		{
+			name: "vanilla serviceBackend",
+			args: args{
+				ing: &networking.Ingress{
+					ObjectMeta: metav1.ObjectMeta{
+						Annotations: map[string]string{},
+					},
+				},
+				backend: networking.IngressBackend{
+					ServiceName: "my-svc",
+					ServicePort: portHTTP,
+				},
+			},
+			want: EnhancedBackend{
+				Action: Action{
+					Type: ActionTypeForward,
+					ForwardConfig: &ForwardActionConfig{
+						TargetGroups: []TargetGroupTuple{
+							{
+								ServiceName: awssdk.String("my-svc"),
+								ServicePort: &portHTTP,
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "vanilla serviceBackend with additional conditions",
+			args: args{
+				ing: &networking.Ingress{
+					ObjectMeta: metav1.ObjectMeta{
+						Annotations: map[string]string{
+							"alb.ingress.kubernetes.io/conditions.my-svc": `[{"field":"http-header","httpHeaderConfig":{"httpHeaderName": "HeaderName", "values":["HeaderValue1", "HeaderValue2"]}}]`,
+						},
+					},
+				},
+				backend: networking.IngressBackend{
+					ServiceName: "my-svc",
+					ServicePort: portHTTP,
+				},
+			},
+			want: EnhancedBackend{
+				Action: Action{
+					Type: ActionTypeForward,
+					ForwardConfig: &ForwardActionConfig{
+						TargetGroups: []TargetGroupTuple{
+							{
+								ServiceName: awssdk.String("my-svc"),
+								ServicePort: &portHTTP,
+							},
+						},
+					},
+				},
+				Conditions: []RuleCondition{
+					{
+						Field: RuleConditionFieldHTTPHeader,
+						HTTPHeaderConfig: &HTTPHeaderConditionConfig{
+							HTTPHeaderName: "HeaderName",
+							Values:         []string{"HeaderValue1", "HeaderValue2"},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "annotation-based serviceBackend",
+			args: args{
+				ing: &networking.Ingress{
+					ObjectMeta: metav1.ObjectMeta{
+						Annotations: map[string]string{
+							"alb.ingress.kubernetes.io/actions.fake-my-svc": `{"type":"forward","forwardConfig":{"targetGroups":[{"serviceName":"my-svc","servicePort":"http"}]}}`,
+						},
+					},
+				},
+				backend: networking.IngressBackend{
+					ServiceName: "fake-my-svc",
+					ServicePort: intstr.FromString("use-annotation"),
+				},
+			},
+			want: EnhancedBackend{
+				Action: Action{
+					Type: ActionTypeForward,
+					ForwardConfig: &ForwardActionConfig{
+						TargetGroups: []TargetGroupTuple{
+							{
+								ServiceName: awssdk.String("my-svc"),
+								ServicePort: &portHTTP,
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "annotation-based with additional conditions",
+			args: args{
+				ing: &networking.Ingress{
+					ObjectMeta: metav1.ObjectMeta{
+						Annotations: map[string]string{
+							"alb.ingress.kubernetes.io/actions.fake-my-svc":    `{"type":"forward","forwardConfig":{"targetGroups":[{"serviceName":"my-svc","servicePort":"http"}]}}`,
+							"alb.ingress.kubernetes.io/conditions.fake-my-svc": `[{"field":"http-header","httpHeaderConfig":{"httpHeaderName": "HeaderName", "values":["HeaderValue1", "HeaderValue2"]}}]`,
+						},
+					},
+				},
+				backend: networking.IngressBackend{
+					ServiceName: "fake-my-svc",
+					ServicePort: intstr.FromString("use-annotation"),
+				},
+			},
+			want: EnhancedBackend{
+				Action: Action{
+					Type: ActionTypeForward,
+					ForwardConfig: &ForwardActionConfig{
+						TargetGroups: []TargetGroupTuple{
+							{
+								ServiceName: awssdk.String("my-svc"),
+								ServicePort: &portHTTP,
+							},
+						},
+					},
+				},
+				Conditions: []RuleCondition{
+					{
+						Field: RuleConditionFieldHTTPHeader,
+						HTTPHeaderConfig: &HTTPHeaderConditionConfig{
+							HTTPHeaderName: "HeaderName",
+							Values:         []string{"HeaderValue1", "HeaderValue2"},
+						},
+					},
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			annotationParser := annotations.NewSuffixAnnotationParser("alb.ingress.kubernetes.io")
+			b := &defaultEnhancedBackendBuilder{
+				annotationParser: annotationParser,
+			}
+			got, err := b.Build(context.Background(), tt.args.ing, tt.args.backend)
+			if tt.wantErr != nil {
+				assert.EqualError(t, err, tt.wantErr.Error())
+			} else {
+				assert.NoError(t, err)
+				assert.Equal(t, tt.want, got, "diff", cmp.Diff(tt.want, got))
+			}
+		})
+	}
+}
+
+func Test_defaultEnhancedBackendBuilder_buildConditions(t *testing.T) {
+	type args struct {
+		ingAnnotation map[string]string
+		svcName       string
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    []RuleCondition
+		wantErr error
+	}{
+		{
+			name: "host header condition",
+			args: args{
+				ingAnnotation: map[string]string{
+					"alb.ingress.kubernetes.io/conditions.rule-path1": `[{"field":"host-header","hostHeaderConfig":{"values":["anno.example.com"]}}]`,
+				},
+				svcName: "rule-path1",
+			},
+			want: []RuleCondition{
+				{
+					Field: RuleConditionFieldHostHeader,
+					HostHeaderConfig: &HostHeaderConditionConfig{
+						Values: []string{"anno.example.com"},
+					},
+				},
+			},
+		},
+		{
+			name: "host header condition - old camelcase case json key",
+			args: args{
+				ingAnnotation: map[string]string{
+					"alb.ingress.kubernetes.io/conditions.rule-path1": `[{"Field":"host-header","HostHeaderConfig":{"Values":["anno.example.com"]}}]`,
+				},
+				svcName: "rule-path1",
+			},
+			want: []RuleCondition{
+				{
+					Field: RuleConditionFieldHostHeader,
+					HostHeaderConfig: &HostHeaderConditionConfig{
+						Values: []string{"anno.example.com"},
+					},
+				},
+			},
+		},
+		{
+			name: "path pattern condition",
+			args: args{
+				ingAnnotation: map[string]string{
+					"alb.ingress.kubernetes.io/conditions.rule-path2": `[{"field":"path-pattern","pathPatternConfig":{"values":["/anno/path2"]}}]`,
+				},
+				svcName: "rule-path2",
+			},
+			want: []RuleCondition{
+				{
+					Field: RuleConditionFieldPathPattern,
+					PathPatternConfig: &PathPatternConditionConfig{
+						Values: []string{"/anno/path2"},
+					},
+				},
+			},
+		},
+		{
+			name: "path pattern condition - old camelcase case json key",
+			args: args{
+				ingAnnotation: map[string]string{
+					"alb.ingress.kubernetes.io/conditions.rule-path2": `[{"Field":"path-pattern","PathPatternConfig":{"Values":["/anno/path2"]}}]`,
+				},
+				svcName: "rule-path2",
+			},
+			want: []RuleCondition{
+				{
+					Field: RuleConditionFieldPathPattern,
+					PathPatternConfig: &PathPatternConditionConfig{
+						Values: []string{"/anno/path2"},
+					},
+				},
+			},
+		},
+		{
+			name: "http header condition",
+			args: args{
+				ingAnnotation: map[string]string{
+					"alb.ingress.kubernetes.io/conditions.rule-path3": `[{"field":"http-header","httpHeaderConfig":{"httpHeaderName": "HeaderName", "values":["HeaderValue1", "HeaderValue2"]}}]`,
+				},
+				svcName: "rule-path3",
+			},
+			want: []RuleCondition{
+				{
+					Field: RuleConditionFieldHTTPHeader,
+					HTTPHeaderConfig: &HTTPHeaderConditionConfig{
+						HTTPHeaderName: "HeaderName",
+						Values:         []string{"HeaderValue1", "HeaderValue2"},
+					},
+				},
+			},
+		},
+		{
+			name: "http header condition - old camelcase case json key",
+			args: args{
+				ingAnnotation: map[string]string{
+					"alb.ingress.kubernetes.io/conditions.rule-path3": `[{"Field":"http-header","HttpHeaderConfig":{"HttpHeaderName": "HeaderName", "Values":["HeaderValue1", "HeaderValue2"]}}]`,
+				},
+				svcName: "rule-path3",
+			},
+			want: []RuleCondition{
+				{
+					Field: RuleConditionFieldHTTPHeader,
+					HTTPHeaderConfig: &HTTPHeaderConditionConfig{
+						HTTPHeaderName: "HeaderName",
+						Values:         []string{"HeaderValue1", "HeaderValue2"},
+					},
+				},
+			},
+		},
+		{
+			name: "http request method condition",
+			args: args{
+				ingAnnotation: map[string]string{
+					"alb.ingress.kubernetes.io/conditions.rule-path4": `[{"field":"http-request-method","httpRequestMethodConfig":{"values":["GET", "HEAD"]}}]`,
+				},
+				svcName: "rule-path4",
+			},
+			want: []RuleCondition{
+				{
+					Field: RuleConditionFieldHTTPRequestMethod,
+					HTTPRequestMethodConfig: &HTTPRequestMethodConditionConfig{
+						Values: []string{"GET", "HEAD"},
+					},
+				},
+			},
+		},
+		{
+			name: "http request method condition - old camelcase case json key",
+			args: args{
+				ingAnnotation: map[string]string{
+					"alb.ingress.kubernetes.io/conditions.rule-path4": `[{"Field":"http-request-method","HttpRequestMethodConfig":{"Values":["GET", "HEAD"]}}]`,
+				},
+				svcName: "rule-path4",
+			},
+			want: []RuleCondition{
+				{
+					Field: RuleConditionFieldHTTPRequestMethod,
+					HTTPRequestMethodConfig: &HTTPRequestMethodConditionConfig{
+						Values: []string{"GET", "HEAD"},
+					},
+				},
+			},
+		},
+		{
+			name: "query string condition",
+			args: args{
+				ingAnnotation: map[string]string{
+					"alb.ingress.kubernetes.io/conditions.rule-path5": `[{"field":"query-string","queryStringConfig":{"values":[{"key":"paramA","value":"valueA1"},{"key":"paramA","value":"valueA2"}]}}]`,
+				},
+				svcName: "rule-path5",
+			},
+			want: []RuleCondition{
+				{
+					Field: RuleConditionFieldQueryString,
+					QueryStringConfig: &QueryStringConditionConfig{
+						Values: []QueryStringKeyValuePair{
+							{
+								Key:   awssdk.String("paramA"),
+								Value: "valueA1",
+							},
+							{
+								Key:   awssdk.String("paramA"),
+								Value: "valueA2",
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "query string condition - old camelcase case json key",
+			args: args{
+				ingAnnotation: map[string]string{
+					"alb.ingress.kubernetes.io/conditions.rule-path5": `[{"Field":"query-string","QueryStringConfig":{"Values":[{"Key":"paramA","Value":"valueA1"},{"Key":"paramA","Value":"valueA2"}]}}]`,
+				},
+				svcName: "rule-path5",
+			},
+			want: []RuleCondition{
+				{
+					Field: RuleConditionFieldQueryString,
+					QueryStringConfig: &QueryStringConditionConfig{
+						Values: []QueryStringKeyValuePair{
+							{
+								Key:   awssdk.String("paramA"),
+								Value: "valueA1",
+							},
+							{
+								Key:   awssdk.String("paramA"),
+								Value: "valueA2",
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "source IP condition",
+			args: args{
+				ingAnnotation: map[string]string{
+					"alb.ingress.kubernetes.io/conditions.rule-path6": `[{"field":"source-ip","sourceIpConfig":{"values":["192.168.0.0/16", "172.16.0.0/16"]}}]`,
+				},
+				svcName: "rule-path6",
+			},
+			want: []RuleCondition{
+				{
+					Field: RuleConditionFieldSourceIP,
+					SourceIPConfig: &SourceIPConditionConfig{
+						Values: []string{"192.168.0.0/16", "172.16.0.0/16"},
+					},
+				},
+			},
+		},
+		{
+			name: "source IP condition - old camelcase case json key",
+			args: args{
+				ingAnnotation: map[string]string{
+					"alb.ingress.kubernetes.io/conditions.rule-path6": `[{"Field":"source-ip","SourceIpConfig":{"Values":["192.168.0.0/16", "172.16.0.0/16"]}}]`,
+				},
+				svcName: "rule-path6",
+			},
+			want: []RuleCondition{
+				{
+					Field: RuleConditionFieldSourceIP,
+					SourceIPConfig: &SourceIPConditionConfig{
+						Values: []string{"192.168.0.0/16", "172.16.0.0/16"},
+					},
+				},
+			},
+		},
+		{
+			name: "multiple conditions",
+			args: args{
+				ingAnnotation: map[string]string{
+					"alb.ingress.kubernetes.io/conditions.rule-path7": `[{"field":"http-header","httpHeaderConfig":{"httpHeaderName": "HeaderName", "values":["HeaderValue"]}},{"field":"query-string","queryStringConfig":{"values":[{"key":"paramA","value":"valueA"}]}},{"field":"query-string","queryStringConfig":{"values":[{"key":"paramB","value":"valueB"}]}}]`,
+				},
+				svcName: "rule-path7",
+			},
+			want: []RuleCondition{
+				{
+					Field: RuleConditionFieldHTTPHeader,
+					HTTPHeaderConfig: &HTTPHeaderConditionConfig{
+						HTTPHeaderName: "HeaderName",
+						Values:         []string{"HeaderValue"},
+					},
+				},
+				{
+					Field: RuleConditionFieldQueryString,
+					QueryStringConfig: &QueryStringConditionConfig{
+						Values: []QueryStringKeyValuePair{
+							{
+								Key:   awssdk.String("paramA"),
+								Value: "valueA",
+							},
+						},
+					},
+				},
+				{
+					Field: RuleConditionFieldQueryString,
+					QueryStringConfig: &QueryStringConditionConfig{
+						Values: []QueryStringKeyValuePair{
+							{
+								Key:   awssdk.String("paramB"),
+								Value: "valueB",
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "multiple conditions - old camelcase case json key",
+			args: args{
+				ingAnnotation: map[string]string{
+					"alb.ingress.kubernetes.io/conditions.rule-path7": `[{"Field":"http-header","HttpHeaderConfig":{"HttpHeaderName": "HeaderName", "Values":["HeaderValue"]}},{"Field":"query-string","QueryStringConfig":{"Values":[{"Key":"paramA","Value":"valueA"}]}},{"Field":"query-string","QueryStringConfig":{"Values":[{"Key":"paramB","Value":"valueB"}]}}]`,
+				},
+				svcName: "rule-path7",
+			},
+			want: []RuleCondition{
+				{
+					Field: RuleConditionFieldHTTPHeader,
+					HTTPHeaderConfig: &HTTPHeaderConditionConfig{
+						HTTPHeaderName: "HeaderName",
+						Values:         []string{"HeaderValue"},
+					},
+				},
+				{
+					Field: RuleConditionFieldQueryString,
+					QueryStringConfig: &QueryStringConditionConfig{
+						Values: []QueryStringKeyValuePair{
+							{
+								Key:   awssdk.String("paramA"),
+								Value: "valueA",
+							},
+						},
+					},
+				},
+				{
+					Field: RuleConditionFieldQueryString,
+					QueryStringConfig: &QueryStringConditionConfig{
+						Values: []QueryStringKeyValuePair{
+							{
+								Key:   awssdk.String("paramB"),
+								Value: "valueB",
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			annotationParser := annotations.NewSuffixAnnotationParser("alb.ingress.kubernetes.io")
+			b := &defaultEnhancedBackendBuilder{
+				annotationParser: annotationParser,
+			}
+			got, err := b.buildConditions(context.Background(), tt.args.ingAnnotation, tt.args.svcName)
+			if tt.wantErr != nil {
+				assert.EqualError(t, err, tt.wantErr.Error())
+			} else {
+				assert.NoError(t, err)
+				assert.Equal(t, tt.want, got, "diff", cmp.Diff(tt.want, got))
+			}
+		})
+	}
+}
+
+func Test_defaultEnhancedBackendBuilder_buildActionViaAnnotation(t *testing.T) {
+	type args struct {
+		ingAnnotation map[string]string
+		svcName       string
+	}
+
+	portHTTP := intstr.FromString("http")
+	port80 := intstr.FromInt(80)
+	port443 := intstr.FromInt(443)
+	_ = port443
+	tests := []struct {
+		name    string
+		args    args
+		want    Action
+		wantErr error
+	}{
+		{
+			name: "forward action - simplified schema",
+			args: args{
+				ingAnnotation: map[string]string{
+					"alb.ingress.kubernetes.io/actions.forward-single-tg": `{"type":"forward","targetGroupARN": "tg-arn"}`,
+				},
+				svcName: "forward-single-tg",
+			},
+			want: Action{
+				Type: ActionTypeForward,
+				ForwardConfig: &ForwardActionConfig{
+					TargetGroups: []TargetGroupTuple{
+						{
+							TargetGroupARN: awssdk.String("tg-arn"),
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "forward action - simplified schema - old camelcase case json key",
+			args: args{
+				ingAnnotation: map[string]string{
+					"alb.ingress.kubernetes.io/actions.forward-single-tg": `{"Type":"forward","TargetGroupArn": "tg-arn"}`,
+				},
+				svcName: "forward-single-tg",
+			},
+			want: Action{
+				Type: ActionTypeForward,
+				ForwardConfig: &ForwardActionConfig{
+					TargetGroups: []TargetGroupTuple{
+						{
+							TargetGroupARN: awssdk.String("tg-arn"),
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "forward action - advanced schema",
+			args: args{
+				ingAnnotation: map[string]string{
+					"alb.ingress.kubernetes.io/actions.forward-multiple-tg": `{"type":"forward","forwardConfig":{"targetGroups":[{"serviceName":"service-1","servicePort":"http","weight":20},{"serviceName":"service-2","servicePort":80,"weight":20},{"targetGroupARN":"tg-arn","weight":60}],"targetGroupStickinessConfig":{"enabled":true,"durationSeconds":200}}}`,
+				},
+				svcName: "forward-multiple-tg",
+			},
+			want: Action{
+				Type: ActionTypeForward,
+				ForwardConfig: &ForwardActionConfig{
+					TargetGroups: []TargetGroupTuple{
+						{
+							ServiceName: awssdk.String("service-1"),
+							ServicePort: &portHTTP,
+							Weight:      awssdk.Int64(20),
+						},
+						{
+							ServiceName: awssdk.String("service-2"),
+							ServicePort: &port80,
+							Weight:      awssdk.Int64(20),
+						},
+						{
+							TargetGroupARN: awssdk.String("tg-arn"),
+							Weight:         awssdk.Int64(60),
+						},
+					},
+					TargetGroupStickinessConfig: &TargetGroupStickinessConfig{
+						Enabled:         awssdk.Bool(true),
+						DurationSeconds: awssdk.Int64(200),
+					},
+				},
+			},
+		},
+		{
+			name: "forward action - advanced schema - old camelcase case json key",
+			args: args{
+				ingAnnotation: map[string]string{
+					"alb.ingress.kubernetes.io/actions.forward-multiple-tg": `{"Type":"forward","ForwardConfig":{"TargetGroups":[{"ServiceName":"service-1","ServicePort":"http","Weight":20},{"ServiceName":"service-2","ServicePort":"80","Weight":20},{"TargetGroupArn":"tg-arn","Weight":60}],"TargetGroupStickinessConfig":{"Enabled":true,"DurationSeconds":200}}}`,
+				},
+				svcName: "forward-multiple-tg",
+			},
+			want: Action{
+				Type: ActionTypeForward,
+				ForwardConfig: &ForwardActionConfig{
+					TargetGroups: []TargetGroupTuple{
+						{
+							ServiceName: awssdk.String("service-1"),
+							ServicePort: &portHTTP,
+							Weight:      awssdk.Int64(20),
+						},
+						{
+							ServiceName: awssdk.String("service-2"),
+							ServicePort: &port80,
+							Weight:      awssdk.Int64(20),
+						},
+						{
+							TargetGroupARN: awssdk.String("tg-arn"),
+							Weight:         awssdk.Int64(60),
+						},
+					},
+					TargetGroupStickinessConfig: &TargetGroupStickinessConfig{
+						Enabled:         awssdk.Bool(true),
+						DurationSeconds: awssdk.Int64(200),
+					},
+				},
+			},
+		},
+		{
+			name: "forward action - advanced schema - both string format and integer format integers should be supported",
+			args: args{
+				ingAnnotation: map[string]string{
+					"alb.ingress.kubernetes.io/actions.forward-multiple-tg": `{"type":"forward","forwardConfig":{"targetGroups":[{"serviceName":"service-1","servicePort":"80","weight":40},{"serviceName":"service-2","servicePort":443,"weight":60}]}}`,
+				},
+				svcName: "forward-multiple-tg",
+			},
+			want: Action{
+				Type: ActionTypeForward,
+				ForwardConfig: &ForwardActionConfig{
+					TargetGroups: []TargetGroupTuple{
+						{
+							ServiceName: awssdk.String("service-1"),
+							ServicePort: &port80,
+							Weight:      awssdk.Int64(40),
+						},
+						{
+							ServiceName: awssdk.String("service-2"),
+							ServicePort: &port443,
+							Weight:      awssdk.Int64(60),
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "redirect action",
+			args: args{
+				ingAnnotation: map[string]string{
+					"alb.ingress.kubernetes.io/actions.redirect-to-eks": `{"type":"redirect","redirectConfig":{"host":"aws.amazon.com","path":"/eks/","port":"443","protocol":"HTTPS","query":"k=v","statusCode":"HTTP_302"}}`,
+				},
+				svcName: "redirect-to-eks",
+			},
+			want: Action{
+				Type: ActionTypeRedirect,
+				RedirectConfig: &RedirectActionConfig{
+					Host:       awssdk.String("aws.amazon.com"),
+					Path:       awssdk.String("/eks/"),
+					Port:       awssdk.String("443"),
+					Protocol:   awssdk.String("HTTPS"),
+					Query:      awssdk.String("k=v"),
+					StatusCode: "HTTP_302",
+				},
+			},
+		},
+		{
+			name: "redirect action - old camelcase case json key",
+			args: args{
+				ingAnnotation: map[string]string{
+					"alb.ingress.kubernetes.io/actions.redirect-to-eks": `{"Type":"redirect","RedirectConfig":{"Host":"aws.amazon.com","Path":"/eks/","Port":"443","Protocol":"HTTPS","Query":"k=v","StatusCode":"HTTP_302"}}`,
+				},
+				svcName: "redirect-to-eks",
+			},
+			want: Action{
+				Type: ActionTypeRedirect,
+				RedirectConfig: &RedirectActionConfig{
+					Host:       awssdk.String("aws.amazon.com"),
+					Path:       awssdk.String("/eks/"),
+					Port:       awssdk.String("443"),
+					Protocol:   awssdk.String("HTTPS"),
+					Query:      awssdk.String("k=v"),
+					StatusCode: "HTTP_302",
+				},
+			},
+		},
+		{
+			name: "redirect action - ssl-redirect",
+			args: args{
+				ingAnnotation: map[string]string{
+					"alb.ingress.kubernetes.io/actions.ssl-redirect": `{"type":"redirect","redirectConfig":{"port":"443","protocol":"HTTPS","statusCode":"HTTP_301"}}`,
+				},
+				svcName: "ssl-redirect",
+			},
+			want: Action{
+				Type: ActionTypeRedirect,
+				RedirectConfig: &RedirectActionConfig{
+					Port:       awssdk.String("443"),
+					Protocol:   awssdk.String("HTTPS"),
+					StatusCode: "HTTP_301",
+				},
+			},
+		},
+		{
+			name: "fixed response action",
+			args: args{
+				ingAnnotation: map[string]string{
+					"alb.ingress.kubernetes.io/actions.response-503": `{"type":"fixed-response","fixedResponseConfig":{"contentType":"text/plain","statusCode":"503","messageBody":"503 error text"}}`,
+				},
+				svcName: "response-503",
+			},
+			want: Action{
+				Type: ActionTypeFixedResponse,
+				FixedResponseConfig: &FixedResponseActionConfig{
+					ContentType: awssdk.String("text/plain"),
+					MessageBody: awssdk.String("503 error text"),
+					StatusCode:  "503",
+				},
+			},
+		},
+		{
+			name: "fixed response action - old camelcase case json key",
+			args: args{
+				ingAnnotation: map[string]string{
+					"alb.ingress.kubernetes.io/actions.response-503": `{"Type":"fixed-response","FixedResponseConfig":{"ContentType":"text/plain","StatusCode":"503","MessageBody":"503 error text"}}`,
+				},
+				svcName: "response-503",
+			},
+			want: Action{
+				Type: ActionTypeFixedResponse,
+				FixedResponseConfig: &FixedResponseActionConfig{
+					ContentType: awssdk.String("text/plain"),
+					MessageBody: awssdk.String("503 error text"),
+					StatusCode:  "503",
+				},
+			},
+		},
+		{
+			name: "non-exists action",
+			args: args{
+				ingAnnotation: map[string]string{},
+				svcName:       "non-exists",
+			},
+			wantErr: errors.New("missing actions.non-exists configuration"),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			annotationParser := annotations.NewSuffixAnnotationParser("alb.ingress.kubernetes.io")
+			b := &defaultEnhancedBackendBuilder{
+				annotationParser: annotationParser,
+			}
+			got, err := b.buildActionViaAnnotation(context.Background(), tt.args.ingAnnotation, tt.args.svcName)
+			if tt.wantErr != nil {
+				assert.EqualError(t, err, tt.wantErr.Error())
+			} else {
+				assert.NoError(t, err)
+				assert.Equal(t, tt.want, got, "diff", cmp.Diff(tt.want, got))
+			}
+		})
+	}
+}
+
+func Test_defaultEnhancedBackendBuilder_buildActionViaServiceAndServicePort(t *testing.T) {
+	portHTTP := intstr.FromString("http")
+	type args struct {
+		svcName string
+		svcPort intstr.IntOrString
+	}
+	tests := []struct {
+		name string
+		args args
+		want Action
+	}{
+		{
+			name: "standard case",
+			args: args{
+				svcName: "my-svc",
+				svcPort: portHTTP,
+			},
+			want: Action{
+				Type: ActionTypeForward,
+				ForwardConfig: &ForwardActionConfig{
+					TargetGroups: []TargetGroupTuple{
+						{
+							ServiceName: awssdk.String("my-svc"),
+							ServicePort: &portHTTP,
+						},
+					},
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			b := &defaultEnhancedBackendBuilder{}
+			got := b.buildActionViaServiceAndServicePort(context.Background(), tt.args.svcName, tt.args.svcPort)
+
+			assert.Equal(t, tt.want, got)
+		})
+	}
+}
diff --git a/pkg/ingress/model_builder_test.go b/pkg/ingress/model_builder_test.go
new file mode 100644
index 000000000..065dfa1eb
--- /dev/null
+++ b/pkg/ingress/model_builder_test.go
@@ -0,0 +1,1093 @@
+package ingress
+
+import (
+	"context"
+	awssdk "github.com/aws/aws-sdk-go/aws"
+	ec2sdk "github.com/aws/aws-sdk-go/service/ec2"
+	"github.com/golang/mock/gomock"
+	"github.com/stretchr/testify/assert"
+	corev1 "k8s.io/api/core/v1"
+	networking "k8s.io/api/networking/v1beta1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/util/intstr"
+	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
+	"k8s.io/client-go/tools/record"
+	mock_services "sigs.k8s.io/aws-load-balancer-controller/mocks/aws/services"
+	mock_ingress "sigs.k8s.io/aws-load-balancer-controller/mocks/ingress"
+	mock_networking "sigs.k8s.io/aws-load-balancer-controller/mocks/networking"
+	"sigs.k8s.io/aws-load-balancer-controller/pkg/annotations"
+	"sigs.k8s.io/aws-load-balancer-controller/pkg/deploy"
+	elbv2model "sigs.k8s.io/aws-load-balancer-controller/pkg/model/elbv2"
+	testclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
+	"sigs.k8s.io/controller-runtime/pkg/log"
+	"testing"
+)
+
+func Test_defaultModelBuilder_Build(t *testing.T) {
+	type discoverSubnetsCall struct {
+		schema  elbv2model.LoadBalancerScheme
+		subnets []*ec2sdk.Subnet
+		err     error
+	}
+
+	type env struct {
+		svcs []*corev1.Service
+	}
+	type fields struct {
+		discoverSubnetsCalls []discoverSubnetsCall
+	}
+	type args struct {
+		ingGroup Group
+	}
+
+	ns_1_svc_1 := &corev1.Service{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: "ns-1",
+			Name:      "svc-1",
+		},
+		Spec: corev1.ServiceSpec{
+			Ports: []corev1.ServicePort{
+				{
+					Name:       "http",
+					Port:       80,
+					TargetPort: intstr.FromInt(8080),
+				},
+			},
+		},
+	}
+	ns_1_svc_2 := &corev1.Service{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: "ns-1",
+			Name:      "svc-2",
+			Annotations: map[string]string{
+				"alb.ingress.kubernetes.io/target-type":                  "instance",
+				"alb.ingress.kubernetes.io/backend-protocol":             "HTTP",
+				"alb.ingress.kubernetes.io/healthcheck-protocol":         "HTTP",
+				"alb.ingress.kubernetes.io/healthcheck-port":             "traffic-port",
+				"alb.ingress.kubernetes.io/healthcheck-path":             "/",
+				"alb.ingress.kubernetes.io/healthcheck-interval-seconds": "15",
+				"alb.ingress.kubernetes.io/healthcheck-timeout-seconds":  "5",
+				"alb.ingress.kubernetes.io/healthy-threshold-count":      "2",
+				"alb.ingress.kubernetes.io/unhealthy-threshold-count":    "2",
+				"alb.ingress.kubernetes.io/success-codes":                "200",
+			},
+		},
+		Spec: corev1.ServiceSpec{
+			Ports: []corev1.ServicePort{
+				{
+					Name:       "http",
+					Port:       80,
+					TargetPort: intstr.FromInt(8080),
+				},
+			},
+		},
+	}
+	ns_1_svc_3 := &corev1.Service{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: "ns-1",
+			Name:      "svc-3",
+			Annotations: map[string]string{
+				"alb.ingress.kubernetes.io/target-type":                  "ip",
+				"alb.ingress.kubernetes.io/backend-protocol":             "HTTPS",
+				"alb.ingress.kubernetes.io/healthcheck-protocol":         "HTTPS",
+				"alb.ingress.kubernetes.io/healthcheck-port":             "9090",
+				"alb.ingress.kubernetes.io/healthcheck-path":             "/health-check",
+				"alb.ingress.kubernetes.io/healthcheck-interval-seconds": "20",
+				"alb.ingress.kubernetes.io/healthcheck-timeout-seconds":  "10",
+				"alb.ingress.kubernetes.io/healthy-threshold-count":      "7",
+				"alb.ingress.kubernetes.io/unhealthy-threshold-count":    "5",
+				"alb.ingress.kubernetes.io/success-codes":                "200-300",
+			},
+		},
+		Spec: corev1.ServiceSpec{
+			Ports: []corev1.ServicePort{
+				{
+					Name:       "https",
+					Port:       443,
+					TargetPort: intstr.FromInt(8443),
+				},
+			},
+		},
+	}
+
+	discoverSubnetsCallForInternalLB := discoverSubnetsCall{
+		schema: elbv2model.LoadBalancerSchemeInternal,
+		subnets: []*ec2sdk.Subnet{
+			{
+				SubnetId:  awssdk.String("subnet-a"),
+				CidrBlock: awssdk.String("192.168.0.0/19"),
+			},
+			{
+				SubnetId:  awssdk.String("subnet-b"),
+				CidrBlock: awssdk.String("192.168.32.0/19"),
+			},
+		},
+	}
+	discoverSubnetsCallForInternetFacingLB := discoverSubnetsCall{
+		schema: elbv2model.LoadBalancerSchemeInternetFacing,
+		subnets: []*ec2sdk.Subnet{
+			{
+				SubnetId:  awssdk.String("subnet-c"),
+				CidrBlock: awssdk.String("192.168.64.0/19"),
+			},
+			{
+				SubnetId:  awssdk.String("subnet-d"),
+				CidrBlock: awssdk.String("192.168.96.0/19"),
+			},
+		},
+	}
+
+	tests := []struct {
+		name          string
+		env           env
+		args          args
+		fields        fields
+		wantStackJSON string
+		wantErr       error
+	}{
+		{
+			name: "Ingress - vanilla internal",
+			env: env{
+				svcs: []*corev1.Service{ns_1_svc_1, ns_1_svc_2, ns_1_svc_3},
+			},
+			fields: fields{
+				discoverSubnetsCalls: []discoverSubnetsCall{discoverSubnetsCallForInternalLB},
+			},
+			args: args{
+				ingGroup: Group{
+					ID: GroupID{Namespace: "ns-1", Name: "ing-1"},
+					Members: []*networking.Ingress{
+						{
+							ObjectMeta: metav1.ObjectMeta{
+								Namespace: "ns-1",
+								Name:      "ing-1",
+							},
+							Spec: networking.IngressSpec{
+								Rules: []networking.IngressRule{
+									{
+										Host: "app-1.example.com",
+										IngressRuleValue: networking.IngressRuleValue{
+											HTTP: &networking.HTTPIngressRuleValue{
+												Paths: []networking.HTTPIngressPath{
+													{
+														Path: "/svc-1",
+														Backend: networking.IngressBackend{
+															ServiceName: ns_1_svc_1.Name,
+															ServicePort: intstr.FromString("http"),
+														},
+													},
+													{
+														Path: "/svc-2",
+														Backend: networking.IngressBackend{
+															ServiceName: ns_1_svc_2.Name,
+															ServicePort: intstr.FromString("http"),
+														},
+													},
+												},
+											},
+										},
+									},
+									{
+										Host: "app-2.example.com",
+										IngressRuleValue: networking.IngressRuleValue{
+											HTTP: &networking.HTTPIngressRuleValue{
+												Paths: []networking.HTTPIngressPath{
+													{
+														Path: "/svc-3",
+														Backend: networking.IngressBackend{
+															ServiceName: ns_1_svc_3.Name,
+															ServicePort: intstr.FromString("https"),
+														},
+													},
+												},
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			wantStackJSON: `
+{
+    "id":"ns-1/ing-1",
+    "resources":{
+        "AWS::EC2::SecurityGroup":{
+            "ManagedLBSecurityGroup":{
+                "spec":{
+                    "groupName":"k8s-ns1-ing1-bd83176788",
+                    "description":"[k8s] Managed SecurityGroup for LoadBalancer",
+                    "ingress":[
+                        {
+                            "ipProtocol":"tcp",
+                            "fromPort":80,
+                            "toPort":80,
+                            "ipRanges":[
+                                {
+                                    "cidrIP":"0.0.0.0/0"
+                                }
+                            ]
+                        }
+                    ]
+                }
+            }
+        },
+        "AWS::ElasticLoadBalancingV2::Listener":{
+            "80":{
+                "spec":{
+                    "loadBalancerARN":{
+                        "$ref":"#/resources/AWS::ElasticLoadBalancingV2::LoadBalancer/LoadBalancer/status/loadBalancerARN"
+                    },
+                    "port":80,
+                    "protocol":"HTTP",
+                    "defaultActions":[
+                        {
+                            "type":"fixed-response",
+                            "fixedResponseConfig":{
+                                "contentType":"text/plain",
+                                "statusCode":"404"
+                            }
+                        }
+                    ]
+                }
+            }
+        },
+        "AWS::ElasticLoadBalancingV2::ListenerRule":{
+            "80:1":{
+                "spec":{
+                    "listenerARN":{
+                        "$ref":"#/resources/AWS::ElasticLoadBalancingV2::Listener/80/status/listenerARN"
+                    },
+                    "priority":1,
+                    "actions":[
+                        {
+                            "type":"forward",
+                            "forwardConfig":{
+                                "targetGroups":[
+                                    {
+                                        "targetGroupARN":{
+                                            "$ref":"#/resources/AWS::ElasticLoadBalancingV2::TargetGroup/ns-1/ing-1-svc-1:http/status/targetGroupARN"
+                                        }
+                                    }
+                                ]
+                            }
+                        }
+                    ],
+                    "conditions":[
+                        {
+                            "field":"host-header",
+                            "hostHeaderConfig":{
+                                "values":[
+                                    "app-1.example.com"
+                                ]
+                            }
+                        },
+                        {
+                            "field":"path-pattern",
+                            "pathPatternConfig":{
+                                "values":[
+                                    "/svc-1"
+                                ]
+                            }
+                        }
+                    ]
+                }
+            },
+            "80:2":{
+                "spec":{
+                    "listenerARN":{
+                        "$ref":"#/resources/AWS::ElasticLoadBalancingV2::Listener/80/status/listenerARN"
+                    },
+                    "priority":2,
+                    "actions":[
+                        {
+                            "type":"forward",
+                            "forwardConfig":{
+                                "targetGroups":[
+                                    {
+                                        "targetGroupARN":{
+                                            "$ref":"#/resources/AWS::ElasticLoadBalancingV2::TargetGroup/ns-1/ing-1-svc-2:http/status/targetGroupARN"
+                                        }
+                                    }
+                                ]
+                            }
+                        }
+                    ],
+                    "conditions":[
+                        {
+                            "field":"host-header",
+                            "hostHeaderConfig":{
+                                "values":[
+                                    "app-1.example.com"
+                                ]
+                            }
+                        },
+                        {
+                            "field":"path-pattern",
+                            "pathPatternConfig":{
+                                "values":[
+                                    "/svc-2"
+                                ]
+                            }
+                        }
+                    ]
+                }
+            },
+            "80:3":{
+                "spec":{
+                    "listenerARN":{
+                        "$ref":"#/resources/AWS::ElasticLoadBalancingV2::Listener/80/status/listenerARN"
+                    },
+                    "priority":3,
+                    "actions":[
+                        {
+                            "type":"forward",
+                            "forwardConfig":{
+                                "targetGroups":[
+                                    {
+                                        "targetGroupARN":{
+                                            "$ref":"#/resources/AWS::ElasticLoadBalancingV2::TargetGroup/ns-1/ing-1-svc-3:https/status/targetGroupARN"
+                                        }
+                                    }
+                                ]
+                            }
+                        }
+                    ],
+                    "conditions":[
+                        {
+                            "field":"host-header",
+                            "hostHeaderConfig":{
+                                "values":[
+                                    "app-2.example.com"
+                                ]
+                            }
+                        },
+                        {
+                            "field":"path-pattern",
+                            "pathPatternConfig":{
+                                "values":[
+                                    "/svc-3"
+                                ]
+                            }
+                        }
+                    ]
+                }
+            }
+        },
+        "AWS::ElasticLoadBalancingV2::LoadBalancer":{
+            "LoadBalancer":{
+                "spec":{
+                    "name":"k8s-ns1-ing1-b7e914000d",
+                    "type":"application",
+                    "scheme":"internal",
+                    "ipAddressType":"ipv4",
+                    "subnetMapping":[
+                        {
+                            "subnetID":"subnet-a"
+                        },
+                        {
+                            "subnetID":"subnet-b"
+                        }
+                    ],
+                    "securityGroups":[
+                        {
+                            "$ref":"#/resources/AWS::EC2::SecurityGroup/ManagedLBSecurityGroup/status/groupID"
+                        }
+                    ]
+                }
+            }
+        },
+        "AWS::ElasticLoadBalancingV2::TargetGroup":{
+            "ns-1/ing-1-svc-1:http":{
+                "spec":{
+                    "name":"k8s-ns1-svc1-2718fcb5d7",
+                    "targetType":"instance",
+                    "port":1,
+                    "protocol":"HTTP",
+                    "healthCheckConfig":{
+                        "port":"traffic-port",
+                        "protocol":"HTTP",
+                        "path":"/",
+                        "matcher":{
+                            "httpCode":"200"
+                        },
+                        "intervalSeconds":15,
+                        "timeoutSeconds":5,
+                        "healthyThresholdCount":2,
+                        "unhealthyThresholdCount":2
+                    }
+                }
+            },
+            "ns-1/ing-1-svc-2:http":{
+                "spec":{
+                    "name":"k8s-ns1-svc2-2718fcb5d7",
+                    "targetType":"instance",
+                    "port":1,
+                    "protocol":"HTTP",
+                    "healthCheckConfig":{
+                        "port":"traffic-port",
+                        "protocol":"HTTP",
+                        "path":"/",
+                        "matcher":{
+                            "httpCode":"200"
+                        },
+                        "intervalSeconds":15,
+                        "timeoutSeconds":5,
+                        "healthyThresholdCount":2,
+                        "unhealthyThresholdCount":2
+                    }
+                }
+            },
+            "ns-1/ing-1-svc-3:https":{
+                "spec":{
+                    "name":"k8s-ns1-svc3-da71e18041",
+                    "targetType":"ip",
+                    "port":1,
+                    "protocol":"HTTPS",
+                    "healthCheckConfig":{
+                        "port":9090,
+                        "protocol":"HTTPS",
+                        "path":"/health-check",
+                        "matcher":{
+                            "httpCode":"200-300"
+                        },
+                        "intervalSeconds":20,
+                        "timeoutSeconds":10,
+                        "healthyThresholdCount":7,
+                        "unhealthyThresholdCount":5
+                    }
+                }
+            }
+        },
+        "K8S::ElasticLoadBalancingV2::TargetGroupBinding":{
+            "ns-1/ing-1-svc-1:http":{
+                "spec":{
+                    "template":{
+                        "metadata":{
+                            "name":"k8s-ns1-svc1-2718fcb5d7",
+                            "namespace":"ns-1",
+                            "creationTimestamp":null
+                        },
+                        "spec":{
+                            "targetGroupARN":{
+                                "$ref":"#/resources/AWS::ElasticLoadBalancingV2::TargetGroup/ns-1/ing-1-svc-1:http/status/targetGroupARN"
+                            },
+                            "targetType":"instance",
+                            "serviceRef":{
+                                "name":"svc-1",
+                                "port":"http"
+                            },
+                            "networking":{
+                                "ingress":[
+                                    {
+                                        "from":[
+                                            {
+                                                "securityGroup":{
+                                                    "groupID":{
+                                                        "$ref":"#/resources/AWS::EC2::SecurityGroup/ManagedLBSecurityGroup/status/groupID"
+                                                    }
+                                                }
+                                            }
+                                        ],
+                                        "ports":[
+                                            {
+                                                "protocol":"TCP"
+                                            }
+                                        ]
+                                    }
+                                ]
+                            }
+                        }
+                    }
+                }
+            },
+            "ns-1/ing-1-svc-2:http":{
+                "spec":{
+                    "template":{
+                        "metadata":{
+                            "name":"k8s-ns1-svc2-2718fcb5d7",
+                            "namespace":"ns-1",
+                            "creationTimestamp":null
+                        },
+                        "spec":{
+                            "targetGroupARN":{
+                                "$ref":"#/resources/AWS::ElasticLoadBalancingV2::TargetGroup/ns-1/ing-1-svc-2:http/status/targetGroupARN"
+                            },
+                            "targetType":"instance",
+                            "serviceRef":{
+                                "name":"svc-2",
+                                "port":"http"
+                            },
+                            "networking":{
+                                "ingress":[
+                                    {
+                                        "from":[
+                                            {
+                                                "securityGroup":{
+                                                    "groupID":{
+                                                        "$ref":"#/resources/AWS::EC2::SecurityGroup/ManagedLBSecurityGroup/status/groupID"
+                                                    }
+                                                }
+                                            }
+                                        ],
+                                        "ports":[
+                                            {
+                                                "protocol":"TCP"
+                                            }
+                                        ]
+                                    }
+                                ]
+                            }
+                        }
+                    }
+                }
+            },
+            "ns-1/ing-1-svc-3:https":{
+                "spec":{
+                    "template":{
+                        "metadata":{
+                            "name":"k8s-ns1-svc3-da71e18041",
+                            "namespace":"ns-1",
+                            "creationTimestamp":null
+                        },
+                        "spec":{
+                            "targetGroupARN":{
+                                "$ref":"#/resources/AWS::ElasticLoadBalancingV2::TargetGroup/ns-1/ing-1-svc-3:https/status/targetGroupARN"
+                            },
+                            "targetType":"ip",
+                            "serviceRef":{
+                                "name":"svc-3",
+                                "port":"https"
+                            },
+                            "networking":{
+                                "ingress":[
+                                    {
+                                        "from":[
+                                            {
+                                                "securityGroup":{
+                                                    "groupID":{
+                                                        "$ref":"#/resources/AWS::EC2::SecurityGroup/ManagedLBSecurityGroup/status/groupID"
+                                                    }
+                                                }
+                                            }
+                                        ],
+                                        "ports":[
+                                            {
+                                                "protocol":"TCP"
+                                            }
+                                        ]
+                                    }
+                                ]
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}`,
+		},
+		{
+			name: "Ingress - vanilla internet-facing",
+			env: env{
+				svcs: []*corev1.Service{ns_1_svc_1, ns_1_svc_2, ns_1_svc_3},
+			},
+			fields: fields{
+				discoverSubnetsCalls: []discoverSubnetsCall{discoverSubnetsCallForInternetFacingLB},
+			},
+			args: args{
+				ingGroup: Group{
+					ID: GroupID{Namespace: "ns-1", Name: "ing-1"},
+					Members: []*networking.Ingress{
+						{
+							ObjectMeta: metav1.ObjectMeta{
+								Namespace: "ns-1",
+								Name:      "ing-1",
+								Annotations: map[string]string{
+									"alb.ingress.kubernetes.io/scheme": "internet-facing",
+								},
+							},
+							Spec: networking.IngressSpec{
+								Rules: []networking.IngressRule{
+									{
+										Host: "app-1.example.com",
+										IngressRuleValue: networking.IngressRuleValue{
+											HTTP: &networking.HTTPIngressRuleValue{
+												Paths: []networking.HTTPIngressPath{
+													{
+														Path: "/svc-1",
+														Backend: networking.IngressBackend{
+															ServiceName: ns_1_svc_1.Name,
+															ServicePort: intstr.FromString("http"),
+														},
+													},
+													{
+														Path: "/svc-2",
+														Backend: networking.IngressBackend{
+															ServiceName: ns_1_svc_2.Name,
+															ServicePort: intstr.FromString("http"),
+														},
+													},
+												},
+											},
+										},
+									},
+									{
+										Host: "app-2.example.com",
+										IngressRuleValue: networking.IngressRuleValue{
+											HTTP: &networking.HTTPIngressRuleValue{
+												Paths: []networking.HTTPIngressPath{
+													{
+														Path: "/svc-3",
+														Backend: networking.IngressBackend{
+															ServiceName: ns_1_svc_3.Name,
+															ServicePort: intstr.FromString("https"),
+														},
+													},
+												},
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			wantStackJSON: `
+{
+    "id":"ns-1/ing-1",
+    "resources":{
+        "AWS::EC2::SecurityGroup":{
+            "ManagedLBSecurityGroup":{
+                "spec":{
+                    "groupName":"k8s-ns1-ing1-bd83176788",
+                    "description":"[k8s] Managed SecurityGroup for LoadBalancer",
+                    "ingress":[
+                        {
+                            "ipProtocol":"tcp",
+                            "fromPort":80,
+                            "toPort":80,
+                            "ipRanges":[
+                                {
+                                    "cidrIP":"0.0.0.0/0"
+                                }
+                            ]
+                        }
+                    ]
+                }
+            }
+        },
+        "AWS::ElasticLoadBalancingV2::Listener":{
+            "80":{
+                "spec":{
+                    "loadBalancerARN":{
+                        "$ref":"#/resources/AWS::ElasticLoadBalancingV2::LoadBalancer/LoadBalancer/status/loadBalancerARN"
+                    },
+                    "port":80,
+                    "protocol":"HTTP",
+                    "defaultActions":[
+                        {
+                            "type":"fixed-response",
+                            "fixedResponseConfig":{
+                                "contentType":"text/plain",
+                                "statusCode":"404"
+                            }
+                        }
+                    ]
+                }
+            }
+        },
+        "AWS::ElasticLoadBalancingV2::ListenerRule":{
+            "80:1":{
+                "spec":{
+                    "listenerARN":{
+                        "$ref":"#/resources/AWS::ElasticLoadBalancingV2::Listener/80/status/listenerARN"
+                    },
+                    "priority":1,
+                    "actions":[
+                        {
+                            "type":"forward",
+                            "forwardConfig":{
+                                "targetGroups":[
+                                    {
+                                        "targetGroupARN":{
+                                            "$ref":"#/resources/AWS::ElasticLoadBalancingV2::TargetGroup/ns-1/ing-1-svc-1:http/status/targetGroupARN"
+                                        }
+                                    }
+                                ]
+                            }
+                        }
+                    ],
+                    "conditions":[
+                        {
+                            "field":"host-header",
+                            "hostHeaderConfig":{
+                                "values":[
+                                    "app-1.example.com"
+                                ]
+                            }
+                        },
+                        {
+                            "field":"path-pattern",
+                            "pathPatternConfig":{
+                                "values":[
+                                    "/svc-1"
+                                ]
+                            }
+                        }
+                    ]
+                }
+            },
+            "80:2":{
+                "spec":{
+                    "listenerARN":{
+                        "$ref":"#/resources/AWS::ElasticLoadBalancingV2::Listener/80/status/listenerARN"
+                    },
+                    "priority":2,
+                    "actions":[
+                        {
+                            "type":"forward",
+                            "forwardConfig":{
+                                "targetGroups":[
+                                    {
+                                        "targetGroupARN":{
+                                            "$ref":"#/resources/AWS::ElasticLoadBalancingV2::TargetGroup/ns-1/ing-1-svc-2:http/status/targetGroupARN"
+                                        }
+                                    }
+                                ]
+                            }
+                        }
+                    ],
+                    "conditions":[
+                        {
+                            "field":"host-header",
+                            "hostHeaderConfig":{
+                                "values":[
+                                    "app-1.example.com"
+                                ]
+                            }
+                        },
+                        {
+                            "field":"path-pattern",
+                            "pathPatternConfig":{
+                                "values":[
+                                    "/svc-2"
+                                ]
+                            }
+                        }
+                    ]
+                }
+            },
+            "80:3":{
+                "spec":{
+                    "listenerARN":{
+                        "$ref":"#/resources/AWS::ElasticLoadBalancingV2::Listener/80/status/listenerARN"
+                    },
+                    "priority":3,
+                    "actions":[
+                        {
+                            "type":"forward",
+                            "forwardConfig":{
+                                "targetGroups":[
+                                    {
+                                        "targetGroupARN":{
+                                            "$ref":"#/resources/AWS::ElasticLoadBalancingV2::TargetGroup/ns-1/ing-1-svc-3:https/status/targetGroupARN"
+                                        }
+                                    }
+                                ]
+                            }
+                        }
+                    ],
+                    "conditions":[
+                        {
+                            "field":"host-header",
+                            "hostHeaderConfig":{
+                                "values":[
+                                    "app-2.example.com"
+                                ]
+                            }
+                        },
+                        {
+                            "field":"path-pattern",
+                            "pathPatternConfig":{
+                                "values":[
+                                    "/svc-3"
+                                ]
+                            }
+                        }
+                    ]
+                }
+            }
+        },
+        "AWS::ElasticLoadBalancingV2::LoadBalancer":{
+            "LoadBalancer":{
+                "spec":{
+                    "name":"k8s-ns1-ing1-159dd7a143",
+                    "type":"application",
+                    "scheme":"internet-facing",
+                    "ipAddressType":"ipv4",
+                    "subnetMapping":[
+                        {
+                            "subnetID":"subnet-c"
+                        },
+                        {
+                            "subnetID":"subnet-d"
+                        }
+                    ],
+                    "securityGroups":[
+                        {
+                            "$ref":"#/resources/AWS::EC2::SecurityGroup/ManagedLBSecurityGroup/status/groupID"
+                        }
+                    ]
+                }
+            }
+        },
+        "AWS::ElasticLoadBalancingV2::TargetGroup":{
+            "ns-1/ing-1-svc-1:http":{
+                "spec":{
+                    "name":"k8s-ns1-svc1-2718fcb5d7",
+                    "targetType":"instance",
+                    "port":1,
+                    "protocol":"HTTP",
+                    "healthCheckConfig":{
+                        "port":"traffic-port",
+                        "protocol":"HTTP",
+                        "path":"/",
+                        "matcher":{
+                            "httpCode":"200"
+                        },
+                        "intervalSeconds":15,
+                        "timeoutSeconds":5,
+                        "healthyThresholdCount":2,
+                        "unhealthyThresholdCount":2
+                    }
+                }
+            },
+            "ns-1/ing-1-svc-2:http":{
+                "spec":{
+                    "name":"k8s-ns1-svc2-2718fcb5d7",
+                    "targetType":"instance",
+                    "port":1,
+                    "protocol":"HTTP",
+                    "healthCheckConfig":{
+                        "port":"traffic-port",
+                        "protocol":"HTTP",
+                        "path":"/",
+                        "matcher":{
+                            "httpCode":"200"
+                        },
+                        "intervalSeconds":15,
+                        "timeoutSeconds":5,
+                        "healthyThresholdCount":2,
+                        "unhealthyThresholdCount":2
+                    }
+                }
+            },
+            "ns-1/ing-1-svc-3:https":{
+                "spec":{
+                    "name":"k8s-ns1-svc3-da71e18041",
+                    "targetType":"ip",
+                    "port":1,
+                    "protocol":"HTTPS",
+                    "healthCheckConfig":{
+                        "port":9090,
+                        "protocol":"HTTPS",
+                        "path":"/health-check",
+                        "matcher":{
+                            "httpCode":"200-300"
+                        },
+                        "intervalSeconds":20,
+                        "timeoutSeconds":10,
+                        "healthyThresholdCount":7,
+                        "unhealthyThresholdCount":5
+                    }
+                }
+            }
+        },
+        "K8S::ElasticLoadBalancingV2::TargetGroupBinding":{
+            "ns-1/ing-1-svc-1:http":{
+                "spec":{
+                    "template":{
+                        "metadata":{
+                            "name":"k8s-ns1-svc1-2718fcb5d7",
+                            "namespace":"ns-1",
+                            "creationTimestamp":null
+                        },
+                        "spec":{
+                            "targetGroupARN":{
+                                "$ref":"#/resources/AWS::ElasticLoadBalancingV2::TargetGroup/ns-1/ing-1-svc-1:http/status/targetGroupARN"
+                            },
+                            "targetType":"instance",
+                            "serviceRef":{
+                                "name":"svc-1",
+                                "port":"http"
+                            },
+                            "networking":{
+                                "ingress":[
+                                    {
+                                        "from":[
+                                            {
+                                                "securityGroup":{
+                                                    "groupID":{
+                                                        "$ref":"#/resources/AWS::EC2::SecurityGroup/ManagedLBSecurityGroup/status/groupID"
+                                                    }
+                                                }
+                                            }
+                                        ],
+                                        "ports":[
+                                            {
+                                                "protocol":"TCP"
+                                            }
+                                        ]
+                                    }
+                                ]
+                            }
+                        }
+                    }
+                }
+            },
+            "ns-1/ing-1-svc-2:http":{
+                "spec":{
+                    "template":{
+                        "metadata":{
+                            "name":"k8s-ns1-svc2-2718fcb5d7",
+                            "namespace":"ns-1",
+                            "creationTimestamp":null
+                        },
+                        "spec":{
+                            "targetGroupARN":{
+                                "$ref":"#/resources/AWS::ElasticLoadBalancingV2::TargetGroup/ns-1/ing-1-svc-2:http/status/targetGroupARN"
+                            },
+                            "targetType":"instance",
+                            "serviceRef":{
+                                "name":"svc-2",
+                                "port":"http"
+                            },
+                            "networking":{
+                                "ingress":[
+                                    {
+                                        "from":[
+                                            {
+                                                "securityGroup":{
+                                                    "groupID":{
+                                                        "$ref":"#/resources/AWS::EC2::SecurityGroup/ManagedLBSecurityGroup/status/groupID"
+                                                    }
+                                                }
+                                            }
+                                        ],
+                                        "ports":[
+                                            {
+                                                "protocol":"TCP"
+                                            }
+                                        ]
+                                    }
+                                ]
+                            }
+                        }
+                    }
+                }
+            },
+            "ns-1/ing-1-svc-3:https":{
+                "spec":{
+                    "template":{
+                        "metadata":{
+                            "name":"k8s-ns1-svc3-da71e18041",
+                            "namespace":"ns-1",
+                            "creationTimestamp":null
+                        },
+                        "spec":{
+                            "targetGroupARN":{
+                                "$ref":"#/resources/AWS::ElasticLoadBalancingV2::TargetGroup/ns-1/ing-1-svc-3:https/status/targetGroupARN"
+                            },
+                            "targetType":"ip",
+                            "serviceRef":{
+                                "name":"svc-3",
+                                "port":"https"
+                            },
+                            "networking":{
+                                "ingress":[
+                                    {
+                                        "from":[
+                                            {
+                                                "securityGroup":{
+                                                    "groupID":{
+                                                        "$ref":"#/resources/AWS::EC2::SecurityGroup/ManagedLBSecurityGroup/status/groupID"
+                                                    }
+                                                }
+                                            }
+                                        ],
+                                        "ports":[
+                                            {
+                                                "protocol":"TCP"
+                                            }
+                                        ]
+                                    }
+                                ]
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}`,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ctrl := gomock.NewController(t)
+			defer ctrl.Finish()
+
+			ctx := context.Background()
+			k8sSchema := runtime.NewScheme()
+			clientgoscheme.AddToScheme(k8sSchema)
+			k8sClient := testclient.NewFakeClientWithScheme(k8sSchema)
+			for _, svc := range tt.env.svcs {
+				assert.NoError(t, k8sClient.Create(ctx, svc.DeepCopy()))
+			}
+			eventRecorder := record.NewFakeRecorder(10)
+			vpcID := "vpc-dummy"
+			clusterName := "cluster-dummy"
+			ec2Client := mock_services.NewMockEC2(ctrl)
+			subnetsResolver := mock_networking.NewMockSubnetsResolver(ctrl)
+			for _, call := range tt.fields.discoverSubnetsCalls {
+				subnetsResolver.EXPECT().DiscoverSubnets(gomock.Any(), call.schema).Return(call.subnets, call.err)
+			}
+			certDiscovery := mock_ingress.NewMockCertDiscovery(ctrl)
+			annotationParser := annotations.NewSuffixAnnotationParser("alb.ingress.kubernetes.io")
+			authConfigBuilder := NewDefaultAuthConfigBuilder(annotationParser)
+			enhancedBackendBuilder := NewDefaultEnhancedBackendBuilder(annotationParser)
+			ruleOptimizer := NewDefaultRuleOptimizer(&log.NullLogger{})
+
+			stackMarshaller := deploy.NewDefaultStackMarshaller()
+
+			b := &defaultModelBuilder{
+				k8sClient:              k8sClient,
+				eventRecorder:          eventRecorder,
+				ec2Client:              ec2Client,
+				vpcID:                  vpcID,
+				clusterName:            clusterName,
+				annotationParser:       annotationParser,
+				subnetsResolver:        subnetsResolver,
+				certDiscovery:          certDiscovery,
+				authConfigBuilder:      authConfigBuilder,
+				enhancedBackendBuilder: enhancedBackendBuilder,
+				ruleOptimizer:          ruleOptimizer,
+				logger:                 &log.NullLogger{},
+			}
+
+			gotStack, _, err := b.Build(context.Background(), tt.args.ingGroup)
+			if tt.wantErr != nil {
+				assert.EqualError(t, err, tt.wantErr.Error())
+			} else {
+				assert.NoError(t, err)
+				stackJSON, err := stackMarshaller.Marshal(gotStack)
+				assert.NoError(t, err)
+				assert.JSONEq(t, tt.wantStackJSON, stackJSON)
+			}
+		})
+	}
+}