파이널 프로젝트를 진행하며 장고를 배포할 예정이다.
아직 장고 완성은 안 됐지만 추후에 반드시 필요한 과정이므로 테스트용 장고를 하나 만들어 배포 연습을 해보았다.
단계별로 하나씩 따라해보자.
여기서 nginx와 gunicorn의 차이에 대해 궁금하다면 아래 포스팅을 참조하자.
https://velog.io/@happyyeon/Play-Data-Final-Project-System-Architecture
src
디렉토리 안에서 장고 프로젝트를 생성한다.
우리는 깃허브에 장고 프로젝트를 업로드 할 것이기 때문에 secret key를 가려줘야 한다.
settings.py 에서 secret key를 긁어서 secrets.json으로 생성 (manage.py와 같은 경로에 위치 시킨다.)
// secrets.json
{
"SECRET_KEY": "{본인 secret key}"
}
그리고 settings.py의 secret key 부분을 다음 코드로 대체 한다.
# settings.py
import os
with open("./secrets.json") as f:
secrets = json.loads(f.read())
SECRET_KEY = secrets['SECRET_KEY']
그리고 리포지토리 최상단 폴더에서 .gitignore 파일에 secrets.json을 등록한다.
secrets.json
나는 postgreSQL을 사용했기 때문에 기본 sqlite3 DB를 postgre로 바꿔준다.
# settings.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'db',
'USER': 'admin',
'PASSWORD': 'admin',
'HOST': 'db',
'PORT': '5432', # 5432는 PostgreSQL의 기본포트이다
}
}
아직 데이터베이스 생성은 안 했지만
name: db, user: admin, password: admin
로 생성할 것이기 때문에 위와 같이 작성했다.
이때 postgre의 host는 docker-compose 파일의 db 이름과 일치해야 한다.
나는 docker-compose의 db를
db:
image: postgres:12.2
container_name: ps01
environment:
POSTGRES_DB: db
POSTGRES_USER: admin
POSTGRES_PASSWORD: admin
이렇게 작성했다.
따라서 HOST를 'db'로 할당했다.
# settings.py
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
장고를 로컬에서만 개발할 때는 장고가 자동으로 정적 파일을 찾아서 제공해주지만 운영 환경에서는 nginx가 서빙하는 것이 훨씬 빠르다.
따라서, nginx가 접근할 수 있도록 정적 파일을 한 곳에 모아놓아야 하는데 그 위치가 바로 STATIC_ROOT다.
여기에 경로 지정을 해주어야 nginx가 정적 파일을 찾아서 서빙이 가능하다.
ALLOWED_HOSTS = ['*']
장고를 배포시 외부에서 접속하는 ip를 allowed_hosts에 추가해주어야 한다.
여기서 *은 모든 ip를 허용하겠다는 의미다.
version: "3"
services:
db:
image: postgres:12.2
container_name: ps01
environment:
POSTGRES_DB: db
POSTGRES_USER: admin
POSTGRES_PASSWORD: admin
web:
build: .
container_name: dg01
command: bash -c "
/src/wait-for-it.sh db:5432 -- &&
python3 manage.py collectstatic --no-input &&
python3 manage.py makemigrations &&
python3 manage.py migrate &&
gunicorn config.wsgi:application -b 0:8000"
depends_on:
- db
volumes:
- ./src:/src
- ./wait-for-it.sh:/src/wait-for-it.sh
nginx:
image: nginx:1.17.10
container_name: ng01
ports:
- "80:80"
volumes:
- ./src:/src
- ./config/nginx/nginx.conf:/etc/nginx/conf.d/default.conf
depends_on:
- web
이 3개의 컨테이너를 띄울 것이다.
nginx와 postgres는 도커 이미지를 다운받아 올 것이고 web은 dockerfile을 이용해서 빌드할 것이다.
web은 db에 의존하고 nginx는 web에 의존한다.
다시 말해, db가 먼저 띄워져야 web을 띄울 수 있고 web이 띄워져야 nginx를 띄울 수 있다.
하지만 depends_on
옵션만으로는 의존성 문제를 완전히 해결할 수 없다.
depends_on
은 단순히 출발 순서만 지정해주는 것이기 때문에 어떤 컨테이너가 먼저 띄워질지는 모른다.
위 그림과 같이 web이 db보다 먼저 띄워지면 참조할 db가 없기 때문에 오류가 발생한다. 운이 좋게도 db가 먼저 띄워지면 문제가 없겠지만 이를 항상 보장할 수는 없기 때문에 우리는 db가 완전히 실행될 때까지 웹 컨테이너에게 기다리라는 명령을 해주어야 한다.
/src/wait-for-it.sh db:5432 --
추가
COPY wait-for-it.sh /wait-for-it.sh
RUN chmod +x /wait-for-it.sh
추가
docker-compose.yml과 같은 위치에 wait-for-it.sh를 추가한다.
그리고 실행 권한을 부여해준다.
chmod +x wait-for-it.sh
#!/usr/bin/env bash
# Use this script to test if a given TCP host/port are available
WAITFORIT_cmdname=${0##*/}
echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }
usage()
{
cat << USAGE >&2
Usage:
$WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args]
-h HOST | --host=HOST Host or IP under test
-p PORT | --port=PORT TCP port under test
Alternatively, you specify the host and port as host:port
-s | --strict Only execute subcommand if the test succeeds
-q | --quiet Don't output any status messages
-t TIMEOUT | --timeout=TIMEOUT
Timeout in seconds, zero for no timeout
-- COMMAND ARGS Execute command with args after the test finishes
USAGE
exit 1
}
wait_for()
{
if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
else
echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout"
fi
WAITFORIT_start_ts=$(date +%s)
while :
do
if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then
nc -z $WAITFORIT_HOST $WAITFORIT_PORT
WAITFORIT_result=$?
else
(echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1
WAITFORIT_result=$?
fi
if [[ $WAITFORIT_result -eq 0 ]]; then
WAITFORIT_end_ts=$(date +%s)
echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds"
break
fi
sleep 1
done
return $WAITFORIT_result
}
wait_for_wrapper()
{
# In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692
if [[ $WAITFORIT_QUIET -eq 1 ]]; then
timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
else
timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
fi
WAITFORIT_PID=$!
trap "kill -INT -$WAITFORIT_PID" INT
wait $WAITFORIT_PID
WAITFORIT_RESULT=$?
if [[ $WAITFORIT_RESULT -ne 0 ]]; then
echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
fi
return $WAITFORIT_RESULT
}
# process arguments
while [[ $# -gt 0 ]]
do
case "$1" in
*:* )
WAITFORIT_hostport=(${1//:/ })
WAITFORIT_HOST=${WAITFORIT_hostport[0]}
WAITFORIT_PORT=${WAITFORIT_hostport[1]}
shift 1
;;
--child)
WAITFORIT_CHILD=1
shift 1
;;
-q | --quiet)
WAITFORIT_QUIET=1
shift 1
;;
-s | --strict)
WAITFORIT_STRICT=1
shift 1
;;
-h)
WAITFORIT_HOST="$2"
if [[ $WAITFORIT_HOST == "" ]]; then break; fi
shift 2
;;
--host=*)
WAITFORIT_HOST="${1#*=}"
shift 1
;;
-p)
WAITFORIT_PORT="$2"
if [[ $WAITFORIT_PORT == "" ]]; then break; fi
shift 2
;;
--port=*)
WAITFORIT_PORT="${1#*=}"
shift 1
;;
-t)
WAITFORIT_TIMEOUT="$2"
if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi
shift 2
;;
--timeout=*)
WAITFORIT_TIMEOUT="${1#*=}"
shift 1
;;
--)
shift
WAITFORIT_CLI=("$@")
break
;;
--help)
usage
;;
*)
echoerr "Unknown argument: $1"
usage
;;
esac
done
if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then
echoerr "Error: you need to provide a host and port to test."
usage
fi
WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15}
WAITFORIT_STRICT=${WAITFORIT_STRICT:-0}
WAITFORIT_CHILD=${WAITFORIT_CHILD:-0}
WAITFORIT_QUIET=${WAITFORIT_QUIET:-0}
# Check to see if timeout is from busybox?
WAITFORIT_TIMEOUT_PATH=$(type -p timeout)
WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH)
WAITFORIT_BUSYTIMEFLAG=""
if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then
WAITFORIT_ISBUSY=1
# Check if busybox timeout uses -t flag
# (recent Alpine versions don't support -t anymore)
if timeout &>/dev/stdout | grep -q -e '-t '; then
WAITFORIT_BUSYTIMEFLAG="-t"
fi
else
WAITFORIT_ISBUSY=0
fi
if [[ $WAITFORIT_CHILD -gt 0 ]]; then
wait_for
WAITFORIT_RESULT=$?
exit $WAITFORIT_RESULT
else
if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
wait_for_wrapper
WAITFORIT_RESULT=$?
else
wait_for
WAITFORIT_RESULT=$?
fi
fi
if [[ $WAITFORIT_CLI != "" ]]; then
if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then
echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess"
exit $WAITFORIT_RESULT
fi
exec "${WAITFORIT_CLI[@]}"
else
exit $WAITFORIT_RESULT
fi
이미지를 만드는 방법으로 크게 2가지가 있다.
nginx와 db를 자세히 읽어보면 image 옵션이 있지만 web은 image 대신 build라고 되어있다.
다시 말해, image는 도커 허브에 만들어져있는 이미지를 불러오겠다는 뜻이고 build는 내가 만든 Dockerfile로 이미지를 생성하여 쓰겠다는 뜻이다.
나는 nginx와 postgres는 도커 허브 오피셜 이미지를 들고 왔고 web(django) 컨테이너만 Dockerfile을 이용했기 때문에 1개의 도커 파일이면 충분하다.
FROM ubuntu:22.04
# 우분투에서 다운로드 속도가 느리기 때문에 다운로드 서버를 바꿔주었다
RUN sed -i 's@archive.ubuntu.com@mirror.kakao.com@g' /etc/apt/sources.list
# apt 업그레이트 및 업데이트
RUN apt-get -y update && apt-get -y dist-upgrade
# apt-utils dialog : 우분투 초기 설정 / libpq-dev : PostgreSQL 의존성
RUN apt-get install -y apt-utils dialog libpq-dev
# pip dev 설치
RUN apt-get install -y python3-pip python3-dev
# 파이썬에서 콘솔 출력이 느릴 경우 다음과 같이 환경 변수를 설정해준다.
ENV PYTHONUNBUFFERED=0
# Django 기본 언어를 한국어로 설정하면 파이썬 기본 인코딩과 충돌되어 한글 출력, 입력시에 에러가 난다.
# 따라서 파이썬 기본 인코딩을 한국어를 사용할 수 있는 utf-8으로 설정한다.
ENV PYTHONIOENCODING=utf-8
RUN mkdir /config
# 호스트에 있는 requirements.txt 파일을 컨테이너 내부로 복사해준다.
ADD /config/requirements.txt /config/
# requirements.txt에 있는 파이썬 패키지 설치
RUN pip3 install -r /config/requirements.txt
### 작업 디렉토리 ###
# Django 소스코드가 들어갈 폴더 생성
RUN mkdir /src;
# 작업 디렉토리 src로 변경
WORKDIR /src
COPY wait-for-it.sh /wait-for-it.sh
RUN chmod +x /wait-for-it.sh
requirements.txt
aiohttp==3.8.4
aiosignal==1.3.1
asgiref==3.6.0
async-timeout==4.0.2
attrs==23.1.0
certifi==2023.5.7
charset-normalizer==3.1.0
decouple==0.0.7
Django==4.2.1
django-environ==0.10.0
frozenlist==1.3.3
gunicorn==20.1.0
idna==2.10
multidict==6.0.4
openai==0.27.6
requests==2.30.0
sqlparse==0.4.4
tqdm==4.65.0
urllib3==2.0.2
yarl==1.9.2
psycopg2==2.8.4
aiohttp==3.8.4
aiosignal==1.3.1
asgiref==3.6.0
async-timeout==4.0.2
attrs==23.1.0
certifi==2023.5.7
charset-normalizer==3.1.0
contourpy==1.0.7
cycler==0.11.0
decouple==0.0.7
Django==4.2.1
django-environ==0.10.0
djangorestframework==3.14.0
fonttools==4.39.4
frozenlist==1.3.3
gunicorn==20.1.0
idna==2.10
JPype1==1.4.1
kiwisolver==1.4.4
konlpy==0.6.0
lxml==4.9.2
Markdown==3.4.3
matplotlib==3.7.1
multidict==6.0.4
numpy==1.24.3
openai==0.27.6
packaging==23.1
Pillow==9.5.0
PyMySQL==1.0.3
pyparsing==3.0.9
python-dateutil==2.8.2
pytz==2023.3
requests==2.30.0
six==1.16.0
sqlparse==0.4.4
tqdm==4.65.0
urllib3==2.0.2
wordcloud==1.9.1.1
yarl==1.9.2
nginx의 설정 파일이다.
nginx.conf
server {
listen 80;
location / {
proxy_set_header Host $host:$server_port;
proxy_pass http://web:8000;
proxy_redirect off;
}
location /static/ {
alias /src/.static_root/;
}
location /media/ {
alias /src/media/;
}
access_log /var/log/nginx/8000_access.log;
error_log /var/log/nginx/8000_error.log;
}
proxy_pass http://web:{8000}의 포트와
docker-compose.yml
의
gunicorn config.wsgi:application -b 0:{8000}의 포트가 일치해야 한다.
아까도 말했듯이 nginx는 정적 파일을 서빙해주는 툴이다. 따라서, static과 media의 위치를 알려준다.
80번 포트로 nginx를 열었고 이를 장고 8000번 포트에 링크시켜줬다.
무슨 말이냐 하면
위 그림과 같이 된다는 뜻. (그림에서는 DB 인스턴스를 따로 생성시켜줬지만 현재 실습 과정에서는 무시한다. 그냥 nginx, django, db 3개의 컨테이너를 운영하는 테스트를 해보는중)
이제 EC2 인스턴스에서 리포지토리를 다운받아 실행시키기만 하면 된다.
리눅스 이미지로 t2.medium 정도로 생성한다.
(왜냐하면 나는 플레이 데이터에서 주는 지원금이 있기 때문!! ㅎㅎ)
그리고 보안 그룹 설정에서 80번 포트를 허용해주는 것을 잊지 말자.
나의 리포지토리 이름은 awstest이다.
백그라운드로 docker-compose.yml 실행
sudo docker-compose up -d
컨테이너가 잘 띄워진 것을 확인할 수 있다.
그리고 본인 퍼블릭ip로 접속하면 장고가 정상적으로 호스팅 되는 것을 볼 수 있다.