Skip to content

Commit 34bd211

Browse files
authored
Merge pull request #50 from nginxinc/session-persistence
Added support for session persistence with the sticky cookie method for the NGINX Plus controller
2 parents 14ba6c4 + 37e66f5 commit 34bd211

File tree

7 files changed

+132
-4
lines changed

7 files changed

+132
-4
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ The NGINX Plus Ingress Controller leverages the advanced features of NGINX Plus,
4242
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.
4343
* **Real-time Statistics**
4444
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.
45+
* **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.
4546

4647
## Advanced load balancing (beyond Ingress)
4748

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Session Persistence
2+
3+
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.
4+
5+
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.
6+
7+
## Syntax
8+
9+
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:
10+
```
11+
nginx.com/sticky-cookie-services: "service1[;service2;...]"
12+
```
13+
Here each service follows the following syntactic rule:
14+
```
15+
serviceName=serviceName cookieName [expires=time] [domain=domain] [httponly] [secure] [path=path]
16+
```
17+
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.
18+
19+
## Example
20+
21+
In the following example we enable session persistence for two services -- the *tea-svc* service and the *coffee-svc* service:
22+
```yaml
23+
apiVersion: extensions/v1beta1
24+
kind: Ingress
25+
metadata:
26+
name: cafe-ingress-with-session-persistence
27+
annotations:
28+
nginx.com/sticky-cookie-services: "serviceName=coffee-svc srv_id expires=1h path=/coffee;serviceName=tea-svc srv_id expires=2h path=/tea"
29+
spec:
30+
rules:
31+
- host: cafe.example.com
32+
http:
33+
paths:
34+
- path: /tea
35+
backend:
36+
serviceName: tea-svc
37+
servicePort: 80
38+
- path: /coffee
39+
backend:
40+
serviceName: coffee-svc
41+
servicePort: 80
42+
```
43+
For both services, the sticky cookie has the same *srv_id* name. However, we specify the different values of expiration time and a path.
44+
45+
## Notes
46+
47+
Session persistence **works** even in the case where you have more than one replicas of the NGINX Plus Ingress controller running.
48+
49+
## Advanced Session Persistence
50+
51+
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.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
apiVersion: extensions/v1beta1
2+
kind: Ingress
3+
metadata:
4+
name: cafe-ingress-with-session-persistence
5+
annotations:
6+
nginx.com/sticky-cookie-services: "serviceName=coffee-svc srv_id expires=1h path=/coffee;serviceName=tea-svc srv_id expires=2h path=/tea"
7+
spec:
8+
rules:
9+
- host: cafe.example.com
10+
http:
11+
paths:
12+
- path: /tea
13+
backend:
14+
serviceName: tea-svc
15+
servicePort: 80
16+
- path: /coffee
17+
backend:
18+
serviceName: coffee-svc
19+
servicePort: 80

