2018.11.02 - Deploy(2) - uWSGI와 Nginx

8 분 소요

uWSGI와 Nginx

nginx는 외부에서 오는 요청을 처리하는 데 특화가 되어있다. uWSGI에서는 Http 프로토콜을 쓰는 것이 아니라 Unix Socket(Unix에서 쓰는 프로토콜)이라는 것을 사용한다. 따라서 request에서 Django까지 가는 처리 로직은 다음과 같다

request  ->  Container  ->  Nginx  ->  uWSGI(Unix Socket)  ->  Django

처음 요청이 들어오면 도커에 있는 컨테이너로 보낸다. 이 컨테이너에서 받은 요청을 Nginx로 보내주고 uWSGI를 거쳐서 Django로 들어간다.

Docker에 Nginx와 uWSGI 설치

Nginx와 uWSGI를 도커에서 실행하기위해 지난 포스트에서 했던 ec2-deploy프로젝트의 Dockerfile를 다음과 같이 수정해준다.

# Docker를 ubuntu(v.18.04)에서 실행
FROM    	ubuntu:18.04
MAINTAINER 	dreamong91@gmail

# -y 속성은 설치 도중 yes/no 가 나왔을 때 자동으로 yes를 선택하는 옵션
RUN		apt -y update
RUN 		apt -y dist-upgrade
RUN		apt -y install python3-pip

# Nginx, uWSGI 설치 (WebServer, WSGI)
RUN		apt -y install nginx
RUN		pip3 install uwsgi

# docker build할때의 PATH에 해당하는 폴더의 전체 내용을 
# Image의 /srv/project/폴더 내부에 복사
COPY		./srv/project/
WORKDIR		/srv/project
RUN		pip3 install -r requirements.txt

# 프로세스를 실행할 명령
WORKDIR		/srv/project/app
CMD		python3 manage.py runserver 0:8000

# 현재 Django의 위치(ec2-deploy)의 파일들을 docker의 /srv/project/ 경로에 복사
COPY		./srv/project/
# /srv/project 로 이동
WORKDIR		/srv/project
# 이동한 위치에서 requirements.txt 안에 pip3 패키지들을 설치
RUN		pip3 install -r requirements.txt

WORKDIR	/srv/project/app
# 이동한 위치에서 runserver 실행
CMD		python3 manage.py runserver 0:8000

위의 내용을 Dockerfile에 수정해주고 나서 다시 도커를 build후 실행시킨다.

$ docker build -t ec2-deploy -f Dockerfile .
$ docker run --rm -it -p 7000:80 --name ec2 ec2-deploy /bin/bash

여기서 docker run명령어를 실행할때 –name ec2 를 지정해주면 run을 실행하여 만든 컨테이너에 다시 접근할 때 container ID를 몰라도 바로 저 ec2를 통해 바로 접근할 수 있다.

위의 명령어 두개를 통해 도커를 실행시킨 후 uwsgi를 입력해 본다.

root@dockerID/PATH:/srv/project/app# uwsgi

uWSGI setting

정상적으로 uwsgi가 설치되었다면 Command not found가 나타나지 않는다. 정상적으로 설치가 끝났다면 uWSGI설정을 해준다. 설정 방법은 다음과 같다.

# 설정 방법
uwsgi \
--http :(port) \
--home (virtualenv경로) \
--chdir <django프로젝트 경로> \
-w <설정 패키지명>.wsgi

# 터미널에 입력할 코드
uwsgi \
--http :8000 \
--chdir /srv/project/app \
-w config.wsgi

위의 코드를 정상적으로 입력하고 인터넷 창에 localhost:7000을 입력하여 접근하면 빈 창이 나온다. 만약 이미지 파일이 나타난다면 [캐시 삭제 후 새로고침] = ⌘ + ⇧ + R 을 눌러준다.

ec2-deploy프로젝트의 config.wsgi.py 파일을 보면 그 안에 아래의 함수가 선언되어있는 것이 보인다. 이 부분이 애플리케이션이라는 변수가 uwsgi에 전달해줄 Django의 정보를 wsgi의 규격에 맞게 정보를 갖는다.

application = get_wsgi_application()

settings 옵션을 환경에 따라 분리하기

먼저 confing안에 settings.py파일을 Convert to python packages한다. 그리고 개발환경이나 배포환경에 필요한 만큼 파일을 나누어 준다. 우리는 그 안에 base.py와 dev.py 그리고 production.py를 만들어 준다. 그럼 파일 구조가 이렇게 된다.

settings
	├── __init__.py
	├── base.py
	├── dev.py
	└── production.py

