Repository: zalando-incubator/kube-ingress-aws-controller
Pull request #435 (2021-09-16 10:12:11)

Adds support for configurable HTTP target port
New `--alb-http-target-port` and `--nlb-http-target-port` flags allow to configure a second target
port distinct from `--target-port` that would be used by an HTTP listener.

In order to support two target ports controller creates two target
groups that use the same healthcheck configuration.

Signed-off-by: Alexander Yastrebov <alexander.yastrebov@zalando.de>

szuecs: :+1: 
mikkeloscar: :+1: 

Patch:
diff --git a/aws/adapter.go b/aws/adapter.go
index a80354b..3427b69 100644
--- a/aws/adapter.go
+++ b/aws/adapter.go
@@ -43,6 +43,8 @@ type Adapter struct {
 	healthCheckInterval         time.Duration
 	healthCheckTimeout          time.Duration
 	targetPort                  uint
+	albHTTPTargetPort           uint
+	nlbHTTPTargetPort           uint
 	targetHTTPS                 bool
 	creationTimeout             time.Duration
 	idleConnectionTimeout       time.Duration
@@ -250,6 +252,18 @@ func (a *Adapter) WithTargetPort(port uint) *Adapter {
 	return a
 }
 
+// WithALBHTTPTargetPort configures the ALB HTTP target port
+func (a *Adapter) WithALBHTTPTargetPort(port uint) *Adapter {
+	a.albHTTPTargetPort = port
+	return a
+}
+
+// WithNLBHTTPTargetPort configures the NLB HTTP target port
+func (a *Adapter) WithNLBHTTPTargetPort(port uint) *Adapter {
+	a.nlbHTTPTargetPort = port
+	return a
+}
+
 // WithTargetHTTPS returns the receiver adapter after specifying the target port will use HTTPS
 func (a *Adapter) WithTargetHTTPS(https bool) *Adapter {
 	a.targetHTTPS = https
@@ -511,8 +525,8 @@ func (a *Adapter) FindManagedStacks() ([]*Stack, error) {
 func (a *Adapter) UpdateTargetGroupsAndAutoScalingGroups(stacks []*Stack) {
 	targetGroupARNs := make([]string, 0, len(stacks))
 	for _, stack := range stacks {
-		if stack.TargetGroupARN != "" {
-			targetGroupARNs = append(targetGroupARNs, stack.TargetGroupARN)
+		if len(stack.TargetGroupARNs) > 0 {
+			targetGroupARNs = append(targetGroupARNs, stack.TargetGroupARNs...)
 		}
 	}
 
@@ -596,6 +610,8 @@ func (a *Adapter) CreateStack(certificateARNs []string, scheme, securityGroup, o
 		},
 		targetPort:                        a.targetPort,
 		targetHTTPS:                       a.targetHTTPS,
+		httpDisabled:                      a.httpDisabled(loadBalancerType),
+		httpTargetPort:                    a.httpTargetPort(loadBalancerType),
 		timeoutInMinutes:                  uint(a.creationTimeout.Minutes()),
 		stackTerminationProtection:        a.stackTerminationProtection,
 		idleConnectionTimeoutSeconds:      uint(a.idleConnectionTimeout.Seconds()),
@@ -610,7 +626,6 @@ func (a *Adapter) CreateStack(certificateARNs []string, scheme, securityGroup, o
 		cwAlarms:                          cwAlarms,
 		httpRedirectToHTTPS:               a.httpRedirectToHTTPS,
 		nlbCrossZone:                      a.nlbCrossZone,
-		nlbHTTPEnabled:                    a.nlbHTTPEnabled,
 		http2:                             http2,
 		tags:                              a.stackTags,
 		internalDomains:                   a.internalDomains,
@@ -647,6 +662,8 @@ func (a *Adapter) UpdateStack(stackName string, certificateARNs map[string]time.
 		},
 		targetPort:                        a.targetPort,
 		targetHTTPS:                       a.targetHTTPS,
+		httpDisabled:                      a.httpDisabled(loadBalancerType),
+		httpTargetPort:                    a.httpTargetPort(loadBalancerType),
 		timeoutInMinutes:                  uint(a.creationTimeout.Minutes()),
 		stackTerminationProtection:        a.stackTerminationProtection,
 		idleConnectionTimeoutSeconds:      uint(a.idleConnectionTimeout.Seconds()),
@@ -661,7 +678,6 @@ func (a *Adapter) UpdateStack(stackName string, certificateARNs map[string]time.
 		cwAlarms:                          cwAlarms,
 		httpRedirectToHTTPS:               a.httpRedirectToHTTPS,
 		nlbCrossZone:                      a.nlbCrossZone,
-		nlbHTTPEnabled:                    a.nlbHTTPEnabled,
 		http2:                             http2,
 		tags:                              a.stackTags,
 		internalDomains:                   a.internalDomains,
@@ -676,6 +692,22 @@ func (a *Adapter) UpdateStack(stackName string, certificateARNs map[string]time.
 	return updateStack(a.cloudformation, spec)
 }
 
+func (a *Adapter) httpTargetPort(loadBalancerType string) uint {
+	if loadBalancerType == LoadBalancerTypeApplication && a.albHTTPTargetPort != 0 {
+		return a.albHTTPTargetPort
+	} else if loadBalancerType == LoadBalancerTypeNetwork && a.nlbHTTPTargetPort != 0 {
+		return a.nlbHTTPTargetPort
+	}
+	return a.targetPort
+}
+
+func (a *Adapter) httpDisabled(loadBalancerType string) bool {
+	if loadBalancerType == LoadBalancerTypeNetwork {
+		return !a.nlbHTTPEnabled
+	}
+	return false
+}
+
 func (a *Adapter) stackName() string {
 	return normalizeStackName(a.ClusterID())
 }
@@ -688,7 +720,7 @@ func (a *Adapter) GetStack(stackID string) (*Stack, error) {
 // DeleteStack deletes the CloudFormation stack with the given name
 func (a *Adapter) DeleteStack(stack *Stack) error {
 	for _, asg := range a.TargetedAutoScalingGroups {
-		if err := detachTargetGroupsFromAutoScalingGroup(a.autoscaling, []string{stack.TargetGroupARN}, asg.name); err != nil {
+		if err := detachTargetGroupsFromAutoScalingGroup(a.autoscaling, stack.TargetGroupARNs, asg.name); err != nil {
 			return fmt.Errorf("DeleteStack failed to detach: %v", err)
 		}
 	}
diff --git a/aws/cf.go b/aws/cf.go
index f943939..b27ce1e 100644
--- a/aws/cf.go
+++ b/aws/cf.go
@@ -30,7 +30,7 @@ type Stack struct {
 	HTTP2             bool
 	OwnerIngress      string
 	CWAlarmConfigHash string
-	TargetGroupARN    string
+	TargetGroupARNs   []string
 	WAFWebACLID       string
 	CertificateARNs   map[string]time.Time
 	tags              map[string]string
@@ -83,8 +83,14 @@ func (o stackOutput) dnsName() string {
 	return o[outputLoadBalancerDNSName]
 }
 
-func (o stackOutput) targetGroupARN() string {
-	return o[outputTargetGroupARN]
+func (o stackOutput) targetGroupARNs() (arns []string) {
+	if arn, ok := o[outputTargetGroupARN]; ok {
+		arns = append(arns, arn)
+	}
+	if arn, ok := o[outputHTTPTargetGroupARN]; ok {
+		arns = append(arns, arn)
+	}
+	return
 }
 
 // convertStackParameters converts a list of cloudformation stack parameters to
@@ -101,6 +107,7 @@ const (
 	// The following constants should be part of the Output section of the CloudFormation template
 	outputLoadBalancerDNSName = "LoadBalancerDNSName"
 	outputTargetGroupARN      = "TargetGroupARN"
+	outputHTTPTargetGroupARN  = "HTTPTargetGroupARN"
 
 	parameterLoadBalancerSchemeParameter             = "LoadBalancerSchemeParameter"
 	parameterLoadBalancerSecurityGroupParameter      = "LoadBalancerSecurityGroupParameter"
@@ -109,7 +116,8 @@ const (
 	parameterTargetGroupHealthCheckPortParameter     = "TargetGroupHealthCheckPortParameter"
 	parameterTargetGroupHealthCheckIntervalParameter = "TargetGroupHealthCheckIntervalParameter"
 	parameterTargetGroupHealthCheckTimeoutParameter  = "TargetGroupHealthCheckTimeoutParameter"
-	parameterTargetTargetPortParameter               = "TargetGroupTargetPortParameter"
+	parameterTargetGroupTargetPortParameter          = "TargetGroupTargetPortParameter"
+	parameterTargetGroupHTTPTargetPortParameter      = "TargetGroupHTTPTargetPortParameter"
 	parameterTargetGroupVPCIDParameter               = "TargetGroupVPCIDParameter"
 	parameterListenerCertificatesParameter           = "ListenerCertificatesParameter"
 	parameterListenerSslPolicyParameter              = "ListenerSslPolicyParameter"
@@ -131,6 +139,8 @@ type stackSpec struct {
 	healthCheck                       *healthCheck
 	targetPort                        uint
 	targetHTTPS                       bool
+	httpDisabled                      bool
+	httpTargetPort                    uint
 	timeoutInMinutes                  uint
 	customTemplate                    string
 	stackTerminationProtection        bool
@@ -146,7 +156,6 @@ type stackSpec struct {
 	cwAlarms                          CloudWatchAlarmList
 	httpRedirectToHTTPS               bool
 	nlbCrossZone                      bool
-	nlbHTTPEnabled                    bool
 	http2                             bool
 	denyInternalDomains               bool
 	denyInternalDomainsResponse       denyResp
@@ -188,7 +197,7 @@ func createStack(svc cloudformationiface.CloudFormationAPI, spec *stackSpec) (st
 			cfParam(parameterLoadBalancerSecurityGroupParameter, spec.securityGroupID),
 			cfParam(parameterLoadBalancerSubnetsParameter, strings.Join(spec.subnets, ",")),
 			cfParam(parameterTargetGroupVPCIDParameter, spec.vpcID),
-			cfParam(parameterTargetTargetPortParameter, fmt.Sprintf("%d", spec.targetPort)),
+			cfParam(parameterTargetGroupTargetPortParameter, fmt.Sprintf("%d", spec.targetPort)),
 			cfParam(parameterListenerSslPolicyParameter, spec.sslPolicy),
 			cfParam(parameterIpAddressTypeParameter, spec.ipAddressType),
 			cfParam(parameterLoadBalancerTypeParameter, spec.loadbalancerType),
@@ -207,6 +216,13 @@ func createStack(svc cloudformationiface.CloudFormationAPI, spec *stackSpec) (st
 		)
 	}
 
+	if !spec.httpDisabled && spec.httpTargetPort != spec.targetPort {
+		params.Parameters = append(
+			params.Parameters,
+			cfParam(parameterTargetGroupHTTPTargetPortParameter, fmt.Sprintf("%d", spec.httpTargetPort)),
+		)
+	}
+
 	for certARN, ttl := range spec.certificateARNs {
 		params.Tags = append(params.Tags, cfTag(certificateARNTagPrefix+certARN, ttl.Format(time.RFC3339)))
 	}
@@ -256,7 +272,7 @@ func updateStack(svc cloudformationiface.CloudFormationAPI, spec *stackSpec) (st
 			cfParam(parameterLoadBalancerSecurityGroupParameter, spec.securityGroupID),
 			cfParam(parameterLoadBalancerSubnetsParameter, strings.Join(spec.subnets, ",")),
 			cfParam(parameterTargetGroupVPCIDParameter, spec.vpcID),
-			cfParam(parameterTargetTargetPortParameter, fmt.Sprintf("%d", spec.targetPort)),
+			cfParam(parameterTargetGroupTargetPortParameter, fmt.Sprintf("%d", spec.targetPort)),
 			cfParam(parameterListenerSslPolicyParameter, spec.sslPolicy),
 			cfParam(parameterIpAddressTypeParameter, spec.ipAddressType),
 			cfParam(parameterLoadBalancerTypeParameter, spec.loadbalancerType),
@@ -273,6 +289,13 @@ func updateStack(svc cloudformationiface.CloudFormationAPI, spec *stackSpec) (st
 		)
 	}
 
+	if !spec.httpDisabled && spec.httpTargetPort != spec.targetPort {
+		params.Parameters = append(
+			params.Parameters,
+			cfParam(parameterTargetGroupHTTPTargetPortParameter, fmt.Sprintf("%d", spec.httpTargetPort)),
+		)
+	}
+
 	for certARN, ttl := range spec.certificateARNs {
 		params.Tags = append(params.Tags, cfTag(certificateARNTagPrefix+certARN, ttl.Format(time.RFC3339)))
 	}
@@ -434,7 +457,7 @@ func mapToManagedStack(stack *cloudformation.Stack) *Stack {
 	return &Stack{
 		Name:              aws.StringValue(stack.StackName),
 		DNSName:           outputs.dnsName(),
-		TargetGroupARN:    outputs.targetGroupARN(),
+		TargetGroupARNs:   outputs.targetGroupARNs(),
 		Scheme:            parameters[parameterLoadBalancerSchemeParameter],
 		SecurityGroup:     parameters[parameterLoadBalancerSecurityGroupParameter],
 		SSLPolicy:         parameters[parameterListenerSslPolicyParameter],
diff --git a/aws/cf_template.go b/aws/cf_template.go
index ed53357..b1adbf7 100644
--- a/aws/cf_template.go
+++ b/aws/cf_template.go
@@ -64,7 +64,7 @@ func generateTemplate(spec *stackSpec) (string, error) {
 			Description: "The healthcheck port",
 			Default:     "9999",
 		},
-		parameterTargetTargetPortParameter: &cloudformation.Parameter{
+		parameterTargetGroupTargetPortParameter: &cloudformation.Parameter{
 			Type:        "Number",
 			Description: "The target port",
 			Default:     "9999",
@@ -112,63 +112,95 @@ func generateTemplate(spec *stackSpec) (string, error) {
 		}
 	}
 
-	// Add an HTTP Listener resource
-	if spec.loadbalancerType == LoadBalancerTypeApplication {
-		if spec.httpRedirectToHTTPS {
-			template.AddResource("HTTPListener", &cloudformation.ElasticLoadBalancingV2Listener{
-				DefaultActions: &cloudformation.ElasticLoadBalancingV2ListenerActionList{
-					{
-						Type: cloudformation.String("redirect"),
-						RedirectConfig: &cloudformation.ElasticLoadBalancingV2ListenerRedirectConfig{
-							Protocol:   cloudformation.String("HTTPS"),
-							Port:       cloudformation.String("443"),
-							Host:       cloudformation.String("#{host}"),
-							Path:       cloudformation.String("/#{path}"),
-							Query:      cloudformation.String("#{query}"),
-							StatusCode: cloudformation.String("HTTP_301"),
+	const httpsTargetGroupName = "TG"
+
+	template.Outputs = map[string]*cloudformation.Output{
+		outputLoadBalancerDNSName: &cloudformation.Output{
+			Description: "DNS name for the LoadBalancer",
+			Value:       cloudformation.GetAtt("LB", "DNSName").String(),
+		},
+		outputTargetGroupARN: &cloudformation.Output{
+			Description: "The ARN of the TargetGroup",
+			Value:       cloudformation.Ref(httpsTargetGroupName).String(),
+		},
+	}
+
+	template.AddResource(httpsTargetGroupName, newTargetGroup(spec, parameterTargetGroupTargetPortParameter))
+
+	if !spec.httpDisabled {
+		// Use the same target group for HTTP Listener or create another one if needed
+		httpTargetGroupName := httpsTargetGroupName
+		if spec.httpTargetPort != spec.targetPort {
+			httpTargetGroupName = "TGHTTP"
+			template.Parameters[parameterTargetGroupHTTPTargetPortParameter] = &cloudformation.Parameter{
+				Type:        "Number",
+				Description: "The HTTP target port",
+			}
+			template.Outputs[outputHTTPTargetGroupARN] = &cloudformation.Output{
+				Description: "The ARN of the HTTP TargetGroup",
+				Value:       cloudformation.Ref(httpTargetGroupName).String(),
+			}
+			template.AddResource(httpTargetGroupName, newTargetGroup(spec, parameterTargetGroupHTTPTargetPortParameter))
+		}
+
+		// Add an HTTP Listener resource
+		if spec.loadbalancerType == LoadBalancerTypeApplication {
+			if spec.httpRedirectToHTTPS {
+				template.AddResource("HTTPListener", &cloudformation.ElasticLoadBalancingV2Listener{
+					DefaultActions: &cloudformation.ElasticLoadBalancingV2ListenerActionList{
+						{
+							Type: cloudformation.String("redirect"),
+							RedirectConfig: &cloudformation.ElasticLoadBalancingV2ListenerRedirectConfig{
+								Protocol:   cloudformation.String("HTTPS"),
+								Port:       cloudformation.String("443"),
+								Host:       cloudformation.String("#{host}"),
+								Path:       cloudformation.String("/#{path}"),
+								Query:      cloudformation.String("#{query}"),
+								StatusCode: cloudformation.String("HTTP_301"),
+							},
 						},
 					},
-				},
-				LoadBalancerArn: cloudformation.Ref("LB").String(),
-				Port:            cloudformation.Integer(80),
-				Protocol:        cloudformation.String("HTTP"),
-			})
-		} else {
+					LoadBalancerArn: cloudformation.Ref("LB").String(),
+					Port:            cloudformation.Integer(80),
+					Protocol:        cloudformation.String("HTTP"),
+				})
+			} else {
+				template.AddResource("HTTPListener", &cloudformation.ElasticLoadBalancingV2Listener{
+					DefaultActions: &cloudformation.ElasticLoadBalancingV2ListenerActionList{
+						{
+							Type:           cloudformation.String("forward"),
+							TargetGroupArn: cloudformation.Ref(httpTargetGroupName).String(),
+						},
+					},
+					LoadBalancerArn: cloudformation.Ref("LB").String(),
+					Port:            cloudformation.Integer(80),
+					Protocol:        cloudformation.String("HTTP"),
+				})
+				if spec.denyInternalDomains {
+					template.AddResource(
+						"HTTPRuleBlockInternalTraffic",
+						generateDenyInternalTrafficRule(
+							"HTTPListener",
+							internalTrafficDenyRulePriority,
+							spec.internalDomains,
+							spec.denyInternalDomainsResponse,
+						),
+					)
+				}
+			}
+		} else if spec.loadbalancerType == LoadBalancerTypeNetwork {
 			template.AddResource("HTTPListener", &cloudformation.ElasticLoadBalancingV2Listener{
 				DefaultActions: &cloudformation.ElasticLoadBalancingV2ListenerActionList{
 					{
 						Type:           cloudformation.String("forward"),
-						TargetGroupArn: cloudformation.Ref("TG").String(),
+						TargetGroupArn: cloudformation.Ref(httpTargetGroupName).String(),
 					},
 				},
 				LoadBalancerArn: cloudformation.Ref("LB").String(),
 				Port:            cloudformation.Integer(80),
-				Protocol:        cloudformation.String("HTTP"),
+				Protocol:        cloudformation.String("TCP"),
 			})
-			if spec.denyInternalDomains {
-				template.AddResource(
-					"HTTPRuleBlockInternalTraffic",
-					generateDenyInternalTrafficRule(
-						"HTTPListener",
-						internalTrafficDenyRulePriority,
-						spec.internalDomains,
-						spec.denyInternalDomainsResponse,
-					),
-				)
-			}
 		}
-	} else if spec.loadbalancerType == LoadBalancerTypeNetwork && spec.nlbHTTPEnabled {
-		template.AddResource("HTTPListener", &cloudformation.ElasticLoadBalancingV2Listener{
-			DefaultActions: &cloudformation.ElasticLoadBalancingV2ListenerActionList{
-				{
-					Type:           cloudformation.String("forward"),
-					TargetGroupArn: cloudformation.Ref("TG").String(),
-				},
-			},
-			LoadBalancerArn: cloudformation.Ref("LB").String(),
-			Port:            cloudformation.Integer(80),
-			Protocol:        cloudformation.String("TCP"),
-		})
 	}
 
 	if len(spec.certificateARNs) > 0 {
@@ -187,7 +219,7 @@ func generateTemplate(spec *stackSpec) (string, error) {
 				DefaultActions: &cloudformation.ElasticLoadBalancingV2ListenerActionList{
 					{
 						Type:           cloudformation.String("forward"),
-						TargetGroupArn: cloudformation.Ref("TG").String(),
+						TargetGroupArn: cloudformation.Ref(httpsTargetGroupName).String(),
 					},
 				},
 				Certificates: &cloudformation.ElasticLoadBalancingV2ListenerCertificatePropertyList{
@@ -216,7 +248,7 @@ func generateTemplate(spec *stackSpec) (string, error) {
 				DefaultActions: &cloudformation.ElasticLoadBalancingV2ListenerActionList{
 					{
 						Type:           cloudformation.String("forward"),
-						TargetGroupArn: cloudformation.Ref("TG").String(),
+						TargetGroupArn: cloudformation.Ref(httpsTargetGroupName).String(),
 					},
 				},
 				Certificates: &cloudformation.ElasticLoadBalancingV2ListenerCertificatePropertyList{
@@ -337,41 +369,6 @@ func generateTemplate(spec *stackSpec) (string, error) {
 
 	template.AddResource("LB", lb)
 
-	targetGroupAttributes := cloudformation.ElasticLoadBalancingV2TargetGroupTargetGroupAttributeList{
-		{
-			Key:   cloudformation.String("deregistration_delay.timeout_seconds"),
-			Value: cloudformation.String(fmt.Sprintf("%d", spec.deregistrationDelayTimeoutSeconds)),
-		},
-	}
-
-	protocol := "HTTP"
-	healthCheckProtocol := "HTTP"
-	if spec.loadbalancerType == LoadBalancerTypeNetwork {
-		protocol = "TCP"
-		healthCheckProtocol = "HTTP"
-	} else if spec.targetHTTPS {
-		protocol = "HTTPS"
-		healthCheckProtocol = "HTTPS"
-	}
-
-	targetGroup := &cloudformation.ElasticLoadBalancingV2TargetGroup{
-		TargetGroupAttributes: &targetGroupAttributes,
-
-		HealthCheckIntervalSeconds: cloudformation.Ref(parameterTargetGroupHealthCheckIntervalParameter).Integer(),
-		HealthCheckPath:            cloudformation.Ref(parameterTargetGroupHealthCheckPathParameter).String(),
-		HealthCheckPort:            cloudformation.Ref(parameterTargetGroupHealthCheckPortParameter).String(),
-		HealthCheckProtocol:        cloudformation.String(healthCheckProtocol),
-		Port:                       cloudformation.Ref(parameterTargetTargetPortParameter).Integer(),
-		Protocol:                   cloudformation.String(protocol),
-		VPCID:                      cloudformation.Ref(parameterTargetGroupVPCIDParameter).String(),
-	}
-
-	// custom target group healthcheck only supported when the target group protocol is != TCP
-	if protocol != "TCP" {
-		targetGroup.HealthCheckTimeoutSeconds = cloudformation.Ref(parameterTargetGroupHealthCheckTimeoutParameter).Integer()
-	}
-	template.AddResource("TG", targetGroup)
-
 	if spec.loadbalancerType == LoadBalancerTypeApplication && spec.wafWebAclId != "" {
 		if strings.HasPrefix(spec.wafWebAclId, "arn:aws:wafv2:") {
 			template.AddResource("WAFAssociation", &cloudformation.WAFv2WebACLAssociation{
@@ -410,17 +407,6 @@ func generateTemplate(spec *stackSpec) (string, error) {
 		})
 	}
 
-	template.Outputs = map[string]*cloudformation.Output{
-		"LoadBalancerDNSName": &cloudformation.Output{
-			Description: "DNS name for the LoadBalancer",
-			Value:       cloudformation.GetAtt("LB", "DNSName").String(),
-		},
-		"TargetGroupARN": &cloudformation.Output{
-			Description: "The ARN of the TargetGroup",
-			Value:       cloudformation.Ref("TG").String(),
-		},
-	}
-
 	stackTemplate, err := json.MarshalIndent(template, "", "    ")
 	if err != nil {
 		return "", err
@@ -456,3 +442,37 @@ func generateDenyInternalTrafficRule(listenerName string, rulePriority int64, in
 		ListenerArn: cloudformation.Ref(listenerName).String(),
 	}
 }
+
+func newTargetGroup(spec *stackSpec, targetPortParameter string) *cloudformation.ElasticLoadBalancingV2TargetGroup {
+	protocol := "HTTP"
+	healthCheckProtocol := "HTTP"
+	if spec.loadbalancerType == LoadBalancerTypeNetwork {
+		protocol = "TCP"
+		healthCheckProtocol = "HTTP"
+	} else if spec.targetHTTPS {
+		protocol = "HTTPS"
+		healthCheckProtocol = "HTTPS"
+	}
+
+	targetGroup := &cloudformation.ElasticLoadBalancingV2TargetGroup{
+		TargetGroupAttributes: &cloudformation.ElasticLoadBalancingV2TargetGroupTargetGroupAttributeList{
+			{
+				Key:   cloudformation.String("deregistration_delay.timeout_seconds"),
+				Value: cloudformation.String(fmt.Sprintf("%d", spec.deregistrationDelayTimeoutSeconds)),
+			},
+		},
+		HealthCheckIntervalSeconds: cloudformation.Ref(parameterTargetGroupHealthCheckIntervalParameter).Integer(),
+		HealthCheckPath:            cloudformation.Ref(parameterTargetGroupHealthCheckPathParameter).String(),
+		HealthCheckPort:            cloudformation.Ref(parameterTargetGroupHealthCheckPortParameter).String(),
+		HealthCheckProtocol:        cloudformation.String(healthCheckProtocol),
+		Port:                       cloudformation.Ref(targetPortParameter).Integer(),
+		Protocol:                   cloudformation.String(protocol),
+		VPCID:                      cloudformation.Ref(parameterTargetGroupVPCIDParameter).String(),
+	}
+
+	// custom target group healthcheck only supported when the target group protocol is != TCP
+	if protocol != "TCP" {
+		targetGroup.HealthCheckTimeoutSeconds = cloudformation.Ref(parameterTargetGroupHealthCheckTimeoutParameter).Integer()
+	}
+	return targetGroup
+}
diff --git a/controller.go b/controller.go
index aaa8d90..6d90732 100644
--- a/controller.go
+++ b/controller.go
@@ -43,6 +43,8 @@ var (
 	healthCheckInterval           time.Duration
 	healthCheckTimeout            time.Duration
 	targetPort                    uint
+	albHTTPTargetPort             uint
+	nlbHTTPTargetPort             uint
 	targetHTTPS                   bool
 	metricsAddress                string
 	disableSNISupport             bool
@@ -111,6 +113,10 @@ func loadSettings() error {
 		Default(strconv.FormatUint(aws.DefaultHealthCheckPort, 10)).UintVar(&healthCheckPort)
 	kingpin.Flag("target-port", "sets the target port for the created target groups").
 		Default(strconv.FormatUint(aws.DefaultTargetPort, 10)).UintVar(&targetPort)
+	kingpin.Flag("alb-http-target-port", "Sets the target port for ALB HTTP listener different from --target-port.").
+		UintVar(&albHTTPTargetPort)
+	kingpin.Flag("nlb-http-target-port", "Sets the target port for NLB HTTP listener different from --target-port. Requires --nlb-http-enabled.").
+		UintVar(&nlbHTTPTargetPort)
 	kingpin.Flag("target-https", "sets the target protocol to https").
 		Default("false").BoolVar(&targetHTTPS)
 	kingpin.Flag("health-check-interval", "sets the health check interval for the created target groups. The flag accepts a value acceptable to time.ParseDuration").
@@ -186,6 +192,18 @@ func loadSettings() error {
 		return fmt.Errorf("invalid target port: %d. please use a valid TCP port", targetPort)
 	}
 
+	if albHTTPTargetPort > 65535 { // default 0
+		return fmt.Errorf("invalid ALB HTTP target port: %d. please use a valid TCP port", albHTTPTargetPort)
+	}
+
+	if nlbHTTPTargetPort > 65535 { // default 0
+		return fmt.Errorf("invalid NLB HTTP target port: %d. please use a valid TCP port", nlbHTTPTargetPort)
+	}
+
+	if nlbHTTPTargetPort > 0 && !nlbHTTPEnabled {
+		return fmt.Errorf("NLB HTTP is not enabled")
+	}
+
 	if maxCertsPerALB > aws.DefaultMaxCertsPerALB {
 		return fmt.Errorf("invalid max number of certificates per ALB: %d. AWS does not allow more than %d", maxCertsPerALB, aws.DefaultMaxCertsPerALB)
 	}
@@ -255,6 +273,8 @@ func main() {
 		WithHealthCheckInterval(healthCheckInterval).
 		WithHealthCheckTimeout(healthCheckTimeout).
 		WithTargetPort(targetPort).
+		WithALBHTTPTargetPort(albHTTPTargetPort).
+		WithNLBHTTPTargetPort(nlbHTTPTargetPort).
 		WithTargetHTTPS(targetHTTPS).
 		WithCreationTimeout(creationTimeout).
 		WithStackTerminationProtection(stackTerminationProtection).


Test patch:
diff --git a/aws/cf_template_test.go b/aws/cf_template_test.go
index 5327519..d0e5181 100644
--- a/aws/cf_template_test.go
+++ b/aws/cf_template_test.go
@@ -135,6 +135,102 @@ func TestGenerateTemplate(t *testing.T) {
 				)
 			},
 		},
+		{
+			name: "ALB should only have HTTP listener when no certificate is present",
+			spec: &stackSpec{
+				loadbalancerType: LoadBalancerTypeApplication,
+			},
+			validate: func(t *testing.T, template *cloudformation.Template) {
+				requireTargetGroups(t, template, "TG")
+				requireListeners(t, template, "HTTPListener")
+
+				tg := template.Resources["TG"].Properties.(*cloudformation.ElasticLoadBalancingV2TargetGroup)
+
+				require.Contains(t, template.Parameters, parameterTargetGroupTargetPortParameter)
+				require.Equal(t, cloudformation.Ref(parameterTargetGroupTargetPortParameter).Integer(), tg.Port)
+				require.Equal(t, cloudformation.String("HTTP"), tg.Protocol)
+				require.Equal(t, cloudformation.String("HTTP"), tg.HealthCheckProtocol)
+
+				validateTargetGroupListener(t, template, "TG", "HTTPListener", 80, "HTTP")
+				validateTargetGroupOutput(t, template, "TG", "TargetGroupARN")
+			},
+		},
+		{
+			name: "ALB should have two listeners when certificate is present",
+			spec: &stackSpec{
+				loadbalancerType: LoadBalancerTypeApplication,
+				certificateARNs:  map[string]time.Time{"domain.company.com": time.Now()},
+			},
+			validate: func(t *testing.T, template *cloudformation.Template) {
+				requireTargetGroups(t, template, "TG")
+				requireListeners(t, template, "HTTPListener", "HTTPSListener")
+
+				tg := template.Resources["TG"].Properties.(*cloudformation.ElasticLoadBalancingV2TargetGroup)
+
+				require.Contains(t, template.Parameters, parameterTargetGroupTargetPortParameter)
+				require.Equal(t, cloudformation.Ref(parameterTargetGroupTargetPortParameter).Integer(), tg.Port)
+				require.Equal(t, cloudformation.String("HTTP"), tg.Protocol)
+				require.Equal(t, cloudformation.String("HTTP"), tg.HealthCheckProtocol)
+
+				validateTargetGroupListener(t, template, "TG", "HTTPListener", 80, "HTTP")
+				validateTargetGroupListener(t, template, "TG", "HTTPSListener", 443, "HTTPS")
+				validateTargetGroupOutput(t, template, "TG", "TargetGroupARN")
+			},
+		},
+		{
+			name: "ALB should use separate Target Group for HTTP when HTTP target port is configured",
+			spec: &stackSpec{
+				loadbalancerType: LoadBalancerTypeApplication,
+				certificateARNs:  map[string]time.Time{"domain.company.com": time.Now()},
+				httpTargetPort:   8888,
+			},
+			validate: func(t *testing.T, template *cloudformation.Template) {
+				requireTargetGroups(t, template, "TGHTTP", "TG")
+				requireListeners(t, template, "HTTPListener", "HTTPSListener")
+
+				tgHTTP := template.Resources["TGHTTP"].Properties.(*cloudformation.ElasticLoadBalancingV2TargetGroup)
+				require.Contains(t, template.Parameters, parameterTargetGroupHTTPTargetPortParameter)
+				require.Equal(t, cloudformation.Ref(parameterTargetGroupHTTPTargetPortParameter).Integer(), tgHTTP.Port)
+				require.Equal(t, cloudformation.String("HTTP"), tgHTTP.Protocol)
+				require.Equal(t, cloudformation.String("HTTP"), tgHTTP.HealthCheckProtocol)
+
+				validateTargetGroupListener(t, template, "TGHTTP", "HTTPListener", 80, "HTTP")
+				validateTargetGroupOutput(t, template, "TGHTTP", "HTTPTargetGroupARN")
+
+				tg := template.Resources["TG"].Properties.(*cloudformation.ElasticLoadBalancingV2TargetGroup)
+				require.Contains(t, template.Parameters, parameterTargetGroupTargetPortParameter)
+				require.Equal(t, cloudformation.Ref(parameterTargetGroupTargetPortParameter).Integer(), tg.Port)
+				require.Equal(t, cloudformation.String("HTTP"), tg.Protocol)
+				require.Equal(t, cloudformation.String("HTTP"), tg.HealthCheckProtocol)
+
+				validateTargetGroupListener(t, template, "TG", "HTTPSListener", 443, "HTTPS")
+				validateTargetGroupOutput(t, template, "TG", "TargetGroupARN")
+			},
+		},
+		{
+			name: "ALB should use one Target Group when HTTP target port equals target port",
+			spec: &stackSpec{
+				loadbalancerType: LoadBalancerTypeApplication,
+				certificateARNs:  map[string]time.Time{"domain.company.com": time.Now()},
+				targetPort:       9999,
+				httpTargetPort:   9999,
+			},
+			validate: func(t *testing.T, template *cloudformation.Template) {
+				requireTargetGroups(t, template, "TG")
+				requireListeners(t, template, "HTTPListener", "HTTPSListener")
+
+				tg := template.Resources["TG"].Properties.(*cloudformation.ElasticLoadBalancingV2TargetGroup)
+				require.NotContains(t, template.Parameters, parameterTargetGroupHTTPTargetPortParameter)
+				require.Contains(t, template.Parameters, parameterTargetGroupTargetPortParameter)
+				require.Equal(t, cloudformation.Ref(parameterTargetGroupTargetPortParameter).Integer(), tg.Port)
+				require.Equal(t, cloudformation.String("HTTP"), tg.Protocol)
+				require.Equal(t, cloudformation.String("HTTP"), tg.HealthCheckProtocol)
+
+				validateTargetGroupListener(t, template, "TG", "HTTPListener", 80, "HTTP")
+				validateTargetGroupListener(t, template, "TG", "HTTPSListener", 443, "HTTPS")
+				validateTargetGroupOutput(t, template, "TG", "TargetGroupARN")
+			},
+		},
 		{
 			name: "http -> https redirect should be enabled for Application load balancers",
 			spec: &stackSpec{
@@ -142,7 +238,8 @@ func TestGenerateTemplate(t *testing.T) {
 				httpRedirectToHTTPS: true,
 			},
 			validate: func(t *testing.T, template *cloudformation.Template) {
-				require.NotNil(t, template.Resources["HTTPListener"])
+				requireTargetGroups(t, template, "TG")
+				requireListeners(t, template, "HTTPListener")
 				listener := template.Resources["HTTPListener"].Properties.(*cloudformation.ElasticLoadBalancingV2Listener)
 				require.Len(t, *listener.DefaultActions, 1)
 				redirectConfig := []cloudformation.ElasticLoadBalancingV2ListenerAction(*listener.DefaultActions)[0].RedirectConfig
@@ -156,10 +253,10 @@ func TestGenerateTemplate(t *testing.T) {
 			spec: &stackSpec{
 				loadbalancerType:    LoadBalancerTypeNetwork,
 				httpRedirectToHTTPS: true,
-				nlbHTTPEnabled:      true,
 			},
 			validate: func(t *testing.T, template *cloudformation.Template) {
-				require.NotNil(t, template.Resources["HTTPListener"])
+				requireTargetGroups(t, template, "TG")
+				requireListeners(t, template, "HTTPListener")
 				listener := template.Resources["HTTPListener"].Properties.(*cloudformation.ElasticLoadBalancingV2Listener)
 				require.Len(t, *listener.DefaultActions, 1)
 				redirectConfig := []cloudformation.ElasticLoadBalancingV2ListenerAction(*listener.DefaultActions)[0].RedirectConfig
@@ -181,13 +278,15 @@ func TestGenerateTemplate(t *testing.T) {
 			},
 		},
 		{
-			name: "nlb HTTP listener should not be enabled when nlbHTTPEnabled is set to false",
+			name: "nlb HTTP listener should not be enabled when HTTP is disabled",
 			spec: &stackSpec{
 				loadbalancerType: LoadBalancerTypeNetwork,
-				nlbHTTPEnabled:   false,
+				certificateARNs:  map[string]time.Time{"domain.company.com": time.Now()},
+				httpDisabled:     true,
 			},
 			validate: func(t *testing.T, template *cloudformation.Template) {
-				require.NotContains(t, template.Resources, "HTTPListener")
+				requireTargetGroups(t, template, "TG")
+				requireListeners(t, template, "HTTPSListener")
 			},
 		},
 		{
@@ -344,10 +443,8 @@ func TestGenerateTemplate(t *testing.T) {
 		{
 			name: "Does not create deny internal traffic rule on NLBs",
 			spec: &stackSpec{
-				loadbalancerType: LoadBalancerTypeNetwork,
-				certificateARNs:  map[string]time.Time{"domain.company.com": time.Now()},
-				nlbHTTPEnabled:   true,
-
+				loadbalancerType:    LoadBalancerTypeNetwork,
+				certificateARNs:     map[string]time.Time{"domain.company.com": time.Now()},
 				denyInternalDomains: true,
 			},
 			validate: func(t *testing.T, template *cloudformation.Template) {
@@ -363,13 +460,15 @@ func TestGenerateTemplate(t *testing.T) {
 				targetHTTPS:      true,
 			},
 			validate: func(t *testing.T, template *cloudformation.Template) {
-				tg, ok := template.Resources["TG"].Properties.(*cloudformation.ElasticLoadBalancingV2TargetGroup)
-				require.True(t, ok, "couldn't convert resource to ElasticLoadBalancingV2TargetGroup")
+				requireTargetGroups(t, template, "TG")
+				requireListeners(t, template, "HTTPListener", "HTTPSListener")
+
+				tg := template.Resources["TG"].Properties.(*cloudformation.ElasticLoadBalancingV2TargetGroup)
 				require.Equal(t, cloudformation.String("HTTPS"), tg.Protocol)
 				require.Equal(t, cloudformation.String("HTTPS"), tg.HealthCheckProtocol)
 
-				listener := template.Resources["HTTPListener"].Properties.(*cloudformation.ElasticLoadBalancingV2Listener)
-				require.Equal(t, cloudformation.String("HTTP"), listener.Protocol)
+				validateTargetGroupListener(t, template, "TG", "HTTPListener", 80, "HTTP")
+				validateTargetGroupListener(t, template, "TG", "HTTPSListener", 443, "HTTPS")
 			},
 		},
 		{
@@ -377,15 +476,17 @@ func TestGenerateTemplate(t *testing.T) {
 			spec: &stackSpec{
 				loadbalancerType: LoadBalancerTypeNetwork,
 				certificateARNs:  map[string]time.Time{"domain.company.com": time.Now()},
-				nlbHTTPEnabled:   true,
 			},
 			validate: func(t *testing.T, template *cloudformation.Template) {
-				tg, ok := template.Resources["TG"].Properties.(*cloudformation.ElasticLoadBalancingV2TargetGroup)
-				require.True(t, ok, "couldn't convert resource to ElasticLoadBalancingV2TargetGroup")
-				listener := template.Resources["HTTPListener"].Properties.(*cloudformation.ElasticLoadBalancingV2Listener)
-				require.Equal(t, cloudformation.String("TCP"), listener.Protocol)
+				requireTargetGroups(t, template, "TG")
+				requireListeners(t, template, "HTTPListener", "HTTPSListener")
+
+				tg := template.Resources["TG"].Properties.(*cloudformation.ElasticLoadBalancingV2TargetGroup)
 				require.Equal(t, cloudformation.String("TCP"), tg.Protocol)
 				require.Equal(t, cloudformation.String("HTTP"), tg.HealthCheckProtocol)
+
+				validateTargetGroupListener(t, template, "TG", "HTTPListener", 80, "TCP")
+				validateTargetGroupListener(t, template, "TG", "HTTPSListener", 443, "TLS")
 			},
 		},
 		{
@@ -393,16 +494,95 @@ func TestGenerateTemplate(t *testing.T) {
 			spec: &stackSpec{
 				loadbalancerType: LoadBalancerTypeNetwork,
 				certificateARNs:  map[string]time.Time{"domain.company.com": time.Now()},
-				nlbHTTPEnabled:   true,
 				targetHTTPS:      true,
 			},
 			validate: func(t *testing.T, template *cloudformation.Template) {
-				tg, ok := template.Resources["TG"].Properties.(*cloudformation.ElasticLoadBalancingV2TargetGroup)
-				require.True(t, ok, "couldn't convert resource to ElasticLoadBalancingV2TargetGroup")
-				listener := template.Resources["HTTPListener"].Properties.(*cloudformation.ElasticLoadBalancingV2Listener)
-				require.Equal(t, cloudformation.String("TCP"), listener.Protocol)
+				requireTargetGroups(t, template, "TG")
+				requireListeners(t, template, "HTTPListener", "HTTPSListener")
+
+				tg := template.Resources["TG"].Properties.(*cloudformation.ElasticLoadBalancingV2TargetGroup)
+				require.Equal(t, cloudformation.String("TCP"), tg.Protocol)
+				require.Equal(t, cloudformation.String("HTTP"), tg.HealthCheckProtocol)
+
+				validateTargetGroupListener(t, template, "TG", "HTTPListener", 80, "TCP")
+				validateTargetGroupListener(t, template, "TG", "HTTPSListener", 443, "TLS")
+			},
+		},
+		{
+			name: "NLB should use separate Target Group for HTTP when HTTP target port is configured",
+			spec: &stackSpec{
+				loadbalancerType: LoadBalancerTypeNetwork,
+				certificateARNs:  map[string]time.Time{"domain.company.com": time.Now()},
+				httpTargetPort:   8888,
+			},
+			validate: func(t *testing.T, template *cloudformation.Template) {
+				requireTargetGroups(t, template, "TGHTTP", "TG")
+				requireListeners(t, template, "HTTPListener", "HTTPSListener")
+
+				tgHTTP := template.Resources["TGHTTP"].Properties.(*cloudformation.ElasticLoadBalancingV2TargetGroup)
+				require.Contains(t, template.Parameters, parameterTargetGroupHTTPTargetPortParameter)
+				require.Equal(t, cloudformation.Ref(parameterTargetGroupHTTPTargetPortParameter).Integer(), tgHTTP.Port)
+				require.Equal(t, cloudformation.String("TCP"), tgHTTP.Protocol)
+				require.Equal(t, cloudformation.String("HTTP"), tgHTTP.HealthCheckProtocol)
+
+				validateTargetGroupListener(t, template, "TGHTTP", "HTTPListener", 80, "TCP")
+				validateTargetGroupOutput(t, template, "TGHTTP", "HTTPTargetGroupARN")
+
+				tg := template.Resources["TG"].Properties.(*cloudformation.ElasticLoadBalancingV2TargetGroup)
+				require.Contains(t, template.Parameters, parameterTargetGroupTargetPortParameter)
+				require.Equal(t, cloudformation.Ref(parameterTargetGroupTargetPortParameter).Integer(), tg.Port)
 				require.Equal(t, cloudformation.String("TCP"), tg.Protocol)
 				require.Equal(t, cloudformation.String("HTTP"), tg.HealthCheckProtocol)
+
+				validateTargetGroupListener(t, template, "TG", "HTTPSListener", 443, "TLS")
+				validateTargetGroupOutput(t, template, "TG", "TargetGroupARN")
+			},
+		},
+		{
+			name: "NLB should use one Target Group when HTTP target port equals target port",
+			spec: &stackSpec{
+				loadbalancerType: LoadBalancerTypeNetwork,
+				certificateARNs:  map[string]time.Time{"domain.company.com": time.Now()},
+				targetPort:       9999,
+				httpTargetPort:   9999,
+			},
+			validate: func(t *testing.T, template *cloudformation.Template) {
+				requireTargetGroups(t, template, "TG")
+				requireListeners(t, template, "HTTPListener", "HTTPSListener")
+
+				tg := template.Resources["TG"].Properties.(*cloudformation.ElasticLoadBalancingV2TargetGroup)
+				require.NotContains(t, template.Parameters, parameterTargetGroupHTTPTargetPortParameter)
+				require.Contains(t, template.Parameters, parameterTargetGroupTargetPortParameter)
+				require.Equal(t, cloudformation.Ref(parameterTargetGroupTargetPortParameter).Integer(), tg.Port)
+				require.Equal(t, cloudformation.String("TCP"), tg.Protocol)
+				require.Equal(t, cloudformation.String("HTTP"), tg.HealthCheckProtocol)
+
+				validateTargetGroupListener(t, template, "TG", "HTTPListener", 80, "TCP")
+				validateTargetGroupListener(t, template, "TG", "HTTPSListener", 443, "TLS")
+				validateTargetGroupOutput(t, template, "TG", "TargetGroupARN")
+			},
+		},
+		{
+			name: "NLB HTTP target port is ignored when HTTP is disabled",
+			spec: &stackSpec{
+				loadbalancerType: LoadBalancerTypeNetwork,
+				certificateARNs:  map[string]time.Time{"domain.company.com": time.Now()},
+				httpDisabled:     true,
+				httpTargetPort:   8888,
+			},
+			validate: func(t *testing.T, template *cloudformation.Template) {
+				requireTargetGroups(t, template, "TG")
+				requireListeners(t, template, "HTTPSListener")
+
+				tg := template.Resources["TG"].Properties.(*cloudformation.ElasticLoadBalancingV2TargetGroup)
+				require.NotContains(t, template.Parameters, parameterTargetGroupHTTPTargetPortParameter)
+				require.Contains(t, template.Parameters, parameterTargetGroupTargetPortParameter)
+				require.Equal(t, cloudformation.Ref(parameterTargetGroupTargetPortParameter).Integer(), tg.Port)
+				require.Equal(t, cloudformation.String("TCP"), tg.Protocol)
+				require.Equal(t, cloudformation.String("HTTP"), tg.HealthCheckProtocol)
+
+				validateTargetGroupListener(t, template, "TG", "HTTPSListener", 443, "TLS")
+				validateTargetGroupOutput(t, template, "TG", "TargetGroupARN")
 			},
 		},
 	} {
@@ -421,3 +601,45 @@ func TestGenerateTemplate(t *testing.T) {
 		})
 	}
 }
+
+func validateTargetGroupListener(t *testing.T, template *cloudformation.Template, targetGroup string, name string, port int64, protocol string) {
+	resource, ok := template.Resources[name]
+	require.True(t, ok, "Resource %s expected", name)
+
+	listener, ok := resource.Properties.(*cloudformation.ElasticLoadBalancingV2Listener)
+	require.True(t, ok, "Wrong type")
+	require.Equal(t, cloudformation.String("forward"), (*listener.DefaultActions)[0].Type)
+	require.Equal(t, cloudformation.Ref(targetGroup).String(), (*listener.DefaultActions)[0].TargetGroupArn)
+	require.Equal(t, cloudformation.Integer(port), listener.Port)
+	require.Equal(t, cloudformation.String(protocol), listener.Protocol)
+}
+
+func validateTargetGroupOutput(t *testing.T, template *cloudformation.Template, targetGroup, name string) {
+	output, ok := template.Outputs[name]
+	require.True(t, ok, "Resource %s expected", name)
+	require.Equal(t, map[string]interface{}{"Ref": targetGroup}, output.Value)
+}
+
+func requireTargetGroups(t *testing.T, template *cloudformation.Template, expected ...string) {
+	requireResources(t, template, expected, func(v interface{}) bool {
+		_, ok := v.(*cloudformation.ElasticLoadBalancingV2TargetGroup)
+		return ok
+	})
+}
+
+func requireListeners(t *testing.T, template *cloudformation.Template, expected ...string) {
+	requireResources(t, template, expected, func(v interface{}) bool {
+		_, ok := v.(*cloudformation.ElasticLoadBalancingV2Listener)
+		return ok
+	})
+}
+
+func requireResources(t *testing.T, template *cloudformation.Template, expected []string, matches func(interface{}) bool) {
+	var names []string
+	for name, resource := range template.Resources {
+		if matches(resource.Properties) {
+			names = append(names, name)
+		}
+	}
+	require.ElementsMatch(t, expected, names)
+}
diff --git a/aws/cf_test.go b/aws/cf_test.go
index e0877cc..ee75851 100644
--- a/aws/cf_test.go
+++ b/aws/cf_test.go
@@ -58,6 +58,32 @@ func TestCreatingStack(t *testing.T) {
 			"fake-stack-id",
 			false,
 		},
+		{
+			"stack with ALB http port",
+			stackSpec{
+				name:             "foo",
+				securityGroupID:  "bar",
+				vpcID:            "baz",
+				loadbalancerType: LoadBalancerTypeApplication,
+				httpTargetPort:   7777,
+			},
+			cfMockOutputs{createStack: R(mockCSOutput("fake-stack-id"), nil)},
+			"fake-stack-id",
+			false,
+		},
+		{
+			"stack with NLB http port",
+			stackSpec{
+				name:             "foo",
+				securityGroupID:  "bar",
+				vpcID:            "baz",
+				loadbalancerType: LoadBalancerTypeNetwork,
+				httpTargetPort:   8888,
+			},
+			cfMockOutputs{createStack: R(mockCSOutput("fake-stack-id"), nil)},
+			"fake-stack-id",
+			false,
+		},
 	} {
 		t.Run(ti.name, func(t *testing.T) {
 			c := &mockCloudFormationClient{outputs: ti.givenOutputs}
@@ -124,6 +150,32 @@ func TestUpdatingStack(t *testing.T) {
 			"fake-stack-id",
 			false,
 		},
+		{
+			"stack with ALB http port",
+			stackSpec{
+				name:             "foo",
+				securityGroupID:  "bar",
+				vpcID:            "baz",
+				loadbalancerType: LoadBalancerTypeApplication,
+				httpTargetPort:   7777,
+			},
+			cfMockOutputs{updateStack: R(mockUSOutput("fake-stack-id"), nil)},
+			"fake-stack-id",
+			false,
+		},
+		{
+			"stack with NLB http port",
+			stackSpec{
+				name:             "foo",
+				securityGroupID:  "bar",
+				vpcID:            "baz",
+				loadbalancerType: LoadBalancerTypeNetwork,
+				httpTargetPort:   8888,
+			},
+			cfMockOutputs{updateStack: R(mockUSOutput("fake-stack-id"), nil)},
+			"fake-stack-id",
+			false,
+		},
 	} {
 		t.Run(ti.name, func(t *testing.T) {
 			c := &mockCloudFormationClient{outputs: ti.givenOutputs}
@@ -332,6 +384,20 @@ func TestFindManagedStacks(t *testing.T) {
 								{OutputKey: aws.String(outputTargetGroupARN), OutputValue: aws.String("tg-arn")},
 							},
 						},
+						{
+							StackName:   aws.String("managed-stack-http-arn"),
+							StackStatus: aws.String(cloudformation.StackStatusCreateComplete),
+							Tags: []*cloudformation.Tag{
+								cfTag(kubernetesCreatorTag, DefaultControllerID),
+								cfTag(clusterIDTagPrefix+"test-cluster", resourceLifecycleOwned),
+								cfTag(certificateARNTagPrefix+"cert-arn", time.Time{}.Format(time.RFC3339)),
+							},
+							Outputs: []*cloudformation.Output{
+								{OutputKey: aws.String(outputLoadBalancerDNSName), OutputValue: aws.String("example.com")},
+								{OutputKey: aws.String(outputTargetGroupARN), OutputValue: aws.String("tg-arn")},
+								{OutputKey: aws.String(outputHTTPTargetGroupARN), OutputValue: aws.String("http-tg-arn")},
+							},
+						},
 						{
 							StackName:   aws.String("managed-stack-not-ready"),
 							StackStatus: aws.String(cloudformation.StackStatusUpdateInProgress),
@@ -369,7 +435,7 @@ func TestFindManagedStacks(t *testing.T) {
 					CertificateARNs: map[string]time.Time{
 						"cert-arn": time.Time{},
 					},
-					TargetGroupARN: "tg-arn",
+					TargetGroupARNs: []string{"tg-arn"},
 					tags: map[string]string{
 						kubernetesCreatorTag:                 DefaultControllerID,
 						clusterIDTagPrefix + "test-cluster":  resourceLifecycleOwned,
@@ -384,7 +450,22 @@ func TestFindManagedStacks(t *testing.T) {
 					CertificateARNs: map[string]time.Time{
 						"cert-arn": time.Time{},
 					},
-					TargetGroupARN: "tg-arn",
+					TargetGroupARNs: []string{"tg-arn"},
+					tags: map[string]string{
+						kubernetesCreatorTag:                 DefaultControllerID,
+						clusterIDTagPrefix + "test-cluster":  resourceLifecycleOwned,
+						certificateARNTagPrefix + "cert-arn": time.Time{}.Format(time.RFC3339),
+					},
+					status: cloudformation.StackStatusCreateComplete,
+					HTTP2:  true,
+				},
+				{
+					Name:    "managed-stack-http-arn",
+					DNSName: "example.com",
+					CertificateARNs: map[string]time.Time{
+						"cert-arn": time.Time{},
+					},
+					TargetGroupARNs: []string{"tg-arn", "http-tg-arn"},
 					tags: map[string]string{
 						kubernetesCreatorTag:                 DefaultControllerID,
 						clusterIDTagPrefix + "test-cluster":  resourceLifecycleOwned,
@@ -443,7 +524,7 @@ func TestFindManagedStacks(t *testing.T) {
 				{
 					Name:            "managed-stack-not-ready",
 					DNSName:         "example-notready.com",
-					TargetGroupARN:  "tg-arn",
+					TargetGroupARNs: []string{"tg-arn"},
 					CertificateARNs: map[string]time.Time{},
 					tags: map[string]string{
 						kubernetesCreatorTag:                DefaultControllerID,
@@ -455,7 +536,7 @@ func TestFindManagedStacks(t *testing.T) {
 				{
 					Name:            "managed-stack",
 					DNSName:         "example.com",
-					TargetGroupARN:  "tg-arn",
+					TargetGroupARNs: []string{"tg-arn"},
 					CertificateARNs: map[string]time.Time{},
 					tags: map[string]string{
 						kubernetesCreatorTag:                DefaultControllerID,
@@ -536,7 +617,47 @@ func TestGetStack(t *testing.T) {
 				CertificateARNs: map[string]time.Time{
 					"cert-arn": time.Time{},
 				},
-				TargetGroupARN: "tg-arn",
+				TargetGroupARNs: []string{"tg-arn"},
+				tags: map[string]string{
+					kubernetesCreatorTag:                 DefaultControllerID,
+					clusterIDTagPrefix + "test-cluster":  resourceLifecycleOwned,
+					certificateARNTagPrefix + "cert-arn": time.Time{}.Format(time.RFC3339),
+				},
+				status: cloudformation.StackStatusCreateComplete,
+				HTTP2:  true,
+			},
+			wantErr: false,
+		},
+		{
+			name: "successful-call-http-arn",
+			given: cfMockOutputs{
+				describeStackPages: R(nil, nil),
+				describeStacks: R(&cloudformation.DescribeStacksOutput{
+					Stacks: []*cloudformation.Stack{
+						{
+							StackName:   aws.String("managed-stack"),
+							StackStatus: aws.String(cloudformation.StackStatusCreateComplete),
+							Tags: []*cloudformation.Tag{
+								cfTag(kubernetesCreatorTag, DefaultControllerID),
+								cfTag(clusterIDTagPrefix+"test-cluster", resourceLifecycleOwned),
+								cfTag(certificateARNTagPrefix+"cert-arn", time.Time{}.Format(time.RFC3339)),
+							},
+							Outputs: []*cloudformation.Output{
+								{OutputKey: aws.String(outputLoadBalancerDNSName), OutputValue: aws.String("example.com")},
+								{OutputKey: aws.String(outputTargetGroupARN), OutputValue: aws.String("tg-arn")},
+								{OutputKey: aws.String(outputHTTPTargetGroupARN), OutputValue: aws.String("tg-http-arn")},
+							},
+						},
+					},
+				}, nil),
+			},
+			want: &Stack{
+				Name:    "managed-stack",
+				DNSName: "example.com",
+				CertificateARNs: map[string]time.Time{
+					"cert-arn": time.Time{},
+				},
+				TargetGroupARNs: []string{"tg-arn", "tg-http-arn"},
 				tags: map[string]string{
 					kubernetesCreatorTag:                 DefaultControllerID,
 					clusterIDTagPrefix + "test-cluster":  resourceLifecycleOwned,