【GitHub Actions CD 實戰】#05 整合實戰:React + FastAPI 全端專案完整 CD 流程

測驗:整合實戰 – React + FastAPI 全端專案完整 CD 流程

共 5 題,點選答案後會立即顯示結果

1. 在全端部署的 workflow 中,為什麼通常要先部署後端再部署前端?

  • A. 因為後端程式碼比較大,需要更多時間
  • B. 因為前端需要知道後端的 API 網址才能正確呼叫
  • C. 因為 GitHub Actions 的規定要求後端必須先執行
  • D. 因為 Railway 的部署速度比 Vercel 快

2. 在 GitHub Actions 中,如何讓一個 job 等待另一個 job 完成後才執行?

deploy-frontend: ???: deploy-backend runs-on: ubuntu-latest
  • A. depends
  • B. after
  • C. needs
  • D. wait-for

3. 在 Vite (React) 專案中,前端要讀取環境變數必須使用什麼前綴?

  • A. REACT_APP_
  • B. VITE_
  • C. NEXT_PUBLIC_
  • D. PUBLIC_

4. 當前端和後端部署在不同網域時,後端需要設定什麼才能讓前端正常呼叫 API?

  • A. SSL 憑證
  • B. 負載平衡器
  • C. CORS (跨來源資源共用)
  • D. CDN 快取

5. 在 workflow 中,如何將一個 job 的輸出傳遞給另一個 job 使用?

deploy-backend: outputs: api_url: ${{ steps.deploy.outputs.url }} deploy-frontend: needs: deploy-backend env: API_URL: ???
  • A. ${{ outputs.api_url }}
  • B. ${{ jobs.deploy-backend.api_url }}
  • C. ${{ deploy-backend.outputs.api_url }}
  • D. ${{ needs.deploy-backend.outputs.api_url }}

一句話說明

一個 workflow 同時部署前端和後端,處理好它們之間的依賴關係。

這篇教什麼

前四篇我們分別學了:

  • #01 CD 的基本概念和觸發時機
  • #02 使用 secrets 保護敏感資訊
  • #03 部署到 Vercel
  • #04 部署到 Railway

這篇把它們全部串起來,看懂一個完整的全端部署 workflow。


30 秒看懂:完整 workflow 長這樣

name: Deploy Full Stack
on:
  push:
    branches: [main]

jobs:
  deploy-backend:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to Railway
        run: railway up --service backend
        env:
          RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
    outputs:
      api_url: ${{ steps.get-url.outputs.url }}

  deploy-frontend:
    needs: deploy-backend  # 等後端部署完
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to Vercel
        run: vercel deploy --prod
        env:
          VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
          VITE_API_URL: ${{ needs.deploy-backend.outputs.api_url }}
Code language: PHP (php)

這段做了什麼

  1. 先部署後端到 Railway
  2. 取得後端 API 網址
  3. 用這個網址部署前端到 Vercel

Monorepo vs 多 Repo:兩種專案結構

結構 1:Monorepo(前後端同一個 repo)

my-fullstack-app/
├── .github/
│   └── workflows/
│       └── deploy.yml    # 一個 workflow 管兩個
├── frontend/             # React 前端
│   ├── src/
│   └── package.json
└── backend/              # FastAPI 後端
    ├── app/
    └── requirements.txt
Code language: PHP (php)

workflow 特點

jobs:
  deploy-backend:
    # ...部署 backend/ 目錄

  deploy-frontend:
    needs: deploy-backend  # 同一個 workflow 可以設定依賴
    # ...部署 frontend/ 目錄
Code language: PHP (php)

結構 2:多 Repo(前後端各自獨立)

# Repo 1: my-app-frontend
frontend/
├── .github/workflows/deploy.yml
└── src/

# Repo 2: my-app-backend
backend/
├── .github/workflows/deploy.yml
└── app/
Code language: PHP (php)

workflow 特點

