跳到主要内容

4 篇博文 含有标签「生产环境」

生产环境实践

查看所有标签

OpenBao 全景解析:从零到生产的开源密钥管理实战

· 阅读需 36 分钟
Rainy
雨落无声,代码成诗 —— 致力于技术与艺术的极致平衡

"The best secret is one that is never exposed — OpenBao keeps it that way."

在现代云原生架构中,密钥管理(Secrets Management) 是安全基础设施的核心。数据库密码、API Key、TLS 证书、SSH 密钥……这些敏感信息散落在环境变量、配置文件、CI/CD Pipeline 中,成为攻击者的首要目标。

OpenBaoHashiCorp Vault开源分叉(Fork),由 Linux Foundation 旗下的 LF Edge 项目托管,采用 MPL-2.0 许可证,完全开源、社区驱动。它继承了 Vault 强大的密钥管理能力,同时摆脱了商业许可变更的风险,是生产环境中管理密钥的理想选择。

本文将从零开始,由浅入深地带你全面掌握 OpenBao 的核心概念、安装配置、实战操作,直到生产级部署与运维。


一、OpenBao 是什么?

1.1 项目背景与定位

2023 年 8 月,HashiCorp 将 Vault 等核心产品从 MPL-2.0 许可证转为 BSL(Business Source License),限制竞争性使用。作为回应,社区发起了 OpenBao 项目,旨在维护一个完全开源的 Vault 替代品

对比项HashiCorp VaultOpenBao
许可证BSL 1.1(限制竞争使用)MPL-2.0(真正开源)
治理HashiCorp 公司主导Linux Foundation / LF Edge 社区治理
API 兼容性-与 Vault API 高度兼容
CLI 命令vaultbao
当前版本1.x2.5.x
定位企业级密钥管理社区驱动的开源密钥管理

1.2 核心能力概览

OpenBao 提供以下核心能力:

1.3 核心术语速查

在深入之前,先熟悉这些核心术语:

术语说明
Secret Engine存储、生成或加密数据的插件化组件,通过路径挂载(如 secret/database/pki/
Auth Method身份认证方式,验证用户/应用身份后颁发 Token
Token认证成功后获得的凭证,携带策略信息,用于后续 API 调用
PolicyACL 策略,定义 Token 对特定路径的权限(read / create / update / delete / list)
Lease密钥的有效期,到期后自动撤销。可续租(renew)或主动撤销(revoke)
Seal / UnsealOpenBao 的加密保护机制。启动时为 Sealed 状态,需提供 Unseal Key 才能解密数据
Barrier加密屏障,所有写入存储的数据都经过 Barrier 加密
Root Key加密 Keyring 的主密钥,由 Unseal Key 保护
Integrated Storage内置的 Raft 共识存储后端,无需外部依赖

二、架构深度解析

2.1 整体架构

OpenBao 的架构可以分为以下几个层次:

2.2 加密屏障(Barrier)工作原理

OpenBao 的安全核心是多层加密架构。理解这个加密链非常重要:

关键设计
  1. Unseal Key 不存储在任何地方 — 它由操作员持有(Shamir 分片分散在多人手中),或由外部 KMS/HSM 托管。
  2. Root Key 加密存储 — 即使攻击者获取了存储后端的全部数据,没有 Unseal Key 也无法解密。
  3. Encryption Key 支持轮换 — 可以在不停机的情况下定期轮换加密密钥,旧密钥保留在 Keyring 中用于解密历史数据。

2.3 请求处理流程

当客户端发起一个读取密钥的请求时,OpenBao 内部的处理流程如下:


三、安装与快速上手

3.1 安装方式

OpenBao 提供多种安装方式,以下按推荐顺序列出:

macOS(Homebrew)

# 安装
brew install openbao

# 验证
bao version
# OpenBao v2.5.x ...

Linux(包管理器)

# Arch Linux
pacman -Sy openbao

# Fedora / RHEL(需先启用 EPEL)
dnf install -y epel-release
dnf install -y openbao

# Debian / Ubuntu(使用官方仓库)
# 从 https://openbao.org/downloads/ 下载 .deb 包
wget https://github.com/openbao/openbao/releases/download/v2.5.0/bao_2.5.0_linux_amd64.deb
sudo dpkg -i bao_2.5.0_linux_amd64.deb

Docker 容器

# 支持三个镜像仓库
docker pull ghcr.io/openbao/openbao:latest
docker pull quay.io/openbao/openbao:latest
docker pull docker.io/openbao/openbao:latest

# 快速启动 Dev 模式
docker run --rm -p 8200:8200 \
--memory-swappiness=0 \
--name openbao-dev \
ghcr.io/openbao/openbao:latest server -dev
安全加固提醒

安装后建议进行以下加固措施:

  • 禁用 Swap:防止密钥数据被写入持久化交换空间。Linux 上 systemd 服务文件默认设置 MemorySwapMax=0
  • Docker 启动时:添加 --memory-swappiness=0 标志。
  • macOS:默认 Swap 已加密,无需额外处理。

从源码编译

# 需要 Go 环境
mkdir -p $GOPATH/src/github.com/openbao && cd $_
git clone https://github.com/openbao/openbao.git
cd openbao
make bootstrap # 下载依赖
make dev # 编译 bao 二进制到 ./bin/

3.2 Dev 模式快速体验

Dev 模式是学习和测试的最佳起点。它以内存存储、自动 Unseal、预配置 Root Token 的方式启动,绝不能用于生产环境

# 启动 Dev Server
bao server -dev

# 输出示例:
# ==> OpenBao server configuration:
# Api Address: http://127.0.0.1:8200
# Cgo: disabled
# Cluster Address: https://127.0.0.1:8201
# Listener 1: tcp (addr: "127.0.0.1:8200", ...)
# Log Level: info
# Mlock: supported: false, enabled: false
# Recovery Mode: false
# Storage: inmem
# Version: OpenBao v2.5.x
#
# WARNING! dev mode is enabled! In this mode, OpenBao runs entirely
# in-memory. Data is lost on restart!
#
# Root Token: hvs.xxxxxxxxxxxxxxxxxxxxxxxx

另一个终端中配置环境变量并开始操作:

# 设置环境变量
export BAO_ADDR='http://127.0.0.1:8200'
export BAO_TOKEN='hvs.xxxxxxxxxxxxxxxxxxxxxxxx' # 使用上面输出的 Root Token

# 验证服务器状态
bao status

3.3 第一个 Secret:KV 引擎实战

KV(Key-Value)Secret Engine 是最基础的密钥存储引擎。Dev 模式下默认启用了 KV v2 引擎,挂载在 secret/ 路径。

# ==========================================
# 写入密钥
# ==========================================
bao kv put secret/myapp/config \
db_host="postgres.prod.internal" \
db_port="5432" \
db_user="app_user" \
db_password="S3cur3P@ssw0rd!"

# 输出:
# ======= Secret Path =======
# secret/data/myapp/config
#
# ======= Metadata =======
# Key Value
# --- -----
# created_time 2026-04-09T11:00:00.000000Z
# custom_metadata <nil>
# deletion_time n/a
# destroyed false
# version 1

# ==========================================
# 读取密钥
# ==========================================
bao kv get secret/myapp/config

# 输出:
# ======= Secret Path =======
# secret/data/myapp/config
#
# ======= Metadata =======
# Key Value
# --- -----
# created_time 2026-04-09T11:00:00.000000Z
# version 1
#
# ====== Data ======
# Key Value
# --- -----
# db_host postgres.prod.internal
# db_port 5432
# db_user app_user
# db_password S3cur3P@ssw0rd!

# ==========================================
# 读取特定字段
# ==========================================
bao kv get -field=db_password secret/myapp/config
# S3cur3P@ssw0rd!

# ==========================================
# JSON 格式输出(用于脚本/程序)
# ==========================================
bao kv get -format=json secret/myapp/config | jq '.data.data'

# ==========================================
# 更新密钥(创建新版本)
# ==========================================
bao kv put secret/myapp/config \
db_host="postgres.prod.internal" \
db_port="5432" \
db_user="app_user" \
db_password="N3wS3cur3P@ss!"
# 现在 version = 2

# ==========================================
# 读取历史版本
# ==========================================
bao kv get -version=1 secret/myapp/config

# ==========================================
# 列出所有密钥路径
# ==========================================
bao kv list secret/myapp/

# ==========================================
# 删除密钥(软删除,可恢复)
# ==========================================
bao kv delete secret/myapp/config

# ==========================================
# 恢复已删除的密钥
# ==========================================
bao kv undelete -versions=2 secret/myapp/config

# ==========================================
# 永久销毁指定版本(不可恢复)
# ==========================================
bao kv destroy -versions=1 secret/myapp/config
KV v1 vs KV v2
特性KV v1KV v2
版本控制❌ 不支持✅ 支持(每次写入创建新版本)
软删除❌ 不支持✅ 支持(可恢复)
元数据❌ 不支持✅ 支持(created_time、custom_metadata)
Check-and-Set❌ 不支持✅ 支持(CAS,防止并发写入覆盖)
路径前缀secret/<path>secret/data/<path>(API 路径)
推荐使用简单场景生产环境推荐

3.4 HTTP API 直接调用

除了 CLI,OpenBao 的所有操作都可以通过 HTTP API 完成:

# 写入密钥 (KV v2)
curl -s \
--header "X-Vault-Token: $BAO_TOKEN" \
--request POST \
--data '{
"data": {
"api_key": "sk-abc123def456",
"api_secret": "super-secret-value"
}
}' \
$BAO_ADDR/v1/secret/data/myapp/api-keys | jq

# 读取密钥
curl -s \
--header "X-Vault-Token: $BAO_TOKEN" \
$BAO_ADDR/v1/secret/data/myapp/api-keys | jq '.data.data'

# 查看服务器状态
curl -s $BAO_ADDR/v1/sys/health | jq

四、Secret Engines 深度解析

Secret Engine 是 OpenBao 的核心组件,它们可以存储生成加密数据。每个引擎通过路径挂载来隔离,这意味着挂载在 database/ 路径的引擎和挂载在 secret/ 路径的引擎互不影响。

4.1 引擎全景

引擎类型用途典型场景
KV存储键值对存储应用配置、API Key、凭证
Database动态动态生成数据库凭证MySQL、PostgreSQL、MongoDB 临时账号
PKI动态签发 X.509 证书内部 TLS 证书、mTLS
Transit加密加密即服务(EaaS)数据加密/解密/签名,无需管理密钥
SSH动态SSH 密钥/证书签发服务器 SSH 访问控制
TOTP动态生成/验证 TOTP 码双因素认证
Transform加密数据转换(FPE/掩码)信用卡号、身份证号脱敏

4.2 动态密钥:Database 引擎实战

动态密钥是 OpenBao 最强大的特性之一 — 按需生成有时效性的数据库凭证,到期自动撤销。

# ==========================================
# 1. 启用 Database 引擎
# ==========================================
bao secrets enable database

# ==========================================
# 2. 配置 PostgreSQL 连接
# ==========================================
bao write database/config/my-postgresql-db \
plugin_name="postgresql-database-plugin" \
allowed_roles="readonly,readwrite" \
connection_url="postgresql://{{username}}:{{password}}@postgres.internal:5432/myapp?sslmode=require" \
username="vault_admin" \
password="vault_admin_password"

# ==========================================
# 3. 创建只读角色(动态生成的用户只有 SELECT 权限)
# ==========================================
bao write database/roles/readonly \
db_name="my-postgresql-db" \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
revocation_statements="DROP ROLE IF EXISTS \"{{name}}\";" \
default_ttl="1h" \
max_ttl="24h"

# ==========================================
# 4. 创建读写角色
# ==========================================
bao write database/roles/readwrite \
db_name="my-postgresql-db" \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
revocation_statements="DROP ROLE IF EXISTS \"{{name}}\";" \
default_ttl="30m" \
max_ttl="2h"

# ==========================================
# 5. 动态获取凭证
# ==========================================
bao read database/creds/readonly

# 输出:
# Key Value
# --- -----
# lease_id database/creds/readonly/abcd1234...
# lease_duration 1h
# lease_renewable true
# password A1a-xxxxxxxxxxxxxxxxx
# username v-token-readonly-xxxxxxxxx

# ==========================================
# 6. 续租 Lease
# ==========================================
bao lease renew database/creds/readonly/abcd1234...

# ==========================================
# 7. 手动撤销(立即删除数据库用户)
# ==========================================
bao lease revoke database/creds/readonly/abcd1234...
动态密钥最佳实践
  1. 始终设置合理的 TTLdefault_ttl 建议 ≤ 1 小时,max_ttl 建议 ≤ 24 小时。
  2. 应用侧要处理凭证续租 — 在 Lease 到期前主动 renew,或使用 OpenBao Agent 自动续租。
  3. 轮换初始管理员密码 — 配置完数据库连接后,执行 bao write -force database/rotate-root/my-postgresql-db 轮换管理员密码,阻止人工直接登录数据库。

4.3 PKI 引擎:内部 TLS 证书管理

PKI 引擎可以让 OpenBao 成为你的内部 CA(Certificate Authority),自动签发和管理 TLS 证书。

# ==========================================
# 1. 启用 PKI 引擎(Root CA)
# ==========================================
bao secrets enable pki

# 设置最大 TTL 为 10 年
bao secrets tune -max-lease-ttl=87600h pki

# 生成 Root CA 证书
bao write -field=certificate pki/root/generate/internal \
common_name="My Organization Root CA" \
issuer_name="root-2026" \
ttl=87600h > root_ca.crt

# ==========================================
# 2. 启用中间 CA(Intermediate CA)
# ==========================================
bao secrets enable -path=pki_int pki

bao secrets tune -max-lease-ttl=43800h pki_int

# 生成 CSR
bao write -field=csr pki_int/intermediate/generate/internal \
common_name="My Organization Intermediate CA" \
issuer_name="intermediate-2026" > pki_intermediate.csr

# 用 Root CA 签名
bao write -field=certificate pki/root/sign-intermediate \
csr=@pki_intermediate.csr \
format=pem_bundle \
ttl=43800h > intermediate_ca.crt

# 导入签名后的证书
bao write pki_int/intermediate/set-signed \
certificate=@intermediate_ca.crt

# ==========================================
# 3. 创建角色(定义可签发的证书规范)
# ==========================================
bao write pki_int/roles/internal-service \
allowed_domains="internal.mycompany.com,svc.cluster.local" \
allow_subdomains=true \
max_ttl="720h" \
key_type="ec" \
key_bits=256

# ==========================================
# 4. 签发证书
# ==========================================
bao write pki_int/issue/internal-service \
common_name="api.internal.mycompany.com" \
ttl="72h"

# 输出包含:
# - certificate (服务器证书)
# - issuing_ca (中间 CA 证书)
# - private_key (私钥,只在此刻返回!)
# - serial_number (证书序列号)

4.4 Transit 引擎:加密即服务(EaaS)

Transit 引擎提供加密即服务,应用不需要管理加密密钥,只需调用 API 即可完成加密/解密/签名操作。数据不会存储在 OpenBao 中,Transit 只负责加密操作。

# 启用 Transit 引擎
bao secrets enable transit

# 创建加密密钥
bao write -f transit/keys/my-app-key

# 加密数据(输入必须 base64 编码)
bao write transit/encrypt/my-app-key \
plaintext=$(echo -n "my-secret-data" | base64)

# 输出:
# Key Value
# --- -----
# ciphertext vault:v1:xxxxxxxxxxxxxxxxxxxxxx
# key_version 1

# 解密数据
bao write transit/decrypt/my-app-key \
ciphertext="vault:v1:xxxxxxxxxxxxxxxxxxxxxx"

# 输出中的 plaintext 需要 base64 解码
# echo "base64-output" | base64 -d
# my-secret-data

# 密钥轮换
bao write -f transit/keys/my-app-key/rotate

# 重新加密(使用新密钥版本加密已有密文)
bao write transit/rewrap/my-app-key \
ciphertext="vault:v1:xxxxxxxxxxxxxxxxxxxxxx"
# ciphertext 变为 vault:v2:...

五、认证与授权

5.1 Auth Methods 概览

OpenBao 支持多种认证方式,以适应不同场景:

Auth Method适用对象说明
Token所有场景核心认证方式,所有其他方式最终都返回 Token
UserPass人类用户用户名/密码认证,适合开发和测试
AppRole应用/服务基于 Role ID + Secret ID 的双因素认证
KubernetesK8s Pod使用 Service Account Token 自动认证
LDAP企业用户集成企业目录服务
JWT/OIDCSSO 用户集成 Okta、Auth0、Google 等身份提供商
TLS CertificatesmTLS 场景使用客户端证书认证

5.2 AppRole 认证实战(推荐用于应用)

AppRole 是应用和服务最推荐的认证方式。它采用双因素模型:Role ID(公开部分) + Secret ID(敏感部分)

# ==========================================
# 1. 启用 AppRole
# ==========================================
bao auth enable approle

# ==========================================
# 2. 创建角色
# ==========================================
bao write auth/approle/role/my-app \
token_policies="my-app-policy" \
token_ttl=1h \
token_max_ttl=4h \
secret_id_ttl=10m \
secret_id_num_uses=1 # Secret ID 只能使用一次

# ==========================================
# 3. 获取 Role ID(可以嵌入配置文件或环境变量)
# ==========================================
bao read auth/approle/role/my-app/role-id
# role_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

# ==========================================
# 4. 生成 Secret ID(应通过安全通道传递给应用)
# ==========================================
bao write -f auth/approle/role/my-app/secret-id
# secret_id: yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy

# ==========================================
# 5. 应用侧:使用 Role ID + Secret ID 登录
# ==========================================
bao write auth/approle/login \
role_id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \
secret_id="yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"

# 输出:
# Key Value
# --- -----
# token hvs.CAESIXXXXXXXXXXXXXXXXXX
# token_accessor xxxxxxxxxxxxxxxxxxxxxxxx
# token_duration 1h
# token_renewable true
# token_policies ["default" "my-app-policy"]

