-
Flask Project - MyBlog 배포하기(nginx+gunicorn+flask with docker-compose)프로젝트 2024. 4. 18. 21:18반응형
이번에는 지금까지 만든 프로젝트를 클라우드 서비스를 활용해서 배포해보겠습니다.
배포를 하기 전에 꼭 알아야 하는 것이 있습니다. Flask app은 대표적인 WSGI(Web Server Gateway Interface) 웹 애플리케이션 중 하나로, 배포 시 웹 서버와 WSGI 서버와 묶여서 배포됩니다. 그렇다면 웹 서버는 무엇이고, WSGI 서버는 무엇인지 알아볼까요?
리버스 프록시 서버 구조 1. 웹 서버 - Nginx, Apache 등
웹 서버는 클라이언트로부터 HTTP 요청을 받아들이고, 정적 파일을 제공하거나 동적 요청을 처리하여 해당 요청에 대한 응답을 생성하는 소프트웨어입니다. 주로 정적 파일 서빙, 리버스 프록시, 로드 밸런싱, SSL 암호화 등의 기능을 제공합니다. 본 프로젝트에서는 리버스 프록시 역할을 위해 웹 서버 중 하나인 nginx를 사용할 예정입니다. 상대적으로 가볍고 빠르게 사용하기 좋습니다. flask와 연동하는 레퍼런스도 많기 때문에 추천드립니다.
2. WSGI 서버 - gunicorn, uWSGI, mod_wsgi
WSGI 서버는 WSGI 웹 애플리케이션과 웹 서버 간의 통신을 관리하는 인터페이스입니다. Nginx와 Apache같은 범용 웹 서버는 WSGI 처리 기능을 가지고 있지 않습니다.
그래서 Nginx와 Flask 애플리케이션간의 통신을 제어하는 WSGI 서버(gunicorn, uWSGI, mod_wsgi)가 필요합니다. 본 프로젝트는 가벼운 프로젝트이기 때문에 복잡하지 않은 프로젝트에 적합한 gunicorn을 사용하겠습니다.
3. WSGI 웹 애플리케이션 - Flask, Django
WSGI 웹 애플리케이션은 웹 서버와 웹 애플리케이션 사이의 표준 인터페이스입니다. 이 인터페이스는 웹 서버가 웹 애플리케이션을 실행하고 통신하는 방법을 정의합니다.
본 프로젝트에서는 3번만 구현이 완료된 상태입니다. AWS EC2에 웹 서버와 WSGI 서버를 구축하고 1번, 2번, 3번을 docker-compose로 묶어서 컨테이너로 묶으면 배포가 완료됩니다. 그 뒤에 CI/CD 툴을 사용해 CI/CD 시스템을 구축해보겠습니다. 최종적으로 CI/CD 시스템 구축까지 구현이 끝나면, 실제 사용자 피드백을 받고, 추가적인 기능 개발을 해보겠습니다.
클라우드 서비스를 사용해보기 전에, 기존 프로젝트를 새롭게 구성해보도록 하겠습니다.
기존에는 별도 브랜치 분기 없이 main 브랜치로 작업을 계속했었습니다. 그런데 이제 github의 본래 목적인 형상관리(버전관리)를 하기 위해 브랜치를 추가해보겠습니다. 본 프로젝트는 개인 프로젝트이기때문에 배포용(main), 개발용(dev) 브랜치 2개로 나누어 관리하겠습니다.
1. 로컬 프로젝트 폴더는 dev 브랜치와 연결해둔 뒤, 개발이 완료되면 dev 브랜치에 push를합니다.
2. main 브랜치에서는 pull request를 통해 변경 사항을 확인하고 merge를 진행합니다.
3. 이후 주요 테스트와 배포는 main 브랜치를 통해서 진행됩니다.
새롭게 구성된 폴더 구조입니다.
MyBlog_project | ├─flask_app │ ├─blog │ │ ├─config | | | ├─... │ │ ├─db | | | ├─blog.db │ │ ├─static │ │ ├─templates │ │ │ ├─auth │ │ │ └─views │ ├─migrations │ ├─tests | ├─venv | ├─.gitignore | ├─app.py | ├─Dockerfile | ├─Readme.md | └─requirements.txt ├─nginx | ├─default.conf | ├─Dockerfile | └─nginx.conf ├─docker-compose.yml └─run_docker.sh
1. flask_app
flask_app폴더에서 Dockerfile과 requirements.txt가 추가됐습니다. 그리고 blog.db 파일을 db 폴더 내부로 옮겼습니다.
requirements.txt 파일은 가상환경 활성화 상태에서 아래 명령어를 입력하면 됩니다. 그러면 의존성 라이브러리들과 버전이 기록됩니다. 본 프로젝트에서 gunicorn을 사용해 웹 서버와 플라스크 애플리케이션을 연결할 예정이기 때문에 기록하기 전에 gunicorn 설치 잊지 말아주세요!
pip install gunicorn (설치 시 생략) pip freeze > requirements.txt
blog.db 파일을 db 폴더로 옮긴 이유는, 나중에 docker-compose.yml 작성 시 설명하겠습니다. blog.db 파일의 경로가 변경되면서 Config 클래스에도 약간 변화가 생겼습니다. 아래처럼 BASE_DB_DIR 경로로 수정해주세요!
# config/__init__.py import os class Config(): BASE_DIR = os.path.dirname(os.path.dirname(__file__)) BASE_DB_DIR = os.path.join(BASE_DIR, 'db') BASE_DB_NAME = "blog.db" SQLALCHEMY_DATABASE_URI = 'sqlite:///{}'.format(os.path.join(BASE_DB_DIR, BASE_DB_NAME)) ''' 이후 생략 '''
Dockfile의 경우 해당 플라스크 애플리케이션을 실행할 환경을 이미지로 만들어주는 파일입니다.
# flask app Dockerfile # 베이스 이미지 선택 FROM python:3.8.8 # 작업 디렉토리 설정 RUN mkdir /flask_app WORKDIR /flask_app # 패키지 설치 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 애플리케이션 소스 코드 복사 COPY ./blog ./blog COPY app.py . # Port 명시 EXPOSE 8888
프로젝트 폴더에서 blog폴더와 app.py, requirements.txt만 필요하기 때문에 해당 파일들만 컨테이너 내부에 COPY해줬습니다.
2. nginx
nginx 폴더의 경우 nginx 웹 서버의 기본 config 파일인 nginx.conf, 가상 호스트 config 파일인 default.conf, 이들을 이미지로 만들 Dockerfile이 존재합니다.
nginx.conf 파일과 default.conf 파일은 가장 기본적인 것만 작성했습니다. 주석을 참고해주세요.
# nginx.conf user nginx; worker_processes 1; pid /var/run/nginx.pid; # 별도 모듈 사용시 주석 제거 # include /etc/nginx/modules-enabled/*.conf; events{ # 최대 동시 연결 숫자 worker_connections 768; # multi_accept on; } http{ ## # Basic Settings ## sendfile on; tcp_nopush on; tcp_nodelay on; # 다운로드 시 기본 파일 설정 include /etc/nginx/mime.types; default_type text/html; ## # Logging Settings ## # log 포맷 지정, 경로 지정 log_format main '$remote_addr - $remote_user [$time_local] "$request"' '$status $body_bytes_sent "http_referer"' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; error_log /var/log/nginx/error.log warn; keepalive_timeout 60; ## # Virtual Host Configs ## include /etc/nginx/conf.d/*.conf; # 여러 도메인 사용시 주석 해제 # include /etc/nginx/sites-enabled/*; }
# default.conf server { # nginx 서버 8080 포트 listen 8080; # server_name EC2 ip 주소 or 도메인(도메인 할당 받은거 있으면) location / { # 플라스크 WSGI 애플리케이션(서비스 이름) 8888포트 proxy_pass http://flask_app:8888; # 클라이언트가 요청한 호스트(백엔드 서버:플라스크 앱)의 헤더 유지 # 클라이언트 실제 IP 주소를 헤더에 추가 # 경유 프록시 서버의 IP 주소 기록 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }
위 conf파일들을 묶어서 이미지로 만드는 Dockerfile을 작성해보겠습니다.
# nginx Dockerfile # 베이스 이미지 선택 FROM nginx:1.18.0 # 새로운 conf 파일로 업데이트 RUN rm /etc/nginx/nginx.conf COPY nginx.conf /etc/nginx/ RUN rm /etc/nginx/conf.d/default.conf COPY default.conf /etc/nginx/conf.d
nginx 이미지를 가져오면, 해당 컨테이너에 존재하는 nginx.conf 파일과 default.conf 파일을 내 프로젝트 폴더에서 작성한 파일들로 대체하는 스크립트입니다.
3. docker-compose.yml
이제 2개의 컨테이너를 함께 실행하고 서로 통신하기 위해서 docker-compose를 사용하겠습니다.
# docker-compose.yml version: "3.7" services: flask_app: # 서비스 이름 build: context: ./flask_app # 도커 이미지 빌드 경로 dockerfile: Dockerfile # 도커 파일 이름 지정 container_name: flask_app # 컨테이너 이름 restart: always command: gunicorn -b 0.0.0.0:8888 app:app volumes: - /home/ubuntu/MyBlog_project/flask_app/blog/db:/flask_app/blog/db nginx_server: # 서비스 이름 build: context: ./nginx # 도커 이미지 빌드 경로 dockerfile: Dockerfile # 도커 파일 이름 지정 container_name: nginx_server # 컨테이너 이름 restart: always ports: - "8080:8080" # 호스트 포트 -> 컨테이너 포트 depends_on: - flask_app
해당 파일에서 중요한 부분들을 설명하겠습니다.
flask_app의 command: gunicorn WSGI 서버를 키고 모든 IP 주소에서 8888 포트로 들어오는 요청을 수신하라는 뜻입니다. 그리고 뒤에 {module 이름}:{Flask 객체 이름}은 Gunicorn이 실행할 WSGI 애플리케이션을 지정합니다.
flask_app의 volumes: 본래 컨테이너를 중지하고 다시 실행할 경우 컨테이너 내부의 메모리는 모두 초기화됩니다. 그런데 Database속 데이터는 컨테이너 실행과 무관하게 유지해야합니다. 이 때 volumes를 사용합니다. {호스트 시스템 경로}:{컨테이너 경로} = 호스트 시스템의 특정 경로를 컨테이너 내부에 마운트하여 공유하는 역할을 합니다. 따라서 컨테이너를 중지하고 다시 실행하더라도 해당 경로의 데이터는 영구적으로 유지됩니다.
nginx_server의 ports: 클라이언트가 요청한 URL은 {EC2의 IP 주소}:{Host Port}와 같습니다. 클라이언트가 요청한 port 번호를 내부 컨테이너 포트로 연결해주기 위해 ports를 사용합니다. default.conf 파일에서 8080 포트를 listen하도록 설정했기 때문에 8080 포트로 포트포워딩을 해주었습니다. 만약 default.conf에서 80포트를 listen하면 8080:80이 될것입니다.
nginx_server의 depends_on: 서비스 간 종속성을 정의하는 옵션입니다. 이 옵션은 하나의 서비스가 다른 서비스에 종속됨을 나타내며, 종속된 서비스가 먼저 시작되어야 함을 의미합니다.
4. run_docker.sh
docker-compose.yml로 컨테이너들의 관계를 정의했으므로 docker-compose 명령어를 통해 컨테이너를 실행하면 됩니다. 쉽고 빠르게 기존 컨테이너들을 kill하고 새롭게 시작하도록 하는 shell scripts를 작성해봤습니다.
# run_docker.sh echo killing old docker processes sudo docker-compose rm -fs echo building docker containers as daemon sudo docker-compose up --build -d
프로젝트 루트 폴더에서 터미널을 열고 아래 명령어를 입력하면 컨테이너가 빌드되고 실행됩니다. 다만 이를 실제로 실행시킬 서버는 클라우드 서버이므로, 아직은 입력할 필요 없습니다. 잘 돌아가는지 확인하려면 Windows에서는 docker desktop을 설치한 뒤 위 명령어에서 sudo를 빼고 입력해주세요.
bash run_docker.sh
이제 클라우드 서비스를 제공하는 대표적인 회사들 중 하나인 AWS에서, EC2를 사용해보겠습니다.
EC2(Elastic Compute Cloud)란?
아마존 웹 서비스(AWS)에서 제공하는 클라우드 컴퓨팅 서비스를 뜻합니다. 클라우드 컴퓨팅은 인터넷을 통해 서버, 스토리지, 데이터베이스 등의 하드웨어(컴퓨팅 리소스)를 제공하고, AWS에서 원격으로 제어할 수 있는 가상의 컴퓨터를 한 대 빌리는 것입니다. 사용한 만큼 비용을 지불하기 때문에 탄력적인 이라는 의미의 Elastic 이라는 단어가 붙어있고 Elastic은 비용적인 부분 뿐만이 아니라 필요에 따라 성능, 용량을 자유롭게 조절할 수 있다는 의미도 가지고 있습니다.
EC2 장점 간단히 말해서, 물리적 서버를 실제로 구성하기에는 시간이 오래걸리고 비용도 많이 들기 때문에 EC2를 사용하는겁니다. 특히 EC2 회원가입 후 1년은 프리티어를 사용할 수 있는데, 해당 AMI(Amazon Machine Image)는 무료입니다.
AWS 회원가입 후 EC2 생성하는 건 레퍼런스가 많으니 참고해서 인스턴스 생성해주시면 됩니다. 저는 Ubuntu 22.04로 인스턴스 생성을 했습니다. 윈도우에서 해당 인스턴스에 원격 접속하기 위해서는 키 페어를 .ssh폴더에 위치시키고 VSCode에서 접속할 수 있게 host 설정도 해주면 되는데, 여기서 SKT 공유기 쓰는 분들은 ssh port를 수정해주셔야합니다.
(참고: skt-브로드밴드-ssh막음)
VSCode 에디터든, AWS 직렬 콘솔로 접속하든, 일단 EC2 인스턴스에 접속해주세요.
Docker를 사용해 웹 서버, 애플리케이션을 이미지로 만들고 docker-compose를 이용해 하나로 묶어줄 예정이기 때문에 docker, docker-compose를 install해주세요.
sudo apt install docker docker-compose
/home/ubuntu/ 경로에 MyBlog_project 폴더를 생성해준 뒤 해당 폴더를 main 브랜치와 연결해줍니다. 그 뒤 pull로 해당 소스코드를 가져와줍니다.
git init git remote add origin {git_repo url} git branch -M main git branch --set-upstream-to=origin/main main git pull
이제 /home/ubuntu/MyBlog_project 폴더로 이동 후 아래 명령어를 입력해주면 컨테이너가 실행되면서, 서버로 접속 가능해집니다.
bash run_docker.sh
{EC2 서버 IP}:{8080} 경로로 클라이언트 요청 날려보세요! blog 홈페이지가 뜨면 성공입니다.
+ EC2 재부팅 시 쉘 스크립트 파일 자동 실행(service 파일 생성)
EC2가 종종 shut down되는 경우도 있고, 인스턴스 중지 후 재 실행하는 경우도 있기 때문에, 자동으로 컨테이너를 실행할 수 있도록 서비스 파일을 생성해보겠습니다.
# 파일 생성 후 편집 sudo nano /etc/systemd/system/run_docker.service [Unit] Description=RUN MyBlog Docker Compose Script [Service] Type=oneshot # 재부팅시 한번만 = oneshot User=ubuntu Group=docker WorkingDirectory=/home/ubuntu/MyBlog_project # 작업 디렉토리 설정 ExecStart=/bin/bash /home/ubuntu/MyBlog_project/run_docker.sh # bash run_docker.sh 와 동일 [Install] WantedBy=multi-user.target # 재부팅시 실행 옵션
위처럼 service 파일 생성 후 스크립트를 작성해주세요. 그리고 아래 명령어를 입력해서 확인해보면 됩니다.
sudo systemctl daemon-reload sudo systemctl enable run_docker.service sudo systemctl is-enabled run_docker.service # 재부팅시 실행 여부 확인 = enabled
반응형'프로젝트' 카테고리의 다른 글
Flask Project - MyBlog 댓글 추가하기(feat. flask-migrate) (0) 2024.04.22 Flask Project - MyBlog CI/CD 적용하기(feat. Github Actions) (1) 2024.04.19 Flask Project - MyBlog views(post 생성,수정,삭제) with unittest (0) 2024.04.11 Flask Project - MyBlog auth(회원가입, 로그인, 로그아웃) with unittest (0) 2024.04.09 Flask Project - MyBlog Flask-admin(관리자 페이지 생성 및 관리) (1) 2024.04.08