규도자 개발 블로그

도커(Docker)환경에서 CentOS7에 django - gunicorn - nginx 사용하기 본문

DevOps/Docker

도커(Docker)환경에서 CentOS7에 django - gunicorn - nginx 사용하기

규도자 (gyudoza) 2018. 12. 2. 15:58

도커(Docker)환경에서 CentOS7에 django - gunicorn - nginx 사용하기

저번에는 CentOS7을 바탕으로 systemctl(systemd)및 httpd를 사용하는 방법을 알아봤었다.
도커(Docker)로 CentOS7 이미지 systemctl 사용하기 - 1

도커(Docker)로 CentOS7 이미지 systemctl 사용하기 - 2

이번에는 파이썬(python) 웹프레임워크인 쟝고, 혹은 장고(Django), 그리고 그와 함께 자주 쓰이는 nginx를 연동하는 방법을 알아보자. 데이터베이스는 장고와 다른 데이터베이스를 연동하는 게시물이나 가이드를 찾아보기 바란다. 나는 개인적으로 데이터베이스를 컨테이너로 관리한다는 것을 굉장히 위험하다고 생각하기 때문에 개발환경이면 모를까 실제 배포환경이라면 내가 사용할 클라우드플랫폼에서 제공하는 데이터베이스 서비스를 따로 이용할 것이다. 그러니까 이 게시물에서는 django에서 기본적으로 제공해주는 sqlite를 이용해 환경을 구성할 것이라는 얘기이다.

 일단 장고는 LAMP(Linux, Apache, Mysql or MariaDB, PHP)환경과 달리 웹서버가 받은 호출을 파이썬 인터프리터를 거쳐 해석한 뒤에 다시 요청자에게 뿌려주려면 중간에 다른 과정이 하나 더 필요하다. 바로 WSGI(Web Server Gateway Interface)라는 것인데 웹서버에서 http protocol을 통해 요청이 들어오면 WSGI를 통해 해당 내용이 해석되고 또 이 해석된 내용을 파이썬 인터프리터가 읽어서 연산한 뒤에 다시 또 WSGI를 통해 반환하고 또 이 반환된 값을 웹서버가 요청자에게 전달하 는 것이 그 과정이다. 그리고 이 WSGI역할을 하는게 바로 파이썬 패키지 중 하나인 gunicorn이다. (gunicorn공식홈페이지) 물론 gunicorn말고도 Bjoern, uWSGI, mod_wsgi, CherryPy등의 다른 wsgi도 존재한다.

 흔하게 사용되지 않는 배포 및 개발환경인 탓일까... Docker를 이용해 CentOS7이미지를 떠서 그 안에 django, gunicorn, nginx를 운용하는 환경을 만드는 방법은 그 어디에도 없었다. 그래서 수많은 시행착오와 구글링끝에 성공하였고 다른 사람들은 내가 했던 고생을 또 할 필요가 없으니 이 게시물을 읽고 마음껏 활용해주면 참 기분이 좋을 것 같다. 그럼 시작하겠다.



기본 이미지 준비하기

역시나 이번에도 도커 컨테이너 환경에서 CentOS를 사용하여 웹서버를 서비스하기 위해서는 systemctl을 사용할 수 있는 CentOS이미지가 필요하다. 만약에 해당 내용을 자세히 알고 싶다면 이 글을 시작할 때 올렸던 systemctl 사용하기 게시물을 참고하면 되겠다. 아니면 그냥 아래 내용대로 진행해도 무관하다.

 일단 CentOS에서 systemctl을 사용할 수 있게 docker-machine에서 일련의 작업이 필요하다. Docker Quickstart Terminal을 열고 아래 명령어를 입력하여 docker VM에 접속하자.

docker-machine ssh default

이런 화면이 나오면 성공한 것이다. boot2docker가 패치돼 저번 게시물과 모양이 달라졌다.

그리고 다음 명령어를 통해 컨테이너에서도 systemd를 사용할 수 있게 하자. 일단 sudo -i명령어를 통해 root권한을 획득하는 걸 잊지 말자.

