Jiwon Min Developer

GitHub Actions와 Docker를 활용한 Django 애플리케이션 CI/CD 파이프라인 완벽 구축 가이드

수동 배포의 시대는 저물고 있습니다. 코드를 수정한 후 FTP로 파일을 올리거나, SSH로 서버에 접속하여 git pull을 실행하고 서버를 재시작하는 과정은 실수를 유발하기 쉽고 전체 개발 사이클을 느리게 만드는 주범입니다. 특히 협업 환경에서는 누가, 언제, 어떤 코드를 배포했는지 추적하기 어려워 안정적인 서비스 운영에 큰 걸림돌이 됩니다. 이러한 문제를 해결하기 위해 CI/CD(Continuous Integration/Continuous Deployment), 즉 지속적 통합 및 배포 파이프라인 구축은 이제 선택이 아닌 필수가 되었습니다.

본 포스트에서는 가장 널리 사용되는 웹 프레임워크 중 하나인 Django와 Git 호스팅 서비스의 표준으로 자리 잡은 GitHub의 GitHub Actions를 결합하여, 테스트부터 Docker 이미지 빌드, 그리고 AWS ECR(Elastic Container Registry) 및 EC2(Elastic Compute Cloud) 배포까지 이어지는 완전 자동화된 CI/CD 파이프라인을 구축하는 방법을 심도 있게 다룹니다. 단순히 ‘Hello, World!’ 수준의 튜토리얼을 넘어, 현업에서 즉시 적용 가능한 보안, 성능 최적화, 환경 분리 등 고급 전략까지 포함한 실전 가이드를 제시합니다.

GitHub Actions와 Docker를 활용한 Django 애플리케이션 CI/CD 파이프라인 완벽 구축 가이드

© AI Generated by Imagen 4.0


1. 도입: 왜 Django 프로젝트에 GitHub Actions CI/CD가 필요한가?

Django 프로젝트의 규모가 커지고 팀원이 늘어날수록 코드의 일관성과 배포 안정성을 유지하는 것은 매우 중요해집니다. 개발자가 main 브랜치에 코드를 푸시할 때마다 자동으로 테스트가 실행되고, 테스트를 통과한 코드만이 자동으로 빌드 및 배포되는 환경은 개발 생산성을 극대화하고 휴먼 에러를 최소화하는 핵심 요소입니다.

GitHub Actions는 GitHub 저장소에 내장된 워크플로우 자동화 도구로, 별도의 CI/CD 서버를 구축할 필요 없이 YAML 파일을 통해 손쉽게 파이프라인을 정의할 수 있다는 강력한 장점이 있습니다. 또한, Docker와의 완벽한 통합을 통해 애플리케이션의 실행 환경을 컨테이너 이미지로 표준화하여 ‘내 컴퓨터에서는 잘 됐는데…‘와 같은 고질적인 문제를 원천적으로 차단할 수 있습니다.

이 글에서 우리가 구축할 파이프라인의 전체적인 흐름은 다음과 같습니다.

  1. Trigger: 개발자가 main 브랜치에 코드를 푸시(Push)하거나 병합(Merge)합니다.
  2. Test: GitHub Actions 워크플로우가 실행되어 Django 프로젝트의 유닛 테스트와 린팅(Linting)을 수행합니다.
  3. Build: 테스트가 성공하면, Dockerfile을 기반으로 Django 애플리케이션을 포함하는 Docker 이미지를 빌드합니다.
  4. Push: 빌드된 이미지를 버전 태그와 함께 AWS ECR(Private Docker Image Registry)에 푸시합니다.
  5. Deploy: SSH를 통해 프로덕션 환경인 AWS EC2 인스턴스에 접속하여, ECR에서 최신 이미지를 가져와 새로운 컨테이너를 실행하며 무중단에 가깝게 배포를 완료합니다.

2. 핵심 아키텍처: GitHub Actions 워크플로우 설계

