Skip to content

Commit

Permalink
Added support for session persistence with sticky cookies for NGINX P…
Browse files Browse the repository at this point in the history
…lus controller
  • Loading branch information
pleshakov committed Sep 22, 2016
1 parent 14ba6c4 commit 37e66f5
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 4 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
51 changes: 51 additions & 0 deletions examples/session-persistence/README.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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
40 changes: 36 additions & 4 deletions nginx-plus-controller/nginx/configurator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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,
Expand All @@ -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 {
Expand Down
21 changes: 21 additions & 0 deletions nginx-plus-controller/nginx/configurator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
3 changes: 3 additions & 0 deletions nginx-plus-controller/nginx/ingress.tmpl
Original file line number Diff line number Diff line change
@@ -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}}

Expand Down
1 change: 1 addition & 0 deletions nginx-plus-controller/nginx/nginx.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ type IngressNginxConfig struct {
type Upstream struct {
Name string
UpstreamServers []UpstreamServer
StickyCookie string
}

// UpstreamServer describes a server in an NGINX upstream
Expand Down

0 comments on commit 37e66f5

Please sign in to comment.