Daylogs/Nginx

nginx: if 디렉티브 이해하기

ohgyun 2015. 7. 7. 23:18
발생일: 2015.07.07

키워드: nginx, 엔진엑스, if, rewrite, last, return, if directive, if 디렉티브, if is eval

문제:

nginx 위키에서는 if 디렉티브의 동작이 일관적이지 않고, 요청 케이스마다 동작하는 것이 다를 수도 있기 때문에 가능하면 사용하지 않기를 추천하고 있다.

위키에서는, `location` 블럭의 `if`에서는 `return`과 `last` 플래그가 붙은 `rewrite`만 안전하다고 설명한다.

    - return ...;
    - rewrite ... last;

(`rewrite`의 `last` 플래그는 rewrite 직후 현재 블럭의 프로세싱을 종료한다. nginx rewrite 플래그의 차이점: last 와 break 참고)

그래서 현재 프로젝트의 룰에도 거의 사용하고 있지 않았는데, 얼마 전부터 nginx 단의 agent 분기가 필요해서 한 두개씩 룰이 추가되기 시작했다.

해당 룰은 다른 멤버가 추가했는데, 추가할 당시에도 구문의 순서에 따라 동작하는 방식이 달랐는지,
나중에 코드를 확인해보니 '순서 중요'라고 주석이 달려있더라.

이참에 `if` 디렉티브가 어떻게 동작하는지 자세히 이해하고 넘어가려고 한다.


해결책:

if is eval 위키에 `if` 구문의 동작 방식을 이해하고자 할 때 참고하라는 링크가 있다.
How nginx location if works 포스트인데, 예제도 있고 친절하게 잘 설명되어 있다.

해당 포스트와 몇 가지 테스트를 해보고 정리한 `if` 디렉티브의 동작 방식은 아래와 같다.


- `if` 블럭은 중첩 로케이션 블럭을 정의하는 것이라고 생각하면 된다.
- 가장 먼저 블럭 내의 모든 rewrite 관련 디렉티브가 실행된다.
- 이 때, `return`이나 `rewrite`의 `last` 플래그를 만나면 해당 블럭에서 실행이 종료된다.
- `rewrite` 관련 디렉티브가 실행된 후에는, 매칭되는 `if` 블럭 내의 핸들러가 실행된다.
- `if `조건에 매칭되면, 해당 블럭 내의 핸들러만 실행하고 종료된다.
- 만약 매칭되는 `if` 블럭이 여러 개라면, 핸들러를 포함한 첫 번째 블럭만 실행된다. 
- 핸들러가 없으면 location 블럭의 핸들러를 상속받는다. 이 때, 핸들러에 따라 상속되기도 하고 안되기도 한다.


위 포스트의 것과 테스트한 몇 가지 룰의 케이스를 정리해뒀다.


Case 1

location /proxy {
    set $a 10; # (1)
    if ($a = 10) { # (2)
        set $a 20; # (3)
    }
    set $a 30; # (4)
    proxy_pass http://127.0.0.1:$server_port/$a; # (5)
}

/proxy 로 요청이 들어오면,
먼저, (1) -> (2) -> (3) -> (4) 순으로, 모든 rewrite phase 디렉티브를 순서대로 실행한다.

