Github Actions (CI/CD) 적용하기 (2) - Flask API 서버 배포 자동화

송태하·2026년 6월 4일

CI/CD

목록 보기
2/2
post-thumbnail

이전 글에서는 GitHub Actions를 사용해 프론트엔드 Preview / Production CI/ CD를 구성했다.

이번 글에서는 프론트엔드에서 호출하는 Flask API 서버를 Docker 이미지로 빌드하고, Google Cloud Run에 자동 배포하는 CI/CD 파이프라인을 정리한다

목표

이번 백엔드 CI/CD의 목표는 다음과 같다.

  • Pull Request 생성 시 백엔드 테스트 실행
  • Docker 이미지 빌드 검증
  • main 브랜치 push 시 Docker 이미지 빌드 및 push
  • Google Cloud Run 자동 배포
  • 배포 후 health check 수행
  • 필요 시 수동 rollback 가능하도록 구성

프론트엔드는 Vercel에 배포하고, 백엔드는 Cloud Run에 배포하는 구조이기 때문에 백엔드 전용 workflow를 별도로 분리했다.

Backend CI/CD 워크플로우 설계

백엔드 워크플로우 파일은 다음 위치에 생성했다.

.github/workflows/backend-cloudrun-cicd.yml

이 워크플로우는 백엔드 코드가 변경되었을 때만 실행되도록 paths 조건을 추가 했다.

  on:
    pull_request:
      branches:
        - main
        - develop
      paths:
        - "backend/**"
        - ".github/workflows/backend-cloudrun-cicd.yml"
    push:
      branches:
        - main
      paths:
        - "backend/**"
        - ".github/workflows/backend-cloudrun-cicd.yml"

pull_request에서는 테스트와 Docker build 검증만 수행한다.

반면 main 브랜치에 push 되었을 때는 실제 Cloud Run 배포까지 진행한다.

즉, PR 단계에서는 배포 전에 문제가 없는지 검증하고, main에 반영된 코드만
운영 백엔드로 배포되는 구조다.

backend-test job

첫 번째 job은 backend-test다.

이 job에서는 Python 환경을 설정하고, 백엔드 테스트를 실행한다.


backend-test:
  name: Run backend tests
  runs-on: ubuntu-latest

  steps:
    - name: Checkout
      uses: actions/checkout@v4

    - name: Set up Python
      uses: actions/setup-python@v5
      with:
        python-version: "3.11"

    - name: Install dependencies
      working-directory: ./backend
      run: pip install -r requirements-dev.txt

    - name: Run tests
      working-directory: ./backend
      run: pytest

actions/setup-python@v5를 사용해 Python 3.11 환경을 구성했다.

백엔드 디렉토리 안에 requirements-dev.txt를 따로 두고, 테스트에 필요한 의존성을 설치하도록 했다.

  -r requirements.txt
  pytest
  pytest-flaskd

이렇게 운영 의존성과 테스트 의존성을 분리하면 Docker 이미지에는 불필요한
테스트 패키지를 포함하지 않을 수 있다.

테스트는 pytest로 실행한다.

run: pytest

이 단계에서 테스트가 실패하면 이후 Docker build나 배포 job은 실행되지 않는
다.

Dockerfile 작성

Cloud Run에 배포하기 위해 백엔드 서버를 Docker 이미지로 빌드해야 한다.

백엔드 Dockerfile은 backend/Dockerfile에 작성했다.

  FROM python:3.11-slim

  WORKDIR /app

  COPY requirements.txt .
  RUN pip install --no-cache-dir -r requirements.txt

  COPY . .

  ENV PORT=8080
  EXPOSE 8080

  CMD ["gunicorn", "--bind", "0.0.0.0:8080", "app:app"]

Docker build 검증

PR 단계에서도 Docker 이미지가 정상적으로 빌드되는지 확인해야 한다.

이를 위해 docker-build job을 추가했다.

  docker-build:
    name: Build Docker image with cache
    runs-on: ubuntu-latest
    needs: backend-test
    timeout-minutes: 10

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Build Docker image
        uses: docker/build-push-action@v6
        with:
          context: ./backend
          push: false
          cache-from: type=gha,scope=backend
          cache-to: type=gha,scope=backend,mode=max

여기서 중요한 부분은 needs: backend-test다.

needs: backend-test

테스트가 성공해야만 Docker build가 실행된다.

또한 PR 단계에서는 실제 registry에 이미지를 push하지 않고, 빌드 가능 여부만 확인한다.

Docker build 속도를 개선하기 위해 GitHub Actions cache도 사용했다.

  cache-from: type=gha,scope=backend
  cache-to: type=gha,scope=backend,mode=max

이 설정을 통해 이전 빌드 레이어를 재사용할 수 있다.

Cloud Run 배포 job

실제 배포는 deploy-to-cloud-run job에서 수행한다.

이 job은 main 브랜치에 push 되었을 때만 실행된다.


deploy-to-cloud-run:
  name: Deploy to Cloud Run
  runs-on: ubuntu-latest
  needs: docker-build
  if: github.event_name == 'push'

PR에서는 테스트와 Docker build까지만 실행되고, push 이벤트에서만 배포가 진행된다.

배포에 필요한 값들은 GitHub Actions Variables와 Secrets로 관리했다.

  env:
    GCP_PROJECT_ID: ${{ vars.GCP_PROJECT_ID }}
    GCP_REGION: ${{ vars.GCP_REGION }}
    GAR_REPOSITORY: ${{ vars.GAR_REPOSITORY }}
    CLOUD_RUN_SERVICE: ${{ vars.CLOUD_RUN_SERVICE }}
    IMAGE_NAME: darcana-backend
    OPENAI_API_KEY_SECRET_NAME: ${{ vars.OPENAI_API_KEY_SECRET_NAME }}

