Skip to content
TUWLAB.com
모든 게시물에 대하여 '링크'
방식의 퍼가기만 허용합니다.
한양대학교 전자통신컴퓨터공학부
바라미
  • 352
  • 2715539
DNS Powered by DNSEver.com
Python

[Django Tutorial] 9. Production - uWSGI를 통해 Nginx 웹 서버와 연동하기

Posted 2015. 09. 25 Updated 2018. 06. 17 Views 11347 Replies 1

WSGI(Web Server Gateway Interface)는 파이썬으로 제작한 웹 어플리케이션을 웹 서버와 연동하기 위한 '어댑터'성격의 미들웨어입니다.

WSGI데몬은 자체적으로 프로세스를 생성하여 동작하면서 소켓 파일 또는 소켓 포트를 통해 웹 서버와 통신합니다. 한편, WSGI모듈은 웹 서버에 모듈 형태로 내장되어 웹 서버 자체 프로토콜로 통신을 합니다. 이 글에서 다룰 Nginx와 연동되는 uWSGI가 WSGI데몬에 해당하고, Apache에 내장되는 mod_wsgi가 WSGI모듈에 해당합니다.

[외부] ↔ [웹 서버] ↔ [WSGI 데몬] ↔ [웹 어플리케이션]

[외부] ↔ [웹 서버+WSGI모듈] ↔ [웹 어플리케이션]

가장 중요한 점은 WSGI 데몬인지 모듈인지가 아니라, WSGI가 웹 어플리케이션을 웹 서버에 붙여주는 접착제 역할을 한다는 점입니다. 이를 활용하면 웹 어플리케이션을 작성할 때 복잡한 소켓 통신에 대해서 생각할 필요조차 없고, 단지 WSGI프로토콜에 따라 표준 입출력(Standard Input/Output)을 통해 웹 서버와 통신할 수 있습니다.

Django는 WSGI표준을 지키고 있으며, WSGI통신을 위한 포트를 단일 파일 형태로 제공합니다. 이 파일은 별도로 만들어 줘야 되는 것은 아니며, startproject 명령으로 Django프로젝트를 생성할 때 자동으로 만들어집니다.

지금까지 진행한 예제 디렉토리에서 sitebase/wsgi.py가 바로 WSGI포트 역할을 하는 파일입니다. WSGI데몬이나 모듈을 이 wsgi.py파일에 연결시키고, 파이썬 환경변수(실행파일 및 라이브러리)를 잘 세팅해 주면 웹 서버를 통해 Django로 만든 사이트를 사용할 수 있습니다.

이 글에서는 WSGI데몬 프로그램중 많이 사용되는 uWSGI를 통해 Django사이트를 Nginx 웹 서버와 연동하는 방법에 대해 다루도록 하겠습니다.

 

서버 보안을 고려한 uWSGI구동 환경 세팅하기

제가 처음 uWSGI를 Production Server에 사용하려 할 때, 역시나 많은 사이트와 책들을 참고했습니다. 외국 포럼도 많이 뒤졌었는데, 단지 동작만 되도록 기술해 놓은 글이 대다수인 것을 보고 많이 당황했었습니다. 많은 튜토리얼이나 Q&A포럼에서 데몬을 root권한으로 실행하도록 유도하고 있는데, 웹 어플리케이션과 같이 외부에서 임의로 접속할 수 있는 프로그램을 root권한으로 구동하는 것은 매우 위험한 행위입니다.

Django프레임워크는 파이썬을 기반으로 제작되었고, 파이썬에서는 시스템의 파일 시스템을 직접 다룰 수 있는 API들을 제공하고 있습니다. 웹 어플리케이션은 외부로부터 요청을 받아서 시스템 파일을 조작하는 기능을 수행하는데, 이를 root권한으로 실행한다면 사소한 버그나 보안 결점으로 인해 시스템 전체가 위험해질 수 있습니다. 웹 어플리케이션을 포함한 프레임워크가 100% 무결점이라고 하더라도, 이와 연동되어 동작하는 uWSGI프로그램 자체에 버그나 보안 결점이 존재할지도 모르는 일입니다.

