- Go 86.1%
- Python 7.2%
- Shell 4.9%
- Jinja 1.8%
|
All checks were successful
Build Go Binary / build (push) Successful in 49s
Reviewed-on: #2 |
||
|---|---|---|
| .forgejo/workflows | ||
| .gitignore | ||
| gateway-config-cron | ||
| gateway-haproxy-config.py | ||
| generate_kubeconfig.sh | ||
| go.mod | ||
| go_cron_bash.sh | ||
| haproxy-ingress-configmap.yaml | ||
| haproxy-ingress-rbac.yaml | ||
| haproxy.j2 | ||
| LICENSE | ||
| main.go | ||
| main_test.go | ||
| README.md | ||
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.hostnames→acl hdr(host) -i <hostname>spec.rules[].matches[].path→acl path_beg / path / path_reg <value>spec.rules[].matches[].headers→acl 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 frontend — if <acl> is appended automatically |
"http-request redirect scheme https unless { ssl_fc }" |
haproxy/http-response |
string | http-response directives for the frontend — if <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