본문으로 바로가기
반응형

목적

Django를 배포할때는 Uwsgi를 통해 Nginx 또는 Apache웹서버와 엮어서 배포한다.

이러한 경우에서 만약 AWS ECS를 사용한다면 Docker-compose를 통해 컨테이너화 시켜주는 작업이 필요하고

그러한 과정에 대해 기술한다.

구현

먼저 docker-compose파일은 다음과 같이 총 3개로 나뉜다.

docker-compose-dev.yml

version: '3.7'

services:
nginx:
build:
context: .
dockerfile: docker/nginx/Dockerfile
container_name: nginx
hostname: nginx
ports:
- '80:80'
networks:
- backend
restart: on-failure
links:
- web_service
depends_on:
- web_service

web_service:
build:
context: .
dockerfile: docker/web-dev/Dockerfile
container_name: web_service
hostname: web_service
ports:
- '8000:8000'
networks:
- backend
tty: true
depends_on:
- mongodb
links:
- mongodb
volumes:
- $PWD:/home
env_file:
- docker/env/dev.env

mongodb:
build:
context: .
dockerfile: docker/mongodb/Dockerfile
container_name: mongodb
hostname: mongodb
ports:
- '27017:27017'
networks:
- backend

networks:
backend:
driver: 'bridge'

docker-compose에는 총 3개의 컨테이너가 묶여있다.

  1. 배포를 진행하기 위한 웹서버인 nginx
  2. Django 어플리케이션과 Uwsgi/Supervisor가 들어있는 web_service
  3. 데이터베이스로 사용하는 mongodb

이렇게 총 3개의 컨테이너의 대략적인 연동만을 기술하고 각 컨테이너에서 필요한 세부 작업들은

build아래에 있는 dockerfile에 각 컨테이너의 도커파일의 경로를 지정함으로써 세분화시켜놨다.

docker-compose-prod.yml

version: '3.7'

services:
nginx:
build:
context: .
dockerfile: docker/nginx/Dockerfile
container_name: nginx
hostname: nginx
ports:
- '80:80'
networks:
- backend
restart: on-failure
links:
- web_service
depends_on:
- web_service

web_service:
build:
context: .
dockerfile: docker/web-prod/Dockerfile
container_name: web_service
hostname: web_service
ports:
- '8000:8000'
networks:
- backend
tty: true
depends_on:
- mongodb
links:
- mongodb

mongodb:
build:
context: .
dockerfile: docker/mongodb/Dockerfile
container_name: mongodb
hostname: mongodb
ports:
- '27017:27017'
networks:
- backend

networks:
backend:
driver: 'bridge'

dev파일과 크게 다른점은 없지만 web_service컨테이너 부분에서 volume과 env_file이 빠졌다.

volume은 로컬 환경의 폴더를 직접 마운트시켜서 도커를 띄워놓은 상태에서 로컬 파일의 변경이 감지되면

바로 반영하여 재시작할수 있도록 Hot-reload기능을 위해 dev환경에만 구성해놨다.

또한 env_file은 환경변수를 설정하는 파일의 경로를 지정하는곳인데, 배포환경에서는 AWS Secret Manager를 통해 환경변수를 가져오므로 제거했다.

docker-compose-ecs.yml

version: "3"
services:
web_service:
image: your_ecr_url
ports:
- "8000:8000"
links:
- mongodb
logging:
driver: awslogs
options:
awslogs-group: web_service
awslogs-region: ap-northeast-2
awslogs-stream-prefix: web_service

nginx:
image: your_ecr_url
ports:
- "80:80"
links:
- web_service
logging:
driver: awslogs
options:
awslogs-group: nginx
awslogs-region: ap-northeast-2
awslogs-stream-prefix: nginx

mongodb:
image: your_ecr_url
ports:
- "27017:27017"
logging:
driver: awslogs
options:
awslogs-group: mongodb
awslogs-region: ap-northeast-2
awslogs-stream-prefix: mongodb

이 부분은 ECS에 배포할 때 사용하는 파일이므로 기존 docker-compose와는 조금 다르다.

image부분에 ECR(레포지터리) 주소를 적어줘야한다.

ecs-params.yml

version: 1
task_definition:
services:
web_service:
cpu_shares: 100
mem_limit: 208435456
nginx:
cpu_shares: 100
mem_limit: 208435456
mongodb:
cpu_shares: 100
mem_limit: 208435456

마찬가지로 ECS에 배포할 때 실제 컨테이너의 용량등을 지정하는 파일이다.

각 서버의 환경에 맞게 구성해주면 된다.

/docker/nginx/Dockerfile

FROM nginx:latest
MAINTAINER hide

COPY . ./home
WORKDIR home
RUN rm /etc/nginx/conf.d/default.conf
COPY ./docker/config/nginx.conf /etc/nginx/conf.d/default.conf

다음으로 Nginx컨테이너의 Dockerfile이다.

