Support limit-rpm annotation for ingress-nginx

This commit is contained in:
Pierre Porée
2026-03-06 15:42:04 +01:00
committed by GitHub
parent f3413f840a
commit 469ee709d1
6 changed files with 165 additions and 2 deletions
@@ -381,6 +381,7 @@ The following annotations are organized by category for easier navigation.
| Annotation | Limitations / Notes |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |-----------------------------------------------------------------------------------------------------------|
| <a id="opt-nginx-ingress-kubernetes-iolimit-rps" href="#opt-nginx-ingress-kubernetes-iolimit-rps" title="#opt-nginx-ingress-kubernetes-iolimit-rps">`nginx.ingress.kubernetes.io/limit-rps`</a> | Exceeding the limit returns `429 Too Many Requests` instead of NGINX's default `503 Service Unavailable`. |
| <a id="opt-nginx-ingress-kubernetes-iolimit-rpm" href="#opt-nginx-ingress-kubernetes-iolimit-rpm" title="#opt-nginx-ingress-kubernetes-iolimit-rpm">`nginx.ingress.kubernetes.io/limit-rpm`</a> | Exceeding the limit returns `429 Too Many Requests` instead of NGINX's default `503 Service Unavailable`. |
### Buffering
@@ -453,8 +454,7 @@ In practice, Traefik is slightly more lenient under bursty load, as it smooths o
| <a id="opt-nginx-ingress-kubernetes-iodisable-proxy-intercept-errors" href="#opt-nginx-ingress-kubernetes-iodisable-proxy-intercept-errors" title="#opt-nginx-ingress-kubernetes-iodisable-proxy-intercept-errors">`nginx.ingress.kubernetes.io/disable-proxy-intercept-errors`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iolimit-rate-after" href="#opt-nginx-ingress-kubernetes-iolimit-rate-after" title="#opt-nginx-ingress-kubernetes-iolimit-rate-after">`nginx.ingress.kubernetes.io/limit-rate-after`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iolimit-rate" href="#opt-nginx-ingress-kubernetes-iolimit-rate" title="#opt-nginx-ingress-kubernetes-iolimit-rate">`nginx.ingress.kubernetes.io/limit-rate`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iolimit-whitelist" href="#opt-nginx-ingress-kubernetes-iolimit-whitelist" title="#opt-nginx-ingress-kubernetes-iolimit-whitelist">`nginx.ingress.kubernetes.io/limit-whitelist`</a> | | |
| <a id="opt-nginx-ingress-kubernetes-iolimit-rpm" href="#opt-nginx-ingress-kubernetes-iolimit-rpm" title="#opt-nginx-ingress-kubernetes-iolimit-rpm">`nginx.ingress.kubernetes.io/limit-rpm`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iolimit-whitelist" href="#opt-nginx-ingress-kubernetes-iolimit-whitelist" title="#opt-nginx-ingress-kubernetes-iolimit-whitelist">`nginx.ingress.kubernetes.io/limit-whitelist`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iolimit-burst-multiplier" href="#opt-nginx-ingress-kubernetes-iolimit-burst-multiplier" title="#opt-nginx-ingress-kubernetes-iolimit-burst-multiplier">`nginx.ingress.kubernetes.io/limit-burst-multiplier`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iolimit-connections" href="#opt-nginx-ingress-kubernetes-iolimit-connections" title="#opt-nginx-ingress-kubernetes-iolimit-connections">`nginx.ingress.kubernetes.io/limit-connections`</a> | |
| <a id="opt-nginx-ingress-kubernetes-ioglobal-rate-limit" href="#opt-nginx-ingress-kubernetes-ioglobal-rate-limit" title="#opt-nginx-ingress-kubernetes-ioglobal-rate-limit">`nginx.ingress.kubernetes.io/global-rate-limit`</a> | |
@@ -80,6 +80,7 @@ type ingressConfig struct {
WhitelistSourceRange *string `annotation:"nginx.ingress.kubernetes.io/whitelist-source-range"`
AllowlistSourceRange *string `annotation:"nginx.ingress.kubernetes.io/allowlist-source-range"`
LimitRPM *int `annotation:"nginx.ingress.kubernetes.io/limit-rpm"`
LimitRPS *int `annotation:"nginx.ingress.kubernetes.io/limit-rps"`
CustomHeaders *string `annotation:"nginx.ingress.kubernetes.io/custom-headers"`
@@ -37,6 +37,7 @@ func Test_parseIngressConfig(t *testing.T) {
"nginx.ingress.kubernetes.io/proxy-buffer-size": "16k",
"nginx.ingress.kubernetes.io/proxy-buffers-number": "8",
"nginx.ingress.kubernetes.io/proxy-max-temp-file-size": "100m",
"nginx.ingress.kubernetes.io/limit-rpm": "120",
},
expected: ingressConfig{
SSLPassthrough: ptr.To(true),
@@ -59,6 +60,7 @@ func Test_parseIngressConfig(t *testing.T) {
ProxyBufferSize: ptr.To("16k"),
ProxyBuffersNumber: ptr.To(8),
ProxyMaxTempFileSize: ptr.To("100m"),
LimitRPM: ptr.To(120),
},
},
{
@@ -0,0 +1,42 @@
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-with-limit-rpm
namespace: default
annotations:
nginx.ingress.kubernetes.io/limit-rpm: "10"
spec:
ingressClassName: nginx
rules:
- host: whoami.localhost
http:
paths:
- path: /
pathType: Exact
backend:
service:
name: whoami
port:
number: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-with-limit-rpm-zero
namespace: default
annotations:
nginx.ingress.kubernetes.io/limit-rpm: "0"
spec:
ingressClassName: nginx
rules:
- host: whoami-zero.localhost
http:
paths:
- path: /
pathType: Exact
backend:
service:
name: whoami
port:
number: 80
@@ -1203,6 +1203,8 @@ func (p *Provider) applyMiddlewares(namespace, ingressName, routerKey, rulePath,
applyUpstreamVhost(routerKey, ingressConfig, rt, conf)
applyLimitRPMConfiguration(routerKey, ingressConfig, rt, conf)
applyLimitRPSConfiguration(routerKey, ingressConfig, rt, conf)
if err := p.applyAuthTLSPassCertificateToUpstream(namespace, routerKey, ingressConfig, rt, conf); err != nil {
@@ -1307,6 +1309,24 @@ func (p *Provider) applyCustomHTTPErrors(namespace, ingressName, routerName stri
return nil
}
func applyLimitRPMConfiguration(routerName string, ingressConfig ingressConfig, rt *dynamic.Router, conf *dynamic.Configuration) {
limitRPM := ptr.Deref(ingressConfig.LimitRPM, 0)
if limitRPM <= 0 {
return
}
rateLimitMiddlewareName := routerName + "-limit-rpm"
conf.HTTP.Middlewares[rateLimitMiddlewareName] = &dynamic.Middleware{
RateLimit: &dynamic.RateLimit{
Average: int64(limitRPM),
Period: ptypes.Duration(time.Minute),
Burst: int64(limitRPM) * defaultLimitBurstMultiplier,
},
}
rt.Middlewares = append(rt.Middlewares, rateLimitMiddlewareName)
}
func applyLimitRPSConfiguration(routerName string, ingressConfig ingressConfig, rt *dynamic.Router, conf *dynamic.Configuration) {
limitRPS := ptr.Deref(ingressConfig.LimitRPS, 0)
if limitRPS <= 0 {
@@ -6638,6 +6638,104 @@ func TestLoadIngresses(t *testing.T) {
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Limit RPM",
paths: []string{
"services.yml",
"ingressclasses.yml",
"ingresses/ingress-with-limit-rpm.yml",
},
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Services: map[string]*dynamic.TCPService{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"default-ingress-with-limit-rpm-rule-0-path-0": {
Rule: "Host(`whoami.localhost`) && Path(`/`)",
RuleSyntax: "default",
Middlewares: []string{"default-ingress-with-limit-rpm-rule-0-path-0-limit-rpm", "default-ingress-with-limit-rpm-rule-0-path-0-retry"},
Service: "default-ingress-with-limit-rpm-whoami-80",
},
"default-ingress-with-limit-rpm-zero-rule-0-path-0": {
Rule: "Host(`whoami-zero.localhost`) && Path(`/`)",
RuleSyntax: "default",
Middlewares: []string{"default-ingress-with-limit-rpm-zero-rule-0-path-0-retry"},
Service: "default-ingress-with-limit-rpm-zero-whoami-80",
},
},
Middlewares: map[string]*dynamic.Middleware{
"default-ingress-with-limit-rpm-rule-0-path-0-retry": {
Retry: &dynamic.Retry{
Attempts: 3,
},
},
"default-ingress-with-limit-rpm-rule-0-path-0-limit-rpm": {
RateLimit: &dynamic.RateLimit{
Average: 10,
Burst: 50,
Period: ptypes.Duration(time.Minute),
},
},
"default-ingress-with-limit-rpm-zero-rule-0-path-0-retry": {
Retry: &dynamic.Retry{
Attempts: 3,
},
},
},
Services: map[string]*dynamic.Service{
"default-ingress-with-limit-rpm-whoami-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{URL: "http://10.10.0.1:80"},
{URL: "http://10.10.0.2:80"},
},
Strategy: "wrr",
PassHostHeader: ptr.To(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: dynamic.DefaultFlushInterval,
},
ServersTransport: "default-ingress-with-limit-rpm",
},
},
"default-ingress-with-limit-rpm-zero-whoami-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{URL: "http://10.10.0.1:80"},
{URL: "http://10.10.0.2:80"},
},
Strategy: "wrr",
PassHostHeader: ptr.To(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: dynamic.DefaultFlushInterval,
},
ServersTransport: "default-ingress-with-limit-rpm-zero",
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{
"default-ingress-with-limit-rpm": {
ForwardingTimeouts: &dynamic.ForwardingTimeouts{
DialTimeout: ptypes.Duration(60 * time.Second),
ReadTimeout: ptypes.Duration(60 * time.Second),
WriteTimeout: ptypes.Duration(60 * time.Second),
IdleConnTimeout: ptypes.Duration(60 * time.Second),
},
},
"default-ingress-with-limit-rpm-zero": {
ForwardingTimeouts: &dynamic.ForwardingTimeouts{
DialTimeout: ptypes.Duration(60 * time.Second),
ReadTimeout: ptypes.Duration(60 * time.Second),
WriteTimeout: ptypes.Duration(60 * time.Second),
IdleConnTimeout: ptypes.Duration(60 * time.Second),
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
}
for _, test := range testCases {