Repository: zalando-incubator/kube-ingress-aws-controller (go)

Issue:
Is there a way to migrate from 0.11.14 through 0.12.16 to 0.12.17+ without downtime? Target group creating empty at first
Hi, we have been using this ingress controller for years, and are currently on **0.11.14**. We have been testing upgrade to **0.12.16** which work flawlessly and, while the Cloudformation stack is updated for some reason, the actual resources don't change; there's no downtime.

If we try going to **0.12.17** however (not using the new experimental CNI), the controller comes up, updates our CF stacks, and ends up creating and attaching a new TG, while deleting the old one. The problem? This new TG has no instances in it for 30 seconds, at which point it does. This would incur a 30 seconds downtime across all of production if we went with this, not sure if we can get away with that.

Are there any flags or workarounds to prevent this? Any idea what 0.12.17 is doing differently that it needs to recreate the TG entirely, and empty at first no-less?

Screenshot of it happening right after the CF update:

![image](https://user-images.githubusercontent.com/69006307/168136708-edbc919e-59d8-47f8-bc47-a75131ebe564.png)

Logs of controller as it comes up on new 0.12.17 image (from 0.11.14)



```
time="2022-05-12T17:38:11Z" level=info msg="starting /kube-ingress-aws-controller v0.12.17"
--
Thu, May 12 2022 1:40:17 pm | time="2022-05-12T17:40:17Z" level=info msg="Kubernetes API server: "
Thu, May 12 2022 1:40:17 pm | time="2022-05-12T17:40:17Z" level=info msg="Cluster ID: jason-dev-9999999-us-east-2"
Thu, May 12 2022 1:40:17 pm | time="2022-05-12T17:40:17Z" level=info msg="VPC ID: vpc-9999999999"
Thu, May 12 2022 1:40:17 pm | time="2022-05-12T17:40:17Z" level=info msg="Instance ID: i-05ded1d34e4817610"
Thu, May 12 2022 1:40:17 pm | time="2022-05-12T17:40:17Z" level=info msg="Security group ID: sg-018f425e420f2db38"
Thu, May 12 2022 1:40:17 pm | time="2022-05-12T17:40:17Z" level=info msg="Internal subnet IDs: [subnet-046134d61fe120cbc subnet-09fa2d9414fe47482 subnet-0f876b2913eb84f4b]"
Thu, May 12 2022 1:40:17 pm | time="2022-05-12T17:40:17Z" level=info msg="Public subnet IDs: [subnet-06f1a762865b7ad3c subnet-086df96fe15ae9076 subnet-013c830b5b448157e]"
Thu, May 12 2022 1:40:17 pm | time="2022-05-12T17:40:17Z" level=info msg="EC2 filters: tag:kubernetes.io/cluster/jason-dev-418023852230-us-east-2=owned tag-key=k8s.io/role/node"
Thu, May 12 2022 1:40:17 pm | time="2022-05-12T17:40:17Z" level=info msg="Certificates per ALB: 24 (SNI: true)"
Thu, May 12 2022 1:40:17 pm | time="2022-05-12T17:40:17Z" level=info msg="Blacklisted Certificate ARNs (0): "
Thu, May 12 2022 1:40:17 pm | time="2022-05-12T17:40:17Z" level=info msg="Ingress class filters: alb-istio"
Thu, May 12 2022 1:40:17 pm | time="2022-05-12T17:40:17Z" level=info msg="ALB Logging S3 Bucket: "
Thu, May 12 2022 1:40:17 pm | time="2022-05-12T17:40:17Z" level=info msg="ALB Logging S3 Prefix: "
Thu, May 12 2022 1:40:17 pm | time="2022-05-12T17:40:17Z" level=info msg="CloudWatch Alarm ConfigMap: "
Thu, May 12 2022 1:40:17 pm | time="2022-05-12T17:40:17Z" level=info msg="Default LoadBalancer type: application"
Thu, May 12 2022 1:40:17 pm | time="2022-05-12T17:40:17Z" level=info msg="Target access mode: HostPort"
Thu, May 12 2022 1:40:17 pm | time="2022-05-12T17:40:17Z" level=warning msg="Disabling RouteGroup support because listing RouteGroups failed: resource not found, to get more information https://opensource.zalando.com/skipper/kubernetes/routegroups/#routegroups"
Thu, May 12 2022 1:40:17 pm | time="2022-05-12T17:40:17Z" level=info msg="Found 3 owned auto scaling group(s)"
Thu, May 12 2022 1:40:17 pm | time="2022-05-12T17:40:17Z" level=info msg="Found 3 targeted auto scaling group(s)"
Thu, May 12 2022 1:40:17 pm | time="2022-05-12T17:40:17Z" level=info msg="Found 0 single instance(s)"
Thu, May 12 2022 1:40:17 pm | time="2022-05-12T17:40:17Z" level=info msg="Found 0 EC2 instance(s)"
Thu, May 12 2022 1:40:17 pm | time="2022-05-12T17:40:17Z" level=info msg="Found 16 certificate(s)"
Thu, May 12 2022 1:40:17 pm | time="2022-05-12T17:40:17Z" level=info msg="Found 0 cloudwatch alarm configuration(s)"
Thu, May 12 2022 1:40:21 pm | time="2022-05-12T17:40:21Z" level=info msg="Updating \"internet-facing\" stack for 1 certificates / 1 ingresses"
Thu, May 12 2022 1:40:21 pm | time="2022-05-12T17:40:21Z" level=info msg="Stack \"arn:aws:cloudformation:us-east-2:999999999:stack/kube-ingress-aws-controller-jason-dev-99999999-us-east-2-a173ffb1-df89-4dfc-86ef-d3e3c8495134/d0bb7440-d208-11ec-86f2-028a53c58cfe\" for certificate map[\"arn:aws:acm:us-east-2:99999999:certificate/eeb15f56-192c-41e5-be87-f1edfb9d9c57\":\"0001-01-01 00:00:00 +0000 UTC\"] updated"
Thu, May 12 2022 1:40:21 pm | time="2022-05-12T17:40:21Z" level=info msg="Updating \"internet-facing\" stack for 1 certificates / 1 ingresses"
Thu, May 12 2022 1:40:21 pm | time="2022-05-12T17:40:21Z" level=info msg="Stack \"arn:aws:cloudformation:us-east-2:999999999:stack/kube-ingress-aws-controller-jason-dev-999999999-us-east-2-c8b74d37-e541-42da-805e-026509a41812/11bd7aa0-b1da-11ec-85d9-062a1dd6a318\" for certificate map[\"arn:aws:acm:us-east-2:99999999:certificate/eeb15f56-192c-41e5-be87-f1edfb9d9c57\":\"0001-01-01 00:00:00 +0000 UTC\"] updated"
Thu, May 12 2022 1:40:51 pm | time="2022-05-12T17:40:51Z" level=info msg="Found 3 owned auto scaling group(s)"
Thu, May 12 2022 1:40:51 pm | time="2022-05-12T17:40:51Z" level=info msg="Found 3 targeted auto scaling group(s)"
Thu, May 12 2022 1:40:51 pm | time="2022-05-12T17:40:51Z" level=info msg="Found 0 single instance(s)"
Thu, May 12 2022 1:40:51 pm | time="2022-05-12T17:40:51Z" level=info msg="Found 0 EC2 instance(s)"
Thu, May 12 2022 1:40:51 pm | time="2022-05-12T17:40:51Z" level=info msg="Found 16 certificate(s)"
Thu, May 12 2022 1:40:51 pm | time="2022-05-12T17:40:51Z" level=info msg="Found 0 cloudwatch alarm configuration(s)"


```

Pull request:
Add `Legacy` `target-access-mode` to enable upgrade from pre `0.12.17` version
Before version `0.12.17` loadbalancer target group target type was not
specified and defaulted to `instance` in CloudFormation, see
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticloadbalancingv2-targetgroup.html#cfn-elasticloadbalancingv2-targetgroup-targettype

PR #461 introduced AWS CNI mode and configured target group target type
either to `ip` or `instance`.

Changing target type from unset to `instance` in Cloudformation triggers target
group re-creation which makes it impossible to upgrade from pre `0.12.17` without downtime.

This change:
- makes `target-access-mode` flag required to force users to choose proper value
- introduces a new `Legacy` option that does not set target type and thus enables upgrade from pre `0.12.17`.

Fixes #507

Signed-off-by: Alexander Yastrebov <alexander.yastrebov@zalando.de>

Patch:
diff --git a/README.md b/README.md
index bc8b8ca..4858465 100644
--- a/README.md
+++ b/README.md
@@ -36,18 +36,33 @@ This information is used to manage AWS resources for each ingress objects of the
 
 ## Upgrade
 
+### <v0.14.0 to >=v0.14.0
+
+Version `v0.14.0` makes `target-access-mode` flag required to make upgrading users aware of the [issue](https://github.com/zalando-incubator/kube-ingress-aws-controller/issues/507).
+
+New deployment of the controller should use `--target-access-mode=HostPort` or `--target-access-mode=AWSCNI`.
+
+To upgrade from `<v0.12.17` use `--target-access-mode=Legacy` - it is the same as `HostPort` but does not set target type and
+relies on CloudFormation to use `instance` as a default value.
+
+Note that changing later from `--target-access-mode=Legacy` will change target type in CloudFormation and trigger target group recreation and downtime.
+
+To upgrade from `>=v0.12.17` when `--target-access-mode` is not set use explicit `--target-access-mode=HostPort`.
+
 ### <v0.13.0 to >=0.13.0
 
 Version `v0.13.0` use Ingress version v1 as default. You can downgrade
 ingress version to earlier versions via flag. You will also need to
 allow the access via RBAC, see more information in [<v0.11.0 to >=0.11.0](#v0110-to-0110) below.
 
-### <v0.12.17 to >=v0.12.17
+### <v0.12.17 to <v0.14.0
 
 Please see [release note](https://github.com/zalando-incubator/kube-ingress-aws-controller/releases/tag/v0.12.17)
 and [issue](https://github.com/zalando-incubator/kube-ingress-aws-controller/issues/507)
 this update can cause 30s downtime, if you don't use AWS CNI mode.
 
+Please upgrade to `>=v0.14.0`.
+
 ### <v0.12.0 to <=0.12.16
 
 Version `v0.12.0` changes Network Load Balancer type handling if Application Load Balancer type feature is requested. See [Load Balancers types](#load-balancers-types) notes for details.
@@ -644,25 +659,30 @@ Those ports are now configured individually. If you relied on this behavior, ple
 
 ## AWS CNI Mode (experimental)
 
-The default operation mode of the controller (`target-access-mode=HostPort`) is to link the target groups to the autoscaling group. The target group type is `instance`, requiring the ingress pod to be accessible through a `HostNetwork` and `HostPort`.
+The common operation mode of the controller (`--target-access-mode=HostPort`) is to link the target groups to the autoscaling group.
+The target group type is `instance`, requiring the ingress pod to be accessible through a `HostNetwork` and `HostPort`.
 
-In *AWS CNI Mode* (`target-access-mode=AWSCNI`) the controller actively manages the target group members. Since AWS EKS cluster running AWS VPC CNI have their pods as first class members in the VPCs, they can receive the traffic directly, being managed through a target group type is `ip`, which means there is no necessity for the HostPort indirection.
+In *AWS CNI Mode* (`--target-access-mode=AWSCNI`) the controller actively manages the target group members.
+Since AWS EKS cluster running AWS VPC CNI have their pods as first class members in the VPCs, they can receive the traffic directly,
+being managed through a target group type is `ip`, which means there is no necessity for the HostPort indirection.
 
 ### Notes
 
 - For security reasons the HostPort requirement might be of concern
-- Direct management of the target group members is significantly faster compared to the AWS linked mode, but it requires a running controller for updates. As of now, the controller is not prepared for high availability replicated setup.
-- The registration and deregistration is synced with the pod lifecycle, hence a pod in terminating phase is deregistered from the target group before shut down.
+- Direct management of the target group members is significantly faster compared to the AWS linked mode, but it requires
+  a running controller for updates. As of now, the controller is not prepared for high availability replicated setup.
+- The registration and deregistration is synced with the pod lifecycle, hence a pod in terminating phase is deregistered
+  from the target group before shut down.
 - Ingress pods are not bound to nodes in CNI mode and the deployment can scale independently.
 
 ### Configuration options
 
-| access mode | HostNetwork | HostPort |                      Notes                      |
-| :---------: | :---------: | :------: | :---------------------------------------------: |
-| `HostPort`  |   `true`    |  `true`  | default setup                                   |
-| `AWSCNI`    |   `true`    |  `true`  | PodIP == HostIP: limited scaling and host bound |
-| `AWSCNI`    |   `false`   |  `true`  | PodIP != HostIP: limited scaling and host bound |
-| `AWSCNI`    |   `false`   | `false`  | free scaling, pod VPC CNI IP used               |
+| access mode | HostNetwork | HostPort |                      Notes                             |
+| :---------: | :---------: | :------: | :----------------------------------------------------: |
+| `HostPort`  |   `true`    |  `true`  | target group updated by ASG, see v0.14.0 release notes |
+| `AWSCNI`    |   `true`    |  `true`  | PodIP == HostIP: limited scaling and host bound        |
+| `AWSCNI`    |   `false`   |  `true`  | PodIP != HostIP: limited scaling and host bound        |
+| `AWSCNI`    |   `false`   | `false`  | free scaling, pod VPC CNI IP used                      |
 
 ## Trying it out
 
diff --git a/VERSION b/VERSION
index 3c58828..1ec829a 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-v0.13
+v0.14
diff --git a/aws/adapter.go b/aws/adapter.go
index 85e89dd..5f6d258 100644
--- a/aws/adapter.go
+++ b/aws/adapter.go
@@ -46,6 +46,7 @@ type Adapter struct {
 	albHealthyThresholdCount    uint
 	albUnhealthyThresholdCount  uint
 	nlbHealthyThresholdCount    uint
+	targetType                  string
 	targetPort                  uint
 	albHTTPTargetPort           uint
 	nlbHTTPTargetPort           uint
@@ -133,8 +134,10 @@ const (
 	LoadBalancerTypeNetwork     = "network"
 	IPAddressTypeIPV4           = "ipv4"
 	IPAddressTypeDualstack      = "dualstack"
-	TargetAccessModeAWSCNI      = "AWSCNI"
-	TargetAccessModeHostPort    = "HostPort"
+
+	TargetAccessModeAWSCNI   = "AWSCNI"
+	TargetAccessModeHostPort = "HostPort"
+	TargetAccessModeLegacy   = "Legacy"
 )
 
 var (
@@ -443,8 +446,17 @@ func (a *Adapter) WithInternalDomains(domains []string) *Adapter {
 }
 
 // WithTargetAccessMode returns the receiver adapter after defining the target access mode
-func (a *Adapter) WithTargetAccessMode(t string) *Adapter {
-	a.TargetCNI.Enabled = t == TargetAccessModeAWSCNI
+func (a *Adapter) WithTargetAccessMode(mode string) *Adapter {
+	a.TargetCNI.Enabled = mode == TargetAccessModeAWSCNI
+
+	switch mode {
+	case TargetAccessModeHostPort:
+		a.targetType = elbv2.TargetTypeEnumInstance
+	case TargetAccessModeAWSCNI:
+		a.targetType = elbv2.TargetTypeEnumIp
+	case TargetAccessModeLegacy:
+		a.targetType = ""
+	}
 	return a
 }
 
@@ -669,6 +681,7 @@ func (a *Adapter) CreateStack(certificateARNs []string, scheme, securityGroup, o
 		albHealthyThresholdCount:          a.albHealthyThresholdCount,
 		albUnhealthyThresholdCount:        a.albUnhealthyThresholdCount,
 		nlbHealthyThresholdCount:          a.nlbHealthyThresholdCount,
+		targetType:                        a.targetType,
 		targetPort:                        a.targetPort,
 		targetHTTPS:                       a.targetHTTPS,
 		httpDisabled:                      a.httpDisabled(loadBalancerType),
@@ -690,7 +703,6 @@ func (a *Adapter) CreateStack(certificateARNs []string, scheme, securityGroup, o
 		http2:                             http2,
 		tags:                              a.stackTags,
 		internalDomains:                   a.internalDomains,
-		targetAccessModeCNI:               a.TargetCNI.Enabled,
 		denyInternalDomains:               a.denyInternalDomains,
 		denyInternalDomainsResponse: denyResp{
 			body:        a.denyInternalRespBody,
@@ -725,6 +737,7 @@ func (a *Adapter) UpdateStack(stackName string, certificateARNs map[string]time.
 		albHealthyThresholdCount:          a.albHealthyThresholdCount,
 		albUnhealthyThresholdCount:        a.albUnhealthyThresholdCount,
 		nlbHealthyThresholdCount:          a.nlbHealthyThresholdCount,
+		targetType:                        a.targetType,
 		targetPort:                        a.targetPort,
 		targetHTTPS:                       a.targetHTTPS,
 		httpDisabled:                      a.httpDisabled(loadBalancerType),
@@ -746,7 +759,6 @@ func (a *Adapter) UpdateStack(stackName string, certificateARNs map[string]time.
 		http2:                             http2,
 		tags:                              a.stackTags,
 		internalDomains:                   a.internalDomains,
-		targetAccessModeCNI:               a.TargetCNI.Enabled,
 		denyInternalDomains:               a.denyInternalDomains,
 		denyInternalDomainsResponse: denyResp{
 			body:        a.denyInternalRespBody,
diff --git a/aws/cf.go b/aws/cf.go
index 00a33ff..3dec03d 100644
--- a/aws/cf.go
+++ b/aws/cf.go
@@ -163,6 +163,7 @@ type stackSpec struct {
 	albHealthyThresholdCount          uint
 	albUnhealthyThresholdCount        uint
 	nlbHealthyThresholdCount          uint
+	targetType                        string
 	targetPort                        uint
 	targetHTTPS                       bool
 	httpDisabled                      bool
@@ -186,7 +187,6 @@ type stackSpec struct {
 	denyInternalDomainsResponse       denyResp
 	internalDomains                   []string
 	tags                              map[string]string
-	targetAccessModeCNI               bool
 }
 
 type healthCheck struct {
diff --git a/aws/cf_template.go b/aws/cf_template.go
index 75a2b7b..d147ec2 100644
--- a/aws/cf_template.go
+++ b/aws/cf_template.go
@@ -8,7 +8,6 @@ import (
 	"crypto/sha256"
 	"sort"
 
-	"github.com/aws/aws-sdk-go/service/elbv2"
 	cloudformation "github.com/mweagle/go-cloudformation"
 )
 
@@ -445,10 +444,11 @@ func generateDenyInternalTrafficRule(listenerName string, rulePriority int64, in
 }
 
 func newTargetGroup(spec *stackSpec, targetPortParameter string) *cloudformation.ElasticLoadBalancingV2TargetGroup {
-	targetType := elbv2.TargetTypeEnumInstance
-	if spec.targetAccessModeCNI {
-		targetType = elbv2.TargetTypeEnumIp
+	var targetType *cloudformation.StringExpr
+	if spec.targetType != "" {
+		targetType = cloudformation.String(spec.targetType)
 	}
+
 	protocol := "HTTP"
 	healthCheckProtocol := "HTTP"
 	healthyThresholdCount, unhealthyThresholdCount := spec.albHealthyThresholdCount, spec.albUnhealthyThresholdCount
@@ -477,7 +477,7 @@ func newTargetGroup(spec *stackSpec, targetPortParameter string) *cloudformation
 		UnhealthyThresholdCount:    cloudformation.Integer(int64(unhealthyThresholdCount)),
 		Port:                       cloudformation.Ref(targetPortParameter).Integer(),
 		Protocol:                   cloudformation.String(protocol),
-		TargetType:                 cloudformation.String(targetType),
+		TargetType:                 targetType,
 		VPCID:                      cloudformation.Ref(parameterTargetGroupVPCIDParameter).String(),
 	}
 
diff --git a/controller.go b/controller.go
index 33f488a..4ef133b 100644
--- a/controller.go
+++ b/controller.go
@@ -187,8 +187,12 @@ func loadSettings() error {
 		Default("text/plain").StringVar(&denyInternalRespContentType)
 	kingpin.Flag("deny-internal-domains-response-status-code", "Defines the response status code for a request identified as to an internal domain when -deny-internal-domains is set.").
 		Default("401").IntVar(&denyInternalRespStatusCode)
-	kingpin.Flag("target-access-mode", "Target group accessing Ingress via HostPort or AWS VPC CNI. Set to ASG for HostPort access or CNI for pod direct IP access.").
-		Default(aws.TargetAccessModeHostPort).EnumVar(&targetAccessMode, aws.TargetAccessModeHostPort, aws.TargetAccessModeAWSCNI)
+	kingpin.Flag("target-access-mode", "Defines target type of the target groups in CloudFormation and how loadbalancer targets are discovered. "+
+		"HostPort sets target type to 'instance' and discovers EC2 instances using AWS API and instance filters. "+
+		"AWSCNI sets target type to 'ip' and discovers target IPs using Kubernetes API and Pod label selector. "+
+		"Legacy is the same as HostPort but does not set target type and relies on CloudFormation to use 'instance' as a default value. "+
+		"Changing value from 'Legacy' to 'HostPort' will change target type in CloudFormation and trigger target group recreation and downtime.").
+		Required().EnumVar(&targetAccessMode, aws.TargetAccessModeHostPort, aws.TargetAccessModeAWSCNI, aws.TargetAccessModeLegacy)
 	kingpin.Flag("target-cni-namespace", "AWS VPC CNI only. Defines the namespace for ingress pods that should be linked to target group.").StringVar(&targetCNINamespace)
 	// LabelSelector semantics https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors
 	kingpin.Flag("target-cni-pod-labelselector", "AWS VPC CNI only. Defines the labelselector for ingress pods that should be linked to target group. Supports simple equality and multi value form (a=x,b=y) as well as complex forms (a IN (x,y,z).").StringVar(&targetCNIPodLabelSelector)


Test patch:
diff --git a/aws/adapter_test.go b/aws/adapter_test.go
index e095b4c..5cff74f 100644
--- a/aws/adapter_test.go
+++ b/aws/adapter_test.go
@@ -9,6 +9,8 @@ import (
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/service/ec2"
 	"github.com/aws/aws-sdk-go/service/elbv2"
+
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
@@ -1011,14 +1013,25 @@ func TestAdapter_SetTargetsOnCNITargetGroups(t *testing.T) {
 }
 
 func TestWithTargetAccessMode(t *testing.T) {
-	t.Run("WithTargetAccessMode enables AWS CNI mode", func(t *testing.T) {
+	t.Run("WithTargetAccessMode AWSCNI", func(t *testing.T) {
 		a := &Adapter{TargetCNI: &TargetCNIconfig{Enabled: false}}
 		a = a.WithTargetAccessMode("AWSCNI")
-		require.True(t, a.TargetCNI.Enabled)
+
+		assert.Equal(t, elbv2.TargetTypeEnumIp, a.targetType)
+		assert.True(t, a.TargetCNI.Enabled)
 	})
-	t.Run("WithTargetAccessMode disables AWS CNI mode", func(t *testing.T) {
+	t.Run("WithTargetAccessMode HostPort", func(t *testing.T) {
 		a := &Adapter{TargetCNI: &TargetCNIconfig{Enabled: true}}
 		a = a.WithTargetAccessMode("HostPort")
-		require.False(t, a.TargetCNI.Enabled)
+
+		assert.Equal(t, elbv2.TargetTypeEnumInstance, a.targetType)
+		assert.False(t, a.TargetCNI.Enabled)
+	})
+	t.Run("WithTargetAccessMode Legacy", func(t *testing.T) {
+		a := &Adapter{TargetCNI: &TargetCNIconfig{Enabled: true}}
+		a = a.WithTargetAccessMode("Legacy")
+
+		assert.Equal(t, "", a.targetType)
+		assert.False(t, a.TargetCNI.Enabled)
 	})
 }
diff --git a/aws/cf_template_test.go b/aws/cf_template_test.go
index 6d67cee..706b32d 100644
--- a/aws/cf_template_test.go
+++ b/aws/cf_template_test.go
@@ -615,28 +615,42 @@ func TestGenerateTemplate(t *testing.T) {
 			},
 		},
 		{
-			name: "Default TG type is Instance",
+			name: "Default TG type is not set",
 			spec: &stackSpec{
 				loadbalancerType: LoadBalancerTypeApplication,
+				targetType:       "",
 			},
 			validate: func(t *testing.T, template *cloudformation.Template) {
 				require.NotNil(t, template.Resources, "TG")
-				tg, ok := template.Resources["TG"].Properties.(*cloudformation.ElasticLoadBalancingV2TargetGroup)
-				require.True(t, ok, "couldn't convert resource to ElasticLoadBalancingV2TargetGroup")
-				require.Equal(t, cloudformation.String("instance"), tg.TargetType)
+				tg := template.Resources["TG"].Properties.(*cloudformation.ElasticLoadBalancingV2TargetGroup)
+
+				assert.Nil(t, tg.TargetType)
 			},
 		},
 		{
-			name: "TG type is IP in in mode AWS CNI",
+			name: "Sets 'instance' TG type",
 			spec: &stackSpec{
-				loadbalancerType:    LoadBalancerTypeApplication,
-				targetAccessModeCNI: true,
+				loadbalancerType: LoadBalancerTypeApplication,
+				targetType:       "instance",
 			},
 			validate: func(t *testing.T, template *cloudformation.Template) {
 				require.NotNil(t, template.Resources, "TG")
-				tg, ok := template.Resources["TG"].Properties.(*cloudformation.ElasticLoadBalancingV2TargetGroup)
-				require.True(t, ok, "couldn't convert resource to ElasticLoadBalancingV2TargetGroup")
-				require.Equal(t, cloudformation.String("ip"), tg.TargetType)
+				tg := template.Resources["TG"].Properties.(*cloudformation.ElasticLoadBalancingV2TargetGroup)
+
+				assert.Equal(t, cloudformation.String("instance"), tg.TargetType)
+			},
+		},
+		{
+			name: "Sets 'ip' TG type",
+			spec: &stackSpec{
+				loadbalancerType: LoadBalancerTypeApplication,
+				targetType:       "ip",
+			},
+			validate: func(t *testing.T, template *cloudformation.Template) {
+				require.NotNil(t, template.Resources, "TG")
+				tg := template.Resources["TG"].Properties.(*cloudformation.ElasticLoadBalancingV2TargetGroup)
+
+				assert.Equal(t, cloudformation.String("ip"), tg.TargetType)
 			},
 		},
 	} {