# 各自獨立部署
# 需要手動協調順序
# 後端 API URL 要事先固定
Code language: PHP (php)

翻譯

  • Monorepo:「全部放一起,一次部署,好控制順序」
  • 多 Repo:「分開管理,各部署各的,要事先協調」

部署順序:為什麼後端要先部署

jobs:
  deploy-backend:
    runs-on: ubuntu-latest
    # ...後端部署步驟

  deploy-frontend:
    needs: deploy-backend    # 關鍵:等後端完成
    runs-on: ubuntu-latest
    # ...前端部署步驟
Code language: PHP (php)

翻譯needs: deploy-backend = 「這個 job 要等 deploy-backend 完成才能開始」

為什麼要這個順序?

前端需要知道 → 後端的 API 網址
後端部署完才有 → 確定的網址
所以 → 後端先部署

如果順序錯了會怎樣:

前端先部署 → 用的是舊的/錯的 API 網址 → 前端壞掉

環境變數傳遞:前端怎麼知道後端網址

步驟 1:後端部署後輸出網址

deploy-backend:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4

    - name: Deploy to Railway
      id: deploy    # 給這個步驟一個 ID
      run: |
        railway up --service backend
        echo "url=https://my-backend.railway.app" >> $GITHUB_OUTPUT
      env:
        RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}

  outputs:
    api_url: ${{ steps.deploy.outputs.url }}  # 把網址傳出去
Code language: PHP (php)

逐行翻譯

id: deploy                    # 給這步驟取名叫 deploy
echo "..." >> $GITHUB_OUTPUT  # 把值寫到 GitHub 的輸出
outputs:                      # 這個 job 要對外輸出的值
  api_url: ${{ steps.deploy.outputs.url }}  # 從 deploy 步驟拿 url
Code language: PHP (php)

步驟 2:前端使用這個網址

deploy-frontend:
  needs: deploy-backend    # 需要先等後端
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4

    - name: Build Frontend
      run: npm run build
      env:
        VITE_API_URL: ${{ needs.deploy-backend.outputs.api_url }}
        #              ↑ 從 deploy-backend job 拿 api_url

    - name: Deploy to Vercel
      run: vercel deploy --prod
      env:
        VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
Code language: PHP (php)

翻譯${{ needs.deploy-backend.outputs.api_url }} = 「從我依賴的 deploy-backend job 取得它輸出的 api_url」

React 前端怎麼用這個環境變數

// frontend/src/api.js
const API_URL = import.meta.env.VITE_API_URL;  // Vite 用 VITE_ 前綴

async function fetchUsers() {
  const response = await fetch(`${API_URL}/users`);
  //                           ↑ 這會變成 "https://my-backend.railway.app/users"
  return response.json();
}
Code language: JavaScript (javascript)

CORS 設定:讓前端可以呼叫後端

問題:為什麼需要 CORS?

前端網址:https://my-app.vercel.app
後端網址:https://my-backend.railway.app

瀏覽器:「這兩個網域不同,不能直接呼叫!」
Code language: JavaScript (javascript)

後端 FastAPI 的 CORS 設定

# backend/app/main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import os

app = FastAPI()

# 允許的前端網址
allowed_origins = [
    "https://my-app.vercel.app",        # 正式環境
    "http://localhost:5173",             # 本地開發
]

# 也可以從環境變數讀取
if os.getenv("FRONTEND_URL"):
    allowed_origins.append(os.getenv("FRONTEND_URL"))

app.add_middleware(
    CORSMiddleware,
    allow_origins=allowed_origins,  # 允許哪些網址來呼叫
    allow_credentials=True,
    allow_methods=["*"],            # 允許所有 HTTP 方法
    allow_headers=["*"],            # 允許所有 headers
)

@app.get("/users")
async def get_users():
    return [{"name": "Alice"}, {"name": "Bob"}]
Code language: PHP (php)

逐行翻譯

