콘텐츠로 이동

05. 24시간 일하는 배포 비서 (CI/CD)

"반복적인 배포 작업은 비서(GitHub Actions)에게 맡기고 우리는 코드에 집중합시다."

수동 배포는 실수가 생기기 쉽고 번거롭습니다. 현대적인 개발 방식인 CI(지속적 통합)CD(지속적 배포)를 설정하여, 코드가 안전하게 검증되고 자동으로 클라우드에 반영되는 파이프라인을 완성해 봅시다.


1. GitHub Actions: 내 코드의 자동 컨베이어 벨트

GitHub Actions는 특정 이벤트(예: 코드 push)가 발생하면 미리 정의된 작업(Workflow)을 실행하는 자동화 도구입니다.

🤖 자동화 시나리오

  1. Code Push: 개발자가 main 브랜치에 코드를 합칩니다.
  2. Install & Build: GitHub 서버가 가상 컴퓨터를 켜서 라이브러리를 설치하고 React를 빌드합니다.
  3. Deploy: 빌드가 성공하면 Firebase 서버로 새 버전을 전송합니다.

🔄 자동 배포 흐름 (Frontend + Backend)

  1. 로컬에서 develop 브랜치 작업 후 push
  2. developmain 브랜치로 Pull Request 생성 및 Merge
  3. GitHub Actions가 다음 순서로 작업을 자동 수행합니다:
  4. 백엔드/데이터베이스: functions 빌드 및 Cloud Functions & Firestore 배포
  5. 프론트엔드: frontend 빌드 및 Firebase Hosting 배포

📊 CI/CD 전체 흐름도 (Sequence Diagram)

로컬 개발 환경부터 클라우드 배포까지의 여정을 한눈에 확인해보세요.

sequenceDiagram
    participant Local as 💻 Local Dev (내 컴퓨터)
    participant GitHub as 🐙 GitHub (저장소)
    participant Actions as ⚙️ GitHub Actions (배포 로봇)
    participant IAM as 🔑 GCP I.A.M (보안관)
    participant Firebase as 🔥 Firebase (클라우드)

    Note over Local, Firebase: 1. 코드 작성 및 업로드
    Local->>Local: 코드 수정 (Feature 개발)
    Local->>GitHub: git push origin develop
    GitHub->>GitHub: PR (develop -> main) & Merge

    Note over GitHub, Firebase: 2. 자동 배포 시작 (CI/CD)
    GitHub->>Actions: 🚨 "main 브랜치가 변경됨!" (Trigger)

    rect rgb(240, 248, 255)
    Note right of Actions: 🔐 인증 절차 (Authentication)
    Actions->>IAM: "이 열쇠(Secrets)로 문 좀 열어줘"
    IAM-->>Actions: "확인됨. 지나가세요." (Access Token 발급)
    end

    rect rgb(255, 240, 245)
    Note right of Actions: 🚀 실전 배포 (Deployment)
    Actions->>Firebase: 1. Cloud Functions 배포 (Backend)
    Actions->>Firebase: 2. Hosting 배포 (Frontend)
    Actions->>Firebase: 3. Firestore Rules/Indexes 배포
    end

    Firebase-->>Local: 🌐 배포 완료 (사이트 접속 가능)

2. Secrets: 금고 속에 숨기는 API Key

API Key나 Firebase 서비스 계정 키 같은 민감한 정보는 절대 코드에 직접 적어서 GitHub에 올리면 안 됩니다.

🔑 GitHub Secrets 설정법

  1. GitHub 저장소 > Settings > Secrets and variables > Actions로 이동합니다.
  2. 'New repository secret'을 클릭합니다.
  3. frontend/.env 파일에 있던 내용들을 다음 이름으로 하나씩 저장합니다:
