Ingress tutorials
Add user-defined annotations
This page applies to:
- HAProxy Kubernetes Ingress Controller 3.2 and newer
- HAProxy Enterprise Kubernetes Ingress Controller 3.2 and newer
With user-defined annotations, you can extend the functionality of HAProxy Kubernetes Ingress Controller in custom ways. User-defined annotations attach to the metadata of your Kubernetes resources and are translated into one or many lines of HAProxy configuration. They can be simple, but they can also enable advanced use cases through templating expressions. You can restrict which namespaces, resource kinds, and configuration sections your annotations are available to, and you can define validation logic that ensures users set your annotations correctly.
Prerequisites Jump to heading
Before continuing, ensure that you’ve performed these steps:
-
Install the ValidationRules custom resource definition:
- Install the Community ValidationRules custom resource definition.
- Install the Enterprise ValidationRules custom resource definition.
-
When deploying the ingress controller, add the startup argument
--custom-validation-rules=<namespace>/<validationrules-name>, wherevalidationrules-nameis the name you’ll assign to your ValidationRules custom resource. With Helm, you can set it withextraArgs, as shown in this example:myvals.yamlyamlcontroller:extraArgs:- --custom-validation-rules=haproxy-controller/example-annotationsmyvals.yamlyamlcontroller:extraArgs:- --custom-validation-rules=haproxy-controller/example-annotations
Add a backend annotation Jump to heading
Create a ValidationRules resource to contain the definitions of your user-defined annotations for backends.
-
Add a ValidationRules custom resource.
example-annotations.yamlyamlapiVersion: ingress.v3.haproxy.org/v3kind: ValidationRulesmetadata:name: example-annotationsnamespace: haproxy-controllerspec:prefix: "example.com"validation_rules:retries:section: backendtype: intrule: "value >= 1 && value <= 10"example-annotations.yamlyamlapiVersion: ingress.v3.haproxy.org/v3kind: ValidationRulesmetadata:name: example-annotationsnamespace: haproxy-controllerspec:prefix: "example.com"validation_rules:retries:section: backendtype: intrule: "value >= 1 && value <= 10"prefixindicates the prefix for your annotations. It’s best to use your organization’s domain, such asexample.com. When using your custom annotation, you’ll usebackend.<prefix>/<annotation-name>. By default, user-defined annotations apply to backends, but you can change that by setting thesectionfield toall,frontend, orbackend.validation_rulescontains one or more user-defined annotations. In this example, we’re creating an annotation namedretriesthat can have an integer value between 1 and 10. Use the variablevaluein expressions. This validation logic is implemented via Common Expression Language. You can add more annotations beneathvalidation_rules.
Annotations that set
sectiontobackendcan be attached to Service resources, Ingress resources, and thehaproxy-kubernetes-ingressConfigMap. They take precendence in that order.- When attached to a Service resource, annotations apply to the associated backend.
- When attached to the ConfigMap resource, annotations apply to all backends.
- When attached to an Ingress resource, annotations apply to the associated backend. We don’t recommend this because multiple Ingress resources can apply to the same HAProxy backend, presenting the chance of conflicting annotations. To use annotations on Ingress resources, you must set the startup argument
--enable-custom-annotations-on-ingress.
-
Apply the changes with
kubectl.nixkubectl apply -f example-annotations.yamlnixkubectl apply -f example-annotations.yamloutputtextvalidationrules.ingress.v3.haproxy.org/example-annotations createdoutputtextvalidationrules.ingress.v3.haproxy.org/example-annotations created -
Use the annotation on a Service resource:
example-service.yamlyamlkind: ServiceapiVersion: v1metadata:name: http-echoannotations:backend.example.com/retries: "3"...example-service.yamlyamlkind: ServiceapiVersion: v1metadata:name: http-echoannotations:backend.example.com/retries: "3"...nixkubectl apply -f example-service.yamlnixkubectl apply -f example-service.yamlThe ingress controller logs will show the update.
nixkubectl logs -n haproxy-controller <haproxy-kubernetes-ingress pod>nixkubectl logs -n haproxy-controller <haproxy-kubernetes-ingress pod>outputtext2025/12/11 17:54:18 INFO annotations/cfgSnippetHandler.go:105 [transactionID=43d89ca1-999d-40cf-beb0-d5add7f6286d] reload required : config snippet from {example.com/retries %!s(int=2)} has been updatedoutputtext2025/12/11 17:54:18 INFO annotations/cfgSnippetHandler.go:105 [transactionID=43d89ca1-999d-40cf-beb0-d5add7f6286d] reload required : config snippet from {example.com/retries %!s(int=2)} has been updatedThe generated HAProxy configuration will add
retriesto the backend associated with the Service resource.haproxybackend default_svc_http-echo_http...###_config-snippet_### BEGIN## example.com/retries ###retries 3###_config-snippet_### ENDhaproxybackend default_svc_http-echo_http...###_config-snippet_### BEGIN## example.com/retries ###retries 3###_config-snippet_### ENDIf there was a validation error, the line will be commented out, with the error included.
haproxybackend default_svc_myapp_http...###_config-snippet_### BEGIN### example.com/retries #### ERROR: invalid integer format for 'three': strconv.ParseInt: parsing "three": invalid syntax###_config-snippet_### ENDhaproxybackend default_svc_myapp_http...###_config-snippet_### BEGIN### example.com/retries #### ERROR: invalid integer format for 'three': strconv.ParseInt: parsing "three": invalid syntax###_config-snippet_### END
Add a frontend annotation Jump to heading
Create a ValidationRules resource to contain the definitions of your user-defined annotations for frontends.
-
Add a ValidationRules custom resource.
example-annotations.yamlyamlapiVersion: ingress.v3.haproxy.org/v3kind: ValidationRulesmetadata:name: example-annotationsnamespace: haproxy-controllerspec:prefix: "example.com"validation_rules:maxconn:section: frontendtype: intresources:- http- httpsrule: "value >= 10 && value <= 1000000"example-annotations.yamlyamlapiVersion: ingress.v3.haproxy.org/v3kind: ValidationRulesmetadata:name: example-annotationsnamespace: haproxy-controllerspec:prefix: "example.com"validation_rules:maxconn:section: frontendtype: intresources:- http- httpsrule: "value >= 10 && value <= 1000000"prefixindicates the prefix for your annotations. It’s best to use your organization’s domain, such asexample.com. When using your custom annotation, you’ll usefrontend.<prefix>/<annotation-name>.resourcessets the frontends your annotation will apply to. Frontend names arehttp,https, andstats.validation_rulescontains one or more user-defined annotations. In this example, we’re creating an annotation namedmaxconnthat can have an integer value between 10 and 1000000. Use the variablevaluein expressions. This validation logic is implemented via Common Expression Language. You can add more annotations beneathvalidation_rules.
Annotations that set
sectiontofrontendcan be attached to thehaproxy-kubernetes-ingressConfigMap. -
Apply the changes with
kubectl.nixkubectl apply -f example-annotations.yamlnixkubectl apply -f example-annotations.yamloutputtextvalidationrules.ingress.v3.haproxy.org/example-annotations createdoutputtextvalidationrules.ingress.v3.haproxy.org/example-annotations created -
To add your annotation to the ConfigMap, you can either:
-
Add the annotation using
kubectl annotate:nixkubectl annotate configmap haproxy-kubernetes-ingress --namespace haproxy-controller frontend.example.com/maxconn=10000nixkubectl annotate configmap haproxy-kubernetes-ingress --namespace haproxy-controller frontend.example.com/maxconn=10000 -
Or, use
kubectl editto modify the ConfigMap:nixkubectl edit configmap haproxy-kubernetes-ingress --namespace haproxy-controllernixkubectl edit configmap haproxy-kubernetes-ingress --namespace haproxy-controllerUse the annotation on the ConfigMap, then save the file.
configmap.yamlyamlapiVersion: v1kind: ConfigMapmetadata:name: haproxy-kubernetes-ingressnamespace: haproxy-controllerannotations:frontend.example.com/maxconn: "10000"configmap.yamlyamlapiVersion: v1kind: ConfigMapmetadata:name: haproxy-kubernetes-ingressnamespace: haproxy-controllerannotations:frontend.example.com/maxconn: "10000"
The ingress controller logs will show the update.
nixkubectl logs -n haproxy-controller <haproxy-kubernetes-ingress pod>nixkubectl logs -n haproxy-controller <haproxy-kubernetes-ingress pod>outputtext2025/12/12 21:00:45 INFO controller/global.go:123 [transactionID=abae4313-eb2c-4a33-b881-8e315edf992d] reload required : Frontend config-snippet updated: <nil slice> != [### example.com/maxconn ### maxconn 10000]outputtext2025/12/12 21:00:45 INFO controller/global.go:123 [transactionID=abae4313-eb2c-4a33-b881-8e315edf992d] reload required : Frontend config-snippet updated: <nil slice> != [### example.com/maxconn ### maxconn 10000]The generated HAProxy configuration will add
maxconnto the frontend.haproxyfrontend http...###_config-snippet_### BEGIN## example.com/maxconn ###maxconn 10000###_config-snippet_### ENDhaproxyfrontend http...###_config-snippet_### BEGIN## example.com/maxconn ###maxconn 10000###_config-snippet_### ENDIf there was a validation error, the line will be commented out, with the error included.
haproxy###_config-snippet_### BEGIN### example.com/maxconn #### ERROR: validation failed for rule 'maxconn' with value '0'.# Failed part: 'value >= 10'### custom annotations end ######_config-snippet_### ENDhaproxy###_config-snippet_### BEGIN### example.com/maxconn #### ERROR: validation failed for rule 'maxconn' with value '0'.# Failed part: 'value >= 10'### custom annotations end ######_config-snippet_### END -
Syntax Jump to heading
The validation_rules section in a ValidationRules resource supports the following syntax.
yamltimeout-server: # name of annotationsection: all # can be all, frontend, backend (default)namespaces: # we can limit namespace usage- default- dev- prodresources: # limit usage to Service, Frontend or Backend names (list)- myserviceingresses: # limit usage to specific ingresses- myingressorder_priority: 100 # order of user annotations in config. higher is more prioritytemplate: "timeout server {{.}}" # template we can use (golang templates)type: duration # expected data type for conversion (duration;int;uint;bool;string;float;json;)rule: "value > duration('42s') && value <= duration('42m')" # CEL expression
yamltimeout-server: # name of annotationsection: all # can be all, frontend, backend (default)namespaces: # we can limit namespace usage- default- dev- prodresources: # limit usage to Service, Frontend or Backend names (list)- myserviceingresses: # limit usage to specific ingresses- myingressorder_priority: 100 # order of user annotations in config. higher is more prioritytemplate: "timeout server {{.}}" # template we can use (golang templates)type: duration # expected data type for conversion (duration;int;uint;bool;string;float;json;)rule: "value > duration('42s') && value <= duration('42m')" # CEL expression
Examples Jump to heading
In this section, you’ll find other examples of creating user-defined annotations.
Integer Jump to heading
Here, we define an annotation named max-keep-alive-queue that expects an integer value. You can use the variable value to perform validation logic. In this example, we enforce the rule that the value must be between 10 and 1000000.
example-annotations.yamlyamlapiVersion: ingress.v3.haproxy.org/v3kind: ValidationRulesmetadata:name: example-annotationsnamespace: haproxy-controllerspec:prefix: "example.com"validation_rules:max-keep-alive-queue:section: backendtype: intrule: "value >= 1 && value <= 100"
example-annotations.yamlyamlapiVersion: ingress.v3.haproxy.org/v3kind: ValidationRulesmetadata:name: example-annotationsnamespace: haproxy-controllerspec:prefix: "example.com"validation_rules:max-keep-alive-queue:section: backendtype: intrule: "value >= 1 && value <= 100"
Usage:
example-service.yamlyamlkind: ServiceapiVersion: v1metadata:name: http-echoannotations:backend.example.com/max-keep-alive-queue: "10"...
example-service.yamlyamlkind: ServiceapiVersion: v1metadata:name: http-echoannotations:backend.example.com/max-keep-alive-queue: "10"...
Boolean Jump to heading
Booleans work well for HAProxy directives that don’t accept any arguments, such as option forwardfor. In this example, we use template to control the generated configuration line. The templating language is implemented via Golang templates. The annotation can be set to true only.
example-annotations.yamlyamlapiVersion: ingress.v3.haproxy.org/v3kind: ValidationRulesmetadata:name: example-annotationsnamespace: haproxy-controllerspec:prefix: "example.com"validation_rules:option-forwardfor:section: backendtype: booltemplate: "option forwardfor"rule: "value == true"
example-annotations.yamlyamlapiVersion: ingress.v3.haproxy.org/v3kind: ValidationRulesmetadata:name: example-annotationsnamespace: haproxy-controllerspec:prefix: "example.com"validation_rules:option-forwardfor:section: backendtype: booltemplate: "option forwardfor"rule: "value == true"
Usage:
example-service.yamlyamlkind: ServiceapiVersion: v1metadata:name: http-echoannotations:backend.example.com/option-forwardfor: "true"...
example-service.yamlyamlkind: ServiceapiVersion: v1metadata:name: http-echoannotations:backend.example.com/option-forwardfor: "true"...
Time duration Jump to heading
Some HAProxy directives expect a value indicating a time duration. The rule in this example uses the duration function to check that the value is between 1 minute and 1 hour. Consult the HAProxy Configuration Manual entry for Time format, but note that HAProxy supports days with d, but Common Expression Language doesn’t, so d isn’t allowed here.
example-annotations.yamlyamlapiVersion: ingress.v3.haproxy.org/v3kind: ValidationRulesmetadata:name: example-annotationsnamespace: haproxy-controllerspec:prefix: "example.com"validation_rules:timeout-tunnel:section: backendtype: durationtemplate: "timeout tunnel {{.}}"rule: "value > duration('1m') && value <= duration('1h')"
example-annotations.yamlyamlapiVersion: ingress.v3.haproxy.org/v3kind: ValidationRulesmetadata:name: example-annotationsnamespace: haproxy-controllerspec:prefix: "example.com"validation_rules:timeout-tunnel:section: backendtype: durationtemplate: "timeout tunnel {{.}}"rule: "value > duration('1m') && value <= duration('1h')"
Usage:
example-service.yamlyamlkind: ServiceapiVersion: v1metadata:name: http-echoannotations:backend.example.com/timeout-tunnel: "30m"...
example-service.yamlyamlkind: ServiceapiVersion: v1metadata:name: http-echoannotations:backend.example.com/timeout-tunnel: "30m"...
JSON object Jump to heading
When you set type to json, your annotation can accept a JSON object. This allows you to set multiple values by referring to the keys of a JSON object in template, such as {{.hdr}} and {{.domain}} in this example.
Also in this example, we define a rule that makes those JSON fields mandatory through the in keyword and performs other validation, including checking against regular expressions that the given .domain value is either a domain name or an IPv4 address.
example-annotations.yamlyamlapiVersion: ingress.v3.haproxy.org/v3kind: ValidationRulesmetadata:name: example-annotationsnamespace: haproxy-controllerspec:prefix: "example.com"validation_rules:http-request-set-header-X-Request-ID:section: backendtype: jsontemplate: "http-request set-header X-Request-ID %[unique-id] if { hdr({{.hdr}}) -i {{.domain}} }"rule: "'hdr' in value && 'domain' in value && ((value.hdr == 'host' && value.domain.matches('^([a-zA-Z0-9-]+\\\\.)+[a-zA-Z]{2,}$')) || (value.hdr == 'ip' && value.domain.matches('^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$')))"
example-annotations.yamlyamlapiVersion: ingress.v3.haproxy.org/v3kind: ValidationRulesmetadata:name: example-annotationsnamespace: haproxy-controllerspec:prefix: "example.com"validation_rules:http-request-set-header-X-Request-ID:section: backendtype: jsontemplate: "http-request set-header X-Request-ID %[unique-id] if { hdr({{.hdr}}) -i {{.domain}} }"rule: "'hdr' in value && 'domain' in value && ((value.hdr == 'host' && value.domain.matches('^([a-zA-Z0-9-]+\\\\.)+[a-zA-Z]{2,}$')) || (value.hdr == 'ip' && value.domain.matches('^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$')))"
Usage:
example-service.yamlyamlkind: ServiceapiVersion: v1metadata:name: http-echoannotations:backend.example.com/http-request-set-header-X-Request-ID: '{"hdr":"host", "domain":"example.com"}'...
example-service.yamlyamlkind: ServiceapiVersion: v1metadata:name: http-echoannotations:backend.example.com/http-request-set-header-X-Request-ID: '{"hdr":"host", "domain":"example.com"}'...
Or set it as a multi-line string:
example-service.yamlyamlkind: ServiceapiVersion: v1metadata:name: http-echoannotations:backend.example.com/http-request-set-header-X-Request-ID: |{"hdr":"host","domain":"example.com"}...
example-service.yamlyamlkind: ServiceapiVersion: v1metadata:name: http-echoannotations:backend.example.com/http-request-set-header-X-Request-ID: |{"hdr":"host","domain":"example.com"}...
This will generate:
haproxybackend default_svc_http-echo_http...###_config-snippet_### BEGIN## example.com/http-request-set-header-X-Request-ID ###http-request set-header X-Request-ID %[unique-id] if { hdr(host) -i example.com }###_config-snippet_### END
haproxybackend default_svc_http-echo_http...###_config-snippet_### BEGIN## example.com/http-request-set-header-X-Request-ID ###http-request set-header X-Request-ID %[unique-id] if { hdr(host) -i example.com }###_config-snippet_### END
Multiple lines Jump to heading
To generate multiple lines of HAProxy configuration, add line breaks to your template.
example-annotations.yamlyamlapiVersion: ingress.v3.haproxy.org/v3kind: ValidationRulesmetadata:name: example-annotationsnamespace: haproxy-controllerspec:prefix: "example.com"validation_rules:timeouts:section: backendtype: jsontemplate: |timeout server {{.server}}timeout server-fin {{.server_fin}}timeout tarpit {{.tarpit}}rule: |'server' in value && value.server.matches('^[0-9]+[smh]?$') &&'server_fin' in value && value.server_fin.matches('^[0-9]+[smh]?$') &&'tarpit' in value && value.tarpit.matches('^[0-9]+[smh]?$')
example-annotations.yamlyamlapiVersion: ingress.v3.haproxy.org/v3kind: ValidationRulesmetadata:name: example-annotationsnamespace: haproxy-controllerspec:prefix: "example.com"validation_rules:timeouts:section: backendtype: jsontemplate: |timeout server {{.server}}timeout server-fin {{.server_fin}}timeout tarpit {{.tarpit}}rule: |'server' in value && value.server.matches('^[0-9]+[smh]?$') &&'server_fin' in value && value.server_fin.matches('^[0-9]+[smh]?$') &&'tarpit' in value && value.tarpit.matches('^[0-9]+[smh]?$')
Usage:
example-service.yamlyamlkind: ServiceapiVersion: v1metadata:name: http-echoannotations:backend.example.com/timeouts: |{"server": "42s","server_fin": "10s","tarpit": "5s"}...
example-service.yamlyamlkind: ServiceapiVersion: v1metadata:name: http-echoannotations:backend.example.com/timeouts: |{"server": "42s","server_fin": "10s","tarpit": "5s"}...
This will generate:
haproxybackend default_svc_http-echo_http...###_config-snippet_### BEGIN### example.com/timeouts ###timeout server 42stimeout server-fin 10stimeout tarpit 5s###_config-snippet_### END
haproxybackend default_svc_http-echo_http...###_config-snippet_### BEGIN### example.com/timeouts ###timeout server 42stimeout server-fin 10stimeout tarpit 5s###_config-snippet_### END
Predefined template variables Jump to heading
When using the json type, you get access to predefined template variables that are derived from the ingress controller’s environment.
| Variable | Description |
|---|---|
BACKEND |
The name of the backend in the HAProxy configuration. |
INGRESS |
The name of the Ingress resource. |
NAMESPACE |
The Ingress resource’s namespace. |
POD_IP |
The pod’s cluster IP address from status.podIP. |
POD_NAME |
The pod’s name from metadata.name. |
POD_NAMESPACE |
The pod’s namespace from metadata.namespace. |
SERVICE |
The name of the Service resource. |
Let’s use these variables along with others that we define to set a preamble above the generated configuration.
example-annotations.yamlyamlapiVersion: ingress.v3.haproxy.org/v3kind: ValidationRulesmetadata:name: example-annotationsnamespace: haproxy-controllerspec:prefix: "example.com"validation_rules:retries:section: backendtype: jsontemplate: |# ==============================================# user annotation, owner: {{.owner}} - Reason: {{.reason}} for {{.BACKEND}}# namespace {{.NAMESPACE}}, ingress {{.INGRESS}}, service {{.SERVICE}}# POD_NAME {{.POD_NAME}}, POD_NAMESPACE {{.POD_NAMESPACE}}, POD_IP {{.POD_IP}}# ==============================================retries {{.retries}}# ==============================================rule: |'owner' in value &&'reason' in value &&'retries' in value && int(value.retries) >= 1 && int(value.retries) <= 10
example-annotations.yamlyamlapiVersion: ingress.v3.haproxy.org/v3kind: ValidationRulesmetadata:name: example-annotationsnamespace: haproxy-controllerspec:prefix: "example.com"validation_rules:retries:section: backendtype: jsontemplate: |# ==============================================# user annotation, owner: {{.owner}} - Reason: {{.reason}} for {{.BACKEND}}# namespace {{.NAMESPACE}}, ingress {{.INGRESS}}, service {{.SERVICE}}# POD_NAME {{.POD_NAME}}, POD_NAMESPACE {{.POD_NAMESPACE}}, POD_IP {{.POD_IP}}# ==============================================retries {{.retries}}# ==============================================rule: |'owner' in value &&'reason' in value &&'retries' in value && int(value.retries) >= 1 && int(value.retries) <= 10
Usage:
example-service.yamlyamlkind: ServiceapiVersion: v1metadata:name: http-echoannotations:backend.example.com/retries: '{ "retries": 3, "owner": "jsmith", "reason": "user annotation demo" }'
example-service.yamlyamlkind: ServiceapiVersion: v1metadata:name: http-echoannotations:backend.example.com/retries: '{ "retries": 3, "owner": "jsmith", "reason": "user annotation demo" }'
This will generate:
haproxybackend default_svc_http-echo_http...###_config-snippet_### BEGIN### example.com/retries #### ==============================================# user annotation, owner: jsmith - Reason: user annotation demo for default_svc_http-echo_http# namespace default, ingress example-ingress, service http-echo# POD_NAME haproxy-kubernetes-ingress-64c4ddbc9b-bdq5c, POD_NAMESPACE haproxy-controller, POD_IP 10.244.0.5# ==============================================retries 3# ==============================================###_config-snippet_### END
haproxybackend default_svc_http-echo_http...###_config-snippet_### BEGIN### example.com/retries #### ==============================================# user annotation, owner: jsmith - Reason: user annotation demo for default_svc_http-echo_http# namespace default, ingress example-ingress, service http-echo# POD_NAME haproxy-kubernetes-ingress-64c4ddbc9b-bdq5c, POD_NAMESPACE haproxy-controller, POD_IP 10.244.0.5# ==============================================retries 3# ==============================================###_config-snippet_### END