물론 최초로 명령줄에서 데몬을 실행할때는 root권한으로 실행해야 합니다. 하지만 이는 소켓 파일을 생성하고 소유와 권한을 적절히 바꿔주는 등 root권한으로만 수행할 수 있는 사전 작업을 수행하기 위함이며, 이후에는 미리 설정한 권한으로 자식 프로세스(Child Process)를 생성하고 여기에 실질적인 데몬 프로그램을 올려서 구동합니다.

위에서 문제삼은 것이 바로 이 자식 프로세스를 실행하는 권한입니다. 뒤에서 살펴보겠지만, uWSGI의 환경 설정 파일을 작성할 때 uidgid를 지정할 수 있습니다. 이 항목이 바로 자식 프로세스의 실행 권한과 그룹을 지정하는 곳입니다. 이 둘을 지정하지 않거나 혹은 root로 써 놓을 경우 바로 문제가 될 수 있는 것입니다.

어쩔 수 없는 상황이라 데몬을 root권한으로 구동해야 한다면, 최소 chroot라도 지정해서 자식 프로세스가 접근할 수 있는 디렉토리를 한정해 주는 조치라도 취해 놓아야 합니다.

제가 전제하는 구동 환경의 조건은 위와 같은 보안과 관련된 사안 뿐만 아니라, pyenv및 VirtualEnv를 통해 생성한 Sandbox와 연동하는 조건도 포함되어 있습니다. 어떻게 보면 상당히 까다로운 조건이기도 한데, 그래서인지 어디에서도 제가 원하는 환경대로 uWSGI를 구동하는 방법을 기술해 놓은 곳을 찾을 수 없었습니다. 따라서 상당히 많은 삽질을 하다가 그나마 최선인 방법을 찾아서 적어 놓은 것이 바로 이 글에서 기술하는 방법입니다. 여기에 기술해 놓은 방법이 정답은 아니라는 점을 미리 알려드리며, 나름의 더 나은 세팅 방법을 찾아 보는 것도 좋은 경험이 될 것으로 생각합니다.

[환경 세팅 전제조건]

  • uWSGI 데몬(자식 프로세스)는 nginx:nginx권한으로 실행되어야 한다.
  • 생성되는 소켓 파일(*.sock)의 권한은 644(혹은 600)여야 한다. (즉, nginx권한으로만 쓸 수 있다.)
  • 프로젝트 종속 패키지들은 VirtualEnv에 설치하여 사용하며, 전역으로 설치하지 않는다.
  • 프로젝트 파일 및 VirtualEnv디렉토리(venv)의 소유권은 사용자 계정으로 되어 있어, 언제든지 자유롭게 수정할 수 있어야 한다.

[발견된 문제점]

*참고: 여기서 기술한 방법은 보안 관련 규칙이 혹독한 CentOS서버에서 세팅 및 테스트까지 마쳤습니다. CentOS보다 기본 보안 설정이 상대적으로 약한 Ubuntu서버에서는 아래에서 지적하는 일부 항목이 문제가 되지 않을 수도 있습니다.

  • VirtualEnv로 설치한 Python에는 '__future__'와 같은 일부 라이브러리 구성요소가 누락되어 있다.
  • 권한 문제로 인해 nginx권한으로 실행되는 uWSGI데몬은 pyenv를 통해 설치한 Python바이너리 및 라이브러리에 접근할 수 없다. - 'encodings', 'site', 'os'와 같이 기본 라이브러리에 포함된 모듈을 찾지 못하는 문제가 발생 (← CentOS에서 각 사용자의 홈 디렉토리 퍼미션은 600이며, pyenv로 설치한 파이썬 및 라이브러리는 홈 디렉토리에 설치된다.)

