一、我们的目标
Git Push → GitHub Actions
├── 1. 代码检查(Checkstyle / SpotBugs)
├── 2. 单元测试(JUnit 5 + JaCoCo)
├── 3. 构建(Maven + Docker)
├── 4. 镜像安全扫描(Trivy)
└── 5. 部署到阿里云 ECS(SSH + Docker Compose)
二、准备工作
阿里云 ECS 环境
| 项目 | 配置 |
|---|---|
| 操作系统 | Alibaba Cloud Linux 3 |
| CPU | 4 核 |
| 内存 | 8 GB |
| Docker | 24.0+ |
| Docker Compose | v2 |
GitHub Secrets 配置
在仓库 Settings → Secrets and variables → Actions 中添加:
| Secret Key | 值 |
|---|---|
ALIYUN_HOST |
ECS 公网 IP |
ALIYUN_USERNAME |
SSH 用户名 |
ALIYUN_SSH_KEY |
SSH 私钥 |
ALIYUN_PORT |
SSH 端口(默认 22) |
DOCKER_REGISTRY |
阿里云容器镜像服务地址 |
三、完整 Pipeline
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline
on:
push:
branches: [ main, develop ]
paths-ignore:
- '**.md'
- 'docs/**'
pull_request:
branches: [ main ]
env:
ALIYUN_REGISTRY: ${{ secrets.DOCKER_REGISTRY }}
APP_NAME: myapp
JAVA_VERSION: '21'
jobs:
# ========== 代码检查 ==========
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: 'temurin'
cache: maven
- name: Checkstyle
run: mvn checkstyle:check -q
- name: SpotBugs
run: mvn spotbugs:check -q
# ========== 测试 ==========
test:
needs: lint
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: test123
MYSQL_DATABASE: testdb
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: 'temurin'
cache: maven
- name: Unit Tests
run: mvn test -B
- name: JaCoCo Report
if: always()
run: |
echo "## Test Coverage" >> $GITHUB_STEP_SUMMARY
cat target/site/jacoco/index.html 2>/dev/null | grep -oP 'Total.*?d+%' | head -1 >> $GITHUB_STEP_SUMMARY || true
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: target/surefire-reports/
# ========== 构建 & 扫描 ==========
build:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
outputs:
image_tag: ${{ steps.meta.outputs.tags }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: 'temurin'
cache: maven
- name: Maven Build
run: mvn -B clean package -DskipTests
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.ALIYUN_REGISTRY }}/${{ env.APP_NAME }}
tags: |
type=sha,prefix=
type=raw,value=latest
- name: Login to Aliyun Registry
uses: docker/login-action@v3
with:
registry: ${{ env.ALIYUN_REGISTRY }}
username: ${{ secrets.ALIYUN_REGISTRY_USER }}
password: ${{ secrets.ALIYUN_REGISTRY_PASSWORD }}
- name: Build and Push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Trivy Security Scan
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.ALIYUN_REGISTRY }}/${{ env.APP_NAME }}:${{ github.sha }}
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
exit-code: '1'
# ========== 部署到阿里云 ==========
deploy:
needs: build
runs-on: ubuntu-latest
environment: production
steps:
- name: Deploy to Aliyun ECS
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.ALIYUN_HOST }}
username: ${{ secrets.ALIYUN_USERNAME }}
key: ${{ secrets.ALIYUN_SSH_KEY }}
port: ${{ secrets.ALIYUN_PORT }}
script: |
set -e
# 登录阿里云容器镜像
docker login -u ${{ secrets.ALIYUN_REGISTRY_USER }} -p ${{ secrets.ALIYUN_REGISTRY_PASSWORD }} ${{ secrets.DOCKER_REGISTRY }}
cd /opt/myapp
# 拉取最新镜像
docker compose pull
# 滚动更新(先启动新容器再停止旧容器)
docker compose up -d --remove-orphans
# 等待健康检查通过
echo "⏳ 等待服务启动..."
for i in $(seq 1 30); do
if curl -sf http://localhost:8080/actuator/health > /dev/null; then
echo "✅ 服务已就绪"
break
fi
sleep 2
done
# 清理旧镜像
docker image prune -af --filter "until=24h"
echo "? 部署完成!版本:${{ github.sha }}"
# ========== 失败通知 ==========
notify-failure:
needs: [lint, test, build, deploy]
if: failure()
runs-on: ubuntu-latest
steps:
- name: Send Alert
uses: dawidd6/action-send-mail@v3
with:
server_address: smtp.feishu.cn
server_port: 587
username: ${{ secrets.MAIL_USERNAME }}
password: ${{ secrets.MAIL_PASSWORD }}
subject: "? CI/CD 失败 - ${{ github.repository }}"
to: team@example.com
body: |
仓库:${{ github.repository }}
分支:${{ github.ref_name }}
提交:${{ github.sha }}
链接:${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
四、Docker Compose 生产配置
# /opt/myapp/docker-compose.yml
version: '3.8'
services:
app:
image: ${ALIYUN_REGISTRY}/myapp:${TAG:-latest}
container_name: myapp
restart: unless-stopped
ports:
- "8080:8080"
environment:
SPRING_PROFILES_ACTIVE: prod
JAVA_OPTS: >
-XX:+UseG1GC
-XX:MaxRAMPercentage=75.0
-XX:+UseContainerSupport
volumes:
- /var/log/myapp:/var/log/app
networks:
- app_net
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 60s
deploy:
resources:
limits:
cpus: '2'
memory: 2G
# Filebeat 日志采集
filebeat:
image: elastic/filebeat:8.12.0
volumes:
- /var/log/myapp:/var/log/app:ro
- ./filebeat.yml:/usr/share/filebeat/filebeat.yml:ro
networks:
- app_net
networks:
app_net:
driver: bridge
五、部署策略优化
蓝绿部署
# docker-compose.blue.yml
services:
app-blue:
image: ${ALIYUN_REGISTRY}/myapp:${TAG}
ports:
- "8081:8080"
# deploy.sh
#!/bin/bash
TAG=$1
ACTIVE=$(docker compose ps --format json | grep -o '"Name":"myapp-[^"]*"' | head -1)
if [[ "$ACTIVE" == *"blue"* ]]; then
# 当前运行 blue,部署 green
docker compose -f docker-compose.green.yml up -d
sleep 10
# 切换 Nginx 流量到 green
sed -i 's/8081/8082/' /etc/nginx/conf.d/myapp.conf && nginx -s reload
# 停掉 blue
docker compose -f docker-compose.blue.yml down
else
# 部署 blue
docker compose -f docker-compose.blue.yml up -d
sleep 10
sed -i 's/8082/8081/' /etc/nginx/conf.d/myapp.conf && nginx -s reload
docker compose -f docker-compose.green.yml down
fi
回滚
# rollback.sh
#!/bin/bash
PREV_TAG=$(docker images --format '{{.Tag}}' myapp | grep -v latest | head -1)
echo "回滚到版本: $PREV_TAG"
cd /opt/myapp
TAG=$PREV_TAG docker compose up -d
手动回滚太慢?在 GitHub Actions 中加一个 workflow_dispatch 触发的手动回滚 Job。
六、安全扫描集成
Trivy 会在构建阶段扫描镜像,发现 CRITICAL / HIGH 级别漏洞则阻断部署。日常维护:
# 定期手动扫描
trivy image --severity HIGH,CRITICAL registry.cn-hangzhou.aliyuncs.com/my-namespace/myapp:latest
# 忽略已评估为"不受影响"的漏洞
trivy image --ignorefile .trivyignore registry.cn-hangzhou.aliyuncs.com/my-namespace/myapp:latest
七、总结
| 阶段 | 工具 | 目的 |
|---|---|---|
| Lint | Checkstyle + SpotBugs | 代码质量 |
| Test | JUnit 5 + JaCoCo + Testcontainers | 功能正确性 |
| Build | Maven + Docker BuildKit | 标准化产出 |
| Scan | Trivy | 安全漏洞检测 |
| Deploy | SSH Action + Docker Compose | 自动部署 |
| Monitor | Filebeat → ELK / Grafana | 可观测性 |
CI/CD 不是一次性搭建完就完了,而是随着项目演进持续迭代。每遇到一次线上事故,都应该思考"这个能在 Pipeline 中提前发现吗?"然后加上对应的检查。
