From 37e66f5622faa06570d259af77308658705f762f Mon Sep 17 00:00:00 2001 From: Michael Pleshakov Date: Tue, 20 Sep 2016 14:31:23 +0300 Subject: [PATCH] Added support for session persistence with sticky cookies for NGINX Plus controller --- README.md | 1 + examples/session-persistence/README.md | 51 +++++++++++++++++++ ...cafe-ingress-with-session-persistence.yaml | 19 +++++++ nginx-plus-controller/nginx/configurator.go | 40 +++++++++++++-- .../nginx/configurator_test.go | 21 ++++++++ nginx-plus-controller/nginx/ingress.tmpl | 3 ++ nginx-plus-controller/nginx/nginx.go | 1 + 7 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 examples/session-persistence/README.md create mode 100644 examples/session-persistence/cafe-ingress-with-session-persistence.yaml diff --git a/README.md b/README.md index ebb036eda9..56f588fafd 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ The NGINX Plus Ingress Controller leverages the advanced features of NGINX Plus, Every time the number of pods of services you expose via Ingress changes, the Ingress controller updates the configuration of NGINX to reflect those changes. For open source NGINX software, the configuration file must be changed followed by the configuration reload. For NGINX Plus, we use the [on-the-fly reconfiguration](https://www.nginx.com/products/on-the-fly-reconfiguration/) feature, which allows you to update NGINX Plus on-the-fly without reloading the configuration. This prevents a potential increase of memory usage and overall system overloading, which could occur with frequent configuration reloads. * **Real-time Statistics** NGINX Plus provides you with [advanced statistics](https://www.nginx.com/products/live-activity-monitoring/), which you can access either through the API or via the built-in dashboard. This can give you insights into how NGINX Plus and your applications are performing. +* **Session Persistence** When enabled, NGINX Plus makes sure that all the requests from the same client are always passed to the same backend container using the *sticky cookie* method. Refer to the [session persistence examples](examples/session-persistence) to find out how to configure it. ## Advanced load balancing (beyond Ingress) diff --git a/examples/session-persistence/README.md b/examples/session-persistence/README.md new file mode 100644 index 0000000000..0b404df869 --- /dev/null +++ b/examples/session-persistence/README.md @@ -0,0 +1,51 @@ +# Session Persistence + +It is often required that the requests from a client are always passed to the same backend container. You can enable such behavior with [Session Persistence](https://www.nginx.com/products/session-persistence/), available in the NGINX Plus Ingress controller. + +NGINX Plus supports *the sticky cookie* method. With this method, NGINX Plus adds a session cookie to the first response from the backend container, identifying the container that sent the response. When a client issues the next request, it will send the cookie value and NGINX Plus will route the request to the same container. + +## Syntax + +To enable session persistence for one or multiple services, add the **nginx.com/sticky-cookie-services** annotation to your Ingress resource definition. The annotation specifies services that should have session persistence enabled as well as various attributes of the cookie. The annotation syntax is as follows: +``` +nginx.com/sticky-cookie-services: "service1[;service2;...]" +``` +Here each service follows the following syntactic rule: +``` +serviceName=serviceName cookieName [expires=time] [domain=domain] [httponly] [secure] [path=path] +``` +The syntax of the *cookieName*, *expires*, *domain*, *httponly*, *secure* and *path* parameters is the same as for the [sticky directive](http://nginx.org/en/docs/http/ngx_http_upstream_module.html#sticky) in the NGINX Plus configuration. + +## Example + +In the following example we enable session persistence for two services -- the *tea-svc* service and the *coffee-svc* service: +```yaml +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: cafe-ingress-with-session-persistence + annotations: + nginx.com/sticky-cookie-services: "serviceName=coffee-svc srv_id expires=1h path=/coffee;serviceName=tea-svc srv_id expires=2h path=/tea" +spec: + rules: + - host: cafe.example.com + http: + paths: + - path: /tea + backend: + serviceName: tea-svc + servicePort: 80 + - path: /coffee + backend: + serviceName: coffee-svc + servicePort: 80 +``` +For both services, the sticky cookie has the same *srv_id* name. However, we specify the different values of expiration time and a path. + +## Notes + +Session persistence **works** even in the case where you have more than one replicas of the NGINX Plus Ingress controller running. + +## Advanced Session Persistence + +The NGINX Plus Ingress controller supports only one of the three session persistence methods available in NGINX Plus. Visit [this page](https://www.nginx.com/products/session-persistence/) to learn about all of the methods. If your session persistence requirements are more complex than the ones in the example above, you will have to use a different approach to deploying and configuring NGINX Plus without the Ingress controller. You can read the [Load Balancing Kubernetes Services with NGINX Plus](https://www.nginx.com/blog/load-balancing-kubernetes-services-nginx-plus/) blog post to find out more. diff --git a/examples/session-persistence/cafe-ingress-with-session-persistence.yaml b/examples/session-persistence/cafe-ingress-with-session-persistence.yaml new file mode 100644 index 0000000000..a394687c9e --- /dev/null +++ b/examples/session-persistence/cafe-ingress-with-session-persistence.yaml @@ -0,0 +1,19 @@ +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: cafe-ingress-with-session-persistence + annotations: + nginx.com/sticky-cookie-services: "serviceName=coffee-svc srv_id expires=1h path=/coffee;serviceName=tea-svc srv_id expires=2h path=/tea" +spec: + rules: + - host: cafe.example.com + http: + paths: + - path: /tea + backend: + serviceName: tea-svc + servicePort: 80 + - path: /coffee + backend: + serviceName: coffee-svc + servicePort: 80 diff --git a/nginx-plus-controller/nginx/configurator.go b/nginx-plus-controller/nginx/configurator.go index 3c858019c9..766bba5c1a 100644 --- a/nginx-plus-controller/nginx/configurator.go +++ b/nginx-plus-controller/nginx/configurator.go @@ -86,10 +86,11 @@ func (cnf *Configurator) generateNginxCfg(ingEx *IngressEx, pems map[string]stri upstreams := make(map[string]Upstream) wsServices := getWebsocketServices(ingEx) + spServices := getSessionPersistenceServices(ingEx) if ingEx.Ingress.Spec.Backend != nil { name := getNameForUpstream(ingEx.Ingress, emptyHost, ingEx.Ingress.Spec.Backend.ServiceName) - upstream := cnf.createUpstream(name) + upstream := cnf.createUpstream(name, spServices[ingEx.Ingress.Spec.Backend.ServiceName]) upstreams[name] = upstream } @@ -123,7 +124,7 @@ func (cnf *Configurator) generateNginxCfg(ingEx *IngressEx, pems map[string]stri upsName := getNameForUpstream(ingEx.Ingress, rule.Host, path.Backend.ServiceName) if _, exists := upstreams[upsName]; !exists { - upstream := cnf.createUpstream(upsName) + upstream := cnf.createUpstream(upsName, spServices[path.Backend.ServiceName]) upstreams[upsName] = upstream } @@ -199,6 +200,37 @@ func getWebsocketServices(ingEx *IngressEx) map[string]bool { return wsServices } +func getSessionPersistenceServices(ingEx *IngressEx) map[string]string { + spServices := make(map[string]string) + + if services, exists := ingEx.Ingress.Annotations["nginx.com/sticky-cookie-services"]; exists { + for _, svc := range strings.Split(services, ";") { + if serviceName, sticky, err := parseStickyService(svc); err != nil { + glog.Errorf("In %v nginx.com/sticky-cookie-services contains invalid declaration: %v, ignoring", ingEx.Ingress.Name, err) + } else { + spServices[serviceName] = sticky + } + } + } + + return spServices +} + +func parseStickyService(service string) (serviceName string, stickyCookie string, err error) { + parts := strings.SplitN(service, " ", 2) + + if len(parts) != 2 { + return "", "", fmt.Errorf("Invalid sticky-cookie service format: %s\n", service) + } + + svcNameParts := strings.Split(parts[0], "=") + if len(svcNameParts) != 2 { + return "", "", fmt.Errorf("Invalid sticky-cookie service format: %s\n", svcNameParts) + } + + return svcNameParts[1], parts[1], nil +} + func createLocation(path string, upstream Upstream, cfg *Config, websocket bool) Location { loc := Location{ Path: path, @@ -212,8 +244,8 @@ func createLocation(path string, upstream Upstream, cfg *Config, websocket bool) return loc } -func (cnf *Configurator) createUpstream(name string) Upstream { - return Upstream{Name: name} +func (cnf *Configurator) createUpstream(name string, stickyCookie string) Upstream { + return Upstream{Name: name, StickyCookie: stickyCookie} } func pathOrDefault(path string) string { diff --git a/nginx-plus-controller/nginx/configurator_test.go b/nginx-plus-controller/nginx/configurator_test.go index cbf5318039..81a0785809 100644 --- a/nginx-plus-controller/nginx/configurator_test.go +++ b/nginx-plus-controller/nginx/configurator_test.go @@ -18,3 +18,24 @@ func TestPathOrDefaultReturnActual(t *testing.T) { t.Errorf("pathOrDefault(%q) should return %q", path, path) } } + +func TestParseStickyService(t *testing.T) { + serviceName := "coffee-svc" + serviceNamePart := "serviceName=" + serviceName + stickyCookie := "srv_id expires=1h domain=.example.com path=/" + stickyService := serviceNamePart + " " + stickyCookie + + serviceNameActual, stickyCookieActual, err := parseStickyService(stickyService) + if serviceName != serviceNameActual || stickyCookie != stickyCookieActual || err != nil { + t.Errorf("parseStickyService(%s) should return %q, %q, nil; got %q, %q, %v", stickyService, serviceName, stickyCookie, serviceNameActual, stickyCookieActual, err) + } +} + +func TestParseStickyServiceInvalidFormat(t *testing.T) { + stickyService := "serviceNamecoffee-svc srv_id expires=1h domain=.example.com path=/" + + _, _, err := parseStickyService(stickyService) + if err == nil { + t.Errorf("parseStickyService(%s) should return error, got nil", stickyService) + } +} diff --git a/nginx-plus-controller/nginx/ingress.tmpl b/nginx-plus-controller/nginx/ingress.tmpl index a071d8b6a5..487490cdc5 100644 --- a/nginx-plus-controller/nginx/ingress.tmpl +++ b/nginx-plus-controller/nginx/ingress.tmpl @@ -1,6 +1,9 @@ {{range $upstream := .Upstreams}} upstream {{$upstream.Name}} { zone {{$upstream.Name}} 64k; + {{if $upstream.StickyCookie}} + sticky cookie {{$upstream.StickyCookie}}; + {{end}} state /var/lib/nginx/{{$upstream.Name}}.state; }{{end}} diff --git a/nginx-plus-controller/nginx/nginx.go b/nginx-plus-controller/nginx/nginx.go index 37cb3e4fb5..6586d4de0b 100644 --- a/nginx-plus-controller/nginx/nginx.go +++ b/nginx-plus-controller/nginx/nginx.go @@ -49,6 +49,7 @@ type IngressNginxConfig struct { type Upstream struct { Name string UpstreamServers []UpstreamServer + StickyCookie string } // UpstreamServer describes a server in an NGINX upstream