logo
full-stack
  • Home
  • Pricing
logo
full-stack
Copyright © 2025 full-stack. Ltd.
Links
SubscribeManage Subscription
Powered by Postion - Create. Publish. Own it.
Privacy policy•Terms

Postion

从零到云端:一个真实的 CI/CD 挖坑与填坑全记录

从零到云端:一个真实的 CI/CD 挖坑与填坑全记录

b
by buoooou
•Aug 8, 2025

第一章:地基搭建 —— CI/CD 的前期准备

在编写任何自动化脚本之前,我们必须先建立起安全的信任链,让我们的“自动化管家”(GitHub Actions) 能够合法地访问我们的“云端豪宅”(EC2) 和“中央仓库”(Docker Hub)。

1. 准备一把专用的“部署钥匙” (SSH 密钥)

原则: 绝不使用你的 EC2 主密钥进行自动化操作。我们要遵循“权限最小化”原则,为 CI/CD 创建一把专用的钥匙。

  1. 在本地生成新密钥对: 打开终端,运行以下命令。这把钥匙没有密码,便于自动化脚本使用。

    codeBash

    ssh-keygen -t rsa -b 4096 -f github_actions_deploy_key -N ""
  2. 安装“新门锁”: 将新生成的公钥 (github_actions_deploy_key.pub) 的内容,追加到你 EC2 服务器的 ~/.ssh/authorized_keys 文件中。

    • 重要: authorized_keys 文件是一个列表,可以存放多个公钥。确保你是另起一行追加,而不是覆盖原有内容。

    • 修复权限: 登录 EC2,运行 chmod 700 ~/.ssh 和 chmod 600 ~/.ssh/authorized_keys 来确保 SSH 服务的安全要求。

2. 获取“仓库通行证” (Docker Hub 令牌)

  1. 登录 Docker Hub,在“Account Settings” -> “Security”中创建一个新的 Access Token。

  2. 立即复制并保管好这个令牌,因为它只会显示一次。

3. 建立“数字保险箱” (GitHub Secrets)

将我们刚刚准备好的所有敏感信息,安全地存放到 GitHub 仓库的 "Settings" -> "Secrets and variables" -> "Actions" 中。

Secret 名称存放的值DOCKERHUB_USERNAME你的 Docker Hub 用户名DOCKERHUB_TOKEN上一步生成的 Docker Hub 访问令牌EC2_HOST你 EC2 服务器的公网 IP 地址EC2_USERNAME你登录 EC2 的用户名 (如 ubuntu 或 ec2-user)SSH_PRIVATE_KEY新生成的私钥 github_actions_deploy_key 的全部内容

第二章:最终蓝图 —— 成功运行的配置文件

在我们深入“战争故事”之前,让我们先看一下经历九九八十一难后,最终能完美运行的几个核心配置文件。

1. .dockerignore (根目录)

这个文件至关重要,它能防止我们将本地的 node_modules 等无关文件打包进镜像,避免冲突和镜像臃肿。

codeCode

# .dockerignore
**/node_modules
**/target
frontend/dist
.idea
.vscode
*.iml

2. Dockerfile (根目录,单文件集成方案)

我们选择将前后端打包进一个镜像的“单体部署”策略。这个 Dockerfile 使用多阶段构建,并解决了我们遇到的所有路径和工具问题。

codeDockerfile

# --- 阶段 1: 构建 Angular 前端 ---
FROM node:20-alpine AS frontend-builder
WORKDIR /app

# (关键修复) 先安装 pnpm 工具
RUN npm install -g pnpm

# 复制依赖描述文件以利用缓存
COPY frontend/package.json frontend/pnpm-lock.yaml ./ 
RUN pnpm install 
# 复制所有剩余源代码
COPY frontend/ ./
RUN pnpm run build

# --- 阶段 2: 构建 Spring Boot 后端 ---
FROM maven:3.8.5-openjdk-8 AS backend-builder
WORKDIR /app
# 缓存 Maven 依赖
COPY backend/pom.xml .
RUN mvn dependency:go-offline
# 复制后端源代码
COPY backend/src ./src

