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

471 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [security, secrets-management, canary, rollback, deployment-strategy, feature-flag, database-migration]
create time: 2026-05-18 14:30
---
# 安全与发布策略
## 概述
本文档聚焦生产级部署中的两大决策点:**敏感信息管理**和**发布策略选择**。提供方案对比、实操检查清单以及零停机发布的数据库迁移策略。架构概览参见 [[../03-CICD与GitOps]]。
## Secret 管理:分层决策树
```mermaid
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** | 云厂商深度绑定 | 集成度高,审计完善 | 锁定特定云平台 |
### ❌ 绝对不要做的事
```yaml
# 错误示范:明文写密码
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
DB_PASSWORD: "my-secret-password" # 绝对不要这样做!
```
### ✅ 正确做法
```yaml
# 使用 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:最小权限原则
```yaml
# 限制只有特定 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:
```yaml
# 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 的读写操作:
```yaml
# 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 就会崩溃。遵循以下原则:
### 向前兼容原则
```mermaid
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 — 加列 + 双写:**
> ```sql
> ALTER TABLE orders ADD COLUMN status_v2 VARCHAR(20) NULL;
> ```
> ```go
> // 新代码:写入两个字段
> db.Exec("UPDATE orders SET status=?, status_v2=? WHERE id=?", oldStatus, newV2, orderId)
> ```
>
> **Phase 2 — 回填存量:**
> ```go
> 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 — 切流 + 清理:**
> ```go
> // 新代码:只读写 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,怎么办?
> 最快的回滚不是「修代码再发布」,而是「切回上一个版本」。
### 回滚时机判断
```mermaid
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 的回滚
```bash
# 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 切回蓝环境即可:
```yaml
# 当前绿环境在服务,切回蓝环境:
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 一键回滚
```yaml
# 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 —— **代码提交 ≠ 功能上线**。
### 典型使用模式
```go
// 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`:
```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
```mermaid
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 堆积和重复测试。
## 发布前检查清单
```mermaid
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 |
## 渐进式采纳路径
如果你的团队还在手工部署,不必一步到位。建议分三步走:
```mermaid
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 结合**:
```mermaid
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 管"怎么维持"**。两者合在一起,才构成了完整的现代软件交付链。
## 关联笔记
- [[01-CICD基础与实践]] — CI/CD Pipeline 的搭建与实践
- [[02-GitOps与ArgoCD]] — GitOps 工作流与 ArgoCD
- [[03-Helm模板管理]] — Helm Chart 编写与管理
- [[../03-CICD与GitOps]] — 参考手册与决策指南总入口