From 3148216d38fba0df45ea37b1979e263b209eb2f7 Mon Sep 17 00:00:00 2001 From: weishen Date: Fri, 24 Apr 2026 17:14:00 +0800 Subject: [PATCH] Sync: add aws source identity notes --- .../DATA_MODEL/DATA_MODEL_PERMISSION.md | 1362 +++++++++++++++++ wiki/concepts/AWS-Source-Identity.md | 66 + wiki/index.md | 7 +- wiki/log.md | 28 + wiki/overview.md | 6 + ...optimise-resource-cost-using-automation.md | 48 + ...ntrol-20240319-160204-meeting-recording.md | 52 + ...st-optimization-20240305-160037-meeting.md | 57 + 8 files changed, 1623 insertions(+), 3 deletions(-) create mode 100644 Project/fonrey/DATA_MODEL/DATA_MODEL_PERMISSION.md create mode 100644 wiki/concepts/AWS-Source-Identity.md create mode 100644 wiki/sources/ctp-topic-63-optimise-resource-cost-using-automation.md create mode 100644 wiki/sources/public-cloud-learning-sessions-budget-control-20240319-160204-meeting-recording.md create mode 100644 wiki/sources/public-cloud-learning-sessions-storage-cost-optimization-20240305-160037-meeting.md diff --git a/Project/fonrey/DATA_MODEL/DATA_MODEL_PERMISSION.md b/Project/fonrey/DATA_MODEL/DATA_MODEL_PERMISSION.md new file mode 100644 index 00000000..fcbcba6b --- /dev/null +++ b/Project/fonrey/DATA_MODEL/DATA_MODEL_PERMISSION.md @@ -0,0 +1,1362 @@ +# Fonrey — 权限管理数据模型(DATA_MODEL_PERMISSION) + +> **所属系统**: Fonrey 房产经纪管理系统 +> **版本**: v1.0 +> **日期**: 2026-04-24 +> **关联模块**: `apps/permissions/` +> **PRD 来源**: `PRD/权限管理/权限管理模块PRD.md` +> **技术方案**: `TECH_STACK/权限管理系统技术方案.md`(本文档为其最终固化版,修正了 12 处设计问题) + +--- + +## 一、领域概览(Domain Overview) + +### 1.1 核心概念 + +| 概念 | 表 | 业务说明 | +|------|-----|---------| +| **PermissionDef(权限定义)** | `permission_defs` | 系统所有权限项的元数据目录(约 300+ 条),开发者维护、运营不可编辑。定义权限的 code/名称/值类型/可选范围/默认值 | +| **Role(角色)** | `roles` | 权限模板,管理员创建,如「高级业务员」「分行经理」。绑定角色类别,类别决定可配置权限项的上限 | +| **RolePermission(角色权限值)** | `role_permissions` | 某角色在某权限项上的具体配置值(稀疏存储:只存非默认值)| +| **StaffRole(员工-角色关联)** | `staff_roles` | 员工与角色的多对多关系,支持一人多角色(PRD Story 8),含主角色标识 | +| **StaffPermissionOverride(个人覆盖)** | `staff_permission_overrides` | 员工个人权限定制(稀疏存储:只存与角色合并结果不同的项) | +| **StaffDataScope(员工管理范围)** | `staff_data_scopes` | 员工的**数据可见边界**(PRD 5.6),独立于权限项的 SCOPE 值,支持跨层级叠加(如"本组 ∪ 门店B") | +| **PermissionChangeLog(权限变更日志)** | `permission_change_logs` | 所有角色/个人权限/范围的变更流水(append-only,不可删除,对齐 `staff_transfer_logs` 范式)| + +### 1.2 权限模型(Hybrid RBAC + Override) + +``` +员工最终权限 = 多角色值合并(最宽松) + 个人 Override(覆盖/收窄/扩展) +员工最终数据范围 = staff_data_scopes 的并集(跨层级可叠加) + AND 业务级 attribute/protection 规则(如保护客过滤) +``` + +**优先级**:`PermissionOverride > Merged(RolePermissions) > PermissionDef.default_value` + +**SCOPE 与 DataScope 的职责分离**(关键设计决策): +- `PermissionDef.value_type = SCOPE` 的权限项值(本人/本组/本门店/全公司)→ **权限项自身的范围维度**,如"查看私客范围=本门店" +- `StaffDataScope` → **员工整体的数据边界**,与权限项正交组合。例:员工权限"查看私客=本门店",但 DataScope 额外授予"门店B",则员工可见"所属门店 ∪ 门店B"下的私客 + +### 1.3 与其他模型的关系 + +``` +OrgUnit (组织树) ─────────────┐ + │ │ path 用于 SCOPE 子树查询 + │ ▼ +Staff ──N:M── StaffRole ──N:1── Role ──N:M── RolePermission ──N:1── PermissionDef + │ │ category: 置业顾问/店管/总经 + │ │ + ├──1:N── StaffPermissionOverride ──N:1── PermissionDef + │ + ├──1:N── StaffDataScope ──N:1── OrgUnit (范围节点) + │ + └── staff.role (系统角色) ──── 与 Role 表双轨并行 + (系统角色决定顶层权限上限) + +PermissionChangeLog (append-only) ← 所有变更流水 +``` + +### 1.4 与现有 DATA_MODEL 的对接 + +| 现有模型 | 对接方式 | +|---------|---------| +| `staff.role`(系统角色枚举) | 保留,作为**系统角色**(决定是否能登录、是否系统管理员)。**Role 表是业务角色**(决定业务权限)。两者正交 | +| `staff.is_system_admin` | **权限检查短路**:`is_system_admin=TRUE` 时 `PermissionChecker` 全部返回 True,绕过所有检查 | +| `org_units.path` | `ScopeQueryBuilder` 的核心依赖,用于 "本组/本门店/本区域" 的子树查询(`path LIKE '/{root}/{scope_root}/%'`) | +| `properties.attribute`(公盘/私盘/特盘/封盘) | 业务属性,权限系统 **不干预**。权限决定"能否访问房源列表",attribute 决定"列表内按业务规则过滤" | +| `clients.is_protected`(保护客) | 保护客在 ScopeQueryBuilder 中**额外 AND 保护规则**:非 owner/合保人即使有「本门店」权限也不可见 | +| `property_protections` / 保护房 | 同上,业务状态由 ScopeQueryBuilder 额外过滤 | + +--- + +## 二、实体关系 ER + +``` +┌──────────────────┐ ┌──────────────────┐ ┌────────────────────┐ +│ permission_defs │───N:1─│ role_permissions │───N:1─│ roles │ +│ (权限目录, 租户级)│ │ (角色×权限→值) │ │ (业务角色模板) │ +└──────────────────┘ └──────────────────┘ └────────────────────┘ + │ │ + │ │ N:M via + │ │ + │ ┌─────▼──────┐ + │ N:1 │ staff_roles│ + │ │ is_primary│ + │ └─────┬──────┘ + │ │ N:1 + │ │ + │ ┌───────────────────────────┐ ┌────────▼────────┐ + └──N:1│ staff_permission_overrides│──N:1────│ staff │ + │ (个人差异化权限) │ └────────┬────────┘ + └───────────────────────────┘ │ + │ 1:N + ┌──────────────────────────────────────┤ + ▼ ▼ + ┌──────────────────────┐ ┌──────────────────────────┐ + │ staff_data_scopes │──N:1────►│ org_units (物化路径树) │ + │ (管理范围,可跨层叠加)│ └──────────────────────────┘ + └──────────────────────┘ + + ┌────────────────────────────────┐ + │ permission_change_logs │ (不可删除流水) + │ -> target (role/staff/scope) │ + └────────────────────────────────┘ +``` + +--- + +## 三、Schema 定义 + +### 3.1 permission_defs — 权限定义表(权限目录) + +> **归属 schema**:`tenant_apps`(非 shared),每个租户独立一份初始化数据。 +> **理由**:避免 `django-tenants` 跨 schema 外键限制;为未来"企业版自定义权限项"留空间;通过 data migration 在租户创建时批量 seed 约 300 条默认记录。 + +| 字段 | 类型 | 约束 | 业务说明 | +|------|------|------|---------| +| id | UUID | PK | | +| code | VARCHAR(150) | UNIQUE, NOT NULL | 权限编码,规则:`{module}.{sub_module}.{action}[.{qualifier}]`,如 `client.private.view.scope` | +| module | VARCHAR(50) | NOT NULL | 一级模块枚举(见 §4.1) | +| sub_module | VARCHAR(50) | | 二级模块(如 `二手&租赁`、`商圈精耕`) | +| group_name | VARCHAR(100) | NOT NULL | 分组标题(如「私客基础权限」「联系人基础权限」) | +| name | VARCHAR(200) | NOT NULL | 显示名称 | +| description | TEXT | | 权限作用描述(PRD 5.4.3 每项均有) | +| value_type | VARCHAR(20) | NOT NULL, CHECK | `BOOLEAN` / `SCOPE` / `INTEGER`(见 §4.2) | +| scope_choices | JSONB | DEFAULT `'[]'` | 仅 `SCOPE` 类型有效,可选枚举值的 code 列表,如 `["none","self","store","company"]` | +| integer_min | INTEGER | | 仅 `INTEGER` 有效,最小值 | +| integer_max | INTEGER | | 仅 `INTEGER` 有效,最大值;`NULL`=无上限(业务语义:0 通常代表"不限制") | +| default_value | JSONB | NOT NULL DEFAULT `'{"v":false}'` | 系统最小默认值 | +| max_allowed_categories | VARCHAR(50)[] | DEFAULT `'{}'` | 允许配置此权限的角色类别列表,空数组=所有类别均可(对应 Issue #11)| +| sort_order | INTEGER | NOT NULL DEFAULT 0 | 分组内排序 | +| is_active | BOOLEAN | NOT NULL DEFAULT TRUE | 下线权限项置 FALSE,历史记录保留 | +| is_deprecated | BOOLEAN | NOT NULL DEFAULT FALSE | 废弃标记(不再推荐使用但保持兼容) | +| version | INTEGER | NOT NULL DEFAULT 1 | 权限项定义版本,变更时递增,用于缓存失效 | +| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | | +| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | | + +**关键索引**: +```sql +CREATE UNIQUE INDEX idx_permission_defs_code ON permission_defs(code); +CREATE INDEX idx_permission_defs_module ON permission_defs(module, sub_module, sort_order) WHERE is_active = TRUE; +CREATE INDEX idx_permission_defs_active ON permission_defs(is_active) WHERE is_active = TRUE; +``` + +**校验规则**(Django model `clean()`): +- `code` 必须匹配 `^[a-z_]+\.[a-z_]+(\.[a-z_]+){1,2}$` +- `value_type='SCOPE'` 时 `scope_choices` 非空 +- `default_value` 必须包含 `"v"` 键且值与 `value_type` 兼容 + +--- + +### 3.2 roles — 角色表 + +| 字段 | 类型 | 约束 | 业务说明 | +|------|------|------|---------| +| id | UUID | PK | | +| name | VARCHAR(100) | NOT NULL | 角色名称 | +| category | VARCHAR(30) | NOT NULL, CHECK | `agent`(置业顾问) / `store_manager`(店管) / `director`(总经) / `operator`(运营) / `custom`(见 §4.3) | +| description | TEXT | | 角色描述 | +| template_role_id | UUID | FK→roles, SET NULL | 权限模板来源角色(PRD「引用该角色配置」列) | +| is_system_builtin | BOOLEAN | NOT NULL DEFAULT FALSE | 系统内置角色(如"最大权限角色"),不可删除、不可改名 | +| is_active | BOOLEAN | NOT NULL DEFAULT TRUE | | +| created_by | UUID | FK→staff, SET NULL | 创建人(PRD: 角色类别只能由创建者修改) | +| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | | +| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | | +| updated_by | UUID | FK→staff, SET NULL | | +| deleted_at | TIMESTAMPTZ | | 软删除 | + +**关键索引**: +```sql +-- 软删除友好的唯一索引:同名角色只要有一个未删除即冲突 +CREATE UNIQUE INDEX idx_roles_name_active ON roles(name) WHERE deleted_at IS NULL; +CREATE INDEX idx_roles_category ON roles(category) WHERE deleted_at IS NULL; +CREATE INDEX idx_roles_template ON roles(template_role_id); +``` + +**禁止操作**: +- ❌ 角色被 `staff_roles` 引用时禁止删除(DB 层 `PROTECT`,Application 层给出"请先迁移 N 位员工"提示) +- ❌ `is_system_builtin = TRUE` 的角色禁止删除/改名 +- ❌ `category` 在创建后仅 `created_by` 本人可修改(Application 层校验) + +--- + +### 3.3 role_permissions — 角色权限值表 + +| 字段 | 类型 | 约束 | 业务说明 | +|------|------|------|---------| +| id | UUID | PK | | +| role_id | UUID | NOT NULL, FK→roles, CASCADE | | +| permission_def_id | UUID | NOT NULL, FK→permission_defs, RESTRICT | | +| value | JSONB | NOT NULL | 统一格式 `{"v": }`,如 `{"v": true}` / `{"v": "store"}` / `{"v": 50}` | +| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | | +| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | | +| updated_by | UUID | FK→staff, SET NULL | | + +**关键索引**: +```sql +CREATE UNIQUE INDEX idx_role_permissions_uniq ON role_permissions(role_id, permission_def_id); +CREATE INDEX idx_role_permissions_role ON role_permissions(role_id); +CREATE INDEX idx_role_permissions_def ON role_permissions(permission_def_id); +``` + +**稀疏存储原则**:**只存储非 `default_value` 的项**。保存时若 `value == permission_def.default_value`,执行 DELETE 而非 UPSERT。优化存储与合并性能。 + +--- + +### 3.4 staff_roles — 员工角色关联 + +| 字段 | 类型 | 约束 | 业务说明 | +|------|------|------|---------| +| id | UUID | PK | | +| staff_id | UUID | NOT NULL, FK→staff, CASCADE | | +| role_id | UUID | NOT NULL, FK→roles, PROTECT | 角色被员工引用时禁止删除 | +| is_primary | BOOLEAN | NOT NULL DEFAULT FALSE | 主角色标识,每个员工有且仅有一个主角色(Issue #5) | +| assigned_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | | +| assigned_by | UUID | FK→staff, SET NULL | | +| valid_from | DATE | | 生效日(预留未来"定时生效"功能) | +| valid_until | DATE | | 失效日 | + +**关键索引**: +```sql +CREATE UNIQUE INDEX idx_staff_roles_uniq ON staff_roles(staff_id, role_id); +CREATE UNIQUE INDEX idx_staff_roles_primary ON staff_roles(staff_id) WHERE is_primary = TRUE; +CREATE INDEX idx_staff_roles_role ON staff_roles(role_id); +``` + +**约束触发器**:每次 UPDATE `is_primary=TRUE` 时,自动将该 staff 其他行 `is_primary=FALSE`(Django signal 实现,避免 DB 级递归触发器) + +--- + +### 3.5 staff_permission_overrides — 个人权限覆盖 + +| 字段 | 类型 | 约束 | 业务说明 | +|------|------|------|---------| +| id | UUID | PK | | +| staff_id | UUID | NOT NULL, FK→staff, CASCADE | | +| permission_def_id | UUID | NOT NULL, FK→permission_defs, RESTRICT | | +| value | JSONB | NOT NULL | 个人权限值,同 `{"v": ...}` 格式 | +| override_mode | VARCHAR(10) | NOT NULL DEFAULT 'REPLACE' | `REPLACE`(覆盖角色合并值,PRD 默认) / `RESTRICT`(限制上限) / `GRANT`(仅扩展) — Issue #6 | +| reason | TEXT | | 管理员备注(建议强制,为后续审计) | +| modified_by | UUID | FK→staff, SET NULL | | +| modified_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | | + +**关键索引**: +```sql +CREATE UNIQUE INDEX idx_staff_overrides_uniq ON staff_permission_overrides(staff_id, permission_def_id); +CREATE INDEX idx_staff_overrides_staff ON staff_permission_overrides(staff_id); +``` + +**稀疏存储**:只存与角色合并值不同的项。批量设置角色(PRD Story 2)会清除员工所有 Override;个人编辑(Story 3)按需 UPSERT/DELETE。 + +--- + +### 3.6 staff_data_scopes — 员工数据管理范围(⭐ 新增,Issue #1) + +> PRD 5.6「管理范围」的实体化。与权限项的 `SCOPE` 值互为补充:权限项决定"访问什么数据",DataScope 决定"数据的组织边界"。 + +| 字段 | 类型 | 约束 | 业务说明 | +|------|------|------|---------| +| id | UUID | PK | | +| staff_id | UUID | NOT NULL, FK→staff, CASCADE | | +| scope_type | VARCHAR(20) | NOT NULL, CHECK | `self` / `group` / `store` / `area` / `region` / `company` / `custom_unit` | +| org_unit_id | UUID | FK→org_units, RESTRICT | `scope_type='custom_unit'` 时必填,指向具体的组织节点;其他类型时 NULL(由 `staff.org_unit_id` 动态推导) | +| is_readable | BOOLEAN | NOT NULL DEFAULT TRUE | 可读 | +| is_writable | BOOLEAN | NOT NULL DEFAULT FALSE | 可写(默认只读) | +| granted_by | UUID | FK→staff, SET NULL | | +| granted_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | | +| expires_at | TIMESTAMPTZ | | 临时授权失效时间 | +| reason | TEXT | | 授予原因 | + +**关键索引**: +```sql +CREATE INDEX idx_data_scopes_staff ON staff_data_scopes(staff_id); +CREATE INDEX idx_data_scopes_org ON staff_data_scopes(org_unit_id); +CREATE INDEX idx_data_scopes_expires ON staff_data_scopes(expires_at) WHERE expires_at IS NOT NULL; +``` + +**业务语义**: +- 员工的「管理范围」= 所有 `staff_data_scopes` 记录对应的 `org_units.path` 子树**并集** +- 支持"扩充范围"(PRD 5.3.3):新增一条 `scope_type='custom_unit', org_unit_id=门店B` 记录 +- 保底规则:员工至少有一条 `scope_type='self'` 的记录(入职时自动创建) + +--- + +### 3.7 permission_change_logs — 权限变更流水(⭐ 新增,Issue #4) + +> append-only,**不可删除**,对齐 `staff_transfer_logs` 范式。 + +| 字段 | 类型 | 约束 | 业务说明 | +|------|------|------|---------| +| id | UUID | PK | | +| target_type | VARCHAR(30) | NOT NULL, CHECK | `role` / `role_permission` / `staff_role` / `staff_override` / `staff_scope` | +| target_id | UUID | NOT NULL | 被变更对象的 ID(上述各表的主键) | +| staff_id | UUID | FK→staff, SET NULL | 被影响员工(target 是 `staff_role/staff_override/staff_scope` 时必填,便于按员工查询) | +| role_id | UUID | FK→roles, SET NULL | 被影响角色(便于按角色查询) | +| permission_code | VARCHAR(150) | | 被变更的权限 code(用 code 而非 FK,避免 PermissionDef 删除后日志丢失) | +| action | VARCHAR(20) | NOT NULL, CHECK | `create` / `update` / `delete` / `assign` / `revoke` | +| old_value | JSONB | | 变更前快照 | +| new_value | JSONB | | 变更后快照 | +| operator_id | UUID | NOT NULL, FK→staff, RESTRICT | 操作人 | +| operator_ip | INET | | 操作来源 IP | +| user_agent | TEXT | | 操作终端 UA | +| reason | TEXT | | 操作原因(批量设置角色等场景强制填写) | +| operated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | | +| ⚠️ 无 deleted_at | — | — | 审计日志**不可删除** | + +**关键索引**: +```sql +CREATE INDEX idx_perm_log_staff ON permission_change_logs(staff_id, operated_at DESC) WHERE staff_id IS NOT NULL; +CREATE INDEX idx_perm_log_role ON permission_change_logs(role_id, operated_at DESC) WHERE role_id IS NOT NULL; +CREATE INDEX idx_perm_log_target ON permission_change_logs(target_type, target_id, operated_at DESC); +CREATE INDEX idx_perm_log_operator ON permission_change_logs(operator_id, operated_at DESC); +CREATE INDEX idx_perm_log_time ON permission_change_logs(operated_at DESC); +``` + +**分区建议**:数据量大(每次批量角色变更产生 N 条记录)时按 `operated_at` 月度 RANGE 分区。 + +--- + +## 四、枚举常量 + +### 4.1 PermissionModule — 一级模块(与 PRD 8.2 导航完全对齐) + +``` +home = 首页 +property = 房源 +new_house = 新房 +client = 客源 +transaction = 交易 +data = 数据 +marketing = 营销 +hr = 人事OA +contract = 合同 +trinet = 三网 +system = 系统 +mobile = 移动端 +smart_store = 智能门店 +recharge = 在线充值 +``` + +### 4.2 ValueType — 权限值类型 + +| 值 | 存储格式 | UI 控件 | 示例 | +|----|---------|---------|------| +| `BOOLEAN` | `{"v": true}` | Toggle | 今日新上房源是否显示 | +| `SCOPE` | `{"v": "store"}` | 下拉选择 | 查看私客范围(本人/本组/本门店/本区域/全公司)| +| `INTEGER` | `{"v": 50}` | 数字输入 | 每日最多查看联系人数(0=不限制)| + +### 4.3 RoleCategory — 角色类别 + +``` +agent = 置业顾问(一线经纪人) +store_manager = 店管(店长/区域经理) +director = 总经(公司级管理) +operator = 运营/行政 +custom = 自定义(不继承预设上限) +``` + +### 4.4 ScopeLevel — SCOPE 枚举有序值(用于多角色合并) + +``` +none = 0 无 +self = 1 本人 +group = 2 本组 +store = 3 本门店 +area = 4 本区域 +region = 5 本大区 +company = 6 全公司 +``` + +--- + +## 五、权限解析与查询模式 + +### 5.1 员工权限解析流程(resolver) + +``` +输入: staff_id +Step 1: 若 staff.is_system_admin = TRUE → 返回 "全部权限=true,全部 SCOPE=company"(短路) +Step 2: 加载所有 staff_roles → role_ids +Step 3: 加载所有 role_permissions (role_id IN role_ids) 按 permission_def 分组 +Step 4: 合并多角色值: + BOOLEAN → OR + SCOPE → MAX by ScopeLevel + INTEGER → MAX (0 视为 +∞) +Step 5: 填入未配置权限项的 default_value +Step 6: 叠加 staff_permission_overrides(按 override_mode 规则): + REPLACE → 直接替换合并值 + RESTRICT → min(override_value, merged_value) + GRANT → max(override_value, merged_value) +输出: { permission_code: {"v": value}, ... } +缓存: perm:v{CACHE_VERSION}:{schema}:{staff_id} (TTL=3600) +``` + +### 5.2 数据范围查询(ScopeQueryBuilder) + +核心工具类,职责:将"SCOPE 权限值 + StaffDataScope"转为 Django ORM `Q` 对象。 + +```python +# apps/permissions/services/scope_query.py + +from django.db.models import Q + +class ScopeQueryBuilder: + """ + 用法: + qs = Property.objects.all() + qs = ScopeQueryBuilder(staff, "property.list.view.scope", field_prefix="seller_agent")\ + .apply_to(qs) + """ + def __init__(self, staff, permission_code: str, field_prefix: str): + self.staff = staff + self.perm_code = permission_code + self.field_prefix = field_prefix # e.g. "seller_agent" or "owner" + + def build_q(self) -> Q: + # 1. 系统管理员短路 + if self.staff.is_system_admin: + return Q() # no filter + + # 2. 读取权限值 + checker = PermissionChecker(self.staff) + scope = checker.get_scope(self.perm_code) # e.g. "store" + + # 3. 构建 base Q(基于 SCOPE 值 + staff.org_unit.path 子树) + base_q = self._scope_to_q(scope) + + # 4. 叠加 StaffDataScope 并集 + for ds in StaffDataScope.objects.filter(staff=self.staff, is_readable=True): + base_q |= self._datascope_to_q(ds) + + return base_q + + def _scope_to_q(self, scope: str) -> Q: + """将 SCOPE 枚举转为 Q,基于当前 staff 的 org_unit.path""" + prefix = self.field_prefix + if scope == "none": + return Q(pk__in=[]) # 永远为空 + if scope == "self": + return Q(**{f"{prefix}_id": self.staff.id}) + if scope == "group": + return Q(**{f"{prefix}__org_unit__path__startswith": self.staff.org_unit.path}) + if scope in ("store", "area", "region"): + # 向上定位到对应层级节点 + target_unit = self.staff.org_unit.ancestor_of_type(scope) # "store" → Store 节点 + return Q(**{f"{prefix}__org_unit__path__startswith": target_unit.path}) + if scope == "company": + return Q() + return Q(pk__in=[]) + + def apply_to(self, queryset): + q = self.build_q() + # 业务规则叠加:保护客过滤(客源场景) + if hasattr(queryset.model, "is_protected"): + q &= (Q(is_protected=False) | Q(owner_id=self.staff.id)) + return queryset.filter(q) +``` + +### 5.3 「权限与角色不一致」标记 + +```sql +-- 批量标记逻辑(SQL 层,用于人员列表高频查询) +-- 某员工若存在任一 override 行,其 value != (其角色合并值),则标记不一致 +SELECT DISTINCT o.staff_id +FROM staff_permission_overrides o +JOIN permission_defs pd ON pd.id = o.permission_def_id +WHERE NOT EXISTS ( + -- 简化示例:实际需在应用层对比合并结果 + SELECT 1 FROM role_permissions rp + JOIN staff_roles sr ON sr.role_id = rp.role_id + WHERE sr.staff_id = o.staff_id + AND rp.permission_def_id = o.permission_def_id + AND rp.value = o.value +); +``` + +**实现建议**:查询时应用层计算,结果缓存到 `perm:inconsistent:{schema}:{staff_id}` (TTL=300),变更时失效。 + +### 5.4 查询某角色的应用人数(PRD 5.5.1) + +```sql +SELECT COUNT(DISTINCT staff_id) AS applied_count +FROM staff_roles +WHERE role_id = :role_id + AND (valid_until IS NULL OR valid_until >= CURRENT_DATE); +``` + +### 5.5 按模块懒加载权限面板(PRD 性能考量) + +```sql +-- 一次只加载单模块的权限定义与当前角色/员工的值 +SELECT pd.*, rp.value AS role_value, spo.value AS override_value +FROM permission_defs pd +LEFT JOIN role_permissions rp + ON rp.permission_def_id = pd.id AND rp.role_id = :role_id +LEFT JOIN staff_permission_overrides spo + ON spo.permission_def_id = pd.id AND spo.staff_id = :staff_id +WHERE pd.module = :module AND pd.is_active = TRUE +ORDER BY pd.sub_module, pd.group_name, pd.sort_order; +``` + +--- + +## 六、Redis 缓存策略 + +| Cache Key | TTL | 失效触发 | +|-----------|-----|---------| +| `perm:v{VER}:{schema}:{staff_id}` | 3600s | `invalidate_staff_cache()`:Override/StaffRole 变更 | +| `perm:v{VER}:{schema}:role:{role_id}:staff_ids` | 3600s | 角色权限变更时用 Pipeline 批量失效该 role 下所有 staff 缓存(Issue #8) | +| `perm:inconsistent:{schema}:{staff_id}` | 300s | 同上 | +| `perm:defs:{schema}` | 86400s | PermissionDef 变更(低频) | +| `perm:role_applied_count:{schema}:{role_id}` | 600s | StaffRole 变更 | + +**版本号机制**(Issue #7):`CACHE_VERSION` 在 Django settings 中,升级 PermissionDef 结构时 bump,一键全局失效。 + +**批量失效优化**: +```python +def invalidate_role_cache(role_id: int, tenant_schema: str): + staff_ids = StaffRole.objects.filter(role_id=role_id).values_list("staff_id", flat=True) + pipe = _redis.pipeline() + for sid in staff_ids: + pipe.delete(f"perm:v{CACHE_VERSION}:{tenant_schema}:{sid}") + pipe.delete(f"perm:inconsistent:{tenant_schema}:{sid}") + pipe.execute() # 单次往返 +``` + +--- + +## 七、Django Model 要点 + +```python +# apps/permissions/models.py (精简示例,完整代码见 TECH_STACK/权限管理系统技术方案.md) + +from django.db import models +from django.contrib.postgres.fields import ArrayField +from apps.core.models.base import TimeStampedModel, SoftDeleteModel + +class ValueType(models.TextChoices): + BOOLEAN = "BOOLEAN", "开关型" + SCOPE = "SCOPE", "范围型" + INTEGER = "INTEGER", "数值型" + +class ScopeLevel(models.IntegerChoices): + NONE = 0, "无" + SELF = 1, "本人" + GROUP = 2, "本组" + STORE = 3, "本门店" + AREA = 4, "本区域" + REGION = 5, "本大区" + COMPANY = 6, "全公司" + +class PermissionDef(TimeStampedModel): + code = models.CharField(max_length=150, unique=True) + module = models.CharField(max_length=50) + sub_module = models.CharField(max_length=50, blank=True) + group_name = models.CharField(max_length=100) + name = models.CharField(max_length=200) + description = models.TextField(blank=True) + value_type = models.CharField(max_length=20, choices=ValueType.choices) + scope_choices = models.JSONField(default=list, blank=True) + integer_min = models.IntegerField(null=True, blank=True) + integer_max = models.IntegerField(null=True, blank=True) + default_value = models.JSONField(default=dict) + max_allowed_categories = ArrayField(models.CharField(max_length=50), default=list, blank=True) + sort_order = models.PositiveIntegerField(default=0) + is_active = models.BooleanField(default=True) + is_deprecated = models.BooleanField(default=False) + version = models.IntegerField(default=1) + + class Meta: + db_table = "permission_defs" + indexes = [ + models.Index(fields=["module", "sub_module", "sort_order"]), + ] + +# ... Role / RolePermission / StaffRole / StaffPermissionOverride / StaffDataScope +# ... PermissionChangeLog(append-only,override save() 禁止 update/delete) +``` + +--- + +## 八、关键业务规则与约束 + +### 8.1 批量设置角色(PRD Story 2) + +```python +def bulk_assign_role(staff_ids: list, role_id: UUID, operator, override_individual=False): + """ + override_individual=True 时清除员工所有个人 Override(PRD 提示"将覆盖个人自定义权限") + """ + with transaction.atomic(): + # 1. 清除旧主角色 + StaffRole.objects.filter(staff_id__in=staff_ids, is_primary=True).update(is_primary=False) + # 2. 批量 UPSERT 新主角色 + for sid in staff_ids: + sr, _ = StaffRole.objects.update_or_create( + staff_id=sid, role_id=role_id, + defaults={"is_primary": True, "assigned_by": operator} + ) + PermissionChangeLog.objects.create( + target_type="staff_role", target_id=sr.id, + staff_id=sid, role_id=role_id, action="assign", + operator_id=operator.id + ) + # 3. 可选清除个人 Override + if override_individual: + StaffPermissionOverride.objects.filter(staff_id__in=staff_ids).delete() + # 4. 批量失效缓存 + invalidate_staff_cache_batch(staff_ids, tenant_schema) +``` + +### 8.2 角色删除前置校验 + +```python +def delete_role(role: Role, operator): + applied_count = StaffRole.objects.filter(role=role).count() + if applied_count > 0: + raise ValidationError(f"无法删除:仍有 {applied_count} 位员工使用此角色,请先迁移") + if role.is_system_builtin: + raise ValidationError("系统内置角色不可删除") + role.deleted_at = timezone.now() + role.save() + PermissionChangeLog.objects.create( + target_type="role", target_id=role.id, role_id=role.id, + action="delete", operator_id=operator.id, + old_value={"name": role.name, "category": role.category} + ) +``` + +### 8.3 角色类别与权限项的可配置约束(Issue #11) + +```python +def get_editable_permissions_for_role(role: Role): + """编辑角色权限时,只返回该角色类别允许配置的权限项""" + return PermissionDef.objects.filter( + is_active=True + ).filter( + # max_allowed_categories 为空 或 包含当前角色类别 + models.Q(max_allowed_categories=[]) | + models.Q(max_allowed_categories__contains=[role.category]) + ) +``` + +--- + +## 九、与现有 DATA_MODEL 的集成契约 + +| 接入点 | 契约 | +|--------|------| +| `Staff` | 新增 method `get_permission_checker()` 返回 `PermissionChecker` 实例 | +| `OrgUnit` | 新增 method `ancestor_of_type(type: str)` 返回指定类型的祖先节点(供 SCOPE → store/area 定位) | +| `Property.objects` | Manager 新增 `visible_to(staff)` 方法,内部用 `ScopeQueryBuilder` | +| `Client.objects` | 同上,额外 AND 保护客规则 | +| `apps/core/middleware` | Request 级注入 `request.permission_checker`,供 View/template 使用 | + +--- + +## 十、禁止操作 + +- ❌ **严禁直接 INSERT/UPDATE `permission_change_logs` 跳过业务逻辑** — 所有权限变更必须通过 service 层触发日志写入 +- ❌ **严禁 DELETE `permission_change_logs`** — 审计要求不可删除 +- ❌ **严禁绕过 `ScopeQueryBuilder` 在 View 直接构造 Q** — 会导致越权漏洞,且无法统一审计 +- ❌ **严禁在 Application 层假设 `default_value` 的具体值** — 所有判断必须经 `PermissionChecker` +- ❌ **严禁 `StaffPermissionOverride` 与 `RolePermission` 值完全相同的冗余记录** — 保存时必须先做差异对比(稀疏存储) +- ❌ **严禁硬删除 `PermissionDef`** — 设置 `is_active=FALSE`,历史数据依赖 `code` 保留 +- ❌ **严禁修改 `PermissionDef.code`** — code 是业务代码的硬依赖,变更需走新建 code + 下线旧 code 流程 +- ❌ **严禁修改 `StaffRole.is_primary`** 绕过唯一约束(必须通过 signal 维护) +- ❌ **严禁跨租户查询权限数据** — 所有查询必须通过 `django-tenants` 的 connection schema 路由 + +--- + +## 十一、数据量与性能预测 + +| 表 | 预估行数 | 增长 | 优化策略 | +|----|---------|------|---------| +| `permission_defs` | 300-500 | 低 | 全表缓存 | +| `roles` | 10-50 per tenant | 低 | 无需优化 | +| `role_permissions` | 50-150 per role | 低 | 按 role_id 聚合缓存 | +| `staff_roles` | 1-3 per staff | 低 | 主键即可 | +| `staff_permission_overrides` | 5-50 per staff(稀疏)| 中 | 按 staff_id 缓存 | +| `staff_data_scopes` | 1-5 per staff | 低 | 结合 org_units.path 查询 | +| `permission_change_logs` | 10万-100万+ | **高** | 月度 RANGE 分区,6个月前数据归档冷存储 | + +--- + +## 十二、迁移与初始化清单 + +1. **Migration 001** — 创建 7 张核心表 + 索引 + 触发器 +2. **Data Migration 002** — 从 `fixtures/permission_defs.json` 加载 300 条 PermissionDef 初始数据 +3. **Data Migration 003** — 创建系统内置角色:`最大权限角色`(`is_system_builtin=TRUE`, category=`director`) +4. **Data Migration 004** — 为所有现有员工创建 `scope_type='self'` 的默认 DataScope +5. **Hook** — `Tenant.create` 后钩子自动执行 002/003/004 +6. **信号注册** — Staff 创建时自动 DataScope=self;Role 权限变更 → 缓存失效;Override 保存 → 日志 +7. **权限定义版本管理** — 新增 CLI:`python manage.py sync_permission_defs --fixture-version=1.2` + +--- + +## 十三、开放问题与后续迭代 + +| 问题 | 建议方向 | +|------|---------| +| 权限申请工作流(PRD 非目标) | v2 引入 `permission_requests` 表 + 审批流引擎 | +| 行级权限(如指定小区的房源) | v2 基于 PostgreSQL RLS(Row-Level Security)或 Guardian | +| 权限变更日志看板(PRD 非目标) | v1.5 基于 `permission_change_logs` + Grafana | +| IP 白名单/登录时段 | v2 独立的"安全策略"模块 | +| 跨租户权限(加盟商/联合门店) | v3,需引入联邦身份方案 | + +--- + +_本文档版本 v1.1 | 作者: Backend Architect | 更新时间 2026-04-24_ + +--- + +## 附录 A — 决策确认记录(v1.1 追加) + +| 议题 | 决策 | 日期 | 说明 | +|------|------|------|------| +| **Issue #3**:`permission_defs` 归属 schema | ✅ **`tenant_apps`(每租户独立)** | 2026-04-24 | 避免 django-tenants 跨 schema FK 限制;预留"企业版自定义权限项"能力;通过 `Tenant.create` 后钩子 seed 初始 300 条 | +| **Issue #6**:`staff_permission_overrides.override_mode` 默认值 | ✅ **`REPLACE`** | 2026-04-24 | 契合 PRD "个人特殊授权" 语义;管理员若需限制/扩展可显式选择 RESTRICT/GRANT | + +--- + +## 附录 B — PermissionDef 种子数据样例 + +> 完整 fixture(约 300 条)待 Django 项目 bootstrap 后落地至 `apps/permissions/fixtures/permission_defs.json`。 +> 本附录仅给出**代表性条目**(覆盖 BOOLEAN/SCOPE/INTEGER 三种 value_type + 主要模块)作为结构模板与 code 命名规范参考。 + +```json +[ + { + "code": "home.today_new_property.show", + "module": "home", + "sub_module": "", + "group_name": "首页基础权限", + "name": "今日新上房源是否显示", + "description": "控制首页小组件「今日新上」房源是否对该用户可见", + "value_type": "BOOLEAN", + "default_value": {"v": true}, + "max_allowed_categories": [], + "sort_order": 10 + }, + { + "code": "property.list.view.scope", + "module": "property", + "sub_module": "二手&租赁", + "group_name": "房源列表权限", + "name": "查看房源范围", + "description": "控制房源列表页可见的数据范围;与保护房业务规则叠加生效", + "value_type": "SCOPE", + "scope_choices": ["none", "self", "group", "store", "area", "region", "company"], + "default_value": {"v": "self"}, + "max_allowed_categories": [], + "sort_order": 10 + }, + { + "code": "property.list.export.scope", + "module": "property", + "sub_module": "二手&租赁", + "group_name": "房源列表权限", + "name": "导出房源范围", + "value_type": "SCOPE", + "scope_choices": ["none", "self", "store", "company"], + "default_value": {"v": "none"}, + "max_allowed_categories": ["store_manager", "director"], + "sort_order": 20 + }, + { + "code": "property.owner_phone.view.daily_limit", + "module": "property", + "sub_module": "二手&租赁", + "group_name": "房源隐私权限", + "name": "每日查看业主电话次数上限", + "description": "0 表示不限制;达到上限后次日自然恢复", + "value_type": "INTEGER", + "integer_min": 0, + "integer_max": 999, + "default_value": {"v": 20}, + "max_allowed_categories": [], + "sort_order": 10 + }, + { + "code": "client.private.view.scope", + "module": "client", + "sub_module": "私客", + "group_name": "私客基础权限", + "name": "查看私客范围", + "description": "与保护客规则叠加:非 owner/合保人即使有 store 权限也不可见保护客", + "value_type": "SCOPE", + "scope_choices": ["none", "self", "group", "store", "area", "region", "company"], + "default_value": {"v": "self"}, + "max_allowed_categories": [], + "sort_order": 10 + }, + { + "code": "client.private.transfer.allow", + "module": "client", + "sub_module": "私客", + "group_name": "私客操作权限", + "name": "是否允许转移私客", + "value_type": "BOOLEAN", + "default_value": {"v": false}, + "max_allowed_categories": ["store_manager", "director"], + "sort_order": 20 + }, + { + "code": "transaction.contract.approve.allow", + "module": "transaction", + "sub_module": "合同", + "group_name": "合同审批权限", + "name": "是否可审批合同", + "value_type": "BOOLEAN", + "default_value": {"v": false}, + "max_allowed_categories": ["store_manager", "director"], + "sort_order": 10 + }, + { + "code": "data.report.performance.view.scope", + "module": "data", + "sub_module": "业绩报表", + "group_name": "业绩数据权限", + "name": "业绩报表可见范围", + "value_type": "SCOPE", + "scope_choices": ["none", "self", "group", "store", "area", "company"], + "default_value": {"v": "self"}, + "max_allowed_categories": [], + "sort_order": 10 + }, + { + "code": "hr.staff.create.allow", + "module": "hr", + "sub_module": "员工管理", + "group_name": "员工操作权限", + "name": "是否可新建员工", + "value_type": "BOOLEAN", + "default_value": {"v": false}, + "max_allowed_categories": ["store_manager", "director", "operator"], + "sort_order": 10 + }, + { + "code": "system.role.manage.allow", + "module": "system", + "sub_module": "权限管理", + "group_name": "角色管理权限", + "name": "是否可管理角色与权限", + "description": "授予此权限者可进入角色管理页;系统管理员 is_system_admin=TRUE 自动拥有", + "value_type": "BOOLEAN", + "default_value": {"v": false}, + "max_allowed_categories": ["director", "operator"], + "sort_order": 10 + } +] +``` + +**Seed 覆盖清单**(Django 项目落地时需补齐): + +| 模块 | 预计条目数 | 典型权限项 | +|------|-----------|-----------| +| home | ~15 | 小组件显示/隐藏开关 | +| property | ~60 | 列表/详情/导出/隐私/分享/操作 × SCOPE/BOOLEAN | +| new_house | ~30 | 楼盘/房型/佣金/报备 | +| client | ~50 | 私客/公客/保护客 × 查看/转移/释放/导出 | +| transaction | ~40 | 合同/审批/佣金/收付款 | +| data | ~30 | 业绩/排行/漏斗/业务报表 | +| marketing | ~20 | 营销活动/素材/投放 | +| hr | ~25 | 员工/组织/考勤/调整 | +| contract/trinet/system/mobile/smart_store/recharge | ~30 | 模块特定 | +| **合计** | **~300** | | + +--- + +## 附录 C — Migration DDL 草图 + +> 以下为 Django migration 的 **等价 DDL**,用于评审与 DBA 沟通。实际实现以 `apps/permissions/migrations/000X_*.py` 为准。 + +### C.1 Migration 0001 — 核心 7 张表 + +```sql +-- permission_defs +CREATE TABLE permission_defs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + code VARCHAR(150) NOT NULL UNIQUE, + module VARCHAR(50) NOT NULL, + sub_module VARCHAR(50) NOT NULL DEFAULT '', + group_name VARCHAR(100) NOT NULL, + name VARCHAR(200) NOT NULL, + description TEXT NOT NULL DEFAULT '', + value_type VARCHAR(20) NOT NULL CHECK (value_type IN ('BOOLEAN','SCOPE','INTEGER')), + scope_choices JSONB NOT NULL DEFAULT '[]'::jsonb, + integer_min INTEGER, + integer_max INTEGER, + default_value JSONB NOT NULL DEFAULT '{"v":false}'::jsonb, + max_allowed_categories VARCHAR(50)[] NOT NULL DEFAULT ARRAY[]::VARCHAR[], + sort_order INTEGER NOT NULL DEFAULT 0, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + is_deprecated BOOLEAN NOT NULL DEFAULT FALSE, + version INTEGER NOT NULL DEFAULT 1, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + CONSTRAINT chk_code_format CHECK (code ~ '^[a-z_]+\.[a-z_]+(\.[a-z_]+){1,2}$') +); +CREATE INDEX idx_permission_defs_module ON permission_defs(module, sub_module, sort_order) WHERE is_active = TRUE; +CREATE INDEX idx_permission_defs_active ON permission_defs(is_active) WHERE is_active = TRUE; + +-- roles +CREATE TABLE roles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(100) NOT NULL, + category VARCHAR(30) NOT NULL CHECK (category IN ('agent','store_manager','director','operator','custom')), + description TEXT NOT NULL DEFAULT '', + template_role_id UUID REFERENCES roles(id) ON DELETE SET NULL, + is_system_builtin BOOLEAN NOT NULL DEFAULT FALSE, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_by UUID REFERENCES staff(id) ON DELETE SET NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_by UUID REFERENCES staff(id) ON DELETE SET NULL, + deleted_at TIMESTAMPTZ +); +CREATE UNIQUE INDEX idx_roles_name_active ON roles(name) WHERE deleted_at IS NULL; +CREATE INDEX idx_roles_category ON roles(category) WHERE deleted_at IS NULL; +CREATE INDEX idx_roles_template ON roles(template_role_id); + +-- role_permissions +CREATE TABLE role_permissions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE, + permission_def_id UUID NOT NULL REFERENCES permission_defs(id) ON DELETE RESTRICT, + value JSONB NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_by UUID REFERENCES staff(id) ON DELETE SET NULL +); +CREATE UNIQUE INDEX idx_role_permissions_uniq ON role_permissions(role_id, permission_def_id); +CREATE INDEX idx_role_permissions_role ON role_permissions(role_id); +CREATE INDEX idx_role_permissions_def ON role_permissions(permission_def_id); + +-- staff_roles +CREATE TABLE staff_roles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + staff_id UUID NOT NULL REFERENCES staff(id) ON DELETE CASCADE, + role_id UUID NOT NULL REFERENCES roles(id) ON DELETE RESTRICT, + is_primary BOOLEAN NOT NULL DEFAULT FALSE, + assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + assigned_by UUID REFERENCES staff(id) ON DELETE SET NULL, + valid_from DATE, + valid_until DATE +); +CREATE UNIQUE INDEX idx_staff_roles_uniq ON staff_roles(staff_id, role_id); +CREATE UNIQUE INDEX idx_staff_roles_primary ON staff_roles(staff_id) WHERE is_primary = TRUE; +CREATE INDEX idx_staff_roles_role ON staff_roles(role_id); + +-- staff_permission_overrides +CREATE TABLE staff_permission_overrides ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + staff_id UUID NOT NULL REFERENCES staff(id) ON DELETE CASCADE, + permission_def_id UUID NOT NULL REFERENCES permission_defs(id) ON DELETE RESTRICT, + value JSONB NOT NULL, + override_mode VARCHAR(10) NOT NULL DEFAULT 'REPLACE' + CHECK (override_mode IN ('REPLACE','RESTRICT','GRANT')), + reason TEXT NOT NULL DEFAULT '', + modified_by UUID REFERENCES staff(id) ON DELETE SET NULL, + modified_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +CREATE UNIQUE INDEX idx_staff_overrides_uniq ON staff_permission_overrides(staff_id, permission_def_id); +CREATE INDEX idx_staff_overrides_staff ON staff_permission_overrides(staff_id); + +-- staff_data_scopes +CREATE TABLE staff_data_scopes ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + staff_id UUID NOT NULL REFERENCES staff(id) ON DELETE CASCADE, + scope_type VARCHAR(20) NOT NULL + CHECK (scope_type IN ('self','group','store','area','region','company','custom_unit')), + org_unit_id UUID REFERENCES org_units(id) ON DELETE RESTRICT, + is_readable BOOLEAN NOT NULL DEFAULT TRUE, + is_writable BOOLEAN NOT NULL DEFAULT FALSE, + granted_by UUID REFERENCES staff(id) ON DELETE SET NULL, + granted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + expires_at TIMESTAMPTZ, + reason TEXT NOT NULL DEFAULT '', + CONSTRAINT chk_custom_unit_has_org CHECK ( + (scope_type = 'custom_unit' AND org_unit_id IS NOT NULL) OR + (scope_type <> 'custom_unit') + ) +); +CREATE INDEX idx_data_scopes_staff ON staff_data_scopes(staff_id); +CREATE INDEX idx_data_scopes_org ON staff_data_scopes(org_unit_id); +CREATE INDEX idx_data_scopes_expires ON staff_data_scopes(expires_at) WHERE expires_at IS NOT NULL; + +-- permission_change_logs (append-only, no deleted_at) +CREATE TABLE permission_change_logs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + target_type VARCHAR(30) NOT NULL + CHECK (target_type IN ('role','role_permission','staff_role','staff_override','staff_scope')), + target_id UUID NOT NULL, + staff_id UUID REFERENCES staff(id) ON DELETE SET NULL, + role_id UUID REFERENCES roles(id) ON DELETE SET NULL, + permission_code VARCHAR(150), + action VARCHAR(20) NOT NULL + CHECK (action IN ('create','update','delete','assign','revoke')), + old_value JSONB, + new_value JSONB, + operator_id UUID NOT NULL REFERENCES staff(id) ON DELETE RESTRICT, + operator_ip INET, + user_agent TEXT, + reason TEXT NOT NULL DEFAULT '', + operated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +CREATE INDEX idx_perm_log_staff ON permission_change_logs(staff_id, operated_at DESC) WHERE staff_id IS NOT NULL; +CREATE INDEX idx_perm_log_role ON permission_change_logs(role_id, operated_at DESC) WHERE role_id IS NOT NULL; +CREATE INDEX idx_perm_log_target ON permission_change_logs(target_type, target_id, operated_at DESC); +CREATE INDEX idx_perm_log_operator ON permission_change_logs(operator_id, operated_at DESC); +CREATE INDEX idx_perm_log_time ON permission_change_logs(operated_at DESC); + +-- 拒绝 UPDATE/DELETE 的 rule(兜底,应用层已禁止) +CREATE RULE no_update_perm_log AS ON UPDATE TO permission_change_logs DO INSTEAD NOTHING; +CREATE RULE no_delete_perm_log AS ON DELETE TO permission_change_logs DO INSTEAD NOTHING; +``` + +### C.2 Migration 0002 — Seed PermissionDef(data migration) + +```python +# 伪代码 +def forwards(apps, schema_editor): + PermissionDef = apps.get_model("permissions", "PermissionDef") + data = json.load(open("apps/permissions/fixtures/permission_defs.json")) + PermissionDef.objects.bulk_create([PermissionDef(**row) for row in data]) +``` + +### C.3 Migration 0003 — Seed 系统内置角色 + +```python +def forwards(apps, schema_editor): + Role = apps.get_model("permissions", "Role") + RolePermission = apps.get_model("permissions", "RolePermission") + PermissionDef = apps.get_model("permissions", "PermissionDef") + + super_role = Role.objects.create( + name="最大权限角色", + category="director", + is_system_builtin=True, + description="系统内置,拥有所有权限项的最高值" + ) + # 将所有 SCOPE 权限设为 company,所有 BOOLEAN 设为 true,所有 INTEGER 设为 0(不限) + rows = [] + for pd in PermissionDef.objects.filter(is_active=True): + if pd.value_type == "BOOLEAN": + v = {"v": True} + elif pd.value_type == "SCOPE": + v = {"v": "company"} + else: + v = {"v": 0} + if v == pd.default_value: + continue # 稀疏存储 + rows.append(RolePermission(role=super_role, permission_def=pd, value=v)) + RolePermission.objects.bulk_create(rows) +``` + +### C.4 Migration 0004 — 为现有员工创建默认 DataScope(self) + +```python +def forwards(apps, schema_editor): + Staff = apps.get_model("org", "Staff") + StaffDataScope = apps.get_model("permissions", "StaffDataScope") + rows = [ + StaffDataScope(staff_id=sid, scope_type="self", is_readable=True, is_writable=True, reason="初始化") + for sid in Staff.objects.values_list("id", flat=True) + ] + StaffDataScope.objects.bulk_create(rows, ignore_conflicts=True) +``` + +### C.5 Tenant.create 后钩子 + +```python +# apps/tenants/signals.py +from django_tenants.utils import tenant_context + +@receiver(post_save, sender=Tenant) +def init_tenant_permissions(sender, instance, created, **kwargs): + if not created: + return + with tenant_context(instance): + call_command("migrate", "permissions") # 0001 + call_command("loaddata", "permission_defs") # 等价于 0002 + seed_builtin_roles() # 0003 逻辑 + seed_default_data_scopes() # 0004 逻辑 +``` + +--- + +## 附录 D — PermissionChecker 伪代码 + +```python +# apps/permissions/services/checker.py + +from dataclasses import dataclass +from django.core.cache import cache +from django.conf import settings + +CACHE_VERSION = getattr(settings, "PERMISSION_CACHE_VERSION", 1) + +@dataclass +class ResolvedValue: + code: str + value_type: str # BOOLEAN / SCOPE / INTEGER + value: object # bool / str / int + source: str # "admin_shortcircuit" / "override" / "role_merge" / "default" + +class PermissionChecker: + def __init__(self, staff): + self.staff = staff + self._resolved: dict[str, ResolvedValue] | None = None + + # ---------- 公共 API ---------- + def has(self, code: str) -> bool: + r = self._get(code) + if r.value_type == "BOOLEAN": + return bool(r.value) + if r.value_type == "SCOPE": + return r.value != "none" + if r.value_type == "INTEGER": + return r.value != 0 and r.value is not None + return False + + def get_scope(self, code: str) -> str: + r = self._get(code) + assert r.value_type == "SCOPE" + return r.value + + def get_int(self, code: str) -> int: + r = self._get(code) + assert r.value_type == "INTEGER" + return r.value + + # ---------- 解析核心 ---------- + def _get(self, code: str) -> ResolvedValue: + if self._resolved is None: + self._resolved = self._resolve_all() + if code not in self._resolved: + raise KeyError(f"Unknown permission code: {code}") + return self._resolved[code] + + def _resolve_all(self) -> dict[str, ResolvedValue]: + # 1) 系统管理员短路 + if self.staff.is_system_admin: + return self._build_admin_values() + + # 2) Redis cache + schema = connection.schema_name + key = f"perm:v{CACHE_VERSION}:{schema}:{self.staff.id}" + cached = cache.get(key) + if cached: + return cached + + # 3) 加载所有 PermissionDef + defs = {pd.code: pd for pd in PermissionDef.objects.filter(is_active=True)} + + # 4) 加载员工所有角色权限值 + role_ids = list(StaffRole.objects.filter(staff=self.staff).values_list("role_id", flat=True)) + role_values: dict[str, list] = {} # code -> [value, value, ...] + qs = RolePermission.objects.filter(role_id__in=role_ids).select_related("permission_def") + for rp in qs: + role_values.setdefault(rp.permission_def.code, []).append(rp.value["v"]) + + # 5) 合并多角色值 + resolved: dict[str, ResolvedValue] = {} + for code, pd in defs.items(): + values = role_values.get(code, []) + if not values: + merged = pd.default_value["v"] + source = "default" + else: + merged = self._merge(pd.value_type, values) + source = "role_merge" + resolved[code] = ResolvedValue(code, pd.value_type, merged, source) + + # 6) 叠加 Override + overrides = StaffPermissionOverride.objects.filter(staff=self.staff).select_related("permission_def") + for o in overrides: + code = o.permission_def.code + current = resolved[code] + new_val = self._apply_override(current, o.value["v"], o.override_mode) + resolved[code] = ResolvedValue(code, current.value_type, new_val, "override") + + # 7) 缓存 + cache.set(key, resolved, timeout=3600) + return resolved + + @staticmethod + def _merge(value_type: str, values: list): + if value_type == "BOOLEAN": + return any(values) + if value_type == "SCOPE": + return max(values, key=lambda s: SCOPE_LEVEL[s]) + if value_type == "INTEGER": + # 0 视为 +∞ + if 0 in values: + return 0 + return max(values) + + @staticmethod + def _apply_override(current: ResolvedValue, ov: object, mode: str): + if mode == "REPLACE": + return ov + if mode == "RESTRICT": + if current.value_type == "BOOLEAN": + return current.value and ov + if current.value_type == "SCOPE": + return min([current.value, ov], key=lambda s: SCOPE_LEVEL[s]) + if current.value_type == "INTEGER": + # 0=+∞,RESTRICT 取最小正值 + a, b = current.value, ov + if a == 0: return b + if b == 0: return a + return min(a, b) + if mode == "GRANT": + if current.value_type == "BOOLEAN": + return current.value or ov + if current.value_type == "SCOPE": + return max([current.value, ov], key=lambda s: SCOPE_LEVEL[s]) + if current.value_type == "INTEGER": + if 0 in (current.value, ov): return 0 + return max(current.value, ov) + + def _build_admin_values(self) -> dict[str, ResolvedValue]: + defs = PermissionDef.objects.filter(is_active=True) + out = {} + for pd in defs: + if pd.value_type == "BOOLEAN": + v = True + elif pd.value_type == "SCOPE": + v = "company" + else: + v = 0 + out[pd.code] = ResolvedValue(pd.code, pd.value_type, v, "admin_shortcircuit") + return out +``` + +--- + +## 附录 E — 缓存失效服务 + +```python +# apps/permissions/services/cache.py + +from django.db import connection +from django.core.cache import cache +from django.conf import settings + +CACHE_VERSION = getattr(settings, "PERMISSION_CACHE_VERSION", 1) + +def _schema() -> str: + return connection.schema_name + +def invalidate_staff(staff_id) -> None: + s = _schema() + cache.delete_many([ + f"perm:v{CACHE_VERSION}:{s}:{staff_id}", + f"perm:inconsistent:{s}:{staff_id}", + ]) + +def invalidate_staff_batch(staff_ids) -> None: + s = _schema() + keys = [] + for sid in staff_ids: + keys.append(f"perm:v{CACHE_VERSION}:{s}:{sid}") + keys.append(f"perm:inconsistent:{s}:{sid}") + cache.delete_many(keys) + +def invalidate_role(role_id) -> None: + """角色权限变更 → 该角色下所有员工缓存失效""" + from apps.permissions.models import StaffRole + staff_ids = list( + StaffRole.objects.filter(role_id=role_id).values_list("staff_id", flat=True) + ) + invalidate_staff_batch(staff_ids) + s = _schema() + cache.delete(f"perm:v{CACHE_VERSION}:{s}:role:{role_id}:staff_ids") + cache.delete(f"perm:role_applied_count:{s}:{role_id}") + +def invalidate_all_tenant() -> None: + """极端情况(PermissionDef 批量变更)— 走版本号 bump 更高效""" + cache.delete(f"perm:defs:{_schema()}") + # 不推荐遍历删除;生产上应 bump settings.PERMISSION_CACHE_VERSION +``` + +**信号连接**(在 `apps.py` ready() 中注册): + +| 信号 | 触发失效 | +|------|---------| +| `post_save` / `post_delete` on `RolePermission` | `invalidate_role(instance.role_id)` | +| `post_save` / `post_delete` on `StaffRole` | `invalidate_staff(instance.staff_id)` + 角色应用数缓存 | +| `post_save` / `post_delete` on `StaffPermissionOverride` | `invalidate_staff(instance.staff_id)` | +| `post_save` / `post_delete` on `StaffDataScope` | `invalidate_staff(instance.staff_id)` | +| `post_save` on `PermissionDef` (version bump) | `invalidate_all_tenant()` + 建议 bump `CACHE_VERSION` | + +--- + +## 附录 F — Manager 扩展(Property / Client) + +> 契合 §九集成契约,提供对外统一的 `visible_to(staff)` 入口,**禁止业务代码直接写过滤 Q**。 + +```python +# apps/property/managers.py +from django.db import models +from apps.permissions.services.scope_query import ScopeQueryBuilder + +class PropertyQuerySet(models.QuerySet): + def visible_to(self, staff): + return ScopeQueryBuilder( + staff=staff, + permission_code="property.list.view.scope", + field_prefix="seller_agent", + ).apply_to(self) + + def writable_by(self, staff): + # 编辑权限:基于同一 scope 字段 + property.list.edit.allow BOOLEAN + checker = staff.get_permission_checker() + if not checker.has("property.list.edit.allow"): + return self.none() + return self.visible_to(staff) + +class PropertyManager(models.Manager.from_queryset(PropertyQuerySet)): + pass + + +# apps/client/managers.py +class ClientQuerySet(models.QuerySet): + def visible_to(self, staff): + # ScopeQueryBuilder.apply_to() 内部已处理 is_protected 过滤 + return ScopeQueryBuilder( + staff=staff, + permission_code="client.private.view.scope", + field_prefix="owner", + ).apply_to(self) +``` + +**使用示例**: + +```python +# views.py +def property_list(request): + qs = Property.objects.visible_to(request.staff) + # ... 继续其他业务过滤 ... +``` + +--- + +## 附录 G — 验收 Checklist(开发启动前) + +- [x] Issue #3 决策:`permission_defs` 归属 `tenant_apps` +- [x] Issue #6 决策:override 默认 `REPLACE` +- [ ] 完整 `permission_defs.json` fixture(~300 条)落地 +- [ ] Migration 0001-0004 实现并 dry-run 通过 +- [ ] `Tenant.create` post_save signal 接入 seed 逻辑 +- [ ] `PermissionChecker` / `ScopeQueryBuilder` / 缓存失效服务单测覆盖 ≥ 80% +- [ ] `Property` / `Client` Manager 接入 `visible_to` +- [ ] `staff.is_system_admin=TRUE` 短路逻辑 e2e 测试 +- [ ] 保护客/保护房过滤规则 e2e 测试 +- [ ] `permission_change_logs` 禁止 UPDATE/DELETE 的数据库级 RULE 验证 + +--- + +_v1.1 追加: 附录 A-G | 2026-04-24_ diff --git a/wiki/concepts/AWS-Source-Identity.md b/wiki/concepts/AWS-Source-Identity.md new file mode 100644 index 00000000..7a3b1da0 --- /dev/null +++ b/wiki/concepts/AWS-Source-Identity.md @@ -0,0 +1,66 @@ +--- +title: "AWS Source Identity" +type: concept +tags: [AWS, Security, IAM, Auditing, FinOps] +created: 2026-04-26 +updated: 2026-04-26 +sources: [Cloud & DevOps/Public-Cloud-Learning-Sessions/05_FinOps/public-cloud-learning-sessions-budget-control-20240319-160204-meeting-recording.md] +last_updated: 2026-04-26 +--- + +# AWS Source Identity + +> **Source Identity** 是 AWS STS(Security Token Service)的一个属性,通过 `sts:SourceIdentity` 在用户假设 IAM 角色时保留原始登录身份,使 CloudTrail 能够追踪联邦登录(Federated Login)跨角色切换的完整用户链。 + +## 定义 + +在 AWS 联邦身份认证场景中,用户通过身份提供商(IdP,如 NetIQ Access Manager)认证后,会假设多个 IAM 角色在不同账户间跳转。**默认情况下,CloudTrail 只记录假设角色后的角色身份,无法追溯到原始登录用户。** + +Source Identity 通过在 `AssumeRole` 请求中携带 `SourceIdentity` 参数,解决了这一问题: + +``` +aws sts assume-role \ + --role-arn arn:aws:iam::123456789012:role/MyRole \ + --source-identity alice@example.com +``` + +## 核心价值 + +| 维度 | 无 Source Identity | 有 Source Identity | +|------|-------------------|-------------------| +| 审计追踪 | 只能看到角色身份 | 可见原始用户身份 | +| FinOps 场景 | 无法关联账户支出到具体用户 | 可将成本责任追溯到个人 | +| 安全调查 | 难以定位跨角色操作的发起人 | 可完整还原操作路径 | +| 合规审计 | 不满足最小权限追溯要求 | 满足审计链要求 | + +## 在 FinOps 中的应用 + +在 [[Budget-Control-Automation]] 场景中,SRE Core 团队通过 Source Identity 实现: +- **用户维度的成本归因**:通过 CloudTrail + Source Identity 将每个 AWS API 调用关联到具体个人 +- **Top Users 报告**:利用 Cost Explorer 数据 + CloudTrail Source Identity 识别账户内日度支出最高的用户 +- **成本责任到人**:账户 owner 可精确定位哪些团队成员产生了异常支出 + +## 与 AWS 服务的集成 + +- **CloudTrail**:Source Identity 字段记录在 CloudTrail 日志的 `userIdentity` 块中 +- **STS (Security Token Service)**:`AssumeRole`、`AssumeRoleWithSAML`、`AssumeRoleWithWebIdentity` 均支持 Source Identity +- **Cost Explorer**:结合 Source Identity 数据可实现用户维度的成本分析 +- **AWS Budgets**:告警流程中的 Lambda 函数可查询 CloudTrail Source Identity 数据进行用户归因 + +## 关键约束 + +- Source Identity 只能**设置**,不能**覆盖**:一旦设置为某个值,在当前会话期间无法更改 +- Source Identity 有长度限制(最大 64 字符) +- 需要 IAM 角色显式授权 `sts:TagSession` 和 `sts:SourceIdentity` 权限才能使用 +- NetIQ Access Manager 等联邦 IdP 需要配置为在假设角色请求中传递 Source Identity + +## 相关概念 + +- [[CloudTrail]]:AWS 审计日志服务,Source Identity 使其具备跨角色用户追踪能力 +- [[IAM-Roles]]:Source Identity 在角色假设场景中使用 +- [[Federated-Identity]]:联邦身份管理(如 NetIQ),Source Identity 解决其跨角色追踪盲区 +- [[FinOps]]:FinOps 审计和成本归因需要 Source Identity 提供用户级可见性 + +## 来源 + +本概念页基于 [[public-cloud-learning-sessions-budget-control-20240319]](SRE Core 团队 Budget Control 自动化学习分享)中关于 Source Identity 实现细节的记录。 diff --git a/wiki/index.md b/wiki/index.md index d99765ab..30b77bd7 100644 --- a/wiki/index.md +++ b/wiki/index.md @@ -4,6 +4,9 @@ - [Overview](overview.md) — living synthesis ## Sources +- [2026-04-24] [Public Cloud Learning Sessions - Budget Control - 20240319](sources/public-cloud-learning-sessions-budget-control-20240319-160204-meeting-recording.md) +- [2026-04-24] [CTP Topic 63 Optimise resource cost using automation](sources/ctp-topic-63-optimise-resource-cost-using-automation.md) +- [2026-04-24] [Public Cloud Learning Sessions - Storage Cost Optimization - 20240305](sources/public-cloud-learning-sessions-storage-cost-optimization-20240305-160037-meeting.md) - [2026-04-24] [CTP Topic 71 PCG's guide to RightSizing, why, how when](sources/ctp-topic-71-pcgs-guide-to-rightsizing-why-how-when.md) - [2026-04-24] [Public Cloud Learning Sessions - Best practices for EC2 cost optimization in AWS - 20240529](sources/public-cloud-learning-sessions-best-practices-for-ec2-cost-optimization-in-aws-2.md) - [2026-04-24] [Public Cloud Learning Sessions - Reducing Cloud Costs - 20250318](sources/public-cloud-learning-sessions-reducing-cloud-costs-20250318-170100-meeting-reco.md) @@ -411,9 +414,6 @@ - [2026-04-19] [public-cloud-learning-sessions-introduction-to-artificial-intelligence-ai-machin](sources/public-cloud-learning-sessions-introduction-to-artificial-intelligence-ai-machin.md) — (expected: wiki/sources/public-cloud-learning-sessions-introduction-to-artificial-intelligence-ai-machin.md — source missing) - [2026-04-19] [cloud-learning-master-index](sources/cloud-learning-master-index.md) — (expected: wiki/sources/cloud-learning-master-index.md — source missing) - [2026-04-19] [ctp-topic-27-aws-instance-scheduler](sources/ctp-topic-27-aws-instance-scheduler.md) — (expected: wiki/sources/ctp-topic-27-aws-instance-scheduler.md — source missing) -- [2026-04-19] [public-cloud-learning-sessions-budget-control-20240319-160204-meeting-recording](sources/public-cloud-learning-sessions-budget-control-20240319-160204-meeting-recording.md) — (expected: wiki/sources/public-cloud-learning-sessions-budget-control-20240319-160204-meeting-recording.md — source missing) -- [2026-04-19] [ctp-topic-63-optimise-resource-cost-using-automation](sources/ctp-topic-63-optimise-resource-cost-using-automation.md) — (expected: wiki/sources/ctp-topic-63-optimise-resource-cost-using-automation.md — source missing) -- [2026-04-19] [public-cloud-learning-sessions-storage-cost-optimization-20240305-160037-meeting](sources/public-cloud-learning-sessions-storage-cost-optimization-20240305-160037-meeting.md) — (expected: wiki/sources/public-cloud-learning-sessions-storage-cost-optimization-20240305-160037-meeting.md — source missing) - [Your-AI-Isn-t-Stupid---It-Just-Needs-a-Better-Harness--Lychee-Technology-Engineering-Blog](sources/Your-AI-Isn-t-Stupid---It-Just-Needs-a-Better-Harness--Lychee-Technology-Engineering-Blog.md) — (expected: wiki/sources/Your-AI-Isn-t-Stupid---It-Just-Needs-a-Better-Harness--Lychee-Technology-Engineering-Blog.md — source missing) - [Expose-hermes-agent-as-an-OpenAI-compatible-API-for-any-frontend](sources/Expose-hermes-agent-as-an-OpenAI-compatible-API-for-any-frontend.md) — (expected: wiki/sources/Expose-hermes-agent-as-an-OpenAI-compatible-API-for-any-frontend.md — source missing) - [zk-steward](sources/zk-steward.md) — (expected: wiki/sources/zk-steward.md — source missing) @@ -802,6 +802,7 @@ - [Automated-Security-Audit](concepts/Automated-Security-Audit.md) - [Availability](concepts/Availability.md) - [AWS-Secrets-Manager](concepts/AWS-Secrets-Manager.md) +- [AWS-Source-Identity](concepts/AWS-Source-Identity.md) - [AWS-Tagging-Standards](concepts/AWS-Tagging-Standards.md) - [AWS-Tags](concepts/AWS-Tags.md) - [BEATS](concepts/BEATS.md) diff --git a/wiki/log.md b/wiki/log.md index 0893fe52..8e9bf850 100644 --- a/wiki/log.md +++ b/wiki/log.md @@ -1,3 +1,11 @@ +## [2026-04-25] ingest | CTP Topic 63 Optimise resource cost using automation +- Source file: Cloud & DevOps/Public-Cloud-Learning-Sessions/05_FinOps/ctp-topic-63-optimise-resource-cost-using-automation.md +- Status: ✅ 成功摄入 +- Summary: 使用自动化手段优化 AWS 云资源成本——五大策略:批准区域标准化、Graviton ARM 实例选型(比 Intel 便宜 20-25%)、承诺计划(1年 40% / 3年 64% 折扣)、GP2→GP3 存储优化(节省 20%)、基于标签的 EC2/RDS 自动化调度(每天只运行 10 小时可节省 70% 成本) +- Concepts created: 无(已存在的 [[Savings-Plans]] 涵盖承诺计划;Graviton/RightSizing 等概念在本 wiki 中出现频次不足以独立建页) +- Source page: wiki/sources/ctp-topic-63-optimise-resource-cost-using-automation.md +- Notes: Pushka 演示 Terraform Scheduler 模块配置(`auto_shutdown = yes` 标签);无内容冲突 + ## [2026-04-24] ingest | Public Cloud Learning Sessions - Best practices for EC2 cost optimization in AWS - 20240529 - Source file: Cloud & DevOps/Public-Cloud-Learning-Sessions/05_FinOps/public-cloud-learning-sessions-best-practices-for-ec2-cost-optimization-in-aws-2.md - Status: ✅ 成功摄入 @@ -9,6 +17,16 @@ - Notes: index.md 已更新(Sources 节新增条目);overview.md 已补充(FinOps 章节新增段落,置于 ctp-topic-13 后);Nitro-System 和 EC2-Purchase-Options 不存在于现有 Wiki,新建 Concept 页面;已建立与 public-cloud-learning-sessions-reducing-cloud-costs-20250318-170100-meeting-reco、ctp-topic-13-cloud-finops-policies 的 Connections 关系 - Conflicts: 与 ctp-topic-14-octane-hub-on-aws 可能的冲突(Graviton 对有状态服务的适用性),已记录于 Source page Contradictions 节 +## [2026-04-25] ingest | Public Cloud Learning Sessions - Storage Cost Optimization - 20240305 +- Source file: Cloud & DevOps/Public-Cloud-Learning-Sessions/05_FinOps/public-cloud-learning-sessions-storage-cost-optimization-20240305-160037-meeting.md +- Status: ✅ 成功摄入 +- Summary: AWS EBS(GP3 20% 节省+独立扩展 IOPS/吞吐)、EFS/FSx(生命周期分层)、S3(Intelligent Tiering 自动冷热迁移+生命周期策略+PrivateLink 规避数据传输费)、ADM 三阶段迁移案例(OpenZFS → 自管理 NetApp on EC2 → FSx for NetApp ONTAP 实现 60% 成本削减) +- Concepts linked: [[EBS-GP3]], [[EBS-Snapshot-Archive]], [[Data-Lifecycle-Manager]], [[AWS-Backup]], [[EFS-Infrequent-Access]], [[S3-Intelligent-Tiering]], [[S3-Lifecycle-Policies]], [[FSx-for-NetApp-ONTAP]], [[AWS-PrivateLink]], [[FinOps]], [[Cloud Cost Optimization]] +- Entities linked: [[AWS]], [[ADM]] +- Source page: wiki/sources/public-cloud-learning-sessions-storage-cost-optimization-20240305-160037-meeting.md +- Notes: index.md 已更新(Sources 节新增条目,置于 ctp-topic-71 前);overview.md 已补充(FinOps 章节新增存储成本优化专题段落);ADM 提及仅 1 次,以 wikilink 形式记录于 Source page;所有 AWS 服务特性概念(EBS-GP3/Snapshot-Archive/EFS-IA/S3-IntelligentTiering 等)已记录于 Source page Key Concepts 节,暂不单独建页;已建立与 public-cloud-learning-sessions-reducing-cloud-costs-20250318、ctp-topic-13-cloud-finops-policies 的 Connections 关系 +- Conflicts: 与 ctp-topic-14-octane-hub-on-aws 可能的 EFS vs EBS 选型冲突,已记录于 Source page Contradictions 节 + ## [2026-04-25] ingest | Public Cloud Learning Sessions - Reducing Cloud Costs - 20250318 - Source file: Cloud & DevOps/Public-Cloud-Learning-Sessions/05_FinOps/public-cloud-learning-sessions-reducing-cloud-costs-20250318-170100-meeting-reco.md - Status: ✅ 成功摄入 @@ -31,6 +49,16 @@ - Notes: PCG 和 Cloud Health 出现次数不足 2 次,不满足独立 Entity 页面创建条件,以 wikilink 形式记录于 Source page;index.md 已更新(替换 expected 条目为实际内容);overview.md Cloud Transformation 章节已补充(置于 ctp-topic-65 后);已建立与 ctp-topic-63(自动化调度优化)、ctp-topic-71(Rightsizing)、ctp-topic-27(AWS Instance Scheduler)的连接关系;FinOps 概念页已存在于 wiki/concepts/,无需新建 - Conflicts: 与 [[ctp-topic-53-why-bother-with-cloud]] 存在视角差异:Topic 13 假设已在云上聚焦优化,Topic 53 聚焦是否应迁移的决策论证;已在 Source page Contradictions 节记录 +## [2026-04-26] ingest | Public Cloud Learning Sessions - Budget Control - 20240319 +- Source file: Cloud & DevOps/Public-Cloud-Learning-Sessions/05_FinOps/public-cloud-learning-sessions-budget-control-20240319-160204-meeting-recording.md +- Status: ✅ 成功摄入 +- Summary: SRE Core 团队(Daniela/Evan/Alan)分享 AWS Budget Control 自动化——解决账户蔓延导致的成本失控。核心架构:AWS Budget → SNS → Lambda → Step Functions → SCP Enforcement(服务控制策略封禁新资源创建)。4 类告警:Forecast/Actual 80-98%/Severe/Enforcement。Source Identity 通过 CloudTrail 追踪联邦登录跨角色切换的原始用户身份。初始范围仅限 Lab 账户。 +- Concepts created: [[AWS-Source-Identity]] +- Concepts linked: [[FinOps]], [[SCP-Enforcement]], [[CloudTrail]], [[Step-Functions]], [[Cost-Explorer]], [[AWS-Budget-Alerts]] +- Entities linked: [[SRE-Core-Team]], [[Phenops-Team]], [[NetIQ]] +- Source page: wiki/sources/public-cloud-learning-sessions-budget-control-20240319-160204-meeting-recording.md +- Notes: index.md 已更新(Sources 节新增条目,Concepts 节新增 AWS-Source-Identity);overview.md 已补充(FinOps 章节新增段落,置于 reducing-cloud-costs-20250318 后);AWS-Source-Identity 为 Source Identity 追踪机制的完整概念页,满足可复用条件;已建立与 ctp-topic-13(治理自动化政策层)、ctp-topic-63(主动优化)、reducing-cloud-costs-20250318(优化手段)的 Connections 关系;无内容冲突 + ## [2026-04-25] ingest | CTP Topic 15 Working with Renovatebot - Source file: Cloud & DevOps/Public-Cloud-Learning-Sessions/06_CI_CD_GitOps/ctp-topic-15-working-with-renovatebot.md - Status: ✅ 成功摄入 diff --git a/wiki/overview.md b/wiki/overview.md index e67faf91..e76f0b58 100644 --- a/wiki/overview.md +++ b/wiki/overview.md @@ -169,10 +169,16 @@ Key concepts: [[Process]], [[Value]], [[Value-Stream]], [[Value-Adding]], [[Wast **[[ctp-topic-13-cloud-finops-policies]]**(CTP Topic 13):PCG 团队 Uday 和 Vinay 主讲 Cloud FinOps 成本优化政策与最佳实践——核心架构:PCG 三层服务模型(成本管理:账单支付/showback-chargeback/预算管理 → 成本优化:Reserved Instances 集中购买与资源去优化 → 治理与自动化:集中式上线/策略开发/自动报告);5 大核心策略(账单可见性、标签合规、账户负责人预算责任、Reserved Instances 集中管理、区域限制);安全控制(预安装 Godrails、联合身份管理 MFA、告警重定向至安全团队);Cloud Health 工具提供资源清单和月度账单洞察;标准化实例选型(M/T/C/R/X 系列)+ Graviton ARM 实例节省成本;研发环境三合一优化(突发性实例 + Spot 实例 + 实例调度器)。属 [[FinOps(云财务管理)]] 在 [[Micro Focus]] 云转型场景的核心实践,与 [[ctp-topic-63-optimise-resource-cost-using-automation]](自动化调度优化)和 [[ctp-topic-71-pcgs-guide-to-rightsizing-why-how-when]](Rightsizing 最佳实践)共同构成完整的 FinOps 知识链路。 +**[[public-cloud-learning-sessions-storage-cost-optimization-20240305-160037-meeting]]**(Public Cloud Learning Sessions):AWS 存储服务成本优化全景——覆盖 EBS(GP3 推荐,比 GP2 便宜 20%,可独立扩展 IOPS/吞吐量;快照支持归档层比标准层低 75% 成本)、EFS/FSx(生命周期策略和分层机制)、S3(Intelligent Tiering 自动冷热迁移无转换费用;生命周期策略管理非当前版本和多段上传过期;数据传输费用需注意,PrivateLink 可规避)和 ADM 迁移案例(OpenZFS → 自管理 NetApp on EC2 → FSx for NetApp ONTAP 实现 60% 成本削减)。属 [[FinOps(云财务管理)]] 存储优化专题,与 [[ctp-topic-13-cloud-finops-policies]](政策框架)和 [[public-cloud-learning-sessions-reducing-cloud-costs-20250318-170100-meeting-reco]](综合成本优化)共同构成完整 FinOps 知识链路。 + +**[[ctp-topic-63-optimise-resource-cost-using-automation]]**(CTP Topic 63):使用自动化手段优化 AWS 云资源成本——涵盖五大核心策略:①批准区域(Approved Region)标准化(Oregon/NVirginia/Frankfurt/London/Sydney/Singapore),提高安全性和成本可预测性;②实例类型选择(M6i/M6g 通用型、T3/T4g 经济型、C 系列计算型、R 系列内存型),同配置 M→R 切换节省 35%,Graviton ARM 比 Intel 便宜 20-25%;③承诺计划(1年约 40% 折扣、3年约 60-64% 折扣);④存储优化(GP2→GP3 节省 20%,及时清理未使用 EBS 卷);⑤自动化调度(基于标签的 EC2/RDS 启动/停止,通过 Lambda + EventBridge + Terraform Scheduler 模块实现,非 7×24 工作负载每天只运行 10 小时可节省 70% 成本)。属 [[FinOps(云财务管理)]] 技术实施层,与 [[ctp-topic-13-cloud-finops-policies]](政策框架)和 [[ctp-topic-71-pcgs-guide-to-rightsizing-why-how-when]](RightSizing)共同构成完整 FinOps 知识链路。 + **[[ctp-topic-71-pcgs-guide-to-rightsizing-why-how-when]]**(CTP Topic 71):PCG 团队讲解 AWS EC2 RightSizing 系统性方法论——核心主题:为何要做 RightSizing、何时做、如何执行的完整指南。问题域聚焦过度配置(over-provisioned)EC2 实例导致的资源浪费。RightSizing 通过分析实例实际资源使用情况,将过度配置的实例调整为合适规格,在不影响性能的前提下实现成本节省。是 [[FinOps(云财务管理)]] 核心技术手段之一。⚠️ 视频尚未完成 Whisper 转录,完整内容待补充。 **[[public-cloud-learning-sessions-reducing-cloud-costs-20250318-170100-meeting-reco]]**(Public Cloud Learning Sessions,Vinay 主讲):AWS 云成本优化技术深度实践——**工作负载优化**聚焦现代化(EC2 新代际/Graviton 20-25% 节省/AMD 6-10% 节省/GP2→GP3 存储 20% 节省/EKS 最新版避免扩展支持费/Spot 实例 90% 折扣)和 Right Sizing(EC2 Right Sizing 报告/实例调度/闲置资源清理)。**费率优化**讲解 Savings Plans 和 Reserved Instances 的两种承诺类别(资源级 vs 灵活),以及完整实施流程(前置 Right Sizing → 分析 24/7 工作负载 → 财务沟通 → 账户所有者审批 → 利用率监控报告)。关键规则:承诺计划仅支持无预付选项,最低交易金额 $5k/年,仅由 Phenops 团队实施。属 FinOps 技术实施层,与 [[ctp-topic-13-cloud-finops-policies]](政策框架)互补,共同构成"政策 → 技术实施"完整链路。 +**[[public-cloud-learning-sessions-budget-control-20240319]]**(Public Cloud Learning Sessions,SRE Core 团队 Daniela/Evan/Alan 主讲):AWS 预算控制自动化深度实践——解决 AWS 账户蔓延导致的成本失控问题。核心架构:AWS Budget → SNS → Lambda → Step Functions → SCP Enforcement(服务控制策略封禁新资源创建)的完整告警与执行链路;告警类型分 4 种(Forecast/Actual 80-98%/Severe/Enforcement),评分系统计算宽限期避免月末轻微超支账户被误处罚;Source Identity(STS SourceIdentity 属性)通过 CloudTrail 追踪联邦登录跨角色切换的原始用户身份,实现成本责任到人;初始范围仅限 Lab 账户。属 [[FinOps(云财务管理)]] Enforcement 执行层,与 [[ctp-topic-13-cloud-finops-policies]](治理与自动化政策)和 [[public-cloud-learning-sessions-reducing-cloud-costs-20250318-170100-meeting-reco]](主动优化手段)共同构成 FinOps 完整闭环(告警→Enforcement→优化)。 + **[[public-cloud-learning-sessions-best-practices-for-ec2-cost-optimization-in-aws-2]]**(Public Cloud Learning Sessions,Mike Dukes 和 Steele Taylor 主讲):AWS EC2 成本优化最佳实践深度解析——核心主题覆盖计算效率、Nitro 系统、Graviton 使用、EC2 Spot 竞价实例和容器化成本部署。AWS Nitro 系统通过将网络、存储和安全组件外部化来提升效率;Graviton 处理器基于 ARM64 架构,提供高达 40% 更好的性价比,功耗比同等 x86 实例减少高达 60%;EC2 Spot 实例利用 AWS 闲置容量提供高达 90% 的按需价格折扣;购买选项包括 On-Demand、Savings Plans 和 Spot Instances。Spot Invaders 游戏作为容错混沌工程的实践案例,展示了在 EKS 上使用 Spot 实例构建弹性应用的最佳实践。Graviton 适用于大多数工作负载(Web 服务、容器、HPC 批处理、大数据、CI/CD),但排除有状态服务(如数据库);Spot 和 Graviton 可组合使用以最大化成本节省。属 [[FinOps(云财务管理)]] 技术实践层,与 [[public-cloud-learning-sessions-reducing-cloud-costs-20250318-170100-meeting-reco]](成本优化技术)和 [[ctp-topic-13-cloud-finops-policies]](政策框架)共同构成完整的 EC2 成本优化知识链路。 **[[ctp-topic-20-program-demand-process-flow-and-poc-onboarding]]**(CTP Topic 20):云转型计划的程序需求流程与 POC 入职流程——Sergio 和 Damian 主讲。核心内容:①需求来源——主要由业务案例(如数据中心关闭)、高层管理人员战略优先级及产品路线图驱动;②Gate Process——Gate 0 评估准入、Gate 1 负责 Design Authority 审批、Gate 3 作为启动迁移的最终准入;③POC 目的——不仅验证架构和技术可行性,还包括让团队熟悉基于 Gruntwork 的新一代 Landing Zone;④新环境特点——强调 IaC(Terraform/Terragrunt)自动化部署,严禁手动构建;⑤PCG 团队——平台控制组,负责提供云环境支持、安全策略制定及协助产品组进行 POC;⑥成功标准——POC 成功标准必须在启动前明确定义。属 CTP 治理知识体系入口,与 [[ctp-topic-65]](价值量化)、[[ctp-topic-57]](需求管理)、[[ctp-topic-30]](变更管理)共同构成完整的治理框架链条。 diff --git a/wiki/sources/ctp-topic-63-optimise-resource-cost-using-automation.md b/wiki/sources/ctp-topic-63-optimise-resource-cost-using-automation.md new file mode 100644 index 00000000..ed0b1a3b --- /dev/null +++ b/wiki/sources/ctp-topic-63-optimise-resource-cost-using-automation.md @@ -0,0 +1,48 @@ +--- +title: "CTP Topic 63 Optimise resource cost using automation" +type: source +tags: [] +date: 2026-04-14 +--- + +## Source File +- [[Cloud & DevOps/Public-Cloud-Learning-Sessions/05_FinOps/ctp-topic-63-optimise-resource-cost-using-automation]] + +## Summary(用中文描述) +- 核心主题:使用自动化手段优化 AWS 云资源成本 +- 问题域:云转型计划中如何通过标准化的实例选型、存储优化、承诺计划和自动化调度降低云支出 +- 方法/机制:批准区域(Approved Region)标准化、实例类型选择(ARM/Graviton)、承诺计划(Savings Plans/Reserved Instances)、EBS 存储优化(GP2→GP3)、基于标签的 EC2/RDS 自动化调度(Scheduler) +- 结论/价值:综合运用多种成本优化手段,组合使用最高可节省 70% 以上的云资源成本 + +## Key Claims(用中文描述) +- 企业使用 AWS Graviton ARM 处理器替代 Intel 实例,可节省 20-25% 成本 +- 同配置将实例从 M 系列切换到 R 系列,可节省约 35% on-demand 价格 +- 通过 1 年承诺计划购买 Reserved Instances,可获得约 40% 折扣;3 年承诺可获得约 60-64% 折扣 +- 将 EBS 存储从 GP2 迁移到 GP3,可直接节省 20% 成本 +- 对于非 7×24 运行的工作负载(如开发测试环境),通过自动化调度每天只运行 10 小时,可节省 70% 成本 + +## Key Quotes +> "Graviton is mature enough for production" — Graviton ARM 实例已成熟可用于生产环境,比同规格 Intel 便宜 20-25% +> "Auto shutdown = yes" — Pushka 演示通过 Terraform 模块配置 Scheduler,设置标签实现实例自动停止 + +## Key Concepts +- [[Approved Region(批准区域)]]:建议使用的云资源部署区域,有助于提高安全性、标准化管理和优化成本 +- [[Instance Type Selection(实例类型选择)]]:根据工作负载选择合适的实例家族(M/T/C/R/X 系列),以优化性能和成本 +- [[Commitment Plan(承诺计划)]]:通过预先承诺使用云资源一段时间(Savings Plans / Reserved Instances),获得折扣价格 +- [[Automation Scheduler(自动化调度)]]:通过设置定时任务,自动启动和停止云资源,以节省非工作时间的资源成本 +- [[Storage Optimization(存储优化)]]:通过选择合适的存储类型(如 GP3 替代 GP2),及时清理无用存储,合理分配存储空间来降低存储成本 +- [[Graviton]]:AWS 自研 ARM 处理器,比同规格 Intel 便宜 20-25%,已成熟用于生产环境 +- [[Terraform Scheduler Module]]:Terraform 模块,通过标签(如 `auto_shutdown = yes`)配置 EC2/RDS 自动启停 + +## Key Entities +- [[Pushka]]:Principal SRE,演示如何使用 Terraform 模块配置 Scheduler 实现实例自动启停 + +## Connections +- [[ctp-topic-13-cloud-finops-policies-best-practices-to-optimize-the-co]] ← topic_13 介绍 FinOps 政策框架,本 Topic 补充技术实施细节 +- [[ctp-topic-71-pcgs-guide-to-rightsizing-why-how-when]] ← topic_71 聚焦 RightSizing,与本 Topic 实例选型优化互补 +- [[public-cloud-learning-sessions-reducing-cloud-costs-20250318-170100-meeting-reco]] ← 综合成本优化技术,含 Savings Plans 实施流程 +- [[public-cloud-learning-sessions-best-practices-for-ec2-cost-optimization-in-aws-2]] ← EC2 成本优化最佳实践,含 Graviton 使用 +- [[public-cloud-learning-sessions-storage-cost-optimization-20240305-160037-meeting]] ← 存储优化专题,含 GP2→GP3 迁移 + +## Contradictions +- 暂无已知冲突 diff --git a/wiki/sources/public-cloud-learning-sessions-budget-control-20240319-160204-meeting-recording.md b/wiki/sources/public-cloud-learning-sessions-budget-control-20240319-160204-meeting-recording.md new file mode 100644 index 00000000..09cab01f --- /dev/null +++ b/wiki/sources/public-cloud-learning-sessions-budget-control-20240319-160204-meeting-recording.md @@ -0,0 +1,52 @@ +--- +title: "Public Cloud Learning Sessions - Budget Control - 20240319" +type: source +tags: [FinOps, AWS, Cost-Optimization, Budget-Control, Alerting] +date: 2024-03-19 +sources: [Cloud & DevOps/Public-Cloud-Learning-Sessions/05_FinOps/public-cloud-learning-sessions-budget-control-20240319-160204-meeting-recording.md] +last_updated: 2026-04-26 +--- + +## Source File +- [[Cloud & DevOps/Public-Cloud-Learning-Sessions/05_FinOps/public-cloud-learning-sessions-budget-control-20240319-160204-meeting-recording.md]] + +## Summary(用中文描述) +- 核心主题:AWS 预算控制自动化系统——面向 SRE Core 团队(Daniela、Evan、Alan)的内部学习分享 +- 问题域:AWS 账户蔓延导致的成本失控,以及现有成本削减措施不可持续的问题 +- 方法/机制:AWS Budget → SNS → Lambda → Step Functions → SCP Enforcement(服务控制策略封禁新资源创建)的完整告警与执行链路;Source Identity 追踪跨角色切换的原始登录身份 +- 结论/价值:提供账户级别的详细告警(支出预测/实际支出/成本驱动因素),并将 Enforcement 从手动审批逐步演进为自动封禁 + +## Key Claims(用中文描述) +- SRE Core 团队通过 AWS Budget 服务 + 自定义 Lambda/Step Functions 构建的自动化预算控制,解决了 AWS 账户蔓延导致的成本失控问题 +- 告警类型分为 4 种:Forecast(预测超支)、Actual(实际超支 80%/90%/95%/98%)、Severe(100% 且评分制触发)、Enforcement(100% 且启用强制执行则触发 SCP 封禁) +- Source Identity(AWS Source Identity 属性)通过 CloudTrail 跨角色切换追踪原始登录用户,解决了联邦登录(NetIQ)中"假设角色后无法识别原始身份"的问题 +- 评分系统(Scoring System)根据账户规模和月末时间节点计算宽限期(Grace Period),避免轻微超支的账户被误处罚 +- 初始实施范围仅限 Lab 账户,其他账户继续接收标准超预算告警 + +## Key Quotes +> "This is the first time that we were able to get to this level of granularity." — Daniel,描述通过 Athena + Cost Explorer 实现的资源级成本粒度 + +## Key Concepts +- [[AWS-Source-Identity]]:通过 `sts:SourceIdentity` 属性在假设角色后保留原始登录身份,使 CloudTrail 可追踪跨角色用户活动 +- [[FinOps]]:云财务管理,本质是将财务责任与工程实践结合,本 Source 展示 FinOps 执行层(告警→Enforcement)的自动化实现 +- [[AWS-Budget-Alerts]]:AWS Budget 服务原生的预算告警机制,通过 SNS → Lambda → Step Functions 扩展为详细告警邮件 +- [[SCP-Enforcement]]:Service Control Policy,在 100% 预算触发时自动封禁账户新资源创建,实现成本治理的"硬执行" +- [[CloudTrail]]:AWS 审计日志服务,Source Identity 机制使其能追踪联邦登录跨角色切换的完整用户链 +- [[Step-Functions]]:AWS Step Functions 编排 Lambda 和数据增强逻辑,实现告警流程自动化 +- [[Cost-Explorer]]:AWS 成本分析工具,提供用户维度的日度支出数据,用于 top users 报告 + +## Key Entities +- [[Daniela]]:SRE Core 团队成员,负责图表和详细成本报告讲解(提及 <2 次,wikilink 记录) +- [[Evan]]:SRE Core 团队成员(提及 <2 次,wikilink 记录) +- [[Alan]]:SRE Core 团队成员,负责 AWS Budget Alerts and Actions 实现细节讲解(提及 <2 次,wikilink 记录) +- [[SRE-Core-Team]]:预算控制自动化系统的开发团队,由 Daniela、Evan、Alan 三人组成 +- [[Phenops-Team]]:FinOps 执行团队,本 Source 中负责预算分配和 Enforcement 审批决策 +- [[NetIQ]]:联邦身份管理系统(NetIQ Access Manager),用于用户认证并提供 Source Identity 的上游身份 + +## Connections +- [[ctp-topic-13-cloud-finops-policies]] ← extends ← [[public-cloud-learning-sessions-budget-control-20240319]]:Topic 13 定义 FinOps 政策框架(成本管理→成本优化→治理自动化三层),本 Source 展示"治理与自动化"层(Budget Enforcement)的具体技术实现 +- [[ctp-topic-63-optimise-resource-cost-using-automation]] ← relates_to ← [[public-cloud-learning-sessions-budget-control-20240319]]:Topic 63 聚焦 RightSizing/承诺计划等主动优化手段,本 Source 聚焦被动告警+强制执行机制,两者互补构成 FinOps 完整闭环 +- [[public-cloud-learning-sessions-reducing-cloud-costs-20250318]] ← extends ← [[public-cloud-learning-sessions-budget-control-20240319]]:2025 版主讲 Vinay(FinOps Lead),补充了 Savings Plans/RI 承诺计划,本 Source 是 2024 早期版本,两者构成 FinOps 知识演进 + +## Contradictions +- 与 [[ctp-topic-13-cloud-finops-policies]] 关于 Enforcement 方式:Topic 13 提到"集中式上线/策略开发/自动报告",本 Source 明确提出 SCP 自动封禁新资源作为 Enforcement 手段;两者不矛盾,Topic 13 描述政策层面,本 Source 描述执行层面 diff --git a/wiki/sources/public-cloud-learning-sessions-storage-cost-optimization-20240305-160037-meeting.md b/wiki/sources/public-cloud-learning-sessions-storage-cost-optimization-20240305-160037-meeting.md new file mode 100644 index 00000000..464347e6 --- /dev/null +++ b/wiki/sources/public-cloud-learning-sessions-storage-cost-optimization-20240305-160037-meeting.md @@ -0,0 +1,57 @@ +--- +title: "Public Cloud Learning Sessions - Storage Cost Optimization - 20240305" +type: source +tags: + - AWS + - Storage + - FinOps + - Cost-Optimization +date: 2024-03-05 +--- + +## Source File +- [[Cloud & DevOps/Public-Cloud-Learning-Sessions/05_FinOps/public-cloud-learning-sessions-storage-cost-optimization-20240305-160037-meeting.md]] + +## Summary(用中文描述) +- 核心主题:AWS 存储服务(EBS/EFS/FSx/S3)的成本优化最佳实践与 ADM 实案例证 +- 问题域:公有云存储选型决策、存储层级管理、生命周期策略、数据传输成本控制 +- 方法/机制:按需选择存储类型与层级、智能分层(Intelligent Tiering)、生命周期策略自动化、DLM/AWS Backup 快照管理 +- 结论/价值:正确的存储选型和分层策略可带来显著成本节省;ADM 通过迁移至 FSx for NetApp ONTAP 实现 60% 成本削减 + +## Key Claims(用中文描述) +- GP3 相比 GP2 提供 20% 成本优化,且可独立扩展 IOPS 和吞吐量 +- EBS 快照归档层提供比标准层低 75% 的存储成本,但恢复时间更长、保留期为 90 天 +- EFS 不频繁访问层最小计费对象大小为 128KB +- S3 Intelligent Tiering 可根据访问模式自动在冷热存储层之间迁移数据,且层间迁移无转换费用 +- ADM 迁移至 AWS FSx for NetApp ONTAP 后,相比最初的自管理 NetApp on EC2 方案实现 60% 成本削减 + +## Key Quotes +> "With GP3, you can scale IOPS and throughput independently of the volume size." — GP3 核心优势 +> "With Intelligent Tiering we can automatically move data from warmer to colder storage tiers based on object access patterns." — S3 Intelligent Tiering 核心机制 + +## Key Concepts +- [[EBS-GP3]]:通用型 SSD(GP3),比 GP2 便宜 20%,可独立扩展 IOPS 和吞吐量 +- [[EBS-Snapshot-Archive]]:EBS 快照归档层,比标准层低 75% 成本,但恢复需 3-12 小时且保留期 90 天 +- [[Data-Lifecycle-Manager]]:AWS DLM,自动化 EBS 快照生命周期管理,可设置保留策略并迁移至归档层 +- [[AWS-Backup]]:AWS 备份服务,可跨服务集中管理备份,支持跨账户跨区域复制 +- [[EFS-Infrequent-Access]]:EFS 不频繁访问层,最小计费对象大小 128KB,通过生命周期策略自动迁移 +- [[S3-Intelligent-Tiering]]:S3 智能分层,根据访问频率自动在多个存储层间迁移,无转换费用 +- [[S3-Lifecycle-Policies]]:S3 生命周期策略,可转换对象层级、过期非当前版本、删除未完成的多段上传 +- [[FSx-for-NetApp-ONTAP]]:AWS 托管 NetApp 文件系统,支持 SSD 和 HDD 分层,自动在层间迁移冷数据 +- [[AWS-PrivateLink]]:通过 AWS 网络内访问 S3 避免数据传输费用 + +## Key Entities +- [[AWS]]:Amazon Web Services,云存储服务提供商(EBS/EFS/FSx/S3) +- [[ADM]]:案例客户,通过三阶段迁移(OpenZFS → 自管理 NetApp on EC2 → FSx for NetApp ONTAP)最终实现 60% 成本削减 + +## Connections +- [[public-cloud-learning-sessions-reducing-cloud-costs-20250318-170100-meeting-reco]] ← extends ← [[ctp-topic-13-cloud-finops-policies]] +- [[EBS-GP3]] ← extends ← [[ctp-topic-13-cloud-finops-policies]](FinOps 存储优化话题扩展) +- [[public-cloud-learning-sessions-best-practices-for-ec2-cost-optimization-in-aws-2]] ← extends ← [[public-cloud-learning-sessions-reducing-cloud-costs-20250318-170100-meeting-reco]](EC2 + Storage 共同构成完整成本优化知识链路) + +## Contradictions +- 与 [[ctp-topic-14-octane-hub-on-aws]] 的潜在冲突(存储选型): + - 冲突点:EFS 与 EBS 的适用场景边界 + - 当前观点:EFS 适合备份,EBS 适合实时数据库(Octane Hub 案例) + - 对方观点:EFS Infrequent Access 和 EFS Archive 层适用于不频繁访问的文件共享场景 + - 说明:两者均正确,但适用场景不同——EFS 更适合跨多实例共享的文件系统,EBS 更适合单实例高性能块存储