mkdir /sys/fs/cgroup/systemd
mount -t cgroup -o none,name=systemd cgroup /sys/fs/cgroup/systemd

systemd디렉토리를 확인하고 다음과 같이 파일목록이 생겼으면 된 것이다.

그리고 아래 내용으로 dockerfile과 docker-compose.yml파일을 생성하여 한 디렉토리 안에 위치시킨다. (윈도우즈10 pro환경에서 docker toolbox를 사용하여 진행하였다. docker for windows는 docker를 실행하는 가상화 이미지에 ssh접속이 까다롭기 때문에 저번에도 말했다시피 docker toolbox를 사용하는 것을 권고드리는 바이다.)

 일단 아래 내용으로 dockerfile과 docker-compose.yml파일을 생성하여 한 디렉토리 안에 위치시킨다. (윈도우즈10 pro환경에서 docker toolbox를 사용하여 진행하였다. docker for windows는 docker를 실행하는 가상화 이미지에 ssh접속이 까다롭기 때문에 저번에도 말했다시피 docker toolbox를 사용하는 것을 권고드리는 바이다.)

 그리고 dockerfile에서 왜 굳이 그냥 centos아니라 버전을 명시해줬냐면 태그를 지정해주지 않으면 항상 latest태그가 자동으로 붙어서 베이스이미지가 되는데 이 latest이미지는 언제든지 바뀔 수 있으므로 아래와 같이 지정해주었다.

dockerfile

FROM centos:7.5.1804

ENV container docker