그리고 __init__.py안의 내용들을 모두 base.py에 옮겨준다. dev.py에는 개발용 설정을 넣어준다. 최소한의 코드만 넣어준다. 그리고 두 파일에 다음과 같이 입력한다. (정확히는 base.py파일에서 잘라내어 옮겨준다.)

# dev.py

from .base import *


DEBUG = True

WSGI_APPLICATION = 'config.wsgi.application'

# Database
# https://docs.djangoproject.com/en/2.1/ref/settings/#databases
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}


# production.py

from .base import *


DEBUG = False

WSGI_APPLICATION = 'config.wsgi.application'

# Database
# https://docs.djangoproject.com/en/2.1/ref/settings/#databases
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}

# 그리고 BASE_ROOT의 경로를 바꿔 준다. 
# dirname속성을 통해 한단계 더 올려준다
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

manage.py 파일을 보면 다음과 같은 코드가 있다.

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')

os.environ.setdefault는 os.environ라는 딕셔너리에 DJANGO_SETTINGS_MODULE가 있다면 그대로 두고 없으면 뒤의 config.settings를 넣는다는 뜻이다. 이 os.environ은 환경 변수를 나타낸다. 이 manage.py가 실행될 때 이 코드가 실행되는데 이 코드에서의 config.settings안의 __init__.py 파일이 비어있으므로 오류가 난다. 따라서 직접 코드를 입력하여 환경 변수를 추가 해 주어야 한다.

export DJANGO_SETTINGS_MODULE=confing.settings.<dev 또는 production>

dev와 production의 차이는 DEBUG의 True, False 차이

다음 코드를 입력하고 ./manage.py runserver를 입력하면 정상적으로 코드가 동작하는 것을 볼 수 있다. (localhost:8000에 연결한다.)

이 코드를 실행할 때 나는 이상한 오류가 났었는데 그 오류는 다른 파일에서 settings 모듈을 잘 못 import했기 때문에 난 오류였다. 항상 settings모듈을 import할 때는 from django.conf import settings에서 import를 해준다.

Nginx 설정 STATIC 설정

Django 공식문서 - Static-file setting

만약 Django의 Static file들을 설정하지 않고 바로 Deploy(배포)하게 되면 Django는 비효율적으로 static file들을 처리한다. 예를들어 다음과 같은 경로에 static file들이 존재한다고 하자.

STATICFILES_DIRS = [
	'정적파일을 찾을 경로1',
	'정적파일을 찾을 경로2',
	'정적파일을 찾을 경로3',
	'정적파일을 찾을 경로4',
	'정적파일을 찾을 경로5',

]
app/
	app1
		static/
	app2
		static/
	app3
		static/
	app4
		static/

static file 설정을 해주지 않으면 위의 경우에 Django는 하나의 static file을 찾기위해 9개의 경로를 탐색하며 static file을 찾는다. 하지만 static file 설정을 해주면 django.contrib.staticfiles에서 9개의 static폴더의 내용을 하나의 폴더에 합친다. STATIC_URL을 기준으로 온 내용은 위에서 합친 하나의 폴더 안에서 검색한다. 이 검색하는 기능을 Nginx(Web Server)에서 실행한다. 이렇게 STATIC File을 모아주는 코드는 collectstatic이다.

$ ./manage.py collectstatic

STATIC_ROOT를 파일을 제공할 경로로 설정해준다. 보통 app폴더와 동일한 위치에 .static폴더를 생성한다. 그리고 다음과 같이 STATIC_ROOT를 설정해 준다.

# config.settings.base.py
STATIC_ROOT = os.path.join(ROOT_DIR, '.static')

위의 설정이 다 끝나면 .gitignore에 /.static을 추가해준다.

Nginx, uWSGI 파일 만들기

Dockerfile안에 내용들을 실행하는데 이 내용들을 파일로 만들어 놓으면 더 편하다. ec2-deploy 바로 아래에 (app폴더와 같은 위치)에 .config파일을 만들고 그 안에 app.nginx파일과 uwsgi.ini파일을 만든다. 뒤에 확장자(nginx, ini)을 입력하고 생성했을 때 확장자가 제대로 입력되지 않는다면 pyCharm에서 ini4Ideanginx Support Plugins를 설치 한다.

이 Nginx가 할 수 있는 일들은 요청을 받고 처리하는 것이다. 이 요청을 가상 서버로 구분을 하는데 그 구분하는 단위를 __virturalhost__라고 한다. Nginx 확장자 폴더 안에 server라는 블럭을 하나 만들면 가상의 서버가 생성된다. 그 안에 설정을 추가해주면 된다. 아래의 내용으로 app.nginx파일을 채워준다.

# app.nginx

server {
    # 80번 포트로부터 받은 요청을 처리
    listen 80;
    # 도메인이 'localhost'에 해당 할 때
    server_name localhost;
    # 인코딩 방식
    charset utf-8;
    # request/response의 최대 사이즈
    # (기본값이 매우 작음)
    client_max_body_size 128M;

    #('/'부터 시작) -> 모든 URL연결에 대해
    location / {
        # uwsgi와의 연결에 unix소켓을 사용
        # "/tmp/app.sock"파일을 사용
        uwsgi_pass  unix:///tmp/app.sock;
        include     uwsgi_params;
    }
}

uWSGI를 소켓에서 동작하도록 만들어 놓고 Nginx가 그 소켓의 내용을 전달하도록 만들어준다. 그리고 이 uWSGI를 소캣을 통해 연결을 주고 받을 수 있도록 설정해준다. 그 설정을 uwsgi.ini에 해준다.

# uwsgi.ini

[uwsgi]
; python application의 경로 (우리의 경우엔 Django project)
chdir = /srv/project/app
; application과 WSGI를 연결해주는 모듈
wsgi = config.wsgi
; socket을 사용해 연결을 주고받음
socket = /tmp/app.sock
; uWSGI가 종료되면 자동으로 소켓 파일을 삭제
vacuum = true

.ini 확장자 파일에서는 주석을 ; 사용해 달아준다.

위의 설정을 끝낸 뒤에 다음 명령어 들을 입력하여 localhost에 접속해 본다.

$ docker run --rm -it -p 7000:80 --name ec2 ec2-deploy /bin/bash

# 도커 환경 안에서 입력해준다.
== Docker 실행중 ==
ubuntu# nginx

nginx명령어를 입력해도 아무 문구가 나타나지 않는다. 왜냐면 nginx는 백그라운드에서 작동하기 때문이다. Shell이 꺼지면 자동으로 nginx도 꺼지고 직접 작동을 멈추고 싶으면 nginx -s stop명령어를 입력하면 작동이 멈춘다.

위의 명령어 두개를 입력하고 다음과 같이 나오면 성공한 것이다.

nginx/sites-available

etc/nginx/sites-available로 이동하면 default파일이 있다. 이 파일을 확인해 보면 여러 내용들이 나온다. 확인하는 방법은 vim이 설치되어있지 않기 때문에 cat명령어를 통해 접근한다.

cat default

이 default파일 안에는 server설정과 root, Location 등 여러가지 설정들이 되어있다.

그리고 sites-available와 연결된 sites-enable이 있다. 예를 들어 설명하면 다음과 같다

etc/nginx/
sites-available/
	A
	B
	C
	D
sites-enabled/
	A(link)
	B(link)
	C(link)
	D(link)

etc/nginx/안에 sites-available, sites-enabled는 위와 같은 구조를 갖는다. 여기서 nginx는 실행될 때 sites-enabled만 실행한다. 따라서 A,B,C,D중에 C파일을 실행시키고 싶지 않으면 sites-enabled에 있는 C파일만 지워주면 된다. 그러면 원본 파일(sites-available, enabled에 있는 파일들은 available에 연결되어있는 파일들이기 때문)에 있는 데이터를 건드리지 않고 A,B,D만 실행시킬 수 있다.

따라서 초기에 설정되어있는 sites-available에 있는 default파일은 우리에게 필요가 없다. 그래서 이 default파일을 지워주고 우리가 작성한 .confing/app.nginx파일을 sites-available에 옮겨줘야한다. 이 과정을 Dockerfile에 작성해 준다.

Dockerfile 설정

# docker build -t ec2-deploy -f Dockerfile .
# . 까지 써줘야함

FROM        ubuntu:18.04
MAINTAINER  dreamong91@gmail.com

# 패키지 업그레이드, python3 설치
RUN         apt -y update
RUN         apt -y dist-upgrade
RUN         apt -y install python3-pip

# Nginx, uWSGI 설치 (WebServer, WSGI)
RUN         apt -y install nginx
RUN         pip3 install uwsgi

# docker build할때의 PATH에 해당하는 폴더의 전체 내용을
# Image의 /srv/project/폴더 내부에 복사
# requirements.txt의 내용이 변경이 없으면 RUN을 건너뛰고
# 변경사항이 있으면 RUN을 실행한다
COPY        requirements.txt /tmp/
RUN         pip3 install -r /tmp/requirements.txt

# 전체 소스코드 복
COPY        ./     /srv/project
WORKDIR     /srv/project

# settings모듈에 대한 환경변수 설정
# export DJANGO_SETTINGS_MODULE=config.settings.production
ENV         DJANGO_SETTINGS_MODULE  config.settings.dev

# 프로세스를 실행할 명령
WORKDIR     /srv/project/app
RUN         python3 manage.py collectstatic --noinput

