From ea7f300c85a6ea7a20be185c5fec54deb7d3a0e2 Mon Sep 17 00:00:00 2001 From: Kangmin Kim <76634341+amazon7737@users.noreply.github.com> Date: Wed, 1 Apr 2026 00:24:07 +0900 Subject: [PATCH] feat(provider/k8s/ingress-nginx): add limit-burst-multiplier annotation support --- .../kubernetes/ingress-nginx.md | 2 +- .../kubernetes/ingress-nginx/annotations.go | 5 +- .../ingress-with-limit-burst-multiplier.yml | 43 +++++++++ .../kubernetes/ingress-nginx/kubernetes.go | 14 ++- .../ingress-nginx/kubernetes_test.go | 92 +++++++++++++++++++ 5 files changed, 151 insertions(+), 5 deletions(-) create mode 100644 pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-limit-burst-multiplier.yml diff --git a/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md b/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md index 94ea3a2fff..b52ce31983 100644 --- a/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md +++ b/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md @@ -384,6 +384,7 @@ The following annotations are organized by category for easier navigation. | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |-----------------------------------------------------------------------------------------------------------| | `nginx.ingress.kubernetes.io/limit-rps` | Exceeding the limit returns `429 Too Many Requests` instead of NGINX's default `503 Service Unavailable`. | | `nginx.ingress.kubernetes.io/limit-rpm` | Exceeding the limit returns `429 Too Many Requests` instead of NGINX's default `503 Service Unavailable`. | +| `nginx.ingress.kubernetes.io/limit-burst-multiplier` | Default to a multiplier of 5 if the configured value is less than 1. Exceeding the limit returns `429 Too Many Requests` instead of NGINX's default `503 Service Unavailable`. | ### Buffering @@ -456,7 +457,6 @@ In practice, Traefik is slightly more lenient under bursty load, as it smooths o | `nginx.ingress.kubernetes.io/limit-rate-after` | | | `nginx.ingress.kubernetes.io/limit-rate` | | | `nginx.ingress.kubernetes.io/limit-whitelist` | | -| `nginx.ingress.kubernetes.io/limit-burst-multiplier` | | | `nginx.ingress.kubernetes.io/limit-connections` | | | `nginx.ingress.kubernetes.io/global-rate-limit` | | | `nginx.ingress.kubernetes.io/global-rate-limit-window` | | diff --git a/pkg/provider/kubernetes/ingress-nginx/annotations.go b/pkg/provider/kubernetes/ingress-nginx/annotations.go index e15d170b8e..8a1d2da951 100644 --- a/pkg/provider/kubernetes/ingress-nginx/annotations.go +++ b/pkg/provider/kubernetes/ingress-nginx/annotations.go @@ -84,8 +84,9 @@ 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"` + LimitRPM *int `annotation:"nginx.ingress.kubernetes.io/limit-rpm"` + LimitRPS *int `annotation:"nginx.ingress.kubernetes.io/limit-rps"` + LimitBurstMultiplier *int `annotation:"nginx.ingress.kubernetes.io/limit-burst-multiplier"` CustomHeaders *string `annotation:"nginx.ingress.kubernetes.io/custom-headers"` UpstreamVhost *string `annotation:"nginx.ingress.kubernetes.io/upstream-vhost"` diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-limit-burst-multiplier.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-limit-burst-multiplier.yml new file mode 100644 index 0000000000..724f3fcd39 --- /dev/null +++ b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-limit-burst-multiplier.yml @@ -0,0 +1,43 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ingress-with-limit-burst-multiplier + namespace: default + annotations: + nginx.ingress.kubernetes.io/limit-rps: "10" + nginx.ingress.kubernetes.io/limit-burst-multiplier: "10" +spec: + ingressClassName: nginx + rules: + - host: whoami-burst.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-burst-multiplier-zero + namespace: default + annotations: + nginx.ingress.kubernetes.io/limit-rps: "10" + nginx.ingress.kubernetes.io/limit-burst-multiplier: "0" +spec: + ingressClassName: nginx + rules: + - host: whoami-burst-zero.localhost + http: + paths: + - path: / + pathType: Exact + backend: + service: + name: whoami + port: + number: 80 diff --git a/pkg/provider/kubernetes/ingress-nginx/kubernetes.go b/pkg/provider/kubernetes/ingress-nginx/kubernetes.go index 8f3f3d715a..94be769f22 100644 --- a/pkg/provider/kubernetes/ingress-nginx/kubernetes.go +++ b/pkg/provider/kubernetes/ingress-nginx/kubernetes.go @@ -1454,6 +1454,16 @@ func (p *Provider) applyCustomHTTPErrors(namespace, ingressName, routerName stri return nil } +func getLimitBurstMultiplier(config IngressConfig) int64 { + multiplier := ptr.Deref(config.LimitBurstMultiplier, defaultLimitBurstMultiplier) + + if multiplier < 1 { + multiplier = defaultLimitBurstMultiplier + } + + return int64(multiplier) +} + func applyLimitRPMConfiguration(routerName string, ingressConfig IngressConfig, rt *dynamic.Router, conf *dynamic.Configuration) { limitRPM := ptr.Deref(ingressConfig.LimitRPM, 0) if limitRPM <= 0 { @@ -1465,7 +1475,7 @@ func applyLimitRPMConfiguration(routerName string, ingressConfig IngressConfig, RateLimit: &dynamic.RateLimit{ Average: int64(limitRPM), Period: ptypes.Duration(time.Minute), - Burst: int64(limitRPM) * defaultLimitBurstMultiplier, + Burst: int64(limitRPM) * getLimitBurstMultiplier(ingressConfig), }, } @@ -1483,7 +1493,7 @@ func applyLimitRPSConfiguration(routerName string, ingressConfig IngressConfig, RateLimit: &dynamic.RateLimit{ Average: int64(limitRPS), Period: ptypes.Duration(time.Second), - Burst: int64(limitRPS) * defaultLimitBurstMultiplier, + Burst: int64(limitRPS) * getLimitBurstMultiplier(ingressConfig), }, } diff --git a/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go b/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go index 43dab89314..cd25e09b2d 100644 --- a/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go +++ b/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go @@ -9640,6 +9640,98 @@ func TestLoadIngresses(t *testing.T) { TLS: &dynamic.TLSConfiguration{}, }, }, + { + desc: "Limit Burst Multiplier", + paths: []string{ + "services.yml", + "ingressclasses.yml", + "ingresses/ingress-with-limit-burst-multiplier.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-burst-multiplier-rule-0-path-0": { + EntryPoints: []string{"http"}, + Rule: `Host("whoami-burst.localhost") && Path("/")`, + RuleSyntax: "default", + Middlewares: []string{"default-ingress-with-limit-burst-multiplier-rule-0-path-0-limit-rps", "default-ingress-with-limit-burst-multiplier-rule-0-path-0-retry"}, + Service: "default-ingress-with-limit-burst-multiplier-whoami-80", + }, + "default-ingress-with-limit-burst-multiplier-rule-0-path-0-tls": { + EntryPoints: []string{"https"}, + Rule: `Host("whoami-burst.localhost") && Path("/")`, + RuleSyntax: "default", + Middlewares: []string{"default-ingress-with-limit-burst-multiplier-rule-0-path-0-tls-limit-rps", "default-ingress-with-limit-burst-multiplier-rule-0-path-0-tls-retry"}, + Service: "default-ingress-with-limit-burst-multiplier-whoami-80", + TLS: &dynamic.RouterTLSConfig{}, + }, + "default-ingress-with-limit-burst-multiplier-zero-rule-0-path-0": { + EntryPoints: []string{"http"}, + Rule: `Host("whoami-burst-zero.localhost") && Path("/")`, + RuleSyntax: "default", + Middlewares: []string{"default-ingress-with-limit-burst-multiplier-zero-rule-0-path-0-limit-rps", "default-ingress-with-limit-burst-multiplier-zero-rule-0-path-0-retry"}, + Service: "default-ingress-with-limit-burst-multiplier-zero-whoami-80", + }, + "default-ingress-with-limit-burst-multiplier-zero-rule-0-path-0-tls": { + EntryPoints: []string{"https"}, + Rule: `Host("whoami-burst-zero.localhost") && Path("/")`, + RuleSyntax: "default", + Middlewares: []string{"default-ingress-with-limit-burst-multiplier-zero-rule-0-path-0-tls-limit-rps", "default-ingress-with-limit-burst-multiplier-zero-rule-0-path-0-tls-retry"}, + Service: "default-ingress-with-limit-burst-multiplier-zero-whoami-80", + TLS: &dynamic.RouterTLSConfig{}, + }, + }, + Middlewares: map[string]*dynamic.Middleware{ + "default-ingress-with-limit-burst-multiplier-rule-0-path-0-retry": {Retry: &dynamic.Retry{Attempts: 3}}, + "default-ingress-with-limit-burst-multiplier-rule-0-path-0-tls-retry": {Retry: &dynamic.Retry{Attempts: 3}}, + "default-ingress-with-limit-burst-multiplier-zero-rule-0-path-0-retry": {Retry: &dynamic.Retry{Attempts: 3}}, + "default-ingress-with-limit-burst-multiplier-zero-rule-0-path-0-tls-retry": {Retry: &dynamic.Retry{Attempts: 3}}, + "default-ingress-with-limit-burst-multiplier-rule-0-path-0-limit-rps": { + RateLimit: &dynamic.RateLimit{Average: 10, Burst: 100, Period: ptypes.Duration(time.Second)}, + }, + "default-ingress-with-limit-burst-multiplier-rule-0-path-0-tls-limit-rps": { + RateLimit: &dynamic.RateLimit{Average: 10, Burst: 100, Period: ptypes.Duration(time.Second)}, + }, + "default-ingress-with-limit-burst-multiplier-zero-rule-0-path-0-limit-rps": { + RateLimit: &dynamic.RateLimit{Average: 10, Burst: 50, Period: ptypes.Duration(time.Second)}, + }, + "default-ingress-with-limit-burst-multiplier-zero-rule-0-path-0-tls-limit-rps": { + RateLimit: &dynamic.RateLimit{Average: 10, Burst: 50, Period: ptypes.Duration(time.Second)}, + }, + }, + Services: map[string]*dynamic.Service{ + "default-ingress-with-limit-burst-multiplier-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-burst-multiplier", + }, + }, + "default-ingress-with-limit-burst-multiplier-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-burst-multiplier-zero", + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{ + "default-ingress-with-limit-burst-multiplier": { + 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-burst-multiplier-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{}, + }, + }, { desc: "Use Regex with Prefix pathType and StrictValidatePathType enabled", paths: []string{