Problem – HAProxy NodePort Conflict in Kubernetes Staging
In the staging environment several AI micro‑services expose a NodePort service. The HAProxy ingress controller attempts to bind each service’s NodePort on the host network interface. When two or more services request the same port, HAProxy pods repeatedly fail to start with errors such as:
2024-06-23T10:12:45Z haproxy[1]: ERROR : bind : address already in use
2024-06-23T10:12:45Z haproxy[1]: Fatal error: cannot bind to 0.0.0.0:30080
Kubernetes also emits validation events:
Warning InvalidService 10s (x2) service my‑service is invalid: spec.ports[0].nodePort: Duplicate value
The immediate impact is a cascade of 502/503 responses from HAProxy, health‑check failures, and a temporary loss of inbound traffic for all AI services in staging.
Root Cause – Why the Conflict Happens
NodePort allocation in Kubernetes follows a simple rule set (Kubernetes Service documentation): ports must be unique across the entire cluster and lie in the range 30000‑32767. The following conditions commonly lead to duplication:
- Static NodePort values in Helm charts – multiple charts reused the same hard‑coded
nodePort: 30080(see the staging outage where three AI micro‑services each defined that value). - CI/CD race condition – rapid creation of services can trigger the allocation race described in kubernetes/kubernetes #103456, causing the API server to accept duplicate values before the controller reconciles.
- Ingress controller binding semantics – HAProxy’s
binddirective (HAProxy Configuration Manual, “bind” directive) binds to the host IP/port for each NodePort. When two services share a port, the second HAProxy process hits “address already in use”.
Thus the conflict is not a HAProxy bug but a mis‑alignment between service definitions and the cluster‑wide uniqueness requirement for NodePorts.
Debug – Investigation Steps
Follow these concrete steps to reproduce and isolate the conflict.
- Inspect HAProxy pod logs for bind errors.
- List all NodePort services and look for duplicate ports.
- Cross‑check Helm values for static
nodePortentries. - Capture the Kubernetes events that indicate duplicate values.
Example commands:
# HAProxy pod logs (replace pod name)
kubectl logs -n ingress haproxy-ingress-5d9c7f9c9f-abcde
# List NodePort services with their ports
kubectl get svc -A -o jsonpath="{range .items[?(@.spec.type=='NodePort')]}{.metadata.namespace}/{.metadata.name}:{.spec.ports[*].nodePort}{'\n'}{end}" | sort
# Show events for a specific service
kubectl describe svc -n ai my-service | grep -i nodeport
Typical output showing duplicates:
ai/service-a:30080
ai/service-b:30080 <-- duplicate
ai/service-c:30081
If the same port appears more than once, the conflict is confirmed.
Solution – Resolving the NodePort Conflict
The fix consists of ensuring each Service receives a unique NodePort. There are two practical approaches:
Approach 1 – Let Kubernetes Allocate Dynamically
Remove the explicit nodePort field from Helm values. Kubernetes will assign an unused port automatically.
Before (static port):
apiVersion: v1
kind: Service
metadata:
name: service-a
spec:
type: NodePort
ports:
- port: 80
targetPort: 8080
nodePort: 30080 # <-- static
After (dynamic allocation):
apiVersion: v1
kind: Service
metadata:
name: service-a
spec:
type: NodePort
ports:
- port: 80
targetPort: 8080
# nodePort omitted – Kubernetes picks a free value
Approach 2 – Allocate Unique Ports via Helm Values
If a fixed port is required (e.g., external firewall rules), centralise the allocation in a shared values.yaml or a ConfigMap and reference it from each chart.
Shared values file (global-nodeports.yaml)
nodePorts:
service-a: 30080
service-b: 30081
service-c: 30082
Chart snippet using the shared values:
apiVersion: v1
kind: Service
metadata:
name: {{ .Chart.Name }}
spec:
type: NodePort
ports:
- port: 80
targetPort: 8080
nodePort: {{ .Values.global.nodePorts.[.Chart.Name] }}
Redeploy the services
# If using Helm
helm upgrade --install service-a ./service-a -f global-nodeports.yaml
helm upgrade --install service-b ./service-b -f global-nodeports.yaml
# Verify new ports
kubectl get svc -A -o wide | grep NodePort
After redeployment, HAProxy pods start without bind errors and begin listening on distinct ports.
Verify – Confirming the Fix
Perform the following checks:
- HAProxy pod status – pods should be
Runningwith no restart loop. - HAProxy logs – no “address already in use” entries.
- Service list – each
NodePortunique. - Functional test – curl the ingress IP on each service’s external port and expect
200 OK.
Example validation commands:
kubectl get pods -n ingress -l app=haproxy-ingress
kubectl logs -n ingress $(kubectl get pods -n ingress -l app=haproxy-ingress -o jsonpath="{.items[0].metadata.name}")
# Simple health‑check
curl -s -o /dev/null -w "%{http_code}" http://:30080/healthz
# Expected output: 200
Prevent – Best Practices to Avoid Future NodePort Collisions
| Practice | Why it Helps |
|---|---|
Never hard‑code nodePort in individual charts |
Reduces the chance of duplicate values across releases. |
| Centralise port allocation in a shared values file or ConfigMap | Provides a single source of truth for static ports. |
Enable service.spec.allocateLoadBalancerNodePorts (Kubernetes 1.20+) |
Lets the control plane enforce uniqueness during rapid service creation. |
| Add a pre‑deployment lint step | Scripts can query existing NodePorts via kubectl get svc -A -o json and abort if a duplicate is detected. |
| Monitor HAProxy bind errors | Alert on log patterns “address already in use” to catch regressions early. |
FAQ – Related Questions
- Why does the conflict appear only in staging and not in production?
Staging often runs multiple feature branches simultaneously, each deploying its own Helm chart with static ports. Production typically uses a single, stable release where ports have been de‑duplicated. - Can I use a LoadBalancer service instead of NodePort to avoid this issue?
Yes. ALoadBalancerservice delegates port allocation to the cloud provider, eliminating the need for manual NodePort management. However, HAProxy still needs uniquebindstatements for each backend. - What if I must keep a specific external port for compliance?
Reserve the port at the cluster level by creating a dummy Service with the desirednodePortand annotate it withhaproxy.org/ignoreso HAProxy does not attempt to bind it. - How do I troubleshoot “cannot bind to 0.0.0.0:<port>” errors in HAProxy logs?
Check for duplicate NodePorts, verify that no other pod on the node is already listening on that port (e.g.,ss -ltnp | grep), and confirm that the HAProxy process has the necessary capabilities (CAP_NET_BIND_SERVICE). - Is there a way to automate unique NodePort assignment in CI pipelines?
Yes. Usekubectl get svc -A -o jsonpath="{.items[*].spec.ports[*].nodePort}"to collect used ports, then generate a non‑conflicting value programmatically before applying manifests.
Related Topic Hub: Distributed Systems Troubleshooting Hub