We reveal the possibilities of map in nginx

map is a powerful directive that can make your configs simple and straightforward.
Perhaps this is the most underrated directive, due to the fact that not everyone knows all of its capabilities.
In a compact form, it helps to process variables, GET parameters, headers, cookies and backstream sets (upstream).
I will try to unleash its capabilities to the habrauser.

For simplicity, in the examples, the map directive will be adjacent to the directives from location.
In the real config, the place map is in the http block.
Short description map
This is a directive that sets the value of one variable (right), depending on another (left).
It looks like this:
map $arg_one $var_two {
    "one"      "two";
}

You can use regular expressions on the left side, including named selections.
On the right side there can be lines, variables and regexp selections from the left side.
The map directive is described in the http block.
Full description can be found in the documentation .

Replacing if with map


If features in nginx
if in nginx is implemented in its own way, rather non-obvious.
This is briefly described on a special page .
As far as I understand, if in nginx is from the category when the world revolves around you, and not vice versa.
For every if in location, nginx generates two config files for itself, with if = true and if = false.
Plus, some directives behave strangely, or don't work at all next to if in the same location.
Therefore, when working with if there is always a chance at all of the wrong behavior that you expected.
For guaranteed behavior, it is better to replace if with map.

Consider examples of replacements, from simple to complex.

Set variable to one value

For this, you most likely already have the following construction written:
if ($arg_one = "one") {
    set $var_two "two";
}

You can do this with map:
map $arg_one $var_two {
    "one"    "two";
}

And in the right place just to use $var_two.
Even in such a simple case, map has advantages:
  • no matter how many more if, or other directives are in location, this variable will get its value;
  • for nginx this is less expensive, since the value of the variable will be calculated only at the time of use;

Set a variable to one of several values

I suppose that they already resort to map for this, but sometimes you can come across an option with several if:
if ($arg_one = "one") {
    set $var_two "two";
}
if ($arg_one = "three") {
    set $var_two "four";
}

You can do this with map:
map $arg_one $var_two {
    "one"    "two";
    "three"  "four";
}

The gain in compactness and readability is already visible.
If you still need to edit the conditions, you need to correct the line in the map.

Nested if (depending on multiple conditions)

I found on the nginx mailing lists questions about nested ifs, when you need to consider several conditions.
Nested if cannot be done, you can make a crutch of several if.
Or you can write one map.
Perhaps you do not know, but in the original part (where the first variable) you can specify not one variable, but the whole text containing several variables in quotation marks.
For example, you need to block users from the user-agent "HackYou", fucking "POST" request at the address "/ admin / some / url".

This, in principle, can be done with if:
if ($http_user_agent ~ "HackYou") {
    set $block "A";
}
if ($method = "POST") {
    set $block "${block}B";
}
if ($uri = "/admin/some/url") {
    set $block "${block}C";
}
if ($block = "ABC") {
    return 403;
}

You can do this with map:
map "$http_user_agent:$method:$uri" $block {
    "HackYou:POST:/admin/some/url"  "1";
}
if ($block) {
    return 403;
}

The colon is just for readability.
In this example, a “point of no return” occurs in the direction of map.
If the number of conditions grows (for example, several user-agents), implementing them with the if set will fail, or it will be a cumbersome construction.

http headers


If you need to add headers depending on some conditions, with if this can be a problem.

For example, such a construction:
if ($arg_a = "1") {
    add_header X-one "one";
}
add_header X-two "two";

It will return only one header (X-one if arg_a = true, and X-two if false).
This is a drawback of add_header and developers will not fix it.
And if you have several if with headers, you may not be able to add several different headers at the same time.

But here map comes to the rescue:
map $arg_a $header_one {
    "1"    "one";
}
add_header X-one $header_one;
add_header X-two "two";