효과적인 CI/CD 파이프라인은 논리적으로 분리된 여러 단계(Job)로 구성됩니다. 우리는 Test, Build & Push, Deploy라는 세 가지 주요 Job으로 워크플로우를 설계할 것입니다.

  • test Job: 코드의 정합성을 검증하는 단계입니다. 이 단계가 실패하면 이후의 빌드 및 배포 Job은 실행되지 않아, 버그가 포함된 코드가 배포되는 것을 방지합니다.
  • build-and-push Job: test Job이 성공했을 때만 실행됩니다. Django 애플리케이션을 Docker 이미지로 패키징하고, 버전 관리를 위해 ECR에 푸시합니다.
  • deploy Job: build-and-push Job이 성공했을 때만 실행됩니다. 실제 운영 서버에 접속하여 새로운 버전의 애플리케이션을 배포합니다.

이러한 구조는 각 단계의 역할을 명확히 하고, 문제 발생 시 어느 단계에서 실패했는지 빠르게 파악할 수 있게 도와줍니다.

GitHub Actions의 주요 개념

워크플로우 YAML 파일을 작성하기 전에, 핵심 개념들을 이해해야 합니다.

  • Workflow: .github/workflows/ 디렉토리에 위치한 YAML 파일로 정의되는 자동화된 프로세스 전체를 의미합니다.
  • Event: 워크플로우 실행을 유발하는 특정 활동입니다. (예: push, pull_request)
  • Job: 하나의 워크플로우는 하나 이상의 Job으로 구성되며, 각 Job은 독립된 가상 환경(Runner)에서 실행됩니다.
  • Step: Job을 구성하는 개별 작업 단위입니다. 셸 명령어를 실행하거나, 미리 만들어진 Action을 사용할 수 있습니다.
  • Action: 워크플로우에서 반복적으로 사용되는 복잡한 작업을 캡슐화한 재사용 가능한 코드입니다. (예: actions/checkout@v3, aws-actions/configure-aws-credentials@v2)

3. 실무 적용: Django CI/CD 워크플로우 YAML 딥다이브

이제 실제 워크플로우 파일을 작성해 보겠습니다. 프로젝트 루트에 .github/workflows/main.yml 파일을 생성하고 아래 내용을 단계별로 채워나갑니다.

3.1. 기본 워크플로우 정의 및 트리거 설정

# .github/workflows/main.yml

name: Django CI/CD

on:
  push:
    branches: [ "main" ] # main 브랜치에 push 이벤트가 발생했을 때만 실행

env:
  AWS_REGION: ap-northeast-2 # 사용할 AWS 리전
  ECR_REPOSITORY: my-django-app # 생성한 ECR 리포지토리 이름
  • name: 워크플로우의 이름을 지정합니다. GitHub UI에 표시됩니다.
  • on.push.branches: main 브랜치에 push 이벤트가 발생할 때 이 워크플로우를 트리거하도록 설정합니다.
  • env: 워크플로우 전체에서 사용할 환경 변수를 정의합니다.

3.2. 테스트(Test) Job 구현

첫 번째 Job으로, Django 프로젝트의 테스트 코드를 실행하여 코드의 안정성을 검증합니다.

# .github/workflows/main.yml (이어서)

jobs:
  test:
    name: Test Django Project
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.10'

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt

      - name: Run tests
        run: |
          python manage.py test
  • runs-on: ubuntu-latest: 이 Job이 최신 버전의 Ubuntu 가상 환경에서 실행됨을 의미합니다.
  • actions/checkout@v3: 저장소의 코드를 Runner로 가져오는 Action입니다.
  • actions/setup-python@v4: 지정된 버전의 Python 환경을 설정합니다.
  • Install dependenciesRun tests: pip를 이용해 의존성 패키지를 설치하고, Django의 내장 test 명령어를 실행합니다.

3.3. 빌드 및 ECR 푸시(Build & Push) Job

test Job이 성공적으로 완료되면, Docker 이미지를 빌드하고 AWS ECR로 푸시합니다. 이 단계에서는 AWS 인증 정보가 필요하므로, GitHub Secrets를 활용해야 합니다.