rewrite phase 디렉티브에는 다음 것들이 있다 (http://nginx.org/en/docs/http/ngx_http_rewrite_module.html)
(break, if, return, rewrite, rewrite_log, set, uninitialized_variable_warn 등)

그 다음, 매칭되는 (2)번 if 블럭으로 들어가 핸들러를 찾는다.
if 블럭 안에 핸들러가 없기 때문에, 바깥 블럭의 `proxy_pass` 블럭을 상속받게 되고, `proxy_pass`로 요청을 전달한다.

`proxy_pass`는 상속되었지만, 다른 모듈들은 상속되지 않을 수 있다.


Case 2

Case 1에서 `if` 블럭 안에 `echo` 핸들러가 추가됐다.

location /proxy {
    set $a 10; # (1)
    if ($a = 10) { # (2)
        set $a 20; # (3)
        echo "a = $a"; # (4)
    }
    set $a 30; # (5)
    proxy_pass http://127.0.0.1:$server_port/$a; # (6)
}

/proxy 로 요청이 들어오면, 동일하게, (1) -> (2) -> (3) -> (5) 순으로 rewrite phase 디렉티브가 실행된다.
다음으로 매칭되는 (2)번 `if` 블럭 안으로 들어오는데, Case 1과 다르게 `echo` 컨텐트 핸들러가 존재한다.

여기서는 컨텐트 핸들러를 실행하고 종료된다. 응답에서는 `a = 30`을 볼 수 있다.


Case 3

Case 2에서 `if` 블럭 안에 `break`가 추가됐다.

location /proxy {
    set $a 10; # (1)
    if ($a = 10) { # (2)
        set $a 20; # (3)
        break; # (4)
        echo "a = $a"; # (5)
    }
    set $a 30; # (6)
    proxy_pass http://127.0.0.1:$server_port/$a; # (7)
}

/proxy 로 요청이 들어오면, (1) -> (2) -> (3) -> (4) 까지 실행되고 rewrite phase 디렉티브는 종료된다.
`break` 디렉티브는 현재 블럭 내에서의 `rewrite` 작업을 종료시키기 때문이다.

다음으로 매칭되는 (2)번 `if` 블럭의 컨텐트 핸들러가 실행된다. 응답에서는 `a = 20`을 보게 된다.


Case 4

Case 3에서 `break` 대신에 `return` 구문을 사용했다.

location /proxy {
    set $a 10; # (1)
    if ($a = 10) { # (2)
        set $a 20; # (3)
        return 200 'a = $a'; # (4)
    }
    set $a 30; # (5)
    proxy_pass http://127.0.0.1:$server_port/$a; # (6)
}

/proxy 로 요청이 들어오면, (1) -> (2) -> (3) -> (4) 에서 요청 프로세스가 종료된다.
아마도 의도한 것과 거의 동일할 것이고, 응답에서는 `a = 20`을 보게 된다.

해당 라인에서 바로 프로세스가 종료되기 때문에, 일반적인 언어에서의 `if`와 동일한 맥락으로 이해할 수 있다.
그래서, 위키에서도 `return`만 안전하다고 한 것 같다.

`last` 플래그도 마찬가지로 안전한데,
(4)번 라인을 `rewrite ^ /foo last;`와 같았다면 해당 라인에서 `rewrite` 후에 바로 프로세싱을 종료하기 때문이다.


Case 5

Case 4에서 `add_header` 모듈을 추가했다.

location /proxy {
    set $a 10; # (1)
    if ($a = 10) { # (2)
        return 200 'a = $a'; # (3)
    }
    add_header x-foo true; # (4)
}

/proxy 로 요청이 들어오면, (1) -> (2) -> (3) 에서 프로세스가 종료되지만,
`add_header`의 값이 상속돼 응답에는 `x-foo: true`가 포함되어 있다.


Case 6

Case 5에서 `if` 블럭 안에 `add_header` 모듈을 추가 정의한다.

location /proxy {
    set $a 10; # (1)
    if ($a = 10) { # (2)
        return 200 'a = $a'; # (3)
        add_header x-bar true; # (4)
    }
    add_header x-foo true; # (5)
}

/proxy 로 요청이 들어오면, (1) -> (2) -> (3) 에서 프로세스가 종료되고,
(4)번 `add_header` 모듈에 의해 응답에는 `x-bar: true` 헤더가 포함되어 있다.

(5)번 라인은 실행되지 않는다. 헤더가 추가로 정의되지 않는 것에 주의해야 한다.


Case 7

`if` 블럭 안에 `rewrite` 구문을 몇 개 추가했다.

location /proxy {
    set $a 10; # (1)
    rewrite ^ /aaa; # (2)
    if ($a = 10) { # (3)
        rewrite ^ /bbb; # (4)
    }
    if ($a = 10) { # (5)
        # 없음
    }
    rewrite ^ /ccc; # (6)
}

/proxy 로 요청이 들어오면, (1) -> (2) -> (3) -> (4) -> (5) -> (6) 순으로 실행되고, 최종적으로 `/ccc`로 rewrite 된다.

각 `rewrite` 구문에서 `break`나 `last` 플래그를 쓰면 원하는 대로 동작 방식으로 변경할 수 있다.
예를 들어, (4)번 라인에 `rewrite ^ /bbb last;`라고 했다면, `/bbb`로 rewrite 하고 프로세스를 종료할 것이다.



정리

음... 꽤 오랜 시간동안 테스트해 보았고, 이제는 거의 동작 방식을 이해한 것 같다.
하지만, 핸들러의 동작이나 모듈의 상속 여부에 따라 처리되는 방식이 달라서 룰만 보고는 확신할 수 없을 것 같다. (예: `add_header` 와 `more_set_header`),
위키에서 얘기한 것처럼, 가능하면 사용을 자제하는 게 좋겠다.

꼭 `if` 구문의 분기가 필요하다면,  아래처럼 `last` 플래그와 함께 rewrite 구문만 풀어 정의하고, 그 외 추가 작업은 다음 블럭에서 구현하는 게 명확할 것 같다.


location /proxy {
    ...
    if ($agent = a) {
        rewrite ^ /xxx last;
    }
    if ($agent = b) {
        rewrite ^ /yyy last;
    }
    if ($agent = c) {
        rewrite ^ /zzz last;
    }
    rewrite ^ /other;
}


참고:


반응형