Nginx directives 기초 총정리-thumbnail

Nginx directives 기초 총정리

기초적으로 설정될 때 쓰이는 directives에 대하여
641

Nginx directives 정리

intro

Nginx와의 인연은 내 첫 프로젝트로 거슬러 올라간다. 당시 공짜로 서비스를 할 수 있었던 Heroku를 바탕으로 내 첫 프로젝트를 올리려고 했다. 하지만 Heroku와 생각 외로 긴 시간을 씨름해야만 했다. 당시 문제는 https 설정에 관한 쪽에서 있었는데, 그 외에도 Heroku 무료 서버는 방문자가 없으면 주기적으로 꺼진다는 단점이 계속 걸렸다. AWS도 많이 쓰는 만큼 정보가 많았지만, 때로 폭탄 과금이 이루어지는 경우가 있다고 했다. AWS를 잠시 멤돌다가 결국에 정착한 곳은 DigitalOcean이었다. 사실 유명하지도 않고 정보도 적었지만, 기본 요금이 저렴하고 서버 컴퓨터만 달랑 제공해주는 환경이라 내가 공부한만큼 자유도 있게 서비스를 꾸려나갈 수 있다는 장점이 있었다(맞다, 누군가에겐 그게 단점으로 보이기도 한다). 물론, 그만큼 나는 열심히 공부를 해야했고, 결국 배포에 한 달이 걸리는, 처음에는 예상하지도 못했던 기간을 소요해야만 했다.

당시에는 그냥 뭣도 모르고, Nginx ubuntu server 정도로 검색해서 나오는 글들을 짬뽕해서 어거지로 구축을 했었다(왜 Apache가 아니라 Nginx였을까?). 글마다 수정하는 Nginx 파일이 달랐고 설정해주어햐 하는 변수도 조금씩 달랐는데, 나는 깔았다 지우고 깔았다 지우고를 반복하다 결국 당시 내가 좋아하던 유튜버가 3-4년 전에 올린 영상을 보고 Nginx 설정을 마무리할 수 있었다. 지나고보니 나는 Reverse Proxy Server를 구축했던 거였는데, 당시에는 그냥 돌아가니까 "오예!" 하고 말았다.

그 뒤에도 멀티도메인 구축을 위해 잠깐 설정파일을 만지면서 다시 공부해야겠다는 생각을 하던 차에, 교육과정에서 AWS 관련 Nginx 질문이 들어왔다. 예전에 Nginx 설정하며 만진 적이 있는 질문이 마침 나와 어렵지 않게 해결해주었다. 기분도 좋아지고 Nginx 내용 자체에 대한 정리를 하자는 의욕이 솟았다. 안 그래도 메일 서버를 만들거나, 멀티도메인에 관심이 있었는데, 이참에 정리를 해야겠다는 생각이 들었다.

그리고 이 글이 그 첫 번째 글이다. 여기서는 직접 Nginx를 이용해 서비스를 할 때 마주하게 될 기본적인 directive들을 정리한다. 아마 두 번째 글에서 Reverse proxy를 다루고 나면 기본적인 정리가 끝나지 않을까 싶다.

Nginx

Daemon

우리는 애플리케이션이 동작하는 위치나 사용자와의 인터렉션 여부를 통해 서비스를 구성하는 애플리케이션을 나눠볼 수 있다. 앞단에서 사용자의 입력에 따라 즉각적으로 대응하는 애플리케이션이 있고, 뒷단에서 앞단의 애플리케이션이 돌아가게끔 해주는 애플리케이션 있다. 후자의 경우를 우리는 데몬Daemon이라고 부르는데, Nginx도 daemon의 일종이다. 다만 일반적인 daemon들은 이름 뒤에 d가 붙는데(named, crond, httpd 등) nginx는 그렇지 않다.