Secret Name Value (.env 파일 참고)
VITE_FIREBASE_API_KEY Firebase Console에서 확인한 API Key
VITE_FIREBASE_AUTH_DOMAIN PROJECT_ID.firebaseapp.com
VITE_FIREBASE_PROJECT_ID Firebase 프로젝트 ID
VITE_FIREBASE_STORAGE_BUCKET PROJECT_ID.firebasestorage.app
VITE_FIREBASE_MESSAGING_SENDER_ID Firebase Console에서 확인
VITE_FIREBASE_APP_ID Firebase Console에서 확인

왜 필요한가요?

  • frontend/.env 파일은 .gitignore에 포함되어 GitHub에 업로드되지 않습니다
  • GitHub Actions 빌드 시 환경 변수가 없으면 invalid-api-key 에러 발생
  • Secrets를 사용하면 민감한 정보를 안전하게 관리하면서 빌드에 주입 가능

📝 Workflow 파일에 Secrets 주입

firebase init이 생성한 .github/workflows/firebase-hosting-merge.yml.github/workflows/firebase-hosting-pull-request.yml 모두 수정:

# 수정 전
- run: cd frontend && npm ci && npm run build

# 수정 후
- run: cd frontend && npm ci && npm run build
  env:
    VITE_FIREBASE_API_KEY: ${{ secrets.VITE_FIREBASE_API_KEY }}
    VITE_FIREBASE_AUTH_DOMAIN: ${{ secrets.VITE_FIREBASE_AUTH_DOMAIN }}
    VITE_FIREBASE_PROJECT_ID: ${{ secrets.VITE_FIREBASE_PROJECT_ID }}
    VITE_FIREBASE_STORAGE_BUCKET: ${{ secrets.VITE_FIREBASE_STORAGE_BUCKET }}
    VITE_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.VITE_FIREBASE_MESSAGING_SENDER_ID }}
    VITE_FIREBASE_APP_ID: ${{ secrets.VITE_FIREBASE_APP_ID }}

ℹ️ 참고: Firebase Config의 모든 값(API Key, Project ID 등)은 클라이언트 측 공개 식별자이며, 공개되어도 안전합니다. 실제 보안은 Firebase Security Rules, Authentication, 도메인 제한으로 관리됩니다. (Firebase 공식 문서)



3. 배포 설정: firebase.json의 진화

1장에서 생성된 firebase.json은 기본 껍데기일 뿐입니다. 실제 배포를 위해선 자동 빌드(Predeploy)백엔드 연결(Rewrites) 설정이 반드시 추가되어야 합니다.

firebase.json 파일을 열어 아래 내용으로 완전히 교체하세요.

{
  "firestore": {
    "database": "(default)",
    "location": "asia-northeast3",
    "rules": "firestore.rules",
    "indexes": "firestore.indexes.json"
  },
  "functions": [
    {
      "source": "functions",
      "codebase": "default",
      "predeploy": [
        "python -m venv functions/venv",
        ". functions/venv/bin/activate && pip install -r functions/requirements.txt"
      ],
      "disallowLegacyRuntimeConfig": true,
      "ignore": [
        "venv",
        ".git",
        "firebase-debug.log",
        "firebase-debug.*.log",
        "*.local"
      ],
      "runtime": "python313"
    }
  ],
  "hosting": {
    "public": "frontend/dist",
    "predeploy": [
      "cd frontend && npm ci && npm run build"
    ],
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "/api/**",
        "function": "fastapi_handler",
        "region": "asia-northeast3"
      },
      {
        "source": "!/assets/**",
        "destination": "/index.html"
      }
    ]
  },
  "emulators": {
    "auth": {
      "port": 9099
    },
    "functions": {
      "port": 5001
    },
    "firestore": {
      "port": 8080
    },
    "hosting": {
      "port": 5002
    },
    "ui": {
      "enabled": true
    }
  }
}

