Scripts that make it possible to have HAProxy dynamically configure itself based on the current state of Services in a kubernetes cluster
  • Go 86.1%
  • Python 7.2%
  • Shell 4.9%
  • Jinja 1.8%
Find a file
holzi1005 04bcd4e7c8
All checks were successful
Build Go Binary / build (push) Successful in 49s
Merge pull request 'feature/gateway-api' (#2) from feature/gateway-api into master
Reviewed-on: #2
2026-04-20 20:24:34 +02:00
.forgejo/workflows test with gateway api for acl 2026-04-12 08:28:50 +02:00
.gitignore add ignore 2026-02-28 22:39:13 +01:00
gateway-config-cron update readme and post current state of HAProxy automation against kubernetes 2018-10-23 11:29:33 -05:00
gateway-haproxy-config.py ad dlables 2024-12-21 19:40:02 +01:00
generate_kubeconfig.sh Add generate_kubeconfig.sh 2025-06-29 15:21:42 +02:00
go.mod add go mod and tests 2026-04-10 08:08:37 +02:00
go_cron_bash.sh Add go_cron_bash.sh 2025-06-29 10:12:03 +02:00
haproxy-ingress-configmap.yaml Add haproxy-ingress-configmap.yaml 2025-06-29 15:17:10 +02:00
haproxy-ingress-rbac.yaml Update haproxy-ingress-rbac.yaml 2025-06-29 17:30:27 +02:00
haproxy.j2 ad dlables 2024-12-21 19:40:02 +01:00
LICENSE Initial commit 2018-10-23 11:12:47 -05:00
main.go add pod id as cookie 2026-04-20 20:23:54 +02:00
main_test.go add two hostnames 2026-04-12 09:07:26 +02:00
README.md test with gateway api for acl 2026-04-12 08:28:50 +02:00

haproxy-kubernetes

Generates HAProxy configuration from Kubernetes Services and Gateway API HTTPRoutes.

Architecture

Kubernetes Services   →  HAProxy Backends  (server IPs, ports, health checks)
Gateway API HTTPRoute →  HAProxy Frontend  (ACLs, use_backend, http-request/response)

The backend name is derived identically from both sources (K8S_<namespace>_<name>), so HTTPRoutes automatically reference the correct backend without additional configuration.


How it works

1. Backends from Services

Every Service with haproxy/enabled: "true" becomes a HAProxy backend. All backend options are configured via annotations on the Service.

2. ACLs and Frontend Rules from Gateway API HTTPRoutes

Every HTTPRoute with haproxy/enabled: "true" and haproxy/frontend.v2 generates ACLs and use_backend rules in the specified HAProxy frontend.

ACLs are built automatically from the HTTPRoute spec:

  • spec.hostnamesacl hdr(host) -i <hostname>
  • spec.rules[].matches[].pathacl path_beg / path / path_reg <value>
  • spec.rules[].matches[].headersacl req.hdr(<name>) -i <value>

Multiple ACLs are combined with and — host and path must both match.

http-request / http-response directives from spec.rules[].filters or annotations are automatically extended with if <acl-condition>.


Environment Variables

Variable Required Default Description
KUBERNETES_HOST yes K8s API server URL
KUBERNETES_TOKEN yes Bearer token for K8s API auth
KUBERNETES_VERIFYSSL no false Verify TLS certificate of K8s API
HAPROXY_TEMPLATE no haproxy.tmpl Path to the Go template file

Service Annotations

Annotation Type Description Example
haproxy/enabled bool Enable this service for HAProxy config generation "true"
haproxy/mode string Backend mode "http" / "tcp"
haproxy/balance string Load balancing algorithm "leastconn" / "roundrobin"
haproxy/cookie-name string Cookie name for session persistence "SRVCOOKIE"
haproxy/cookie-flags string Additional cookie options "insert indirect nocache"
haproxy/health-check string HTTP health check path "/healthz"
haproxy/health-check-port string Port for health check (if different from service port) "8080"
haproxy/server-options string Extra options appended to each server line "ssl verify none"
haproxy/port int Use only this port when multiple ports are defined "8080"
haproxy/http-request string http-request directives for the backend "http-request set-header X-Real-IP %[src]"
haproxy/http-response string http-response directives for the backend "http-response set-header X-Frame-Options DENY"

Service Example

apiVersion: v1
kind: Service
metadata:
  name: myapp
  namespace: default
  annotations:
    haproxy/enabled: "true"
    haproxy/mode: "http"
    haproxy/balance: "leastconn"
    haproxy/health-check: "/healthz"
    haproxy/health-check-port: "8080"
    haproxy/cookie-name: "SRVCOOKIE"
    haproxy/cookie-flags: "insert indirect nocache"
    haproxy/http-request: "http-request set-header X-Real-IP %[src]"
    haproxy/http-response: "http-response set-header X-Frame-Options DENY"
spec:
  ports:
    - port: 8080

HTTPRoute Annotations

Annotation Type Description Example
haproxy/enabled bool Enable this HTTPRoute for HAProxy config "true"
haproxy/frontend.v2 string Target HAProxy frontend name "ft_http"
haproxy/http-request string http-request directives for the frontendif <acl> is appended automatically "http-request redirect scheme https unless { ssl_fc }"
haproxy/http-response string http-response directives for the frontendif <acl> is appended automatically "http-response del-header Server"

HTTPRoute Example

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: myapp-route
  namespace: default
  annotations:
    haproxy/enabled: "true"
    haproxy/frontend.v2: "ft_http"
    haproxy/http-request: "http-request set-header X-Forwarded-Proto https"
    haproxy/http-response: "http-response set-header Strict-Transport-Security max-age=31536000"
spec:
  hostnames:
    - myapp.example.com
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /api
      filters:
        - type: RequestHeaderModifier
          requestHeaderModifier:
            set:
              - name: X-Internal
                value: "true"
            remove:
              - X-Debug
        - type: ResponseHeaderModifier
          responseHeaderModifier:
            set:
              - name: Cache-Control
                value: "no-store"
      backendRefs:
        - name: myapp      # must match Service name
          namespace: default
          port: 8080

Generated HAProxy config:

# In frontend ft_http:
acl K8S_default_myapp_acl_0 hdr(host) -i myapp.example.com
acl K8S_default_myapp_acl_1 path_beg /api
http-request set-header X-Internal true if K8S_default_myapp_acl_0 and K8S_default_myapp_acl_1
http-request del-header X-Debug if K8S_default_myapp_acl_0 and K8S_default_myapp_acl_1
http-request set-header X-Forwarded-Proto https if K8S_default_myapp_acl_0 and K8S_default_myapp_acl_1
http-response set-header Cache-Control no-store if K8S_default_myapp_acl_0 and K8S_default_myapp_acl_1
http-response set-header Strict-Transport-Security max-age=31536000 if K8S_default_myapp_acl_0 and K8S_default_myapp_acl_1
use_backend K8S_default_myapp if K8S_default_myapp_acl_0 and K8S_default_myapp_acl_1

# In backend section:
backend K8S_default_myapp
  mode http
  balance leastconn
  http-request set-header X-Real-IP %[src]
  http-response set-header X-Frame-Options DENY
  server myapp_a1b2c3d4 10.0.0.1:8080 check port 8080 cookie a1b2c3d4

Go Template

Jinja2 wrapper (Ansible creates haproxy.tmpl)

{% for frontend in haproxy_frontend %}
frontend {{ frontend.name }}

  {% for acl in frontend.acl | default([]) %}
  acl {{ acl.string }}
  {% endfor %}

  {% raw %}{{- $frontendName := "{% endraw %}{{ frontend.name }}{% raw %}"}}
  {{- range index .rulesByFrontend $frontendName }}
  {{- if .Acls }}
  {{- range .Acls }}
  acl {{ .Name }} {{ .Match }}
  {{- end }}
  {{- if .HttpRequest }}
  {{ .HttpRequest }}
  {{- end }}
  {{- if .HttpResponse }}
  {{ .HttpResponse }}
  {{- end }}
  use_backend {{ .Backend }} if {{ .UseBackendCond }}
  {{- end }}
  {{- end }}{% endraw %}

  {% if frontend.default_backend is defined %}
  default_backend {{ frontend.default_backend }}
  {% endif %}
{% endfor %}

{% raw %}{{- range .backends }}
backend {{ .Name }}
  mode {{ .Mode }}
  balance {{ .Balance }}
  {{- if .CookieName }}
  cookie {{ .CookieName }} {{ .CookieFlags }}
  {{- end }}
  {{- if .HealthCheck }}
  option httpchk GET {{ .HealthCheck }}
  http-check expect status 200
  {{- end }}
  {{- if .HttpRequest }}
  {{ .HttpRequest }}
  {{- end }}
  {{- if .HttpResponse }}
  {{ .HttpResponse }}
  {{- end }}
  {{- $backend := . }}
  {{- range .Servers }}
  server {{ .Name }} {{ .Address }}:{{ .Port }}{{ if $backend.HealthCheck }} check{{ end }}{{ if $backend.HealthCheckPort }} port {{ $backend.HealthCheckPort }}{{ end }} cookie {{ .Cookie }}{{ if $backend.ServerOptions }} {{ $backend.ServerOptions }}{{ end }}
  {{- end }}
{{- end }}{% endraw %}

Setup

Build

go build -o haproxy-generator .

Run manually

export KUBERNETES_HOST="https://10.0.20.7:6443"
export KUBERNETES_TOKEN="eyJhbGciOi..."
export KUBERNETES_VERIFYSSL="false"
export HAPROXY_TEMPLATE="./haproxy.tmpl"

./haproxy-generator > /etc/haproxy/haproxy.cfg && systemctl restart haproxy

Reload script (/etc/haproxy/haproxy-generator.sh)

#!/bin/bash
/usr/local/bin/haproxy-generator > /etc/haproxy/haproxy.cfg.new
DIFF=$(diff /etc/haproxy/haproxy.cfg /etc/haproxy/haproxy.cfg.new)
/sbin/haproxy -f /etc/haproxy/haproxy.cfg.new -c
VALID=$?

if [ "$DIFF" != "" ] && [ $VALID -eq 0 ]; then
    mv /etc/haproxy/haproxy.cfg.new /etc/haproxy/haproxy.cfg
    systemctl restart haproxy
fi

Systemd Service (/etc/systemd/system/haproxy-generator.service)

[Unit]
Description=HAProxy Config Generator
After=network.target

[Service]
Type=oneshot
Environment=KUBERNETES_HOST=https://10.0.20.7:6443
Environment=KUBERNETES_TOKEN=eyJhbGciOi...
Environment=KUBERNETES_VERIFYSSL=false
Environment=HAPROXY_TEMPLATE=/etc/haproxy/haproxy.tmpl
ExecStart=/bin/bash /etc/haproxy/haproxy-generator.sh

Systemd Timer (/etc/systemd/system/haproxy-generator.timer)

[Unit]
Description=Run HAProxy Config Generator every minute

[Timer]
OnBootSec=1min
OnUnitActiveSec=1min
Unit=haproxy-generator.service
Persistent=true

[Install]
WantedBy=timers.target
systemctl daemon-reload
systemctl enable --now haproxy-generator.timer

Tests

go test ./... -v