이번 미션은 Django 서버를 Docker와 GitHub Action으로 배포하는 것이었다
배포 아키텍처에 대한 이론을 정리하고 실습 내용을 간단하게 정리해보겠다!


1. Django 서버 배포 아키텍처

(1) WSGI란?

WSGIWeb Server Gateway Interface의 약자로(위스키라고 읽음), Web Server가 요청받은 정보를 Application에 전달하는 역할을 한다

Web Server는 Client의 정적인 요청을 처리하는 프로그램이며, 대표적으로 Apache, Nginx가 있다

만약 Client로부터 동적인 요청이 들어오면 Web Application Server(WAS)에요청을 위임하고, 요청에 대한 응답을 Web Server로 보낸다

이때 파이썬 애플리케이션(파이썬 스크립트)가 Web Server와 통신하기 위한 인터페이스가 WSGI이다


(2) Gunicorn이란?

Gunicorn은 Python WSGI로, Web Server(Nginx)로부터 요청을 받아 서버 애플리케이션(Django)으로 전달해주는 역할을 수행한다

Django의 runserver 역시 똑같은 역할을 수행하지만, 보안과 성능상의 이유로 production 환경에서는 사용할 수 없다고 한다


2. Docker

(1) 컨테이너(Container)란?

컨테이너는 애플리케이션을 관련 라이브러리 등과 함께 패키지로 묶어 소프트웨어 구동을 위해 만들어진 환경이다 이를 프로세스의 자원을 격리한다고 말한다
즉 OS 레벨의 가상화 기술인 것이다

컨테이너들은 하나의 운영체제를 공유해서 사용하지만, 컨테이너 각각은 독립된 프로세스와 메모리 영역을 사용한다


(2) Docker란?

도커(Docker)는 컨테이너(Container)를 관리하고 다루는 소프트웨어다
즉, 도커는 컨테이너 기반의 오픈소스 가상화 플랫폼이라고 볼 수 있다

Docker를 사용하면 OS에 관계없이 항상 같은 환경에서 서버가 실행되게 도와준다


(3) Image란?

Docker에서 Image는 컨테이너를 정의하는 읽기 전용 템플릿이다
Image는 컨테이너 실행에 필요한 파일과 설정값 등을 포함하고 있고, 상태값을 가지지 않고 변하지 않는다
그렇기 때문에 이 이미지를 이용한다면 언제든지 동일한 컨테이너를 만들 수 있다

ImageContainer의 스냅샷(snapshot)
ContainerImage의 한 인스턴스(instance)
라고 생각하자


(4) Docker Compose

Docker compose란 이미지를 여러 개 띄워서 이미지간 네트워크도 만들어주고 컨테이너의 밖의 호스트와도 어떻게 연결할지, 파일 시스템은 어떻게 공유할지(volumes) 제어해주는 기술이다

쉽게 DockerDockerfile(서버 운영 기록을 코드화한 것)을 실행시켜주고 docker-composedocker-compose.yml 파일을 실행시켜준다고 생각하자!


3. 로컬 환경에서 Docker 실행

로컬 환경에서 Docker를 실행시켜 서버를 띄우기 위해
Dockerfile과 docker-compose.yml 파일을 작성해준다

# Dockerfile

FROM python:3.8.3-alpine
ENV PYTHONUNBUFFERED 1

RUN mkdir /app
WORKDIR /app

# dependencies for psycopg2-binary
RUN apk add --no-cache mariadb-connector-c-dev
RUN apk update && apk add python3 python3-dev mariadb-dev build-base && pip3 install mysqlclient && apk del python3-dev mariadb-dev build-base


# By copying over requirements first, we make sure that Docker will cache
# our installed requirements rather than reinstall them on every build
COPY requirements.txt /app/requirements.txt
RUN pip install -r requirements.txt

# Now copy in our code, and run it
COPY . /app/

Dockerfile은 하나의 이미지를 만들기 위한 과정이다
RUN pip install -r requirements.txt 명령어를 통해 Docker로 띄운 환경에 라이브러리들을 설치한다

# docker-compose.yml

version: '3'
services:

  db:
    container_name: db
    #image: mysql:5.7 #window
    image: mariadb:latest #mac
    restart: always
    environment:
      MYSQL_ROOT_HOST: '%'
      MYSQL_ROOT_PASSWORD: mysql
    expose:
      - 3306
    ports:
      - "3307:3306"
    env_file:
      - .env
    volumes:
      - dbdata:/var/lib/mysql

  web:
    container_name: web
    build: .
    command: sh -c "python manage.py migrate && python manage.py runserver 0.0.0.0:8000"
    environment:
      MYSQL_ROOT_PASSWORD: -
      DATABASE_NAME: mysql
      DATABASE_USER: 'root'
      DATABASE_PASSWORD: -
      DATABASE_PORT: 3306
      DATABASE_HOST: db
      DJANGO_SETTINGS_MODULE: django_rest_framework_17th.settings.dev
    restart: always
    ports:
      - "8000:8000"
    volumes:
      - .:/app
    depends_on:
      - db

volumes:
  app:
  dbdata:
  

docker-compose -f docker-compose.yml up --build 명령어를 통해 로컬에서 docker를 실행시켜 서버를 띄우고 db를 연결한다

서버가 잘 띄워지는 모습이다!


4. 실 환경에서 서버 배포

(1) EC2 생성

AWS EC2와 RDS를 사용하여 배포할 것이다
EC2와 RDS를 생성하고 연결하는 과정은 velog 이전 포스팅에 잘 정리해놓았으니 간단하게만 설명하겠다

EC2를 생성할 때 가장 중요한 보안그룹 설정!

ssh(22), HTTP(80), HTTPS(443), Django(8000)을 인바운드 보안 그룹 규칙으로 추가해준다
보통 ssh 접속은 보안상의 이유로 내 아이피만 접속 허용하게 설정해두는 것이 좋다 나중에 바꿀 수 있으니 일단 모든 접속을 허용해놓았다

EC2 생성 완료 후 터미널로 접속해보았다
환경변수 설정을 통해ssh 설정한 명령어 만을 통해 터미널로 EC2에 접속 가능하게 설정해두었다

자세한 설정은 이전 포스팅을 참고하자!


(2) RDS 생성

AWS RDS를 설정하고 앞서 만든 EC2와 연결한 후 EC2에 접속하여 테스트해보았다

이렇게 mysql workbench랑도 연결해주었당


(3) Dockerfile.prod, docker-compose.prod.yml 파일 작성

로컬 환경과 다르게 ec2 실 배포 환경은 다르기 때문에 파일을 나누어 상황에 맞게 사용해야한다

# Dockerfile.prod
# BUILDER #
###########

# pull official base image
FROM python:3.8.3-alpine as builder

# set work directory
WORKDIR /usr/src/app


# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# install psycopg2 dependencies
RUN apk update && apk add python3 python3-dev mariadb-dev build-base && pip3 install mysqlclient

# install dependencies
COPY ./requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /usr/src/app/wheels -r requirements.txt


#########
# FINAL #
#########

# pull official base image
FROM python:3.8.3-alpine

# create directory for the app user
RUN mkdir -p /home/app

# create the app user
RUN addgroup -S app && adduser -S app -G app

# create the appropriate directories
ENV HOME=/home/app
ENV APP_HOME=/home/app/web
RUN mkdir $APP_HOME
RUN mkdir $APP_HOME/static
RUN mkdir $APP_HOME/media
WORKDIR $APP_HOME

# install dependencies
RUN apk update && apk add libpq
RUN apk update \
    && apk add --virtual build-deps gcc python3-dev musl-dev \
    && apk add --no-cache mariadb-dev