default.conf에 Nginx 환경설정을 진행해놨고 해당 파일로 기존 환경설정파일을 덮어쓰는 명령어를 입력해놨다.

/docker/config/nginx.conf

upstream django {
# server unix:///home/expression/expression.sock; # for a file socket
server web_service:8001; # for a web port socket (we'll use this first)
}

# configuration of the server
server {
# the port your site will be served on
listen 80;
# the domain name it will serve for
server_name localhost; # substitute your machine's IP address or FQDN
charset utf-8;

# max upload size
client_max_body_size 75M; # adjust to taste

# Django media
location /media {
alias /home/expression/media; # your Django project's media files - amend as required
}

location /static {
alias /home/expression/static; # your Django project's static files - amend as required
}

# Finally, send all non-media requests to the Django server.
location / {
uwsgi_pass django;
include /etc/nginx/uwsgi_params;
}
}

맨 위쪽에 보면 upstream으로 django를 설정해놨다.

서버쪽에 보면 web_service라고 적혀있는데 이는 docker-compose에서 우리가 지정한 웹 컨테이너의 이름이다.

또한 아래쪽에서 media등 static파일들을 엮어주고 가장 아래쪽에 uwsgi_pass를 통해 uwsgi와 엮어준다.

보통 uwsgi와 파이썬 어플리케이션을 엮어줄 때 proxy_pass와 uwsgi_pass를 많이 사용하는데

공식 홈페이지에 따르면 uwsgi_pass 사용을 권장한다고 한다.

/docker/mongodb/Dockerfile

FROM mongo:latest
MAINTAINER hide

EXPOSE 27017
CMD mongod --bind_ip_all

모든 아이피로의 접근을 허용한다.

/docker/web-dev/Dockerfile

FROM python:3.6.5
MAINTAINER hide

COPY Pipfile ./home
COPY Pipfile.lock ./home
WORKDIR /home
RUN pip install pipenv
RUN pipenv install --system
RUN apt-get update && apt-get install -y uwsgi-plugin-python3 \
&& apt-get install -y uwsgi-plugin-python && apt-get install -y vim \
&& apt-get install -y git
RUN apt-get update && apt-get install -y vim && apt-get install -y git
RUN pip install uwsgi && pip install git+https://github.com/Supervisor/supervisor
CMD ./docker/config/run-dev.sh

필요한 디펜던시와 라이브러리를 설치하고 최종적으로 run-dev.sh 파일을 실행한다.

/docker/config/run-dev.sh

#!/bin/bash
supervisord -c /home/docker/config/supervisor-dev.conf

나중에 supervisor를 통해 연결시켜줘야 하므로 supervisor를 실행하는 쉘 스크립트이다.

/docker/config/supervisor-dev.conf

[supervisord]
logfile=/tmp/supervisord.log ; main log file; default $CWD/supervisord.log
logfile_maxbytes=50MB ; max main logfile bytes b4 rotation; default 50MB
logfile_backups=10 ; # of main logfile backups; 0 means none, default 10
loglevel=info ; log level; default info; others: debug,warn,trace
pidfile=/tmp/supervisord.pid ; supervisord pidfile; default supervisord.pid
nodaemon=true ; start in foreground if true; default false
minfds=1024 ; min. avail startup file descriptors; default 1024
minprocs=200 ; min. avail process descriptors;default 200
;umask=022 ; process file creation umask; default 022
;user=chrism ; default is current user, required if root
;identifier=supervisor ; supervisord identifier, default is 'supervisor'
;directory=/tmp ; default is not to cd during start
;nocleanup=true ; don't clean up tempfiles at start; default false
;childlogdir=/tmp ; 'AUTO' child log dir, default $TEMP
;environment=KEY="value" ; key value pairs to add to environment
;strip_ansi=false ; strip ansi escape codes in logs; def. false

[program:uwsgi]
command=uwsgi --py-autoreload 1 --socket /tmp/uwsgi.sock --single-interpreter --enable-threads /home/docker/config/uwsgi.ini
autostart=true
autorestart=true
;stderr_logfile=/home/logs/supervisor_err.log
;stdout_logfile=/home/logs/supervisor_out.log
stopsignal=INT
environment=DJANGO_CONFIGURATION="%(ENV_DJANGO_CONFIGURATION)s", IS_DOCKER="%(ENV_IS_DOCKER)s"

주목해서 볼 점은 아래쪽에 있는 command이다.

이 부분에 실제로 실행할 명령어를 적는다.

uwsgi를 통해 장고를 실행해줘야 하기 때문에 저러한 형태로 명령어를 입력해줬다.

가장 아래에 있는 environment를 통해 환경변수도 설정해줄 수 있다.

환경변수이름="%(ENV_시스템에_등록된_환경변수_이름)s" 형태라고 보면 된다.

/docker/config/uwsgi.ini

[uwsgi]
# The base directory (full path)
chdir = /home

# Django's wsgi file
module = expression.wsgi:application
master = true

# Maximum number of worker processes
processes = %(%k * 3)