사용한 값들은 다음과 같다.

  • GCP_PROJECT_ID: Google Cloud 프로젝트 ID

  • GCP_REGION: Cloud Run 배포 region

  • GAR_REPOSITORY: Artifact Registry repository 이름

  • CLOUD_RUN_SERVICE: Cloud Run service 이름

  • OPENAI_API_KEY_SECRET_NAME: Secret Manager에 저장된 OpenAI API Key secret 이름

    Google Cloud 인증에는 service account key를 사용했다.

  • name: Authenticate to Google Cloud
    uses: google-github-actions/auth@v2
    with:
    credentials_json: ${{ secrets.GCP_SA_KEY }}

    이후 gcloud CLI를 설정한다.

  • name: Setup gcloud
    uses: google-github-actions/setup-gcloud@v2

    Docker 이미지 빌드 및 Push

    Cloud Run에 배포하려면 먼저 API 서버를 Docker 이미지로 빌드하고, Google Artifact Registry에 push해야 한다.

     name: Build and push Docker image
      uses: docker/build-push-action@v6
      with:
        context: ./backend
        push: true
        tags: |
          ${{ env.IMAGE }}
          ${{ env.IMAGE_LATEST }}
        cache-from: type=gha,scope=backend
        cache-to: type=gha,scope=backend,mode=max

    context: ./backend로 백엔드 디렉토리를 기준으로 이미지를 빌드한다.

    PR 단계에서는 push: false로 빌드 가능 여부만 확인했고, main 브랜치에 반영된 뒤에는 push: true로 설정해 Artifact Registry에 이미지를 업로드했다.

    이미지는 현재 커밋을 가리키는 ${GITHUB_SHA} tag와 최신 배포를 가리키는 latest tag를 함께 사용했다.

    Cloud Run 배포

    이미지를 Artifact Registry에 push한 뒤, Cloud Run에 배포한다.

  - name: Deploy to Cloud Run
    run: |
      gcloud run deploy "$CLOUD_RUN_SERVICE" \
        --image "$IMAGE" \
        --project "$GCP_PROJECT_ID" \
        --region "$GCP_REGION" \
        --platform managed \
        --port 8080 \
        --allow-unauthenticated \
        --quiet

--image에는 방금 빌드하고 push한 Docker 이미지 주소가 들어간다.

API 서버는 8080 포트로 실행되기 때문에 --port 8080을 지정했다.

OpenAI API Key는 코드에 직접 넣지 않고, Google Secret Manager에 저장한 값 을 Cloud Run 환경변수로 연결했다.

Health Check

배포 후에는 /health 엔드포인트를 호출해서 API 서버가 정상적으로 응답하는지 확인한다.

  - name: Check health
    run: |
      for attempt in {1..10}; do
        if curl --fail --silent --show-error "$SERVICE_URL/health"; then
          exit 0
        fi
        sleep 6
      done

      exit 1

서버가 바로 준비되지 않을 수 있으므로 최대 10번까지 재시도했다.

백엔드에는 health check용 엔드포인트를 따로 두었다.

  @app.route('/health')
  def health():
      return 'OK', 200

Rollback Workflow

배포 이후 문제가 생겼을 때를 대비해 수동 rollback workflow도 추가했다.


- name: Rollback traffic to revision
  run: |
    gcloud run services update-traffic "$CLOUD_RUN_SERVICE" \
      --to-revisions="${{ inputs.revision }}=100" \
      --project "$GCP_PROJECT_ID" \
      --region "$GCP_REGION"

새 이미지를 다시 빌드하는 것이 아니라, Cloud Run traffic을 이전 revision으로 100% 전환하는 방식이다.

마무리

이번 글에서는 GitHub Actions를 사용해 백엔드 Cloud Run CI/CD를 구성했다.

PR 단계에서는 pytest와 Docker build를 실행해 백엔드 코드가 정상적으로 동작하고 이미지로 빌드 가능한지 검증한다.

main 브랜치에 코드가 반영되면 Docker 이미지를 Artifact Registry에 push하고, Cloud Run에 자동 배포한다.

배포 이후에는 /health 엔드포인트를 호출해 실제 서버가 정상적으로 응답하는지 확인한다.

또한 문제가 생겼을 때를 대비해 수동 rollback workflow도 함께 구성했다.

프론트엔드는 Vercel, 백엔드는 Cloud Run으로 배포 대상이 다르기 때문에 처음에는 설정해야 할 값이 많았다.

하지만 CI/CD를 나누어 구성해두니 PR 단계에서는 안정성을 검증하고, main 반영 이후에는 자동으로 운영 환경까지 배포되는 흐름을 만들 수 있었다.

결과적으로 반복적인 배포 작업을 줄일 수 있었고, GitHub Actions 캐시를 적용해 Docker 빌드 시간도 약 75% 단축할 수 있었다. 현재 백엔드 API 서버의 규모가 크지 않아 실제 시간으로는 24초에서 6초로 약 18초 단축된 정도였지만, 프로젝트 규모가 커지고 의존성이 늘어날수록 캐시를 통한 시간 절감 효과는 더 커질 것으로 기대된다.

또한 배포 전에 테스트와 Docker build를 자동으로 실행하면서, 배포 단계에 들어가기 전에 문제를 더 빠르게 발견할 수 있게 되었다.

0개의 댓글