[사전 준비]

  1. AWS IAM에서 GitHub Actions를 위한 사용자를 생성하고 AmazonEC2ContainerRegistryFullAccess 권한을 부여하세요.
  2. 해당 사용자의 Access Key IDSecret Access Key를 발급받으세요.
  3. GitHub 저장소의 Settings > Secrets and variables > Actions 메뉴에서 AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY라는 이름으로 위에서 발급받은 키를 등록하세요.
# .github/workflows/main.yml (이어서)

  build-and-push:
    name: Build and Push to ECR
    runs-on: ubuntu-latest
    needs: test # 'test' Job이 성공해야만 실행됨

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          aws-access-key-id: $
          aws-secret-access-key: $
          aws-region: $

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1

      - name: Set image tag
        id: image-tag
        run: echo "IMAGE_TAG=$(date +%Y%m%d%H%M%S)" >> $GITHUB_ENV

      - name: Build, tag, and push image to Amazon ECR
        env:
          ECR_REGISTRY: $
          IMAGE_TAG: $
        run: |
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -f Dockerfile .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
  • needs: test: 이 Job이 test Job에 의존함을 명시합니다.
  • aws-actions/configure-aws-credentials@v2: GitHub Secrets에 저장된 AWS 키를 사용하여 Runner 환경에 AWS 인증을 구성합니다.
  • aws-actions/amazon-ecr-login@v1: ECR에 Docker 클라이언트가 로그인하도록 합니다.
  • Set image tag: 현재 시간을 기반으로 고유한 이미지 태그를 생성하여 GITHUB_ENV에 등록합니다.
  • docker builddocker push: Dockerfile을 사용하여 이미지를 빌드하고, ECR 리포지토리 주소와 생성된 태그를 조합하여 ECR에 푸시합니다.

3.4. EC2 서버 자동 배포(Deploy) Job

마지막으로, ECR에 푸시된 최신 이미지를 EC2 인스턴스에서 실행하여 배포를 완료합니다. SSH를 통해 원격 명령을 실행하는 방식을 사용합니다.

[사전 준비]

  1. EC2 인스턴스에 SSH로 접속할 때 사용할 키 페어를 생성합니다.
  2. Private Key(pem 파일 내용)를 EC2_SSH_PRIVATE_KEY라는 이름으로 GitHub Secrets에 등록합니다.
  3. EC2 인스턴스의 Public IP 주소 또는 도메인을 EC2_HOST라는 이름으로, SSH 사용자 이름(예: ubuntu, ec2-user)을 EC2_USERNAME으로 Secrets에 등록합니다.
  4. EC2 인스턴스에 Docker와 Docker Compose를 미리 설치해 둡니다.
  5. EC2 인스턴스가 ECR에서 이미지를 pull할 수 있도록 IAM Role을 인스턴스에 연결해주세요. (AmazonEC2ContainerRegistryReadOnly 권한)
# .github/workflows/main.yml (이어서)

  deploy:
    name: Deploy to EC2
    runs-on: ubuntu-latest
    needs: build-and-push # 'build-and-push' Job이 성공해야만 실행됨

    steps:
      - name: Get image tag
        id: image-tag
        # 이전 Job에서 생성한 태그를 가져오기 위한 편법. 
        # 실제로는 Artifact를 사용하거나 워크플로우 출력(outputs)을 사용하는 것이 더 정석입니다.
        # 여기서는 간단하게 동일한 로직으로 태그를 다시 생성합니다.
        run: echo "IMAGE_TAG=$(date +%Y%m%d%H%M%S)" >> $GITHUB_ENV
        
      - name: Deploy to EC2 instance
        uses: appleboy/ssh-action@master
        with:
          host: $
          username: $
          key: $
          script: |
            # ECR 로그인
            aws ecr get-login-password --region $ | docker login --username AWS --password-stdin $.dkr.ecr.$.amazonaws.com

            # Docker Compose 파일 업데이트 (sed 사용)
            # DOCKER_IMAGE 환경 변수를 설정하여 docker-compose.yml에서 사용
            export DOCKER_IMAGE=$.dkr.ecr.$.amazonaws.com/$:$
            
            # 기존 컨테이너 중지 및 삭제 (있을 경우)
            if [ $(docker ps -q -f name=my-django-container) ]; then
              docker-compose -f /home/ubuntu/my-django-app/docker-compose.yml down
            fi

            # 최신 이미지 pull
            docker pull $DOCKER_IMAGE

            # docker-compose.yml이 위치한 디렉토리로 이동하여 실행
            cd /home/ubuntu/my-django-app
            docker-compose up -d
  • appleboy/ssh-action@master: SSH를 통해 원격 서버에서 스크립트를 실행해주는 매우 유용한 Action입니다.
  • script: EC2 인스턴스에서 실행될 셸 스크립트입니다. ECR 로그인, 최신 이미지 pull, docker-compose up -d를 통해 컨테이너를 백그라운드로 실행하는 과정을 자동화합니다. AWS_ACCOUNT_ID도 Secret으로 등록해야 합니다.

