@@ -686,33 +686,69 @@ func (p *NginxProvisioner) buildNginxDeployment(
686686 }
687687 }
688688
689- var replicas * int32
690- if deploymentCfg .Replicas != nil {
691- replicas = deploymentCfg .Replicas
692- }
693-
694- if isAutoscalingEnabled (& deploymentCfg ) {
695- ctx , cancel := context .WithTimeout (context .Background (), 5 * time .Second )
696- defer cancel ()
697-
698- hpa := & autoscalingv2.HorizontalPodAutoscaler {}
699- err := p .k8sClient .Get (ctx , types.NamespacedName {
700- Namespace : objectMeta .Namespace ,
701- Name : objectMeta .Name ,
702- }, hpa )
703- if err == nil && hpa .Status .DesiredReplicas > 0 {
704- // overwrite with HPA's desiredReplicas
705- replicas = helpers .GetPointer (hpa .Status .DesiredReplicas )
706- }
707- }
708-
689+ // Determine replica count based on HPA status
690+ replicas := p .determineReplicas (objectMeta , deploymentCfg )
709691 if replicas != nil {
710692 deployment .Spec .Replicas = replicas
711693 }
712694
713695 return deployment , nil
714696}
715697
698+ // determineReplicas determines the appropriate replica count for a deployment based on HPA status.
699+ //
700+ // HPA Replicas Management Strategy:
701+ //
702+ // When an HPA is managing a deployment, we must read the current deployment's replicas
703+ // from the cluster and use that value, rather than trying to set our own value or read
704+ // from HPA.Status.DesiredReplicas (which is eventually consistent and stale).
705+ //
706+ // Why we can't use HPA.Status.DesiredReplicas:
707+ // - HPA.Status updates lag behind Deployment.Spec.Replicas changes
708+ // - When HPA scales down: HPA writes Deployment.Spec → then updates its own Status
709+ // - If we read Status during this window, we get the OLD value and overwrite HPA's new value
710+ // - This creates a race condition causing pod churn
711+ //
712+ // Our approach:
713+ // - When HPA exists: Read current deployment replicas from cluster and use that
714+ // - When HPA doesn't exist yet: Set replicas for initial deployment creation
715+ // - When HPA exists but Deployment doesn't exist yet: Set replicas for initial deployment creation
716+ // - When HPA is disabled: Set replicas normally.
717+ func (p * NginxProvisioner ) determineReplicas (
718+ objectMeta metav1.ObjectMeta ,
719+ deploymentCfg ngfAPIv1alpha2.DeploymentSpec ,
720+ ) * int32 {
721+ replicas := deploymentCfg .Replicas
722+
723+ if ! isAutoscalingEnabled (& deploymentCfg ) {
724+ return replicas
725+ }
726+
727+ ctx , cancel := context .WithTimeout (context .Background (), 5 * time .Second )
728+ defer cancel ()
729+
730+ hpa := & autoscalingv2.HorizontalPodAutoscaler {}
731+ err := p .k8sClient .Get (ctx , types.NamespacedName {
732+ Namespace : objectMeta .Namespace ,
733+ Name : objectMeta .Name ,
734+ }, hpa )
735+ if err != nil {
736+ return replicas
737+ }
738+
739+ existingDeployment := & appsv1.Deployment {}
740+ err = p .k8sClient .Get (ctx , types.NamespacedName {
741+ Namespace : objectMeta .Namespace ,
742+ Name : objectMeta .Name ,
743+ }, existingDeployment )
744+
745+ if err == nil && existingDeployment .Spec .Replicas != nil {
746+ replicas = existingDeployment .Spec .Replicas
747+ }
748+
749+ return replicas
750+ }
751+
716752// applyPatches applies the provided patches to the given object.
717753func applyPatches (obj client.Object , patches []ngfAPIv1alpha2.Patch ) error {
718754 if len (patches ) == 0 {
0 commit comments