Skip to content

Configure HAProxy pods and access

The chart deploys two HAProxy replicas by default. Configure Service access, replicas and autoscaling, and resource budgets through Helm values. Use haproxy.podSpec for scheduling, volumes, and other pod settings. Add settings to your complete Helm values file. You can also manage the pods separately.

Resource limits

Use the resource sizing guide to budget the installation. Set HAProxy resources with haproxy.resources and agent resources with haproxy.agent.resources.

Expose traffic and management ports

Separate Services expose controller health and metrics, the admission webhook, HAProxy traffic and stats, and the HAPTIC agent.

Controller Service

The controller's ClusterIP Service exposes health and metrics ports. For a release named haptic, the Service is also named haptic. Other release names use the chart's fullname, usually <release>-haptic:

Name Container port Values key Purpose
healthz 8080 controller.ports.healthz Single source for the process listener, liveness/readiness probes, Service, and /debug/* introspection endpoints
metrics 9090 controller.ports.metrics Single source for the process listener, Service, and Prometheus monitors; 0 disables metrics

The separate <fullname>-webhook Service exposes port 443 (controller.webhook.service.port) and forwards to port 9443 (controller.ports.webhook).

Configure the health and metrics Service under controller.service:

controller:
  service:
    type: ClusterIP
    annotations: {}

HAProxy Service

The <fullname>-haproxy Service sends traffic to HAProxy pods. For a release named haptic, it's haptic-haproxy. It uses NodePort by default; configure its ports through haproxy.service.* and pod ports through haproxy.ports.*:

Name Service port Container port nodePort default
http 80 80 30080
https 443 443 30443
stats 8404 8404 30404

The agent gets its own internal-only ClusterIP Service (<fullname>-haproxy-dataplane, for example <release>-haptic-haproxy-dataplane) on port 5555. Its type comes from haproxy.agent.service.type.

Local access, including kind clusters, works with kubectl port-forward regardless of Service type. For a release named haptic in namespace haptic:

kubectl port-forward -n haptic service/haptic-haproxy 8080:80

In another terminal, send requests to http://localhost:8080 with the hostname configured on your Ingress. HTTP and HTTPS Gateways use their own Services; port-forward to the Gateway's Service to reach its listeners. NodePort access from the host also requires a reachable node address or matching kind port mappings.

LoadBalancer access requires a load-balancer implementation in your cluster. Set the Service type and any annotations required by that implementation:

haproxy:
  service:
    type: LoadBalancer

External / self-managed HAProxy — turn off the chart's HAProxy deployment and manage pods yourself (see HAProxy Pod Requirements):

haproxy:
  enabled: false

PROXY protocol

If an upstream load balancer replaces the client address, HAProxy sees that balancer as the source. Access logs, source-IP rate limits, and IP-based policies then use the address of the balancer.

The load balancer fixes this by adding a PROXY protocol header that carries the original address. Enable the matching listeners:

controller:
  config:
    templatingSettings:
      extraContext:
        proxyProtocol:
          enabled: true
          httpPort: 8081
          httpsPort: 8444

That adds two binds, adds them to the HAProxy Service and the NetworkPolicy, and leaves haproxy.ports.http / haproxy.ports.https exactly as they were. Point the balancer at the new ports:

# On the upstream load balancer
server k8s-https 10.0.0.50:8444 send-proxy-v2

Requests arriving on the PROXY ports carry the real client through the access log's client_ip, src-keyed rate limiting, the WAF, and IP access control lists. Terminated HTTPS on httpsPort uses the same certificates, ciphers, and protocol negotiation as the plain HTTPS bind; with TLS-Passthrough configured, httpsPort attaches to the SNI-routing frontend instead so passthrough hosts keep working.

Send the header, or the connection is dropped

HAProxy has no "PROXY header optional" mode. A connection reaching httpPort or httpsPort without the header is rejected, so only the upstream balancer may target these ports. Everything else — direct access, in-cluster clients, NodePort traffic, probes — keeps using the regular haproxy.ports.http / haproxy.ports.https, which is why these are additional ports rather than a flag on the existing ones.

This is separate from the haproxy-haptic.org/proxy-protocol annotation, which makes HAProxy send a PROXY header to a backend.

Full HAProxy Service reference

haproxy:
  enabled: true
  ports:
    http: 80         # HAProxy container HTTP bind
    https: 443       # HAProxy container HTTPS bind
    stats: 8404      # Stats/health page
    dataplane: 5555  # HAPTIC agent
  service:
    type: NodePort   # ClusterIP, NodePort, or LoadBalancer
    annotations: {}
    loadBalancerIP: ""
    loadBalancerSourceRanges: []
    externalTrafficPolicy: ""   # Cluster | Local
    http:
      port: 80
      nodePort: 30080           # Only honored for NodePort/LoadBalancer
    https:
      port: 443
      nodePort: 30443
    stats:
      port: 8404
      nodePort: 30404

Replicas and autoscaling

The chart runs 2 HAProxy replicas by default. Set haproxy.replicaCount to change the fixed count:

haproxy:
  replicaCount: 3

For traffic-driven autoscaling, enable KEDA under haproxy.keda. When haproxy.keda.enabled is true, the chart creates a ScaledObject and stops writing a fixed replicas onto the Deployment (KEDA owns it), scaling between minReplicaCount and maxReplicaCount from the triggers you define:

haproxy:
  keda:
    enabled: true
    minReplicaCount: 2
    maxReplicaCount: 10
    triggers:
      - type: cpu
        metricType: Utilization
        metadata:
          value: "70"

KEDA must be installed in the cluster, and haproxy.keda.triggers must list at least one trigger — it's empty by default. Any KEDA scaler works; the block above uses CPU utilization.

Initial bootstrap config

New HAProxy pods start with a minimal configuration from haproxy.initialConfig, stored in the <fullname>-haproxy-config ConfigMap. For the default release name, that's haptic-haproxy-config. The controller replaces it with your rendered configuration.

The default keeps /healthz returning 200 on the stats port and /ready returning 503 ("waiting for controller config"), so the pod stays NotReady until the controller applies its first real config.

Once the agent receives and successfully loads the rendered configuration, /ready returns 200 and Kubernetes adds the pod to the Service. If that first apply fails, the agent restores the bootstrap files and the pod stays unready.

On a later container restart, HAProxy reuses the configuration on disk if it passes haproxy -c. It falls back to the bootstrap configuration only if the file is missing or invalid.

If you replace haproxy.initialConfig, preserve the worker socket, /healthz, and a /ready response of 503. Returning 200 would send application traffic to a pod before its routes exist. The value supports Helm template expressions; changing it rolls the HAProxy pods on the next upgrade.

Access logging

Read, customize, and forward request logs with the access logging guide. It also covers request IDs, the Vector sidecar, and monitoring dropped records.

Pod readiness and restarts

Each pod runs HAProxy in master-worker mode plus the HAPTIC agent, which owns the pod's file tree and its runtime sockets. The chart supervises the SPOA hub and Vector processes inside their sidecar containers: a child exit or repeated failed health check leaves HAProxy running while the supervisor restarts only that child, with a backoff capped at 30 seconds.

HAProxy's /ready endpoint controls pod readiness. It returns 503 while the bootstrap configuration is active and 200 once a rendered configuration runs. The agent, SPOA hub, and Vector run as native sidecars: Kubernetes starts them before HAProxy and stops them after HAProxy exits, preserving dependencies during connection draining.

The agent's /readyz endpoint reports whether it can accept configuration updates; a rejected update doesn't make it unready. Its liveness probe uses the local Unix socket. A stopped container or a failing probe on a custom sidecar can still make the pod unready.

The watchdog uses /usr/bin/bash and timeout, which the default images provide. With a custom sidecar image missing either command, the supervisor logs a warning and still restarts child processes that exit.

HAProxy Pod requirements

When haproxy.enabled: false, you're responsible for deploying HAProxy pods yourself. The controller discovers them via the pod selector at controller.config.podSelector, which defaults to:

controller:
  config:
    podSelector:
      matchLabels:
        app.kubernetes.io/component: loadbalancer
        app.kubernetes.io/name: haptic        # set dynamically by the chart
        app.kubernetes.io/instance: haptic  # your Helm release name

If your existing HAProxy pods don't have those exact labels, either relabel them or override controller.config.podSelector.matchLabels to match.

Each discovered pod must:

  1. Carry labels matching podSelector.matchLabels
  2. Run HAProxy in master-worker mode with a master socket the agent can reload through, and a worker stats socket it can run runtime commands on
  3. Run the agent in the same pod, from the HAPTIC image, sharing the config volume with HAProxy
  4. Expose the agent on haproxy.ports.dataplane (default 5555), mounting its TLS identity and trusting the controller identity; see agent certificates
  5. Run the same HAProxy major.minor series as haproxyVersion so the controller validates configuration with the matching binary
  6. Keep the agent available during termination with native-sidecar ordering and the drain hook used by the chart
  7. Provide every dependency selected by your templates, including SPOA plugins and log sockets when enabled

Start from the deployment for your installed version

A separate workload controller must preserve the agent's TLS mounts, shared volumes, runtime sockets, bootstrap configuration, probes, and termination ordering. Use the chart-generated deployment for your version as the reference; a minimal HAProxy-plus-agent manifest omits dependencies enabled by default.

For an existing haptic release in namespace haptic, export its deployment for inspection:

kubectl get deployment haptic-haproxy -n haptic -o yaml > haptic-haproxy-reference.yaml
helm get manifest haptic -n haptic > haptic-release-reference.yaml

The second file includes the associated bootstrap ConfigMaps and Services. Identity Secrets are managed separately, as described in agent certificate management. Set haproxy.enabled: false only once your external workload definition supplies these dependencies and matches the configured pod selector. Helm removes its Deployment when you disable it; plan that ownership change as a rollout.

Found a problem on this page? Report it