Multiple headers - multiple map.
If the variable is empty, nginx simply will not create a header.
In general, in the case of if the headers_more module can help, it does not have the add_header lack of if.
The headers_more module is interesting in itself, with its help it is possible to flexibly manage any headers, both response and request (on the backend).
Paired with the map directive, this module can implement many Wishlist, including the generation of several cookies.

Upstream choice


In directives like proxy_pass (fastcgi_pass and others * _pass), where you can specify upstream as a backend, you can use variables.
Those. this definition works:
proxy_pass http://$php_backend;

This is stated in the documentation :
In this case, the server name is searched among the described server groups and, if not found, it is determined using resolver.

Paired with map, this gives us a field for imagination.
Here is a rough example - let's say we need this:
There is a userid cookie.
If in its value the first number is from 0 to 4, then send requests to upstream old_php_backend.
If from 5 to 9 - on new_php_backend.
If there is no cookie, or it is empty, or the first character is not a number, then on default_php_backend.

Usually done with if, rewrite and multiple locations:
location /some/url/ {
    if ($cookie_userid ~ "~^[0-4]") { rewrite ^(.+)$ /old/$1 last; }
    if ($cookie_userid ~ "~^[5-9]") { rewrite ^(.+)$ /new/$1 last; }
    proxy_pass http://default_php_backend;
}
location /old/some/url/ {
    internal;
    rewrite    ^/old/(.+)$ $1 break;
    proxy_pass http://old_php_backend;
}
location /new/some/url/ {
    internal;
    rewrite    ^/new/(.+)$ $1 break;
    proxy_pass http://new_php_backend;
}

Using map simplifies everything:
map $cookie_userid     $php_backend {
    "~^[0-4]"          "old_php_backend";
    "~^[5-9]"          "new_php_backend";
    default        "default_php_backend";
}
location /some/url/ {
    proxy_pass http://$php_backend;
}

It really works, it is applied on a loaded service, and there are no problems with it.
The main thing is not to forget about default, so that there is always where to send the request.
This technique can be applied with geo / split_clients.
For example, in split_clients, select 1% of requests and direct them to a separate backend for tests.

The variable from map can be used in another map


Such code works:
map $arg_a $var_a {
    "0"    "1";
}
map $var_a $var_b {
    "1"    "2";
}
map $var_b $var_c {
    "2"    "3";
}

When you turn to $var_c, three map directives will fire sequentially.
With the GET parameter a = 0, it $var_cwill contain “3”.
nginx chews a chain of 12 maps, I haven’t tried further.
You can try, for the sake of interest, to find out the maximum length of the chain.
Usually a couple of maps are enough for me, which depend on each other.
It can be useful for me to create complex headers and cookies using nginx tools (when you need to add text to the header, send one header to the backend, and the other to the client).
This can come in handy, as an example development with nested if.
In one map we compute $var_a, in another map we compute $var_b, and the third map depends on "$var_a:var_b".

Map also works with variables from geo and split_clients.
In geo and split_clients, the result variable can be assigned only a simple string, variables and regular expressions cannot be used.
If you, depending on ip, need something more complicated than a simple line, geo + map will help you.
A split_clients + map link will help you, for example, flexibly change the headers for 1% of users.

For example, like this:
split_clients "${remote_addr}XXX" $test_percent {
    1%                            "1";
    *                             "0";
}
map "$test_percent:$http_user_agent" $test_mobile_users {
    "~*^1:.*iphone.*"                  "X-tester: iphone";
}
more_set_input_headers $test_mobile_users;

If the user's ip is in the test 1% of users, and the word iphone is in his user-agent, then the header “X-tester: iphone” is sent along with the request for the backend.
Developers need to respond to this headline and give the test version of the site for iPhones.

Conclusion


As you can see, map helps to make complex logic with a small number of commands.
It allows you to get rid of if in most cases.
And together with other directives, it creates tricky conversions in several lines.
I hope these features will help you, on the one hand, reduce configs, and on the other, implement tricky Wishlist.

Also popular now: