[배포/Docker] Docker + Github Action을 이용한 자동배포2 (feat. Django)

이수진·2022년 3월 18일
5

이전에 벨로그에 Docker + Github Action 관련해서 글을 올렸었던 적이 있습니다.
( 링크 -> https://velog.io/@ssssujini99/Docker-도커란-Docker-GitHub-action을-이용한-자동배포-해보기 )
그때에는 Docker에 관해서 처음 접했고, 배포도 처음이어서 거의 도커가 무엇인지, 가상 컨테이너 등등 주로 "개념"에 초점을 맞추어서 공부를 했습니다.

이번엔 운영진으로서 동아리 스터디를 준비하는 과정에서는 직접 레포도 파고,
그리고 이 전에 도커로 배포만 약 서너번을 했었기 때문에
이번엔 폴더 구조, 그리고 실제적인 배포 플로우를 마지막으로 정리해보려 합니다.


📍 원리: Docker + Github Action

💡Why Docker?

-> 도커는 컨테이너 기반의 오픈소스 가상화 플랫폼으로, 이 "컨테이너"는 애플리케이션을 환경에 구애받지 않고 실행하는 기술입니다.
예를 들면, 원래는 서버를 다른 OS로 띄운다 한다면 원래는 설치 방법도 각 환경마다 다르고, OS가 서로 달라서 배포시에 생기는 문제도 많지만,

🔥어떤 운영체제에서도 같은 환경을 만들어주는 것🔥

이것이 바로 도커의 가장 큰 장점입니다. 그래서 서버에 Docker만 깔고 배포해도 됩니다.

💡Why Github Action?

-> 서버에 접속해서 docker를 실행시켜 주고, 방금 master에 push된 커밋을 복사해주는 역할, 즉 이를 Github Actions가 해줍니다. 자세한 건 글 뒤에서 설명해드리겠습니다.


📍 개발 환경 및 폴더 구조

웹 서버로는 Nginx, WSGI 서버로는 gunicorn, Application 서버로는 django, 데이터베이스로는 mysql을 이용하였습니다.
여기에 이제 docker, docker-compose로 배포를 진행하였습니다.

  • 폴더구조
├── django_rest_framework_15th
    ├── .github/workflows/deploy.yml
    ├── api(앱 폴더)
    ├── django_rest_framework_15th(프로젝트 폴더)
    
    ├── config
        ├── docker/entrypoint.prod.sh
        
        ├── nginx/Dockerfile
        ├── nginx/nginx.conf
        
        ├── scripts/deploy.sh
        
    ├── Dockerfile(로컬용)
    ├── Dockerfile.prod(배포용)
    ├── docker-compose.yaml(로컬용)
    ├── docker-compose.prod.yaml(배포용)
    ├── manage.py
    ├── requirements.txt
    

📍 Docker vs. docker-compose

  • Docker ---(실행)---> Dockerfile
  • docker-compose ---(실행)---> docker-compose.yml

둘의 차이점은 다음과 같습니다.

(이미지, 컨테이너 등의 개념은 이전 저의 글 Docker Github Action에 자세히 있으므로, 이런 개념들은 생략하고 넘어가겠습니다.)

Dockerfile은 하나의 이미지를 만들기 위한 과정입니다,
도커 컨테이너로 시스템을 구축하면 하나 이상의 컨테이너가 서로 통신하며, 그 사이에 의존관계가 생기게 됩니다. 이런 방식으로 시스템을 구축하다보면 단일 컨테이너를 다룰 때는 문제가 되지 않던 부분에도 주의가 필요하게 됩니다. 이때 필요한 것이 "Docker Compose" 입니다.

쉽게말하자면, 하나의 시스템은 여러 어플리케이션이 서로 의존성 있게 구성되어 이루어져 있습니다.
그래서 하나의 컨테이너가 하나의 어플리케이션을 담당하기 때문에 여러 개의 컨테이너가 필요합니다.
그래서 Docker Compose가 필요하며, 이는 yaml 포맷으로 작성되고,
🔥여러 컨테이너들을 한 번에 관리를 할 수 있게 도와주는 역할을 합니다.🔥

이제 각각을 살펴볼 것인데,
저희는 로컬용, 배포용을 따로 두어서 관리를 하는데
배포용만 살펴보도록 하겠습니다.

  • Dockerfile.prod (web 컨테이너 설정에 해당하는 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.yaml
version: '3' # 버전 지정
services: # 컨테이너 설정

  web: # 컨테이너1 : web
    container_name: web
    build:
      context: ./
      dockerfile: Dockerfile.prod # 배포용인 Dockerfile.prod
    command: gunicorn django_rest_framework_15th.wsgi:application --bind 0.0.0.0:8000 # 배포에서는 gunicorn으로 웹서버(nginx) -> 장고 연결
    environment:
      DJANGO_SETTINGS_MODULE: django_rest_framework_15th.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: # 컨테이너2 : nginx
    container_name: nginx
    build: ./config/nginx # 여기에는 Dockerfile이 있으며 이 도커파일에서 nginx에 대한 상위 설정파일인 nginx.conf가 있음
    volumes:
      - static:/home/app/web/static
      - media:/home/app/web/media
    ports:
      - "80:80"
    depends_on:
      - web # 웹 컨테이너에 의존 (그래서 web 컨테이너를 먼저 띄운 후, nginx 컨테이너를 띄움)

volumes:
  static:
  media:

그러면 nginx 컨테이너에 해당하는 Dockerfile과 여기에 해당하는 설정파일이 들어있는 nginx.conf도 함께 확인해보도록 하겠습니다.

  • config/nginx/Dockerfile
FROM nginx:1.19.0-alpine # nginx 베이스 이미지 지정

RUN rm /etc/nginx/conf.d/default.conf # 기존 설정 파일 지우고
COPY nginx.conf /etc/nginx/conf.d # 직접 설정한 nginx.conf 파일로 기존 설정 파일 대체

직접 설정한 nginx.conf 파일로 기존의 파일을 대체합니다.
그리고 이에 해당하는 config/nginx/nginx.conf 도 함께 살펴보겠습니다.

  • config/nginx/nginx.conf
upstream django_rest_framework_15th { # django_rest_framework_15th 라는 upstream 서버 지정 (nginx 입장에서는 django가 upstream 서버)
  server web:8000; # web 컨테이너의 8000포트에 연결 (web은 아까 만든 컨테이너1에 해당)
}

server { # nginx 서버 정의

  listen 80; # 80포트를 열어주기 (http)

  location / { # "/" 도메인에 도달하면 아래 proxy를 수행
    proxy_pass http://django_rest_framework_15th; # django_rest_framework_15th라는 upstream으로 요청을 전달
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $host;
    proxy_redirect off;
  }

  location /static/ { # "/static/" 도메인에 도달하면 아래 alias를 수행
    alias /home/app/web/static/; # 아래 디렉토리(서버 파일시스템)을 맵핑
  }

  location /media/ {
    alias /home/app/web/media/;
  }
}

-> 여기서 설정한 파일대로 요청이 들어오고 응답을 정리해보면 다음과 같습니다.

먼저, 클라이언트로부터 요청이 들어오면(http)
앞단의 Nginx의 80 포트를 오픈합니다.
디폴트 도메인('/')에 도달하면, upstream서버(django)로 요청을 전달하는데
-> 즉, (설정파일에서 볼 수 있듯이) web컨테이너 8000포트에 연결하도록 설정하였습니다.

지금까지 설명한 전체적인 플로우를 그림으로 나타내면 다음과 같습니다.

엄청 이해가 잘 될 겁니다☺️


📍 Github Actions (깃헙 액션)

직접 docker-compose up 명령어를 이용해서
docker-compose.yaml 파일을 실행해 컨테이너를 띄울 수도 있는데, 굳이 Github Actions를 통해 진행할까요?

깃에서 직접 푸쉬도 하고, 또한 배포도 따로 진행해야하면 이는 번거로운 작업이 될 수 있습니다. 그래서 Github에서 제공하는 배포를 자동화하여 처리하는 Github Actions를 이용하는 것입니다.

✅ Github Actions? -> 즉, 푸쉬를 하면 자동으로 배포될 수 있도록 도와줍니다.

Github Actions가 실행시켜주는 파일을 확인해보면 다음과 같습니다.

  • .github/workflows/deploy.yml
name: Deploy to EC2
on: [push]
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

Github Actions가 실행시켜주는 파일의 맨 마지막을 보면 다음과 같은 명령어가 있습니다.

sh /home/ubuntu/srv/ubuntu/config/scripts/deploy.sh

바로 이 deploy.sh를 뜯어보러 갑시다.

  • confing/scripts/deploy.sh
# !/bin/bash

# docker가 없다면, docker 설치
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

# docker-compose가 없다면 docker-compose 설치
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 # 하이라이트 명령어

if ! type docker > /dev/null, if ! type docker-compose > /dev/null 이 두 코드블럭은 docker와 docker-compose를 깔아주는 명령어입니다.
처음 생성된 ec2 인스턴스에는 아무것도 없기 때문에 도커 환경을 만들어주어야 합니다.
다음과 같은 명령어를 통해 ec2 인스턴스 안에 도커 환경이 세팅된다고 볼 수 있습니다.

그리고 마지막에 위치한 명령어,

sudo docker-compose -f /home/ubuntu/srv/ubuntu/docker-compose.prod.yml up --build -d

🔥가장 중요한 명령어!🔥

이 커맨드를 통해 docker-compose.prod.yaml 파일이 실행되고, 도커 컨테이너가 빌드됩니다!
이 스크립트 파일은 Github Actions가 수행했고, 이 스크립트 파일은 EC2 서버에서 실행되고 있습니다. 결국, 이 커맨드에 의해 서버가 build되고 실행된다고 보면 됩니다.

💡 정리하자면,
생성된 ec2 인스턴스 안에 도커 환경을 세팅을 한 뒤에, 도커 컨테이너들을 빌드한다고 보면 됩니다😊


📍 터미널에서 직접 도커 컨테이너 확인하기

ubuntu@ip-172-31-42-158:~$ sudo docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED        STATUS        PORTS                               NAMES
c9c6c0c18ade   ubuntu_web     "sh config/docker/en…"   29 hours ago   Up 29 hours   8000/tcp                            web
048b2ed420b5   ubuntu_nginx   "/docker-entrypoint.…"   2 days ago     Up 2 days     0.0.0.0:80->80/tcp, :::80->80/tcp   nginx

도커 명령어를 통해, 현재 web 컨테이너와 nginx 컨테이너가 잘 띄워져있는것을 볼 수 있습니다.

또한, 추가로 sudo docker exec -it {도커이름} /bin/sh 명령어를 통해서 직접 도커 컨테이너 내부로 들어가서 컨테이너를 확인할 수도 있습니다.


📍 최종 정리

  1. Github Actions가 우리의 코드를 서버에 올리고, deploy.sh를 실행합니다.
  2. deploy.sh는 docker-compose.prod.yml up 를 실행합니다.
  3. docker-compose 명령어를 통해 web 컨테이너와 nginx라는 컨테이너를 빌드하고 실행합니다.
  4. web 컨테이너는 Dockerfile.prod를 기준으로 빌드되며, 이 도커 이미지는 django를 구동하기 위한 환경이 모두 갖추어져 있으며,
    nginx 컨테이너는 nginx/Dockerfile, nginx/nginx.conf를 기준으로 빌드되며, 이 도커 이미지는 nginx 웹서버를 구동하기 위한 환경이 모두 갖추어져 있습니다.
profile
꾸준히, 열심히, 그리고 잘하자

0개의 댓글