allow_origins=allowed_origins  # 只有這些網址可以呼叫我
allow_credentials=True         # 允許帶 cookie
allow_methods=["*"]            # GET, POST, PUT, DELETE 都可以
allow_headers=["*"]            # 所有 header 都可以
Code language: PHP (php)

在 workflow 中設定 FRONTEND_URL

deploy-backend:
  steps:
    - name: Deploy to Railway
      run: railway up --service backend
      env:
        RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
        FRONTEND_URL: "https://my-app.vercel.app"  # 告訴後端前端在哪
Code language: PHP (php)

完整 Workflow 範例

name: Deploy Full Stack App

on:
  push:
    branches: [main]

env:
  FRONTEND_URL: https://my-app.vercel.app
  BACKEND_URL: https://my-backend.railway.app

jobs:
  # ============ 後端部署 ============
  deploy-backend:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install Railway CLI
        run: npm install -g @railway/cli

      - name: Deploy Backend
        id: deploy-backend
        working-directory: ./backend
        run: |
          railway up --service backend
          echo "url=${{ env.BACKEND_URL }}" >> $GITHUB_OUTPUT
        env:
          RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
          FRONTEND_URL: ${{ env.FRONTEND_URL }}

      - name: Health Check
        run: |
          sleep 30  # 等待部署生效
          curl --fail ${{ env.BACKEND_URL }}/health || exit 1
        #       ↑ 如果 health check 失敗,整個 workflow 失敗

    outputs:
      api_url: ${{ steps.deploy-backend.outputs.url }}

  # ============ 前端部署 ============
  deploy-frontend:
    needs: deploy-backend    # 等後端完成
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
          cache-dependency-path: frontend/package-lock.json

      - name: Install Dependencies
        working-directory: ./frontend
        run: npm ci

      - name: Build
        working-directory: ./frontend
        run: npm run build
        env:
          VITE_API_URL: ${{ needs.deploy-backend.outputs.api_url }}

      - name: Deploy to Vercel
        working-directory: ./frontend
        run: |
          npm install -g vercel
          vercel deploy --prod --token ${{ secrets.VERCEL_TOKEN }}

  # ============ 部署後驗證 ============
  verify-deployment:
    needs: [deploy-backend, deploy-frontend]  # 等兩邊都完成
    runs-on: ubuntu-latest
    steps:
      - name: Verify Backend Health
        run: |
          response=$(curl -s ${{ env.BACKEND_URL }}/health)
          echo "Backend response: $response"

      - name: Verify Frontend
        run: |
          response=$(curl -s -o /dev/null -w "%{http_code}" ${{ env.FRONTEND_URL }})
          if [ "$response" != "200" ]; then
            echo "Frontend returned $response"
            exit 1
          fi
          echo "Frontend is healthy"

      - name: Test API Connection
        run: |
          # 模擬前端呼叫後端
          curl -s ${{ env.BACKEND_URL }}/users \
            -H "Origin: ${{ env.FRONTEND_URL }}" \
            || echo "API test completed"
Code language: PHP (php)

這個 workflow 的流程

push to main
    │
    ▼
┌─────────────────┐
│ deploy-backend  │  先部署後端
└────────┬────────┘
         │ 輸出 api_url
         ▼
┌─────────────────┐
│ deploy-frontend │  再部署前端(使用後端網址)
└────────┬────────┘
         │
         ▼
┌─────────────────────┐
│ verify-deployment   │  最後驗證兩邊都正常
└─────────────────────┘

常見問題排除

問題 1:CORS 錯誤

Access to fetch at 'https://backend.railway.app/users' from origin
'https://frontend.vercel.app' has been blocked by CORS policy
Code language: JavaScript (javascript)

檢查點

# 後端的 allowed_origins 有沒有包含前端網址?
allowed_origins = [
    "https://frontend.vercel.app",  # ← 確認有這個
]
Code language: PHP (php)

問題 2:環境變數沒生效

// 前端顯示 undefined
console.log(import.meta.env.VITE_API_URL);  // undefined
Code language: JavaScript (javascript)