4. 성능 최적화 및 Best Practices

의존성 캐싱으로 빌드 시간 단축

매번 워크플로우가 실행될 때마다 pip install을 실행하는 것은 비효율적입니다. actions/cache를 사용하여 pip 캐시를 저장하고 복원하면 빌드 시간을 크게 단축할 수 있습니다.

test Job 수정 예시:

      - name: Cache pip dependencies
        uses: actions/cache@v3
        with:
          path: ~/.cache/pip
          key: $-pip-$
          restore-keys: |
            $-pip-

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt

requirements.txt 파일의 내용이 변경되지 않았다면 캐시된 의존성을 그대로 사용하게 됩니다.

환경 분리 (Staging vs. Production)

실제 운영 환경에서는 main 브랜치는 프로덕션 배포, develop 브랜치는 스테이징 배포와 같이 브랜치별로 배포 환경을 분리하는 것이 안전합니다. GitHub Actions의 if 조건문과 environments 기능을 활용하여 이를 구현할 수 있습니다.

on:
  push:
    branches: [ "main", "develop" ]

...
jobs:
...
  deploy-to-staging:
    if: github.ref == 'refs/heads/develop'
    name: Deploy to Staging
    # ... 스테이징 서버 배포 로직 ...
    
  deploy-to-production:
    if: github.ref == 'refs/heads/main'
    name: Deploy to Production
    needs: build-and-push
    environment: production # GitHub Environment 기능 사용
    # ... 프로덕션 서버 배포 로직 ...

GitHub environment 기능을 사용하면 배포 전 승인 절차를 추가하거나, 해당 환경 전용 Secret을 설정하는 등 더욱 정교한 관리가 가능합니다.

5. 결론: 자동화된 파이프라인으로 개발 생산성 극대화

지금까지 GitHub Actions와 Docker를 활용하여 Django 애플리케이션의 테스트, 빌드, 배포 전 과정을 자동화하는 CI/CD 파이프라인을 구축하는 방법을 상세히 알아보았습니다. 이렇게 잘 구성된 파이프라인은 다음과 같은 명확한 이점을 제공합니다.

  • 신속한 배포: 코드 변경 사항을 몇 분 안에 프로덕션 환경에 반영할 수 있습니다.
  • 안정성 향상: 자동화된 테스트가 배포 전 코드의 품질을 보장하며, 수동 작업으로 인한 실수를 방지합니다.
  • 개발자 경험 향상: 개발자는 배포의 부담에서 벗어나 핵심 비즈니스 로직 개발에만 집중할 수 있습니다.
  • 가시성 확보: GitHub Actions UI를 통해 모든 배포 이력과 성공/실패 여부를 한눈에 파악할 수 있습니다.

본 가이드에서 제시한 YAML 코드와 전략을 기반으로 여러분의 Django 프로젝트에 맞는 최적의 CI/CD 파이프라인을 구축하여 개발 생산성과 서비스 안정성이라는 두 마리 토끼를 모두 잡으시길 바랍니다.

참고문헌