[문제들에 대한 해결책]

  • 사용할 버전의 Python 바이너리 및 라이브러리를 시스템의 별도 공간에 설치하여 사용한다.
    (uWSGI의 환경변수를 통해 별도로 설치한 Python을 사용한다.)
  • 프로젝트에서 사용하는 추가 패키지들은 VirtualEnv 샌드박스(venv)에 설치하여 사용한다.
    (시스템 전역이나 별도 공간에 설치한 Python에 설치하지 않는다.)

▷ 즉, VirtualEnv 샌드박스(venv)에 설치한 구성요소 중 site-packages만 사용하며, 그 외 Python 바이너리와 라이브러리는 시스템의 별도 공간에 설치하여 사용한다.

 

시스템 별도 공간에 Python 설치하기

이 과정이 귀찮다고 시스템 전역에 설치된 Python의 버전을 바꿔버리면, 멀쩡히 잘 동작하던 프로그램(들)이 갑자기 안 된다고 짖어댈 수 있습니다. (e.g. CentOS의 yum등...) 따라서, 시스템 전역에 설치된 Python은 건드리지 않으면서 별도의 공간에 사용할 버전의 Python을 설치하도록 하겠습니다. (참고로, 튜토리얼에서 지금까지 3.4.3버전을 기준으로 진행했었습니다.)

https://www.python.org/downloads/ 에서 사용할 버전의 Python을 다운받고 압축을 풀어 줍니다. 그리고 Configure를 실행할 때, 다음과 같이 Prefix와 Exec Prefix를 지정해 주도록 합니다. 이렇게 함으로써 기존에 설치된 Python에 영향을 주지 않고 새로운 버전의 Python을 설치할 수 있습니다.

./configure --prefix=/usr/local --exec-prefix=/usr/local/python3.4 --enable-shared

이제 빌드를 하고 설치까지 해 줍니다. configure를 실행할 때 위와 똑같이 지정했다면, 바이너리는 /usr/local/python3.4/bin에, 라이브러리는 /usr/local/lib/python3.4에 설치가 될 것입니다.

[180228 추가]

--enable-shared 옵션은 Python을 빌드할 때 통짜 바이너리로 빌드하지 않고 Shared Library를 빌드해서 실행시 불러서 사용하도록 하기 위함입니다. 이 옵션을 주지 않고 진행하면 이후 진행할 uWSGI 플러그인 빌드에서 오류가 발생합니다. (Python 3.6이 문제인지, uWSGI 버전이 올라가면서 생긴 문제인지는 확실하지 않습니다.)

Configure가 완료되면 빌드 및 설치를 진행합니다.

make
sudo make install

[180228 추가]

설치 완료 후 Python 바이너리(/usr/local/python3.x/bin/python3.x)를 실행하면 왜때문인지 "No such file or directory" 오류를 뱉어내며 비정상 종료되는데, 이는 설치한 Shared Library의 위치를 찾지 못해서 발생하는 문제입니다. [...] (Python Install 스크립트 오류 때문으로 생각됩니다.)
다음 명령으로 /usr/lib64 디렉토리(x86인 경우 /usr/lib)에 해당 Shared Library에 대한 심볼릭 링크를 만들어서 문제를 해결하도록 합니다.

ln -s /usr/local/python3.6/lib/libpython3.6m.so.1.0 /usr/lib64/libpython3.6m.so.1.0

FYI: LD_LIBRARY_PATH 환경변수에 라이브러리가 설치된 경로(/usr/local/python3.x/lib)를 추가해서 해결할 수도 있지만, 이 경우 User에 따라 실행이 안 될 수도 있으므로 링크를 만드는 방법을 사용했습니다.

다시 설치한 Python 바이너리를 실행해보고 이상이 없는 경우 다음 단계로 넘어가도록 합니다.

 

uWSGI및 uWSGI Python 플러그인 설치