檢查點

# 1. 環境變數名稱要用 VITE_ 開頭(Vite 專案)
env:
  VITE_API_URL: ...   # 對
  API_URL: ...        # 錯,前端讀不到

# 2. 要在 build 時設定,不是 deploy 時
- name: Build
  run: npm run build
  env:
    VITE_API_URL: ${{ needs.deploy-backend.outputs.api_url }}  # 對

- name: Deploy
  run: vercel deploy
  env:
    VITE_API_URL: ...  # 太晚了,build 已經結束
Code language: PHP (php)

問題 3:部署順序問題

Error: needs.deploy-backend.outputs.api_url is empty
Code language: CSS (css)

檢查點

# 後端 job 有沒有正確設定 outputs?
deploy-backend:
  outputs:
    api_url: ${{ steps.deploy-backend.outputs.url }}
    #              ↑ 這個 id 要和 step 的 id 一致

  steps:
    - name: Deploy
      id: deploy-backend    # ← 確認有這個 id
      run: |
        echo "url=https://..." >> $GITHUB_OUTPUT
Code language: PHP (php)

問題 4:Health check 失敗

curl: (7) Failed to connect to my-backend.railway.app port 443
Code language: CSS (css)

檢查點

# 部署後需要等待一段時間
- name: Health Check
  run: |
    sleep 30           # 等 30 秒讓部署生效
    curl --fail ${{ env.BACKEND_URL }}/health

# 或用重試機制
- name: Health Check with Retry
  run: |
    for i in {1..5}; do
      curl --fail ${{ env.BACKEND_URL }}/health && exit 0
      echo "Attempt $i failed, waiting..."
      sleep 10
    done
    exit 1
Code language: PHP (php)

最佳實踐整理

1. 固定網址 vs 動態網址

# 方式 1:固定網址(簡單,推薦初學者)
env:
  BACKEND_URL: https://my-backend.railway.app
  FRONTEND_URL: https://my-app.vercel.app

# 方式 2:動態取得(進階,更靈活)
- name: Get Railway URL
  id: get-url
  run: |
    url=$(railway status --json | jq -r '.deployments[0].url')
    echo "url=$url" >> $GITHUB_OUTPUT
Code language: PHP (php)

2. 平行部署 vs 順序部署

# 如果前端不需要後端的動態資訊,可以平行部署
jobs:
  deploy-backend:
    # ...
  deploy-frontend:
    # 沒有 needs,兩個同時跑

  verify:
    needs: [deploy-backend, deploy-frontend]  # 等兩邊都完成再驗證
Code language: PHP (php)

3. 回滾策略

- name: Deploy with Rollback
  run: |
    # 記錄當前版本
    current_version=$(railway status --json | jq -r '.version')

    # 嘗試部署
    railway up || {
      echo "Deploy failed, rolling back..."
      railway rollback $current_version
      exit 1
    }
Code language: PHP (php)

Vibe Coder 檢查點

看到全端部署的 workflow 時確認:

  • [ ] 部署順序對嗎?後端需要先部署嗎?
  • [ ] 環境變數有正確傳遞嗎?前端拿得到後端網址嗎?
  • [ ] CORS 設定正確嗎?後端允許前端的網域嗎?
  • [ ] 有健康檢查嗎?部署後有驗證服務正常嗎?
  • [ ] secrets 都設好了嗎?RAILWAYTOKEN、VERCELTOKEN 都有嗎?

系列總結

經過這 5 篇文章,你現在能看懂:

篇數 主題 你學會看懂的
#01 CD 基礎 on: push 觸發條件
#02 Secrets ${{ secrets.XXX }} 敏感資訊
#03 Vercel 前端部署 workflow
#04 Railway 後端部署 workflow
#05 整合 完整全端部署流程

當你看到 AI 生成的部署 workflow,你現在知道:

  • 每一行在做什麼
  • 為什麼要這樣寫
  • 出問題時怎麼排查

