Files
cs-note/hhs/MS/05-部署运维/03-CICD与GitOps/04-安全与发布策略.md
T
2026-05-24 11:42:38 +08:00

16 KiB
Raw Blame History

tags, create time
tags create time
security
secrets-management
canary
rollback
deployment-strategy
feature-flag
database-migration
2026-05-18 14:30

安全与发布策略

概述

本文档聚焦生产级部署中的两大决策点:敏感信息管理和发布策略选择。提供方案对比、实操检查清单以及零停机发布的数据库迁移策略。架构概览参见 ../03-CICD与GitOps。

Secret 管理:分层决策树

flowchart TD
    START["开始选型"] --> SIZE["团队规模?"]
    SIZE -- "< 20人" --> K8SSECRET["K8s 原生 Secret\n✅ 开箱即用"]
    SIZE -- "≥ 20人" --> CLOUD["是否深度绑定云厂商?"]

    CLOUD -- "是 (AWS/GCP/Azure)" --> SECRETMGR["云厂商 Secret Manager\n✅ 集成度高,审计完善"]
    CLOUD -- "否 / 多云" --> ESOOROPS["需要 GitOps?"]

    ESOOROPS -- "是 (ArgoCD)" --> SOPS["SOPS + sealed-secrets\n✅ 加密文件可提交到 Git"]
    ESOOROPS -- "否 / 已有 Vault" --> ESO["External Secrets Operator\n✅ Git 中不存任何密文"]

    style K8SSECRET fill:#e8f5e9
    style SECRETMGR fill:#e3f2fd
    style SOPS fill:#fff3e0
    style ESO fill:#f3e5f5

方案速查表

方案 适用场景 优点 缺点
K8s 原生 Secret 小规模、内部团队 零额外成本,开箱即用 明文存储在 etcd,RBAC 控制有限
External Secrets Operator (ESO) 已有 Vault / AWS SSM Git 中不存任何密文 需维护额外组件
SOPS + sealed-secrets ArgoCD 用户 加密文件可提交到 Git 需管理公钥基础设施
AWS Secrets Manager / GCP Secret Manager 云厂商深度绑定 集成度高,审计完善 锁定特定云平台

❌ 绝对不要做的事

# 错误示范:明文写密码
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  DB_PASSWORD: "my-secret-password"   # 绝对不要这样做!

✅ 正确做法

# 使用 Secret(stringData 接收明文,API Server 自动 Base64)
apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
  namespace: production
type: Opaque
stringData:
  DB_PASSWORD: "${DB_PASSWORD}"       # 通过 CI secrets 注入
  API_KEY: "${API_KEY}"
---
# 在 Deployment 中引用
apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
        - name: order-service
          envFrom:
            - secretRef:
                name: app-secrets

[!warning] 为什么不能把 Secret 明文放进 Git?

即使使用 GitOps(ArgoCD),也不建议将明文密码提交到仓库。一旦权限管控疏漏——比如某个离职员工的账号未被撤销——整个公司的数据库密码就暴露了。使用 ESO 或 sealed-secrets,Git commit 历史中永远不会有明文密钥。

Secret 进阶实践

1. RBAC:最小权限原则

# 限制只有特定 ServiceAccount 可以读取特定 Secret
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: order-service-read-secret
  namespace: production
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: secret-reader         # 仅限 Read,禁止 Create/Update/Delete
subjects:
  - kind: ServiceAccount
    name: order-service-sa    # 仅该 SA 有权读取
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: secret-reader
  namespace: production
rules:
  - apiGroups: [""]
    resources: ["secrets"]
    resourceNames: ["app-secrets"]   # 按名称限定,不可通读所有 Secret
    verbs: ["get", "list"]

[!tip] resourceNames 是什么? K8s RBAC 默认是资源级别的(你能读这个 namespace 下所有 Secret)。加上 resourceNames 后变成细粒度控制——只能读指定的几个 Secret 名。这是防止越权访问的关键手段。

2. 开启 etcd 加密

K8s 的 Secret 在 etcd 中以 Base64 编码存储,而非加密。建议在集群层面启用 EncryptionConfiguration:

# encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
    providers:
      - aescbc:                         # 首选 AES-CBC 加密
          keys:
            - secret: $(ENCRYPTION_KEY) # 从环境变量注入,不要硬编码
      - identity:                       # fallback:旧数据用明文

[!note] 为什么要开启 etcd 加密? 如果不加密, anyone 能访问 etcd(比如 K8s admin 角色的人、云厂商控制台的操作员)就能直接读取所有 Secret 明文。启用后,etcd 中实际存储的是密文。⚠️ 加密必须一次性完成——一旦写入数据,后续切换加密 provider 会导致旧数据丢失。

3. 审计日志

确保 kube-apiserver 启用了审计策略,记录所有 Secret 的读写操作:

# audit-policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
  - level: Metadata                  # Secret 只记录元数据,不记录内容
    resources:
      - group: ""                    # core API group
        resources: ["secrets"]
    verbs: ["get", "list", "watch"]
  - level: None
    resources: ["secrets"]
    verbs: ["update", "patch", "delete"]   # 修改类操作也需记录

数据库迁移策略

[!question] 发布新版本时,代码先上还是数据库先改? 答案:两边要同时准备好——但「先扩后缩」是核心原则。

零停机发布的最大陷阱是数据库 schema 变更。如果代码 A 版本写了字段 X,而代码 B 版本还没发布,此时如果删掉字段 X,代码 A 就会崩溃。遵循以下原则:

向前兼容原则

flowchart LR
    subgraph Phase1["Phase 1: 加字段(向后兼容)"]
        P1A[添加新字段(可为 NULL)] --> P1B[部署新代码(双写新旧字段)]
    end

    subgraph Phase2["Phase 2: 填存量数据"]
        P2A[后台任务回填历史数据] --> P2B[验证数据一致性]
    end

    subgraph Phase3["Phase 3: 切换到新字段"]
        P3A[部署代码(仅读新字段)] --> P3B[删除旧字段]
    end

    P1B --> P2A
    P2B --> P3A

    style Phase1 fill:#e8f5e9
    style Phase2 fill:#fff3e0
    style Phase3 fill:#fce4ec

三阶段详解:

阶段 动作 兼容性保证
Phase 1 ALTER TABLE 新增列(NOT NULL 不行!设为可 NULL)+ 新代码同时写入新旧字段 旧代码继续读旧字段,正常运行
Phase 2 用后台 Job 批量填充新字段的存量数据,直到全量一致 旧代码仍然正常,新代码已具备降级能力
Phase 3 部署仅读新字段的代码 → 确认无误后 ALTER TABLE DROP 旧字段 完全切换到新模式

[!example] 具体例子:给订单表增加 status_v2 枚举字段

Phase 1 — 加列 + 双写:

ALTER TABLE orders ADD COLUMN status_v2 VARCHAR(20) NULL;
// 新代码:写入两个字段
db.Exec("UPDATE orders SET status=?, status_v2=? WHERE id=?", oldStatus, newV2, orderId)

Phase 2 — 回填存量:

func migrateLegacyStatus() {
    batch := 1000
    offset := 0
    for {
        rows, _ := db.Query("SELECT id, status FROM orders LIMIT $1 OFFSET $2", batch, offset)
        // ... 转换 status -> status_v2 并 UPDATE ...
        if rows.Count() < batch { break }
        offset += batch
    }
}

Phase 3 — 切流 + 清理:

// 新代码:只读写 status_v2
db.Query("SELECT * FROM orders WHERE status_v2='paid'")
// 确认全量稳定运行一周后执行:
ALTER TABLE orders DROP COLUMN status;

关键注意事项

  • 永远不要在同一个 deploy 中「删字段 + 上线读该字段的代码」 ——这必出事故
  • ALTER TABLE 对大表是重操作:使用 pt-online-schema-change(MySQL)或 gh-ost 避免锁表
  • DML(增删改行)可以用分批 + delay 方式;DDL(改结构)一定要离线窗口或使用在线工具
  • 每次迁移前备份:mysqldump 或快照,回滚有退路

回滚策略与机制

[!question] 发布后发现 Bug,怎么办? 最快的回滚不是「修代码再发布」,而是「切回上一个版本」。

