300 lines
11 KiB
Markdown
300 lines
11 KiB
Markdown
---
|
||
tags: [cicd, github-actions, docker, kubernetes, pipeline]
|
||
create time: 2026-05-17 10:00
|
||
---
|
||
|
||
# CI/CD 基础与实践
|
||
|
||
## 概述
|
||
|
||
本文档带你从零搭建微服务的自动化发布流水线。内容覆盖:从代码提交到镜像构建、安全扫描、Staging 验证、灰度上线的完整流程,以 GitHub Actions 为例进行实战讲解。更多决策分析(如工具选型)参见 [[../03-CICD与GitOps]]。
|
||
|
||
## 为什么需要 CI/CD?
|
||
|
||
> [!question] 想象一下
|
||
>
|
||
> 你有 50 个微服务,每次发布都需要手动 SSH 到服务器、停旧启新、检查日志——如果出错了还得回滚。**人工操作**在这个规模下就是最大的风险源。
|
||
>
|
||
> CI/CD 的核心目标:**消除发布日的手工操作**,让任何一次 commit 都可以被安全地部署。
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
CODE["代码提交"] --> TEST["测试 & 静态分析"]
|
||
TEST --> SCAN["安全扫描"]
|
||
SCAN --> BUILD["构建镜像"]
|
||
BUILD --> PUSH["推送仓库"]
|
||
PUSH --> STAGING["Staging 验证"]
|
||
STAGING -->|"人工审批"| PROD["Production 部署"]
|
||
|
||
style CODE fill:#e3f2fd
|
||
style TEST fill:#fff3e0
|
||
style SCAN fill:#fce4ec
|
||
style BUILD fill:#e8f5e9
|
||
style PUSH fill:#f3e5f5
|
||
style STAGING fill:#e0f7fa
|
||
style PROD fill:#c8e6c9
|
||
```
|
||
|
||
## 核心设计原则
|
||
|
||
### 一次构建,多处部署
|
||
|
||
镜像不随环境重新编译,只改 K8s ConfigMap 或环境变量。Staging 和 Production 用的是**同一个镜像**。
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
DEV["开发机"] --> BUILD["构建一次"]
|
||
BUILD --> REGISTRY["Docker Registry"]
|
||
REGISTRY --> STAGING["Staging 环境"]
|
||
REGISTRY --> PROD["Production 环境"]
|
||
|
||
STAGING -. "同一镜像" .- PROD
|
||
|
||
style DEV fill:#e3f2fd
|
||
style BUILD fill:#e8f5e9
|
||
style REGISTRY fill:#f3e5f5
|
||
style STAGING fill:#fff3e0
|
||
style PROD fill:#c8e6c9
|
||
```
|
||
|
||
### 语义化版本 + Commit SHA 双标签
|
||
|
||
| Tag 类型 | 示例 | 用途 |
|
||
|----------|------|------|
|
||
| **Commit SHA** | `sha256:a1b2c3d` | 机器精确引用,保证可回滚 |
|
||
| **运行序号** | `v123` | 人类快速参考,方便手动回滚 |
|
||
|
||
> [!tip] 永远不要用 `latest` 做生产环境的镜像标签。`latest` 可被覆盖,回滚时你不知道回滚到哪一刻的镜像。
|
||
|
||
### 路径过滤
|
||
|
||
只对相关服务的代码变更触发构建。`order-service` 改了代码,不应该触发 `payment-service` 的 pipeline。
|
||
|
||
### 流水线即代码
|
||
|
||
Pipeline 配置写在 Git 中(`.github/workflows/`),和源码一起 Review。没人知道它长什么样,就是最好的理由。
|
||
|
||
## Pipeline 实战:单服务完整流水线
|
||
|
||
以下是一个微服务(以 `order-service` 为例)的 CI/CD 流水线。核心思路:**代码推送到 main 分支 → 测试 → 构建镜像 → 推到 Staging → 人工审批 → 灰度上线**。
|
||
|
||
```yaml
|
||
# .github/workflows/deploy.yml
|
||
name: Deploy order-service
|
||
|
||
on:
|
||
push:
|
||
branches: [main]
|
||
paths:
|
||
- "services/order/**" # 只在 order 服务有变更时触发
|
||
|
||
env:
|
||
REGISTRY: registry.example.com
|
||
IMAGE: order-service
|
||
|
||
jobs:
|
||
# ========== Stage 1: Build & Test ==========
|
||
build-and-test:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- name: Run tests
|
||
run: make test
|
||
|
||
- name: Security scan (filesystem)
|
||
uses: aquasecurity/trivy-action@master
|
||
with:
|
||
scan-type: 'fs'
|
||
severity: 'CRITICAL,HIGH'
|
||
|
||
- name: Build Docker image
|
||
run: |
|
||
docker build \
|
||
-t ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ github.sha }} \
|
||
-t ${{ env.REGISTRY }}/${{ env.IMAGE }}:v${{ github.run_number }} \
|
||
-f services/order/Dockerfile \
|
||
services/order
|
||
|
||
- name: Push to registry
|
||
run: |
|
||
echo "${{ secrets.REGISTRY_PASSWORD }}" | \
|
||
docker login -u ${{ secrets.REGISTRY_USER }} --password-stdin
|
||
docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ github.sha }}
|
||
docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:v${{ github.run_number }}
|
||
|
||
# ========== Stage 2: Deploy to Staging ==========
|
||
deploy-staging:
|
||
needs: build-and-test
|
||
runs-on: ubuntu-latest
|
||
environment: staging
|
||
steps:
|
||
- name: Deploy to staging
|
||
run: |
|
||
kubectl set image deployment/order-service \
|
||
order=${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ github.sha }} \
|
||
--namespace=staging
|
||
kubectl rollout status deployment/order-service \
|
||
--namespace=staging --timeout=120s
|
||
|
||
- name: Smoke test
|
||
run: |
|
||
# 等待 Pod Ready 后发送探测请求
|
||
sleep 10
|
||
ENDPOINT=$(kubectl get ingress order-service \
|
||
-o jsonpath='{.spec.rules[0].host}' -n staging)
|
||
curl -f http://$ENDPOINT/health || exit 1
|
||
|
||
# ========== Stage 3: Deploy to Production ==========
|
||
deploy-production:
|
||
needs: deploy-staging
|
||
runs-on: ubuntu-latest
|
||
environment: production # 需人工 Approval Gate
|
||
steps:
|
||
- name: Canary release (10% → 50% → 100%)
|
||
run: |
|
||
kubectl patch canary order-service --type merge \
|
||
-p '{"spec":{"weight":10}}'
|
||
# 等待 10 分钟,检查 error rate / latency
|
||
echo "Monitor metrics before proceeding..."
|
||
|
||
- name: Promote to 50%
|
||
if: success()
|
||
run: |
|
||
kubectl patch canary order-service --type merge \
|
||
-p '{"spec":{"weight":50}}'
|
||
|
||
- name: Full promotion
|
||
if: success()
|
||
run: |
|
||
kubectl patch canary order-service --type merge \
|
||
-p '{"spec":{"weight":100}}'
|
||
```
|
||
|
||
> [!info] Secret 管理原则
|
||
>
|
||
> 所有敏感信息通过 **GitHub Actions Secrets** 存储,按环境分离:
|
||
>
|
||
> | Secret 名称 | 用途 | 存放位置 |
|
||
> |------------|------|---------|
|
||
> | `REGISTRY_USER` / `REGISTRY_PASSWORD` | 镜像仓库认证 | Repository Settings → Secrets |
|
||
> | `KUBECONFIG_STAGING` | K8s 集群配置 | Environment: staging |
|
||
> | `KUBECONFIG_PRODUCTION` | K8s 集群配置 | Environment: production |
|
||
>
|
||
> > [!tip] 不要在 YAML 中明文写密码,哪怕加了 `${{ secrets.XXX }}` 的 workflow 在 fork 的 PR 中仍然有泄露风险。对 fork PR 使用 `environment` 保护可以阻止 secrets 暴露。
|
||
|
||
## Pipeline 逐阶段解读
|
||
|
||
| 阶段 | 做什么 | 为什么重要 |
|
||
|------|--------|-----------|
|
||
| **路径过滤** | `paths` 指定只对特定目录变更触发 | 避免每次 PR 都重建所有服务的镜像 |
|
||
| **Secrets 管理** | GitHub Secrets 按环境分离存储 | 防止密钥泄露到 fork PR 或日志中 |
|
||
| **安全扫描** | Trivy 在构建前后扫描漏洞 | 把安全问题挡在镜像层 |
|
||
| **双 Tag 策略** | 同时打 Commit SHA + run number | SHA 用于精确操作,run number 用于快速参考 |
|
||
| **Environment 保护** | `environment: production` 配置审批人 | GitHub 级别的 gates |
|
||
| **冒烟测试** | 部署后发送健康探测请求 | 确认 Pod 真正 Ready,而非只等 rollout 完成 |
|
||
| **Canary 发布** | 10% → 50% → 100% 逐步放量 | 有 Bug 只影响 10% 用户 |
|
||
|
||
## 回滚与 Rollout
|
||
|
||
> [!question] 上线后发现 Bug,怎么最快恢复?
|
||
>
|
||
> 答案不是「快速修好再发一次」——而是**先回滚,后修复**。MTTR(平均恢复时间)比根因分析优先级更高。
|
||
|
||
### K8s Rollout 模式对比
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
RU["RollingUpdate\n滚动更新"] --> P1["逐步替换旧 Pod"]
|
||
RU --> P2["期间服务不中断"]
|
||
RU --> P3["支持 ProgressDeadline"]
|
||
|
||
RC["Recreate\n全部重建"] --> R1["先杀所有旧 Pod"]
|
||
RC --> R2["再启新 Pod"]
|
||
RC --> R3["有短暂不可用窗口"]
|
||
|
||
style RU fill:#e8f5e9
|
||
style RC fill:#fff3e0
|
||
```
|
||
|
||
| 特性 | RollingUpdate | Recreate |
|
||
|------|--------------|----------|
|
||
| **可用性** | 零停机 | 短暂中断 |
|
||
| **资源峰值** | Old + New 并存 | 只有一个版本运行 |
|
||
| **适用场景** | 绝大多数微服务 | 独占存储卷、单写数据库 |
|
||
| **回滚速度** | `kubectl rollout undo` 秒级恢复 | 重新 deploy 上一个版本 |
|
||
|
||
### 回滚命令速查
|
||
|
||
```bash
|
||
# 查看当前 rollout 状态
|
||
kubectl rollout status deployment/order-service -n production
|
||
|
||
# 回滚到上一个版本
|
||
kubectl rollout undo deployment/order-service -n production
|
||
|
||
# 查看历史版本列表
|
||
kubectl rollout history deployment/order-service -n production
|
||
|
||
# 回滚到指定 revision
|
||
kubectl rollout undo deployment/order-service -n production --to-revision=3
|
||
|
||
# 暂停/恢复 rollout(适合批量变更)
|
||
kubectl rollout pause deployment/order-service -n production
|
||
# ... 应用多个变更 ...
|
||
kubectl rollout resume deployment/order-service -n production
|
||
```
|
||
|
||
> [!tip] 如果使用了 Canary / Istio 等流量治理工具,回滚步骤变为:
|
||
> 1. **先把流量切回稳定版本**(改 weight = 0)— 秒级止血
|
||
> 2. **再删除问题部署** — 释放资源
|
||
> 3. **最后排查和修复**
|
||
>
|
||
> 流量切换比 Pod 重建更快,这是灰度发布最大的安全优势。
|
||
|
||
## 生产级增强
|
||
|
||
上面的示例做了精简,真实环境中通常还会加入:
|
||
|
||
| 增强项 | 说明 | 工具 / 做法 |
|
||
|--------|------|------------|
|
||
| **Dependency Lock** | `go mod tidy` 后提交 `go.sum`,确保每次依赖一致 | `go.sum` / `package-lock.json` |
|
||
| **Trivy Image Scan** | 镜像推送后再扫描一次镜像内漏洞 | `trivy image <image>` |
|
||
| **OpenTelemetry Trace ID** | 注入 trace ID,追踪本次构建产生的请求 | 环境变量注入 `OTEL_SERVICE_VERSION` |
|
||
| **Slack 通知** | 每阶段完成后通过 Webhook 通知团队 | GitHub Actions `slack/notification` |
|
||
| **代码覆盖率门控** | 低于阈值直接失败(coverage < 80% = fail) | `gocover` / `codecov` |
|
||
| **SBOM 生成** | 用 `syft` 生成软件物料清单,满足合规要求 | `syft dir:. -o cyclonedx-json > sbom.json` |
|
||
| **Docker BuildKit** | 利用缓存层加速重建 | `DOCKER_BUILDKIT=1 docker build` |
|
||
| **并发测试** | 单元测试和 lint 并行执行缩短流水线时间 | GitHub Actions `strategy.matrix` |
|
||
|
||
```bash
|
||
# Docker BuildKit 加速构建
|
||
DOCKER_BUILDKIT=1 docker build \
|
||
--cache-from ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ github.sha }} \
|
||
-t ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ github.sha }} \
|
||
services/order
|
||
|
||
# SBOM 生成
|
||
syft dir:. -o cyclonedx-json > sbom.json
|
||
```
|
||
|
||
## 常见陷阱
|
||
|
||
> [!warning] 这些坑踩过一次就记住了
|
||
|
||
1. **不要在 pipeline 里用 `latest` tag** — 不同环境编译行为不一致
|
||
2. **不要让 Staging 和 Production 各自 `docker build`** — 这是最常见的一致性 bug 来源
|
||
3. **不要跳过 Staging 直连 Production** — 哪怕你的测试写得再好,也需要一个集成验证环节
|
||
4. **不要硬编码 Secret 到 YAML 或代码里** — 参考 [[04-安全与发布策略]]
|
||
5. **不要忽略 `kubectl rollout status`** — 不检查 rollout 状态等于盲发
|
||
6. **不要把日志级别开到 DEBUG 上生产** — 不仅浪费存储,还会暴露敏感数据
|
||
7. **不要用 cron 做主触发器替代 push** — 定时构建无法响应具体 commit,丢失审计链
|
||
8. **不要忘记配置 `ProgressDeadlineSeconds`** — 一个卡在 Pending 状态的 rollout 不会被自动中止
|
||
|
||
## 关联笔记
|
||
|
||
- [[02-GitOps与ArgoCD]] — GitOps 模式下的部署工作流
|
||
- [[03-Helm模板管理]] — Helm Chart 编写与管理
|
||
- [[04-安全与发布策略]] — 安全管理和发布策略决策
|
||
- [[../03-CICD与GitOps]] — 参考手册与决策指南
|