COPY --from=builder /usr/src/app/wheels /wheels
COPY --from=builder /usr/src/app/requirements.txt .
RUN pip install mysqlclient
RUN pip install --no-cache /wheels/*
RUN apk del build-deps

# copy entrypoint-prod.sh
COPY ./config/docker/entrypoint.prod.sh $APP_HOME

# copy project
COPY . $APP_HOME

# chown all the files to the app user
RUN chown -R app:app $APP_HOME

# change to the app user
USER app
# docker-compose.prod.yml
version: '3'
services:

  web:
    container_name: web
    build:
      context: ./
      dockerfile: Dockerfile.prod
    command: gunicorn django_rest_framework_17th.wsgi:application --bind 0.0.0.0:8000
    environment:
      DJANGO_SETTINGS_MODULE: django_rest_framework_17th.settings.prod
    env_file:
      - .env
    volumes:
      - static:/home/app/web/static
      - media:/home/app/web/media
    expose:
      - 8000
    entrypoint:
      - sh
      - config/docker/entrypoint.prod.sh

  nginx:
    container_name: nginx
    build: config/nginx
    volumes:
      - static:/home/app/web/static
      - media:/home/app/web/media
    ports:
      - "80:80"
    depends_on:
      - web

volumes:
  static:
  media:

(4) deploy.sh

브랜치에 코드가 푸쉬되면 GitHub Action이 자동으로 deploy.sh를 실행해준다

# deploy.sh

#!/bin/bash

# Installing docker engine if not exists
if ! type docker > /dev/null
then
  echo "docker does not exist"
  echo "Start installing docker"
  sudo apt-get update
  sudo apt install -y apt-transport-https ca-certificates curl software-properties-common
  curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
  sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable"
  sudo apt update
  apt-cache policy docker-ce
  sudo apt install -y docker-ce
fi

# Installing docker-compose if not exists
if ! type docker-compose > /dev/null
then
  echo "docker-compose does not exist"
  echo "Start installing docker-compose"
  sudo curl -L "https://github.com/docker/compose/releases/download/1.27.3/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
  sudo chmod +x /usr/local/bin/docker-compose
fi

echo "start docker-compose up: ubuntu"
sudo docker-compose -f /home/ubuntu/srv/ubuntu/docker-compose.prod.yml up --build -d

sudo docker-compose -f /home/ubuntu/srv/ubuntu/docker-compose.prod.yml up --build -d
deploy.sh의 마지막 명령어이다
결국 이 명령어를 실행시키는 것이 목적!

  • up: docker-compose 파일(f 파라미터가 가리키는)에 정의된 모든 컨테이너를 띄우라는 명령어
  • --build: up할때마다 새로 build를 수행하도록 강제하는 파라미터
  • -d: daemon 실행

(5) GitHub Actions

깃허브 레포지토리에 필요한 secret들을 설정해주고
코드 푸쉬를 하면 자동으로 deploy.yml을 통해 deploy.sh가 실행되고 서버가 띄워진다

name: Deploy to EC2

on:
  push:
    branches:
      - dev

jobs:

  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
    - name: checkout
      uses: actions/checkout@master

    - name: create env file
      run: |
        touch .env
        echo "${{ secrets.ENV_VARS }}" >> .env
    - name: create remote directory
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.HOST }}
        username: ubuntu
        key: ${{ secrets.KEY }}
        script: mkdir -p /home/ubuntu/srv/ubuntu

    - name: copy source via ssh key
      uses: burnett01/rsync-deployments@4.1
      with:
        switches: -avzr --delete
        remote_path: /home/ubuntu/srv/ubuntu/
        remote_host: ${{ secrets.HOST }}
        remote_user: ubuntu
        remote_key: ${{ secrets.KEY }}

    - name: executing remote ssh commands using password
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.HOST }}
        username: ubuntu
        key: ${{ secrets.KEY }}
        script: |
          sh /home/ubuntu/srv/ubuntu/config/scripts/deploy.sh

dev 브랜치에 코드가 푸쉬되면 deploy.yml이 실행되므로 dev 브랜치를 새로 만들어주었다


(6) 결과

api 테스트까지 완료 잘 돌아간당~


5. 겪은 오류

  • Server 500
    sudo docker logs --tail 20 -f 실행되고 있는 docker 컨테이너 ID
    명령어를 통해 에러 로그를 확인했더니
    migrate 명령어 추가하는 거 까먹었다 ㅎ
    ec2 접속한 김에 그냥 바로 migrate 해주었다 헤헤...;;

6. 최종 배포 아키텍처

직접 그려보았다 맞나요...?


7. 회고

EC2, RDS, GitHub Actions를 통한 서버 배포와 CD는 이전 프로젝트 배포에서 해보아서 이해하는 것에 크게 어려움은 없어서 수월하게 한 것 같다
이번 미션을 통해 Container와 Docker에 대해 조금 알게 되었다 더 공부해봐야겠당


profile
핸수

0개의 댓글

Powered by GraphCDN, the GraphQL CDN