Repository: zalando-incubator/kube-ingress-aws-controller
Pull request #242 (2019-03-16 19:58:14)

Add support for Network Load Balancers
first part to support NLB #236

resource: 
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticloadbalancingv2-loadbalancer.html 

Signed-off-by: Sandor Szuecs <sandor.szuecs@zalando.de>

coveralls: 
[![Coverage Status](https://coveralls.io/builds/22223548/badge)](https://coveralls.io/builds/22223548)

Coverage increased (+0.08%) to 46.839% when pulling **adb961e54940dde50dd35dd8cdd3253b77f9b15c on feature/network-loadbalancer** into **c5fd0b24ae038b0dee973cfb19ae7c86e881dad6 on master**.

so0k: we run a lot of NLB without TLS now (we use Cert Manager and do TLS Termination by Envoy proxy)
mikkeloscar: @szuecs FYI: I rebased this on master
szuecs: Thanks @mikkeloscar
mikkeloscar: @szuecs I now also added per ingress configuration for selecting load balancer type and tested in our cluster to validate the upgrade path.

(Needed to do some coding :D)

It needs more testing. I didn't try creating network load Balancers yet. And I think we also need to only conditionally enable HTTPS redirect if `type=application`.
mikkeloscar: And ofc. docs are also missing :)
mikkeloscar: Also securityGroups can't be set with `network` load balancer

```
Security groups are not supported for load balancers with type 'network' (Service: AmazonElasticLoadBalancingV2; Status Code: 400; Error Code: InvalidConfigurationRequest; Request ID: 04ded6d8-c1c7-45ea-9dd0-cf483f761700)
--
```
mikkeloscar: * Updated to handle all cases where some configuration can only be set for one type of load balancer: `network` or `application`
* Successfully deployed a working NLB for an ingress in our test cluster by specifying `zalando.org/aws-load-balancer-type: network`

I found one interesting setting for NLBs: [cross-zone-load-balancing](https://docs.aws.amazon.com/elasticloadbalancing/latest/network/network-load-balancers.html#cross-zone-load-balancing) which when enabled let's NLB load balance across zones. (This is the default and only option for ALBs, but it's configurable for NLBs).

What's interesting is how this setting behaves for the NLB. If you deploy the NLB spanning 3 AZs then you by default get 3 IPs when you resolve the NLB hostname. However if the TargetGroup of the NLB only has healthy instances in 2/3 AZs then you get only 2 IPs resolved. This means that it should be possible to not have cross-zone-load-balancing thus presumably reducing the latency (and traffic cost?). This I have not tested.
However, I worry that updating the DNS record could take too long so if you loose an instance in one AZ, you could still have clients going to the IP of that AZ but the NLB having nowhere to route. This is not a problem in a cross-zone-enabled setup because all NLB interfaces can route to all instances no matter the AZ. We have to test what impact it would have in our Zalando setup. For now I have added a flag on the controller which enables/disables cross-zone-load-balancing for all `network` load balancers. It's disabled by default for now.

Another question is what values we should use for the `zalando.org/aws-load-balancer-type` annotation. Right now we use `application` and `network` because this is the term AWS uses. However service type `LoadBalancer` use the term `nlb` and `elb`: https://github.com/kubernetes/kubernetes/blob/2905275a08f8e3999e7e6bc8b02f407fd6a0ff36/staging/src/k8s.io/legacy-cloud-providers/aws/aws_loadbalancer.go#L66 so maybe we want to call it `alb` and `nlb` to make it more similar for users. WDYT?

## TODO

* [ ] More testing
* [ ] Documenting the feature
mikkeloscar: @szuecs I think we can test the non-cross AZ traffic later as it's just a flag here.

One open thing for this PR is the naming in the ingress, what do you think is best? `application` and `network` as AWS uses or `alb` and `nlb` as is used for the service of type LoadBalancer? I'm mostly leaning towards the `alb`, `nlb` version because it IMO creates a more unified user experience.
szuecs: I don’t have an opinion on that: nlb / alb seems fine to me.
mikkeloscar: * Changed it to be `nlb`/`alb` for the annotation values.
* Documented the feature in the README
* Tested in our test cluster and found a couple of unrelated bugs which I also fixed (Some AWS API call have limits, so you can e.g. only update 10 target groups at a time.)
szuecs: @mikkeloscar so ready to merge? Then I will drop the label and do my last review
mikkeloscar: @szuecs yes, ready for review :)
mikkeloscar: :+1: 
szuecs: :+1: 

Patch:
diff --git a/README.md b/README.md
index 260657b..68e3d58 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,7 @@ This information is used to manage AWS resources for each ingress objects of the
 - Automatic discovery of SSL certificates
 - Automatic forwarding of requests to all Worker Nodes, even with auto scaling
 - Automatic cleanup of unnecessary managed resources
+- Support for both [Application Load Balancers](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html) and [Network Load Balancers](https://docs.aws.amazon.com/elasticloadbalancing/latest/network/introduction.html).
 - Support for internet-facing and internal load balancers
 - Support for multiple Auto Scaling Groups
 - Support for instances that are not part of Auto Scaling Group
@@ -84,15 +85,18 @@ running has the clusterID tag `kubernetes.io/cluster/<cluster-id>=owned` set
 Overview of annotations which can be set.
 
 ### Annotations
-|Name                       | Type |Default
+|Name                       | Value |Default
 |---------------------------|------|------|
-|[alb.ingress.kubernetes.io/ip-address-type](#ip-address-type)|ipv4 \| dualstack|ipv4|
-|zalando.org/aws-load-balancer-ssl-cert|string|N/A|
-|zalando.org/aws-load-balancer-scheme|internal \| internet-facing |internet-facing|
-|zalando.org/aws-load-balancer-shared|true \| false|false|
-|zalando.org/aws-load-balancer-security-group|string|N/A|
-|zalando.org/aws-load-balancer-ssl-policy|string|ELBSecurityPolicy-2016-08|
-|kubernetes.io/ingress.class|string|N/A|
+|[`alb.ingress.kubernetes.io/ip-address-type`](#ip-address-type)|`ipv4` \| `dualstack` |`ipv4`|
+|`zalando.org/aws-load-balancer-ssl-cert`|`string`|N/A|
+|`zalando.org/aws-load-balancer-scheme`|`internal` \| `internet-facing` |`internet-facing`|
+|`zalando.org/aws-load-balancer-shared`|`true` \| `false`|`false`|
+|`zalando.org/aws-load-balancer-security-group`|`string`|N/A|
+|`zalando.org/aws-load-balancer-ssl-policy`|`string`|`ELBSecurityPolicy-2016-08`|
+|`zalando.org/aws-load-balancer-type`| `nlb` \| `alb`|`alb`|
+|`kubernetes.io/ingress.class`|`string`|N/A|
+
+The defaults can also be configured globally via a flag on the controller.
 
 ## AWS Tags
 
diff --git a/aws/adapter.go b/aws/adapter.go
index 35a6f23..003f180 100644
--- a/aws/adapter.go
+++ b/aws/adapter.go
@@ -58,6 +58,7 @@ type Adapter struct {
 	albLogsS3Prefix            string
 	wafWebAclId                string
 	httpRedirectToHttps        bool
+	nlbCrossZone               bool
 	customFilter               string
 }
 
@@ -98,9 +99,15 @@ const (
 	DefaultAlbS3LogsPrefix = ""
 	DefaultWafWebAclId     = ""
 	DefaultCustomFilter    = ""
-
-	ipAddressTypeDualstack = "dualstack"
-	nameTag                = "Name"
+	// DefaultNLBCrossZone specifies the default configuration for cross
+	// zone load balancing: https://docs.aws.amazon.com/elasticloadbalancing/latest/network/network-load-balancers.html#load-balancer-attributes
+	DefaultNLBCrossZone = false
+
+	nameTag                     = "Name"
+	LoadBalancerTypeApplication = "application"
+	LoadBalancerTypeNetwork     = "network"
+	IPAddressTypeIPV4           = "ipv4"
+	IPAddressTypeDualstack      = "dualstack"
 )
 
 var (
@@ -173,6 +180,7 @@ func NewAdapter(newControllerID string) (adapter *Adapter, err error) {
 		albLogsS3Bucket:     DefaultAlbS3LogsBucket,
 		albLogsS3Prefix:     DefaultAlbS3LogsPrefix,
 		wafWebAclId:         DefaultWafWebAclId,
+		nlbCrossZone:        DefaultNLBCrossZone,
 		customFilter:        DefaultCustomFilter,
 	}
 
@@ -269,7 +277,7 @@ func (a *Adapter) WithStackTerminationProtection(terminationProtection bool) *Ad
 
 // WithIpAddressType returns the receiver with ipv4 or dualstack configuration, defaults to ipv4.
 func (a *Adapter) WithIpAddressType(ipAddressType string) *Adapter {
-	if ipAddressType == ipAddressTypeDualstack {
+	if ipAddressType == IPAddressTypeDualstack {
 		a.ipAddressType = ipAddressType
 	}
 	return a
@@ -299,6 +307,13 @@ func (a *Adapter) WithHttpRedirectToHttps(httpRedirectToHttps bool) *Adapter {
 	return a
 }
 
+// WithNLBCrossZone returns the receiver adapter after setting the nlbCrossZone
+// config.
+func (a *Adapter) WithNLBCrossZone(nlbCrossZone bool) *Adapter {
+	a.nlbCrossZone = nlbCrossZone
+	return a
+}
+
 // WithCustomFilter returns the receiver adapter after setting a custom filter expression
 func (a *Adapter) WithCustomFilter(customFilter string) *Adapter {
 	a.customFilter = customFilter
@@ -453,7 +468,7 @@ func (a *Adapter) UpdateTargetGroupsAndAutoScalingGroups(stacks []*Stack) {
 // All the required resources (listeners and target group) are created in a
 // transactional fashion.
 // Failure to create the stack causes it to be deleted automatically.
-func (a *Adapter) CreateStack(certificateARNs []string, scheme, securityGroup, owner, sslPolicy, ipAddressType string, cwAlarms CloudWatchAlarmList) (string, error) {
+func (a *Adapter) CreateStack(certificateARNs []string, scheme, securityGroup, owner, sslPolicy, ipAddressType string, cwAlarms CloudWatchAlarmList, loadBalancerType string) (string, error) {
 	certARNs := make(map[string]time.Time, len(certificateARNs))
 	for _, arn := range certificateARNs {
 		certARNs[arn] = time.Time{}
@@ -488,17 +503,19 @@ func (a *Adapter) CreateStack(certificateARNs []string, scheme, securityGroup, o
 		controllerID:                 a.controllerID,
 		sslPolicy:                    sslPolicy,
 		ipAddressType:                ipAddressType,
+		loadbalancerType:             loadBalancerType,
 		albLogsS3Bucket:              a.albLogsS3Bucket,
 		albLogsS3Prefix:              a.albLogsS3Prefix,
 		wafWebAclId:                  a.wafWebAclId,
 		cwAlarms:                     cwAlarms,
 		httpRedirectToHttps:          a.httpRedirectToHttps,
+		nlbCrossZone:                 a.nlbCrossZone,
 	}
 
 	return createStack(a.cloudformation, spec)
 }
 
-func (a *Adapter) UpdateStack(stackName string, certificateARNs map[string]time.Time, scheme, sslPolicy, ipAddressType string, cwAlarms CloudWatchAlarmList) (string, error) {
+func (a *Adapter) UpdateStack(stackName string, certificateARNs map[string]time.Time, scheme, sslPolicy, ipAddressType string, cwAlarms CloudWatchAlarmList, loadBalancerType string) (string, error) {
 	if _, ok := SSLPolicies[sslPolicy]; !ok {
 		return "", fmt.Errorf("invalid SSLPolicy '%s' defined", sslPolicy)
 	}
@@ -523,6 +540,7 @@ func (a *Adapter) UpdateStack(stackName string, certificateARNs map[string]time.
 		controllerID:                 a.controllerID,
 		sslPolicy:                    sslPolicy,
 		ipAddressType:                ipAddressType,
+		loadbalancerType:             loadBalancerType,
 		albLogsS3Bucket:              a.albLogsS3Bucket,
 		albLogsS3Prefix:              a.albLogsS3Prefix,
 		wafWebAclId:                  a.wafWebAclId,
diff --git a/aws/asg.go b/aws/asg.go
index 4009029..3d37ea8 100644
--- a/aws/asg.go
+++ b/aws/asg.go
@@ -162,9 +162,11 @@ func updateTargetGroupsForAutoScalingGroup(svc autoscalingiface.AutoScalingAPI,
 	// get all target groups to ensure we are only working with target
 	// groups that still exists.
 	tgParams := &elbv2.DescribeTargetGroupsInput{}
-	allTGs := make([]*elbv2.TargetGroup, 0, len(resp.LoadBalancerTargetGroups))
+	allTGs := make(map[string]struct{}, len(resp.LoadBalancerTargetGroups))
 	err = elbv2svc.DescribeTargetGroupsPagesWithContext(context.TODO(), tgParams, func(resp *elbv2.DescribeTargetGroupsOutput, lastPage bool) bool {
-		allTGs = append(allTGs, resp.TargetGroups...)
+		for _, tg := range resp.TargetGroups {
+			allTGs[aws.StringValue(tg.TargetGroupArn)] = struct{}{}
+		}
 		return true
 	})
 	if err != nil {
@@ -178,11 +180,7 @@ func updateTargetGroupsForAutoScalingGroup(svc autoscalingiface.AutoScalingAPI,
 			arns = append(arns, tg.LoadBalancerTargetGroupARN)
 		}
 
-		tgParams := &elbv2.DescribeTagsInput{
-			ResourceArns: arns,
-		}
-
-		tgResp, err := elbv2svc.DescribeTags(tgParams)
+		descs, err := describeTagsChunked(elbv2svc, arns)
 		if err != nil {
 			return err
 		}
@@ -192,14 +190,14 @@ func updateTargetGroupsForAutoScalingGroup(svc autoscalingiface.AutoScalingAPI,
 			tgARN := aws.StringValue(tg.LoadBalancerTargetGroupARN)
 
 			// check that TG exists at all, otherwise detach it
-			if !tgExists(tgARN, allTGs) {
+			if _, ok := allTGs[tgARN]; !ok {
 				detachARNs = append(detachARNs, tgARN)
 				continue
 			}
 
 			// only consider detaching TGs which are owned by the
 			// controller
-			if !tgHasTags(tgResp.TagDescriptions, tgARN, ownerTags) {
+			if !tgHasTags(descs, tgARN, ownerTags) {
 				continue
 			}
 
@@ -243,15 +241,33 @@ func updateTargetGroupsForAutoScalingGroup(svc autoscalingiface.AutoScalingAPI,
 	return nil
 }
 
-// tgExists returns true if the targetGroupARN is found in the list of
-// targetGroups.
-func tgExists(targetGroupARN string, targetGroups []*elbv2.TargetGroup) bool {
-	for _, tg := range targetGroups {
-		if aws.StringValue(tg.TargetGroupArn) == targetGroupARN {
-			return true
+func describeTagsChunked(svc elbv2iface.ELBV2API, arns []*string) ([]*elbv2.TagDescription, error) {
+	descs := make([]*elbv2.TagDescription, 0, len(arns))
+	chunkSize := 20
+
+	for i := 0; i < len(arns); i += chunkSize {
+		end := i + chunkSize
+
+		if end > len(arns) {
+			end = len(arns)
+		}
+
+		arnsChunked := arns[i:end]
+		if len(arnsChunked) > 0 {
+			tgParams := &elbv2.DescribeTagsInput{
+				ResourceArns: arnsChunked,
+			}
+
+			tgResp, err := svc.DescribeTags(tgParams)
+			if err != nil {
+				return nil, err
+			}
+
+			descs = append(descs, tgResp.TagDescriptions...)
 		}
 	}
-	return false
+
+	return descs, nil
 }
 
 // tgHasTags returns true if the specified resource has the expected tags.
@@ -283,13 +299,28 @@ func hasTags(tags []*elbv2.Tag, expectedTags map[string]string) bool {
 }
 
 func detachTargetGroupsFromAutoScalingGroup(svc autoscalingiface.AutoScalingAPI, targetGroupARNs []string, autoScalingGroupName string) error {
-	params := &autoscaling.DetachLoadBalancerTargetGroupsInput{
-		AutoScalingGroupName: aws.String(autoScalingGroupName),
-		TargetGroupARNs:      aws.StringSlice(targetGroupARNs),
-	}
-	_, err := svc.DetachLoadBalancerTargetGroups(params)
-	if err != nil {
-		return err
+	// limit target group updates to 10 at a time since this is the limit
+	// in AWS API.
+	chunkSize := 10
+
+	for i := 0; i < len(targetGroupARNs); i += chunkSize {
+		end := i + chunkSize
+
+		if end > len(targetGroupARNs) {
+			end = len(targetGroupARNs)
+		}
+
+		targetGroupARNsChunked := targetGroupARNs[i:end]
+		if len(targetGroupARNsChunked) > 0 {
+			params := &autoscaling.DetachLoadBalancerTargetGroupsInput{
+				AutoScalingGroupName: aws.String(autoScalingGroupName),
+				TargetGroupARNs:      aws.StringSlice(targetGroupARNsChunked),
+			}
+			_, err := svc.DetachLoadBalancerTargetGroups(params)
+			if err != nil {
+				return err
+			}
+		}
 	}
 
 	return nil
diff --git a/aws/cf.go b/aws/cf.go
index b32ad00..ef7d737 100644
--- a/aws/cf.go
+++ b/aws/cf.go
@@ -26,6 +26,7 @@ type Stack struct {
 	SecurityGroup     string
 	SSLPolicy         string
 	IpAddressType     string
+	LoadBalancerType  string
 	OwnerIngress      string
 	CWAlarmConfigHash string
 	TargetGroupARN    string
@@ -110,6 +111,7 @@ const (
 	parameterListenerCertificatesParameter           = "ListenerCertificatesParameter"
 	parameterListenerSslPolicyParameter              = "ListenerSslPolicyParameter"
 	parameterIpAddressTypeParameter                  = "IpAddressType"
+	parameterLoadBalancerTypeParameter               = "Type"
 )
 
 type stackSpec struct {
@@ -130,11 +132,13 @@ type stackSpec struct {
 	controllerID                 string
 	sslPolicy                    string
 	ipAddressType                string
+	loadbalancerType             string
 	albLogsS3Bucket              string
 	albLogsS3Prefix              string
 	wafWebAclId                  string
 	cwAlarms                     CloudWatchAlarmList
 	httpRedirectToHttps          bool
+	nlbCrossZone                 bool
 }
 
 type healthCheck struct {
@@ -160,6 +164,7 @@ func createStack(svc cloudformationiface.CloudFormationAPI, spec *stackSpec) (st
 			cfParam(parameterTargetTargetPortParameter, fmt.Sprintf("%d", spec.targetPort)),
 			cfParam(parameterListenerSslPolicyParameter, spec.sslPolicy),
 			cfParam(parameterIpAddressTypeParameter, spec.ipAddressType),
+			cfParam(parameterLoadBalancerTypeParameter, spec.loadbalancerType),
 		},
 		Tags: []*cloudformation.Tag{
 			cfTag(kubernetesCreatorTag, spec.controllerID),
@@ -214,6 +219,7 @@ func updateStack(svc cloudformationiface.CloudFormationAPI, spec *stackSpec) (st
 			cfParam(parameterTargetTargetPortParameter, fmt.Sprintf("%d", spec.targetPort)),
 			cfParam(parameterListenerSslPolicyParameter, spec.sslPolicy),
 			cfParam(parameterIpAddressTypeParameter, spec.ipAddressType),
+			cfParam(parameterLoadBalancerTypeParameter, spec.loadbalancerType),
 		},
 		Tags: []*cloudformation.Tag{
 			cfTag(kubernetesCreatorTag, spec.controllerID),
@@ -360,6 +366,7 @@ func mapToManagedStack(stack *cloudformation.Stack) *Stack {
 		SecurityGroup:     parameters[parameterLoadBalancerSecurityGroupParameter],
 		SSLPolicy:         parameters[parameterListenerSslPolicyParameter],
 		IpAddressType:     parameters[parameterIpAddressTypeParameter],
+		LoadBalancerType:  parameters[parameterLoadBalancerTypeParameter],
 		CertificateARNs:   certificateARNs,
 		tags:              tags,
 		OwnerIngress:      ownerIngress,
diff --git a/aws/cf_template.go b/aws/cf_template.go
index fbd282d..b9e6dfc 100644
--- a/aws/cf_template.go
+++ b/aws/cf_template.go
@@ -70,11 +70,23 @@ func generateTemplate(spec *stackSpec) (string, error) {
 		parameterIpAddressTypeParameter: &cloudformation.Parameter{
 			Type:        "String",
 			Description: "IP Address Type, 'ipv4' or 'dualstack'",
-			Default:     "ipv4",
+			Default:     IPAddressTypeIPV4,
 		},
+		parameterLoadBalancerTypeParameter: &cloudformation.Parameter{
+			Type:        "String",
+			Description: "Loadbalancer Type, 'application' or 'network'",
+			Default:     LoadBalancerTypeApplication,
+		},
+	}
+
+	protocol := "HTTP"
+	tlsProtocol := "HTTPS"
+	if spec.loadbalancerType == LoadBalancerTypeNetwork {
+		protocol = "TCP"
+		tlsProtocol = "TLS"
 	}
 
-	if spec.httpRedirectToHttps {
+	if spec.loadbalancerType == LoadBalancerTypeApplication && spec.httpRedirectToHttps {
 		template.AddResource("HTTPListener", &cloudformation.ElasticLoadBalancingV2Listener{
 			DefaultActions: &cloudformation.ElasticLoadBalancingV2ListenerActionList{
 				{
@@ -103,7 +115,7 @@ func generateTemplate(spec *stackSpec) (string, error) {
 			},
 			LoadBalancerArn: cloudformation.Ref("LB").String(),
 			Port:            cloudformation.Integer(80),
-			Protocol:        cloudformation.String("HTTP"),
+			Protocol:        cloudformation.String(protocol),
 		})
 	}
 
@@ -132,7 +144,7 @@ func generateTemplate(spec *stackSpec) (string, error) {
 			},
 			LoadBalancerArn: cloudformation.Ref("LB").String(),
 			Port:            cloudformation.Integer(443),
-			Protocol:        cloudformation.String("HTTPS"),
+			Protocol:        cloudformation.String(tlsProtocol),
 			SslPolicy:       cloudformation.Ref(parameterListenerSslPolicyParameter).String(),
 		})
 
@@ -155,12 +167,25 @@ func generateTemplate(spec *stackSpec) (string, error) {
 
 	// Build up the LoadBalancerAttributes list, as there is no way to make attributes conditional in the template
 	albAttrList := make(cloudformation.ElasticLoadBalancingV2LoadBalancerLoadBalancerAttributeList, 0, 4)
-	albAttrList = append(albAttrList,
-		cloudformation.ElasticLoadBalancingV2LoadBalancerLoadBalancerAttribute{
-			Key:   cloudformation.String("idle_timeout.timeout_seconds"),
-			Value: cloudformation.String(fmt.Sprintf("%d", spec.idleConnectionTimeoutSeconds)),
-		},
-	)
+
+	if spec.loadbalancerType == LoadBalancerTypeApplication {
+		albAttrList = append(albAttrList,
+			cloudformation.ElasticLoadBalancingV2LoadBalancerLoadBalancerAttribute{
+				Key:   cloudformation.String("idle_timeout.timeout_seconds"),
+				Value: cloudformation.String(fmt.Sprintf("%d", spec.idleConnectionTimeoutSeconds)),
+			},
+		)
+	}
+
+	if spec.nlbCrossZone && spec.loadbalancerType == LoadBalancerTypeNetwork {
+		albAttrList = append(albAttrList,
+			cloudformation.ElasticLoadBalancingV2LoadBalancerLoadBalancerAttribute{
+				Key:   cloudformation.String("load_balancing.cross_zone.enabled"),
+				Value: cloudformation.String("true"),
+			},
+		)
+	}
+
 	if spec.albLogsS3Bucket != "" {
 		albAttrList = append(albAttrList,
 			cloudformation.ElasticLoadBalancingV2LoadBalancerLoadBalancerAttribute{
@@ -191,30 +216,46 @@ func generateTemplate(spec *stackSpec) (string, error) {
 		)
 	}
 
-	template.AddResource("LB", &cloudformation.ElasticLoadBalancingV2LoadBalancer{
+	lb := &cloudformation.ElasticLoadBalancingV2LoadBalancer{
 		LoadBalancerAttributes: &albAttrList,
 
-		IPAddressType:  cloudformation.Ref(parameterIpAddressTypeParameter).String(),
-		Scheme:         cloudformation.Ref(parameterLoadBalancerSchemeParameter).String(),
-		SecurityGroups: cloudformation.Ref(parameterLoadBalancerSecurityGroupParameter).StringList(),
-		Subnets:        cloudformation.Ref(parameterLoadBalancerSubnetsParameter).StringList(),
+		IPAddressType: cloudformation.Ref(parameterIpAddressTypeParameter).String(),
+		Scheme:        cloudformation.Ref(parameterLoadBalancerSchemeParameter).String(),
+		Subnets:       cloudformation.Ref(parameterLoadBalancerSubnetsParameter).StringList(),
 		Tags: &cloudformation.TagList{
 			{
 				Key:   cloudformation.String("StackName"),
 				Value: cloudformation.Ref("AWS::StackName").String(),
 			},
 		},
-	})
+	}
+
+	// Security groups can't be set for 'network' load balancers
+	if spec.loadbalancerType != LoadBalancerTypeNetwork {
+		lb.SecurityGroups = cloudformation.Ref(parameterLoadBalancerSecurityGroupParameter).StringList()
+	}
+
+	// TODO(mlarsen): hack to only set type on "new" stacks where this
+	// features was enabled. Adding the Type value for existing Load
+	// Balancers will cause them to be recreated which is disruptive (and
+	// breaks because AWS tries to attach the same TG to multiple LBs).
+	// Can be removed in a later version.
+	if spec.loadbalancerType == LoadBalancerTypeApplication || spec.loadbalancerType == LoadBalancerTypeNetwork {
+		lb.Type = cloudformation.Ref(parameterLoadBalancerTypeParameter).String()
+	}
+
+	template.AddResource("LB", lb)
 	template.AddResource("TG", &cloudformation.ElasticLoadBalancingV2TargetGroup{
 		HealthCheckIntervalSeconds: cloudformation.Ref(parameterTargetGroupHealthCheckIntervalParameter).Integer(),
 		HealthCheckPath:            cloudformation.Ref(parameterTargetGroupHealthCheckPathParameter).String(),
 		HealthCheckPort:            cloudformation.Ref(parameterTargetGroupHealthCheckPortParameter).String(),
+		HealthCheckProtocol:        cloudformation.String("HTTP"),
 		Port:                       cloudformation.Ref(parameterTargetTargetPortParameter).Integer(),
-		Protocol:                   cloudformation.String("HTTP"),
+		Protocol:                   cloudformation.String(protocol),
 		VPCID:                      cloudformation.Ref(parameterTargetGroupVPCIDParameter).String(),
 	})
 
-	if spec.wafWebAclId != "" {
+	if spec.loadbalancerType == LoadBalancerTypeApplication && spec.wafWebAclId != "" {
 		template.AddResource("WAFAssociation", &cloudformation.WAFRegionalWebACLAssociation{
 			ResourceArn: cloudformation.Ref("LB").String(),
 			WebACLID:    cloudformation.String(spec.wafWebAclId),
diff --git a/controller.go b/controller.go
index d679ffc..9a2feeb 100644
--- a/controller.go
+++ b/controller.go
@@ -65,6 +65,8 @@ var (
 	firstRun                   bool = true
 	cwAlarmConfigMap           string
 	cwAlarmConfigMapLocation   *kubernetes.ResourceLocation
+	loadBalancerType           string
+	nlbCrossZone               bool
 )
 
 func loadSettings() error {
@@ -112,6 +114,8 @@ func loadSettings() error {
 	flag.StringVar(&wafWebAclId, "aws-waf-web-acl-id", aws.DefaultWafWebAclId, "Waf web acl id to be associated with the ALB")
 	flag.StringVar(&cwAlarmConfigMap, "cloudwatch-alarms-config-map", "", "ConfigMap location of the form 'namespace/config-map-name' where to read CloudWatch Alarm configuration from. Ignored if empty.")
 	flag.BoolVar(&httpRedirectToHttps, "redirect-http-to-https", defaultHttpRedirectToHttps, "Configure HTTP listener to redirect to HTTPS")
+	flag.StringVar(&loadBalancerType, "load-balancer-type", aws.LoadBalancerTypeApplication, "Sets default Load Balancer type (application or network).")
+	flag.BoolVar(&nlbCrossZone, "nlb-cross-zone", aws.DefaultNLBCrossZone, "Specify whether Network Load Balancers should balance cross availablity zones. This setting only apply to 'network' Load Balancers.")
 
 	flag.Parse()
 
@@ -168,6 +172,10 @@ func loadSettings() error {
 		cwAlarmConfigMapLocation = loc
 	}
 
+	if loadBalancerType != aws.LoadBalancerTypeApplication && loadBalancerType != aws.LoadBalancerTypeNetwork {
+		return fmt.Errorf("invalid load-balancer-type, must be 'application' or 'network'")
+	}
+
 	if quietFlag && debugFlag {
 		log.Warn("--quiet and --debug flags are both set. Debug will be used as logging level.")
 	}
@@ -251,6 +259,7 @@ func main() {
 		WithAlbLogsS3Prefix(albLogsS3Prefix).
 		WithWafWebAclId(wafWebAclId).
 		WithHttpRedirectToHttps(httpRedirectToHttps).
+		WithNLBCrossZone(nlbCrossZone).
 		WithCustomFilter(customFilter)
 
 	certificatesProvider, err := certs.NewCachingProvider(
@@ -277,7 +286,7 @@ func main() {
 		ingressClassFiltersList = strings.Split(ingressClassFilters, ",")
 	}
 
-	kubeAdapter, err = kubernetes.NewAdapter(kubeConfig, ingressClassFiltersList, awsAdapter.SecurityGroupID(), sslPolicy)
+	kubeAdapter, err = kubernetes.NewAdapter(kubeConfig, ingressClassFiltersList, awsAdapter.SecurityGroupID(), sslPolicy, loadBalancerType)
 	if err != nil {
 		log.Fatal(err)
 	}
@@ -302,6 +311,7 @@ func main() {
 	log.Infof("\tALB Logging S3 Bucket: %s", awsAdapter.S3Bucket())
 	log.Infof("\tALB Logging S3 Prefix: %s", awsAdapter.S3Prefix())
 	log.Infof("\tCloudWatch Alarm ConfigMap: %s", cwAlarmConfigMapLocation)
+	log.Infof("\tDefault LoadBalancer type: %s", loadBalancerType)
 
 	ctx, cancel := context.WithCancel(context.Background())
 	go handleTerminationSignals(cancel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
diff --git a/kubernetes/adapter.go b/kubernetes/adapter.go
index d98ddd9..a866f93 100644
--- a/kubernetes/adapter.go
+++ b/kubernetes/adapter.go
@@ -10,13 +10,17 @@ import (
 )
 
 type Adapter struct {
-	kubeClient                  client
-	ingressFilters              []string
-	ingressDefaultSecurityGroup string
-	ingressDefaultSSLPolicy     string
+	kubeClient                     client
+	ingressFilters                 []string
+	ingressDefaultSecurityGroup    string
+	ingressDefaultSSLPolicy        string
+	ingressDefaultLoadBalancerType string
 }
 
-const ipAddressTypeDualstack = "dualstack"
+const (
+	loadBalancerTypeNLB = "nlb"
+	loadBalancerTypeALB = "alb"
+)
 
 var (
 	// ErrMissingKubernetesEnv is returned when the Kubernetes API server environment variables are not defined
@@ -36,20 +40,31 @@ var (
 	// ErrInvalidCertificates is returned when the CA certificates required to communicate with the
 	// API server are invalid
 	ErrInvalidCertificates = errors.New("invalid CA certificates")
+
+	loadBalancerTypesIngressToAWS = map[string]string{
+		loadBalancerTypeALB: aws.LoadBalancerTypeApplication,
+		loadBalancerTypeNLB: aws.LoadBalancerTypeNetwork,
+	}
+
+	loadBalancerTypesAWSToIngress = map[string]string{
+		aws.LoadBalancerTypeApplication: loadBalancerTypeALB,
+		aws.LoadBalancerTypeNetwork:     loadBalancerTypeNLB,
+	}
 )
 
 // Ingress is the ingress-controller's business object
 type Ingress struct {
-	CertificateARN string
-	Namespace      string
-	Name           string
-	Hostname       string
-	Scheme         string
-	Hostnames      []string
-	Shared         bool
-	SecurityGroup  string
-	SSLPolicy      string
-	IPAddressType  string
+	CertificateARN   string
+	Namespace        string
+	Name             string
+	Hostname         string
+	Scheme           string
+	Hostnames        []string
+	Shared           bool
+	SecurityGroup    string
+	SSLPolicy        string
+	IPAddressType    string
+	LoadBalancerType string
 }
 
 // String returns a string representation of the Ingress instance containing the namespace and the resource name.
@@ -71,7 +86,7 @@ func (c *ConfigMap) String() string {
 }
 
 // NewAdapter creates an Adapter for Kubernetes using a given configuration.
-func NewAdapter(config *Config, ingressClassFilters []string, ingressDefaultSecurityGroup, ingressDefaultSSLPolicy string) (*Adapter, error) {
+func NewAdapter(config *Config, ingressClassFilters []string, ingressDefaultSecurityGroup, ingressDefaultSSLPolicy, ingressDefaultLoadBalancerType string) (*Adapter, error) {
 	if config == nil || config.BaseURL == "" {
 		return nil, ErrInvalidConfiguration
 	}
@@ -80,10 +95,11 @@ func NewAdapter(config *Config, ingressClassFilters []string, ingressDefaultSecu
 		return nil, err
 	}
 	return &Adapter{
-		kubeClient:                  c,
-		ingressFilters:              ingressClassFilters,
-		ingressDefaultSecurityGroup: ingressDefaultSecurityGroup,
-		ingressDefaultSSLPolicy:     ingressDefaultSSLPolicy,
+		kubeClient:                     c,
+		ingressFilters:                 ingressClassFilters,
+		ingressDefaultSecurityGroup:    ingressDefaultSecurityGroup,
+		ingressDefaultSSLPolicy:        ingressDefaultSSLPolicy,
+		ingressDefaultLoadBalancerType: loadBalancerTypesAWSToIngress[ingressDefaultLoadBalancerType],
 	}, nil
 }
 
@@ -116,9 +132,9 @@ func (a *Adapter) newIngressFromKube(kubeIngress *ingress) *Ingress {
 		shared = false
 	}
 
-	ipAddressType := "ipv4"
-	if kubeIngress.getAnnotationsString(ingressALBIPAddressType, "") == ipAddressTypeDualstack {
-		ipAddressType = ipAddressTypeDualstack
+	ipAddressType := aws.IPAddressTypeIPV4
+	if kubeIngress.getAnnotationsString(ingressALBIPAddressType, "") == aws.IPAddressTypeDualstack {
+		ipAddressType = aws.IPAddressTypeDualstack
 	}
 
 	sslPolicy := kubeIngress.getAnnotationsString(ingressSSLPolicyAnnotation, a.ingressDefaultSSLPolicy)
@@ -126,17 +142,31 @@ func (a *Adapter) newIngressFromKube(kubeIngress *ingress) *Ingress {
 		sslPolicy = a.ingressDefaultSSLPolicy
 	}
 
+	loadBalancerType := kubeIngress.getAnnotationsString(ingressLoadBalancerTypeAnnotation, a.ingressDefaultLoadBalancerType)
+	if _, ok := loadBalancerTypesIngressToAWS[loadBalancerType]; !ok {
+		loadBalancerType = a.ingressDefaultLoadBalancerType
+	}
+
+	// convert to the internal naming e.g. nlb -> network
+	loadBalancerType = loadBalancerTypesIngressToAWS[loadBalancerType]
+
+	if loadBalancerType == aws.LoadBalancerTypeNetwork {
+		// ensure ipv4 for network load balancers
+		ipAddressType = aws.IPAddressTypeIPV4
+	}
+
 	return &Ingress{
-		CertificateARN: kubeIngress.getAnnotationsString(ingressCertificateARNAnnotation, ""),
-		Namespace:      kubeIngress.Metadata.Namespace,
-		Name:           kubeIngress.Metadata.Name,
-		Hostname:       host,
-		Scheme:         scheme,
-		Hostnames:      hostnames,
-		Shared:         shared,
-		SecurityGroup:  kubeIngress.getAnnotationsString(ingressSecurityGroupAnnotation, a.ingressDefaultSecurityGroup),
-		SSLPolicy:      sslPolicy,
-		IPAddressType:  ipAddressType,
+		CertificateARN:   kubeIngress.getAnnotationsString(ingressCertificateARNAnnotation, ""),
+		Namespace:        kubeIngress.Metadata.Namespace,
+		Name:             kubeIngress.Metadata.Name,
+		Hostname:         host,
+		Scheme:           scheme,
+		Hostnames:        hostnames,
+		Shared:           shared,
+		SecurityGroup:    kubeIngress.getAnnotationsString(ingressSecurityGroupAnnotation, a.ingressDefaultSecurityGroup),
+		SSLPolicy:        sslPolicy,
+		IPAddressType:    ipAddressType,
+		LoadBalancerType: loadBalancerType,
 	}
 }
 
@@ -152,12 +182,13 @@ func newIngressForKube(i *Ingress) *ingress {
 			Namespace: i.Namespace,
 			Name:      i.Name,
 			Annotations: map[string]interface{}{
-				ingressCertificateARNAnnotation: i.CertificateARN,
-				ingressSchemeAnnotation:         i.Scheme,
-				ingressSharedAnnotation:         shared,
-				ingressSecurityGroupAnnotation:  i.SecurityGroup,
-				ingressSSLPolicyAnnotation:      i.SSLPolicy,
-				ingressALBIPAddressType:         i.IPAddressType,
+				ingressCertificateARNAnnotation:   i.CertificateARN,
+				ingressSchemeAnnotation:           i.Scheme,
+				ingressSharedAnnotation:           shared,
+				ingressSecurityGroupAnnotation:    i.SecurityGroup,
+				ingressSSLPolicyAnnotation:        i.SSLPolicy,
+				ingressALBIPAddressType:           i.IPAddressType,
+				ingressLoadBalancerTypeAnnotation: loadBalancerTypesAWSToIngress[i.LoadBalancerType],
 			},
 		},
 		Status: ingressStatus{
diff --git a/kubernetes/ingress.go b/kubernetes/ingress.go
index bb0de40..5d67869 100644
--- a/kubernetes/ingress.go
+++ b/kubernetes/ingress.go
@@ -60,15 +60,16 @@ type ingressLoadBalancer struct {
 
 const (
 	// ingressALBIPAddressType is used in external-dns, https://github.com/kubernetes-incubator/external-dns/pull/1079
-	ingressALBIPAddressType         = "alb.ingress.kubernetes.io/ip-address-type"
-	ingressListResource             = "/apis/extensions/v1beta1/ingresses"
-	ingressPatchStatusResource      = "/apis/extensions/v1beta1/namespaces/%s/ingresses/%s/status"
-	ingressCertificateARNAnnotation = "zalando.org/aws-load-balancer-ssl-cert"
-	ingressSchemeAnnotation         = "zalando.org/aws-load-balancer-scheme"
-	ingressSharedAnnotation         = "zalando.org/aws-load-balancer-shared"
-	ingressSecurityGroupAnnotation  = "zalando.org/aws-load-balancer-security-group"
-	ingressSSLPolicyAnnotation      = "zalando.org/aws-load-balancer-ssl-policy"
-	ingressClassAnnotation          = "kubernetes.io/ingress.class"
+	ingressALBIPAddressType           = "alb.ingress.kubernetes.io/ip-address-type"
+	ingressListResource               = "/apis/extensions/v1beta1/ingresses"
+	ingressPatchStatusResource        = "/apis/extensions/v1beta1/namespaces/%s/ingresses/%s/status"
+	ingressCertificateARNAnnotation   = "zalando.org/aws-load-balancer-ssl-cert"
+	ingressSchemeAnnotation           = "zalando.org/aws-load-balancer-scheme"
+	ingressSharedAnnotation           = "zalando.org/aws-load-balancer-shared"
+	ingressSecurityGroupAnnotation    = "zalando.org/aws-load-balancer-security-group"
+	ingressSSLPolicyAnnotation        = "zalando.org/aws-load-balancer-ssl-policy"
+	ingressLoadBalancerTypeAnnotation = "zalando.org/aws-load-balancer-type"
+	ingressClassAnnotation            = "kubernetes.io/ingress.class"
 )
 
 func (i *ingress) getAnnotationsString(key string, defaultValue string) string {
diff --git a/worker.go b/worker.go
index 74672c1..296929a 100644
--- a/worker.go
+++ b/worker.go
@@ -21,15 +21,16 @@ import (
 )
 
 type loadBalancer struct {
-	ingresses     map[string][]*kubernetes.Ingress
-	scheme        string
-	stack         *aws.Stack
-	shared        bool
-	securityGroup string
-	sslPolicy     string
-	ipAddressType string
-	certTTL       time.Duration
-	cwAlarms      aws.CloudWatchAlarmList
+	ingresses        map[string][]*kubernetes.Ingress
+	scheme           string
+	stack            *aws.Stack
+	shared           bool
+	securityGroup    string
+	sslPolicy        string
+	ipAddressType    string
+	certTTL          time.Duration
+	cwAlarms         aws.CloudWatchAlarmList
+	loadBalancerType string
 }
 
 const (
@@ -69,7 +70,11 @@ func (l *loadBalancer) inSync() bool {
 // adding can fail in case the load balancer reached its limit of ingress
 // certificates or if the scheme doesn't match.
 func (l *loadBalancer) AddIngress(certificateARNs []string, ingress *kubernetes.Ingress, maxCerts int) bool {
-	if l.ipAddressType != ingress.IPAddressType || l.scheme != ingress.Scheme || l.securityGroup != ingress.SecurityGroup || l.sslPolicy != ingress.SSLPolicy {
+	if l.ipAddressType != ingress.IPAddressType ||
+		l.scheme != ingress.Scheme ||
+		l.securityGroup != ingress.SecurityGroup ||
+		l.sslPolicy != ingress.SSLPolicy ||
+		l.loadBalancerType != ingress.LoadBalancerType {
 		return false
 	}
 
@@ -291,14 +296,15 @@ func getAllLoadBalancers(certTTL time.Duration, stacks []*aws.Stack) []*loadBala
 
 	for _, stack := range stacks {
 		lb := &loadBalancer{
-			stack:         stack,
-			ingresses:     make(map[string][]*kubernetes.Ingress),
-			scheme:        stack.Scheme,
-			shared:        stack.OwnerIngress == "",
-			securityGroup: stack.SecurityGroup,
-			sslPolicy:     stack.SSLPolicy,
-			ipAddressType: stack.IpAddressType,
-			certTTL:       certTTL,
+			stack:            stack,
+			ingresses:        make(map[string][]*kubernetes.Ingress),
+			scheme:           stack.Scheme,
+			shared:           stack.OwnerIngress == "",
+			securityGroup:    stack.SecurityGroup,
+			sslPolicy:        stack.SSLPolicy,
+			ipAddressType:    stack.IpAddressType,
+			loadBalancerType: stack.LoadBalancerType,
+			certTTL:          certTTL,
 		}
 		// initialize ingresses map with existing certificates from the
 		// stack.
@@ -333,6 +339,14 @@ func matchIngressesToLoadBalancers(loadBalancers []*loadBalancer, certs Certific
 		// limit is exeeded.
 		added := false
 		for _, lb := range loadBalancers {
+			// TODO(mlarsen): hack to phase out old load balancers
+			// which can't be updated to include type
+			// specification.
+			// Can be removed in a later version
+			if lb.loadBalancerType != aws.LoadBalancerTypeApplication && lb.loadBalancerType != aws.LoadBalancerTypeNetwork {
+				continue
+			}
+
 			if lb.AddIngress(certificateARNs, ingress, certsPerALB) {
 				added = true
 				break
@@ -350,12 +364,13 @@ func matchIngressesToLoadBalancers(loadBalancers []*loadBalancer, certs Certific
 			loadBalancers = append(
 				loadBalancers,
 				&loadBalancer{
-					ingresses:     i,
-					scheme:        ingress.Scheme,
-					shared:        ingress.Shared,
-					securityGroup: ingress.SecurityGroup,
-					sslPolicy:     ingress.SSLPolicy,
-					ipAddressType: ingress.IPAddressType,
+					ingresses:        i,
+					scheme:           ingress.Scheme,
+					shared:           ingress.Shared,
+					securityGroup:    ingress.SecurityGroup,
+					sslPolicy:        ingress.SSLPolicy,
+					ipAddressType:    ingress.IPAddressType,
+					loadBalancerType: ingress.LoadBalancerType,
 				},
 			)
 		}
@@ -394,7 +409,7 @@ func createStack(awsAdapter *aws.Adapter, lb *loadBalancer) {
 
 	log.Infof("creating stack for certificates %q / ingress %q", certificates, lb.ingresses)
 
-	stackId, err := awsAdapter.CreateStack(certificates, lb.scheme, lb.securityGroup, lb.Owner(), lb.sslPolicy, lb.ipAddressType, lb.cwAlarms)
+	stackId, err := awsAdapter.CreateStack(certificates, lb.scheme, lb.securityGroup, lb.Owner(), lb.sslPolicy, lb.ipAddressType, lb.cwAlarms, lb.loadBalancerType)
 	if err != nil {
 		if isAlreadyExistsError(err) {
 			lb.stack, err = awsAdapter.GetStack(stackId)
@@ -413,7 +428,7 @@ func updateStack(awsAdapter *aws.Adapter, lb *loadBalancer) {
 
 	log.Infof("updating %q stack for %d certificates / %d ingresses", lb.scheme, len(certificates), len(lb.ingresses))
 
-	stackId, err := awsAdapter.UpdateStack(lb.stack.Name, certificates, lb.scheme, lb.sslPolicy, lb.ipAddressType, lb.cwAlarms)
+	stackId, err := awsAdapter.UpdateStack(lb.stack.Name, certificates, lb.scheme, lb.sslPolicy, lb.ipAddressType, lb.cwAlarms, lb.loadBalancerType)
 	if isNoUpdatesToBePerformedError(err) {
 		log.Debugf("stack(%q) is already up to date", certificates)
 	} else if err != nil {


Test patch:
diff --git a/aws/cf_template_test.go b/aws/cf_template_test.go
index 5661616..dad0ade 100644
--- a/aws/cf_template_test.go
+++ b/aws/cf_template_test.go
@@ -96,6 +96,50 @@ func TestGenerateTemplate(t *testing.T) {
 				)
 			},
 		},
+		{
+			name: "http -> https redirect should be enabled for Application load balancers",
+			spec: &stackSpec{
+				loadbalancerType:    LoadBalancerTypeApplication,
+				httpRedirectToHttps: true,
+			},
+			validate: func(t *testing.T, template *cloudformation.Template) {
+				require.NotNil(t, template.Resources["HTTPListener"])
+				listener := template.Resources["HTTPListener"].Properties.(*cloudformation.ElasticLoadBalancingV2Listener)
+				require.Len(t, *listener.DefaultActions, 1)
+				redirectConfig := []cloudformation.ElasticLoadBalancingV2ListenerAction(*listener.DefaultActions)[0].RedirectConfig
+				require.NotNil(t, redirectConfig)
+				require.Equal(t, redirectConfig.Protocol, cloudformation.String("HTTPS"))
+				require.Equal(t, redirectConfig.StatusCode, cloudformation.String("HTTP_301"))
+			},
+		},
+		{
+			name: "http -> https redirect should NOT be enabled for Network load balancers",
+			spec: &stackSpec{
+				loadbalancerType:    LoadBalancerTypeNetwork,
+				httpRedirectToHttps: true,
+			},
+			validate: func(t *testing.T, template *cloudformation.Template) {
+				require.NotNil(t, template.Resources["HTTPListener"])
+				listener := template.Resources["HTTPListener"].Properties.(*cloudformation.ElasticLoadBalancingV2Listener)
+				require.Len(t, *listener.DefaultActions, 1)
+				redirectConfig := []cloudformation.ElasticLoadBalancingV2ListenerAction(*listener.DefaultActions)[0].RedirectConfig
+				require.Nil(t, redirectConfig)
+			},
+		},
+		{
+			name: "nlb cross zone load balancing can be enabled for Network load balancers",
+			spec: &stackSpec{
+				loadbalancerType: LoadBalancerTypeNetwork,
+				nlbCrossZone:     true,
+			},
+			validate: func(t *testing.T, template *cloudformation.Template) {
+				require.NotNil(t, template.Resources["LB"])
+				properties := template.Resources["LB"].Properties.(*cloudformation.ElasticLoadBalancingV2LoadBalancer)
+				attributes := []cloudformation.ElasticLoadBalancingV2LoadBalancerLoadBalancerAttribute(*properties.LoadBalancerAttributes)
+				require.Equal(t, attributes[0].Key.Literal, "load_balancing.cross_zone.enabled")
+				require.Equal(t, attributes[0].Value.Literal, "true")
+			},
+		},
 	} {
 		t.Run(test.name, func(t *testing.T) {
 			generated, err := generateTemplate(test.spec)
diff --git a/kubernetes/adapter_test.go b/kubernetes/adapter_test.go
index b3623e4..d4eb934 100644
--- a/kubernetes/adapter_test.go
+++ b/kubernetes/adapter_test.go
@@ -11,6 +11,7 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/zalando-incubator/kube-ingress-aws-controller/aws"
 )
 
 var (
@@ -19,8 +20,10 @@ var (
 	testIngressDefaultSecurityGroup = "sg-foobar"
 	testSecurityGroup               = "sg-123456"
 	testSSLPolicy                   = "ELBSecurityPolicy-TLS-1-2-2017-01"
-	testIPAddressTypeDualStack      = "dualstack"
-	testIPAddressTypeDefault        = "ipv4"
+	testIPAddressTypeDualStack      = aws.IPAddressTypeDualstack
+	testIPAddressTypeDefault        = aws.IPAddressTypeIPV4
+	testLoadBalancerTypeIngress     = loadBalancerTypeALB
+	testLoadBalancerTypeAWS         = aws.LoadBalancerTypeApplication
 )
 
 func TestMappingRoundtrip(tt *testing.T) {
@@ -32,28 +35,30 @@ func TestMappingRoundtrip(tt *testing.T) {
 		{
 			msg: "test parsing a simple ingress object",
 			ingress: &Ingress{
-				Namespace:      "default",
-				Name:           "foo",
-				Hostname:       "bar",
-				Scheme:         "internal",
-				CertificateARN: "zbr",
-				Shared:         true,
-				Hostnames:      []string{"domain.example.org"},
-				SecurityGroup:  testSecurityGroup,
-				SSLPolicy:      testSSLPolicy,
-				IPAddressType:  testIPAddressTypeDefault,
+				Namespace:        "default",
+				Name:             "foo",
+				Hostname:         "bar",
+				Scheme:           "internal",
+				CertificateARN:   "zbr",
+				Shared:           true,
+				Hostnames:        []string{"domain.example.org"},
+				SecurityGroup:    testSecurityGroup,
+				SSLPolicy:        testSSLPolicy,
+				IPAddressType:    testIPAddressTypeDefault,
+				LoadBalancerType: testLoadBalancerTypeAWS,
 			},
 			kubeIngress: &ingress{
 				Metadata: ingressItemMetadata{
 					Namespace: "default",
 					Name:      "foo",
 					Annotations: map[string]interface{}{
-						ingressCertificateARNAnnotation: "zbr",
-						ingressSchemeAnnotation:         "internal",
-						ingressSharedAnnotation:         "true",
-						ingressSecurityGroupAnnotation:  testSecurityGroup,
-						ingressSSLPolicyAnnotation:      testSSLPolicy,
-						ingressALBIPAddressType:         testIPAddressTypeDefault,
+						ingressCertificateARNAnnotation:   "zbr",
+						ingressSchemeAnnotation:           "internal",
+						ingressSharedAnnotation:           "true",
+						ingressSecurityGroupAnnotation:    testSecurityGroup,
+						ingressSSLPolicyAnnotation:        testSSLPolicy,
+						ingressALBIPAddressType:           testIPAddressTypeDefault,
+						ingressLoadBalancerTypeAnnotation: testLoadBalancerTypeIngress,
 					},
 				},
 				Spec: ingressSpec{
@@ -76,27 +81,29 @@ func TestMappingRoundtrip(tt *testing.T) {
 		{
 			msg: "test parsing an ingress object with shared=false annotation",
 			ingress: &Ingress{
-				Namespace:      "default",
-				Name:           "foo",
-				Hostname:       "bar",
-				Scheme:         "internal",
-				CertificateARN: "zbr",
-				Shared:         false,
-				SecurityGroup:  testSecurityGroup,
-				SSLPolicy:      testSSLPolicy,
-				IPAddressType:  testIPAddressTypeDefault,
+				Namespace:        "default",
+				Name:             "foo",
+				Hostname:         "bar",
+				Scheme:           "internal",
+				CertificateARN:   "zbr",
+				Shared:           false,
+				SecurityGroup:    testSecurityGroup,
+				SSLPolicy:        testSSLPolicy,
+				IPAddressType:    testIPAddressTypeDefault,
+				LoadBalancerType: testLoadBalancerTypeAWS,
 			},
 			kubeIngress: &ingress{
 				Metadata: ingressItemMetadata{
 					Namespace: "default",
 					Name:      "foo",
 					Annotations: map[string]interface{}{
-						ingressCertificateARNAnnotation: "zbr",
-						ingressSchemeAnnotation:         "internal",
-						ingressSharedAnnotation:         "false",
-						ingressSecurityGroupAnnotation:  testSecurityGroup,
-						ingressSSLPolicyAnnotation:      testSSLPolicy,
-						ingressALBIPAddressType:         testIPAddressTypeDefault,
+						ingressCertificateARNAnnotation:   "zbr",
+						ingressSchemeAnnotation:           "internal",
+						ingressSharedAnnotation:           "false",
+						ingressSecurityGroupAnnotation:    testSecurityGroup,
+						ingressSSLPolicyAnnotation:        testSSLPolicy,
+						ingressALBIPAddressType:           testIPAddressTypeDefault,
+						ingressLoadBalancerTypeAnnotation: testLoadBalancerTypeIngress,
 					},
 				},
 				Status: ingressStatus{
@@ -112,27 +119,29 @@ func TestMappingRoundtrip(tt *testing.T) {
 		{
 			msg: "test parsing an ingress object with dualstack annotation",
 			ingress: &Ingress{
-				Namespace:      "default",
-				Name:           "foo",
-				Hostname:       "bar",
-				Scheme:         "internal",
-				CertificateARN: "zbr",
-				Shared:         true,
-				SecurityGroup:  testSecurityGroup,
-				SSLPolicy:      testSSLPolicy,
-				IPAddressType:  testIPAddressTypeDualStack,
+				Namespace:        "default",
+				Name:             "foo",
+				Hostname:         "bar",
+				Scheme:           "internal",
+				CertificateARN:   "zbr",
+				Shared:           true,
+				SecurityGroup:    testSecurityGroup,
+				SSLPolicy:        testSSLPolicy,
+				IPAddressType:    testIPAddressTypeDualStack,
+				LoadBalancerType: testLoadBalancerTypeAWS,
 			},
 			kubeIngress: &ingress{
 				Metadata: ingressItemMetadata{
 					Namespace: "default",
 					Name:      "foo",
 					Annotations: map[string]interface{}{
-						ingressCertificateARNAnnotation: "zbr",
-						ingressSchemeAnnotation:         "internal",
-						ingressSharedAnnotation:         "true",
-						ingressSecurityGroupAnnotation:  testSecurityGroup,
-						ingressSSLPolicyAnnotation:      testSSLPolicy,
-						ingressALBIPAddressType:         testIPAddressTypeDualStack,
+						ingressCertificateARNAnnotation:   "zbr",
+						ingressSchemeAnnotation:           "internal",
+						ingressSharedAnnotation:           "true",
+						ingressSecurityGroupAnnotation:    testSecurityGroup,
+						ingressSSLPolicyAnnotation:        testSSLPolicy,
+						ingressALBIPAddressType:           testIPAddressTypeDualStack,
+						ingressLoadBalancerTypeAnnotation: testLoadBalancerTypeIngress,
 					},
 				},
 				Status: ingressStatus{
@@ -147,7 +156,7 @@ func TestMappingRoundtrip(tt *testing.T) {
 		},
 	} {
 		tt.Run(tc.msg, func(t *testing.T) {
-			a, err := NewAdapter(testConfig, testIngressFilter, testIngressDefaultSecurityGroup, testSSLPolicy)
+			a, err := NewAdapter(testConfig, testIngressFilter, testIngressDefaultSecurityGroup, testSSLPolicy, testLoadBalancerTypeAWS)
 			if err != nil {
 				t.Fatalf("cannot create kubernetes adapter: %v", err)
 			}
@@ -214,7 +223,7 @@ func (c *mockClient) patch(res string, payload []byte) (io.ReadCloser, error) {
 }
 
 func TestListIngress(t *testing.T) {
-	a, _ := NewAdapter(testConfig, testIngressFilter, testIngressDefaultSecurityGroup, testSSLPolicy)
+	a, _ := NewAdapter(testConfig, testIngressFilter, testIngressDefaultSecurityGroup, testSSLPolicy, testLoadBalancerTypeAWS)
 	client := &mockClient{}
 	a.kubeClient = client
 	ingresses, err := a.ListIngress()
@@ -232,7 +241,7 @@ func TestListIngress(t *testing.T) {
 }
 
 func TestUpdateIngressLoadBalancer(t *testing.T) {
-	a, _ := NewAdapter(testConfig, testIngressFilter, testSecurityGroup, testSSLPolicy)
+	a, _ := NewAdapter(testConfig, testIngressFilter, testSecurityGroup, testSSLPolicy, testLoadBalancerTypeAWS)
 	client := &mockClient{}
 	a.kubeClient = client
 	ing := &Ingress{
@@ -267,7 +276,7 @@ func TestBrokenConfig(t *testing.T) {
 		{"broken-cert", &Config{BaseURL: "dontcare", TLSClientConfig: TLSClientConfig{CAFile: "testdata/broken.pem"}}},
 	} {
 		t.Run(fmt.Sprintf("%v", test.cfg), func(t *testing.T) {
-			_, err := NewAdapter(test.cfg, testIngressFilter, testSecurityGroup, testSSLPolicy)
+			_, err := NewAdapter(test.cfg, testIngressFilter, testSecurityGroup, testSSLPolicy, testLoadBalancerTypeAWS)
 			if err == nil {
 				t.Error("expected an error")
 			}
@@ -276,7 +285,7 @@ func TestBrokenConfig(t *testing.T) {
 }
 
 func TestAdapter_GetConfigMap(t *testing.T) {
-	a, _ := NewAdapter(testConfig, testIngressFilter, testIngressDefaultSecurityGroup, testSSLPolicy)
+	a, _ := NewAdapter(testConfig, testIngressFilter, testIngressDefaultSecurityGroup, testSSLPolicy, testLoadBalancerTypeAWS)
 	client := &mockClient{}
 	a.kubeClient = client
 
diff --git a/worker_test.go b/worker_test.go
index ce85945..5afc6f6 100644
--- a/worker_test.go
+++ b/worker_test.go
@@ -45,10 +45,10 @@ func TestAddIngress(tt *testing.T) {
 		{
 			name: "ip address type not matching",
 			loadBalancer: &loadBalancer{
-				ipAddressType: "ipv4",
+				ipAddressType: aws.IPAddressTypeIPV4,
 			},
 			ingress: &kubernetes.Ingress{
-				IPAddressType: "dualstack",
+				IPAddressType: aws.IPAddressTypeDualstack,
 			},
 			added: false,
 		},