🔑 무엇이 달라졌나요?

  1. predeploy (자동 빌드 Hook)

    • Frontend: 배포 직전 npm run build를 강제하여, 실수로 구버전을 배포하는 것을 막습니다. (이 명령어는 Windows/Mac/Linux 어디서든 잘 동작합니다.)
    • Backend: 배보 직전 functions 폴더에서 파이썬 가상환경을 만들고, requirements.txt에 명시된 패키지들을 설치합니다.
  2. rewrites (트래픽 교통정리)

    • source: "/api/**": API 요청을 Functions(백엔드)로 납치하여 전달합니다. (CORS 문제 해결!)
    • region: (매우 중요) 서울(asia-northeast3)로 지정하지 않으면 요청이 미국을 찍고 오느라 느려집니다. functions 함수에도 동일한 리전 설정을 해야합니다.
    • destination: "/index.html": 사용자가 /about 같은 주소로 직접 접속해도 404가 뜨지 않고 React 앱이 받도록 합니다. (SPA 라우팅)

4. Workflow 작성: 백엔드 배포 추가

.github/workflows/firebase-hosting-merge.yml 파일을 수정하여 백엔드도 자동 배포되도록 만듭니다.

🔧 Workflow 파일 수정

# This file was auto-generated by the Firebase CLI
# https://github.com/firebase/firebase-tools

name: Deploy to Firebase Hosting on merge
on:
  push:
    branches:
      - main
jobs:
  build_and_deploy:
    runs-on: ubuntu-latest
    env:
      VITE_FIREBASE_API_KEY: ${{ secrets.VITE_FIREBASE_API_KEY }}
      VITE_FIREBASE_AUTH_DOMAIN: ${{ secrets.VITE_FIREBASE_AUTH_DOMAIN }}
      VITE_FIREBASE_PROJECT_ID: ${{ secrets.VITE_FIREBASE_PROJECT_ID }}
      VITE_FIREBASE_STORAGE_BUCKET: ${{ secrets.VITE_FIREBASE_STORAGE_BUCKET }}
      VITE_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.VITE_FIREBASE_MESSAGING_SENDER_ID }}
      VITE_FIREBASE_APP_ID: ${{ secrets.VITE_FIREBASE_APP_ID }}
    steps:
      - uses: actions/checkout@v4
      - name: Check Secrets Presence
        run: |
          if [ -z "$VITE_FIREBASE_API_KEY" ]; then echo "❌ VITE_FIREBASE_API_KEY is missing"; else echo "✅ VITE_FIREBASE_API_KEY is present"; fi
      - name: Set up Python 3.13
        uses: actions/setup-python@v5
        with:
          python-version: "3.13"
      - name: Authenticate to Google Cloud
        uses: google-github-actions/auth@v2
        with:
          credentials_json: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_[YOUR_PROJECT_ID 대문자] }}

      - name: Deploy Cloud Functions and Firestore
        run: npx firebase-tools deploy --only functions,firestore --project [YOUR_PROJECT_ID]
      - uses: FirebaseExtended/action-hosting-deploy@v0
        with:
          repoToken: ${{ secrets.GITHUB_TOKEN }}
          firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_[YOUR_PROJECT_ID 대문자] }}
          channelId: live
          projectId: [YOUR_PROJECT_ID]

.github/workflows/firebase-hosting-pull-request.yml 파일을 수정하여 PR 시에 배포전 간단한 검증을 시행합니다.

# This file was auto-generated by the Firebase CLI
# https://github.com/firebase/firebase-tools

name: Deploy to Firebase Hosting on PR
on: pull_request
permissions:
  checks: write
  contents: read
  pull-requests: write