這就是 Vibe Coder 需要的:不是自己寫,而是看懂 AI 寫的,然後判斷對不對。


延伸:知道就好

這些進階功能遇到再查:

  • Matrix 部署:同時部署到多個環境(staging、production)
  • GitHub Environments:設定不同環境的 secrets 和保護規則
  • Deployment Status API:在 PR 中顯示部署預覽連結
  • Reusable Workflows:把部署流程抽成可重用的 workflow

進階測驗:整合實戰 – React + FastAPI 全端專案完整 CD 流程

測驗目標:驗證你是否能在實際情境中應用所學。
共 5 題,包含情境題與錯誤診斷題。

1. 你正在設計全端專案的 CI/CD 流程,前端使用 React (Vite),後端使用 FastAPI。團隊想要讓前後端可以獨立部署,但部署時又能自動取得最新的後端網址。以下哪種架構設計最合適? 情境題

  • A. 使用多 Repo 架構,前後端各自獨立部署,後端網址寫死在前端程式碼中
  • B. 使用 Monorepo 架構,前後端同時部署,完全不允許獨立部署
  • C. 使用 Monorepo 架構,透過 job outputs 傳遞後端網址給前端,並設定 needs 依賴
  • D. 使用多 Repo 架構,透過 webhook 在後端部署完成後觸發前端重新部署

2. 前端部署完成後,使用者回報 API 呼叫失敗。你在瀏覽器 console 看到以下錯誤訊息: 錯誤診斷

Access to fetch at ‘https://my-backend.railway.app/users’ from origin ‘https://my-app.vercel.app’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.
  • A. 前端的 API 網址設定錯誤,需要修改環境變數
  • B. 後端 FastAPI 的 CORS 設定沒有將前端網址加入 allow_origins
  • C. 後端服務尚未啟動完成,需要等待幾分鐘
  • D. Vercel 的防火牆阻擋了對外部 API 的請求

3. 你的 workflow 中前端 build 完成,但 import.meta.env.VITE_API_URL 在執行時顯示 undefined。檢查 workflow 設定如下: 錯誤診斷

deploy-frontend: needs: deploy-backend steps: – name: Build run: npm run build – name: Deploy to Vercel run: vercel deploy –prod env: VITE_API_URL: ${{ needs.deploy-backend.outputs.api_url }}
  • A. 環境變數應該設定在 Build 步驟,而不是 Deploy 步驟,因為 Vite 在 build 時才會注入環境變數
  • B. 應該使用 REACT_APP_API_URL 而不是 VITE_API_URL
  • C. needs.deploy-backend.outputs 語法錯誤,應該用 jobs.deploy-backend.outputs
  • D. Vercel 不支援從 GitHub Actions 傳遞環境變數

4. 你的團隊希望在前後端都部署完成後,自動執行健康檢查驗證服務正常。應該如何設計 workflow? 情境題

  • A. 在 deploy-frontend job 的最後加入健康檢查步驟
  • B. 在 deploy-backend job 的最後加入健康檢查步驟
  • C. 建立獨立的 verify-deployment job,設定 needs: [deploy-backend, deploy-frontend]
  • D. 使用 cron 排程在部署後 10 分鐘執行獨立的健康檢查 workflow

5. 後端部署到 Railway 後,你執行健康檢查但持續失敗。查看 workflow log 顯示: 錯誤診斷

curl: (7) Failed to connect to my-backend.railway.app port 443: Connection refused

你的健康檢查步驟如下:

– name: Health Check run: curl –fail ${{ env.BACKEND_URL }}/health
  • A. Railway 服務完全部署失敗,需要檢查部署 log
  • B. 部署完成後需要等待一段時間讓服務啟動,應該在健康檢查前加入 sleep 或重試機制
  • C. BACKEND_URL 環境變數設定錯誤,指向了錯誤的網址
  • D. Railway 的 port 443 被防火牆封鎖,需要改用其他 port

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *