Project Management/Github

Django CICD with GitHub Actions - did coding 학습 정리

bluebamus 2023. 6. 9.

영상 정보 : https://www.youtube.com/watch?v=enHcgLYlhxs 

 

프로젝트 정보 : https://didcoding.com/tutorial/continuous-delivery-for-django/

 

Continuous Delivery For Django - Did Coding

 

didcoding.com

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 설정 및 배포를 하고 있는데 이 방법이 좋을지 라이브러리가 좋을지는 고민할 필요가 있을것 같다.

댓글