Profile picture

[배포] Github Actions로 Djnago 블로그 배포 자동화하기

JaehyoJJAng2023년 11월 24일

💡 개요

Waytothem 이라는 Django 백엔드 기반으로 배포된 블로그가 하나 있다.

해당 블로그의 경우 Lightsail 5달러 Plan 짜리 인스턴스 위에서 거의 1년 6개월 간 운영 중에 있고 하나의 인스턴스에 도커 컴포즈로 Nginx + Certbot + DB(Postgresql) + App이 기동된다.

현재 해당 블로그의 배포 파이프라인은 정말 단순하다.

  1. 로컬에서 소스코드 업데이트 후 깃허브에 업로드 (git push)
  2. Lightsail 인스턴스에 SSH 접속 후 변경된 코드 업데이트 (git pull)
  3. docker-compose up -d --build (Re-building)

위 단계로 직접 "수동"으로 개발자(나)가 서비스를 업데이트한다.

어느 순간, 이 과정이 너무 귀찮게 느껴지더라. 하지만 아는 거라곤 수동 배포밖에 할 줄 몰라서 배포 자동화에 대해 계속 공부하기 시작했다.
Travis CI, Jenkins 등을 공부해봤는데 나한테는 Github Actions 만큼 간편하고 쉬운게 없더라.


하지만 해당 프로젝트에 단위 테스트 코드는 작성하지 않았어서 테스트 파이프라인을 작성할 수는 없었다.
1인 개발이기도 하고 해서 조금 간단하게 배포 자동화 파이프라인을 도입하려고 한다.

소스코드 변경이 감지되면 자동으로 배포하는 식으로 프로젝트에 적용해보자!

📕 Container Topology

image


📌 깃허브 소스코드

https://github.com/JaehyoJJAng/Python_Web_Development_2022


📌 사전 준비

1. SSH Key 생성

Github Actions에서 SSH로 Lightsail에 접속하기 위해서는 로컬에 SSH Key를 만들어야 한다.
순서는 아래와 같다.

  • 1. ssh-keygen 명령어로 로컬 ~/.ssh 경로에 'github_id_rsa' 라는 이름의 ssh key pair를 생성한다.
  • 2. 생성된 키 페어 중 'github_id_rsa.pub' 파일의 내용을 cat 커맨드로 읽은 후 복사하여 [https://github.com/JaehyoJJAng/Python_Web_Development_2022] 레포지토리의 secret 환경 변수로 넣어준다.

먼저 로컬에 ssh key를 생성하자.

ssh-keygen -t rsa -b 4096 -C "github actions" -f ~/.ssh/github_id_rsa

🔺 authorized_keys

Lightsail 서버의 ~/.ssh/authorized_keys 파일에 'github_id_rsa.pub' 파일의 내용이 추가되어 있어야 한다.


2. github secret 등록

생성된 github_id_rsa.pub 파일의 내용을 복사한 후 https://github.com/JaehyoJJAng/Python_Web_Development_2022의 Settings - Security - Secrets and variables - Actions로 이동하여 'New repository secret'을 클릭하자.
image


'SSH_PRIVATE_KEY' 라는 이름으로 키 네임을 정하고 'secret' 항목에는 위에서 복사했던 'github_id_rsa' 개인 Private key 파일의 내용을 붙여넣기 하자.
image


그리고 Lightsail 인스턴스의 Public IP, SSH로 접속할 Username명, 로컬의 SSH known_host 파일 내용을 secret으로 만들어준 후 값을 넣어주자.
image


그리고 github actions에서 Docker hub에 로그인도 해야하므로 docker hub 로그인 관련 정보도 생성해주자.
image


3. Django Allowed hosts

가비아 도메인을 lightsail 인스턴스에 연결하였으므로 Django 프로젝트의 ALLOWED_HOSTS에 해당 도메인을 추가해주어야 한다.

vi ./Python_Web_Development_2022/WTT/.env.prod
...
# 해당 변수에 도메인명 추가
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1] 3.x.x.x. waytothem.store www.waytothem.store
...

4. .dockerignore

컨테이너 이미지에 .env 같은 환경 변수가 포함된 채 컨테이너 이미지가 Public registry에 배포 되는 경우 심각한 보안 위협을 당할 수 있게된다.

그걸 방지하고자 Dockerfile이 존재하는 WTT/ 디렉토리 내에 .dockerignore를 작성할 것이다.

vi ./WTT/.dockerignore
.env.prod
.env.dev
.env.prod.db
Dockerfile
django_프로젝트생성.txt

📌 docker compose 수정

기존 docker-compose.yaml의 경우 프로젝트 내의 Dockerfile을 참조하여 이미지가 빌드되도록 설정 되어있다.

version: '3.2'

services:
  nginx:
    build: ./nginx
    ...

  web:
    build:
      context: ./WTT
      dockerfile: Dockerfile
    ...

  db:
    image: postgres:12.0-alpine
    ...

db 컨테이너를 제외한 nginx, web 컨테이너가 현재 프로젝트 내의 Dockerfile을 참조하여 이미지를 빌드하고 있다.


추후에 Github Actions를 통해 이미지를 자동으로 빌드/배포하고 배포된 이미지를 도커 허브에서 받아올 것이기에 아래처럼 미리 수정해주자.

version: '3.2'

services:
  nginx:
    image: <YourDockerUserName>/wtt-nginx:latest # 도커 허브에 배포된 이미지 사용
    ...

  web:
    image: <YourDockerUserName>/wtt-web:latest   # 도커 허브에 배포된 이미지 사용
    ...

  db:
    image: postgres:12.0-alpine
    ...

📌 배포

배포 시 사용되는 프로젝트는 깃허브 소스코드에 링크된 깃허브 주소에 업로드 되어있다.


1. workflow

현재 프로젝트 경로(~/Python_Web_Development_2022)에 .github/workflows/BlogDeploy.yaml 파일을 만들자.

mkdir -p .github/workflows && touch .github/workflows/BlogDeploy.yaml

2. workflow 작성

BlogDeploy.yaml 파일에 아래 내용을 붙여넣기 하자.

name: BlogDeploy
on:
  push:
    branches:
      - "main"

jobs:
  deploy_job:
    runs-on: ubuntu-22.04
    steps:
      - name: "[Checkout] 1. Checkout repository"
        uses: actions/checkout@v3

      - name: "[Docker Login] 2. Dockerhub Login"
        uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
        with:
          username:  {% raw %} ${{ secrets.DOCKER_HUB_USERNAME }} {% endraw %}
          password:  {% raw %} ${{ secrets.DOCKER_HUB_PASSWORD }} {% endraw %}

      - name: "[Build] 3. nginx image build & push"
        uses: docker/build-push-action@v2
        with:
          context: ./nginx # Dockerfile이 있는 디렉토리 지정
          push: true
          tags: |
            <YourDockerUserName>/wtt-nginx:latest
      
      - name: "[Build] 4. WTT image build & push"
        uses: docker/build-push-action@v2
        with:
          context: ./WTT
          push: true
          tags: |
            <YourDockerUserName>/wtt-web:latest

      - name: "[Deploy] 5. Set up SSH"
        run: |
          mkdir -p ~/.ssh/
          echo {% raw %} "${{ secrets.SSH_PRIVATE_KEY }}" | tee -a ~/.ssh/id_rsa {% endraw %}
          chmod 600 ~/.ssh/id_rsa
      
      - name: "[Deploy] 6. Set up Known hosts"
        run: |
          echo {% raw %} "${{ secrets.SSH_KNOWN_HOST }}" | tee -a ~/.ssh/known_hosts {% endraw %}
          chmod 644 ~/.ssh/known_hosts
      
      - name: "[Deploy] 7. SSH and Deploy"
        run: |
          ssh -i {% raw %} ~/.ssh/id_rsa ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_PUBLIC_IP }} {% endraw %} "
          cd Python_Web_Development_2022
          docker-compose -f docker-compose.yml up -d --build
          "

3. workflow 배포

workflow 작성이 다 끝났다면 commit & push를 하자.

git add .
git commit -m "Update BlogDeploy Workflow"
git push -u origin main

https://github.com/JaehyoJJAng/Python_Web_Development_2022 레포지토리의 Actions 탭으로 이동하여 workflow가 정상적으로 실행됐는지 확인하자.
image
배포는 성공적으로 된 것 같다!


이번에는 대문 페이지의 텍스트를 아래처럼 변경하고 다시 push 해보자.

$ vi ./WTT/single_pages/templates/landing.html

<section id="body-wrapper">
    <div class="container">
        <div class="row justify-content-between">
            <div class="col-lg-6 text-muted">
                <h1 class="mt-5 text-center text-dark text">IT</br>코딩 블로그</h1>
                <div class="card mt-1">
                    <div class="card-body">
                        <p class="text-center" style="font-size: 150%;">
                            </br>파이썬과 도커</br>그리고 리눅스와 셸 스크립팅에</br>많은 관심이 있습니다!
                             </br>
                            <span style="font-size: 80%;">
                                </br>해당 블로그는 IT 실습 관련하여</br>많은 글이 쓰여질 예정입니다.
                            </span>
                        </p>
                    </div>
                </div>
            </div>
...
</section>
git add .
git commit -m "landing 페이지 대문 글 수정"
git push -u origin main

image
배포는 정상적으로 된 것 같다.


http://www.waytothem.store로 접속해 변경사항이 적용 됐는지 확인하자.
image
잘 적용됐다!


4. HTTPS 적용

마지막으로 서비스에 HTTPS를 적용해보도록 하자.
무료 SSL 인증서인 Let's Encrypt를 이용할 거고 오픈 소스인 certbot을 사용하여 SSL 인증서를 자동으로 발급 받을 것이다.


먼저 Let's Encrypt를 nginx + 도커 환경에서 자동으로 인증 받을 수 있도록 도와주는 셸 스크립트를 다운로드 받아야 한다.
(nginx-certbot(github))

curl -L -O https://github.com/wmnnd/nginx-certbot/raw/master/init-letsencrypt.sh

다운로드 받았으면 init-letsencrypt.sh 내용 중 일부분을 아래처럼 수정해줘야 한다.

#!/bin/bash

if ! [ -x "$(command -v docker-compose)" ]; then
  echo 'Error: docker-compose is not installed.' >&2
  exit 1
fi

domains=(waytothem.store www.waytothem.store) # 본인 도메인 주소 입력
rsa_key_size=4096
data_path="./data/certbot" # 인증서를 저장할 경로 기입 (필자는 기본 입력된 경로 그대로 사용함.)
email="내 이메일 주소 기입"

... // 그 외에 설정은 건드릴게 없음. 디폴트로 냅두기

기존 docker-compose.yaml 파일에 추가 및 수정해줘야 할 것이 있다.

version: "3.3"

services:
  nginx:
    image: yshrim12/wtt-nginx:latest
    volumes:
      - type: volume
        source: "static_volume"
        target: "/usr/src/app/_static"
      - type: volume
        source: "media_volume"
        target: "/usr/src/app/_media"
      - "./data/certbot/conf:/etc/letsencrypt"
      - "./data/certbot/www:/var/www/certbot"
    ports:
      - "80:80"
      - "443:443"
    depends_on:
      - "web"
    container_name: nginx
    command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"

  certbot:
    image: certbot/certbot
    restart: always
    volumes:
      - "./data/certbot/conf:/etc/letsencrypt"
      - "./data/certbot/www:/var/www/certbot"
    container_name: certbot
    depends_on:
      - "nginx"
    networks:
      - "web-net"
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"

    # ... My Apps (web, db)...
    web:
      ...
    
    db:
      ...
  1. nginx 컨테이너에 volumes에 certbot 인증서가 생성될 경로를 잡아주었다.
    • volumes의 경우 init-letsencrypt.sh에서 수정한 data_path 경로와 동일해야 한다.
  2. nginx 컨테이너에 command 옵션을 추가하였다.
  3. certbot 컨테이너를 위와 같이 만들어주었다.

그리고 기존 nginx.conf 파일도 아래처럼 수정해줘야 한다.


기존 nginx.conf 내용

upstream  greedy_jaehyo {
    server web:8000;

}

server {
    listen 80;
    location / {
        proxy_pass http://greedy_jaehyo;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_redirect off;
    }

    location /static/ {
        alias /usr/src/app/_static/;
    }

    location /media/ {
        alias /usr/src/app/_media/;
    }
}

수정된 nginx.conf 내용

upstream wtt {
    server web:8000;
}

server {
    listen 80;
    server_name waytothem.store;

    location / {
        return 301 https://$host$request_uri;
    }

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }
}

server {
    listen 443 ssl;
    server_name waytothem.store;

    location / {
        proxy_pass http://wtt;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_redirect off;
    }

    location /static/ {
        alias /usr/src/app/_static/;
    }

    location /media/ {
        alias /usr/src/app/_media/;
    }

    ssl_certificate /etc/letsencrypt/live/waytothem.store/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/waytothem.store/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}

이제 모든 준비가 끝났다.

먼저 프로젝트 경로에 있는 init-letsencrypt.sh 파일에 실행 권한을 부여하자.

chmod +x ./init-letsencrypt.sh

이제 sudo 권한으로 해당 스크립트를 실행하면 된다.

sudo bash ./init-letsencrypt.sh

image


[Other Projects] 💥 [Docker] certbot + nginx로 SSL 인증서 발급하기 - 내가 만든 사이트에 HTTPS 설정하기


🔺 보안 이슈

tree -L 2 .
.
├── WTT
│   ├── Dockerfile
│   ├── _media
│   ├── _static
│   ├── accountapp
│   ├── blog
│   ├── board
│   ├── django_프로젝트생성.txt
│   ├── do_it_django_prj
│   ├── manage.py
│   ├── profileapp
│   ├── requirements.txt
│   ├── single_pages
│   ├── .env.prod
│   ├── .env.prod.db
│   ├── .env.dev
│   └── templates
├── docker-compose.dev.yml
├── docker-compose.yml
├── migrate.sh
├── nginx
│   ├── Dockerfile
│   └── nginx.conf
└── venv
    ├── bin
    ├── include
    ├── lib
    ├── lib64 -> lib
    └── pyvenv.cfg

16 directories, 10 files

현재 프로젝트 경로인 WTT/ 디렉토리 하위에 .env 환경 변수들이 위치해있다.


.dockerignore에 해당 환경 변수 파일들을 넣었음에도 컨테이너에 반영이 되길래 무슨 이유인지 확인해보니 아래 이유 때문이었다!

version: '3.2'

services:
  nginx:
    image: yshrim12/wtt-nginx:latest
    
  web:
    image: yshrim12/wtt-web:latest
    ...
    volumes:
      ...
      - type: bind
        source: "./WTT"
        target: "/usr/src/app"
    env_file:
      - "./WTT/.env.prod"
    ...

  db:
    image: postgres:12.0-alpine
    volumes:
      - type: volume
        source: "postgres_data"
        target: "/var/lib/postgresql/data"
    env_file:
      - "./WTT/.env.prod.db"
    container_name: postgresql
...

web 컨테이너의 volumes를 확인해보자. bind 맵핑으로 ./WTT:/usr/src/app 경로로 마운트가 잡혀있다.

아래 글은 GPT의 답변이다.

  • .dockerignore 파일은 Docker 빌드 컨텍스트 내에서 Docker에 의해 사용되며, 이 파일에 명시된 패턴에 따라 빌드 컨텍스트의 파일과 디렉토리가 Docker 빌드 프로세스에 의해 제외됩니다. 이 파일은 Docker 빌드 단계에서만 영향을 미치며, 이미지가 실행 중인 컨테이너에는 적용되지 않습니다.

  • 즉, .dockerignore 파일은 Dockerfile에서 COPY 명령어를 사용하여 호스트 파일 시스템에서 이미지로 복사할 때만 영향을 미칩니다. 이미지가 실행 중인 컨테이너에 영향을 미치지 않습니다. 컨테이너 내의 파일 및 디렉토리 접근 권한 및 구성은 해당 컨테이너의 설정 및 실행 환경에 따라 결정됩니다.

  • 따라서 .dockerignore 파일에 명시된 내용이 컨테이너에 적용되지 않을 수 있습니다. 컨테이너에서 특정 파일이나 디렉토리를 제외하려면 Dockerfile이나 컨테이너 실행 명령에서 해당 파일이나 디렉토리를 명시적으로 제외해야 합니다.

  • 예를 들어, Dockerfile에서 COPY 명령어를 사용할 때 .dockerignore 파일에 명시된 파일이 복사되지 않도록 하기 위해서는 COPY 명령어에서 해당 파일을 명시적으로 제외하거나, Dockerfile 내에서 이 파일들을 삭제하는 작업을 추가할 수 있습니다.

  • 컨테이너에서 특정 파일이나 디렉토리를 제외하려면 Dockerfile 내에서 이에 대한 조치를 취해야 합니다. .dockerignore 파일은 Docker 빌드 프로세스에서 사용되는 것이며, 빌드 컨텍스트 내의 파일을 Docker 이미지로 복사할 때만 해당 파일이 무시되도록 하는 역할을 합니다.


그렇기 때문에 ./WTT 디렉토리 하위에 있는 .env 환경 변수 파일들은 부모 디렉토리로 빼내와야 한다. 빼내오기 전에 .env 라는 새로운 폴더를 프로젝트 경로에 생성하자.

$ pwd
/home/ubuntu/Python_Web_Development_2022

$ mkdir .env

$ mv ./WTT/.env* .env/

그리고 docker-compose.yaml에서 web 컨테이너와 db 컨테이너의 env_file 옵션도 아래처럼 수정하자.

...
  web:
    ...
    env_file:
      - ".env/.env.prod"

  db:
    ...
    env_file:
      - ".env/.env.prod.db"

🔺 그럼 이제 컨테이너에 .env 환경 변수 파일들이 같이 올라가는 일은 없을 것이다!


Loading script...