一、我们的目标
``
Git Push → GitHub Actions
├── 1. 代码检查(Checkstyle / SpotBugs)
├── 2. 单元测试(JUnit 5 + JaCoCo)
├── 3. 构建(Maven + Docker)
├── 4. 镜像安全扫描(Trivy)
└── 5. 部署到阿里云 ECS(SSH + Docker Compose)
`
二、准备工作
阿里云 ECS 环境
GitHub Secrets 配置
在仓库 Settings → Secrets and variables → Actions 中添加:
| ECS 公网 IP | SSH 用户名 | SSH 私钥 | SSH 端口(默认 22) | 阿里云容器镜像服务地址 三、完整 Pipeline
`yaml
.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 生产配置
`yaml
/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
`
五、部署策略优化
蓝绿部署
`yaml
`docker-compose.blue.yml
services:
app-blue:
image: ${ALIYUN_REGISTRY}/myapp:${TAG}
ports:
- "8081:8080"
`bash
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
`
回滚
`bash
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 级别漏洞则阻断部署。日常维护:
`bash
定期手动扫描
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 ``七、总结
CI/CD 不是一次性搭建完就完了,而是随着项目演进持续迭代。每遇到一次线上事故,都应该思考"这个能在 Pipeline 中提前发现吗?"然后加上对应的检查。