# The socket (use the full path to be safe
socket = :8001
chmod-socket = 664
pidfile = /tmp/expression.pid

# Clear environment on exit
vacuum = true
max-requests = 5000

# Logging
;logto = /home/logs/uwsgi.log

# For lazy apps
lazy-apps = true

module쪽에 프로젝트이름.wsgi:application 형태로 작성해주면 해당 파일을 읽어서 실행시킨다.

/docker/web-prod/Dockerfile

FROM python:3.6.5
MAINTAINER hide

COPY Pipfile ./home
COPY Pipfile.lock ./home
COPY . ./home
WORKDIR /home
RUN pip install pipenv
RUN pipenv install --system
RUN apt-get update && apt-get install -y uwsgi-plugin-python3 \
&& apt-get install -y uwsgi-plugin-python && apt-get install -y vim \
&& apt-get install -y git
RUN apt-get update && apt-get install -y vim && apt-get install -y git
RUN pip install uwsgi && pip install git+https://github.com/Supervisor/supervisor
RUN chmod 777 /home/docker/config/run-prod.sh
CMD /home/docker/config/run-prod.sh

dev와 마찬가지로 라이브러리를 설치하고 최종적으로 run-prod.sh파일을 실행시킨다.

/docker/config/run-prod.sh

#!/bin/bash
python3 /home/docker/config/secret_manager.py
source ~/.bashrc
supervisord -c /home/docker/config/supervisor-prod.conf

추후에 AWS Secret Manager를 통해 저장된 환경변수를 불러올것이고 환경변수들을 .bashrc에 저장시킬 것이다.

바로 활성화시켜주기 위해 source 명령어를 사용했고 최종적으로 supervisor를 실행시킨다.

supervisor와 관련 내용들은 dev환경과 같으므로 설명을 생략한다.

/docker/config/secret_manager.py

# Use this code snippet in your app.
# If you need more information about configurations or implementing the sample code, visit the AWS docs:
# https://aws.amazon.com/developers/getting-started/python/

import boto3
import base64
from botocore.exceptions import ClientError
import ast
import os


def get_secret():
secret_name = "SecretManager이름"
region_name = "ap-northeast-2"

# Create a Secrets Manager client
session = boto3.session.Session()
client = session.client(
service_name='secretsmanager',
region_name=region_name
)
# In this sample we only handle the specific exceptions for the 'GetSecretValue' API.
# See https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html
# We rethrow the exception by default.

try:
get_secret_value_response = client.get_secret_value(
SecretId=secret_name
)
except ClientError as e:
if e.response['Error']['Code'] == 'DecryptionFailureException':
# Secrets Manager can't decrypt the protected secret text using the provided KMS key.
# Deal with the exception here, and/or rethrow at your discretion.
raise e
elif e.response['Error']['Code'] == 'InternalServiceErrorException':
# An error occurred on the server side.
# Deal with the exception here, and/or rethrow at your discretion.
raise e
elif e.response['Error']['Code'] == 'InvalidParameterException':
# You provided an invalid value for a parameter.
# Deal with the exception here, and/or rethrow at your discretion.
raise e
elif e.response['Error']['Code'] == 'InvalidRequestException':
# You provided a parameter value that is not valid for the current state of the resource.
# Deal with the exception here, and/or rethrow at your discretion.
raise e
elif e.response['Error']['Code'] == 'ResourceNotFoundException':
# We can't find the resource that you asked for.
# Deal with the exception here, and/or rethrow at your discretion.
raise e
else:
# Decrypts secret using the associated KMS CMK.
# Depending on whether the secret is a string or binary, one of these fields will be populated.
if 'SecretString' in get_secret_value_response:
secret = get_secret_value_response['SecretString']
return ast.literal_eval(secret)
else:
decoded_binary_secret = base64.b64decode(get_secret_value_response['SecretBinary'])
return ast.literal_eval(decoded_binary_secret)


if __name__ == '__main__':
env = get_secret()
if env:
for key, value in env.items():
os.system('echo "export {}={}" >> ~/.bashrc'.format(key, value))

secret_name에 자신의 SecretManager 이름을 적어주고 해당 내용을 불러와서 .bashrc에 저장시킨다.

그리고 위에서 나와있던 run-prod.sh 쉘 스크립트 내부에서 source를 통해 적용시킨다.

배포 및 실행 방법

개발 환경에서는 다음과 같이 실행하면 된다.


docker-compose -f docker-compose-dev.yml up


배포 환경에서는 다음과 같이 빌드한다.


docker-compose -f docker-compose-prod.yml build


그리고 아래와 같이 tag를 붙이고 ECR에 업로드한다.


docker tag 빌드된 nginx 이름:latest NGINX_ECR주소

docker push ECR주소

docker tag 빌드된 web_service 이름:latest WEB_SERVICE_ECR주소

docker push WEB_SERVICE_ECR주소

docker tag 빌드된 mongodb 이름:latest MONGODB_ECR_주소

docker push MONGODB_ECR_주소

반응형