(Ubuntu) Nginx의 메인 설정 파일은 /etc/nginx 디렉토리에 위치한다. 이름은 nginx.conf로 되어 있다. 물론 apt-get으로 설치한 경우에 그렇고 컴파일로 설치했다면 /usr/local/nginx 디렉토리에 위치한다. 해당 파일에 들어가보면 다양한 추가 설정 파일들을 include 해오면서 실행이 되고 있다는 걸 알 수 있다.

include /etc/nginx/modules-enabled/*.conf;
include /etc/nginx/sites-enabled/*;

실제로 이렇게 와일드카드wildcard 문자를 포함하는 경우가 많은데, wildcard 문자가 포함되지 않고 특정 파일을 지정하는 경우 해당 파일이 없으면 오류가 난다. 하지만 wildcard 문자를 포함한 include문은 설령 해당하는 파일이 없더라도 에러를 띄우지 않는다.

Core Functionality(Main Block)

user

Syntax:	user user [group];
Default: user nobody nobody;
master process와 worker process

Nginx는 master process와 worker process로 구성되는데, master process는 root에서 실행되며 root에서 실행되지 않을 경우 403 Forbidden 오류가 발생한다. 그러므로 user에서는 master process가 아닌 worker process의 user와 group 권한을 지정해주게 된다.

따라서 혹시나 user root;나 혹은 user root root;와 같이 worker process의 권한이 root로 설정되어 있다면 새로운 계정을 만들어서, 바꾸어주는 것이 좋다. 그리고 새로운 계정을 만들었을 때 그 계정은 shell에 접속할 수 없도록 만들어주는 것이 좋다.

$ useradd --shell /usr/sbin/nologin www-data
user www-data;

worker_processes

Syntax:	worker_processes number | auto;
Default: worker_processes 1;

worker process의 수를 정한다. load pattern과 하드디스크의 수, 그리고 무엇보다 CPU core의 수에 따라서 주로 결정이 되는데, 판단이 잘 서지 않는다면 CPU core 수에 맞춰주거나, 더 나은 방식으로는 그냥 auto를 설정해주는 방식이 있다.

worker_processes auto;

pid

nginx deamon의 pid 파일 경로를 지정해준다. 대개 컴파일 할 때 결정되어 있는 경우가 많고, nginx의 init 스크립트에서 사용될 수 있어 웬만하면 수정을 하지 않는 편이 좋다.

Events Block

worker_connections

Syntax:	worker_connections number;
Default: worker_connections 512;

한 개의 worker process가 동시에 처리 가능한 연결의 수를 지정해준다. 만일 위에서 설정한 worker_proecesses가 4였고, worker_connections를 1024로 설정했다면, 총 4096개의 연결을 동시에 처리할 수 있다는 뜻이다. 유의할 점은 여기에서 말하는 연결은 클라이언트와의 연결뿐 아니라 프록시 서버 등과의 연결도 포함한다는 것이다.

worker_connections 1024;

multi_accept

Syntax:	multi_accept on | off;
Default: multi_accept off;

worker process가 한 번에 하나의 연결만 새로 승인해줄 것인지, 혹은 한꺼번에 여러 개의 연결을 새로 승인하도록 할 것인지를 정하는 옵션이다.

multi_accept on;

HTTP Block

add_header

HTTP의 헤더를 설정하는 파트다. X-Frame-Options, X-Content-Type-Options, X-XSS-Protection, Strict-Transport-Security 등의 옵션을 줄 수 있다. 해당 내용들을 다 다루는 것은 nginx보다는 security에 관련된 글을 쓰게 될 때 하는 것이 맞겠다는 생각이 든다.

add_header X-Frame-Options SAMEORIGIN;

sendfile, tcp_nopush, tcp_nodelay (with Nagle Algorithm)

Nagle Algorithm

Nagle Algorithm은 네트워크를 통해 보내게 되는 패킷의 수를 줄여서 최종적으로는 네트워크 부하를 줄이기 위해서 만들어진 알고리즘이다. Nagle이 off된 경우의 패킷 전송을 살펴보자면, 위 그림의 오른쪽과 같이 클라이언트의 ACK를 기다리지 않고 바로바로 패킷을 전송해주어 클라이언트가 긴 대기시간을 갖지 않아도 되게 해준다. 하지만 단점은 네트워크에 부하가 올 수 있다는 점인데, 자주 작은 크기의 패킷을 계속해서 전송하다 보면, TCP 패킷의 헤더만 하더라도 40byte(TCP Header 20byte, IPv4 Header 20byte)가 되기 때문에 네트워크에 부담을 줄 수 있다.

반면 그림 왼쪽처럼 Nagle이 on이 되면 클라이언트로부터 ACK가 올 때까지 buffer에 데이터를 충분히 쌓아둔 뒤, 데이터가 MSS를 넘거나 ACK가 오면 이를 보내주는 방식이다. 이 방식은 네트워크 부하를 줄여준다는 장점을 가지지만, 그만큼 느려지기 때문에 본인의 서버/클라이언트의 용도에 따라서 잘 설정해주는 것이 중요하다. 아래는 Nagle Algorithm pesudo code다.

#define MSS "maximum segment size"
if there is new data to send
  if the window size >= MSS and available data is >= MSS
    send complete MSS segment now
  else
    if there is unconfirmed data still in the pipe
      enqueue data in the buffer until an acknowledge is received
    else
      send data immediately
    end if
  end if
end if

sendfile() 메서드를 사용하여 파일을 전송할지 말지를 결정한다. read(), write()를 대신하여 커널 내부에서 파일을 복사하므로 속도가 향상되는 효과를 볼 수 있다.

sendfile on;

Linux 기준으로 소켓 옵션인 TCP_CORK를 사용할지 말지를 결정하는 옵션이다. FreeBSD 기준으로는 TCP_NOPUSH 옵션에 적용된다. TCP_NODELAY와는 Linux 2.5.71 버전부터 병기될 수 있게 바뀌었다. sendfile()을 사용했을 때에만 활성화할 수 있는 옵션이다.
TCP_CORK를 사용하게 되면 데이터의 크기가 MSS(Maximum Segment Size)에 도달하거나 요청이 들어온 후 timeout이 발생하는 경우(200ms가 경과하는 경우)에 데이터를 전송하게 된다. 어찌보면 Nagle Algorithm과 아주 유사하다.
앞서 설명한 Nagle Algorithm과의 유의미한 차이는 ACK에 반응하느냐 아니냐의 차이다. 예를 들어 Nagle Algorithm은 MSS에 도달하지 않더라도, ACK를 모두 받았다고 한다면 바로 패킷을 전송하지만, TCP_CORK는 MSS를 넘지 않는다면 timeout이 발생할 때까지 기다렸다가 데이터를 전송하게 된다.

tcp_nopush on;

위에서 설명된 Nagle Algorithm을 사용하지 않겠다는 의미다.

tcp_nodelay on;

해당 내용을 조사하면서 Silly Window, Window Size, writev(), three-way handshake 등 꼬리를 물고 이어지는 개념들이 나왔지만, 웹 개발 단계에서는 위에 적힌 내용 정도만 다 숙지하더라도 충분하고 또 충분하지 싶어 멈추었다.
다만 이것만은 언급하고 넘어가자. 이 셋을 한꺼번에 사용했을 때는 최적화에서 이점을 가져올 수 있는데, 우선 sendfile과 함께 쓰인 tcp_nopush는 클라이언트로 패킷을 전송하기 전에 패킷이 가득 찼는지 확인한다. 이를 통해 네트워크 오버헤드를 줄일 수 있고 파일 전송 속도도 빨라진다. 그런 다음 마지막 패킷이 남게 되면 nginx는 tcp_nopush 대신 tcp_nodelay가 소켓이 데이터를 보내도록 강제하게 하여 파일당 최대 200ms를 절약할 수 있다.

keepalive_timeout (with HTTP keep-alive)

웹사이트를 구현하다보면, 한 페이지를 보는 데에도 index.html, style.css, favicon.ico, banner.png 등 수많은 파일이 서버로부터 넘어와야 한다는 것을 알 수 있을 것이다(직접 눈으로 보고 싶다면 개발자도구를 켜서 Network란에 들어간 뒤 페이지를 새로고침해보자). 이런 경우는 웹 시장이 커지고 유저들의 니즈도 늘어나면서 더더욱이나 많아지고 있다. 그러다보니 매번 TCP 연결을 맺기 위해 SYN과 ACK를 주고받는 three-way handshake를 맺게 되면 아래와 같은 일이 벌어진다.

Client : "안녕, 나는 Client야. 내 목소리 들려?"
Server : "안녕, 나는 Server야. 잘 들려. 데이터 받을 준비 되었니?"
Client : "응 준비되었어. index.html 좀 줄래?"
Server : "여기 index.html이 있어. 받아."
Client : "안녕, 나는 Client야. 내 목소리 들려?"
Server : "안녕, 나는 Server야. 잘 들려. 데이터 받을 준비 되었니?"
Client : "응 준비되었어. style.css 좀 줄래?"
Server : "여기 style.css가 있어. 받아."
Client : "안녕, 나는 Client야. 내 목소리 들려?"
...

인사를 한 번만 하고 아래와 같이 대화하기 위해 만든 것이 Persistent Connection이다.

Client : "안녕, 나는 Client야. 내 목소리 들려?"
Server : "안녕, 나는 Server야. 잘 들려. 데이터 받을 준비 되었니?"
Client : "응 준비되었어. index.html 좀 줄래?"
Server : "여기 index.html이 있어. 받아."
Client : "style.css 좀 줄래?"
Server : "여기 style.css가 있어. 받아."
Client : "favicon.ico 좀 줄래?"
...

당연한 말이지만 만일 필요한 때에 적절하게 Persistent Connection을 맺으면 네트워크 비용이나 부하가 감소하고 latency가 감소하는 이점이 있다.

Syntax:	keepalive_timeout timeout [header_timeout];
Default: keepalive_timeout 75s;

HTTP의 keep-alive는 앞서 서술한 persistent connection을 맺을 수 있게 도와주는 기법이다. HTTP/1.0부터 지원이 되고 있고, HTTP/1.1부터는 애초에 default로 Connection: keep-alive가 설정된다. keepalive_timeout 옵션은 해당 keep-alive가 기본적으로 얼마만큼 지속될지를 결정해준다. 해당 값이 지나치게 높을 경우 불필요한 연결을 지속해야 하므로 적당한 값이 권장된다.

keepalive_timeout 75;

types_hash_max_size, server_names_hash_bucket_size

Syntax:	types_hash_bucket_size size;
Default: types_hash_bucket_size 64;
Syntax:	server_names_hash_bucket_size size;
Default: server_names_hash_bucket_size 32|64|128;

nginx는 static한 데이터(server name, directive's values, names of request header strings 등)를 모아두기 위해 hash table을 사용한다. 도메인 이름이 길거나 여러 가상 호스트 도메인을 등록할 때 문제가 있을 수도 있기 때문에, server_names_hash_bucket_size의 경우 기본적으로 넉넉한 값을 권장한다. types_hash_bucket_size의 경우는 웬만하면 nginx가 부여한 기본값을 바꾸지 않는 편이 좋다.

types_hash_bucket_size 1024;
server_names_hash_bucket_size 128;

server_tokens

Syntax:	server_tokens on | off | build | string;
Default: server_tokens on;

Server response header field나 nginx 에러 페이지에 nginx 버전을 표기할 것인지를 묻는 옵션이다. 보안을 생각한다면 off로 설정하는 것을 권장한다.

server_tokens off;

server_name_in_redirect

Syntax:	server_name_in_redirect on | off;
Default: server_name_in_redirect off;

내부적으로 URL 경로를 재설정을 할 때 사용된다. 이를테면 https://example.com/article?id=10&author=Back+Howard와 같은 URL보다는 https://example.com/article-Best-Way-To-Make-Milk-By-Back-Howard와 같은 URL이 검색엔진에게나 유저에게나 더 좋을 것이다. 이때 사용되는 것이 경로 재설정이다. on을 사용하면 nginx는 경로 재설정을 할 때 server_name에 처음으로 설정된 Host name을 사용하고, off인 경우에는 HTTP request header의 Host값을 사용한다.

server_name_in_redirect on;

ssl settings

Syntax:	ssl_protocols [SSLv2] [SSLv3] [TLSv1] [TLSv1.1] [TLSv1.2] [TLSv1.3];
Default: ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

통신에서 사용할 TLS protocol을 특정한다.

ssl_protocols TLSv1.2 TLSv1.3
Syntax:	ssl_prefer_server_ciphers on | off;
Default: ssl_prefer_server_ciphers off;

SSLv3 혹은 TLS protocol을 사용할 때 서버 cipher를 클라이언트 cipher보다 우선하여 사용하도록 지정한다.

ssl_prefer_server_ciphers on;
Syntax:	ssl_ciphers ciphers;
Default: ssl_ciphers HIGH:!aNULL:!MD5;

사용 가능한 cipher를 특정해둔다. OpenSSL library에서 이해할 수 있는 포맷이어야 한다.

ssl_ciphers ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP;
Syntax:	ssl_session_timeout time;
Default: ssl_session_timeout 5m;

클라이언트가 session parameter를 재사용할 수 있도록 할 시간을 지정한다.

ssl_session_timeout 10m;

log settings

누가 언제 접속했는지, 무슨 에러가 났는지 등에 관한 로그를 남기는 기능이다. 해당 내용은 server 별로 할당해주는 것이 좋으며, 로그 path 또한 서버별로 분리해주는 것이 좋다. 로그 레벨, 포맷 등까지도 지정이 가능하다.

Syntax:	access_log path [format [buffer=size] [gzip[=level]] [flush=time] [if=condition]];
        access_log off;
Default: access_log logs/access.log combined;
access_log /var/log/nginx/MyServer1/access.log
Syntax:	error_log file [level];
Default: error_log logs/error.log error;
error_log /var/log/nginx/MyServer1/error.log

gzip

Syntax:	gzip on | off;
Default: gzip off;

response를 압축해서 보내도록 지시한다.

gzip on;

Server Block

root

Syntax:	root path;
Default: root html;

request가 들어왔을 때 root directory가 어디로 될지를 결정해준다. 해당 내용은 location block에서도 사용할 수 있다.

root /usr/share/nginx/html;

index

Syntax:	index file ...;
Default: index index.html;

초기 화면에서 보여줄 index 파일의 이름을 알려준다. 역시 해당 내용은 location block에서도 사용할 수 있다.

index index.html index.htm;

server_name

Syntax:	server_name name ...;
Default: server_name "";

하나의 IP에 여러 가지 domain name을 서비스할 수 있도록 해주는 directive다. 서버는 해당 directive를 확인하고 request header를 확인한 뒤 어떤 domain으로 요청을 처리할지를 결정한다.

server_name example.com www.example.com;

Location Block

proxy_pass

Syntax:	proxy_pass URL;
Default: —

위의 server_name과 location block에 명시된 경로를 합쳤을 때 서버의 어디로 해당 요청을 매핑해줄지 결정한다. domain name, IP, port 등까지 특정할 수 있다.
가령 아래와 같이 설정이 되어 있고, 한 유저가 www.example.com/api로 접속했다면, 해당 유저의 요청은 서버의 8000번 포트의 index page로 가게될 것이다.

server_name example.com www.example.com;

location /api {
  proxy_pass http://127.0.0.1:8000/;
}

내가 아직 구현해보지는 않았지만, upstream을 이용하는 경우도 있다. proxy_passupstream을 같이 사용하는 경우 아래와 위의 코드는 아래처럼 바뀐다.

upstream backend {
    server http://127.0.0.1:8000/;
}

server {
    server_name example.com www.example.com;

    location / {
        proxy_pass http://backend;
    }
}

위 아래 두 코드는 서로 거의 동일한 역할을 하기 때문에 사실상 upstream으로 구현했을 때 득이 없다. 하지만 아래와 같이 upstream의 기능을 십분 활용하면 nginx의 장점을 극대화할 수 있다.

upstream backend {
    ip_hash;

    server backend1.example.com/ weight=3;
    server http://127.0.0.1:8001/;
    server http://127.0.0.1:8002/ down;
    server http://127.0.0.1:8003/ max_fails=3 fail_timeout=30s;
    server unix:/tmp/backend3;

    keepalive 32;
    sticky cookie srv_id expires=1h domain=.example.com path=/;
}

server {
    server_name example.com www.example.com;

    location / {
        proxy_pass http://backend;
    }
}

예시로 가져온 코드를 그대로 쓰는 일은 없길 바란다. 예시를 위해 몇 가지 directive들을 가져왔을 뿐이라, 실제로 좋은 코드인지, 작동이 정상으로 되는지는 알지 못한다.
코드를 보면 우선 여러 대의 서버가 backend에 연결되어 있음을 알 수 있다. 서버별로 다른 포트를 사용하거나 심지어는 Unix-domain socket도 사용이 가능하다. ip_hash directive를 통해 load balancing method를 규정하고 있고, weight를 통해 각 서버에 weight를 주거나, down을 통해 일시적으로 다운된 서버를 표시해주거나, max_failsfail_timeout을 통해 connection fail을 어떻게 처리할지 서버별로 설정해줄 수 있다. keepalive를 통해 persistent connection을 따로 설정해줄 수 있고, cookie directive를 통해 같은 클라이언트는 저번에 request를 받았던 동일한 서버로 연결해줄 수도 있다.
이런 수많은 기능들 때문에 Upstream Block을 사용해주는 것이 아닐까 싶다.

proxy_http_version

Syntax:	proxy_http_version 1.0 | 1.1;
Default: proxy_http_version 1.0;

proxy를 위한 HTTP protocol을 설정한다.

proxy_http_version 1.0;

proxy_set_header

Syntax:	proxy_set_header field value;
Default: proxy_set_header Host $proxy_host;
         proxy_set_header Connection close;

프록시된 서버의 request header 필드를 재정의하거나 추가한다.

proxy_cache_bypass

Syntax:	proxy_cache_bypass string ...;
Default: —

cache에서 response를 가져오지 않을 조건을 설정한다.

proxy_cache_bypass $http_upgrade;

SSL settings by Certbot

$ sudo certbot --nginx

위 명령어를 실행하면 Let's Encrypt에서 SSL certification key를 발급해주면서 동시에 nginx 설정 파일에 SSL 관련된 설정들이 자동으로 생성되게 된다(listen 443, ssl_certificate, ssl_certificate_key, ssl_dhparam, ssl_session_cache 등). 해당 내용 directive들에 관해서는 굳이 언급할 정도로 어렵거나 중요한 개념이 언급되어야 하는 건 아닌 것 같아, 해당 내용은 생략한다.


참고서적 :
끌레망 네델꾸, 『NGINX HTTP SERVER』, 에이콘, 2020.

참고사이트 :
nginx documentation
nginx optimization understanding sendfile tcp_nodelay and tcp_nopush
keep alive란? (persistent connection에 대하여)
TCP_CORK: More than you ever wanted to know
stackoverflow: Is there any significant difference between TCP_CORK and TCP_NODELAY in this use-case?
NAGLE 알고리즘과 TCP_CORK