jobs:
  build_and_preview:
    if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
    runs-on: ubuntu-latest
    env:
      VITE_FIREBASE_API_KEY: ${{ secrets.VITE_FIREBASE_API_KEY }}
      VITE_FIREBASE_AUTH_DOMAIN: ${{ secrets.VITE_FIREBASE_AUTH_DOMAIN }}
      VITE_FIREBASE_PROJECT_ID: ${{ secrets.VITE_FIREBASE_PROJECT_ID }}
      VITE_FIREBASE_STORAGE_BUCKET: ${{ secrets.VITE_FIREBASE_STORAGE_BUCKET }}
      VITE_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.VITE_FIREBASE_MESSAGING_SENDER_ID }}
      VITE_FIREBASE_APP_ID: ${{ secrets.VITE_FIREBASE_APP_ID }}
    steps:
      - uses: actions/checkout@v4
      - name: Check Secrets Presence
        run: |
          if [ -z "$VITE_FIREBASE_API_KEY" ]; then echo "❌ VITE_FIREBASE_API_KEY is missing"; else echo "✅ VITE_FIREBASE_API_KEY is present"; fi
      - uses: FirebaseExtended/action-hosting-deploy@v0
        with:
          repoToken: ${{ secrets.GITHUB_TOKEN }}
          firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_[YOUR_PROJECT_ID 대문자] }}
          projectId: [YOUR_PROJECT_ID]
      - name: Set up Python 3.13
        uses: actions/setup-python@v5
        with:
          python-version: "3.13"

      - name: Validation Check (Pip Install)
        run: |
          cd functions
          pip install -r requirements.txt
          python -m py_compile *.py

5. 배포 전 필수 설정: Google Cloud IAM

GitHub Actions가 자동으로 배포하려면 서비스 계정에 적절한 권한이 부여되어야 합니다. 첫 배포 전에 한 번에 설정해두면 나중에 배포 에러로 고생하지 않습니다!

⚠️ 참고: [ID]-compute@... 계정은 Cloud Functions를 처음 배포한 후에 IAM 페이지에 나타납니다. 첫 배포 후 권한을 추가하세요.

🔐 IAM 권한 설정 방법

  1. Google Cloud Console > IAM 접속
  2. 상단에서 Firebase 프로젝트 선택

1️⃣ GitHub Actions 서비스 계정 (github-action-...)

이 계정이 GitHub에서 자동 배포를 수행합니다. 다음 역할들을 부여하세요:

  • Cloud 함수 관리자 (Cloud Functions Admin)
  • 서비스 계정 사용자 (Service Account User)
  • 서비스 사용량 소비자 (Service Usage Consumer)
  • Artifact Registry 관리자 (Artifact Registry Administrator)
  • Cloud Build 편집자 (Cloud Build Editor)
  • Firebase 규칙 관리자 (Firebase Rules Admin)
  • Cloud Datastore 인덱스 관리자 (Cloud Datastore Index Admin)
  • 보안 비밀 관리자 뷰어 (Secret Manager Viewer)
  • 보안 비밀 관리자 보안 비밀 접근자 (Secret Manager Secret Accessor)
  • Cloud Run 뷰어 (Cloud Run Viewer)

2️⃣ Default Compute Service Account ([ID]-compute@...)

이 계정은 Cloud Functions가 실행될 때 사용됩니다. 하지만 처음 프로젝트를 만들면 IAM 목록에 안 보일 수 있습니다.

계정이 안 보인다면? (Fail-First Strategy)

이 계정은 배포 시도(Deployment Attempt)가 있어야 생성됩니다.

  1. 먼저 터미널에서 firebase deploy --only functions 명령어로 배포를 시도하세요.
    • (주의) 이때 functions/main.py의 리전 설정이 한국(asia-northeast3)인지 꼭 확인하세요!
  2. 권한 문제로 배포가 실패(Fail)할 것입니다.
  3. 에러 로그에 [PROJECT-ID]-compute@developer.gserviceaccount.com 주소가 찍혀있는지 확인하세요.
  4. 이제 IAM 페이지 > 액세스 권한 부여 버튼을 누르고, 방금 확인한 이메일을 직접 입력하여 아래 권한을 부여합니다.
  • Cloud Datastore 사용자 (Cloud Datastore User) - Firestore 접근용
  • 보안 비밀 관리자 보안 비밀 접근자 (Secret Manager Secret Accessor)
  • Cloud 빌드 서비스 계정 (Cloud Build Service Account)

3️⃣ Firebase 서비스 에이전트 (firebase-adminsdk-...)