回滚时机判断

flowchart TD
    ALERT["告警触发 / 监控异常"] --> THRESHOLD{"指标超过阈值?"}

    THRESHOLD -- "Error Rate > 目标值" --> AUTO_ROLLBACK["⚡ 自动回滚 Canary"]
    THRESHOLD -- "Latency p99 升高" --> MANUAL["人工评估是否需要回滚"]
    THRESHOLD -- "业务指标下降但不紧急" --> MONITOR["持续观察,暂不回滚"]

    AUTO_ROLLBACK --> LOG["通知 Team + 记录事件"]
    MANUAL -- "决定回滚" --> LOG
    MANUAL -- "决定观察" --> MONITOR

    LOG --> POSTMORTEM["事后复盘 Post-mortem"]

    style AUTO_ROLLBACK fill:#ffebee
    style MANUAL fill:#fff3e0
    style MONITOR fill:#e8f5e9
    style POSTMORTEM fill:#e3f2fd

Rolling Update 的回滚

# kubectl rollout 是最基础的回滚手段
kubectl rollout status deployment/order-service    # 查看当前状态
kubectl rollout undo deployment/order-service      # 回滚到上一版本
kubectl rollout history deployment/order-service   # 查看所有 revision
kubectl rollout undo deployment/order-service --to-revision=3   # 回滚到指定版本

# 也可以直接用之前验证过的镜像重新 apply
kubectl set image deployment/order-service order-service=myregistry/app:v1.2.3@sha256:...

Blue-Green 的回滚

Blue-Green 天然支持秒级回滚——只需把 Service 的 selector 切回蓝环境即可:

# 当前绿环境在服务,切回蓝环境:
apiVersion: v1
kind: Service
metadata:
  name: order-service
spec:
  selector:
    app: order-service
    version: blue   # ← 从 green 改成 blue
  ports:
    - port: 80
      targetPort: 8080

[!tip] 如何做到无缝切换? 配合 外部 DNS(Route53 / CloudFlare)可以跨 Kubernetes Service 实现全局流量切换。Service Level Switching 毫秒级,DNS TTL 通常分钟级。关键是把流量入口层和服务层都做好双写。

ArgoCD 一键回滚

# ArgoCD Application 定义
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: order-service
spec:
  project: default
  source:
    repoURL: https://github.com/team/k8s-manifests.git
    targetRevision: v1.2.3       # ← 改到这个 tag 即回滚
    path: overlays/production
  syncPolicy:
    automated:
      prune: true                 # 自动删除不在 Git 中的资源
      selfHeal: true              # 检测到漂移自动修复

ArgoCD 的回滚就是「改 Git 里的目标版本 + push」——这就是 GitOps 的魅力:回滚也是提交。

特性开关(Feature Flag)

[!question] 有没有一种方法可以让新功能「已经部署到生产」但「对用户还不可见」? 答案是 Feature Flag —— 代码提交 ≠ 功能上线。

典型使用模式

// feature_flags.go
package flags

import "github.com/thomaspoison/go-feature-flag/provider/fileprovider"

func init() {
    ffclient.Init(ffconfig.Config{
        FilePath: "features.yaml",    // 配置文件路径
        PollInterval: 30,             // 每 30 秒刷新
    })
}

// 调用处
if flags.Value("enable-new-checkout", false) {
    // 新功能逻辑
    return NewCheckoutFlow(ctx)
}
return LegacyCheckoutFlow(ctx)  // 兜底:传统流程

对应配置文件 features.yaml:

enable-new-checkout:
  percentage: 0           # 初始为 0%,对所有人关闭
  rollout: gradual        # 渐进式放量
  rules:
    - attribute: env
      operator: equal
      value: production
    invert: false
    values: [50]          # 灰度到 50% 的用户

何时使用 Feature Flag

场景 推荐度 理由
复杂业务逻辑开关 ⭐⭐⭐⭐⭐ 无需重新部署即可上线下线功能
A/B 测试 ⭐⭐⭐⭐⭐ 按用户维度控制实验组/对照组
简单配置项 ⭐⭐ 用 ConfigMap 就够了,Flag 太重
临时 Debug ⭐⭐⭐ 用环境变量更直接