nginx-plus-controller/nginx/configurator.go

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,11 @@ func (cnf *Configurator) generateNginxCfg(ingEx *IngressEx, pems map[string]stri
8686
upstreams := make(map[string]Upstream)
8787

8888
wsServices := getWebsocketServices(ingEx)
89+
spServices := getSessionPersistenceServices(ingEx)
8990

9091
if ingEx.Ingress.Spec.Backend != nil {
9192
name := getNameForUpstream(ingEx.Ingress, emptyHost, ingEx.Ingress.Spec.Backend.ServiceName)
92-
upstream := cnf.createUpstream(name)
93+
upstream := cnf.createUpstream(name, spServices[ingEx.Ingress.Spec.Backend.ServiceName])
9394
upstreams[name] = upstream
9495
}
9596

@@ -123,7 +124,7 @@ func (cnf *Configurator) generateNginxCfg(ingEx *IngressEx, pems map[string]stri
123124
upsName := getNameForUpstream(ingEx.Ingress, rule.Host, path.Backend.ServiceName)
124125

125126
if _, exists := upstreams[upsName]; !exists {
126-
upstream := cnf.createUpstream(upsName)
127+
upstream := cnf.createUpstream(upsName, spServices[path.Backend.ServiceName])
127128
upstreams[upsName] = upstream
128129
}
129130

@@ -199,6 +200,37 @@ func getWebsocketServices(ingEx *IngressEx) map[string]bool {
199200
return wsServices
200201
}
201202

203+
func getSessionPersistenceServices(ingEx *IngressEx) map[string]string {
204+
spServices := make(map[string]string)
205+
206+
if services, exists := ingEx.Ingress.Annotations["nginx.com/sticky-cookie-services"]; exists {
207+
for _, svc := range strings.Split(services, ";") {
208+
if serviceName, sticky, err := parseStickyService(svc); err != nil {
209+
glog.Errorf("In %v nginx.com/sticky-cookie-services contains invalid declaration: %v, ignoring", ingEx.Ingress.Name, err)
210+
} else {
211+
spServices[serviceName] = sticky
212+
}
213+
}
214+
}
215+
216+
return spServices
217+
}
218+
219+
func parseStickyService(service string) (serviceName string, stickyCookie string, err error) {
220+
parts := strings.SplitN(service, " ", 2)
221+
222+
if len(parts) != 2 {
223+
return "", "", fmt.Errorf("Invalid sticky-cookie service format: %s\n", service)
224+
}
225+
226+
svcNameParts := strings.Split(parts[0], "=")
227+
if len(svcNameParts) != 2 {
228+
return "", "", fmt.Errorf("Invalid sticky-cookie service format: %s\n", svcNameParts)
229+
}
230+
231+
return svcNameParts[1], parts[1], nil
232+
}
233+
202234
func createLocation(path string, upstream Upstream, cfg *Config, websocket bool) Location {
203235
loc := Location{
204236
Path: path,
@@ -212,8 +244,8 @@ func createLocation(path string, upstream Upstream, cfg *Config, websocket bool)
212244
return loc
213245
}
214246

215-
func (cnf *Configurator) createUpstream(name string) Upstream {
216-
return Upstream{Name: name}
247+
func (cnf *Configurator) createUpstream(name string, stickyCookie string) Upstream {
248+
return Upstream{Name: name, StickyCookie: stickyCookie}
217249
}
218250

219251
func pathOrDefault(path string) string {

nginx-plus-controller/nginx/configurator_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,24 @@ func TestPathOrDefaultReturnActual(t *testing.T) {
1818
t.Errorf("pathOrDefault(%q) should return %q", path, path)
1919
}
2020
}
21+
22+
func TestParseStickyService(t *testing.T) {
23+
serviceName := "coffee-svc"
24+
serviceNamePart := "serviceName=" + serviceName
25+
stickyCookie := "srv_id expires=1h domain=.example.com path=/"
26+
stickyService := serviceNamePart + " " + stickyCookie
27+
28+
serviceNameActual, stickyCookieActual, err := parseStickyService(stickyService)
29+
if serviceName != serviceNameActual || stickyCookie != stickyCookieActual || err != nil {
30+
t.Errorf("parseStickyService(%s) should return %q, %q, nil; got %q, %q, %v", stickyService, serviceName, stickyCookie, serviceNameActual, stickyCookieActual, err)
31+
}
32+
}
33+
34+
func TestParseStickyServiceInvalidFormat(t *testing.T) {
35+
stickyService := "serviceNamecoffee-svc srv_id expires=1h domain=.example.com path=/"
36+
37+
_, _, err := parseStickyService(stickyService)
38+
if err == nil {
39+
t.Errorf("parseStickyService(%s) should return error, got nil", stickyService)
40+
}
41+
}

nginx-plus-controller/nginx/ingress.tmpl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
{{range $upstream := .Upstreams}}
22
upstream {{$upstream.Name}} {
33
zone {{$upstream.Name}} 64k;
4+
{{if $upstream.StickyCookie}}
5+
sticky cookie {{$upstream.StickyCookie}};
6+
{{end}}
47
state /var/lib/nginx/{{$upstream.Name}}.state;
58
}{{end}}
69

nginx-plus-controller/nginx/nginx.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ type IngressNginxConfig struct {
4949
type Upstream struct {
5050
Name string
5151
UpstreamServers []UpstreamServer
52+
StickyCookie string
5253
}
5354

5455
// UpstreamServer describes a server in an NGINX upstream

0 commit comments

Comments
 (0)