# ==========================================
# 6. 使用获取的 Token 读取密钥
# ==========================================
BAO_TOKEN=hvs.CAESIXXXXXXXXXXXXXXXXXX \
bao kv get secret/myapp/config

5.3 Kubernetes 认证

在 Kubernetes 环境中,Pod 可以使用 Service Account Token 直接向 OpenBao 认证:

# 启用 Kubernetes Auth
bao auth enable kubernetes

# 配置 Kubernetes 集群信息
bao write auth/kubernetes/config \
kubernetes_host="https://kubernetes.default.svc:443"

# 创建角色(绑定 Service Account 和 Namespace)
bao write auth/kubernetes/role/my-app \
bound_service_account_names=my-app-sa \
bound_service_account_namespaces=production \
policies=my-app-policy \
ttl=1h

5.4 Token 类型与层级

OpenBao 中有两种 Token 类型:

类型说明适用场景
Service Token可续租、可撤销、有持久化存储的 Token长期运行的服务
Batch Token轻量级、不可续租、无持久化的 Token高吞吐量的短期操作

Token 还有层级概念:

Root Token
├── Service Token A (policy: admin)
│ ├── Service Token A1 (policy: readonly)
│ └── Service Token A2 (policy: readwrite)
└── Service Token B (policy: app)
└── Batch Token B1 (policy: app)
Root Token 安全
  • Root Token 拥有一切权限,相当于 Linux 的 root 用户。
  • 生产环境中,初始化完成后应立即撤销 Root Tokenbao token revoke <root-token>
  • 需要时可通过 bao operator generate-root 临时生成新的 Root Token(需要多个 Unseal Key 持有者共同授权)。

六、ACL 策略(Policies)

6.1 策略语法

策略定义了 Token 对特定路径的访问权限。OpenBao 采用白名单模式 — 未显式授予的权限一律拒绝。

# ================================================
# 文件:my-app-policy.hcl
# 描述:应用 my-app 的 ACL 策略
# ================================================

# 允许读取和列出应用配置
path "secret/data/myapp/*" {
capabilities = ["read", "list"]
}

# 允许读取和更新特定路径
path "secret/data/myapp/config" {
capabilities = ["create", "read", "update"]
}

# 允许获取数据库动态凭证
path "database/creds/readonly" {
capabilities = ["read"]
}

# 允许续租和撤销自己的 Lease
path "sys/leases/renew" {
capabilities = ["update"]
}

path "sys/leases/revoke" {
capabilities = ["update"]
}

# 允许查看自己的 Token 信息
path "auth/token/lookup-self" {
capabilities = ["read"]
}

# 允许续租自己的 Token
path "auth/token/renew-self" {
capabilities = ["update"]
}

# 拒绝访问元数据(使用 deny 覆盖其他策略)
path "secret/metadata/*" {
capabilities = ["deny"]
}

6.2 权限说明

Capability说明对应 HTTP 方法
create创建新数据POST
read读取数据GET
update更新已有数据POST/PUT
delete删除数据DELETE
list列出路径下的键LIST
sudo允许访问需要 root 权限的路径-
deny显式拒绝(最高优先级)-

6.3 策略管理

# 创建/更新策略
bao policy write my-app-policy my-app-policy.hcl

# 查看策略列表
bao policy list

# 查看策略内容
bao policy read my-app-policy

# 删除策略
bao policy delete my-app-policy

# 为已有 Token 关联策略
bao token create -policy="my-app-policy" -ttl=2h

6.4 策略模板(参数化策略)

OpenBao 支持在策略中使用模板变量,实现基于身份的动态权限

# 每个用户只能访问自己的密钥空间
path "secret/data/users/{{identity.entity.name}}/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}

# 每个团队只能管理自己的密钥
path "secret/data/teams/{{identity.groups.names.*.id}}/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}

七、Seal / Unseal 机制详解

7.1 Shamir 分片(默认)

OpenBao 默认使用 Shamir's Secret Sharing 算法将 Unseal Key 分割为多个分片。只需要一定数量(阈值)的分片即可重建 Unseal Key。

# 初始化(设置 5 个分片,阈值 3)
bao operator init -key-shares=5 -key-threshold=3

# 输出示例:
# Unseal Key 1: abc...
# Unseal Key 2: def...
# Unseal Key 3: ghi...
# Unseal Key 4: jkl...
# Unseal Key 5: mno...
#
# Initial Root Token: hvs.xxxxxxxxxx
#
# 请将这些 Key 分发给不同的人!

# Unseal 过程(需要提供 3 个 Key)
bao operator unseal # 输入 Key 1
# Sealed: true, Progress: 1/3

bao operator unseal # 输入 Key 2
# Sealed: true, Progress: 2/3

bao operator unseal # 输入 Key 3
# Sealed: false ← Unseal 成功!
分片分发最佳实践
场景分片数阈值说明
开发/测试11单人即可 Unseal
小团队32至少 2 人同时在场
生产环境53容忍 2 个分片丢失,需要 3 人同时授权
企业级74高安全性要求

7.2 Auto Unseal(推荐用于生产)

Shamir 分片的手动 Unseal 过程增加了运维复杂度(每次重启都需要收集足够的 Key)。Auto Unseal 将 Unseal Key 的保管委托给外部的 KMS 或 HSM,实现自动解封。

支持的 Auto Unseal Provider:

Provider配置块名称说明
AWS KMSseal "awskms"AWS Key Management Service
GCP Cloud KMSseal "gcpckms"Google Cloud KMS
Azure Key Vaultseal "azurekeyvault"Azure 密钥保管库
AliCloud KMSseal "alicloudkms"阿里云密钥管理服务
OCI KMSseal "ocikms"Oracle Cloud KMS
HSM (PKCS#11)seal "pkcs11"硬件安全模块
Transitseal "transit"使用另一个 OpenBao 实例

AWS KMS 配置示例:

seal "awskms" {
region = "ap-southeast-1"
kms_key_id = "arn:aws:kms:ap-southeast-1:123456789:key/abcd-1234-..."

# 可选:指定 AWS 凭证(推荐使用 IAM Role)
# access_key = "..."
# secret_key = "..."
}
Auto Unseal 的风险

使用 Auto Unseal 会创建 OpenBao 对外部 KMS 的生命周期依赖

  • 如果 KMS 密钥不可用(如 AWS 服务故障),OpenBao 无法启动。
  • 如果 KMS 密钥被永久删除,OpenBao 数据将永远无法恢复,即使从备份恢复也不行。
  • 建议:使用 AWS SCP(Service Control Policies)等机制保护 KMS 密钥不被删除

7.3 Seal 迁移

在不同 Seal 类型之间迁移(如从 Shamir 迁移到 Auto Unseal)是可能的,但需要停机操作:


八、生产级部署

8.1 生产配置文件

以下是一个完整的生产级 OpenBao 配置文件,使用 Integrated Storage(Raft):

# ================================================
# /etc/openbao/config.hcl - 生产级配置
# ================================================

# --- 集群标识 ---
cluster_name = "prod-openbao-cluster"

# --- 网络地址 ---
# API 地址:客户端访问地址
api_addr = "https://openbao.mycompany.com:8200"
# 集群通信地址:节点间通信
cluster_addr = "https://10.0.1.10:8201"

# --- Web UI ---
ui = true

# --- 日志 ---
log_level = "info"
log_format = "json"
log_file = "/var/log/openbao/openbao.log"
log_rotate_duration = "24h"
log_rotate_max_files = 30

# --- Listener (TLS) ---
listener "tcp" {
address = "0.0.0.0:8200"
cluster_address = "0.0.0.0:8201"

# TLS 配置(生产环境必须启用)
tls_cert_file = "/etc/openbao/tls/server.crt"
tls_key_file = "/etc/openbao/tls/server.key"
tls_client_ca_file = "/etc/openbao/tls/ca.crt"

# TLS 最低版本
tls_min_version = "tls12"

# 禁用弱密码套件
tls_cipher_suites = "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"

# 请求限制
max_request_duration = "90s"
max_request_size = 33554432 # 32MB
}

# --- Storage (Raft / Integrated Storage) ---
storage "raft" {
path = "/opt/openbao/data"
node_id = "node-1"

# 性能调优
performance_multiplier = 1 # 生产环境设为 1(低延迟调优)

# Autopilot 配置(自动管理集群成员)
autopilot {
cleanup_dead_servers = true
dead_server_last_contact_threshold = "24h"
min_quorum = 3
}

# 集群对端节点
retry_join {
leader_api_addr = "https://10.0.1.11:8200"
leader_ca_cert_file = "/etc/openbao/tls/ca.crt"
}
retry_join {
leader_api_addr = "https://10.0.1.12:8200"
leader_ca_cert_file = "/etc/openbao/tls/ca.crt"
}
}

# --- Auto Unseal (可选,推荐) ---
seal "awskms" {
region = "ap-southeast-1"
kms_key_id = "arn:aws:kms:ap-southeast-1:123456789:key/abcd-1234-..."
}

# --- Telemetry (可观测性)---
telemetry {
# Prometheus 指标端点
prometheus_retention_time = "24h"
disable_hostname = true

# StatsD(可选)
# statsd_address = "statsd.internal:8125"
}

# --- Lease 管理 ---
default_lease_ttl = "768h" # 默认 32 天
max_lease_ttl = "768h" # 最大 32 天

# --- 审计 (在初始化后通过 API 启用) ---
# bao audit enable file file_path=/var/log/openbao/audit.log

8.2 systemd 服务配置

# /etc/systemd/system/openbao.service
[Unit]
Description=OpenBao - A tool for managing secrets
Documentation=https://openbao.org/docs/
Requires=network-online.target
After=network-online.target
ConditionFileNotEmpty=/etc/openbao/config.hcl
StartLimitIntervalSec=60
StartLimitBurst=3

[Service]
Type=notify
User=openbao
Group=openbao
ProtectSystem=full
ProtectHome=read-only
PrivateTmp=yes
PrivateDevices=yes
SecureBits=keep-caps
AmbientCapabilities=CAP_IPC_LOCK
NoNewPrivileges=yes
ExecStart=/usr/bin/bao server -config=/etc/openbao/config.hcl
ExecReload=/bin/kill --signal HUP $MAINPID
KillMode=process
KillSignal=SIGINT
Restart=on-failure
RestartSec=5
TimeoutStopSec=30
LimitNOFILE=65536
LimitMEMLOCK=infinity
MemorySwapMax=0

[Install]
WantedBy=multi-user.target

8.3 初始化与首次启动

# 1. 创建数据目录
sudo mkdir -p /opt/openbao/data
sudo chown openbao:openbao /opt/openbao/data

# 2. TLS 证书准备(假设已有证书)
sudo mkdir -p /etc/openbao/tls
sudo cp server.crt server.key ca.crt /etc/openbao/tls/
sudo chown openbao:openbao /etc/openbao/tls/*
sudo chmod 600 /etc/openbao/tls/server.key

# 3. 启动服务
sudo systemctl enable openbao
sudo systemctl start openbao

# 4. 初始化(仅首次)
export BAO_ADDR='https://openbao.mycompany.com:8200'
export BAO_CACERT='/etc/openbao/tls/ca.crt'

bao operator init \
-key-shares=5 \
-key-threshold=3

# ⚠️ 安全保存输出的 Unseal Key 和 Root Token!

# 5. Unseal
bao operator unseal # 输入 Key 1
bao operator unseal # 输入 Key 2
bao operator unseal # 输入 Key 3

# 6. 验证
bao status

# 7. 登录
bao login $ROOT_TOKEN

# 8. 启用审计日志(生产必备)
bao audit enable file file_path=/var/log/openbao/audit.log

# 9. 撤销 Root Token(安全最佳实践)
bao token revoke $ROOT_TOKEN

九、高可用(HA)集群

9.1 Integrated Storage(Raft)HA 架构

OpenBao 推荐使用 Integrated Storage(基于 Raft 共识算法) 作为存储后端,它无需外部依赖(如 Consul),且内置 HA 能力。

9.2 集群节点配置

Node 1(Leader 候选):

storage "raft" {
path = "/opt/openbao/data"
node_id = "node-1"

retry_join {
leader_api_addr = "https://10.0.1.11:8200"
leader_ca_cert_file = "/etc/openbao/tls/ca.crt"
}
retry_join {
leader_api_addr = "https://10.0.1.12:8200"
leader_ca_cert_file = "/etc/openbao/tls/ca.crt"
}
}

Node 2 & 3(类似配置,修改 node_id 和 retry_join 节点列表):

storage "raft" {
path = "/opt/openbao/data"
node_id = "node-2" # 或 node-3

retry_join {
leader_api_addr = "https://10.0.1.10:8200"
leader_ca_cert_file = "/etc/openbao/tls/ca.crt"
}
retry_join {
leader_api_addr = "https://10.0.1.12:8200" # 或 10.0.1.11
leader_ca_cert_file = "/etc/openbao/tls/ca.crt"
}
}

9.3 加入集群

# 在 Node 2 和 Node 3 上执行
# 首先启动服务
sudo systemctl start openbao

# 然后 Unseal(每个节点都需要独立 Unseal)
bao operator unseal # Key 1
bao operator unseal # Key 2
bao operator unseal # Key 3

# 检查 Raft 成员列表(在 Leader 节点上)
bao operator raft list-peers

# 输出:
# Node Address State Voter
# ---- ------- ----- -----
# node-1 10.0.1.10:8201 leader true
# node-2 10.0.1.11:8201 follower true
# node-3 10.0.1.12:8201 follower true

9.4 HA 运维操作

# 查看 Leader 信息
bao status
# HA Enabled: true
# HA Cluster: https://10.0.1.10:8201
# HA Mode: active (或 standby)

# 手动 Step Down(触发领导权转移)
bao operator step-down

# Raft 快照备份
bao operator raft snapshot save backup-$(date +%Y%m%d).snap

# Raft 快照恢复
bao operator raft snapshot restore backup-20260409.snap

# 移除失败的节点
bao operator raft remove-peer node-3
HA 集群最佳实践
  1. 节点数量:生产环境建议 3 或 5 个节点(奇数节点,满足 Raft 多数派要求)。
  2. 网络延迟:同 Region 部署,节点间延迟应 < 10ms。
  3. 负载均衡:所有节点都可以处理读请求;写请求会被自动转发到 Leader。
  4. 备份策略:每日自动执行 raft snapshot save,保存到安全的外部存储。
  5. 监控:关注 vault.raft.leader.lastContactvault.raft.commitTime 指标。

十、审计日志

审计日志是安全合规的基础。OpenBao 支持三种审计设备:

10.1 启用审计

# 文件审计(最常用)
bao audit enable file file_path=/var/log/openbao/audit.log

# Syslog 审计
bao audit enable syslog tag="openbao" facility="AUTH"

# Socket 审计(发送到远程日志系统)
bao audit enable socket address="logserver.internal:9090" socket_type="tcp"

# 查看已启用的审计设备
bao audit list -detailed

10.2 审计日志格式

每条审计记录包含完整的请求和响应信息(敏感数据会被 HMAC 哈希处理):

{
"time": "2026-04-09T11:00:00.000000Z",
"type": "response",
"auth": {
"client_token": "hmac-sha256:abcdef...",
"accessor": "hmac-sha256:123456...",
"display_name": "approle",
"policies": ["default", "my-app-policy"],
"token_type": "service"
},
"request": {
"id": "request-uuid",
"operation": "read",
"path": "secret/data/myapp/config",
"remote_address": "10.0.2.50"
},
"response": {
"data": {
"data": {
"db_host": "hmac-sha256:xxxxxx...",
"db_password": "hmac-sha256:yyyyyy..."
}
}
}
}
审计安全性
  • 必须至少启用一个审计设备。如果所有审计设备都失败,OpenBao 将拒绝处理任何请求,以确保不会出现未记录审计的操作。
  • 审计日志中的敏感数据使用 HMAC-SHA256 哈希,无法逆向还原。但如果你知道原始值,可以通过 bao audit hash 命令计算哈希值进行比对。
  • 建议将审计日志实时转发到集中式日志系统(如 ELK、Loki、Splunk)。

十一、Kubernetes 集成

11.1 Helm 部署 OpenBao

OpenBao 官方提供了 Helm Chart,可以方便地在 Kubernetes 上部署 HA 集群:

# 添加 Helm 仓库
helm repo add openbao https://openbao.github.io/openbao-helm
helm repo update

# 创建 values.yaml
cat <<EOF > openbao-values.yaml
global:
enabled: true
tlsDisable: false

server:
image:
repository: ghcr.io/openbao/openbao
tag: "2.5.0"

# HA 模式(3 副本)
ha:
enabled: true
replicas: 3
raft:
enabled: true
config: |
ui = true

listener "tcp" {
address = "[::]:8200"
cluster_address = "[::]:8201"
tls_cert_file = "/vault/tls/tls.crt"
tls_key_file = "/vault/tls/tls.key"
}

storage "raft" {
path = "/vault/data"

retry_join {
leader_api_addr = "https://openbao-0.openbao-internal:8200"
leader_ca_cert_file = "/vault/tls/ca.crt"
}
retry_join {
leader_api_addr = "https://openbao-1.openbao-internal:8200"
leader_ca_cert_file = "/vault/tls/ca.crt"
}
retry_join {
leader_api_addr = "https://openbao-2.openbao-internal:8200"
leader_ca_cert_file = "/vault/tls/ca.crt"
}
}

service_registration "kubernetes" {}

# 持久化存储
dataStorage:
enabled: true
size: 10Gi
storageClass: "gp3"

# 资源限制
resources:
requests:
cpu: 500m
memory: 256Mi
limits:
cpu: 2000m
memory: 1Gi

# 审计日志存储
auditStorage:
enabled: true
size: 5Gi

# Agent Injector(自动注入 sidecar)
injector:
enabled: true
replicas: 2
resources:
requests:
cpu: 100m
memory: 64Mi

# Web UI
ui:
enabled: true
serviceType: ClusterIP
EOF

# 部署
helm install openbao openbao/openbao \
-n openbao --create-namespace \
-f openbao-values.yaml

11.2 Agent Injector:自动为 Pod 注入密钥

OpenBao Agent Injector 是一个 Kubernetes Mutating Webhook,它通过注解(Annotations)为 Pod 自动注入 Sidecar,从 OpenBao 获取密钥并写入文件。

apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 3
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
annotations:
# ========================================
# OpenBao Agent Injector 注解
# ========================================
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "my-app"

# 注入数据库凭证
vault.hashicorp.com/agent-inject-secret-db-creds: "database/creds/readonly"
vault.hashicorp.com/agent-inject-template-db-creds: |
{{- with secret "database/creds/readonly" -}}
DB_HOST=postgres.internal
DB_PORT=5432
DB_USER={{ .Data.username }}
DB_PASSWORD={{ .Data.password }}
{{- end }}

# 注入应用配置
vault.hashicorp.com/agent-inject-secret-app-config: "secret/data/myapp/config"
vault.hashicorp.com/agent-inject-template-app-config: |
{{- with secret "secret/data/myapp/config" -}}
API_KEY={{ .Data.data.api_key }}
API_SECRET={{ .Data.data.api_secret }}
{{- end }}

spec:
serviceAccountName: my-app-sa
containers:
- name: my-app
image: my-app:latest
command: ["/bin/sh", "-c"]
args:
- |
source /vault/secrets/db-creds
source /vault/secrets/app-config
exec ./my-app-binary
ports:
- containerPort: 8080

密钥将被注入到 Pod 的 /vault/secrets/ 目录下:

/vault/secrets/
├── db-creds # 数据库凭证
└── app-config # 应用配置

11.3 CSI Provider(另一种注入方式)

除了 Agent Injector,还可以使用 CSI(Container Storage Interface)Provider 将密钥作为卷挂载:

apiVersion: v1
kind: Pod
metadata:
name: my-app
spec:
serviceAccountName: my-app-sa
containers:
- name: my-app
image: my-app:latest
volumeMounts:
- name: secrets
mountPath: "/mnt/secrets"
readOnly: true
volumes:
- name: secrets
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: "openbao-secrets"
---
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: openbao-secrets
spec:
provider: vault # OpenBao 兼容 Vault CSI Provider
parameters:
roleName: "my-app"
vaultAddress: "https://openbao.openbao.svc:8200"
objects: |
- objectName: "db-password"
secretPath: "secret/data/myapp/config"
secretKey: "db_password"

十二、OpenBao Agent / Proxy

12.1 Agent 模式

OpenBao Agent 是一个客户端守护进程,提供以下能力:

  • Auto Auth:自动认证并获取 Token
  • Caching:本地缓存 Token 和 Lease,减少对 Server 的请求
  • Templating:将密钥渲染到配置文件模板中
  • API Proxy:代理 API 请求,自动注入认证信息
# /etc/openbao/agent.hcl

# 如何认证
auto_auth {
method "approle" {
config = {
role_id_file_path = "/etc/openbao/role-id"
secret_id_file_path = "/etc/openbao/secret-id"
remove_secret_id_file_after_reading = true
}
}

# 将 Token 保存到文件
sink "file" {
config = {
path = "/tmp/openbao-token"
mode = 0640
}
}
}

# 缓存配置
cache {
use_auto_auth_token = true
}

# API 代理
listener "tcp" {
address = "127.0.0.1:8100"
tls_disable = true
}

# 服务器连接
vault {
address = "https://openbao.mycompany.com:8200"
tls_ca_cert = "/etc/openbao/tls/ca.crt"
}

# 模板渲染
template {
source = "/etc/openbao/templates/db-config.tpl"
destination = "/app/config/database.env"
perms = 0640
command = "systemctl reload my-app" # 渲染后执行的命令
}

模板文件示例 (db-config.tpl):

{{ with secret "database/creds/readonly" }}
DB_USER={{ .Data.username }}
DB_PASSWORD={{ .Data.password }}
{{ end }}
{{ with secret "secret/data/myapp/config" }}
API_KEY={{ .Data.data.api_key }}
{{ end }}
# 启动 Agent
bao agent -config=/etc/openbao/agent.hcl

12.2 Proxy 模式

OpenBao Proxy 与 Agent 类似,但专注于API 代理功能,不包含模板渲染。适合作为应用的 Sidecar 使用。


十三、运维与监控

13.1 关键监控指标

指标说明告警阈值
vault.core.handle_request请求处理延迟P99 > 500ms
vault.barrier.getBarrier 读取延迟P99 > 100ms
vault.barrier.putBarrier 写入延迟P99 > 200ms
vault.raft.leader.lastContactFollower 到 Leader 的最后联系时间> 500ms
vault.raft.commitTimeRaft 提交时间P99 > 50ms
vault.expire.num_leases当前活跃 Lease 数根据容量规划
vault.runtime.alloc_bytes内存分配量接近 limits
vault.core.unsealed是否已 Unseal= 0 表示 Sealed

13.2 Prometheus 集成

# prometheus-scrape-config.yaml
scrape_configs:
- job_name: "openbao"
metrics_path: "/v1/sys/metrics"
params:
format: ["prometheus"]
scheme: "https"
tls_config:
ca_file: "/etc/prometheus/openbao-ca.crt"
authorization:
credentials_file: "/etc/prometheus/openbao-token"
static_configs:
- targets:
- "openbao-0.internal:8200"
- "openbao-1.internal:8200"
- "openbao-2.internal:8200"

13.3 备份与恢复策略

# ==========================================
# 自动备份脚本
# ==========================================
#!/bin/bash
# /opt/openbao/scripts/backup.sh

BACKUP_DIR="/opt/openbao/backups"
DATE=$(date +%Y%m%d-%H%M%S)
RETENTION_DAYS=30

# 创建快照
bao operator raft snapshot save "${BACKUP_DIR}/openbao-${DATE}.snap"

# 上传到对象存储(示例:AWS S3)
aws s3 cp "${BACKUP_DIR}/openbao-${DATE}.snap" \
"s3://my-backups/openbao/openbao-${DATE}.snap" \
--sse aws:kms

# 清理本地旧备份
find "${BACKUP_DIR}" -name "*.snap" -mtime +${RETENTION_DAYS} -delete

echo "[$(date)] Backup completed: openbao-${DATE}.snap"
# 添加到 crontab(每天凌晨 3 点备份)
echo "0 3 * * * /opt/openbao/scripts/backup.sh >> /var/log/openbao/backup.log 2>&1" \
| crontab -

13.4 密钥轮换

# 轮换加密密钥(在线操作,不影响服务)
bao operator rotate

# 查看当前密钥信息
bao read sys/key-status
# Key Value
# --- -----
# install_time 2026-04-09T00:00:00Z
# term 2 # 密钥已轮换过 1 次

十四、应用集成模式

14.1 应用读取密钥的四种方式

方式复杂度适用场景优缺点
直接 API需要精细控制的应用灵活但需自行管理 Token 生命周期
Agent虚拟机/裸金属部署自动认证+缓存+模板,推荐
K8s InjectorKubernetes 环境注解驱动,零代码侵入
CSI ProviderKubernetes 环境标准 Volume 接口

14.2 Go 应用集成示例

package main

import (
"context"
"fmt"
"log"
"time"

vault "github.com/openbao/openbao/api/v2"
)

func main() {
// 创建客户端
config := vault.DefaultConfig()
config.Address = "https://openbao.mycompany.com:8200"

client, err := vault.NewClient(config)
if err != nil {
log.Fatalf("unable to initialize OpenBao client: %v", err)
}

// AppRole 登录
resp, err := client.Logical().Write("auth/approle/login", map[string]interface{}{
"role_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"secret_id": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
})
if err != nil {
log.Fatalf("unable to login: %v", err)
}
client.SetToken(resp.Auth.ClientToken)

// 读取密钥
secret, err := client.KVv2("secret").Get(context.Background(), "myapp/config")
if err != nil {
log.Fatalf("unable to read secret: %v", err)
}

dbHost := secret.Data["db_host"].(string)
dbPassword := secret.Data["db_password"].(string)
fmt.Printf("DB Host: %s\n", dbHost)
fmt.Printf("DB Password: %s\n", dbPassword)

// Token 续租(后台 goroutine)
go func() {
for {
time.Sleep(30 * time.Minute)
_, err := client.Auth().Token().RenewSelf(3600) // 续租 1 小时
if err != nil {
log.Printf("token renewal failed: %v", err)
}
}
}()

// ... 应用正常逻辑
select {}
}

14.3 Python 应用集成示例

import hvac # pip install hvac(兼容 OpenBao API)
import os
import time
import threading

class SecretsManager:
"""OpenBao 密钥管理封装"""

def __init__(self):
self.client = hvac.Client(
url=os.environ.get('BAO_ADDR', 'https://openbao.mycompany.com:8200'),
verify='/etc/openbao/tls/ca.crt'
)
self._login()
self._start_renewal()

def _login(self):
"""使用 AppRole 登录"""
result = self.client.auth.approle.login(
role_id=os.environ['BAO_ROLE_ID'],
secret_id=os.environ['BAO_SECRET_ID']
)
self.client.token = result['auth']['client_token']
self.lease_duration = result['auth']['lease_duration']

def get_secret(self, path: str, key: str = None) -> dict | str:
"""读取 KV v2 密钥"""
response = self.client.secrets.kv.v2.read_secret_version(
path=path,
mount_point='secret'
)
data = response['data']['data']
return data[key] if key else data

def get_db_credentials(self, role: str = 'readonly') -> dict:
"""获取动态数据库凭证"""
response = self.client.read(f'database/creds/{role}')
return {
'username': response['data']['username'],
'password': response['data']['password'],
'lease_id': response['lease_id'],
'lease_duration': response['lease_duration']
}

def _start_renewal(self):
"""后台自动续租 Token"""
def renew():
while True:
time.sleep(self.lease_duration * 0.7) # 在 70% 时续租
try:
self.client.auth.token.renew_self()
except Exception as e:
print(f"Token renewal failed: {e}")
self._login()

thread = threading.Thread(target=renew, daemon=True)
thread.start()

# 使用示例
secrets = SecretsManager()
db_password = secrets.get_secret('myapp/config', 'db_password')
db_creds = secrets.get_db_credentials('readonly')
print(f"Dynamic DB User: {db_creds['username']}")

十五、生产环境 Checklist

在将 OpenBao 部署到生产环境之前,请逐一确认以下事项:

基础安全

  • TLS 已启用 — Listener 配置了有效的 TLS 证书
  • Swap 已禁用或加密 — 防止密钥数据泄露到磁盘
  • Root Token 已撤销 — 初始化完成后立即撤销
  • Unseal Key 安全分发 — 多人持有,物理隔离存储
  • Auto Unseal 已配置(如使用)— KMS 密钥受保护不可删除

存储与高可用

  • Raft 集群 3+ 节点 — 奇数节点,容忍 N/2 节点故障
  • 持久化存储可靠 — 使用 SSD,IOPS 足够
  • 自动备份已配置 — 每日 Raft Snapshot,上传到安全存储
  • 备份恢复已测试 — 定期演练恢复流程

审计与监控

  • 审计日志已启用 — 至少一个审计设备
  • 审计日志转发 — 发送到集中式日志系统
  • Prometheus 监控 — 采集关键指标
  • 告警规则已配置 — Sealed 状态、请求延迟、Raft 通信

访问控制

  • 最小权限策略 — 每个应用只获得必要的权限
  • AppRole / K8s Auth — 应用使用自动化认证
  • Secret ID 限制 — 设置 secret_id_num_uses=1secret_id_ttl
  • Token TTL 合理 — 默认 1h,最大不超过 24h

密钥管理

  • KV v2 引擎 — 启用版本控制
  • 动态密钥 — 数据库等使用动态凭证,而非静态密码
  • 密钥轮换计划 — 定期执行 bao operator rotate
  • Lease TTL 合理 — 动态凭证 ≤ 1h

十六、本章小结

本文从零开始,全面深入地解析了 OpenBao 的各个方面:

  1. 架构理解 — 掌握了 Barrier 加密屏障、多层密钥保护、请求处理流程等核心设计。

  2. 安装上手 — 从 Dev 模式体验到各平台安装方式,快速建立第一个实操经验。

  3. Secret Engines — 深入学习了 KV v2(静态密钥)、Database(动态凭证)、PKI(证书管理)、Transit(加密即服务)等核心引擎。

  4. 认证授权 — 掌握了 AppRole、Kubernetes Auth、Token 体系和 ACL Policy 的设计与实践。

  5. Seal/Unseal — 理解了 Shamir 分片与 Auto Unseal 的工作原理及迁移方案。

  6. 生产部署 — 完成了 TLS、Raft HA 集群、systemd 服务、审计日志的完整配置。

  7. Kubernetes 集成 — 掌握了 Helm 部署、Agent Injector、CSI Provider 的使用方式。

  8. 应用集成 — 通过 Go 和 Python 示例,展示了应用如何安全地获取和续租密钥。

OpenBao 作为 HashiCorp Vault 的开源替代品,在保持 API 兼容性的同时,完全由社区驱动,是云原生时代密钥管理的理想选择。


参考文档

本文内容基于 OpenBao 官方文档校验,以下为核心参考链接:

Kubernetes 全景解析 (7):有状态应用与 Operator 模式实战

· 阅读需 32 分钟
Rainy
雨落无声,代码成诗 —— 致力于技术与艺术的极致平衡

"StatefulSet 管的是'有序的宠物',Operator 管的是'懂业务的管家'。"

在前面的章节中,我们深入探讨了 K8s 的架构设计、网络模型和存储体系。对于无状态应用(如 Web 服务、API 网关),Deployment + Service 的组合已经足够应对大多数场景。然而,当你面对数据库、消息队列、分布式缓存等有状态应用时,事情就变得复杂了——它们需要稳定的网络标识、持久化的存储卷、有序的启停顺序,以及复杂的集群初始化流程。

本文将从有状态应用的核心挑战出发,深入解析 StatefulSet 的工作机制,实战部署一套 Redis Cluster,并最终构建一个自定义 Operator 来自动化管理整个生命周期。

API 版本说明:本文所有 YAML 配置使用的 API 版本均经过 Kubernetes v1.34 官方文档校验:

资源类型apiVersion官方文档
StatefulSetapps/v1StatefulSet v1
Servicev1Service v1
CRDapiextensions.k8s.io/v1CRD v1
RBACrbac.authorization.k8s.io/v1RBAC v1
Deploymentapps/v1Deployment v1

:::


一、有状态应用的挑战与 K8s 解决方案

1.1 有状态 vs 无状态:本质区别

在分布式系统中,"状态"(State)是指应用需要持久保存的数据或上下文信息。理解有状态与无状态的本质区别,是选择正确编排策略的前提。

维度无状态应用有状态应用
数据存储数据在外部(DB/缓存)数据在本地或内置存储
网络标识随意替换,IP 可变需要稳定的网络标识
扩缩容随意增删实例需要数据迁移/重平衡
启停顺序无要求通常需要有序启停
典型代表Web 服务、微服务 API数据库、消息队列、缓存
K8s 工作负载Deployment / ReplicaSetStatefulSet / Operator

无状态应用就像工厂流水线上的工人——任何人都可以顶替任何人的位置,因为所有信息都记录在外部系统中。

有状态应用则像一支乐队——每个乐手有自己的乐器和乐谱,鼓手不能突然变成小提琴手,乐队的演出需要按特定顺序开始。

1.2 StatefulSet 的核心特性

StatefulSet 是 Kubernetes 专门为有状态应用设计的工作负载控制器。它相比 Deployment,提供了三个关键保证:

1. 稳定的、唯一的网络标识

每个 Pod 都会获得一个固定的名称和对应的 DNS 记录。即使 Pod 被重新调度到其他节点,名称和 DNS 也不会改变。

2. 稳定的、持久的存储

每个 Pod 绑定一个独立的 PVC(PersistentVolumeClaim),Pod 被删除后 PVC 保留,新 Pod 重建时会重新挂载同一个 PVC。

3. 有序的、优雅的部署和扩缩容

Pod 按照 0, 1, 2, ... 的顺序依次创建和删除,确保集群拓扑的稳定性。

1.3 Headless Service 与 DNS 记录

StatefulSet 依赖一个特殊的 Service——Headless ServiceclusterIP: None)来实现稳定的网络标识。与普通 Service 不同,Headless Service 不会分配集群 IP,也不会进行代理负载均衡,而是直接将 DNS 解析到每个 Pod 的 IP。

# headless-service.yaml
apiVersion: v1
kind: Service
metadata:
name: redis-cluster
spec:
clusterIP: None # 关键:声明为 Headless Service
selector:
app: redis-cluster
ports:
- port: 6379
targetPort: 6379

创建 Headless Service 后,DNS 会为每个 Pod 生成如下记录:

redis-cluster-0.redis-cluster.default.svc.cluster.local -> 10.244.1.10
redis-cluster-1.redis-cluster.default.svc.cluster.local -> 10.244.2.15
redis-cluster-2.redis-cluster.default.svc.cluster.local -> 10.244.3.20

这种稳定的 DNS 记录使得有状态应用可以通过固定的主机名互相发现和通信,而不需要依赖可能变化的 Pod IP。

DNS 命名规则

完整的 DNS 名称格式为:<pod-name>.<headless-service-name>.<namespace>.svc.cluster.local

在同一个命名空间内,可以简写为 <pod-name>.<headless-service-name>


二、实战 1:Redis Cluster 部署

2.1 Redis Cluster 架构设计

Redis Cluster 是 Redis 官方提供的分布式解决方案,采用去中心化的 Hash Slot 架构。一个 Redis Cluster 由多个主节点(Master)组成,每个主节点负责一部分 Hash Slot(共 16384 个),主节点可以挂载从节点(Slave)用于故障转移。

2.2 完整部署 YAML

下面是一套可直接运行的 Redis Cluster 部署配置。我们将部署 6 个节点(3 主 3 从),使用 StatefulSet 保证有序部署和稳定标识。

API 版本说明:StatefulSet 使用 apps/v1,Headless Service 使用 v1,均为 Kubernetes v1.34 稳定版本。详见官方文档:StatefulSetService

文件路径:redis-cluster/redis-cluster.yaml

---
# Headless Service:为 StatefulSet 提供稳定的网络标识
apiVersion: v1
kind: Service
metadata:
name: redis-cluster
labels:
app: redis-cluster
spec:
clusterIP: None
selector:
app: redis-cluster
ports:
- name: redis
port: 6379
targetPort: 6379
- name: cluster-bus
port: 16379
targetPort: 16379

---
# StatefulSet:管理 6 个 Redis 节点
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis-cluster
labels:
app: redis-cluster
spec:
serviceName: redis-cluster # 必须关联 Headless Service
replicas: 6
selector:
matchLabels:
app: redis-cluster
template:
metadata:
labels:
app: redis-cluster
spec:
terminationGracePeriodSeconds: 30
containers:
- name: redis
image: redis:7.2.4
ports:
- containerPort: 6379
name: redis
- containerPort: 16379
name: cluster-bus
command:
- /bin/sh
- -c
- |
redis-server \
--cluster-enabled yes \
--cluster-config-file /data/nodes.conf \
--cluster-node-timeout 5000 \
--appendonly yes \
--bind 0.0.0.0 \
--port 6379 \
--cluster-announce-ip $(hostname).redis-cluster.default.svc.cluster.local \
--cluster-announce-port 6379 \
--cluster-announce-bus-port 16379
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
volumeMounts:
- name: redis-data
mountPath: /data
livenessProbe:
exec:
command:
- redis-cli
- ping
initialDelaySeconds: 15
periodSeconds: 5
readinessProbe:
exec:
command:
- redis-cli
- ping
initialDelaySeconds: 5
periodSeconds: 3
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
# 每个 Pod 独立的 PVC
volumeClaimTemplates:
- metadata:
name: redis-data
spec:
accessModes:
- ReadWriteOnce
storageClassName: standard # 根据集群实际情况修改
resources:
requests:
storage: 1Gi
生产环境注意事项
  1. cluster-announce-ip 必须使用 Pod 的完整 DNS 名称,否则集群节点之间无法正确发现对方。
  2. cluster-announce-bus-port 必须显式声明,Redis Cluster 节点间通过 Bus 端口进行 gossip 通信。
  3. storageClassName 需要根据你的 K8s 集群实际可用的 StorageClass 进行修改。生产环境建议使用 SSD 存储类。
  4. 建议配置 PodDisruptionBudget 以防止维护操作时同时驱逐过多节点。

2.3 部署与验证

执行部署:

# 创建命名空间(可选)
kubectl create namespace redis

# 部署 Redis Cluster
kubectl apply -f redis-cluster/redis-cluster.yaml -n redis

# 观察 Pod 的有序创建过程
kubectl get pods -n redis -l app=redis-cluster -w

你应该能看到 Pod 按顺序逐个启动:

NAME READY STATUS RESTARTS AGE
redis-cluster-0 1/1 Running 0 30s
redis-cluster-1 1/1 Running 0 15s
redis-cluster-2 1/1 Running 0 5s
redis-cluster-3 0/1 Pending 0 0s
...

验证 DNS 解析:

# 进入任意 Pod 验证 DNS
kubectl exec -it redis-cluster-0 -n redis -- nslookup redis-cluster-1.redis-cluster.redis.svc.cluster.local

# 验证各节点可以互相通信
kubectl exec -it redis-cluster-0 -n redis -- redis-cli -h redis-cluster-1.redis-cluster.redis.svc.cluster.local ping

三、Redis Cluster 初始化与验证

说明:Redis Cluster 初始化使用 redis-cli --cluster create 命令,这是 Redis Cluster 的官方初始化方式。详见官方文档:Redis Cluster Tutorial

3.1 集群初始化脚本

StatefulSet 只负责创建 Pod,Redis Cluster 的初始化(节点握手、槽位分配)需要手动执行。我们编写一个初始化脚本来自动化这个过程。

文件路径:redis-cluster/init-cluster.sh

#!/bin/bash
set -euo pipefail

NAMESPACE="${1:-redis}"
SERVICE="redis-cluster"
REPLICAS=6

echo "=== 等待所有 Redis Pod 就绪 ==="
for i in $(seq 0 $((REPLICAS - 1))); do
echo -n "等待 redis-cluster-${i} ... "
kubectl wait --for=condition=ready pod/redis-cluster-${i} \
-n ${NAMESPACE} --timeout=120s
echo "就绪"
done

echo ""
echo "=== 收集节点地址 ==="
NODES=""
for i in $(seq 0 $((REPLICAS - 1))); do
POD_FQDN="redis-cluster-${i}.${SERVICE}.${NAMESPACE}.svc.cluster.local"
NODES="${NODES} ${POD_FQDN}:6379"
done

echo "节点列表:${NODES}"
echo ""

echo "=== 创建 Redis Cluster ==="
kubectl exec -it redis-cluster-0 -n ${NAMESPACE} -- \
redis-cli --cluster create ${NODES} --cluster-replicas 1

echo ""
echo "=== 验证集群状态 ==="
kubectl exec -it redis-cluster-0 -n ${NAMESPACE} -- \
redis-cli --cluster check redis-cluster-0.${SERVICE}.${NAMESPACE}.svc.cluster.local:6379

执行初始化:

chmod +x redis-cluster/init-cluster.sh
./redis-cluster/init-cluster.sh redis

3.2 槽位分配与节点握手

初始化成功后,你应该能看到类似以下输出:

>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
M: xxx... redis-cluster-0.redis-cluster.redis.svc.cluster.local:6379
S: yyy... redis-cluster-3.redis-cluster.redis.svc.cluster.local:6379
M: zzz... redis-cluster-1.redis-cluster.redis.svc.cluster.local:6379
S: aaa... redis-cluster-4.redis-cluster.redis.svc.cluster.local:6379
M: bbb... redis-cluster-2.redis-cluster.redis.svc.cluster.local:6379
S: ccc... redis-cluster-5.redis-cluster.redis.svc.cluster.local:6379
Can I set the above configuration? (type 'yes' to accept): yes
>>> Nodes configuration updated
>>> Cluster state ok

3.3 数据读写验证

# 写入数据
kubectl exec -it redis-cluster-0 -n redis -- \
redis-cli SET greeting "Hello from Redis Cluster"

# 读取数据(从任意节点)
kubectl exec -it redis-cluster-1 -n redis -- \
redis-cli GET greeting

# 查看集群信息
kubectl exec -it redis-cluster-0 -n redis -- \
redis-cli CLUSTER INFO

# 查看节点列表
kubectl exec -it redis-cluster-0 -n redis -- \
redis-cli CLUSTER NODES

3.4 故障转移测试

# 查看当前主从关系
kubectl exec -it redis-cluster-0 -n redis -- redis-cli CLUSTER NODES

# 模拟主节点故障(删除 redis-cluster-0)
kubectl delete pod redis-cluster-0 -n redis

# 等待 StatefulSet 重建 Pod
kubectl get pods -n redis -l app=redis-cluster -w

# 重建完成后,检查集群状态
# 对应的从节点应该已经提升为主节点
kubectl exec -it redis-cluster-1 -n redis -- redis-cli CLUSTER NODES

# 验证数据仍然可用
kubectl exec -it redis-cluster-1 -n redis -- redis-cli GET greeting
故障转移注意事项
  1. Redis Cluster 的故障转移由集群内部自动完成,不需要外部干预。
  2. 当原主节点恢复后,它会自动变为新主节点的从节点,不会抢回主节点角色。
  3. 在生产环境中,建议设置 cluster-node-timeout 为合理值(通常 5-15 秒),以平衡故障检测速度和误判率。

四、Operator 模式深度解析

4.1 Operator 的设计哲学

通过前面的实战,你可能已经发现一个问题:Redis Cluster 的初始化是手动执行的。StatefulSet 只负责创建 Pod,但集群的初始化、扩缩容、故障恢复等操作仍然需要人工介入。

这正是 Operator 模式要解决的核心问题。

Operator 的本质是将人类运维特定应用的知识编码为自动化软件。

CoreOS(现 Red Hat)在 2016 年提出了 Operator 模式的概念。其核心思想是:

  1. 人类运维人员知道如何部署、扩缩容、备份、恢复一个特定应用
  2. 将这些领域知识编码为 Kubernetes 控制器
  3. 控制器通过 CRD(Custom Resource Definition) 扩展 K8s API,引入应用特定的资源类型
  4. 通过 Reconciliation Loop 持续调谐实际状态与期望状态

简单来说,Operator = CRD + Controller + 特定领域知识

4.2 Operator 核心组件

一个完整的 Operator 由以下三个核心组件构成:

组件职责类比
CRD(Custom Resource Definition)定义自定义资源的 Schema(结构)数据库的表结构
ControllerWatch 资源变化,执行调谐逻辑数据库的触发器
Reconciliation Loop持续将实际状态趋近期望状态自动驾驶的反馈控制

4.3 开发框架对比

目前主流的 Operator 开发框架有三个:

特性controller-runtimeKubebuilderOperator SDK
定位底层库脚手架工具全功能 SDK
语言GoGoGo / Ansible / Helm
复杂度中等
灵活性
项目脚手架无(需手动搭建)内置 kubebuilder init内置 operator-sdk init
Webhook 支持手动注册自动生成脚手架自动生成脚手架
关系Kubebuilder 和 Operator SDK 的底层依赖基于 controller-runtime基于 controller-runtime
框架选择建议
  • 学习 Operator 原理:使用 controller-runtime,理解底层机制
  • 快速开发生产级 Operator:使用 Kubebuilder,脚手架完善、社区活跃
  • 非 Go 技术栈:使用 Operator SDK 的 Ansible/Helm 模式

本文的实战部分将使用 controller-runtime,帮助你理解 Operator 的核心原理。


五、实战 2:自定义 Redis Operator

5.1 项目初始化

# 创建项目目录
mkdir redis-operator && cd redis-operator

# 初始化 Go Module
go mod init github.com/rainlib/redis-operator

# 安装依赖
# controller-runtime v0.18.0 与 Kubernetes v1.34 API 兼容
go get sigs.k8s.io/controller-runtime@v0.18.0
go get k8s.io/api@v0.30.0
go get k8s.io/apimachinery@v0.30.0

依赖版本说明:controller-runtime v0.18.0 与 Kubernetes v1.34 API 完全兼容。详见官方文档:controller-runtime

5.2 CRD 定义

文件路径:api/v1alpha1/rediscluster_types.go

package v1alpha1

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// RedisClusterSpec 定义用户期望的集群状态
type RedisClusterSpec struct {
// 主节点数量(必须为奇数)
MasterCount int32 `json:"masterCount"`

// 每个主节点的从节点数量
ReplicasPerMaster int32 `json:"replicasPerMaster"`

// Redis 镜像
Image string `json:"image"`

// 资源请求与限制
Resources ResourceRequirements `json:"resources,omitempty"`
}

type ResourceRequirements struct {
CPURequest string `json:"cpuRequest,omitempty"`
MemoryRequest string `json:"memoryRequest,omitempty"`
CPULimit string `json:"cpuLimit,omitempty"`
MemoryLimit string `json:"memoryLimit,omitempty"`
}

// ClusterPhase 表示集群当前阶段
type ClusterPhase string

const (
ClusterPhaseCreating ClusterPhase = "Creating"
ClusterPhaseInitializing ClusterPhase = "Initializing"
ClusterPhaseRunning ClusterPhase = "Running"
ClusterPhaseScaling ClusterPhase = "Scaling"
ClusterPhaseFailed ClusterPhase = "Failed"
)

// RedisClusterStatus 反映集群的实际状态
type RedisClusterStatus struct {
// 集群当前阶段
Phase ClusterPhase `json:"phase,omitempty"`

// 各节点的就绪状态
ReadyNodes int32 `json:"readyNodes,omitempty"`

// 集群是否已初始化
ClusterInitialized bool `json:"clusterInitialized,omitempty"`

// 最近一次操作的消息
Message string `json:"message,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:shortName=rc

// RedisCluster 是自定义资源的顶层类型
type RedisCluster struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec RedisClusterSpec `json:"spec,omitempty"`
Status RedisClusterStatus `json:"status,omitempty"`
}

// +kubebuilder:object:root=true

// RedisClusterList 用于列表操作
type RedisClusterList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`

Items []RedisCluster `json:"items"`
}

func init() {
SchemeBuilder.Register(&RedisCluster{}, &RedisClusterList{})
}

5.3 CRD YAML 生成

API 版本说明:CRD 使用 apiextensions.k8s.io/v1,这是 Kubernetes v1.34 中的稳定版本。apiextensions.k8s.io/v1beta1 已在 v1.22 中移除,请确保使用 v1。详见官方文档:CustomResourceDefinition v1

文件路径:config/crd/redisclusters.rainlib.io.yaml

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: redisclusters.rainlib.io
spec:
group: rainlib.io
names:
kind: RedisCluster
listKind: RedisClusterList
plural: redisclusters
shortNames:
- rc
singular: rediscluster
scope: Namespaced
versions:
- name: v1alpha1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
required:
- masterCount
- replicasPerMaster
- image
properties:
masterCount:
type: integer
minimum: 1
description: 主节点数量
replicasPerMaster:
type: integer
minimum: 0
description: 每个主节点的从节点数量
image:
type: string
description: Redis 镜像地址
resources:
type: object
properties:
cpuRequest:
type: string
memoryRequest:
type: string
cpuLimit:
type: string
memoryLimit:
type: string
status:
type: object
properties:
phase:
type: string
enum:
- Creating
- Initializing
- Running
- Scaling
- Failed
readyNodes:
type: integer
clusterInitialized:
type: boolean
message:
type: string
subresources:
status: {}

5.4 Controller 实现

文件路径:controllers/rediscluster_controller.go

package controllers

import (
"context"
"fmt"
"time"

redisv1alpha1 "github.com/rainlib/redis-operator/api/v1alpha1"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/log"
)

const (
finalizerName = "rainlib.io/rediscluster-finalizer"
)

// RedisClusterReconciler 调谐 RedisCluster 资源
type RedisClusterReconciler struct {
client.Client
Scheme *runtime.Scheme
}

// +kubebuilder:rbac:groups=rainlib.io,resources=redisclusters,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=rainlib.io,resources=redisclusters/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=rainlib.io,resources=redisclusters/finalizers,verbs=update
// +kubebuilder:rbac:groups=apps,resources=statefulsets,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch
// +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch

func (r *RedisClusterReconciler) Reconcile(
ctx context.Context,
req ctrl.Request,
) (ctrl.Result, error) {
logger := log.FromContext(ctx)

// 1. 获取 RedisCluster 实例
var rc redisv1alpha1.RedisCluster
if err := r.Get(ctx, req.NamespacedName, &rc); err != nil {
if errors.IsNotFound(err) {
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
}

// 2. 处理 Finalizer(删除逻辑)
if !rc.DeletionTimestamp.IsZero() {
if controllerutil.ContainsFinalizer(&rc, finalizerName) {
logger.Info("执行清理逻辑")
// 这里可以添加资源清理逻辑(如外部存储清理)
controllerutil.RemoveFinalizer(&rc, finalizerName)
if err := r.Update(ctx, &rc); err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}

// 3. 添加 Finalizer
if !controllerutil.ContainsFinalizer(&rc, finalizerName) {
controllerutil.AddFinalizer(&rc, finalizerName)
if err := r.Update(ctx, &rc); err != nil {
return ctrl.Result{}, err
}
}

// 4. 创建/更新 Headless Service
if err := r.reconcileService(ctx, &rc); err != nil {
logger.Error(err, "调和 Service 失败")
r.updateStatus(ctx, &rc, redisv1alpha1.ClusterPhaseFailed, 0, false, err.Error())
return ctrl.Result{}, err
}

// 5. 创建/更新 StatefulSet
if err := r.reconcileStatefulSet(ctx, &rc); err != nil {
logger.Error(err, "调和 StatefulSet 失败")
r.updateStatus(ctx, &rc, redisv1alpha1.ClusterPhaseFailed, 0, false, err.Error())
return ctrl.Result{}, err
}

// 6. 检查 Pod 就绪状态
readyNodes, err := r.getReadyNodeCount(ctx, &rc)
if err != nil {
return ctrl.Result{}, err
}

totalNodes := rc.Spec.MasterCount * (1 + rc.Spec.ReplicasPerMaster)

if readyNodes < totalNodes {
logger.Info("等待所有节点就绪",
"ready", readyNodes, "total", totalNodes)
r.updateStatus(ctx, &rc, redisv1alpha1.ClusterPhaseCreating, readyNodes, false,
fmt.Sprintf("等待节点就绪 %d/%d", readyNodes, totalNodes))
return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
}

// 7. 初始化集群(如果尚未初始化)
if !rc.Status.ClusterInitialized {
logger.Info("开始初始化 Redis Cluster")
r.updateStatus(ctx, &rc, redisv1alpha1.ClusterPhaseInitializing, readyNodes, false,
"正在初始化集群")

if err := r.initializeCluster(ctx, &rc); err != nil {
logger.Error(err, "初始化集群失败")
r.updateStatus(ctx, &rc, redisv1alpha1.ClusterPhaseFailed, readyNodes, false, err.Error())
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
}

r.updateStatus(ctx, &rc, redisv1alpha1.ClusterPhaseRunning, readyNodes, true,
"集群已就绪")
}

return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
}

// reconcileService 创建或更新 Headless Service
func (r *RedisClusterReconciler) reconcileService(
ctx context.Context,
rc *redisv1alpha1.RedisCluster,
) error {
logger := log.FromContext(ctx)

desired := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: rc.Name,
Namespace: rc.Namespace,
},
Spec: corev1.ServiceSpec{
ClusterIP: "None",
Selector: map[string]string{
"app": rc.Name,
"managed-by": "redis-operator",
"redis-cluster": rc.Name,
},
Ports: []corev1.ServicePort{
{Name: "redis", Port: 6379, TargetPort: intstr.FromInt(6379)},
{Name: "bus", Port: 16379, TargetPort: intstr.FromInt(16379)},
},
},
}

// 设置 OwnerReference
if err := controllerutil.SetControllerReference(rc, desired, r.Scheme); err != nil {
return err
}

var existing corev1.Service
err := r.Get(ctx, types.NamespacedName{Name: rc.Name, Namespace: rc.Namespace}, &existing)

if errors.IsNotFound(err) {
logger.Info("创建 Headless Service")
return r.Create(ctx, desired)
} else if err != nil {
return err
}

// Service 已存在,更新
existing.Spec.Selector = desired.Spec.Selector
existing.Spec.Ports = desired.Spec.Ports
return r.Update(ctx, &existing)
}

// reconcileStatefulSet 创建或更新 StatefulSet
func (r *RedisClusterReconciler) reconcileStatefulSet(
ctx context.Context,
rc *redisv1alpha1.RedisCluster,
) error {
logger := log.FromContext(ctx)

totalReplicas := rc.Spec.MasterCount * (1 + rc.Spec.ReplicasPerMaster)

cpuReq := rc.Spec.Resources.CPURequest
memReq := rc.Spec.Resources.MemoryRequest
cpuLim := rc.Spec.Resources.CPULimit
memLim := rc.Spec.Resources.MemoryLimit

if cpuReq == "" {
cpuReq = "100m"
}
if memReq == "" {
memReq = "256Mi"
}
if cpuLim == "" {
cpuLim = "500m"
}
if memLim == "" {
memLim = "512Mi"
}

desired := &appsv1.StatefulSet{
ObjectMeta: metav1.ObjectMeta{
Name: rc.Name,
Namespace: rc.Namespace,
},
Spec: appsv1.StatefulSetSpec{
ServiceName: rc.Name,
Replicas: &totalReplicas,
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": rc.Name,
"managed-by": "redis-operator",
"redis-cluster": rc.Name,
},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"app": rc.Name,
"managed-by": "redis-operator",
"redis-cluster": rc.Name,
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "redis",
Image: rc.Spec.Image,
Command: []string{
"/bin/sh", "-c",
fmt.Sprintf(`
redis-server \
--cluster-enabled yes \
--cluster-config-file /data/nodes.conf \
--cluster-node-timeout 5000 \
--appendonly yes \
--bind 0.0.0.0 \
--port 6379 \
--cluster-announce-ip $(hostname).%s.%s.svc.cluster.local \
--cluster-announce-port 6379 \
--cluster-announce-bus-port 16379
`, rc.Name, rc.Namespace),
},
Ports: []corev1.ContainerPort{
{ContainerPort: 6379, Name: "redis"},
{ContainerPort: 16379, Name: "bus"},
},
VolumeMounts: []corev1.VolumeMount{
{Name: "data", MountPath: "/data"},
},
LivenessProbe: &corev1.Probe{
Exec: &corev1.ExecAction{
Command: []string{"redis-cli", "ping"},
},
InitialDelaySeconds: 15,
PeriodSeconds: 5,
},
ReadinessProbe: &corev1.Probe{
Exec: &corev1.ExecAction{
Command: []string{"redis-cli", "ping"},
},
InitialDelaySeconds: 5,
PeriodSeconds: 3,
},
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse(cpuReq),
corev1.ResourceMemory: resource.MustParse(memReq),
},
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse(cpuLim),
corev1.ResourceMemory: resource.MustParse(memLim),
},
},
},
},
},
},
VolumeClaimTemplates: []corev1.PersistentVolumeClaim{
{
ObjectMeta: metav1.ObjectMeta{
Name: "data",
},
Spec: corev1.PersistentVolumeClaimSpec{
AccessModes: []corev1.PersistentVolumeAccessMode{
corev1.ReadWriteOnce,
},
Resources: corev1.VolumeResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceStorage: resource.MustParse("1Gi"),
},
},
},
},
},
},
}

if err := controllerutil.SetControllerReference(rc, desired, r.Scheme); err != nil {
return err
}

var existing appsv1.StatefulSet
err := r.Get(ctx, types.NamespacedName{Name: rc.Name, Namespace: rc.Namespace}, &existing)

if errors.IsNotFound(err) {
logger.Info("创建 StatefulSet", "replicas", totalReplicas)
return r.Create(ctx, desired)
} else if err != nil {
return err
}

// StatefulSet 已存在,检查是否需要更新
if *existing.Spec.Replicas != totalReplicas || existing.Spec.Template.Spec.Containers[0].Image != rc.Spec.Image {
logger.Info("更新 StatefulSet")
existing.Spec.Replicas = &totalReplicas
existing.Spec.Template.Spec.Containers[0].Image = rc.Spec.Image
return r.Update(ctx, &existing)
}

return nil
}

// getReadyNodeCount 统计就绪的 Pod 数量
func (r *RedisClusterReconciler) getReadyNodeCount(
ctx context.Context,
rc *redisv1alpha1.RedisCluster,
) (int32, error) {
var podList corev1.PodList
if err := r.List(ctx, &podList,
client.InNamespace(rc.Namespace),
client.MatchingLabels{
"app": rc.Name,
"redis-cluster": rc.Name,
},
); err != nil {
return 0, err
}

var ready int32
for _, pod := range podList.Items {
for _, cond := range pod.Status.Conditions {
if cond.Type == corev1.PodReady && cond.Status == corev1.ConditionTrue {
ready++
break
}
}
}
return ready, nil
}

// initializeCluster 执行 Redis Cluster 初始化
func (r *RedisClusterReconciler) initializeCluster(
ctx context.Context,
rc *redisv1alpha1.RedisCluster,
) error {
logger := log.FromContext(ctx)

// 在实际生产中,这里会通过 exec 进入 Pod 执行 redis-cli --cluster create
// 为简化示例,这里记录初始化事件
logger.Info("Redis Cluster 初始化完成",
"masters", rc.Spec.MasterCount,
"replicas", rc.Spec.ReplicasPerMaster)

// 生产环境实现示例(伪代码):
// 1. 收集所有 Pod 的 FQDN
// 2. exec 进入第一个 Pod 执行 redis-cli --cluster create
// 3. 验证集群状态

return nil
}

// updateStatus 更新 RedisCluster 的状态子资源
func (r *RedisClusterReconciler) updateStatus(
ctx context.Context,
rc *redisv1alpha1.RedisCluster,
phase redisv1alpha1.ClusterPhase,
readyNodes int32,
initialized bool,
message string,
) {
rc.Status.Phase = phase
rc.Status.ReadyNodes = readyNodes
rc.Status.ClusterInitialized = initialized
rc.Status.Message = message

if err := r.Status().Update(ctx, rc); err != nil {
log.FromContext(ctx).Error(err, "更新状态失败")
}
}

// SetupWithManager 注册 Controller
func (r *RedisClusterReconciler) SetupWithManager(
mgr ctrl.Manager,
) error {
return ctrl.NewControllerManagedBy(mgr).
For(&redisv1alpha1.RedisCluster{}).
Owns(&appsv1.StatefulSet{}).
Owns(&corev1.Service{}).
Complete(r)
}
注意

上面的 Controller 代码中使用了 intstr.FromInt,需要在 import 中添加 "k8s.io/apimachinery/pkg/util/intstr"

5.5 Reconciliation Loop 工作流程

下图展示了 Operator 的 Reconciliation Loop 在不同场景下的工作流程:

5.6 主入口

文件路径:main.go

package main

import (
"os"

redisv1alpha1 "github.com/rainlib/redis-operator/api/v1alpha1"
"github.com/rainlib/redis-operator/controllers"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
)

var (
scheme = runtime.NewScheme()
)

func init() {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(redisv1alpha1.AddToScheme(scheme))
}

func main() {
ctrl.SetLogger(zap.New(zap.UseDevMode(true)))

mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
Metrics: metricsserver.Options{
BindAddress: ":8080",
},
HealthProbeBindAddress: ":8081",
LeaderElection: true,
LeaderElectionID: "redis-operator.rainlib.io",
})
if err != nil {
setupLog.Error(err, "无法创建 Manager")
os.Exit(1)
}

if err := (&controllers.RedisClusterReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "无法创建 Controller")
os.Exit(1)
}

if err := mgr.AddHealthzCheck("healthz", healthz.PingHealthz); err != nil {
setupLog.Error(err, "无法设置健康检查")
os.Exit(1)
}

setupLog.Info("启动 Manager")
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
setupLog.Error(err, "Manager 运行错误")
os.Exit(1)
}
}

六、Operator 部署与测试

6.1 RBAC 配置

API 版本说明:RBAC 资源使用 rbac.authorization.k8s.io/v1,这是 Kubernetes v1.34 中的稳定版本。详见官方文档:RBAC v1

文件路径:config/rbac/rbac.yaml

---
apiVersion: v1
kind: ServiceAccount
metadata:
name: redis-operator
namespace: redis-operator

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: redis-operator-role
rules:
# 管理 RedisCluster CRD
- apiGroups: ["rainlib.io"]
resources: ["redisclusters"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["rainlib.io"]
resources: ["redisclusters/status"]
verbs: ["get", "update", "patch"]
- apiGroups: ["rainlib.io"]
resources: ["redisclusters/finalizers"]
verbs: ["update"]
# 管理 StatefulSet
- apiGroups: ["apps"]
resources: ["statefulsets"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# 管理 Service
- apiGroups: [""]
resources: ["services"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# 读取 Pod 状态
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
# 创建 Event
- apiGroups: [""]
resources: ["events"]
verbs: ["create", "patch"]

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: redis-operator-rolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: redis-operator-role
subjects:
- kind: ServiceAccount
name: redis-operator
namespace: redis-operator

6.2 部署 Operator

API 版本说明:Deployment 使用 apps/v1,ServiceAccount 使用 v1,均为 Kubernetes v1.34 稳定版本。详见官方文档:Deployment v1ServiceAccount v1

文件路径:config/deployment/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
name: redis-operator
namespace: redis-operator
labels:
app: redis-operator
spec:
replicas: 1
selector:
matchLabels:
app: redis-operator
template:
metadata:
labels:
app: redis-operator
spec:
serviceAccountName: redis-operator
containers:
- name: redis-operator
image: rainlib/redis-operator:latest
args:
- --leader-elect
ports:
- containerPort: 8080
name: metrics
- containerPort: 9443
name: webhook
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
livenessProbe:
httpGet:
path: /healthz
port: 8081
initialDelaySeconds: 15
periodSeconds: 20
readinessProbe:
httpGet:
path: /readyz
port: 8081
initialDelaySeconds: 5
periodSeconds: 10
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true

部署步骤:

# 1. 创建命名空间
kubectl create namespace redis-operator

# 2. 安装 CRD
kubectl apply -f config/crd/redisclusters.rainlib.io.yaml

# 3. 安装 RBAC
kubectl apply -f config/rbac/rbac.yaml

# 4. 部署 Operator
kubectl apply -f config/deployment/deployment.yaml

# 5. 验证 Operator 运行状态
kubectl get pods -n redis-operator
kubectl logs -n redis-operator -l app=redis-operator

6.3 创建 RedisCluster 实例

文件路径:config/samples/rediscluster-sample.yaml

apiVersion: rainlib.io/v1alpha1
kind: RedisCluster
metadata:
name: my-redis
namespace: redis
spec:
masterCount: 3
replicasPerMaster: 1
image: redis:7.2.4
resources:
cpuRequest: "200m"
memoryRequest: "512Mi"
cpuLimit: "1000m"
memoryLimit: "1Gi"
# 创建命名空间
kubectl create namespace redis

# 创建 RedisCluster 实例
kubectl apply -f config/samples/rediscluster-sample.yaml

# 观察 Operator 行为
kubectl get rediscluster -n redis -w

# 查看 Operator 日志
kubectl logs -n redis-operator -l app=redis-operator -f

# 查看创建的资源
kubectl get statefulset,service,pod -n redis

6.4 验证 Operator 行为

自动扩缩容测试:

# 修改 replicasPerMaster 从 1 到 2
kubectl patch rediscluster my-redis -n redis --type=merge \
-p '{"spec":{"replicasPerMaster":2}}'

# 观察 Operator 自动更新 StatefulSet
kubectl get pods -n redis -w
kubectl logs -n redis-operator -l app=redis-operator -f

故障恢复测试:

# 模拟 Pod 故障
kubectl delete pod my-redis-0 -n redis

# 观察 StatefulSet 自动重建 Pod
kubectl get pods -n redis -w

# 验证 RedisCluster 状态
kubectl get rediscluster my-redis -n redis -o yaml

七、高级 Operator 模式

7.1 Finalizer 机制

Finalizer 是 Kubernetes 中用于防止资源被意外删除的机制。当资源带有 Finalizer 时,K8s 不会立即删除该资源,而是先设置 deletionTimestamp,等待 Controller 执行清理逻辑后再移除 Finalizer,最终完成删除。

// 处理删除逻辑
if !rc.DeletionTimestamp.IsZero() {
if controllerutil.ContainsFinalizer(&rc, finalizerName) {
// 1. 执行自定义清理逻辑
if err := r.cleanupExternalResources(ctx, &rc); err != nil {
return ctrl.Result{}, err
}
// 2. 移除 Finalizer,允许 K8s 完成删除
controllerutil.RemoveFinalizer(&rc, finalizerName)
if err := r.Update(ctx, &rc); err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
生产环境注意事项

Finalizer 中的清理逻辑必须保证幂等性(Idempotent),因为 Reconcile 可能会被多次调用。如果清理逻辑失败且不移除 Finalizer,资源将一直处于"正在删除"状态,成为僵尸资源。

7.2 OwnerReference 与级联删除

通过 controllerutil.SetControllerReference 设置的 OwnerReference 实现了级联删除:当 RedisCluster 被删除时,其创建的所有 StatefulSet、Service 等子资源会自动被 K8s 回收。

// 设置 OwnerReference
if err := controllerutil.SetControllerReference(rc, desired, r.Scheme); err != nil {
return err
}

OwnerReference 的传播策略有两种:

策略行为适用场景
Background(默认)立即删除父资源,后台异步删除子资源大多数场景
Foreground先删除所有子资源,再删除父资源需要确保子资源被正确清理的场景

7.3 Webhook(Admission / Mutating)

Webhook 允许 Operator 在资源创建或更新时进行拦截和修改,实现更精细的控制。

API 版本说明:ValidatingWebhookConfiguration 和 MutatingWebhookConfiguration 使用 admissionregistration.k8s.io/v1,这是 Kubernetes v1.34 中的稳定版本。详见官方文档:ValidatingWebhookConfigurationMutatingWebhookConfiguration

Validating Webhook(验证 webhook):拒绝不合法的请求。

// 验证 masterCount 必须为奇数
func (r *RedisCluster) ValidateCreate() (admission.Warnings, error) {
if r.Spec.MasterCount%2 == 0 {
return nil, fmt.Errorf("masterCount must be an odd number, got %d", r.Spec.MasterCount)
}
return nil, nil
}

func (r *RedisCluster) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
oldRC := old.(*RedisCluster)
if oldRC.Spec.MasterCount != r.Spec.MasterCount {
return nil, fmt.Errorf("masterCount is immutable after creation")
}
return nil, nil
}

Mutating Webhook(变更 webhook):自动修改请求内容。

// 自动设置默认值
func (r *RedisCluster) Default() {
if r.Spec.Image == "" {
r.Spec.Image = "redis:7.2.4"
}
if r.Spec.ReplicasPerMaster == 0 {
r.Spec.ReplicasPerMaster = 1
}
}

7.4 Leader Election(高可用)

当 Operator 部署多个副本时,Leader Election 确保只有一个实例在执行 Reconcile,其余实例处于 Standby 状态。

mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
LeaderElection: true, // 启用 Leader Election
LeaderElectionID: "redis-operator.rainlib.io", // Leader Election 的标识
LeaderElectionNamespace: "redis-operator", // 选举的命名空间
LeaseDuration: &metav1.Duration{Seconds: 15}, // 租约时长
RenewDeadline: &metav1.Duration{Seconds: 10}, // 续约截止时间
RetryPeriod: &metav1.Duration{Seconds: 2}, // 重试间隔
})
Leader Election 原理

Leader Election 基于 K8s 的 Lease(租约)资源实现。Manager 启动时会尝试创建一个 Lease 对象,创建成功的实例成为 Leader 并定期续约。如果 Leader 宕机无法续约,其他 Standby 实例会竞争成为新 Leader。


八、生产环境最佳实践

8.1 Operator 开发规范

规范说明
最小权限原则RBAC 仅授予必要的权限,避免使用 cluster-admin
幂等 Reconcile所有调谐逻辑必须幂等,确保重复执行结果一致
状态子资源分离使用 Status 子资源记录实际状态,与 Spec 分离
事件记录关键操作必须通过 EventRecorder 记录事件,便于审计和排查
错误处理区分暂时性错误(Requeue)和永久性错误(不 Requeue)
日志规范使用结构化日志,包含关键上下文(资源名、命名空间等)

8.2 监控与日志

// 使用 Prometheus 指标暴露 Operator 运行状态
import (
"sigs.k8s.io/controller-runtime/pkg/metrics"
"github.com/prometheus/client_golang/prometheus"
)

var (
reconcileTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "redis_operator_reconcile_total",
Help: "Total number of reconciliation operations",
},
[]string{"namespace", "name"},
)
reconcileErrors = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "redis_operator_reconcile_errors_total",
Help: "Total number of reconciliation errors",
},
[]string{"namespace", "name"},
)
)

func init() {
metrics.Registry.MustRegister(reconcileTotal, reconcileErrors)
}

推荐的监控大盘指标:

  • Reconcile 成功/失败次数
  • Reconcile 耗时分布
  • 每个 RedisCluster 的 Phase 分布
  • Redis 节点的内存、连接数、命令处理量
  • PVC 使用率

8.3 版本升级策略

API 版本说明:CRD 版本管理使用 apiextensions.k8s.io/v1,支持多版本共存、版本转换和弃用标记。详见官方文档:CRD Versioning

# 使用 CRD 版本管理 API 演进
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: redisclusters.rainlib.io
spec:
versions:
- name: v1alpha1
served: true # 仍然提供服务
storage: false # 不再作为存储版本
deprecated: true # 标记为已弃用
deprecationWarning: "rainlib.io/v1alpha1 RedisCluster is deprecated; use rainlib.io/v1"
- name: v1
served: true
storage: true # 作为存储版本
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
masterCount:
type: integer
minimum: 1
# v1 新增字段
persistence:
type: object
properties:
enabled:
type: boolean
storageClass:
type: string
size:
type: string
CRD 版本升级注意事项
  1. 不可删除存储版本:一旦某个版本被标记为 storage: true,就不能直接删除该版本,必须先创建新版本并迁移数据。
  2. Webhook 版本转换:如果需要在不同 CRD 版本间自动转换,需要实现 Conversion Webhook。
  3. 向后兼容:新增字段应该有默认值,确保旧版本 CR 在新版本 Controller 下仍能正常工作。

九、本章小结与系列总结

核心知识回顾

本文从有状态应用的核心挑战出发,系统性地讲解了 StatefulSet 和 Operator 两个关键概念:

StatefulSet 解决的问题:

  • 稳定的网络标识(Headless Service + 固定 DNS)
  • 持久的存储(VolumeClaimTemplate + PVC)
  • 有序的部署与扩缩容(顺序创建/逆序删除)

Operator 解决的问题:

  • 将运维知识编码为自动化控制器
  • 通过 CRD 扩展 K8s API,引入应用特定的资源类型
  • 通过 Reconciliation Loop 持续调谐实际状态与期望状态
  • 自动处理集群初始化、扩缩容、故障恢复等复杂操作

Operator 高级模式:

  • Finalizer:确保资源删除前的清理逻辑被执行
  • OwnerReference:实现级联删除,避免资源泄漏
  • Webhook:在 API 层面进行验证和变更
  • Leader Election:支持多副本高可用部署

Kubernetes 全景解析系列总结

至此,Kubernetes 全景解析系列的核心内容已经覆盖完毕。让我们回顾整个系列的知识体系:

章节主题核心收获
(0)架构设计与核心概念理解 K8s 的设计哲学与控制面/数据面架构
(1)工作负载管理掌握 Pod/Deployment/StatefulSet/DaemonSet 的使用场景
(2)网络模型理解 CNI、Service、Ingress 的工作原理
(3)存储体系与配置管理掌握 PV/PVC/StorageClass/ConfigMap/Secret
(4)调度策略理解节点选择、亲和性、污点与容忍
(5)安全机制掌握 RBAC、NetworkPolicy、PodSecurity
(6)Helm 包管理学会使用 Helm 打包和分发应用
(7)有状态应用与 Operator掌握 StatefulSet 和 Operator 开发

Kubernetes 的学习是一个持续深化的过程。掌握了本系列的内容,你已经具备了在生产和开发环境中使用 K8s 的核心能力。下一步建议:

  1. 深入实践:选择一个真实的有状态应用(如 PostgreSQL、Kafka),尝试为其编写 Operator
  2. 参与社区:关注 CNCF 生态,学习优秀的开源 Operator 实现(如 Prometheus Operator、Argo CD Operator)
  3. 探索高级主题:Service Mesh(Istio/Linkerd)、Serverless(Knative)、GitOps(Argo CD/Flux)等

"Kubernetes 不是终点,而是起点。理解了它的设计哲学,你就能更好地理解整个云原生生态。"


系列导航:

Kubernetes 全景解析 (6):生产级微服务架构实战

· 阅读需 38 分钟
Rainy
雨落无声,代码成诗 —— 致力于技术与艺术的极致平衡

"纸上得来终觉浅,绝知此事要躬行。"

在前面的系列文章中,我们系统学习了 K8s 的架构设计、工作负载管理、网络模型、存储体系与配置管理。现在是时候将这些知识串联起来,完成一次从零到生产的完整实战演练。

本文将以一个电商微服务系统为蓝本,手把手带你完成以下全流程:

  • 多服务编排与部署
  • Ingress 七层路由与 TLS 终止
  • HPA 弹性伸缩与高可用保障
  • 健康检查与优雅停机
  • Prometheus + Grafana 监控体系
  • ArgoCD GitOps 持续交付

所有 YAML 配置均基于 Kubernetes v1.34(Of Wind & Will)官方 API 验证,可直接应用于你的集群。

API 版本说明

本文所有 YAML 配置使用的 API 版本均经过 Kubernetes v1.34 官方文档校验:

资源类型apiVersion官方文档
Deployment / StatefulSetapps/v1Workload Resources
Servicev1Service Resources
Ingress / IngressClassnetworking.k8s.io/v1Ingress
HorizontalPodAutoscalerautoscaling/v2HPA v2
PodDisruptionBudgetpolicy/v1PDB
ResourceQuota / LimitRangev1Config and Storage Resources
RBACrbac.authorization.k8s.io/v1Authorization Resources
ServiceMonitormonitoring.coreos.com/v1Prometheus Operator
ArgoCD Applicationargoproj.io/v1alpha1ArgoCD CRDs
StorageClassstorage.k8s.io/v1Storage Resources

一、实战场景概述

1.1 电商微服务架构设计

我们以一个典型的电商系统为例,将其拆分为以下五个核心微服务:

服务职责技术栈端口
API Gateway统一入口、路由转发、限流熔断APISIX / Nginx80/443
用户服务注册、登录、用户信息管理Go (Gin)8080
商品服务商品 CRUD、分类管理、搜索Java (Spring Boot)8081
订单服务下单、支付回调、订单查询Node.js (Express)8082
支付服务支付对接、退款、对账Go (Gin)8083

底层依赖两个有状态服务:

服务职责端口
PostgreSQL关系型数据库(用户、订单)5432
Redis缓存、会话管理、分布式锁6379

1.2 微服务架构拓扑

1.3 技术栈选择

层级技术选型选型理由
网关层APISIX高性能、支持 gRPC 转发、插件生态丰富
服务层Go + Java + Node.js模拟真实多语言微服务环境
数据层PostgreSQL 16 + Redis 7成熟稳定、社区活跃
监控层Prometheus + Grafana云原生监控事实标准
日志层Fluent Bit轻量级、资源占用低
部署层ArgoCDGitOps 声明式持续交付

二、基础设施准备

2.1 命名空间与资源配额

生产环境中,不同团队的服务应该隔离在不同的命名空间中,并通过 ResourceQuota 限制资源使用。

k8s/00-namespace/namespace.yaml
---
apiVersion: v1
kind: Namespace
metadata:
name: microservices
labels:
app.kubernetes.io/part-of: ecommerce
app.kubernetes.io/managed-by: argocd
---
apiVersion: v1
kind: Namespace
metadata:
name: data
labels:
app.kubernetes.io/part-of: ecommerce
app.kubernetes.io/managed-by: argocd
---
apiVersion: v1
kind: Namespace
metadata:
name: gateway
labels:
app.kubernetes.io/part-of: ecommerce
app.kubernetes.io/managed-by: argocd
k8s/00-namespace/resource-quota.yaml
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: microservices-quota
namespace: microservices
spec:
hard:
requests.cpu: "10"
requests.memory: 20Gi
limits.cpu: "20"
limits.memory: 40Gi
pods: "50"
services: "20"
persistentvolumeclaims: "10"
---
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: microservices
spec:
limits:
- default:
cpu: "500m"
memory: "512Mi"
defaultRequest:
cpu: "100m"
memory: "128Mi"
max:
cpu: "2"
memory: "2Gi"
min:
cpu: "50m"
memory: "64Mi"
type: Container
生产环境注意事项

ResourceQuota 的值应根据集群总容量和业务优先级进行合理分配。建议预留 20% 的资源缓冲,避免某个命名空间的突发流量影响其他业务。LimitRange 确保即使开发者忘记设置资源限制,容器也不会无限制地消耗节点资源。

2.2 ConfigMap 与 Secret 配置管理

将配置从镜像中分离出来,是 12-Factor App 的核心原则之一。

k8s/01-config/configmap.yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: microservices
data:
# 数据库连接配置
DB_HOST: "postgresql.data.svc.cluster.local"
DB_PORT: "5432"
DB_NAME: "ecommerce"
DB_POOL_SIZE: "20"
DB_CONNECTION_TIMEOUT: "30"

# Redis 连接配置
REDIS_HOST: "redis.data.svc.cluster.local"
REDIS_PORT: "6379"
REDIS_DB: "0"
REDIS_POOL_SIZE: "50"

# 日志配置
LOG_LEVEL: "info"
LOG_FORMAT: "json"

# 服务间调用超时
SERVICE_TIMEOUT: "10s"
SERVICE_RETRY: "3"
---
apiVersion: v1
kind: ConfigMap
metadata:
name: gateway-config
namespace: gateway
data:
# 网关路由配置
UPSTREAM_USER: "user-service.microservices.svc.cluster.local:8080"
UPSTREAM_PRODUCT: "product-service.microservices.svc.cluster.local:8081"
UPSTREAM_ORDER: "order-service.microservices.svc.cluster.local:8082"
UPSTREAM_PAYMENT: "payment-service.microservices.svc.cluster.local:8083"
k8s/01-config/secret.yaml
---
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: microservices
type: Opaque
stringData:
DB_USERNAME: "ecommerce_app"
DB_PASSWORD: "S3cureP@ssw0rd!2026"
---
apiVersion: v1
kind: Secret
metadata:
name: redis-credentials
namespace: microservices
type: Opaque
stringData:
REDIS_PASSWORD: "R3disS3cret!2026"
---
apiVersion: v1
kind: Secret
metadata:
name: tls-secret
namespace: gateway
type: kubernetes.io/tls
stringData:
tls.crt: |
-----BEGIN CERTIFICATE-----
# 此处替换为你的 TLS 证书
-----END CERTIFICATE-----
tls.key: |
-----BEGIN PRIVATE KEY-----
# 此处替换为你的 TLS 私钥
-----END PRIVATE KEY-----
安全警告

切勿将 Secret 明文提交到 Git 仓库! 生产环境应使用以下方案之一:

  • Sealed Secrets(Bitn Labs):加密后可安全提交 Git
  • External Secrets Operator:从 AWS Secrets Manager / HashiCorp Vault 同步
  • SOPS(Mozilla):基于 GPG/KMS 的加密工具

本文示例中的 stringData 仅用于演示,生产环境请务必使用加密方案。

2.3 PV/PVC 持久化存储规划

k8s/02-storage/storageclass.yaml
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: kubernetes.io/aws-ebs # 根据云厂商调整
parameters:
type: gp3
fsType: ext4
iopsPerGB: "50"
allowVolumeExpansion: true
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: standard-hdd
provisioner: kubernetes.io/aws-ebs
parameters:
type: gp3
fsType: ext4
allowVolumeExpansion: true
reclaimPolicy: Delete
volumeBindingMode: Immediate
k8s/02-storage/pvc.yaml
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgresql-data
namespace: data
spec:
storageClassName: fast-ssd
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: redis-data
namespace: data
spec:
storageClassName: fast-ssd
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi

三、核心服务部署

3.1 API Gateway 部署

网关是整个系统的统一入口,负责路由转发、限流、熔断和认证。

k8s/03-services/gateway-deployment.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-gateway
namespace: gateway
labels:
app: api-gateway
version: v1
spec:
replicas: 3
selector:
matchLabels:
app: api-gateway
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: api-gateway
version: v1
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9090"
prometheus.io/path: "/metrics"
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: api-gateway
topologyKey: kubernetes.io/hostname
terminationGracePeriodSeconds: 60
containers:
- name: apisix
image: apache/apisix:3.9.0-debian
ports:
- name: http
containerPort: 9080
protocol: TCP
- name: https
containerPort: 9443
protocol: TCP
- name: metrics
containerPort: 9090
protocol: TCP
envFrom:
- configMapRef:
name: gateway-config
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: "1"
memory: 512Mi
livenessProbe:
httpGet:
path: /healthz
port: 9090
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /healthz
port: 9090
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15"]
---
apiVersion: v1
kind: Service
metadata:
name: api-gateway
namespace: gateway
labels:
app: api-gateway
spec:
type: ClusterIP
ports:
- name: http
port: 80
targetPort: 9080
protocol: TCP
- name: https
port: 443
targetPort: 9443
protocol: TCP
selector:
app: api-gateway

3.2 用户服务部署

k8s/03-services/user-service.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
namespace: microservices
labels:
app: user-service
version: v1
spec:
replicas: 3
selector:
matchLabels:
app: user-service
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: user-service
version: v1
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/metrics"
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: user-service
topologyKey: kubernetes.io/hostname
terminationGracePeriodSeconds: 30
containers:
- name: user-service
image: registry.example.com/ecommerce/user-service:v1.0.0
ports:
- name: http
containerPort: 8080
protocol: TCP
- name: grpc
containerPort: 9090
protocol: TCP
env:
- name: SERVICE_NAME
value: "user-service"
- name: SERVICE_PORT
value: "8080"
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: DB_HOST
- name: DB_PORT
valueFrom:
configMapKeyRef:
name: app-config
key: DB_PORT
- name: DB_NAME
valueFrom:
configMapKeyRef:
name: app-config
key: DB_NAME
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_USERNAME
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_PASSWORD
- name: REDIS_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: REDIS_HOST
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: redis-credentials
key: REDIS_PASSWORD
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: "1"
memory: 512Mi
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 15
periodSeconds: 15
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /readyz
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 10"]
---
apiVersion: v1
kind: Service
metadata:
name: user-service
namespace: microservices
labels:
app: user-service
spec:
type: ClusterIP
ports:
- name: http
port: 8080
targetPort: 8080
protocol: TCP
- name: grpc
port: 9090
targetPort: 9090
protocol: TCP
selector:
app: user-service

3.3 订单服务部署

k8s/03-services/order-service.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
namespace: microservices
labels:
app: order-service
version: v1
spec:
replicas: 3
selector:
matchLabels:
app: order-service
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: order-service
version: v1
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8082"
prometheus.io/path: "/metrics"
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: order-service
topologyKey: kubernetes.io/hostname
terminationGracePeriodSeconds: 60
containers:
- name: order-service
image: registry.example.com/ecommerce/order-service:v1.0.0
ports:
- name: http
containerPort: 8082
protocol: TCP
env:
- name: SERVICE_NAME
value: "order-service"
- name: SERVICE_PORT
value: "8082"
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: DB_HOST
- name: DB_PORT
valueFrom:
configMapKeyRef:
name: app-config
key: DB_PORT
- name: DB_NAME
valueFrom:
configMapKeyRef:
name: app-config
key: DB_NAME
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_USERNAME
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_PASSWORD
- name: REDIS_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: REDIS_HOST
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: redis-credentials
key: REDIS_PASSWORD
- name: USER_SERVICE_URL
value: "http://user-service.microservices.svc.cluster.local:8080"
- name: PRODUCT_SERVICE_URL
value: "http://product-service.microservices.svc.cluster.local:8081"
- name: PAYMENT_SERVICE_URL
value: "http://payment-service.microservices.svc.cluster.local:8083"
resources:
requests:
cpu: 300m
memory: 384Mi
limits:
cpu: "1"
memory: 768Mi
livenessProbe:
httpGet:
path: /healthz
port: 8082
initialDelaySeconds: 20
periodSeconds: 15
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /readyz
port: 8082
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15"]
---
apiVersion: v1
kind: Service
metadata:
name: order-service
namespace: microservices
labels:
app: order-service
spec:
type: ClusterIP
ports:
- name: http
port: 8082
targetPort: 8082
protocol: TCP
selector:
app: order-service

3.4 商品服务部署

k8s/03-services/product-service.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: product-service
namespace: microservices
labels:
app: product-service
version: v1
spec:
replicas: 3
selector:
matchLabels:
app: product-service
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: product-service
version: v1
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8081"
prometheus.io/path: "/metrics"
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: product-service
topologyKey: kubernetes.io/hostname
terminationGracePeriodSeconds: 30
containers:
- name: product-service
image: registry.example.com/ecommerce/product-service:v1.0.0
ports:
- name: http
containerPort: 8081
protocol: TCP
env:
- name: SERVICE_NAME
value: "product-service"
- name: SERVICE_PORT
value: "8081"
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: DB_HOST
- name: DB_PORT
valueFrom:
configMapKeyRef:
name: app-config
key: DB_PORT
- name: DB_NAME
valueFrom:
configMapKeyRef:
name: app-config
key: DB_NAME
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_USERNAME
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_PASSWORD
- name: REDIS_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: REDIS_HOST
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: redis-credentials
key: REDIS_PASSWORD
resources:
requests:
cpu: 300m
memory: 512Mi
limits:
cpu: "2"
memory: 1Gi
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8081
initialDelaySeconds: 30
periodSeconds: 15
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8081
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 10"]
---
apiVersion: v1
kind: Service
metadata:
name: product-service
namespace: microservices
labels:
app: product-service
spec:
type: ClusterIP
ports:
- name: http
port: 8081
targetPort: 8081
protocol: TCP
selector:
app: product-service

3.5 支付服务部署

k8s/03-services/payment-service.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-service
namespace: microservices
labels:
app: payment-service
version: v1
spec:
replicas: 2
selector:
matchLabels:
app: payment-service
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: payment-service
version: v1
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8083"
prometheus.io/path: "/metrics"
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: payment-service
topologyKey: kubernetes.io/hostname
terminationGracePeriodSeconds: 60
containers:
- name: payment-service
image: registry.example.com/ecommerce/payment-service:v1.0.0
ports:
- name: http
containerPort: 8083
protocol: TCP
env:
- name: SERVICE_NAME
value: "payment-service"
- name: SERVICE_PORT
value: "8083"
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: DB_HOST
- name: DB_PORT
valueFrom:
configMapKeyRef:
name: app-config
key: DB_PORT
- name: DB_NAME
valueFrom:
configMapKeyRef:
name: app-config
key: DB_NAME
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_USERNAME
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_PASSWORD
- name: PAYMENT_GATEWAY_KEY
valueFrom:
secretKeyRef:
name: payment-secret
key: GATEWAY_API_KEY
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: "1"
memory: 512Mi
livenessProbe:
httpGet:
path: /healthz
port: 8083
initialDelaySeconds: 15
periodSeconds: 15
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /readyz
port: 8083
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15"]
---
apiVersion: v1
kind: Service
metadata:
name: payment-service
namespace: microservices
labels:
app: payment-service
spec:
type: ClusterIP
ports:
- name: http
port: 8083
targetPort: 8083
protocol: TCP
selector:
app: payment-service

3.6 数据库部署(StatefulSet)

有状态服务使用 StatefulSet 部署,确保稳定的网络标识和持久化存储。

k8s/03-services/postgresql-statefulset.yaml
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgresql
namespace: data
labels:
app: postgresql
spec:
serviceName: postgresql-headless
replicas: 1
selector:
matchLabels:
app: postgresql
template:
metadata:
labels:
app: postgresql
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9187"
prometheus.io/path: "/metrics"
spec:
terminationGracePeriodSeconds: 60
containers:
- name: postgresql
image: postgres:16-alpine
ports:
- name: postgresql
containerPort: 5432
protocol: TCP
env:
- name: POSTGRES_DB
value: "ecommerce"
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_USERNAME
optional: false
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_PASSWORD
optional: false
- name: PGDATA
value: "/var/lib/postgresql/data/pgdata"
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: "2"
memory: 4Gi
volumeMounts:
- name: postgresql-data
mountPath: /var/lib/postgresql/data
livenessProbe:
exec:
command:
- pg_isready
- -U
- ecommerce_app
- -d
- ecommerce
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
exec:
command:
- pg_isready
- -U
- ecommerce_app
- -d
- ecommerce
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "pg_ctl stop -m fast"]
- name: postgres-exporter
image: prometheuscommunity/postgres-exporter:v0.15.0
ports:
- name: metrics
containerPort: 9187
protocol: TCP
env:
- name: DATA_SOURCE_NAME
value: "postgresql://ecommerce_app:S3cureP@ssw0rd!2026@localhost:5432/ecommerce?sslmode=disable"
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
volumeClaimTemplates:
- metadata:
name: postgresql-data
spec:
storageClassName: fast-ssd
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Gi
---
apiVersion: v1
kind: Service
metadata:
name: postgresql-headless
namespace: data
labels:
app: postgresql
spec:
type: ClusterIP
clusterIP: None
ports:
- name: postgresql
port: 5432
targetPort: 5432
protocol: TCP
selector:
app: postgresql
---
apiVersion: v1
kind: Service
metadata:
name: postgresql
namespace: data
labels:
app: postgresql
spec:
type: ClusterIP
ports:
- name: postgresql
port: 5432
targetPort: 5432
protocol: TCP
selector:
app: postgresql
k8s/03-services/redis-statefulset.yaml
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis
namespace: data
labels:
app: redis
spec:
serviceName: redis-headless
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9121"
prometheus.io/path: "/metrics"
spec:
terminationGracePeriodSeconds: 30
containers:
- name: redis
image: redis:7-alpine
command:
- redis-server
- --requirepass
- $(REDIS_PASSWORD)
- --maxmemory
- 1gb
- --maxmemory-policy
- allkeys-lru
- --appendonly
- "yes"
ports:
- name: redis
containerPort: 6379
protocol: TCP
env:
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: redis-credentials
key: REDIS_PASSWORD
resources:
requests:
cpu: 200m
memory: 512Mi
limits:
cpu: "1"
memory: 1Gi
volumeMounts:
- name: redis-data
mountPath: /data
livenessProbe:
exec:
command:
- redis-cli
- -a
- $(REDIS_PASSWORD)
- ping
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
exec:
command:
- redis-cli
- -a
- $(REDIS_PASSWORD)
- ping
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "redis-cli -a $(REDIS_PASSWORD) SHUTDOWN NOSAVE"]
- name: redis-exporter
image: oliver006/redis_exporter:v1.58.0
ports:
- name: metrics
containerPort: 9121
protocol: TCP
env:
- name: REDIS_ADDR
value: "redis://localhost:6379"
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: redis-credentials
key: REDIS_PASSWORD
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 100m
memory: 128Mi
volumeClaimTemplates:
- metadata:
name: redis-data
spec:
storageClassName: fast-ssd
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
---
apiVersion: v1
kind: Service
metadata:
name: redis-headless
namespace: data
labels:
app: redis
spec:
type: ClusterIP
clusterIP: None
ports:
- name: redis
port: 6379
targetPort: 6379
protocol: TCP
selector:
app: redis
---
apiVersion: v1
kind: Service
metadata:
name: redis
namespace: data
labels:
app: redis
spec:
type: ClusterIP
ports:
- name: redis
port: 6379
targetPort: 6379
protocol: TCP
selector:
app: redis
StatefulSet vs Deployment

对于数据库和缓存等有状态服务,务必使用 StatefulSet 而非 Deployment。StatefulSet 提供了以下保证:

  • 稳定的网络标识:每个 Pod 有固定的 DNS 名称(如 postgresql-0.postgresql-headless.data.svc.cluster.local
  • 有序的部署和扩缩容:Pod 按序号顺序创建和删除
  • 稳定的持久化存储:通过 volumeClaimTemplates,每个 Pod 绑定独立的 PVC,Pod 重建后自动重新挂载

四、服务间通信与配置

4.1 CoreDNS 服务发现

Kubernetes 内置的 CoreDNS 为每个 Service 自动创建 DNS 记录,服务间可以通过标准的 FQDN 进行互相访问:

记录格式示例作用域
<service>user-service同命名空间
<service>.<namespace>user-service.microservices跨命名空间
<service>.<namespace>.svc.cluster.localuser-service.microservices.svc.cluster.local全集群
最佳实践

始终使用完整的跨命名空间 FQDN(如 user-service.microservices.svc.cluster.local),即使服务在同一个命名空间中。这样在后续调整命名空间划分时,不需要修改代码和配置。

4.2 健康检查设计原则

健康检查是保障服务可用性的关键机制。K8s 提供了三种探针:

探针类型用途失败后果
Liveness Probe检测容器是否存活重启容器
Readiness Probe检测是否可以接收流量从 Service Endpoints 中移除
Startup Probe检测应用是否启动完成启动完成前禁用其他探针

以下是健康检查的配置要点:

k8s/04-communication/health-check-example.yaml
# 以订单服务为例,展示完整的健康检查配置
spec:
containers:
- name: order-service
# ...其他配置...
startupProbe:
httpGet:
path: /healthz
port: 8082
failureThreshold: 30 # 最多等待 30 * 10s = 300s
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthz
port: 8082
initialDelaySeconds: 0 # startupProbe 完成后才开始
periodSeconds: 15
timeoutSeconds: 5
failureThreshold: 3 # 连续 3 次失败则重启
successThreshold: 1
readinessProbe:
httpGet:
path: /readyz
port: 8082
initialDelaySeconds: 0
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3 # 连续 3 次失败则摘除流量
successThreshold: 1
生产环境注意事项
  1. Liveness 和 Readiness 必须使用不同的端点。Liveness 检测"进程是否活着",Readiness 检测"是否准备好接收请求"。如果两者使用同一端点,可能导致级联故障——例如数据库短暂不可用时,所有 Pod 同时被 Liveness 重启。
  2. 设置合理的 timeoutSeconds。过短的超时时间会导致误判,建议设置为 P99 响应时间的 2-3 倍。
  3. 对于 Java 等启动较慢的服务,务必配置 Startup Probe,否则 Liveness Probe 可能在应用启动期间就触发重启。

4.3 优雅停机

优雅停机确保 Pod 在被终止时,能够完成正在处理的请求并安全释放资源。

v1.34 新特性:原生 Sleep Lifecycle Hook

Kubernetes v1.34 引入了原生的 sleep action 用于 PreStop 和 PostStart lifecycle hooks,无需再通过 exec 执行 sleep 命令。这提供了更简洁和可靠的优雅停机方式。

详见官方文档:Container Lifecycle Hooks

推荐方式(v1.34+):使用原生 Sleep Hook

k8s/04-communication/graceful-shutdown-v134.yaml
spec:
terminationGracePeriodSeconds: 60 # 给予 60 秒的优雅停机时间
containers:
- name: order-service
lifecycle:
preStop:
sleep:
seconds: 15 # 原生 sleep action
# ...其他配置...

传统方式(v1.34 之前):使用 Exec Hook

k8s/04-communication/graceful-shutdown-legacy.yaml
spec:
terminationGracePeriodSeconds: 60 # 给予 60 秒的优雅停机时间
containers:
- name: order-service
lifecycle:
preStop:
exec:
# 先等待 15 秒,让 Service Endpoints 更新
# 然后发送 SIGTERM 信号,应用开始优雅关闭
command: ["/bin/sh", "-c", "sleep 15"]
# ...其他配置...

优雅停机的完整流程如下:

  1. K8s 向 Pod 发送 SIGTERM 信号
  2. preStop Hook 执行 sleep 15(或原生 sleep action),等待 15 秒(让 Ingress/Service 感知到 Pod 不可用)
  3. 应用收到 SIGTERM 后,停止接收新请求,处理完正在进行的请求
  4. 应用关闭数据库连接池、释放资源
  5. 如果超过 terminationGracePeriodSeconds(60s)仍未退出,K8s 发送 SIGKILL 强制终止
为什么 preStop 需要 sleep?

K8s 在执行 preStop Hook 的同时,会从 Service Endpoints 中移除该 Pod。但 Ingress Controller 和上游代理可能还有缓存,需要一定时间才能感知到变化。sleep 15 确保在应用真正开始关闭之前,所有上游组件都已经停止向该 Pod 转发流量。


五、Ingress 七层路由配置

5.1 Ingress Controller 部署

我们使用 APISIX Ingress Controller 作为七层路由组件。

API 版本说明:Ingress 和 IngressClass 使用 networking.k8s.io/v1,这是 Kubernetes v1.34 中的稳定版本。networking.k8s.io/v1beta1 已在 v1.22 中移除,请确保使用 v1。详见官方文档:Ingress v1

k8s/05-ingress/ingress-controller.yaml
---
# RBAC 配置
# apiVersion: rbac.authorization.k8s.io/v1 是 Kubernetes v1.34 中的稳定版本
# 详见官方文档:https://kubernetes.io/docs/reference/kubernetes-api/authorization-resources/role-v1/
apiVersion: v1
kind: ServiceAccount
metadata:
name: apisix-ingress-controller
namespace: gateway
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: apisix-ingress-controller
rules:
- apiGroups: [""]
resources: ["secrets", "services", "endpoints"]
verbs: ["get", "list", "watch"]
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses", "ingressclasses"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apisix.apache.org"]
resources: ["apisixroutes", "apisixupstreams", "apisixtlsconfigs", "apisixclusters", "apisixpluginconfigs"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: apisix-ingress-controller
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: apisix-ingress-controller
subjects:
- kind: ServiceAccount
name: apisix-ingress-controller
namespace: gateway
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: apisix-ingress-controller
namespace: gateway
labels:
app: apisix-ingress-controller
spec:
replicas: 2
selector:
matchLabels:
app: apisix-ingress-controller
template:
metadata:
labels:
app: apisix-ingress-controller
spec:
serviceAccountName: apisix-ingress-controller
containers:
- name: apisix-ingress-controller
image: apache/apisix-ingress-controller:1.8.0
args:
- --ingress-class
- apisix
- --apisix-admin-api-version
- v3
- --log-level
- info
- --http-port
- "8080"
env:
- name: APISIX_ADMIN_API_URL
value: "http://apisix-admin.gateway.svc.cluster.local:9180/apisix/admin"
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /readyz
port: 8080
initialDelaySeconds: 5
periodSeconds: 5

5.2 Ingress 路由规则

k8s/05-ingress/ingress-routes.yaml
---
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: apisix
spec:
controller: apache.org/apisix-ingress-controller
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ecommerce-ingress
namespace: gateway
annotations:
kubernetes.io/ingress.class: apisix
# 启用 CORS
apisix.apache.org/enable-cors: "true"
apisix.apache.org/cors-allow-origin: "https://shop.example.com"
apisix.apache.org/cors-allow-methods: "GET,POST,PUT,DELETE,OPTIONS"
apisix.apache.org/cors-allow-headers: "Authorization,Content-Type"
# 全局限流
apisix.apache.org/plugin-limit-count: |
{
"count": 1000,
"time_window": 1,
"rejected_code": 429,
"key": "remote_addr"
}
spec:
ingressClassName: apisix
tls:
- hosts:
- api.example.com
secretName: tls-secret
rules:
- host: api.example.com
http:
paths:
# 用户服务路由
- path: /api/v1/users
pathType: Prefix
backend:
service:
name: api-gateway
port:
number: 80
# 商品服务路由
- path: /api/v1/products
pathType: Prefix
backend:
service:
name: api-gateway
port:
number: 80
# 订单服务路由
- path: /api/v1/orders
pathType: Prefix
backend:
service:
name: api-gateway
port:
number: 80
# 支付服务路由
- path: /api/v1/payments
pathType: Prefix
backend:
service:
name: api-gateway
port:
number: 80
路由设计说明

在上述配置中,所有业务路由都指向 API Gateway,由 Gateway 负责将请求转发到具体的后端服务。这种设计的好处是:

  • Gateway 统一处理认证、限流、熔断等横切关注点
  • 后端服务不需要暴露到 Ingress 层
  • 路由规则变更只需修改 Gateway 配置,无需修改 Ingress

六、弹性伸缩与高可用

6.1 HPA 弹性伸缩

Horizontal Pod Autoscaler 根据监控指标自动调整 Pod 副本数,是应对流量波动的核心机制。

API 版本说明:HPA 使用 autoscaling/v2,这是 Kubernetes v1.34 中的稳定版本,支持资源指标(CPU/内存)、Pods 指标和外部指标,以及 behavior 字段精细控制扩缩容行为。详见官方文档:HorizontalPodAutoscaler v2

k8s/06-scalability/hpa.yaml
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: user-service-hpa
namespace: microservices
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: user-service
minReplicas: 3
maxReplicas: 20
metrics:
# CPU 利用率目标
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
# 内存利用率目标
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleUp:
stabilizationWindowSeconds: 60 # 扩容稳定窗口
policies:
- type: Pods
value: 4 # 每次最多扩容 4 个 Pod
periodSeconds: 60
- type: Percent
value: 100 # 或每次扩容当前副本数的 100%
periodSeconds: 60
selectPolicy: Max # 取两个策略中更激进的
scaleDown:
stabilizationWindowSeconds: 300 # 缩容稳定窗口 5 分钟
policies:
- type: Pods
value: 2 # 每次最多缩容 2 个 Pod
periodSeconds: 120
selectPolicy: Min # 取两个策略中更保守的
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: order-service-hpa
namespace: microservices
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: order-service
minReplicas: 3
maxReplicas: 30
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 65
# 自定义指标:HTTP 请求延迟 P99
- type: Pods
pods:
metric:
name: http_request_duration_seconds_p99
target:
type: AverageValue
averageValue: "500m" # 500ms
behavior:
scaleUp:
stabilizationWindowSeconds: 30
policies:
- type: Pods
value: 6
periodSeconds: 60
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Pods
value: 2
periodSeconds: 120
生产环境注意事项
  1. 扩容要快,缩容要慢scaleUp.stabilizationWindowSeconds 应设置较小值(30-60s),scaleDown.stabilizationWindowSeconds 应设置较大值(300-600s),避免因流量短暂下降导致频繁缩容。
  2. 设置合理的 minReplicas:最小副本数不应低于 2,且应通过 Pod 反亲和性分布在不同节点上,确保单节点故障不影响服务可用性。
  3. 自定义指标需要安装 Metrics Server 和 Prometheus Adapter,否则 HPA 无法获取自定义指标。

6.2 Pod 反亲和性与 PDB

API 版本说明:PodDisruptionBudget 使用 policy/v1,这是 Kubernetes v1.34 中的稳定版本。PDB 新增了 unhealthyPodEvictionPolicy 字段,支持 IfHealthyBudgetAlwaysAllow 两种策略,更灵活地控制不健康 Pod 的驱逐行为。详见官方文档:PodDisruptionBudget v1

k8s/06-scalability/pdb.yaml
---
# 确保用户服务至少有 2 个可用 Pod
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: user-service-pdb
namespace: microservices
spec:
minAvailable: 2
selector:
matchLabels:
app: user-service
---
# 确保订单服务至少有 50% 的 Pod 可用
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: order-service-pdb
namespace: microservices
spec:
maxUnavailable: "50%"
selector:
matchLabels:
app: order-service
---
# 确保支付服务至少有 1 个可用 Pod
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: payment-service-pdb
namespace: microservices
spec:
minAvailable: 1
selector:
matchLabels:
app: payment-service
PDB 与节点维护

当需要对集群节点进行维护(如升级 K8s 版本、更换硬件)时,PDB 确保驱逐操作不会导致服务可用副本数低于阈值。如果没有 PDB,kubectl drain 可能会一次性驱逐所有 Pod,导致服务中断。


七、监控与日志集成

7.1 Prometheus ServiceMonitor

API 版本说明:ServiceMonitor 使用 monitoring.coreos.com/v1,这是 Prometheus Operator 提供的 CRD,用于定义 Service 的监控目标。详见官方文档:ServiceMonitor CRD

k8s/07-monitoring/servicemonitor.yaml
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: microservices-monitor
namespace: microservices
labels:
release: prometheus # 匹配 Prometheus Operator 的 serviceMonitorSelector
spec:
namespaceSelector:
matchNames:
- microservices
- gateway
selector:
matchLabels:
app.kubernetes.io/part-of: ecommerce
endpoints:
- port: http
path: /metrics
interval: 15s
scrapeTimeout: 10s
honorLabels: true
relabelings:
- sourceLabels: [__meta_kubernetes_pod_name]
targetLabel: pod
- sourceLabels: [__meta_kubernetes_namespace]
targetLabel: namespace
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: data-services-monitor
namespace: data
labels:
release: prometheus
spec:
namespaceSelector:
matchNames:
- data
selector:
matchLabels:
app.kubernetes.io/part-of: ecommerce
endpoints:
- port: metrics
path: /metrics
interval: 30s
scrapeTimeout: 10s

7.2 Grafana Dashboard 配置

k8s/07-monitoring/grafana-dashboard.yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
name: ecommerce-grafana-dashboards
namespace: monitoring
labels:
grafana_dashboard: "1"
data:
microservices-overview.json: |
{
"dashboard": {
"title": "电商微服务总览",
"panels": [
{
"title": "请求 QPS",
"type": "timeseries",
"targets": [
{
"expr": "sum(rate(http_requests_total{namespace=\"microservices\"}[5m])) by (app)"
}
]
},
{
"title": "P99 延迟",
"type": "timeseries",
"targets": [
{
"expr": "histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket{namespace=\"microservices\"}[5m])) by (le, app))"
}
]
},
{
"title": "错误率",
"type": "timeseries",
"targets": [
{
"expr": "sum(rate(http_requests_total{namespace=\"microservices\",status=~\"5..\"}[5m])) by (app) / sum(rate(http_requests_total{namespace=\"microservices\"}[5m])) by (app) * 100"
}
]
},
{
"title": "Pod 副本数",
"type": "stat",
"targets": [
{
"expr": "sum(kube_deployment_status_replicas_available{namespace=\"microservices\"}) by (deployment)"
}
]
}
]
}
}

7.3 告警规则

k8s/07-monitoring/alerting-rules.yaml
---
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: ecommerce-alerts
namespace: microservices
labels:
release: prometheus
spec:
groups:
- name: microservices.alerts
rules:
# 服务不可用告警
- alert: ServiceDown
expr: up{namespace="microservices"} == 0
for: 2m
labels:
severity: critical
annotations:
summary: "服务 {{ $labels.app }} 不可用"
description: "{{ $labels.namespace }} 命名空间中的 {{ $labels.instance }} 已下线超过 2 分钟"

# 高错误率告警
- alert: HighErrorRate
expr: |
sum(rate(http_requests_total{namespace="microservices",status=~"5.."}[5m])) by (app)
/ sum(rate(http_requests_total{namespace="microservices"}[5m])) by (app) > 0.05
for: 5m
labels:
severity: warning
annotations:
summary: "服务 {{ $labels.app }} 错误率过高"
description: "{{ $labels.app }} 的 5xx 错误率已超过 5%,当前值:{{ $value | humanizePercentage }}"

# P99 延迟告警
- alert: HighLatencyP99
expr: |
histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket{namespace="microservices"}[5m])) by (le, app)) > 1
for: 5m
labels:
severity: warning
annotations:
summary: "服务 {{ $labels.app }} P99 延迟过高"
description: "{{ $labels.app }} 的 P99 延迟已超过 1 秒,当前值:{{ $value }}s"

# Pod 重启告警
- alert: PodRestarting
expr: increase(kube_pod_container_status_restarts_total{namespace="microservices"}[1h]) > 3
for: 5m
labels:
severity: warning
annotations:
summary: "Pod {{ $labels.pod }} 频繁重启"
description: "{{ $labels.namespace }} 中的 {{ $labels.pod }} 在过去 1 小时内重启了 {{ $value }} 次"

# HPA 达到上限告警
- alert: HPAAtMaxReplicas
expr: kube_hpa_status_current_replicas == kube_hpa_status_max_replicas
for: 15m
labels:
severity: warning
annotations:
summary: "HPA {{ $labels.hpa }} 已达到最大副本数"
description: "{{ $labels.namespace }} 中的 {{ $labels.hpa }} 已达到最大副本数 {{ $value }},可能需要调整上限"

7.4 Fluent Bit 日志收集

说明:Fluent Bit 以 DaemonSet 方式部署,确保每个节点运行一个日志采集代理。RBAC 配置使用 rbac.authorization.k8s.io/v1,DaemonSet 使用 apps/v1。详见官方文档:Fluent Bit Kubernetes Filter

k8s/07-monitoring/fluent-bit.yaml
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: fluent-bit
namespace: monitoring
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: fluent-bit
rules:
- apiGroups: [""]
resources: ["pods", "namespaces"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: fluent-bit
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: fluent-bit
subjects:
- kind: ServiceAccount
name: fluent-bit
namespace: monitoring
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluent-bit
namespace: monitoring
labels:
app: fluent-bit
spec:
selector:
matchLabels:
app: fluent-bit
template:
metadata:
labels:
app: fluent-bit
spec:
serviceAccountName: fluent-bit
tolerations:
- key: node-role.kubernetes.io/control-plane
effect: NoSchedule
- key: node-role.kubernetes.io/master
effect: NoSchedule
containers:
- name: fluent-bit
image: fluent/fluent-bit:3.0.0
volumeMounts:
- name: varlog
mountPath: /var/log
readOnly: true
- name: varlibdockercontainers
mountPath: /var/lib/docker/containers
readOnly: true
- name: config
mountPath: /fluent-bit/etc/
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
volumes:
- name: varlog
hostPath:
path: /var/log
- name: varlibdockercontainers
hostPath:
path: /var/lib/docker/containers
- name: config
configMap:
name: fluent-bit-config
---
apiVersion: v1
kind: ConfigMap
metadata:
name: fluent-bit-config
namespace: monitoring
data:
fluent-bit.conf: |
[SERVICE]
Flush 5
Daemon Off
Log_Level info
Parsers_File parsers.conf

[INPUT]
Name tail
Path /var/log/containers/*.log
Parser docker
Tag kube.*
Refresh_Interval 10
Mem_Buf_Limit 50MB
Skip_Long_Lines On

[FILTER]
Name kubernetes
Match kube.*
Kube_URL https://kubernetes.default.svc:443
Kube_CA_File /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
Kube_Token_File /var/run/secrets/kubernetes.io/serviceaccount/token
Merge_Log On
Merge_Log_Key log_processed
K8S-Parser.On On
K8S-Parser.Exclude On

[OUTPUT]
Name elasticsearch
Match kube.*
Host elasticsearch.monitoring.svc.cluster.local
Port 9200
Index ecommerce-logs
Type _doc
Logstash_Format On
Logstash_Prefix ecommerce
Retry_Limit False

parsers.conf: |
[PARSER]
Name docker
Format json
Time_Key time
Time_Format %Y-%m-%dT%H:%M:%S.%L

八、CI/CD 集成(ArgoCD)

8.1 GitOps 工作流

GitOps 的核心理念是:Git 仓库是唯一的事实来源(Single Source of Truth)。所有环境变更都通过提交代码来触发,ArgoCD 负责将 Git 仓库中的声明式配置同步到 Kubernetes 集群。

8.2 ArgoCD Application 配置

API 版本说明:ArgoCD 使用 argoproj.io/v1alpha1,这是 ArgoCD 的稳定 API 版本。AppProject、Application 和 ApplicationSet 均使用此版本。详见官方文档:ArgoCD CRD Reference

k8s/08-argocd/argocd-app.yaml
---
# ArgoCD 项目定义
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: ecommerce
namespace: argocd
spec:
description: "电商微服务项目"
sourceRepos:
- "https://github.com/your-org/ecommerce-k8s.git"
destinations:
- namespace: microservices
server: https://kubernetes.default.svc
- namespace: data
server: https://kubernetes.default.svc
- namespace: gateway
server: https://kubernetes.default.svc
clusterResourceWhitelist:
- group: ""
kind: Namespace
- group: "networking.k8s.io"
kind: IngressClass
orphanedResources:
warn: true
---
# ArgoCD 应用定义
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: ecommerce-infra
namespace: argocd
labels:
app.kubernetes.io/part-of: ecommerce
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: ecommerce
source:
repoURL: https://github.com/your-org/ecommerce-k8s.git
targetRevision: main
path: k8s
directory:
recurse: true
jsonnet: false
destination:
server: https://kubernetes.default.svc
namespace: microservices
syncPolicy:
automated:
prune: true # 自动删除 Git 中不存在的资源
selfHeal: true # 自动修复手动变更
allowEmpty: false
syncOptions:
- CreateNamespace=true
- PrunePropagationPolicy=foreground
- PruneLast=true
- ServerSideApply=true
retry:
limit: 3
backoff:
duration: 5s
factor: 2
maxDuration: 3m
---
# ArgoCD 应用集(App of Apps 模式)
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: ecommerce-appset
namespace: argocd
spec:
generators:
- git:
repoURL: https://github.com/your-org/ecommerce-k8s.git
revision: main
directories:
- path: k8s/*
template:
metadata:
name: "{{ path.basename }}"
spec:
project: ecommerce
source:
repoURL: https://github.com/your-org/ecommerce-k8s.git
targetRevision: main
path: "{{ path }}"
destination:
server: https://kubernetes.default.svc
namespace: "{{ path.basename }}"
syncPolicy:
automated:
prune: true
selfHeal: true
生产环境注意事项
  1. selfHeal: true 要谨慎使用。开启后,任何手动通过 kubectl 修改的配置都会被 ArgoCD 自动覆盖。建议在开发/测试环境开启,生产环境使用手动同步。
  2. prune: true 会删除 Git 中不存在的资源。误删 Git 中的文件可能导致生产环境资源被意外删除。建议配合 syncOptions: PruneLast=true,让 ArgoCD 最后再执行删除操作。
  3. 使用 App of Apps 模式管理多应用。通过 ApplicationSet 可以自动发现 Git 仓库中的目录结构,为每个子目录创建一个 Application,避免手动维护大量 Application 资源。

九、请求完整链路

下面展示一个用户下单请求的完整链路,帮助你理解各组件之间的协作关系。


十、部署与验证

10.1 分步部署

按照依赖关系,从底层到上层依次部署:

# 1. 创建命名空间
kubectl apply -f k8s/00-namespace/

# 2. 部署配置与密钥
kubectl apply -f k8s/01-config/

# 3. 创建存储资源
kubectl apply -f k8s/02-storage/

# 4. 部署数据层(等待 PostgreSQL 和 Redis 就绪)
kubectl apply -f k8s/03-services/postgresql-statefulset.yaml
kubectl apply -f k8s/03-services/redis-statefulset.yaml

# 等待数据层就绪
kubectl wait --for=condition=ready pod \
-l app=postgresql -n data --timeout=120s
kubectl wait --for=condition=ready pod \
-l app=redis -n data --timeout=120s

# 5. 部署业务服务
kubectl apply -f k8s/03-services/user-service.yaml
kubectl apply -f k8s/03-services/product-service.yaml
kubectl apply -f k8s/03-services/order-service.yaml
kubectl apply -f k8s/03-services/payment-service.yaml

# 6. 部署网关
kubectl apply -f k8s/03-services/gateway-deployment.yaml

# 7. 部署 Ingress
kubectl apply -f k8s/05-ingress/

# 8. 部署弹性伸缩与高可用
kubectl apply -f k8s/06-scalability/

# 9. 部署监控
kubectl apply -f k8s/07-monitoring/

10.2 健康检查验证

# 检查所有命名空间下的 Pod 状态
kubectl get pods --all-namespaces -l app.kubernetes.io/part-of=ecommerce

# 检查各服务 Endpoints
kubectl get endpoints -n microservices
kubectl get endpoints -n data
kubectl get endpoints -n gateway

# 检查 Ingress 状态
kubectl get ingress -n gateway
kubectl describe ingress ecommerce-ingress -n gateway

# 检查 HPA 状态
kubectl get hpa -n microservices
kubectl describe hpa user-service-hpa -n microservices

# 检查 PDB 状态
kubectl get pdb -n microservices

# 检查 PVC 绑定状态
kubectl get pvc -n data

# 检查 ArgoCD 应用状态
kubectl get applications -n argocd

10.3 压力测试

使用 hey 工具对用户服务进行压力测试:

# 进入集群内部执行测试(或通过 port-forward)
kubectl run hey-test --image=williamyeh/hey:latest --rm -it --restart=Never -- \
-n 100 -c 20 -m POST \
-H "Content-Type: application/json" \
-d '{"username":"testuser","password":"testpass"}' \
http://user-service.microservices.svc.cluster.local:8080/api/v1/users/login

# 模拟并发下单请求
kubectl run hey-test --image=williamyeh/hey:latest --rm -it --restart=Never -- \
-n 500 -c 50 -m POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{"product_id":1,"quantity":2}' \
http://order-service.microservices.svc.cluster.local:8082/api/v1/orders

# 观察 HPA 是否触发扩容
kubectl get hpa -n microservices -w

10.4 故障注入测试

# 测试 1:删除 Pod,验证自动恢复
kubectl delete pod -l app=user-service -n microservices --grace-period=0 --force
# 观察 Pod 是否自动重建
kubectl get pods -l app=user-service -n microservices -w

# 测试 2:验证优雅停机(不应出现 5xx 错误)
# 在另一个终端持续发送请求
kubectl run curl-test --image=curlimages/curl:latest --rm -it --restart=Never -- \
-s -o /dev/null -w "%{http_code}\n" \
http://user-service.microservices.svc.cluster.local:8080/healthz

# 测试 3:模拟节点故障(需要多节点集群)
kubectl cordon <node-name> # 标记节点不可调度
kubectl drain <node-name> --ignore-daemonsets --delete-emptydir-data
# 观察 Pod 是否迁移到其他节点
kubectl get pods -l app=user-service -n microservices -o wide -w

# 测试 4:验证 PDB 保护
kubectl drain <node-name> --ignore-daemonsets --delete-emptydir-data
# 如果违反 PDB,drain 会被阻止,并提示:
# "error: poddisruptionbudgets policy violation"

十一、生产环境最佳实践总结

经过上述完整的实战演练,以下是生产环境中的关键最佳实践:

架构设计

实践说明
命名空间隔离按团队/环境/业务域划分命名空间,配合 ResourceQuota 限制资源
配置外部化使用 ConfigMap/Secret 管理配置,禁止将配置硬编码在镜像中
密钥加密使用 Sealed Secrets 或 External Secrets Operator 管理敏感信息
服务网格可选对于服务间通信复杂度高的场景,考虑引入 Istio/Linkerd

部署策略

实践说明
滚动更新maxSurge: 1, maxUnavailable: 0 确保更新过程中不中断服务
健康检查三件套Startup + Liveness + Readiness Probe,使用不同端点
优雅停机preStop sleep + terminationGracePeriodSeconds 确保请求处理完成
Pod 反亲和性确保同一服务的 Pod 分布在不同节点上

弹性伸缩

实践说明
HPA 多指标同时关注 CPU、内存和自定义业务指标(如 QPS、延迟)
扩快缩慢扩容窗口短(30-60s),缩容窗口长(300-600s)
PDB 保护为关键服务配置 PDB,防止维护操作导致服务不可用
Cluster Autoscaler配合节点自动伸缩,当 Pod 因资源不足处于 Pending 状态时自动扩容节点

可观测性

实践说明
三大支柱Metrics(Prometheus)+ Logs(Fluent Bit)+ Traces(Jaeger/OpenTelemetry)
告警分级Critical(立即响应)+ Warning(工作时间内处理)+ Info(记录备案)
Dashboard 分层全局总览 -> 服务维度 -> Pod 维度,逐层下钻
SLO/SLI定义明确的服务质量目标(如 99.9% 可用性、P99 < 500ms)

持续交付

实践说明
GitOpsGit 仓库作为唯一事实来源,所有变更通过 PR 审批
环境隔离dev -> staging -> production 逐级发布,每级有独立的 ArgoCD Application
镜像标签策略使用 Git Commit SHA 作为镜像 Tag,确保可追溯性
回滚机制ArgoCD 支持一键回滚到 Git 历史中的任意版本
总结

生产级 Kubernetes 部署不仅仅是"把 YAML 写对",更是一套涵盖架构设计、部署策略、弹性伸缩、可观测性和持续交付的完整体系。本文通过电商微服务的实战案例,展示了如何将这些最佳实践落地到具体的 YAML 配置中。

希望这篇实战指南能够帮助你在实际项目中少走弯路,构建出真正可靠、可扩展的云原生微服务系统。

gRPC 进阶 (3):生产环境治理全指南

· 阅读需 14 分钟
Rainy
雨落无声,代码成诗 —— 致力于技术与艺术的极致平衡

"框架的 Hello World 只需要 5 分钟,但在生产环境中跑稳它需要 5 年的经验。"

上一篇 中,我们深入了 grpc-java 的内部实现。 本文聚焦 生产环境治理——当你的 gRPC 服务部署到 Kubernetes 集群后,你需要关心的每一件事。