# (关键修复) 从前端构建产物的 browser 子目录中复制内容
COPY --from=frontend-builder /app/dist/*/browser/* ./src/main/resources/static/

# 打包后端应用,此时前端文件已在 static 目录中
RUN mvn package -DskipTests

# --- 阶段 3: 创建最终的运行镜像 ---
FROM eclipse-temurin:8-jre-alpine
WORKDIR /app
# 使用通配符复制 JAR 包
COPY --from=backend-builder /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

3. .github/workflows/main.yml (CI/CD 核心流程)

这份文件只负责协调,将所有构建的脏活累活都交给了 Dockerfile。

codeYaml

name: Build, Push Docker Image, and Deploy

on:
  push:
    branches:
      - main

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Code
        uses: actions/checkout@v4

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Build and Push Docker Image
        uses: docker/build-push-action@v5
        with:
          context: .
          file: ./Dockerfile
          push: true
          tags: |
            ${{ secrets.DOCKERHUB_USERNAME }}/airline-order-manage:latest
            ${{ secrets.DOCKERHUB_USERNAME }}/airline-order-manage:${{ github.sha }}

      - name: Deploy to EC2
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USERNAME }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd ~/airline-cicd
            docker-compose pull
            docker-compose up -d --remove-orphans
            docker image prune -f

4. docker-compose.yml (预先放在 EC2 的 ~/airline-cicd 目录)

这是服务器上的“部署蓝图”,它使用 CI/CD 推送到 Docker Hub 的镜像。

codeYaml

# version: 字段已过时,可以删除
services:
  database:
    image: mysql:8.0
    container_name: airline-mysql-db
    restart: always
    environment:
      # 最佳实践:使用 .env 文件管理这些敏感信息
      MYSQL_ROOT_PASSWORD: YourPassword
      MYSQL_DATABASE: myappdb
      MYSQL_USER: myappuser
      MYSQL_PASSWORD: YourPassword
    volumes:
      - db_data:/var/lib/mysql
    networks:
      - app-network

  app:
    # 核心:使用 CI/CD 构建的镜像
    image: kroulzhang/airline-order-manage:latest
    container_name: my-springboot-angular-app
    restart: always
    ports:
      - "8080:8080"
    depends_on:
      - database
    environment:
      - SPRING_DATASOURCE_URL=jdbc:mysql://database:3306/myappdb?useSSL=false&allowPublicKeyRetrieval=true
      - SPRING_DATASOURCE_USERNAME=myappuser
      - SPRING_DATASOURCE_PASSWORD=YourPassword
    networks:
      - app-network

volumes:
  db_data:
networks:
  app-network:

第三章:战争故事 —— 我们的排错日记

通往自动化的道路从不平坦。以下是我们在这条路上遇到的每一个“坑”以及如何填平它们的真实记录。

  • 问题 #1:npm ci 失败,抱怨缺少 package-lock.json

    • 症状: 前端安装依赖步骤报错。

    • 诊断: 我们发现本地使用 pnpm 开发,生成的是 pnpm-lock.yaml,而 CI 脚本却在调用 npm ci,两者“语言不通”。

    • 解决: 在 CI 脚本和 Dockerfile 中,统一使用 pnpm。引入 pnpm/action-setup 来安装 pnpm,并将所有 npm 命令替换为 pnpm。

  • 问题 #2:pom.xml 或 package.json “未找到”

    • 症状: CI 中的构建步骤报错 No such file or directory。

    • 诊断: 我们的代码在 backend 和 frontend 子目录中,但 CI 脚本默认在根目录运行命令。

    • 解决: 为 mvn 和 pnpm 相关的步骤添加 working-directory 参数,精确定位到各自的子目录。

  • 问题 #3:Docker 构建失败,pnpm: not found

    • 症状: Dockerfile 内部执行 RUN pnpm install 时报错。

    • 诊断: 我们意识到,FROM node:20-alpine 这个基础镜像默认只带了 npm,不包含 pnpm。

    • 解决: 在 Dockerfile 中,pnpm install 之前,先加一行 RUN npm install -g pnpm 来安装 pnpm 工具。

  • 问题 #4:Docker 构建失败,前端产物 not found

    • 症状: COPY --from=frontend-builder ... 步骤报错,找不到源文件。

    • 诊断: 我们 COPY 命令中写的源路径,与 pnpm run build 日志中实际的 Output location 不匹配。

    • 解决: 仔细阅读构建日志,找到 Angular CLI 告知的真实输出路径,并修正 Dockerfile 中的 COPY 命令。最终我们发现,文件在 dist/*/browser 这个更深的目录里。

  • 问题 #5:部署到 EC2 失败,ssh: handshake failed

    • 症状: CI/CD 的最后一步 SSH 连接失败。

    • 诊断: “钥匙”和“门锁”不匹配。最可能的原因是 GitHub Secrets 中的 SSH_PRIVATE_KEY 内容不完整、格式错误,或与 EC2 authorized_keys 中的公钥不配对。

    • 解决: 重新、极其小心地生成一对专用的密钥,将公钥追加到服务器,将私钥的完整内容更新到 GitHub Secrets。

  • 问题 #6:部署后,EC2 上容器名称冲突

    • 症状: docker-compose up 报错 container name is already in use。

    • 诊断: 服务器上存在一个由旧的、非 Compose 方式创建的同名“历史遗留”容器。

    • 解决: 运行 docker-compose down 和/或 docker rm [container_name] 来彻底清理旧容器。如果容器卡在“移除中”的僵尸状态,使用 sudo systemctl restart docker 来重启 Docker 服务。

  • 问题 #7:部署成功,但访问是 Whitelabel Error Page (404)

    • 症状: CI/CD 流水线显示成功,但访问 http://...:8080/ 却看到 Spring Boot 的 404 页面。

    • 诊断: 这是整个过程中最微妙的问题。通过在本地解剖 app.jar 文件,我们最终确认:前端文件被正确打包了,但被放在了 static/browser/ 这个子目录里。Spring Boot 默认只在 static/ 的根目录寻找 index.html,因此找不到。

    • 解决: 再次修正 Dockerfile 中的 COPY 命令,确保它复制的是 browser 文件夹内部的内容,而不是 browser 文件夹本身,到 static 目录中。最终的命令是 COPY --from=frontend-builder /app/dist/*/browser/* ./src/main/resources/static/。

结语

我们的 CI/CD 之旅,就像一次真实的软件开发过程的缩影:从一个清晰的目标开始,在实践中不断遇到预期之外的问题,然后通过观察日志、隔离变量、理解工具原理,一步步逼近真相,最终构建出一个健壮、可靠的系统。

Comments (0)

Continue Reading

Angular 官方全家桶深度解析:路由、表单与 HTTP 的协同艺术

Published Aug 4, 2025

React vs. Next.js:从引擎到赛车的进化

Published Aug 4, 2025

Angular:企业级应用的全能平台与架构师

Published Aug 4, 2025

从 npm 到 yarn 再到 pnpm:前端包管理器的“内卷”与进化

Published Aug 7, 2025

告别“魔法”——拥抱 Zoneless 模式的机遇与陷阱

Published Aug 7, 2025

前端架构的演进:从混沌到秩序,Monorepo 为何成为现代 Web 开发的新宠?

Published Aug 7, 2025