# Nginx
# 기존에 존재하던 Nginx설정 파일들 삭제
RUN         rm -rf /etc/nginx/sites-available/*
RUN         rm -rf /etc/nginx/sites-enabled/*

# 프로젝트 Nginx설정파일 복사 및 enabled로 링크 설정
# cp 명령어의 -f 옵션은 강제 옵션이다. 그 파일이 있더라도 덮어 씌운다.
RUN         cp -f  /srv/project/.config/app.nginx \
                   /etc/nginx/sites-available
RUN         ln -sf /etc/nginx/sites-available/app.nginx \
                   /etc/nginx/sites-enabled/app.nginx

위의 Dockerfile을 만들어주고 다시 docker를 빌드하고 실행시킨다. 그리고 터미널을 하나 더 열어서 아래의 명령어들을 이어서 실행해 본다.

$ docker build -t ec2-deploy -f Dockerfile .
$ docker run --rm -it -p 7000:80 --name ec2 ec2-deploy /bin/bash

== docker 실행중 ==
root@46c35c4b6142:/srv/project/app# uwsgi --ini /srv/project/.config/uwsgi.ini

# New Terminal  
$ docker exec -it ec2 /bin/bash

== docker 실행중 ==
root@46c35c4b6142:/srv/project/app# nginx

uwsgi --ini /srv/project/.config/uwsgi.ini명령어는 socket파일을 만들어 준 것이다. 그 뒤 nginx는 nginx를 실행시킨 것이다. 이렇게 정상적으로 오류 없이 실행되면 localhost:7000에는 다음과 같은 화면이 뜬다.

502error

하지만 이러한 에러가 나와도 백그라운드에서 존재하는 nginx의 에러 메세지를 볼 수가 없다. nginx는 이러한 에러 메세지를 파일 형태로 보여주는데 이 에러 메세지는 /var/log/nginx에 저장된다. 이 경로에 가보면 error.log파일이 있다. cat 명령어를 통해 출력해보면 에러 메세지들이 나온다.

permission denied Error

docker안에서 ps -aux명령어를 입력하면 작업관리자 같은 내용이 쭉 나온다. 이 명령어는 유저 정보를 같이 보여주는데, 위에서 나온 502에러같은 경우 ‘www-data’ 유저가 ‘root’ 권한을 가진 socket파일에 데이터를 전송 할 수 없기 때문에 발생하는 permission denied error이다. 이 에러를 해결하기 위해 socket파일의 소유자 정보를 변경해준다. 방법은 uwsgi.ini 파일 마지막에 chown-socket = www-data를 추가해 준다.

# uwsgi.ini

[uwsgi]
; python application의 경로 (우리의 경우엔 Django project)
chdir = /srv/project/app
; application과 WSGI를 연결해주는 모듈
wsgi = config.wsgi
; socket을 사용해 연결을 주고받음
socket = /tmp/app.sock
; uWSGI가 종료되면 자동으로 소켓 파일을 삭제
vacuum = true
; socket파일의 소유자 변경
; chmod-socket = 777
chown-socket = www-data

여기까지의 설정을 정리하면 다음과 같다.

Host(7000)
	Container(80)
		Nginx
			URL ('/')
				/tmp/app.socket
					uWSGI
						config.wsgi
							Django

Host(7000) 으로 온 요청이 Container(80)으로 들어와서 Nginx으로 들어온 다음 Root URL이 (‘/’)인 경우에 /tmp/app.socket을 이용해서 uWSGI와 연결이 되고 config.wsgi을 통해 Django와 연결이 된 것이다.

Static File을 Nginx를 통해 처리

그러나 아직 static, media 경로를 통해 들어온 파일들에 대한 처리가 되지 않는다. 이 설정을 하기위해 app.nginx파일에 local 설정을 추가해준다.

# app.nginx

server {
    # 80번 포트로부터 받은 요청을 처리
    listen 80;
    # 도메인이 'localhost'에 해당 할 때
    server_name localhost;
    # 인코딩 방식
    charset utf-8;
    # request/response의 최대 사이즈
    # (기본값이 매우 작음)
    client_max_body_size 128M;

    #('/'부터 시작) -> 모든 URL연결에 대해
    location / {
        # uwsgi와의 연결에 unix소켓을 사용
        # "/tmp/app.sock"파일을 사용
        uwsgi_pass  unix:///tmp/app.sock;
        include     uwsgi_params;
    }

    location /static/ {
        alias /srv/project/.static/;
    }
    location /media/ {
        alias /srv/project/.media/;
    }
}

alias를 이용해서 static과 media 경로를 통해 들어온 파일들을 Django까지 가지않고 alias설정해준 경로에서 처리하게 한다.