Firebase Admin SDK 연동에 필요합니다:

  • 보안 비밀 관리자 관리 (Secret Manager Admin)
  • 서비스 계정 사용자 (Service Account User)
  • 서비스 계정 토큰 생성자 (Service Account Token Creator)
  • Firebase 인증 관리자 (Firebase Authentication Admin)
  • Firebase Admin SDK 관리자 서비스 에이전트

IAM 페이지에서 엑세스 권한 부여 버튼을 누르고 주 구성원 검색으로 github, compute, firebase 키워드로 검색하면 연관 구성원을 찾을 수 있습니다.


6. 배포 후 추가 설정

아래는 배포 후 추가로 필요한 설정입니다.

💡 GEMINI_API_KEY 설정 (Secret Manager)

보안을 위해 API 키는 Firebase Secret Manager를 통해 관리합니다.

  1. 터미널에서 아래 명령어를 실행하여 API 키를 등록하세요:
    firebase functions:secrets:set GEMINI_API_KEY
    
  2. functions/main.pyfastapi_handlersecrets=["GEMINI_API_KEY"]가 추가되어 있는지 확인하세요.
  3. 이 방식은 GitHub Secrets보다 안전하며 프로덕션 환경에서 권장되는 방식입니다.

💡 중요: 'Cloud Run 호출자' 권한과 보안

배포 후 프론트엔드에서 백엔드를 호출하려면 allUsers에게 Cloud Run 호출자 권한을 부여해야 합니다.

  • 1차 관문 (IAM): allUsers 권한은 "누구나 서버 문 앞까지 올 수 있다"는 뜻입니다. 웹 앱 특성상 익명의 브라우저 요청을 받아야 하므로 필수입니다.
  • 2차 관문 (Application): 실제 보안은 코드 내부에서 이루어집니다. Firebase Auth 토큰을 검증하여, 로그인된 유효한 사용자만 기능을 사용할 수 있도록 철저히 보호합니다.

[설정 방법]

  1. Google Cloud Console > Cloud Run 접속
  2. fastapi-handler 서비스 클릭 → 보안(Security) 탭 이동
  3. 인증(Authentication) 섹션 확인
  4. 🔘 공개 액세스 허용 (Allow unauthenticated invocations) 선택
  5. 저장 (Save) 클릭

5. [Check] 자동 배포 성공 확인

설정을 마치고 코드를 push 하면 GitHub의 Actions 탭에서 비서가 일하는 모습을 실시간으로 볼 수 있습니다.

  • 초록색 체크(✅): 배포 성공! 이제 웹사이트에 접속하면 수정사항이 반영되어 있습니다.
  • 빨간색 엑스(❌): 배포 실패. 로그를 확인하여 어디서 문제가 생겼는지(빌드 에러, 권한 부족 등) 확인합니다.

6. 핵심 정리

용어 설명 비유
CI (Continuous Integration) 코드 변경사항을 자동으로 테스트하고 통합하는 것 재료가 들어올 때마다 신선도 검사하기
CD (Continuous Deployment) 검증된 코드를 실제 서버에 자동으로 반영하는 것 완성된 요리를 서빙 로봇이 배달하기
GitHub Secrets 민감한 정보를 저장하는 GitHub의 보안 저장소 식당 카운터 밑의 금고
YAML (.yml) 자동화 설정 등을 적는 문서 규격 업무 매뉴얼

준비 완료! 이제 우리는 코드만 짜면 배포는 알아서 되는 '무한 동력' 시스템을 가졌습니다. 드디어 마지막 06장에서는 이 모든 기술을 총동원하여, Gemini AI가 상담하고 수강신청을 도와주는 최종 프로젝트(Capstone)를 완성해 보겠습니다!


실습 과제: 간단한 글자 하나를 수정하고 GitHub에 push 해보세요. 내 손을 거치지 않고도 잠시 후 웹사이트가 업데이트되는 마법 같은 과정을 지켜보세요!