Feature Flag vs Release Branch

flowchart LR
    trunk["主分支 main/master\n🟢 始终可构建"] --> CI["CI 自动化构建"]

    subgraph flag["Feature Flag 方式"]
        F1["新功能开发完成"] --> F2["合并到 main\nFlag = OFF"] --> F3["发布到生产"] --> F4["调试后 Flag = ON"]
    end

    subgraph branch["Release Branch 方式"]
        B1["创建 release/v2.0 分支"] --> B2["隔离新功能"] --> B3["发布时合入 main"]
    end

    CI --> F1
    CI --> B2

    style trunk fill:#e8f5e9
    style flag fill:#f3e5f5
    style branch fill:#fff3e9

核心差异:Feature Flag 保持单一 main 分支,Release Branch 产生长期隔离分支。现代 GitOps 实践更推荐 Flag,因为可以避免 merge conflict 堆积和重复测试。

发布前检查清单

flowchart TD
    PRE["发布前检查"] --> T1{"镜像 Tag 是否精确?"}
    T1 -- ✅ SHA-based --> T2{"Staging 是否已通过?"}
    T1 -- ❌ latest / head --> FAIL1["❌ 拒绝部署"]
    T2 -- ✅ Passed --> T3{"Prod Approval 是否完成?"}
    T2 -- ❌ Failed/Pending --> FAIL2["❌ 暂停等待"]
    T3 -- ✅ Approved --> T4{"Canary 指标是否正常?"}
    T3 -- ❌ Rejected --> FAIL2
    T4 -- ✅ All Green --> SUCCESS["✅ 全量发布"]
    T4 -- ❌ Error Rate 升高 --> ROLLBACK["⚠️ 自动回滚"]

    style FAIL1 fill:#ffebee
    style FAIL2 fill:#fff3e0
    style SUCCESS fill:#e8f5e9
    style ROLLBACK fill:#fce4ec
# 检查项 工具/方法
1 镜像 Tag 使用 Commit SHA GitHub Actions ${{ github.sha }}
2 单元测试覆盖率达标 make test + coverage threshold
3 安全扫描无 HIGH/CRITICAL Trivy / Snyk
4 Staging 环境冒烟测试通过 自动化 health check
5 Prod 审批人工确认 GitHub Environment Protection Rule
6 Canary 阶段 error rate < 0.1% Prometheus + Alertmanager
7 Git manifest 已更新并 merged ArgoCD Application status = Synced

渐进式采纳路径

如果你的团队还在手工部署,不必一步到位。建议分三步走:

flowchart LR
    STEP1[("1. 自动化构建+Push")] --> STEP2[("2. Staging + 审批关卡")] --> STEP3[("3. GitOps, Git 即真相源")]

    style STEP1 fill:#e8f5e9
    style STEP2 fill:#fff3e0
    style STEP3 fill:#e3f2fd
  1. 第一步:用 GitHub Actions/GitLab CI 实现自动化构建 + push — 推荐立即做
  2. 第二步:引入 Staging 环境和审批关卡 — 降低上线风险
  3. 第三步:迁移到 GitOps(ArgoCD) — 让 Git 成为唯一真相源

实际落地时,很多团队的最终架构是 CI/CD + GitOps 结合:

flowchart LR
    Dev["开发者 push 代码"] --> CI["GitHub Actions\n(CI: 测试→构建→Push镜像)"]
    CI -->|"推送新镜像"| IMGREG["Container Registry"]

    Dev2["开发者 PR 修改 K8s manifest"] --> GIT[(Git Manifest Repo)]

    subgraph Cluster["K8s Cluster"]
        Argo["ArgoCD"] -->|"自动同步"| K8s["Pod/Service/ConfigMap"]
    end

    GIT -.->|Watch 变动| Argo

    style CI fill:#e8f5e9
    style Argo fill:#fff3e0
    style K8s fill:#e3f2fd

核心分工:CI/CD 管"怎么建",GitOps 管"怎么维持"。两者合在一起,才构成了完整的现代软件交付链。

关联笔记