471 lines
16 KiB
Markdown
471 lines
16 KiB
Markdown
---
|
||
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]] — 参考手册与决策指南总入口
|