[AWS] Docker(nginx + django + postgres)

이정연·2023년 6월 22일
3

Play Data

목록 보기
26/26

Why?

파이널 프로젝트를 진행하며 장고를 배포할 예정이다.

아직 장고 완성은 안 됐지만 추후에 반드시 필요한 과정이므로 테스트용 장고를 하나 만들어 배포 연습을 해보았다.

단계별로 하나씩 따라해보자.

개략도

  1. 로컬에서 개발한 장고를 깃허브 리포지토리에 올린다.
  2. 리포지토리에 있는 도커와 장고 파일을 EC2 인스턴스로 받아온다.
  3. EC2 인스턴스에서 docker-compose 파일을 실행하여 3개의 컨테이너를 운영한다.

여기서 nginx와 gunicorn의 차이에 대해 궁금하다면 아래 포스팅을 참조하자.

https://velog.io/@happyyeon/Play-Data-Final-Project-System-Architecture

리포지토리 생성

  • config: nginx 및 컨테이너 환경 설정 파일
  • docker-compose.yml: 여러 개의 컨테이너를 한꺼번에 빌딩하기 위한 파일
  • Dockerfile: 컨테이너 생성 설정
  • src: 장고 소스 코드

src

src 디렉토리 안에서 장고 프로젝트를 생성한다.

settings.py

1. secret key 분리

우리는 깃허브에 장고 프로젝트를 업로드 할 것이기 때문에 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

2. DB 환경설정

나는 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'로 할당했다.

3. 운영 정적 파일 위치 지정

# settings.py
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')

장고를 로컬에서만 개발할 때는 장고가 자동으로 정적 파일을 찾아서 제공해주지만 운영 환경에서는 nginx가 서빙하는 것이 훨씬 빠르다.

따라서, nginx가 접근할 수 있도록 정적 파일을 한 곳에 모아놓아야 하는데 그 위치가 바로 STATIC_ROOT다.

여기에 경로 지정을 해주어야 nginx가 정적 파일을 찾아서 서빙이 가능하다.

4. ALLOWED_HOST

ALLOWED_HOSTS = ['*']

장고를 배포시 외부에서 접속하는 ip를 allowed_hosts에 추가해주어야 한다.

여기서 *은 모든 ip를 허용하겠다는 의미다.

docker-compose.yml

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
  1. db
  2. web
  3. nginx

이 3개의 컨테이너를 띄울 것이다.

nginx와 postgres는 도커 이미지를 다운받아 올 것이고 web은 dockerfile을 이용해서 빌드할 것이다.

web은 db에 의존하고 nginx는 web에 의존한다.

다시 말해, db가 먼저 띄워져야 web을 띄울 수 있고 web이 띄워져야 nginx를 띄울 수 있다.

하지만 depends_on 옵션만으로는 의존성 문제를 완전히 해결할 수 없다.

depends_on은 단순히 출발 순서만 지정해주는 것이기 때문에 어떤 컨테이너가 먼저 띄워질지는 모른다.

위 그림과 같이 web이 db보다 먼저 띄워지면 참조할 db가 없기 때문에 오류가 발생한다. 운이 좋게도 db가 먼저 띄워지면 문제가 없겠지만 이를 항상 보장할 수는 없기 때문에 우리는 db가 완전히 실행될 때까지 웹 컨테이너에게 기다리라는 명령을 해주어야 한다.

docker-compose command

/src/wait-for-it.sh db:5432 --

추가

Dockerfile

COPY wait-for-it.sh /wait-for-it.sh
RUN chmod +x /wait-for-it.sh

추가

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

왜 컨테이너는 3개인데 Dockerfile은 1개일까?

이미지를 만드는 방법으로 크게 2가지가 있다.

  1. 이미 누군가 만들어 놓은 이미지를 가져온다.(도커 허브에서)
  2. Dockerfile을 이용해서 내가 직접 만든다.(커스터마이징 가능)

nginx와 db를 자세히 읽어보면 image 옵션이 있지만 web은 image 대신 build라고 되어있다.

다시 말해, image는 도커 허브에 만들어져있는 이미지를 불러오겠다는 뜻이고 build는 내가 만든 Dockerfile로 이미지를 생성하여 쓰겠다는 뜻이다.

나는 nginx와 postgres는 도커 허브 오피셜 이미지를 들고 왔고 web(django) 컨테이너만 Dockerfile을 이용했기 때문에 1개의 도커 파일이면 충분하다.

Dockerfile

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

config

  • nginx --> nginx 관련 설정 파일
  • requirements.txt --> django 관련 설정 파일

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의 설정 파일이다.

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개의 컨테이너를 운영하는 테스트를 해보는중)

AWS EC2

이제 EC2 인스턴스에서 리포지토리를 다운받아 실행시키기만 하면 된다.

리눅스 이미지로 t2.medium 정도로 생성한다.
(왜냐하면 나는 플레이 데이터에서 주는 지원금이 있기 때문!! ㅎㅎ)

그리고 보안 그룹 설정에서 80번 포트를 허용해주는 것을 잊지 말자.

본인 EC2 접속

도커 설치 후 정상 작동 확인

리포지토리 클론 후 도커 컴포즈 실행

나의 리포지토리 이름은 awstest이다.

백그라운드로 docker-compose.yml 실행

sudo docker-compose up -d

컨테이너가 잘 띄워진 것을 확인할 수 있다.

그리고 본인 퍼블릭ip로 접속하면 장고가 정상적으로 호스팅 되는 것을 볼 수 있다.

Reference

https://github.com/vishnubob/wait-for-it

profile
0x68656C6C6F21

0개의 댓글