Repository: kubernetes-sigs/aws-alb-ingress-controller
Pull request #1518 (2020-10-19 03:54:22)

multiple bug fixes for v2.0.0-rc4
1. Tune the targetGroupBinding reconcile Requeue behavior
    * requeue with a fixed delay only when there are new targets registered or initial targets.
    * requeue with a backoff delay otherwise.
    * fixed delay is changed to 15 second to match TargetGroup's default healthCheckInterval
  
   Above requeue strategy will prevent the controller to send too many DescribeTargetHealth API call when targetGroup is not used or securityGroup rules is not setup correctly.

2. Adjust the resource synthesize order by make TargetGroupBinding  synthesize happens after listenerRule, such that when targets are been registered, it's already associated with a LoadBalancer.

3. Use OptimisticLock via Object's resourceVersion to prevent race condition when updating collection fields.

4. support both `clientID` and `clientId`(old one) for OIDC secret.

5.  Fix the log level settings. (ctrl.setLogger can only be set once)

k8s-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/aws-alb-ingress-controller/pull/1518#" title="Author self-approved">M00nF1sh</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign  after the PR has been reviewed.
You can assign the PR to them by writing `/assign ` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Faws-alb-ingress-controller).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubernetes-sigs/aws-alb-ingress-controller/blob/v2_ga/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/aws-alb-ingress-controller/pull/1518#pullrequestreview-511526409" title="Approved">kishorj</a>*, *<a href="https://github.com/kubernetes-sigs/aws-alb-ingress-controller/pull/1518#" title="Author self-approved">M00nF1sh</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign  after the PR has been reviewed.
You can assign the PR to them by writing `/assign ` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Faws-alb-ingress-controller).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubernetes-sigs/aws-alb-ingress-controller/blob/v2_ga/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->