RUN (cd /lib/systemd/system/sysinit.target.wants/; for i in *; do [ $i == systemd-tmpfiles-setup.service ] || rm -f $i; done); \
rm -f /lib/systemd/system/multi-user.target.wants/*;\
rm -f /etc/systemd/system/*.wants/*;\
rm -f /lib/systemd/system/local-fs.target.wants/*; \
rm -f /lib/systemd/system/sockets.target.wants/*udev*; \
rm -f /lib/systemd/system/sockets.target.wants/*initctl*; \
rm -f /lib/systemd/system/basic.target.wants/*;\
rm -f /lib/systemd/system/anaconda.target.wants/*;

EXPOSE 80

VOLUME [ "/sys/fs/cgroup" ]

CMD ["/usr/sbin/init"]

docker-compose.yml

version: "3"

services:
  centos7-django:
    privileged: true
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "80:80"
    volumes:
      - /sys/fs/cgroup:/sys/fs/cgroup:ro
    cap_add:
      - SYS_ADMIN

이 상태에서 docker-compose up -d명령어를 실행하면 docker-compose.yml에 적힌 매개변수 및 설정을 가지고 docker 이미지가 생성되는 동시에 컨테이너로 올라간다.

성공적으로 이미지가 빌드되고 컨테이너로 올라간 모습이다. 컨테이너는 실행되었으니

docker exec -it {생성된 이미지 네임 혹은 컨테이너 아이디} /bin/bash

를 입력하여 생성된 컨테이너 안에 접속하자. docker-compose 명령어로 빌드한 컨테이너 혹은 이미지는 상위 디렉터리 이름과 함께 묶인 이름으로 생성되므로 각 사용자 환경마다 다른 이름으로 생성될 것이다. 이에 주의하자. 밑의 화면처럼 만들어진 컨테이너 안에 접속하였으면 systemctl status명령어를 통해 해당 컨테이너에서 systemd에 접근할 수 있는지 점검해보자. 아래와 같이 나온다면 일단 systemd를 사용할 수 있는 CentOS7컨테이너 생성에 성공한 것이다.


failed to get D-Bus connection: Operation not permitted에러가 출력된다면... 최상단에 CentOS 이미지 systemctl사용하기를 보고 오는 걸 권고드리는 바이다. 아무튼 systemctl문제는 해결하였으니 필요한 패키지들을 다운받자. 다음 명령어를 입력하면 된다.




python3, django, gunicorn설치 및 테스트해보기


$ yum -y install https://centos7.iuscommunity.org/ius-release.rpm
#ius커뮤니티 저장소를 등록
$ yum -y install python36u python36u-libs python36u-devel python36u-pip
#python3.6버전대의 필요한 패키지들 설치
$ yum -y install gcc gcc-c++ zlib-devel openssl openssl-devel sqlite sqlite-devel wget tree nginx vim
#필요한 기타 패키지들 설치

일련의 과정들이 끝나면 한번 nginx명령어를 통해 컨테이너가 제대로 작동 및 포트연결이 되어있는지 확인해보자. 그냥 nginx를 입력하면 nginx가 작동된다. docker quickstart terminal을 켰을 때 생성된 IP로 접속했을 때 아래와 같은 화면이 나타나면 성공한 것이다.


이 화면을 볼 수 있으면 CentOS7의 컨테이너에서 systemd와 nginx가 제대로 돌고 있는 것이다. 이것까지 확인했으면 nginx -s stop명령어를 통해 일단 nginx를 멈춰두자. 이제는 본격적으로 python과 django, gunicorn과 nginx를 연결해볼 시간이다.

일단 python이 잘 설치됐는지 확인해본 후 보다 쉽게 사용하기 위해 python3.6으로 등록돼있을 symlink를 python3로 변경하자 (그냥 python이라는 명령어를 쳤을 때 CentOS7에서는 기본적으로 설치돼있는 python2.x인터프리터를 실행하게 돼있다. 그렇다고 그냥 python을 쳤을 때 python3.x가 실행되게 하면 잠재적인 오류가 발생할 수 있다. 자세한 건 이 글을 참고하자.)

 일단 설치된 파이썬의 버전을 확인해보자. 위 패키지를 통해 설치된 파이썬은 3.6.x일 것이다.

$ python3.6 -V
>Python 3.6.x

그냥 python명령어를 쳤을 때 python3가 실행되게 하면 잠재적인 위험이 따르므로 python3를 쳤을 때 python3.6이 실행되게 하자.

$ cd /usr/bin
$ ln -s python3.6 python3
$ python -V
>>Python 2.7.5
$ python3 -V
>>Pyton 3.6.5

자 이렇게 우리는 python3가 필요할 때마다 타이핑 두 번을 더 하는 수고를 덜게 됐다. 맨 뒤의 2.7.x, 3.6.x넘버링은 시기나 OS버전에 따라 언제든지 바뀔 수 있으니 참고 바란다. 이제 우리는 python3라는 명령어로 python3를 사용할 수 있게 됐다. 이제 작업할 폴더로 이동하자. 나는 /var/www디렉터리를 만들어 웹어플리케이션을 위한 파일을 위치시킬 것이다. 이에는 python에서 거의 필수 요소라고 할 수 있는 venv과 django를 위치시킬 것이다. 그리고 venv안에 gunicorn을 포함시킬 것이다.

 어쩌면 어떤 사람들은 docker자체가 가상화기술인데 왜 또 그 안에서 pyvenv를 사용하는 데에 의문이 들겠지만 그냥 pyvenv를 사용하는 게 패키지관리가 더 쉽고 python자체를 사용하는 데에 더 편하기 때문이다. 하지만 이건 이 글의 주제가 아니므로 각설하고... 다음 명령어를 통해 파이썬 가상환경을 만들어 접속하자.

$ mkdir /var/www
$ cd /var/www
#프로젝트를 진행할 디렉토리를 생성하고 이동한다
$ python3 -m venv myprojectvenv
#myprojectvenv라는 파이썬 가상환경을 생성한다.
#생성하고 나면 현재 디렉토리에 myprojectvenv라는 디렉터리가가 생기는데 이것이 바로 pyvenv이다.
$ . myprojectvenv/bin/activate
#혹은
$ source myprojectvenv/bin/activate
#여기까지 제대로 진행됐으면 쉘 앞에 (myprojectvenv)가 붙을 것이다. 이제 다시 파이썬버전을 확인해보자. 가상환경에서 나가고 싶을 땐 해당 폴더의 bin/deactivate를 같은 방법으로 실행하면 된다.
(myprojectvenv)$ python -V
Python 3.6.5
#이제는 파이썬 3.6.5를 기반으로한 venv에 들어왔기 때문에 기본 python명령어가 python3를 가리키게 된다.
(myprojectvenv)$ pip install --upgrade pip
#pip를 최신버전으로 업데이트한다.
(myprojectvenv)$ pip install django gunicorn
#django와 gunicorn을 설치한다.

자, 여기까지 됐으면 가상환경에 필요한 패키지까지 전부 설치가 완료된 것이다. 이제 한번 django프로젝트를 만들어보자.

(myprojectvenv)$ cd /var/www/
(myprojectvenv)$ django-admin startproject myproject

이렇게 하면 www디렉토리에 myproject라는 디렉토리가 생긴다. 일단 django프레임워크가 제대로 작동하는지 django내장 웹서버를 실행해보자. 그 전에, 지금 실행되고 있는 nginx를 중지하자. nginx -s stop을 입력하면 nginx 웹서버 프로세스가 종료된다. 그리고 django프레임워크는 등록해놓은 host의 ip주소를 등록해야만 작동하기 때문에 docker에서 localhost와 연결돼있는 ip주소를 settings.py에 등록해야 한다. docker에서 사용하고 있는 ip주소는 docker quickstart terminal을 켰을 때 알 수 있다. 다른 방법이 있는지는... 잘 모르겠다. 딱히 의식하고 있는 부분이 아니였는데... 방법을 알고 계신 분은 알려줬으면 좋겠다. 구글링해도 나오지 않고 net-tools를 깔아서 ifconfig로 확인해봐도 192.168.99.100이라고 적힌 부분은 확인할 수 없었고 sh에서도 docker inspect {container id}를 입력해봐도 내부설정값에 대한 값만 나올 뿐이었다. 주소를 완전 까먹었으면 kitematic을 통해서 들어갈 수 있기는 하다.

아무튼 settings.py를 수정해보도록 하자.

(myprojectvenv)$ cd myproject/myproject/
(myprojectvenv)$ vim(혹은 vi, nano) settings.py

그리고 28번째 라인(2018년 12월 2일 django기준)에 있는 ALLOWED_HOSTS = []를 다음과 같이 변경하자.

...

ALLOWD_HOSTS = ['*']

...

여기에 docker와 localhost가 연결된 ip주소를 직접 입력해도 되지만 실제 배포환경에서는 ip가 다를 테니 모든 ip에서의 작동을 허용한다는 의미로 와일드카드를 지정해주었다. 그리고 정적디렉터리에 대한 선언도 필요하다. settings.py 최 하단에 STATIC_URL = '/static/'이라는 부분 밑에 다음과 같이 선언해주자.

...

STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static/')

...

이제 migrate과 collectstatic을 수행하여 django에서 자체적으로 지원하는 내장서버를 실행하여 제대로 django가 작동하는지 확인해보자. manage.py가 있는 위치로 가자. 아마 제대로 따라왔다면 /var/www/myproject안에 manage.pymyproject디렉토리가 함께 존재할 것이다. 아래과정을 수행하도록 하자.

(myprojectvenv)$ python manage.py migrate
(myprojectvenv)$ pyrhon manage.py createsuperuser
#superuser계정정보는 자기 입맛대로 만들자.
(myprojectvenv)$ python manage.py collectstatic
(myprojectvenv)$ python manage.py runserver 0.0.0.0:80

아래 화면을 볼 수 있다면 제대로 진행된 것이다.




주소 뒤에 /admin을 입력하여 createsuperuser를 했을 때 생성했던 계정으로 접속하면 관리자화면 또한 볼 수 있다.




아주 잘 작동한다. 여기까지 확인했으면 Ctrl + c명령어를 입력하여 웹서버를 종료시키자. 이번에는 gunicorn을 이용하여 웹서버를 실행해볼 것이다. manage.py가 있는 디렉토리 위치에서 다음 명령어를 실행시키자.

(myprojectvenv)$ gunicorn --bind 0.0.0.0:80 myproject.wsgi

그러면 꼭 django내장 웹서버를 실행시킨 것 같이 커맨드를 입력할 수 없는 상태가 되고 작동 중인 웹사이트에 대한 요청이 커맨드창에 출력될 것이다. 이번에도 /admin에 접속해보자.




이번에는 아까와 달리 css를 불러오지 못하는 모양새이다. gunicorn은 따로 정적파일에 대한 설정이 있어야 정상적으로 불러올 수 있어서 이렇게 된 것이다. 정상이니 너무 당황하지 말자. 여기까지 확인했으면 다시 커맨드창에서 Ctrl + c를 눌러 종료시키도록 하자. 이제는 django를 gunicorn + nginx로 함께 작동시킬 차례이다. 아까 위에서 설명한, deactivate를 통해 pyvenv에서 빠져나오는 방법을 실행해도 되고 실행하지 않아도 된다. 이제는 systemd를 통해 gunicorn서비스를 등록하는 방식으로 nginx와 연결시킬 것이므로 루트권한 및 systemd를 사용할 수 있는 CentOS이미지여야 한다. gunicorn systemd 서비스 파일을 작성해보자. 이제부터는 경로 그 자체를 nginx, gunicorn, systemd파일 여기저기에 넣게 되는 경우가 많으니 오탈자에 주의하도록 하자.

(myprojectvenv)$ vim /etc/systemd/system/gunicorn.service

그리고 아래 내용을 입력한다.

[Unit]
Description=gunicorn daemon
After=network.target

[Service]
WorkingDirectory=/var/www/myproject
ExecStart=/var/www/myprojectvenv/bin/gunicorn --access-logfile - --workers 3 --bind unix:/var/www/myproject/myproject.sock myproject.wsgi:application

[Install]
WantedBy=multi-user.target

위 ExecStart에서 볼 수 있다시피 해당 설정은 workers를 3으로 설정했으며 더 필요할 시에는 이 숫자를 느리도록 하자. 만약에 특정 그룹이나 특정 유저를 실행자로 지정하여 실행하게 하고 싶다면 [Service]란을 다음과 같이 작성하면 된다.

[Unit]
Description=gunicorn daemon
After=network.target

[Service]
User=service_executor
Group=www-data
WorkingDirectory=/var/www/myproject
ExecStart=/var/www/myprojectvenv/bin/gunicorn --access-logfile - --workers 3 --bind unix:/var/www/myproject/myproject.sock myproject.wsgi:application

[Install]
WantedBy=multi-user.target

이렇게 한다면 gunicorn을 systemd에 등록 및 실행할 때, 여태 만들어진 파일및 디렉토리등은 root가 작성하였기 때문에 권한문제가 발생하여 .sock파일 등을 디렉토리 안에 생성하지 못하고 gunicorn이 실행되지 못했다는 에러가 발생할 수 있다. 만약에 서버스 유저와 그룹을 따로 지정하였다면 우리가 작업하고 있는 /var/www디렉터리 안 요소들의 권한과 소유자를 적절하게 바꿔주도록 하자. 하지만 나는 그냥 root권한으로 진행하겠다.

위 내용을 제대로 입력하고 저장했다면 아래 명령어를 입력하여 gunicorn실행 및 부팅시 자동실행설정을 하자.

(myprojectvenv)$ systemctl start gunicorn
(myprojectvenv)$ systemctl enable gunicorn

그리고 제대로 작동하고 있는지 확인해보자.

(myprojectvenv)$ systemctl status gunicorn

다음 명령어를 입력했을 때 아래와 비슷한 화면이 나온다면 제대로 진행된 것이다.



제대로 실행되고 있지 못하다면 아래와 비슷한 화면이 나올 것이다.


딱 봐도 실패한 느낌이지 않은가. 위 에러내역을 잘 보면... 내가 gunicorn.service파일을 작성할 때 gunicorn이 설치돼있는 가상환경의 경로를 잘못지정했다는 사실을 알 수 있다. 중간 빨간색 글씨 부분의 myprojectenv에서 가운데 v를 까먹었다... virtual environment로 생각하고 만든 디렉토리 이름을 그냥 environment로 착각했나보다.
 행여 나처럼 service파일 내용을 잘못 입력하여 오류가 난 상황이라면 일단 systemctl stop gunicorn명령어로 gunicorn service를 중지시키고 systemctl daemon-reload명령어를 통해 수정된 service파일의 내용을 새로이 적용시켜줘야 한다. 그리고 다시 systemctl start gunicorn으로 실행하는 것도 잊지 말자.




nginx와 연동하기


gunicorn service를 성공적으로 등록하였고 실행까지 정상적으로 된다면 이제 nginx와 연결해보자. nginx의 설정파일을 수정할 차례이다. 다음명령어를 입력하여 nginx의 설정파일을 열자.

(myprojectvenv)$ cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.backup
(myprojectvenv)$ vim /etc/nginx/nginx.conf

그러면 각종 옵션으로 어지럽게 적혀있는 nginx의 설정들을 확인할 수 있을 것이다. 여기에서 직접 수정해도 되지만 그건 nginx에서 권고하는 방법이 아니므로 다른 방법으로 진행해보자. 일단 혹시모르는 불상사를 피하기 위해 해당 설정파일을 사전에 백업해놓았다.
설정파일을 열었다면 38번 라인부터 57번 라인까지... 그러니까 server에 대한 내용을 주석처리하자.




참, vim에디터에서 line number를 알고 싶다면 명령어창에서 :set number를 입력하면 된다. 물론 내용을 백업해놨으므로 주석처리가 아니라 그냥 지워도 된다. 이제 우리가 만든 프로젝트에 맞는 설정파일을 작성하자. 다음 명령어를 통해 다시 텍스트에디터를 킨다.

(myprojectvenv)$ vim /etc/nginx/conf.d/myproject.conf

그리고 아래 내용을 입력하도록 하자.

server {
        listen 80;
        server_name 192.168.99.100;

        location = /favicon.ico { access_log off; log_not_found off; }

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

        location / {
#                include proxy_params;
                proxy_set_header Host $http_host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto $scheme;
                proxy_pass http://unix:/var/www/myproject/myproject.sock;
        }

        error_page 404 /404.html;
            location = /40x.html {
        }

        error_page 500 502 503 504 /50x.html;
            location = /50x.html {
        }

}

보통 location / 란에는 include proxy_params;라는 구문이 적혀있는데 제대로 불러오지 못하는 경우가 많으므로 이런식으로 직접 써넣는 방법이 잘 통할 때가 많다. 위에 주석처리 스크린샷에서 볼 수 있다시피 nginx의 conf.d 디렉토리 안의 내용을 전부 불러오게 돼있으므로 이런식으로 관리하는 게 보편적이다. 전부 써놓고 저장했다면 nginx -t를 통해 구문을 검사해보자. syntax체크와 configuration체크가 전부 완료됐으면 다시 한번 실행해보자. nginx를 입력하면 nginx가 실행된다. 그리고 다시 접속해보자.


다시 이 반가운 화면을 만날 수 있다. 아까 gunicorn의 bind명령어로 실행했을 때와는 달리


/admin주소를 치고 들어가봐도 정적파일을 제대로 불러와 뿌려주는 모습이다.


자, 이렇게 Docker에 올려져있는 CentOS7 Container에서 django를 gunicorn wsgi와 nginx를 통해 작동하게 해보았다. 가끔 이 과정에서 nginx의 프로세스가 꼬여서 nginx -s stop 및 reopen등의 명령어가 먹지 않을 때가 있는데 이땐 ps -ef | grep nginx 명령어를 통해 현재 root권한으로 실행되고 있는 nginx프로세스를 죽이고 nginx를 재실행하면 된다. 이렇게 재실행하고 systemctl stop gunicorn, systemctl start gunicorn을 하면 제대로 django의 첫 화면을 만날 수 있고 nginx의 다른 명령어들도 제대로 먹힌다.

 사실 다 끝내고 나서 nginx를 이리저리 굴려보는데 nginx: [error] open() "/run/nginx.pid" failed (2: No such file or directory)에러가 나와서 당황했다. 아무튼 이렇게 간단히 해결되었다. 진짜 끝~

Comments