uWSGI는 pip나 yum(또는 apt-get)으로 설치할 수도 있으나, 그럴 경우 별도로 빌드해서 붙이는 Python 플러그인과 충돌을 일으켜 Segmentation Fault Error를 발생시키는 등 상당히 성가시게 만듭니다. 패키지 관리자를 통해 Python 플러그인을 설치하면 이런 문제가 발생하지 않지만, 그러면 플러그인과 연동할 Python의 경로를 임의로 지정할 수 없습니다. 따라서, uWSGI를 직접 다운받아서 빌드해서 설치하고, Python 플러그인도 직접 빌드해서 붙이도록 합니다.

https://uwsgi-docs.readthedocs.io/en/latest/Download.html 에 접속해서 최신판 uWSGI를 다운받고 압축을 풀어줍니다. 여기서는 별도의 Configure과정 없이 바로 Make명령을 실행해 주면 빌드가 됩니다. (혹, 빌드과정에서 뭐가 없다고 짖어대면 오류메시지를 복사해서 구글에 붙여넣으면 설치방법을 쉽게 알 수 있습니다. 그대로 설치하고 다시 시도합니다. 이걸 성공할때까지 반복합니다.)

make PROFILE=nolang

[180617 추가]

"PROFILE=nolang" 옵션은 빌드시 시스템 전역에 설치된 Python을 기본 플러그인으로 포함하지 않도록 하기 위함입니다.

이 옵션을 포함하지 않고 빌드하면 이후 진행하는 설정 과정에서 아무리 플러그인을 별도로 빌드해서 지정해도 항상 시스템에 기본 설치된 Python만을 사용하게 됩니다. uWSGI 로그에 자꾸 Python 무슨 모듈이 없다고 난리를 치는 경우, 이 옵션을 빼먹지 않았는지 다시 한 번 확인하도록 합니다.

빌드가 완료되면 현재 디렉토리에 uwsgi라는 이름의 바이너리가 생기는데, 이를 /usr/local/bin/uwsgi로 복사해줍니다. (이동이 아니라 복사하라고 하는 이유는 바로 다음단계에서 또 써야 하기 때문입니다.)

cp ./uwsgi /usr/local/bin/uwsgi

uWSGI의 특이한 점 중 하나가, 바로 생성한 바이너리를 통해 붙일 플러그인을 빌드한다는 점입니다. 그래서 위에서 이동이 아닌 복사를 했습니다. 다음과 같이 Python의 경로를 지정하고 플러그인을 빌드해 줍니다.

PYTHON=/usr/local/python3.4/bin/python3.4 ./uwsgi --build-plugin "plugins/python python34"

[180228 추가]

플러그인 빌드시 다음과 같은 오류가 발생하면 Python 빌드 및 설치 과정을 잘못 수행한 것이므로 전 단계로 돌아가서 빼 먹은 절차가 없는지 다시 확인하도록 합니다.

i) Symbol 추가 오류:

*** uWSGI building and linking plugin from plugins/python ***
[gcc -pthread] python36_plugin.so
/usr/bin/ld: /usr/local/lib/python3.6/config-3.6m-x86_64-linux-gnu/libpython3.6m.a(abstract.o): relocation R_X86_64_32S against `_Py_NotImplementedStruct' can not be used when making a shared object; recompile with -fPIC
/usr/local/lib/python3.6/config-3.6m-x86_64-linux-gnu/libpython3.6m.a: error adding symbols: Bad value
collect2: error: ld returned 1 exit status
*** unable to build python36 plugin ***

→ Python 빌드 과정에서 configure를 수행할 때 --enable-shared 옵션을 주지 않은 것이므로 Python 빌드 및 설치를 다시 진행하도록 합니다.

ii) Shared Library 로딩 오류:

/usr/local/python3.6/bin/python3.6: error while loading shared libraries: libpython3.6m.so.1.0: cannot open shared object file: No such file or directory

→ Python 설치 후 Shared Library가 설치된 경로를 찾지 못해서 발생하는 오류입니다. 없다는 파일(libpython3.6m.so.1.0)이 어디 짱박혀 있는지 찾아서 /usr/lib64 (x86인 경우 /usr/lib)로 심볼릭 링크를 생성해 주도록 합니다.

빌드가 완료되면 역시 현재 디렉토리에 python34_plugin.so파일이 생성되는데, 이게 바로 uWSGI에 붙이는 Python 플러그인입니다. 이를 다음과 같이 적절한 곳에 옮겨줍니다.

mkdir -p /usr/local/lib/uwsgi/plugins
cp ./*.so /usr/local/lib/uwsgi/plugins

 

[TIP] 여러 프로젝트에서 서로 다른 버전의 Python을 uWSGI와 연동해야 한다면, 위의 Python설치 및 uWSGI설치 과정을 해당 Python버전으로 반복해 주면 됩니다. uWSGI에서 사용할 Python 플러그인은 각 데몬별로 지정해 줄 수 있기 때문입니다. 단, 플러그인 빌드시 다음과 같이 이름을 다르게 지정해 줘야 기존 파일을 덮어쓰지 않습니다.

예): Python 2.7.x 플러그인을 빌드할 때:

PYTHON=/usr/local/python2.7/bin/python2.7 \
./uwsgi --build-plugin "plugins/python python27"

참고: uWSGI를 이미 설치하고 빌드 디렉토리를 모두 삭제한 상태에서 플러그인을 추가 빌드하려는 경우, uWSGI 빌드를 생략하고 바로 플러그인을 빌드할 수 있습니다. 같은 버전의 uWSGI 소스코드를 받아서 압축을 풀고 위의 플러그인 빌드 명령을 실행하되, 이미 설치한 uWSGI 바이너리를 지정해 주도록 합니다.

 

프로젝트 디렉토리 이동

완성한 myproject디렉토리를 /var/www (혹은, 임의의 출판용 디렉토리)로 옮겨줍니다. 그리고, myproject/venv디렉토리를 삭제하고 VirtualEnv명령으로 샌드박스를 다시 생성합니다.

디렉토리를 이동하면 그 때마다 VirtualEnv 샌드박스를 다시 생성해 주어야 합니다.

/usr/local/python3.4/bin/python3.4 -m venv venv
source venv/bin/activate

[180617 추가]

이전에는 virtualenv명령으로 샌드박스를 생성하도록 소개했었는데("virtualenv venv --python=/usr/local/python3.4/bin/python3.4"), 이는 시스템에 설치된 Python을 가지고 샌드박스를 만드는 것이므로 올바른 방법이 아닙니다. (경우에 따라 오류가 나면서 생성 자체가 안되기도 합니다.)

사용할 버전의 Python 바이너리로 샌드박스를 만들어야 필요한 패키지 라이브러리가 올바르게 생성되며, 여러가지 Side Effect가 발생하지 않습니다.

다음으로 필요한 의존성 패키지들을 설치해 줍니다. 이 패키지들은 시스템 전역이 아닌 VirtualEnv 샌드박스 내에 설치됩니다.

pip install -r requirements.txt

 

uWSGI데몬 설정파일 작성

/var/uwsgi/sites-available/myproject.ini파일을 다음과 같이 작성합니다.

[uwsgi]
; Daemon Permission
uid         = nginx
gid         = nginx
 
plugins-dir = /usr/local/lib/uwsgi/plugins
plugins     = python34
 
chdir       = /var/www/myproject
module      = sitebase.wsgi
 
; Python Environment
python-path = /var/www/myproject
python-path = /var/www/myproject/venv/lib/python3.4/site-packages
 
; Socket Configurations
pidfile     = /var/run/nginx/myproject.pid
socket      = /var/run/nginx/myproject.sock
chmod-socket= 644 
processes   = 1
 
; Run in Daemon Mode
daemonize   = /var/log/nginx/myproject.log
 
; Additional Flags
master      = true
vacuum      = true
enable-threads = true

중요한 줄은 강조 표시를 해 두었습니다. 각 줄에 대한 설명은 다음과 같습니다.

[3-4] uid, gid: nginx:nginx권한으로 데몬(자식 프로세스)을 실행합니다.

[6] plugins-dir: 플러그인을 찾을 디렉토리를 지정합니다.

[7] plugins: Python 플러그인의 이름을 지정합니다.
(파일명이 python34_plugin.so일 때, 언더바 앞의 python34만 입력합니다.)

[13-14] python-path: Python Path를 지정합니다. 이 두 디렉토리는 꼭 포함되어 있어야 합니다.

[17-18] pidfile, socket: pid파일과 웹 서버와 통신을 위한 소켓 파일의 경로를 지정합니다. pid파일은 뒤에서 설명할 데몬 제어에 필요한 Process ID를 저장하고 있습니다.

[23] daemonize: 프로세스를 데몬 모드로 실행할 경우 이 필드에 로그파일명을 기재해 주도록 합니다. 설정 과정에서 디버깅을 할 때 이 줄을 주석 처리하면 로그 출력을 콘솔에서 바로 볼 수 있습니다.

 

uWSGI 데몬 시작하기

다음과 같이 uwsgi를 실행하면서 --ini옵션으로 위에서 작성한 설정 파일을 지정해 주도록 합니다.

sudo uwsgi --ini /var/uwsgi/sites-available/myproject.ini

참고로, uWSGI 데몬을 제어하는 명령들을 정리하면 다음과 같습니다. (PID는 PID파일의 내용을 의미합니다.)

[Start]

uwsgi --ini {{ INI 파일 }}

[Stop]

uwsgi --stop {{ PID 파일 }}
(또는)
kill -INT {{ PID }}

[Restart]

uwsgi --reload {{ PID 파일 }}
(또는)
kill -HUP {{ PID }}

 

Nginx 사이트 설정 파일 작성하기

Nginx 사이트 설정 파일을 다음과 같이 작성하고 활성화(nginx.conf에 include)한 뒤, Nginx를 재시작해줍니다.

server {
    listen 80; 
    server_name www.myproject.com;
    charset utf-8;

    location /static  {
        alias /var/www/myproject/static;
    }

    location /  {
        uwsgi_pass  unix:///var/run/nginx/myproject.sock;
        include     uwsgi_params;
    }   
}

server_name 부분은 운영에 사용할 도메인으로 적절히 수정하도록 합니다.

[6-7] static파일을 Nginx에서 바로 처리하기 위한 설정입니다. 이 설정에서 '/static'경로로 들어오는 요청은 모두 Nginx가 직접 /var/www/myproject/static디렉토리에서 찾아서 응답하게 됩니다. 따라서 Django로 직접 처리할 때에 비해 Static파일의 응답 속도가 빨라집니다. (참고: 이 구문은 'location / {...}'보다 위에 있어야 합니다.)

[11] '/'경로에 대한 요청을 모두 uWSGI소켓으로 연결시킵니다.

[12] uWSGI 파라미터를 함께 전달하기 위해 미리 정의된 파라미터 맵핑 파일을 Include합니다.

 

이제 브라우저를 열고 server_name에 지정한 도메인으로 접속하면 myproject사이트를 볼 수 있을 것입니다.

 

서비스 선택
이용중인 SNS 버튼을 클릭하여 로그인 해주세요.
SNS 계정을 통해 로그인하면 회원가입 없이 댓글을 남길 수 있습니다.
댓글
?
Powered by SocialXE

  • 김명준김명준 2015.11.18 13:49:27
    파이썬, 장고 관련해서 엄청 많은 사이트와 커뮤니티를 뒤졌는데 여기만큼 자세히 설명한 곳이 없네요.
    친절한 설명 감사합니다^^

Powered by Xpress Engine / Designed by Sketchbook

sketchbook5, 스케치북5

sketchbook5, 스케치북5