Patch:
diff --git a/docs/guide/ingress/annotations.md b/docs/guide/ingress/annotations.md
index 611ae9a14..8233561af 100644
--- a/docs/guide/ingress/annotations.md
+++ b/docs/guide/ingress/annotations.md
@@ -419,7 +419,7 @@ ALB supports authentication with Cognito or OIDC. See [Authenticate Users Using
           namespace: testcase
           name: my-k8s-secret
         data:
-          clientId: base64 of your plain text clientId
+          clientID: base64 of your plain text clientId
           clientSecret: base64 of your plain text clientSecret
         ```
 
diff --git a/main.go b/main.go
index 590640456..b56878b75 100644
--- a/main.go
+++ b/main.go
@@ -18,6 +18,7 @@ package main
 
 import (
 	"context"
+	"github.com/go-logr/logr"
 	"github.com/spf13/pflag"
 	zapraw "go.uber.org/zap"
 	k8sruntime "k8s.io/apimachinery/pkg/runtime"
@@ -58,18 +59,18 @@ func init() {
 }
 
 func main() {
-	setLogLevel("info")
-	setupLog.Info("version",
+	infoLogger := getLoggerWithLogLevel("info")
+	infoLogger.Info("version",
 		"GitVersion", version.GitVersion,
 		"GitCommit", version.GitCommit,
 		"BuildDate", version.BuildDate,
 	)
 	controllerCFG, err := loadControllerConfig()
 	if err != nil {
-		setupLog.Error(err, "unable to load controller config")
+		infoLogger.Error(err, "unable to load controller config")
 		os.Exit(1)
 	}
-	setLogLevel(controllerCFG.LogLevel)
+	ctrl.SetLogger(getLoggerWithLogLevel(controllerCFG.LogLevel))
 
 	cloud, err := aws.NewCloud(controllerCFG.AWSConfig, metrics.Registry)
 	if err != nil {
@@ -153,8 +154,8 @@ func loadControllerConfig() (config.ControllerConfig, error) {
 	return controllerCFG, nil
 }
 
-// setLogLevel sets the log level of controller.
-func setLogLevel(logLevel string) {
+// getLoggerWithLogLevel returns logger with specific log level.
+func getLoggerWithLogLevel(logLevel string) logr.Logger {
 	var zapLevel zapraw.AtomicLevel
 	switch logLevel {
 	case "info":
@@ -168,5 +169,5 @@ func setLogLevel(logLevel string) {
 	logger := zap.New(zap.UseDevMode(false),
 		zap.Level(zapLevel),
 		zap.StacktraceLevel(zapraw.NewAtomicLevelAt(zapraw.FatalLevel)))
-	ctrl.SetLogger(runtime.NewConciseLogger(logger))
+	return runtime.NewConciseLogger(logger)
 }
diff --git a/pkg/deploy/elbv2/target_group_binding_synthesizer.go b/pkg/deploy/elbv2/target_group_binding_synthesizer.go
index 2dbe0fa28..beffa5c49 100644
--- a/pkg/deploy/elbv2/target_group_binding_synthesizer.go
+++ b/pkg/deploy/elbv2/target_group_binding_synthesizer.go
@@ -19,6 +19,8 @@ func NewTargetGroupBindingSynthesizer(k8sClient client.Client, trackingProvider
 		tgbManager:       tgbManager,
 		logger:           logger,
 		stack:            stack,
+
+		unmatchedK8sTGBs: nil,
 	}
 }
 
@@ -29,6 +31,8 @@ type targetGroupBindingSynthesizer struct {
 	tgbManager       TargetGroupBindingManager
 	logger           logr.Logger
 	stack            core.Stack
+
+	unmatchedK8sTGBs []*elbv2api.TargetGroupBinding
 }
 
 func (s *targetGroupBindingSynthesizer) Synthesize(ctx context.Context) error {
@@ -43,11 +47,8 @@ func (s *targetGroupBindingSynthesizer) Synthesize(ctx context.Context) error {
 	if err != nil {
 		return err
 	}
-	for _, k8sTGB := range unmatchedK8sTGBs {
-		if err := s.tgbManager.Delete(ctx, k8sTGB); err != nil {
-			return err
-		}
-	}
+	s.unmatchedK8sTGBs = unmatchedK8sTGBs
+
 	for _, resTGB := range unmatchedResTGBs {
 		tgbStatus, err := s.tgbManager.Create(ctx, resTGB)
 		if err != nil {
@@ -66,7 +67,11 @@ func (s *targetGroupBindingSynthesizer) Synthesize(ctx context.Context) error {
 }
 
 func (s *targetGroupBindingSynthesizer) PostSynthesize(ctx context.Context) error {
-	// nothing to do here.
+	for _, k8sTGB := range s.unmatchedK8sTGBs {
+		if err := s.tgbManager.Delete(ctx, k8sTGB); err != nil {
+			return err
+		}
+	}
 	return nil
 }
 
@@ -93,7 +98,7 @@ type resAndK8sTargetGroupBindingPair struct {
 func matchResAndK8sTargetGroupBindings(resTGBs []*elbv2model.TargetGroupBindingResource, k8sTGBs []*elbv2api.TargetGroupBinding) ([]resAndK8sTargetGroupBindingPair, []*elbv2model.TargetGroupBindingResource, []*elbv2api.TargetGroupBinding, error) {
 	var matchedResAndK8sTGBs []resAndK8sTargetGroupBindingPair
 	var unmatchedResTGBs []*elbv2model.TargetGroupBindingResource
-	var unmatchedK8sTGs []*elbv2api.TargetGroupBinding
+	var unmatchedK8sTGBs []*elbv2api.TargetGroupBinding
 	resTGBsByARN, err := mapResTargetGroupBindingByARN(resTGBs)
 	if err != nil {
 		return nil, nil, nil, err
@@ -115,10 +120,10 @@ func matchResAndK8sTargetGroupBindings(resTGBs []*elbv2model.TargetGroupBindingR
 		unmatchedResTGBs = append(unmatchedResTGBs, resTGBsByARN[tgARN])
 	}
 	for _, tgARN := range k8sTGBARNs.Difference(resTGBARNs).List() {
-		unmatchedK8sTGs = append(unmatchedK8sTGs, k8sTGBsByARN[tgARN])
+		unmatchedK8sTGBs = append(unmatchedK8sTGBs, k8sTGBsByARN[tgARN])
 	}
 
-	return matchedResAndK8sTGBs, unmatchedResTGBs, unmatchedK8sTGs, nil
+	return matchedResAndK8sTGBs, unmatchedResTGBs, unmatchedK8sTGBs, nil
 }
 
 func mapResTargetGroupBindingByARN(resTGBs []*elbv2model.TargetGroupBindingResource) (map[string]*elbv2model.TargetGroupBindingResource, error) {
diff --git a/pkg/deploy/stack_deployer.go b/pkg/deploy/stack_deployer.go
index 7d359c314..e31280647 100644
--- a/pkg/deploy/stack_deployer.go
+++ b/pkg/deploy/stack_deployer.go
@@ -86,10 +86,10 @@ func (d *defaultStackDeployer) Deploy(ctx context.Context, stack core.Stack) err
 	synthesizers := []ResourceSynthesizer{
 		ec2.NewSecurityGroupSynthesizer(d.cloud.EC2(), d.trackingProvider, d.ec2TaggingManager, d.ec2SGManager, d.vpcID, d.logger, stack),
 		elbv2.NewTargetGroupSynthesizer(d.cloud.ELBV2(), d.trackingProvider, d.elbv2TaggingManager, d.elbv2TGManager, d.logger, stack),
-		elbv2.NewTargetGroupBindingSynthesizer(d.k8sClient, d.trackingProvider, d.elbv2TGBManager, d.logger, stack),
 		elbv2.NewLoadBalancerSynthesizer(d.cloud.ELBV2(), d.trackingProvider, d.elbv2TaggingManager, d.elbv2LBManager, d.logger, stack),
 		elbv2.NewListenerSynthesizer(d.cloud.ELBV2(), d.elbv2LSManager, d.logger, stack),
 		elbv2.NewListenerRuleSynthesizer(d.cloud.ELBV2(), d.elbv2LRManager, d.logger, stack),
+		elbv2.NewTargetGroupBindingSynthesizer(d.k8sClient, d.trackingProvider, d.elbv2TGBManager, d.logger, stack),
 	}
 
 	if d.addonsConfig.WAFV2Enabled {
diff --git a/pkg/ingress/model_build_actions.go b/pkg/ingress/model_build_actions.go
index 3827d79f2..3daaa44ee 100644
--- a/pkg/ingress/model_build_actions.go
+++ b/pkg/ingress/model_build_actions.go
@@ -199,8 +199,21 @@ func (t *defaultModelBuildTask) buildAuthenticateOIDCAction(ctx context.Context,
 		return elbv2model.Action{}, err
 	}
 
-	clientID := strings.TrimRightFunc(string(secret.Data["clientId"]), unicode.IsSpace)
-	clientSecret := string(secret.Data["clientSecret"])
+	rawClientID, ok := secret.Data["clientID"]
+	// AWSALBIngressController looks for clientId, we should be backwards-compatible here.
+	if !ok {
+		rawClientID, ok = secret.Data["clientId"]
+	}
+	if !ok {
+		return elbv2model.Action{}, errors.Errorf("missing clientID, secret: %v", secretKey)
+	}
+	rawClientSecret, ok := secret.Data["clientSecret"]
+	if !ok {
+		return elbv2model.Action{}, errors.Errorf("missing clientSecret, secret: %v", secretKey)
+	}
+
+	clientID := strings.TrimRightFunc(string(rawClientID), unicode.IsSpace)
+	clientSecret := string(rawClientSecret)
 	return elbv2model.Action{
 		Type: elbv2model.ActionTypeAuthenticateOIDC,
 		AuthenticateOIDCConfig: &elbv2model.AuthenticateOIDCActionConfig{
diff --git a/pkg/k8s/finalizer.go b/pkg/k8s/finalizer.go
index dc85313ba..311195770 100644
--- a/pkg/k8s/finalizer.go
+++ b/pkg/k8s/finalizer.go
@@ -5,6 +5,7 @@ import (
 	"github.com/go-logr/logr"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/client-go/util/retry"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 )
@@ -28,37 +29,50 @@ func NewDefaultFinalizerManager(k8sClient client.Client, log logr.Logger) Finali
 
 type defaultFinalizerManager struct {
 	k8sClient client.Client
-	log       logr.Logger
+
+	log logr.Logger
 }
 
 func (m *defaultFinalizerManager) AddFinalizers(ctx context.Context, obj APIObject, finalizers ...string) error {
-	oldObj := obj.DeepCopyObject()
-	needsUpdate := false
-	for _, finalizer := range finalizers {
-		if !HasFinalizer(obj, finalizer) {
-			controllerutil.AddFinalizer(obj, finalizer)
-			needsUpdate = true
+	return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
+		if err := m.k8sClient.Get(ctx, NamespacedName(obj), obj); err != nil {
+			return err
 		}
-	}
-	if !needsUpdate {
-		return nil
-	}
-	return m.k8sClient.Patch(ctx, obj, client.MergeFrom(oldObj))
+
+		oldObj := obj.DeepCopyObject()
+		needsUpdate := false
+		for _, finalizer := range finalizers {
+			if !HasFinalizer(obj, finalizer) {
+				controllerutil.AddFinalizer(obj, finalizer)
+				needsUpdate = true
+			}
+		}
+		if !needsUpdate {
+			return nil
+		}
+		return m.k8sClient.Patch(ctx, obj, client.MergeFromWithOptions(oldObj, client.MergeFromWithOptimisticLock{}))
+	})
 }
 
 func (m *defaultFinalizerManager) RemoveFinalizers(ctx context.Context, obj APIObject, finalizers ...string) error {
-	oldObj := obj.DeepCopyObject()
-	needsUpdate := false
-	for _, finalizer := range finalizers {
-		if HasFinalizer(obj, finalizer) {
-			controllerutil.RemoveFinalizer(obj, finalizer)
-			needsUpdate = true
+	return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
+		if err := m.k8sClient.Get(ctx, NamespacedName(obj), obj); err != nil {
+			return err
 		}
-	}
-	if !needsUpdate {
-		return nil
-	}
-	return m.k8sClient.Patch(ctx, obj, client.MergeFrom(oldObj))
+
+		oldObj := obj.DeepCopyObject()
+		needsUpdate := false
+		for _, finalizer := range finalizers {
+			if HasFinalizer(obj, finalizer) {
+				controllerutil.RemoveFinalizer(obj, finalizer)
+				needsUpdate = true
+			}
+		}
+		if !needsUpdate {
+			return nil
+		}
+		return m.k8sClient.Patch(ctx, obj, client.MergeFromWithOptions(oldObj, client.MergeFromWithOptimisticLock{}))
+	})
 }
 
 // HasFinalizer tests whether k8s object has specified finalizer
diff --git a/pkg/targetgroupbinding/resource_manager.go b/pkg/targetgroupbinding/resource_manager.go
index d077f5b61..096a62a23 100644
--- a/pkg/targetgroupbinding/resource_manager.go
+++ b/pkg/targetgroupbinding/resource_manager.go
@@ -22,7 +22,7 @@ import (
 	"time"
 )
 
-const defaultTargetHealthRequeueDuration = 10 * time.Second
+const defaultTargetHealthRequeueDuration = 15 * time.Second
 
 // ResourceManager manages the TargetGroupBinding resource.
 type ResourceManager interface {
@@ -109,12 +109,15 @@ func (m *defaultResourceManager) reconcileWithIPTargetType(ctx context.Context,
 		return err
 	}
 
-	anyPodNeedFurtherProbe, err := m.updateTargetHealthPodCondition(ctx, tgb, matchedEndpointAndTargets)
+	anyPodNeedFurtherProbe, err := m.updateTargetHealthPodCondition(ctx, tgb, matchedEndpointAndTargets, unmatchedEndpoints)
 	if err != nil {
 		return err
 	}
-	if anyPodNeedFurtherProbe || len(unmatchedEndpoints) != 0 {
-		return runtime.NewRequeueAfterError(nil, m.targetHealthRequeueDuration)
+	if anyPodNeedFurtherProbe {
+		if containsTargetsInInitialState(matchedEndpointAndTargets) || len(unmatchedEndpoints) != 0 {
+			return runtime.NewRequeueAfterError(nil, m.targetHealthRequeueDuration)
+		}
+		return runtime.NewRequeueError(nil)
 	}
 	_ = drainingTargets
 	return nil
@@ -166,13 +169,34 @@ func (m *defaultResourceManager) cleanupTargets(ctx context.Context, tgb *elbv2a
 	return nil
 }
 
-// updateTargetHealthPodCondition will updates pod's targetHealth condition for multiple pods and its matched targets.
-// returns whether further probe is needed or not.
-func (m *defaultResourceManager) updateTargetHealthPodCondition(ctx context.Context, tgb *elbv2api.TargetGroupBinding, matchedEndpointAndTargets []podEndpointAndTargetPair) (bool, error) {
+// updateTargetHealthPodCondition will updates pod's targetHealth condition for matchedEndpointAndTargets and unmatchedEndpoints.
+// returns whether further probe is needed or not
+func (m *defaultResourceManager) updateTargetHealthPodCondition(ctx context.Context, tgb *elbv2api.TargetGroupBinding,
+	matchedEndpointAndTargets []podEndpointAndTargetPair, unmatchedEndpoints []backend.PodEndpoint) (bool, error) {
+
 	targetHealthCondType := BuildTargetHealthPodConditionType(tgb)
 	anyPodNeedFurtherProbe := false
+
 	for _, endpointAndTarget := range matchedEndpointAndTargets {
-		needFurtherProbe, err := m.updateTargetHealthPodConditionForPod(ctx, endpointAndTarget.endpoint.Pod, endpointAndTarget.target, targetHealthCondType)
+		pod := endpointAndTarget.endpoint.Pod
+		targetHealth := endpointAndTarget.target.TargetHealth
+		needFurtherProbe, err := m.updateTargetHealthPodConditionForPod(ctx, pod, targetHealth, targetHealthCondType)
+		if err != nil {
+			return false, err
+		}
+		if needFurtherProbe {
+			anyPodNeedFurtherProbe = true
+		}
+	}
+
+	for _, endpoint := range unmatchedEndpoints {
+		pod := endpoint.Pod
+		targetHealth := &elbv2sdk.TargetHealth{
+			State:       awssdk.String(elbv2sdk.TargetHealthStateEnumInitial),
+			Reason:      awssdk.String(elbv2sdk.TargetHealthReasonEnumElbRegistrationInProgress),
+			Description: awssdk.String("Target registration is in progress"),
+		}
+		needFurtherProbe, err := m.updateTargetHealthPodConditionForPod(ctx, pod, targetHealth, targetHealthCondType)
 		if err != nil {
 			return false, err
 		}
@@ -185,57 +209,61 @@ func (m *defaultResourceManager) updateTargetHealthPodCondition(ctx context.Cont
 
 // updateTargetHealthPodConditionForPod updates pod's targetHealth condition for a single pod and its matched target.
 // returns whether further probe is needed or not.
-func (m *defaultResourceManager) updateTargetHealthPodConditionForPod(ctx context.Context, pod *corev1.Pod, target TargetInfo, targetHealthCondType corev1.PodConditionType) (bool, error) {
-	if !k8s.IsPodHasReadinessGate(pod, targetHealthCondType) {
-		return false, nil
-	}
-	targetHealthCondStatus := corev1.ConditionFalse
-	if target.IsHealthy() {
-		targetHealthCondStatus = corev1.ConditionTrue
-	}
-	var reason, message string
-	if target.TargetHealth != nil {
-		reason = awssdk.StringValue(target.TargetHealth.Reason)
-		message = awssdk.StringValue(target.TargetHealth.Description)
-	}
-
-	existingTargetHealthCond := k8s.GetPodCondition(pod, targetHealthCondType)
-	// we skip patch pod if it's already true, and match current computed status/reason/message.
-	if existingTargetHealthCond != nil &&
-		existingTargetHealthCond.Status == corev1.ConditionTrue &&
-		existingTargetHealthCond.Status == targetHealthCondStatus &&
-		existingTargetHealthCond.Reason == reason &&
-		existingTargetHealthCond.Message == message {
-		return false, nil
-	}
-
-	newTargetHealthCond := corev1.PodCondition{
-		Type:          targetHealthCondType,
-		Status:        targetHealthCondStatus,
-		LastProbeTime: metav1.Now(),
-		Reason:        reason,
-		Message:       message,
-	}
-	if existingTargetHealthCond == nil || existingTargetHealthCond.Status != targetHealthCondStatus {
-		newTargetHealthCond.LastTransitionTime = metav1.Now()
-	}
+func (m *defaultResourceManager) updateTargetHealthPodConditionForPod(ctx context.Context, pod *corev1.Pod,
+	targetHealth *elbv2sdk.TargetHealth, targetHealthCondType corev1.PodConditionType) (bool, error) {
 
-	if err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
-		pod := pod.DeepCopy()
+	needFurtherProbe := false
+	if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
 		if err := m.k8sClient.Get(ctx, k8s.NamespacedName(pod), pod); err != nil {
 			return err
 		}
+		if !k8s.IsPodHasReadinessGate(pod, targetHealthCondType) {
+			needFurtherProbe = false
+			return nil
+		}
+		targetHealthCondStatus := corev1.ConditionUnknown
+		var reason, message string
+		if targetHealth != nil {
+			if awssdk.StringValue(targetHealth.State) == elbv2sdk.TargetHealthStateEnumHealthy {
+				targetHealthCondStatus = corev1.ConditionTrue
+			} else {
+				targetHealthCondStatus = corev1.ConditionFalse
+			}
+
+			reason = awssdk.StringValue(targetHealth.Reason)
+			message = awssdk.StringValue(targetHealth.Description)
+		}
+
+		existingTargetHealthCond := k8s.GetPodCondition(pod, targetHealthCondType)
+		// we skip patch pod if it's already true, and match current computed status/reason/message.
+		if existingTargetHealthCond != nil &&
+			existingTargetHealthCond.Status == corev1.ConditionTrue &&
+			existingTargetHealthCond.Status == targetHealthCondStatus &&
+			existingTargetHealthCond.Reason == reason &&
+			existingTargetHealthCond.Message == message {
+			needFurtherProbe = false
+			return nil
+		}
+
+		newTargetHealthCond := corev1.PodCondition{
+			Type:          targetHealthCondType,
+			Status:        targetHealthCondStatus,
+			LastProbeTime: metav1.Now(),
+			Reason:        reason,
+			Message:       message,
+		}
+		if existingTargetHealthCond == nil || existingTargetHealthCond.Status != targetHealthCondStatus {
+			newTargetHealthCond.LastTransitionTime = metav1.Now()
+		}
 
 		oldPod := pod.DeepCopy()
 		k8s.UpdatePodCondition(pod, newTargetHealthCond)
-		if err := m.k8sClient.Status().Patch(ctx, pod, client.MergeFrom(oldPod)); err != nil {
-			return err
-		}
-		return nil
+		needFurtherProbe = targetHealthCondStatus != corev1.ConditionTrue
+		return m.k8sClient.Status().Patch(ctx, pod, client.MergeFromWithOptions(oldPod, client.MergeFromWithOptimisticLock{}))
 	}); err != nil {
 		return false, err
 	}
-	return targetHealthCondStatus != corev1.ConditionTrue, nil
+	return needFurtherProbe, nil
 }
 
 func (m *defaultResourceManager) deregisterTargets(ctx context.Context, tgARN string, targets []TargetInfo) error {
@@ -286,6 +314,15 @@ func partitionTargetsByDrainingStatus(targets []TargetInfo) ([]TargetInfo, []Tar
 	return notDrainingTargets, drainingTargets
 }
 
+func containsTargetsInInitialState(matchedEndpointAndTargets []podEndpointAndTargetPair) bool {
+	for _, endpointAndTarget := range matchedEndpointAndTargets {
+		if endpointAndTarget.target.IsInitial() {
+			return true
+		}
+	}
+	return false
+}
+
 func matchPodEndpointWithTargets(endpoints []backend.PodEndpoint, targets []TargetInfo) ([]podEndpointAndTargetPair, []backend.PodEndpoint, []TargetInfo) {
 	var matchedEndpointAndTargets []podEndpointAndTargetPair
 	var unmatchedEndpoints []backend.PodEndpoint
diff --git a/pkg/targetgroupbinding/targets_manager_types.go b/pkg/targetgroupbinding/targets_manager_types.go
index 56e4ae94b..5f765fef0 100644
--- a/pkg/targetgroupbinding/targets_manager_types.go
+++ b/pkg/targetgroupbinding/targets_manager_types.go
@@ -33,6 +33,7 @@ func (t *TargetInfo) IsNotRegistered() bool {
 		awssdk.StringValue(t.TargetHealth.Reason) == elbv2sdk.TargetHealthReasonEnumTargetNotRegistered
 }
 
+// IsDraining returns whether target is in draining state.
 func (t *TargetInfo) IsDraining() bool {
 	if t.TargetHealth == nil {
 		return false
@@ -40,6 +41,14 @@ func (t *TargetInfo) IsDraining() bool {
 	return awssdk.StringValue(t.TargetHealth.State) == elbv2sdk.TargetHealthStateEnumDraining
 }
 
+// IsInitial returns whether target is in initial state.
+func (t *TargetInfo) IsInitial() bool {
+	if t.TargetHealth == nil {
+		return false
+	}
+	return awssdk.StringValue(t.TargetHealth.State) == elbv2sdk.TargetHealthStateEnumInitial
+}
+
 // UniqueIDForTargetDescription generates a unique ID to differentiate targets.
 func UniqueIDForTargetDescription(target elbv2sdk.TargetDescription) string {
 	return fmt.Sprintf("%v:%v", awssdk.StringValue(target.Id), awssdk.Int64Value(target.Port))


Test patch:
diff --git a/pkg/ingress/model_build_actions_test.go b/pkg/ingress/model_build_actions_test.go
new file mode 100644
index 000000000..7c8a8fbdd
--- /dev/null
+++ b/pkg/ingress/model_build_actions_test.go
@@ -0,0 +1,250 @@
+package ingress
+
+import (
+	"context"
+	awssdk "github.com/aws/aws-sdk-go/aws"
+	"github.com/pkg/errors"
+	"github.com/stretchr/testify/assert"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
+	elbv2model "sigs.k8s.io/aws-load-balancer-controller/pkg/model/elbv2"
+	testclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
+	"testing"
+)
+
+func Test_defaultModelBuildTask_buildAuthenticateOIDCAction(t *testing.T) {
+	type env struct {
+		secrets []*corev1.Secret
+	}
+	type args struct {
+		authCfg   AuthConfig
+		namespace string
+	}
+	authBehaviorAuthenticate := elbv2model.AuthenticateOIDCActionConditionalBehaviorAuthenticate
+	tests := []struct {
+		name    string
+		env     env
+		args    args
+		want    elbv2model.Action
+		wantErr error
+	}{
+		{
+			name: "clientID & clientSecret configured",
+			env: env{
+				secrets: []*corev1.Secret{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Namespace: "my-ns",
+							Name:      "my-k8s-secret",
+						},
+						Data: map[string][]byte{
+							"clientID":     []byte("my-client-id"),
+							"clientSecret": []byte("my-client-secret"),
+						},
+					},
+				},
+			},
+			args: args{
+				authCfg: AuthConfig{
+					Type: AuthTypeCognito,
+					IDPConfigOIDC: &AuthIDPConfigOIDC{
+						Issuer:                "https://example.com",
+						AuthorizationEndpoint: "https://authorization.example.com",
+						TokenEndpoint:         "https://token.example.com",
+						UserInfoEndpoint:      "https://userinfo.example.co",
+						SecretName:            "my-k8s-secret",
+						AuthenticationRequestExtraParams: map[string]string{
+							"key1": "value1",
+						},
+					},
+					OnUnauthenticatedRequest: "authenticate",
+					Scope:                    "email",
+					SessionCookieName:        "my-session-cookie",
+					SessionTimeout:           65536,
+				},
+				namespace: "my-ns",
+			},
+			want: elbv2model.Action{
+				Type: elbv2model.ActionTypeAuthenticateOIDC,
+				AuthenticateOIDCConfig: &elbv2model.AuthenticateOIDCActionConfig{
+					Issuer:                "https://example.com",
+					AuthorizationEndpoint: "https://authorization.example.com",
+					TokenEndpoint:         "https://token.example.com",
+					UserInfoEndpoint:      "https://userinfo.example.co",
+					ClientID:              "my-client-id",
+					ClientSecret:          "my-client-secret",
+					AuthenticationRequestExtraParams: map[string]string{
+						"key1": "value1",
+					},
+					OnUnauthenticatedRequest: &authBehaviorAuthenticate,
+					Scope:                    awssdk.String("email"),
+					SessionCookieName:        awssdk.String("my-session-cookie"),
+					SessionTimeout:           awssdk.Int64(65536),
+				},
+			},
+		},
+		{
+			name: "clientID & clientSecret configured - legacy clientId",
+			env: env{
+				secrets: []*corev1.Secret{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Namespace: "my-ns",
+							Name:      "my-k8s-secret",
+						},
+						Data: map[string][]byte{
+							"clientId":     []byte("my-client-id"),
+							"clientSecret": []byte("my-client-secret"),
+						},
+					},
+				},
+			},
+			args: args{
+				authCfg: AuthConfig{
+					Type: AuthTypeCognito,
+					IDPConfigOIDC: &AuthIDPConfigOIDC{
+						Issuer:                "https://example.com",
+						AuthorizationEndpoint: "https://authorization.example.com",
+						TokenEndpoint:         "https://token.example.com",
+						UserInfoEndpoint:      "https://userinfo.example.co",
+						SecretName:            "my-k8s-secret",
+						AuthenticationRequestExtraParams: map[string]string{
+							"key1": "value1",
+						},
+					},
+					OnUnauthenticatedRequest: "authenticate",
+					Scope:                    "email",
+					SessionCookieName:        "my-session-cookie",
+					SessionTimeout:           65536,
+				},
+				namespace: "my-ns",
+			},
+			want: elbv2model.Action{
+				Type: elbv2model.ActionTypeAuthenticateOIDC,
+				AuthenticateOIDCConfig: &elbv2model.AuthenticateOIDCActionConfig{
+					Issuer:                "https://example.com",
+					AuthorizationEndpoint: "https://authorization.example.com",
+					TokenEndpoint:         "https://token.example.com",
+					UserInfoEndpoint:      "https://userinfo.example.co",
+					ClientID:              "my-client-id",
+					ClientSecret:          "my-client-secret",
+					AuthenticationRequestExtraParams: map[string]string{
+						"key1": "value1",
+					},
+					OnUnauthenticatedRequest: &authBehaviorAuthenticate,
+					Scope:                    awssdk.String("email"),
+					SessionCookieName:        awssdk.String("my-session-cookie"),
+					SessionTimeout:           awssdk.Int64(65536),
+				},
+			},
+		},
+		{
+			name: "missing IDPConfigOIDC",
+			args: args{
+				authCfg: AuthConfig{
+					Type:          AuthTypeCognito,
+					IDPConfigOIDC: nil,
+				},
+			},
+			wantErr: errors.New("missing IDPConfigOIDC"),
+		},
+		{
+			name: "missing clientID",
+			env: env{
+				secrets: []*corev1.Secret{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Namespace: "my-ns",
+							Name:      "my-k8s-secret",
+						},
+						Data: map[string][]byte{
+							"clientSecret": []byte("my-client-secret"),
+						},
+					},
+				},
+			},
+			args: args{
+				authCfg: AuthConfig{
+					Type: AuthTypeCognito,
+					IDPConfigOIDC: &AuthIDPConfigOIDC{
+						Issuer:                "https://example.com",
+						AuthorizationEndpoint: "https://authorization.example.com",
+						TokenEndpoint:         "https://token.example.com",
+						UserInfoEndpoint:      "https://userinfo.example.co",
+						SecretName:            "my-k8s-secret",
+						AuthenticationRequestExtraParams: map[string]string{
+							"key1": "value1",
+						},
+					},
+					OnUnauthenticatedRequest: "authenticate",
+					Scope:                    "email",
+					SessionCookieName:        "my-session-cookie",
+					SessionTimeout:           65536,
+				},
+				namespace: "my-ns",
+			},
+			wantErr: errors.New("missing clientID, secret: my-ns/my-k8s-secret"),
+		},
+		{
+			name: "missing clientSecret",
+			env: env{
+				secrets: []*corev1.Secret{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Namespace: "my-ns",
+							Name:      "my-k8s-secret",
+						},
+						Data: map[string][]byte{
+							"clientID": []byte("my-client-id"),
+						},
+					},
+				},
+			},
+			args: args{
+				authCfg: AuthConfig{
+					Type: AuthTypeCognito,
+					IDPConfigOIDC: &AuthIDPConfigOIDC{
+						Issuer:                "https://example.com",
+						AuthorizationEndpoint: "https://authorization.example.com",
+						TokenEndpoint:         "https://token.example.com",
+						UserInfoEndpoint:      "https://userinfo.example.co",
+						SecretName:            "my-k8s-secret",
+						AuthenticationRequestExtraParams: map[string]string{
+							"key1": "value1",
+						},
+					},
+					OnUnauthenticatedRequest: "authenticate",
+					Scope:                    "email",
+					SessionCookieName:        "my-session-cookie",
+					SessionTimeout:           65536,
+				},
+				namespace: "my-ns",
+			},
+			wantErr: errors.New("missing clientSecret, secret: my-ns/my-k8s-secret"),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			k8sSchema := runtime.NewScheme()
+			clientgoscheme.AddToScheme(k8sSchema)
+			k8sClient := testclient.NewFakeClientWithScheme(k8sSchema)
+			for _, secret := range tt.env.secrets {
+				err := k8sClient.Create(context.Background(), secret.DeepCopy())
+				assert.NoError(t, err)
+			}
+
+			task := &defaultModelBuildTask{
+				k8sClient: k8sClient,
+			}
+			got, err := task.buildAuthenticateOIDCAction(context.Background(), tt.args.authCfg, tt.args.namespace)
+			if tt.wantErr != nil {
+				assert.EqualError(t, err, tt.wantErr.Error())
+			} else {
+				assert.NoError(t, err)
+				assert.Equal(t, tt.want, got)
+			}
+		})
+	}
+}
diff --git a/pkg/targetgroupbinding/resource_manager_test.go b/pkg/targetgroupbinding/resource_manager_test.go
new file mode 100644
index 000000000..bb51a1a69
--- /dev/null
+++ b/pkg/targetgroupbinding/resource_manager_test.go
@@ -0,0 +1,360 @@
+package targetgroupbinding
+
+import (
+	"context"
+	awssdk "github.com/aws/aws-sdk-go/aws"
+	elbv2sdk "github.com/aws/aws-sdk-go/service/elbv2"
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+	"github.com/stretchr/testify/assert"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
+	"sigs.k8s.io/aws-load-balancer-controller/pkg/equality"
+	"sigs.k8s.io/aws-load-balancer-controller/pkg/k8s"
+	testclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
+	"sigs.k8s.io/controller-runtime/pkg/log"
+	"testing"
+)
+
+func Test_defaultResourceManager_updateTargetHealthPodConditionForPod(t *testing.T) {
+	type args struct {
+		pod                  *corev1.Pod
+		targetHealth         *elbv2sdk.TargetHealth
+		targetHealthCondType corev1.PodConditionType
+	}
+
+	tests := []struct {
+		name    string
+		args    args
+		want    bool
+		wantPod *corev1.Pod
+		wantErr error
+	}{
+		{
+			name: "pod contains readinessGate and targetHealth is healthy - add pod condition",
+			args: args{
+				pod: &corev1.Pod{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "default",
+						Name:      "my-pod",
+					},
+					Spec: corev1.PodSpec{
+						ReadinessGates: []corev1.PodReadinessGate{
+							{
+								ConditionType: "target-health.elbv2.k8s.aws/my-tgb",
+							},
+						},
+					},
+					Status: corev1.PodStatus{
+						Conditions: []corev1.PodCondition{
+							{
+								Type:   corev1.ContainersReady,
+								Status: corev1.ConditionTrue,
+							},
+						},
+					},
+				},
+				targetHealth: &elbv2sdk.TargetHealth{
+					State: awssdk.String(elbv2sdk.TargetHealthStateEnumHealthy),
+				},
+				targetHealthCondType: "target-health.elbv2.k8s.aws/my-tgb",
+			},
+			want: false,
+			wantPod: &corev1.Pod{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: "default",
+					Name:      "my-pod",
+				},
+				Spec: corev1.PodSpec{
+					ReadinessGates: []corev1.PodReadinessGate{
+						{
+							ConditionType: "target-health.elbv2.k8s.aws/my-tgb",
+						},
+					},
+				},
+				Status: corev1.PodStatus{
+					Conditions: []corev1.PodCondition{
+						{
+							Type:   corev1.ContainersReady,
+							Status: corev1.ConditionTrue,
+						},
+						{
+							Type:   "target-health.elbv2.k8s.aws/my-tgb",
+							Status: corev1.ConditionTrue,
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "pod contains readinessGate and targetHealth is healthy - update pod condition",
+			args: args{
+				pod: &corev1.Pod{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "default",
+						Name:      "my-pod",
+					},
+					Spec: corev1.PodSpec{
+						ReadinessGates: []corev1.PodReadinessGate{
+							{
+								ConditionType: "target-health.elbv2.k8s.aws/my-tgb",
+							},
+						},
+					},
+					Status: corev1.PodStatus{
+						Conditions: []corev1.PodCondition{
+							{
+								Type:   corev1.ContainersReady,
+								Status: corev1.ConditionTrue,
+							},
+							{
+								Type:    "target-health.elbv2.k8s.aws/my-tgb",
+								Status:  corev1.ConditionFalse,
+								Reason:  elbv2sdk.TargetHealthReasonEnumElbRegistrationInProgress,
+								Message: "Target registration is in progress",
+							},
+						},
+					},
+				},
+				targetHealth: &elbv2sdk.TargetHealth{
+					State: awssdk.String(elbv2sdk.TargetHealthStateEnumHealthy),
+				},
+				targetHealthCondType: "target-health.elbv2.k8s.aws/my-tgb",
+			},
+			want: false,
+			wantPod: &corev1.Pod{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: "default",
+					Name:      "my-pod",
+				},
+				Spec: corev1.PodSpec{
+					ReadinessGates: []corev1.PodReadinessGate{
+						{
+							ConditionType: "target-health.elbv2.k8s.aws/my-tgb",
+						},
+					},
+				},
+				Status: corev1.PodStatus{
+					Conditions: []corev1.PodCondition{
+						{
+							Type:   corev1.ContainersReady,
+							Status: corev1.ConditionTrue,
+						},
+						{
+							Type:   "target-health.elbv2.k8s.aws/my-tgb",
+							Status: corev1.ConditionTrue,
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "pod contains readinessGate and targetHealth is unhealthy - add pod condition",
+			args: args{
+				pod: &corev1.Pod{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "default",
+						Name:      "my-pod",
+					},
+					Spec: corev1.PodSpec{
+						ReadinessGates: []corev1.PodReadinessGate{
+							{
+								ConditionType: "target-health.elbv2.k8s.aws/my-tgb",
+							},
+						},
+					},
+					Status: corev1.PodStatus{
+						Conditions: []corev1.PodCondition{
+							{
+								Type:   corev1.ContainersReady,
+								Status: corev1.ConditionTrue,
+							},
+							{
+								Type:    "target-health.elbv2.k8s.aws/my-tgb",
+								Status:  corev1.ConditionFalse,
+								Reason:  elbv2sdk.TargetHealthReasonEnumElbRegistrationInProgress,
+								Message: "Target registration is in progress",
+							},
+						},
+					},
+				},
+				targetHealth: &elbv2sdk.TargetHealth{
+					State:       awssdk.String(elbv2sdk.TargetHealthStateEnumUnhealthy),
+					Reason:      awssdk.String(elbv2sdk.TargetHealthReasonEnumTargetFailedHealthChecks),
+					Description: awssdk.String("Health checks failed"),
+				},
+				targetHealthCondType: "target-health.elbv2.k8s.aws/my-tgb",
+			},
+			want: true,
+			wantPod: &corev1.Pod{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: "default",
+					Name:      "my-pod",
+				},
+				Spec: corev1.PodSpec{
+					ReadinessGates: []corev1.PodReadinessGate{
+						{
+							ConditionType: "target-health.elbv2.k8s.aws/my-tgb",
+						},
+					},
+				},
+				Status: corev1.PodStatus{
+					Conditions: []corev1.PodCondition{
+						{
+							Type:   corev1.ContainersReady,
+							Status: corev1.ConditionTrue,
+						},
+						{
+							Type:    "target-health.elbv2.k8s.aws/my-tgb",
+							Status:  corev1.ConditionFalse,
+							Reason:  elbv2sdk.TargetHealthReasonEnumTargetFailedHealthChecks,
+							Message: "Health checks failed",
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "pod contains readinessGate and targetHealth is nil - currentPod has no condition",
+			args: args{
+				pod: &corev1.Pod{
+					ObjectMeta: metav1.ObjectMeta{
+						Namespace: "default",
+						Name:      "my-pod",
+					},
+					Spec: corev1.PodSpec{
+						ReadinessGates: []corev1.PodReadinessGate{
+							{
+								ConditionType: "target-health.elbv2.k8s.aws/my-tgb",
+							},
+						},
+					},
+					Status: corev1.PodStatus{
+						Conditions: []corev1.PodCondition{
+							{
+								Type:   corev1.ContainersReady,
+								Status: corev1.ConditionTrue,
+							},
+						},
+					},
+				},
+				targetHealth:         nil,
+				targetHealthCondType: "target-health.elbv2.k8s.aws/my-tgb",
+			},
+			want: true,
+			wantPod: &corev1.Pod{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: "default",
+					Name:      "my-pod",
+				},
+				Spec: corev1.PodSpec{
+					ReadinessGates: []corev1.PodReadinessGate{
+						{
+							ConditionType: "target-health.elbv2.k8s.aws/my-tgb",
+						},
+					},
+				},
+				Status: corev1.PodStatus{
+					Conditions: []corev1.PodCondition{
+						{
+							Type:   corev1.ContainersReady,
+							Status: corev1.ConditionTrue,
+						},
+						{
+							Type:   "target-health.elbv2.k8s.aws/my-tgb",
+							Status: corev1.ConditionUnknown,
+						},
+					},
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			k8sSchema := runtime.NewScheme()
+			clientgoscheme.AddToScheme(k8sSchema)
+			k8sClient := testclient.NewFakeClientWithScheme(k8sSchema)
+
+			m := &defaultResourceManager{
+				k8sClient: k8sClient,
+				logger:    &log.NullLogger{},
+			}
+
+			ctx := context.Background()
+			pod := tt.args.pod.DeepCopy()
+			err := k8sClient.Create(ctx, pod)
+			assert.NoError(t, err)
+
+			got, err := m.updateTargetHealthPodConditionForPod(context.Background(),
+				pod, tt.args.targetHealth, tt.args.targetHealthCondType)
+			if tt.wantErr != nil {
+				assert.EqualError(t, err, tt.wantErr.Error())
+			} else {
+				assert.NoError(t, err)
+				assert.Equal(t, tt.want, got)
+
+				updatedPod := &corev1.Pod{}
+				err := m.k8sClient.Get(ctx, k8s.NamespacedName(pod), updatedPod)
+				assert.NoError(t, err)
+
+				opts := cmp.Options{
+					equality.IgnoreFakeClientPopulatedFields(),
+					cmpopts.IgnoreTypes(metav1.Time{}),
+				}
+				assert.True(t, cmp.Equal(tt.wantPod, updatedPod, opts), "diff", cmp.Diff(tt.wantPod, updatedPod, opts))
+			}
+		})
+	}
+}
+
+func Test_containsTargetsInInitialState(t *testing.T) {
+	type args struct {
+		matchedEndpointAndTargets []podEndpointAndTargetPair
+	}
+	tests := []struct {
+		name string
+		args args
+		want bool
+	}{
+		{
+			name: "contains initial targets",
+			args: args{
+				matchedEndpointAndTargets: []podEndpointAndTargetPair{
+					{
+						target: TargetInfo{
+							TargetHealth: &elbv2sdk.TargetHealth{
+								State:       awssdk.String(elbv2sdk.TargetHealthStateEnumInitial),
+								Reason:      awssdk.String(elbv2sdk.TargetHealthReasonEnumElbRegistrationInProgress),
+								Description: awssdk.String("Target registration is in progress"),
+							},
+						},
+					},
+				},
+			},
+			want: true,
+		},
+		{
+			name: "contains no initial targets",
+			args: args{
+				matchedEndpointAndTargets: []podEndpointAndTargetPair{
+					{
+						target: TargetInfo{
+							TargetHealth: &elbv2sdk.TargetHealth{
+								State: awssdk.String(elbv2sdk.TargetHealthStateEnumHealthy),
+							},
+						},
+					},
+				},
+			},
+			want: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := containsTargetsInInitialState(tt.args.matchedEndpointAndTargets)
+			assert.Equal(t, tt.want, got)
+		})
+	}
+}
diff --git a/pkg/targetgroupbinding/targets_manager_types_test.go b/pkg/targetgroupbinding/targets_manager_types_test.go
index 7a7a7a1e0..b062e8308 100644
--- a/pkg/targetgroupbinding/targets_manager_types_test.go
+++ b/pkg/targetgroupbinding/targets_manager_types_test.go
@@ -128,6 +128,74 @@ func TestTargetInfo_IsNotRegistered(t *testing.T) {
 	}
 }
 
+func TestTargetInfo_IsInitial(t *testing.T) {
+	tests := []struct {
+		name   string
+		target TargetInfo
+		want   bool
+	}{
+		{
+			name: "target with initial state and initial healthCheck reason",
+			target: TargetInfo{
+				Target: elbv2sdk.TargetDescription{
+					Id:   awssdk.String("192.168.1.1"),
+					Port: awssdk.Int64(8080),
+				},
+				TargetHealth: &elbv2sdk.TargetHealth{
+					Reason: awssdk.String(elbv2sdk.TargetHealthReasonEnumElbInitialHealthChecking),
+					State:  awssdk.String(elbv2sdk.TargetHealthStateEnumInitial),
+				},
+			},
+			want: true,
+		},
+		{
+			name: "target with initial state and elb registrationInProgress reason",
+			target: TargetInfo{
+				Target: elbv2sdk.TargetDescription{
+					Id:   awssdk.String("192.168.1.1"),
+					Port: awssdk.Int64(8080),
+				},
+				TargetHealth: &elbv2sdk.TargetHealth{
+					Reason: awssdk.String(elbv2sdk.TargetHealthReasonEnumElbRegistrationInProgress),
+					State:  awssdk.String(elbv2sdk.TargetHealthStateEnumInitial),
+				},
+			},
+			want: true,
+		},
+		{
+			name: "target with unknown TargetHealth",
+			target: TargetInfo{
+				Target: elbv2sdk.TargetDescription{
+					Id:   awssdk.String("192.168.1.1"),
+					Port: awssdk.Int64(8080),
+				},
+				TargetHealth: nil,
+			},
+			want: false,
+		},
+		{
+			name: "target with unused state and targetNotInUse reason",
+			target: TargetInfo{
+				Target: elbv2sdk.TargetDescription{
+					Id:   awssdk.String("192.168.1.1"),
+					Port: awssdk.Int64(8080),
+				},
+				TargetHealth: &elbv2sdk.TargetHealth{
+					Reason: awssdk.String(elbv2sdk.TargetHealthReasonEnumTargetNotInUse),
+					State:  awssdk.String(elbv2sdk.TargetHealthStateEnumUnused),
+				},
+			},
+			want: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t1 *testing.T) {
+			got := tt.target.IsInitial()
+			assert.Equal(t, tt.want, got)
+		})
+	}
+}
+
 func TestUniqueIDForTargetDescription(t *testing.T) {
 	type args struct {
 		target elbv2sdk.TargetDescription