如何从本地部署转向使用 GitHub Actions 实现快速 CI/CD
我一直在构建一个发音辅导网络应用来帮助我提高英语的韵律。我是西班牙语母语者,所以我想把它开放出来,看看是否能帮助其他学习英语作为第二语言的人。后端使用 Python 和 FastAPI,使用 Parselmouth(Praat 的 Python 封装库)进行音频分析,并使用 Google Gemini 来生成辅导反馈。前端使用 React 配合 Vite 和 TypeScript。
对于基础设施,我选择了 Google Cloud。后端运行在 Cloud Run 上,前端托管在 Firebase Hosting 上,用户数据存储在 Firestore 中。Docker 镜像推送到 Artifact Registry。代码托管在 GitHub 上。这是一个相当标准的个人项目设置,我希望它能保持在免费套餐限制内。
问题所在
我过去是从本地机器部署前端和后端的。前端部署速度够快,但后端部署很痛苦。每次部署都需要从头开始构建 Docker 镜像,这需要 5-10 分钟,因为 pip 需要下载所有 Python 依赖项。
我希望转向 CI/CD,这样在我推送到 main 分支时部署就能自动发生。但我也不想每次推送都等 10 分钟。所以我着手让 GitHub Actions 智能地判断哪些内容实际发生了变化。
Monorepo 结构
所有内容都放在一个仓库中,包含一个 frontend/ 文件夹和一个 backend/ 文件夹。工作流在推送到 main 时触发,我希望它能确定实际更改了哪些部分,并只部署那一部分。前端文件变了?只部署到 Firebase。后端代码变了?只部署到 Cloud Run。如果 requirements.txt 变了,就重建 Docker 基础镜像。但仅限于那时。
寻找检测变化的方法
我搜索了一下,找到了一个名为 dorny/paths-filter 的 action。它会检查一次推送中哪些文件发生了变化,并输出其他 job 可以使用的布尔值。我将其设置为工作流中的第一个 job:
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
backend: ${{ steps.filter.outputs.backend }}
frontend: ${{ steps.filter.outputs.frontend }}
requirements: ${{ steps.filter.outputs.requirements }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
backend:
- 'backend/**'
frontend:
- 'frontend/**'
requirements:
- 'backend/requirements.txt'
- 'backend/Dockerfile.base'这为我提供了三个输出,我可以在其他 job 中引用它们来决定是否运行。
处理缓慢的 Docker 构建
即使检测变化功能正常,后端部署仍然很慢。每次构建都会从头开始运行 pip install。我的 requirements.txt 包含 ML 库、音频处理等内容。这需要下载很多东西。
我意识到我可能一个月才更改一次依赖项。其余时间我只是更改应用程序代码。所以我尝试将 Dockerfile 分为两部分。
Dockerfile.base
我把所有耗时的东西都放在这里:系统包、pip 依赖项。想法是只构建一次并将其推送到 Artifact Registry,然后重复使用它。
FROM python:3.11-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
ffmpeg \
espeak-ng \
libsndfile1 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --upgrade pip && \
pip install --no-cache-dir -r requirements.txtDockerfile.fast
这个从基础镜像开始,只复制代码。没有 apt-get。没有 pip。只有一个 COPY 命令。
FROM us-central1-docker.pkg.dev/lescoach/prosody-repo/prosody-api-base:latest
WORKDIR /app
COPY app/ ./app/
ENV PYTHONUNBUFFERED=1
ENV PORT=8080
EXPOSE 8080
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]这使构建时间从 5-10 分钟缩短到大约 30 秒,这是一个不错的改进。
组合工作流
我设置了基础镜像构建 job,使其仅在依赖项实际更改时运行:
build-base-image:
runs-on: ubuntu-latest
needs: detect-changes
if: needs.detect-changes.outputs.requirements == 'true'
steps:
- uses: actions/checkout@v4
- uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.WIF_PROVIDER }}
service_account: ${{ secrets.WIF_SERVICE_ACCOUNT }}
- uses: google-github-actions/setup-gcloud@v2
- run: gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
- name: Build and push base image
working-directory: backend
run: |
docker build -f Dockerfile.base -t ${{ env.ARTIFACT_REGISTRY }}/prosody-api-base:latest .
docker push ${{ env.ARTIFACT_REGISTRY }}/prosody-api-base:latest后端部署 job 经历了一些反复试验。如果基础镜像构建运行了,它需要等待它完成,但如果跳过了,它也需要正常工作。我最终得到了这个:
deploy-backend:
runs-on: ubuntu-latest
needs: [detect-changes, build-base-image]
if: |
always() &&
needs.detect-changes.outputs.backend == 'true' &&
(needs.build-base-image.result == 'success' || needs.build-base-image.result == 'skipped')
steps:
# auth steps...
- name: Check if base image exists
id: check-base
run: |
if docker manifest inspect ${{ env.ARTIFACT_REGISTRY }}/prosody-api-base:latest > /dev/null 2>&1; then
echo "exists=true" >> $GITHUB_OUTPUT
else
echo "exists=false" >> $GITHUB_OUTPUT
fi
- name: Build base image if missing
if: steps.check-base.outputs.exists == 'false'
working-directory: backend
run: |
docker build -f Dockerfile.base -t ${{ env.ARTIFACT_REGISTRY }}/prosody-api-base:latest .
docker push ${{ env.ARTIFACT_REGISTRY }}/prosody-api-base:latest
- name: Build and push app image
working-directory: backend
run: |
docker build -f Dockerfile.fast -t ${{ env.ARTIFACT_REGISTRY }}/prosody-api:${{ github.sha }} .
docker push ${{ env.ARTIFACT_REGISTRY }}/prosody-api:${{ github.sha }}
- uses: google-github-actions/deploy-cloudrun@v2
with:
service: prosody-api
region: us-central1
image: ${{ env.ARTIFACT_REGISTRY }}/prosody-api:${{ github.sha }}
flags: --allow-unauthenticated我了解到需要使用 always()。如果没有它,如果上游 job 被跳过,该 job 甚至不会评估其条件。我还添加了一个 docker manifest inspect 检查,以处理基础镜像尚不存在的情况,例如在首次部署时或如果有人不小心删除了它。
前端部分
前端 job 最终更简单,因为它完全不依赖于后端:
deploy-frontend:
runs-on: ubuntu-latest
needs: detect-changes
if: needs.detect-changes.outputs.frontend == 'true'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- run: npm ci
working-directory: frontend
- run: npm run build
working-directory: frontend
- uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: ${{ secrets.GITHUB_TOKEN }}
firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_LESCOACH }}
channelId: live
projectId: lescoach
entryPoint: frontend如果只更改了前端,那么只有这个 job 会运行。
身份验证设置
我选择了工作负载身份联合而不是服务帐户密钥。前期设置更多,但我喜欢不必处理轮换密钥或担心机密中存放 JSON 文件。GitHub 通过 OIDC 获取短期令牌,这对我来说感觉更干净。
我的最终结果
仅前端更改现在的部署时间不到一分钟。后端代码更改大约需要 90 秒。依赖项更改仍然需要大约 4 分钟,但对我来说这种情况很少发生。

大多数日子里我都在调整 UI 方面的内容,现在部署速度很快。整个工作流最终大约有 130 行 YAML。paths-filter action 负责了大部分判断哪些内容发生变化的工作。