Django CICD with GitHub Actions - did coding 학습 정리
영상 정보 : https://www.youtube.com/watch?v=enHcgLYlhxs
프로젝트 정보 : https://didcoding.com/tutorial/continuous-delivery-for-django/
1. 프로젝트 설명 : 해당 프로젝트는 github action을 이용해 docker 이미지를 빌드하고 가상 서버에 배포 한다.
2. 순차 작업 순서 설명 :
1. DigitalOcean에 가상 서버를 만들고 ssh 키 생성 등을 미리 설정해 둔다.
2. nginx 등의 요구되는 설정 파일을 미리 만들고 dockefile 또한 만들어 둔다.
3. github의 secrets에 노출되면 안되는 정보들을 입력한다.
4. docker build, login, push를 테스트해 문제가 없는지 확인한다.
5. main.yml 파일을 만들어 secrets 정보를 이용해 .env 파일을 만들고 이미지를 build하고 push 한다
6. docker-compose 파일을 2개로 나누어 생성한다.
1. CICD의 docker 이미지 빌드를 위한 것
2. 배포된 이후 Product에서 자체적으로 사용하기 위한 것
7. 테스트 후 문제가 없다면, deploy 항목을 정의한다.
1. build라는 job을 needs로 정의한다.
2. if: github.ref == 'refs/heads/main' 항목을 정의한다. 현재 작업하는 Git 참조가 'refs/heads/main'과 정확히 동일한지 확인하며 main 브랜치라는 조건이 참이면 워크플로의 후속 단계가 실행된다.
8. .env 파일을 만들고 ssh 키를 ssh-agent에 추가한다.
9. scp를 이용해 이전해야 하는 설정파일 등을 서버에 복사한다.
10. 'ENDSSH'를 이용해 명령어를 배포서버에 전달한다.
1. heredoc은 입력 블록을 ssh 명령에 전달하는 데 사용된다. ENDSSH는 heredoc 블록의 끝을 표시하는 구분 기호로
이 구분 기호 다음의 모든 명령 또는 텍스트는 SSH 연결을 통해 원격 서버에 대한 입력으로 전송된다.
3. 사용된 코드 정리 :
1. main.yml
name: Continuous Integration and Delivery
on: [push]
env:
APP_IMAGE: ghcr.io/$(echo $GITHUB_REPOSITORY | tr '[:upper:]' '[:lower:]')/app
CELERY_IMAGE: ghcr.io/$(echo $GITHUB_REPOSITORY | tr '[:upper:]' '[:lower:]')/celery
BEAT_IMAGE: ghcr.io/$(echo $GITHUB_REPOSITORY | tr '[:upper:]' '[:lower:]')/beat
FLOWER_IMAGE: ghcr.io/$(echo $GITHUB_REPOSITORY | tr '[:upper:]' '[:lower:]')/flower
NGINX_IMAGE: ghcr.io/$(echo $GITHUB_REPOSITORY | tr '[:upper:]' '[:lower:]')/nginx
jobs:
build:
name: Build Docker Images
runs-on: ubuntu-latest
steps:
- name: Checkout master
uses: actions/checkout@v1
- name: Add environment variables to .env
run: |
echo DEBUG=0 >> .env
echo PRODUCTION=1 >> .env
echo SQL_ENGINE=django.db.backends.postgresql_psycopg2 >> .env
echo DATABASE=postgres >> .env
echo SECRET_KEY=${{ secrets.SECRET_KEY }} >> .env
echo SQL_DATABASE=${{ secrets.SQL_DATABASE }} >> .env
echo SQL_USER=${{ secrets.SQL_USER }} >> .env
echo SQL_PASSWORD=${{ secrets.SQL_PASSWORD }} >> .env
echo SQL_HOST=${{ secrets.SQL_HOST }} >> .env
echo SQL_PORT=${{ secrets.SQL_PORT }} >> .env
echo DONOT_REPLY_EMAIL=${{ secrets.DONOT_REPLY_EMAIL }} >> .env
echo GOOGLE_APP_PASSWORD=${{ secrets.GOOGLE_APP_PASSWORD }} >> .env
echo DJANGO_ALLOWED_HOSTS=${{ secrets.DJANGO_ALLOWED_HOSTS }} >> .env
- name: Set environment variables
run: |
echo "APP_IMAGE=$(echo ${{env.APP_IMAGE}} )" >> $GITHUB_ENV
echo "CELERY_IMAGE=$(echo ${{env.CELERY_IMAGE}} )" >> $GITHUB_ENV
echo "BEAT_IMAGE=$(echo ${{env.BEAT_IMAGE}} )" >> $GITHUB_ENV
echo "FLOWER_IMAGE=$(echo ${{env.FLOWER_IMAGE}} )" >> $GITHUB_ENV
echo "NGINX_IMAGE=$(echo ${{env.NGINX_IMAGE}} )" >> $GITHUB_ENV
- name: Log in to GitHub Packages
run: echo ${PERSONAL_ACCESS_TOKEN} | docker login ghcr.io -u ${{ secrets.NAMESPACE }} --password-stdin
env:
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
- name: Pull images
run: |
docker pull ${{ env.APP_IMAGE }} || true
docker pull ${{ env.CELERY_IMAGE }} || true
docker pull ${{ env.BEAT_IMAGE }} || true
docker pull ${{ env.FLOWER_IMAGE }} || true
docker pull ${{ env.NGINX_IMAGE }} || true
- name: Build images
run: |
docker-compose -f docker-compose.cicd.yml build
- name: Push images
run: |
docker push ${{ env.APP_IMAGE }}
docker push ${{ env.CELERY_IMAGE }}
docker push ${{ env.BEAT_IMAGE }}
docker push ${{ env.FLOWER_IMAGE }}
docker push ${{ env.NGINX_IMAGE }}
2. docker-compoce.cicd.yml
version: '3'
services:
db:
image: postgres:13.0-alpine
volumes:
- postgres_data:/var/lib/postgresql/data/
env_file:
- .env
container_name: did_django_schedule_jobs_v2_db_prod
networks:
- main_prod
app:
build:
context: ./backend
dockerfile: Dockerfile.prod
cache_from:
- "${APP_IMAGE}"
image: "${APP_IMAGE}"
restart: always
command: gunicorn did_django_schedule_jobs_v2.wsgi:application --bind 0.0.0.0:8000
volumes:
- static_volume:/home/app/web/staticfiles
- media_volume:/home/app/web/mediafiles
expose:
- 8000
env_file:
- .env
depends_on:
- db
networks:
- main_prod
container_name: did_django_schedule_jobs_v2_django_app_prod
redis:
image: redis:6-alpine
expose:
- 6379
ports:
- "6379:6379"
networks:
- main_prod
container_name: did_django_schedule_jobs_v2_redis_prod
celery_worker:
restart: always
build:
context: ./backend
dockerfile: Dockerfile.prod
cache_from:
- "${CELERY_IMAGE}"
image: "${CELERY_IMAGE}"
command: celery -A did_django_schedule_jobs_v2 worker --loglevel=info --logfile=logs/celery.log
volumes:
- ./backend:/home/app/web/
networks:
- main_prod
env_file:
- .env
depends_on:
- db
- redis
- app
container_name: did_django_schedule_jobs_v2_celery_worker_prod
celery-beat:
build:
context: ./backend
dockerfile: Dockerfile.prod
cache_from:
- "${BEAT_IMAGE}"
image: "${BEAT_IMAGE}"
command: celery -A did_django_schedule_jobs_v2 beat -l info
volumes:
- ./backend:/home/app/web/
networks:
- main_prod
env_file:
- .env
depends_on:
- db
- redis
- app
container_name: did_django_schedule_jobs_v2_celery_beat_prod
flower:
build:
context: ./backend
dockerfile: Dockerfile.prod
cache_from:
- "${FLOWER_IMAGE}"
image: "${FLOWER_IMAGE}"
command: "celery -A did_django_schedule_jobs_v2 flower
--broker=redis://redis:6379//
--env-file=.env
--basic_auth=bobby:password"
ports:
- 5555:5555
networks:
- main_prod
env_file:
- .env
depends_on:
- db
- app
- redis
- celery_worker
container_name: did_django_schedule_jobs_v2_flower_prod
nginx:
container_name: did_django_schedule_jobs_v2_nginx_prod
restart: always
build:
context: ./nginx
cache_from:
- "${NGINX_IMAGE}"
image: "${NGINX_IMAGE}"
ports:
- "8080:8080"
networks:
- main_prod
volumes:
- static_volume:/home/app/web/staticfiles
- media_volume:/home/app/web/mediafiles
depends_on:
- app
volumes:
postgres_data:
static_volume:
media_volume:
networks:
main_prod:
driver: bridge
3. docker-compose.prod.yml
version: '3'
services:
db:
image: postgres:13.0-alpine
volumes:
- postgres_data:/var/lib/postgresql/data/
env_file:
- .env
container_name: did_django_schedule_jobs_v2_db_prod
networks:
- main_prod
app:
image: "${APP_IMAGE}"
restart: always
command: gunicorn did_django_schedule_jobs_v2.wsgi:application --bind 0.0.0.0:8000
volumes:
- static_volume:/home/app/web/staticfiles
- media_volume:/home/app/web/mediafiles
ports:
- 8000:8000
env_file:
- .env
depends_on:
- db
networks:
- main_prod
container_name: did_django_schedule_jobs_v2_django_app_prod
redis:
image: redis:6-alpine
expose:
- 6379
ports:
- "6379:6379"
networks:
- main_prod
container_name: did_django_schedule_jobs_v2_redis_prod
celery_worker:
restart: always
image: "${CELERY_IMAGE}"
command: celery -A did_django_schedule_jobs_v2 worker --loglevel=info --logfile=logs/celery.log
volumes:
- ./backend:/home/app/web/
networks:
- main_prod
env_file:
- .env
depends_on:
- db
- redis
- app
container_name: did_django_schedule_jobs_v2_celery_worker_prod
celery-beat:
image: "${BEAT_IMAGE}"
command: celery -A did_django_schedule_jobs_v2 beat -l info
volumes:
- ./backend:/home/app/web/
networks:
- main_prod
env_file:
- .env
depends_on:
- db
- redis
- app
container_name: did_django_schedule_jobs_v2_celery_beat_prod
flower:
image: "${FLOWER_IMAGE}"
command: "celery -A did_django_schedule_jobs_v2 flower
--broker=redis://redis:6379//
--env-file=..env
--basic_auth=bobby:password"
ports:
- 5555:5555
networks:
- main_prod
env_file:
- .env
depends_on:
- db
- app
- redis
- celery_worker
container_name: did_django_schedule_jobs_v2_flower_prod
nginx:
container_name: did_django_schedule_jobs_v2_nginx_prod
restart: always
image: "${NGINX_IMAGE}"
ports:
- "8080:8080"
networks:
- main_prod
volumes:
- static_volume:/home/app/web/staticfiles
- media_volume:/home/app/web/mediafiles
depends_on:
- app
volumes:
postgres_data:
static_volume:
media_volume:
networks:
main_prod:
driver: bridge
4. main.yml에 dploy 추
name: Continuous Integration and Delivery
on: [push]
env:
APP_IMAGE: ghcr.io/$(echo $GITHUB_REPOSITORY | tr '[:upper:]' '[:lower:]')/app
CELERY_IMAGE: ghcr.io/$(echo $GITHUB_REPOSITORY | tr '[:upper:]' '[:lower:]')/celery
BEAT_IMAGE: ghcr.io/$(echo $GITHUB_REPOSITORY | tr '[:upper:]' '[:lower:]')/beat
FLOWER_IMAGE: ghcr.io/$(echo $GITHUB_REPOSITORY | tr '[:upper:]' '[:lower:]')/flower
NGINX_IMAGE: ghcr.io/$(echo $GITHUB_REPOSITORY | tr '[:upper:]' '[:lower:]')/nginx
jobs:
build:
name: Build Docker Images
runs-on: ubuntu-latest
steps:
- name: Checkout master
uses: actions/checkout@v1
- name: Add environment variables to .env
run: |
echo DEBUG=0 >> .env
echo PRODUCTION=1 >> .env
echo SQL_ENGINE=django.db.backends.postgresql_psycopg2 >> .env
echo DATABASE=postgres >> .env
echo SECRET_KEY=${{ secrets.SECRET_KEY }} >> .env
echo SQL_DATABASE=${{ secrets.SQL_DATABASE }} >> .env
echo SQL_USER=${{ secrets.SQL_USER }} >> .env
echo SQL_PASSWORD=${{ secrets.SQL_PASSWORD }} >> .env
echo SQL_HOST=${{ secrets.SQL_HOST }} >> .env
echo SQL_PORT=${{ secrets.SQL_PORT }} >> .env
echo DONOT_REPLY_EMAIL=${{ secrets.DONOT_REPLY_EMAIL }} >> .env
echo GOOGLE_APP_PASSWORD=${{ secrets.GOOGLE_APP_PASSWORD }} >> .env
echo DJANGO_ALLOWED_HOSTS=${{ secrets.DJANGO_ALLOWED_HOSTS }} >> .env
- name: Set environment variables
run: |
echo "APP_IMAGE=$(echo ${{env.APP_IMAGE}} )" >> $GITHUB_ENV
echo "CELERY_IMAGE=$(echo ${{env.CELERY_IMAGE}} )" >> $GITHUB_ENV
echo "BEAT_IMAGE=$(echo ${{env.BEAT_IMAGE}} )" >> $GITHUB_ENV
echo "FLOWER_IMAGE=$(echo ${{env.FLOWER_IMAGE}} )" >> $GITHUB_ENV
echo "NGINX_IMAGE=$(echo ${{env.NGINX_IMAGE}} )" >> $GITHUB_ENV
- name: Log in to GitHub Packages
run: echo ${PERSONAL_ACCESS_TOKEN} | docker login ghcr.io -u ${{ secrets.NAMESPACE }} --password-stdin
env:
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
- name: Pull images
run: |
docker pull ${{ env.APP_IMAGE }} || true
docker pull ${{ env.CELERY_IMAGE }} || true
docker pull ${{ env.BEAT_IMAGE }} || true
docker pull ${{ env.FLOWER_IMAGE }} || true
docker pull ${{ env.NGINX_IMAGE }} || true
- name: Build images
run: |
docker-compose -f docker-compose.cicd.yml build
- name: Push images
run: |
docker push ${{ env.APP_IMAGE }}
docker push ${{ env.CELERY_IMAGE }}
docker push ${{ env.BEAT_IMAGE }}
docker push ${{ env.FLOWER_IMAGE }}
docker push ${{ env.NGINX_IMAGE }}
deploy:
name: Deploy to DigitalOcean
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout master
uses: actions/checkout@v1
- name: Add environment variables to .env
run: |
echo DEBUG=0 >> .env
echo PRODUCTION=1 >> .env
echo SQL_ENGINE=django.db.backends.postgresql_psycopg2 >> .env
echo DATABASE=postgres >> .env
echo SECRET_KEY=${{ secrets.SECRET_KEY }} >> .env
echo SQL_DATABASE=${{ secrets.SQL_DATABASE }} >> .env
echo SQL_USER=${{ secrets.SQL_USER }} >> .env
echo SQL_PASSWORD=${{ secrets.SQL_PASSWORD }} >> .env
echo SQL_HOST=${{ secrets.SQL_HOST }} >> .env
echo SQL_PORT=${{ secrets.SQL_PORT }} >> .env
echo DONOT_REPLY_EMAIL=${{ secrets.DONOT_REPLY_EMAIL }} >> .env
echo GOOGLE_APP_PASSWORD=${{ secrets.GOOGLE_APP_PASSWORD }} >> .env
echo APP_IMAGE=${{ env.APP_IMAGE }} >> .env
echo CELERY_IMAGE=${{ env.CELERY_IMAGE }} >> .env
echo BEAT_IMAGE=${{ env.BEAT_IMAGE }} >> .env
echo FLOWER_IMAGE=${{ env.FLOWER_IMAGE }} >> .env
echo NGINX_IMAGE=${{ env.NGINX_IMAGE }} >> .env
echo NAMESPACE=${{ secrets.NAMESPACE }} >> .env
echo DJANGO_ALLOWED_HOSTS=${{ secrets.DJANGO_ALLOWED_HOSTS }} >> .env
echo PERSONAL_ACCESS_TOKEN=${{ secrets.PERSONAL_ACCESS_TOKEN }} >> .env
- name: Add the private SSH key to the ssh-agent
env:
SSH_AUTH_SOCK: /tmp/ssh_agent.sock
run: |
mkdir -p ~/.ssh
ssh-agent -a $SSH_AUTH_SOCK > /dev/null
ssh-keyscan github.com >> ~/.ssh/known_hosts
ssh-add - <<< "${{ secrets.PRIVATE_KEY }}"
- name: Build and deploy images on DigitalOcean
env:
SSH_AUTH_SOCK: /tmp/ssh_agent.sock
run: |
scp -o StrictHostKeyChecking=no -r ./.env ./backend ./logs ./nginx ./docker-compose.prod.yml root@${{ secrets.DIGITAL_OCEAN_IP_ADDRESS }}:/home/bobby/cicd
ssh -o StrictHostKeyChecking=no root@${{ secrets.DIGITAL_OCEAN_IP_ADDRESS }} << 'ENDSSH'
cd /home/bobby/cicd
source .env
docker login ghcr.io -u $NAMESPACE -p $PERSONAL_ACCESS_TOKEN
docker pull $APP_IMAGE
docker pull $CELERY_IMAGE
docker pull $BEAT_IMAGE
docker pull $FLOWER_IMAGE
docker pull $NGINX_IMAGE
docker-compose -f docker-compose.prod.yml up -d
ENDSSH
정리 : 해당 프로젝트는 재미있는 작업을 한다. .env 파일을 생성하고 이미지를 만들고 업로드 한다. 다만 테스트는 없으며 프로젝트의 실재 동작은 가상서버에서 동작하고 있는 docker로 들어가 구동시켜 줘야 한다. 이러한 불편한 사항을 줄이고자 한다면, docker를 이용한 인프라 항목을 분리하고, migration된 django 최종 프로젝트를 별도의 볼륨 파일에 넣어주는 작업으로 분리하면 된다. 관리를 위한다면 각각의 프로젝트가 되는게 좋을 것이고 함께 관리를 하고자 한다면 yml을 별도로 관리해주고 트리거를 docker 인프라 설치에 대한 workflow의 동작이 성공적으로 완료가 된 후로 해주면 된다.
다른 라이브러리를 사용하지 않고 ssh 설정 및 배포를 하고 있는데 이 방법이 좋을지 라이브러리가 좋을지는 고민할 필요가 있을것 같다.
'Project Management > Github' 카테고리의 다른 글
django : 제로부터 시작하는 인스타그램 devops 4편 | github action을 사용해서 CI/CD구축하기 - 학습 정리 (0) | 2023.06.08 |
---|---|
code academy 학습 정리 2 - Github Actions | Create Cron Schedule | Sending Email (0) | 2023.06.06 |
code academy 학습 정리 1 - Django automated testing with GitHub Actions (0) | 2023.06.06 |
GitHub Actions Runner 빌드 실전 적용기 / if(kakao)2022 정리 (0) | 2023.06.05 |
github actions 자동 배포 : ssh-action 사용 방법 (0) | 2023.06.04 |
댓글