1.7 实战:建立 `pg36_shop` 地图与实验基线
现在把前六节的地图落到一个真实对象上。本实验会在 L1 沙箱创建 pg36_shop 数据库、shop 模式和三个专用角色,随后从 PostgreSQL 与 Pigsty 两侧收集证据。
实验分为两个风险级别:
setup与快照采集:R1·可逆变更,只创建以pg36_命名的教学对象;reset:sql:R2·破坏性演练,会删除整个pg36_shop数据库,只能在确认可销毁的 L1 中执行。
准备两个不含密码的连接 URI。将 <L1_HOST> 替换为实际域名或 IP;密码由 psql 询问或使用 ch02 将介绍的安全凭据机制:
export PG36_BOOTSTRAP_URL='postgresql://dbuser_dba@<L1_HOST>:5436/postgres?application_name=pg36-ch01-admin'
export PG36_SHOP_ADMIN_URL='postgresql://dbuser_dba@<L1_HOST>:5436/pg36_shop?application_name=pg36-ch01-admin'这里使用 5436 default 服务,目的是跟随主库且绕过事务连接池执行管理脚本。若你的环境修改了 Pigsty 默认服务,请根据实际配置替换,不能照抄端口猜路径。
1.7.1 创建数据库、业务模式和最小角色
本章只建立权限骨架,不创建订单、商品或支付表:
| 角色 | 是否登录 | 责任 |
|---|---|---|
pg36_owner | 否 | 拥有数据库和模式;迁移时由受控管理会话 SET ROLE 使用 |
pg36_app | 是 | 应用运行角色;只获得 shop 中未来业务对象的读写权限 |
pg36_ro | 是 | 只读角色;只获得 shop 中未来业务对象的读取权限 |
对象所有者使用 NOLOGIN,避免应用直接以所有者身份绕过授权边界。两个登录角色在本章故意不设置密码;这既避免在教程中分发固定密码,也使它们在默认密码认证规则下暂时无法远程登录。ch02 会为连接与凭据建立正式工作流。
下载或打开三份伴随实验文件:
setup.sql:幂等创建角色、数据库、模式和默认权限;verify.sql:机器验证状态并输出摘要;reset.sql:带确认口令的实验清理。
执行初始化:
psql -X "$PG36_BOOTSTRAP_URL" \
-v ON_ERROR_STOP=1 \
-f setup.sql执行身份为 dbuser_dba 或等价的实验管理员;目标必须是 L1 的 postgres 数据库。脚本会:
- 仅在缺失时创建三个
pg36_角色,并收敛高风险属性; - 仅在缺失时从
template0创建 UTF-8 数据库; - 创建由
pg36_owner拥有的shop模式; - 撤销
public模式的公共建对象权限; - 为两个运行角色授予模式使用权和未来对象的默认权限;
- 为
pg36_shop中的两个运行角色设置pg_catalog, shop搜索路径。
成功末尾应出现:
[setup] complete: roles intentionally have no password in this chapter这条消息只证明脚本执行完毕,不能替代下一目的状态验证。
与 Pigsty 声明式配置对齐
SQL 能证明 PostgreSQL 对象机制,但 Pigsty 管理的长期环境还应把期望状态写入 inventory,避免下次自动化执行时出现配置漂移。最小声明可采用下面的结构;不要把实际明文密码直接写进公开配置:
pg_users:
- { name: pg36_owner, login: false, pgbouncer: false, comment: pg36_shop object owner }
- { name: pg36_app, login: true, pgbouncer: false, comment: pg36_shop runtime role }
- { name: pg36_ro, login: true, pgbouncer: false, comment: pg36_shop read-only role }
pg_databases:
- name: pg36_shop
owner: pg36_owner
encoding: UTF8
pgbouncer: true
schemas:
- { name: shop, owner: pg36_owner }本章先将 pgbouncer: false 用于尚无凭据的两个登录角色;数据库本身可以加入连接池。设置正式认证材料后,再把用户加入 PgBouncer。若在既有集群中应用声明,应先评审差异,然后使用:
cd ~/pigsty
bin/pgsql-user pg-meta pg36_owner
bin/pgsql-user pg-meta pg36_app
bin/pgsql-user pg-meta pg36_ro
bin/pgsql-db pg-meta pg36_shop这些是 R1 操作,目标集群名不一定是 pg-meta。运行前用 ansible-inventory --graph 确认限制范围;若清单里已经存在同名但含义不同的对象,立即停止,不要让自动化强行“收敛”。
1.7.2 生成连接快照、对象树、服务拓扑与环境清单
建立证据目录:
mkdir -p evidence/ch01连接快照
psql -X "$PG36_SHOP_ADMIN_URL" -A -t -v ON_ERROR_STOP=1 -c "
SELECT jsonb_build_object(
'captured_at', clock_timestamp(),
'database', current_database(),
'session_user', session_user,
'current_user', current_user,
'server_addr', inet_server_addr(),
'server_port', inet_server_port(),
'backend_pid', pg_backend_pid(),
'server_version', current_setting('server_version'),
'search_path', current_setting('search_path'),
'in_recovery', pg_is_in_recovery()
);" > evidence/ch01/connection.json这里以管理员会话采集,因此 search_path 不会冒充 pg36_app 的角色级设置。角色设置由 verify.sql 直接查询目录验证。
对象树
psql -X "$PG36_SHOP_ADMIN_URL" > evidence/ch01/objects.txt <<'PSQL'
\pset pager off
\conninfo
\dn+
\du+ pg36_*
\d shop.*
PSQL此时 shop 模式尚无业务关系,\d shop.* 返回“没有找到任何关系”是正确结果。对象树的目标是证明数据库、模式和角色边界,不是提前制造表。
服务拓扑与环境清单
在 Pigsty 管理节点执行:
cd ~/pigsty
{
printf 'captured_at=%s\n' "$(date -Is)"
printf 'node=%s\n' "$(hostname -f 2>/dev/null || hostname)"
printf 'pigsty=%s\n' "$(git describe --tags --always 2>/dev/null || printf unknown)"
printf '%s\n' '--- inventory ---'
ansible-inventory --graph
printf '%s\n' '--- runtime ---'
pg list pg-meta
printf '%s\n' '--- listening ports ---'
ss -lnt | awk 'NR == 1 || $4 ~ /:(5432|5433|5434|5436|5438|6432)$/'
} > evidence/ch01/platform.txt将 pg-meta 替换为实际集群名。ss 只证明端口正在监听,不证明后端角色、路由正确或 SQL 可用;它必须与 pg list 和连接快照合读。
版本清单
在客户端记录客户端与服务器版本:
{
psql --version
psql -X "$PG36_SHOP_ADMIN_URL" -A -t -c \
"SELECT 'server=' || current_setting('server_version');"
} > evidence/ch01/versions.txt客户端与服务端小版本不同不一定是错误,但必须留痕。涉及协议、元命令或版本特性的实验以实际版本为准。
1.7.3 建立 verify:state 与三档 reset
verify:state 不是“脚本没报错”的同义词。它从系统目录重新读取最终状态,检查对象所有者、角色、权限和数据库级搜索路径:
psql -X "$PG36_BOOTSTRAP_URL" \
-v ON_ERROR_STOP=1 \
-f verify.sql \
| tee evidence/ch01/verify.txt成功输出的值会因环境不同而变化,但必须包含:
status=ok
database=pg36_shop
database_owner=pg36_owner
schema=shop
schema_owner=pg36_owner
in_recovery=false在备库或错误路由上执行时,脚本不应被“修到能过”;先回到 1.1 确认为什么管理连接没有进入可写主库。
本书使用三档复位,它们按影响范围命名,不代表都要在每章执行:
| 复位 | 影响范围 | ch01 的实现 | 风险与使用条件 |
|---|---|---|---|
reset:sql | 教学数据库、模式、角色和数据 | 删除 pg36_shop 与三个 pg36_ 角色 | R2;仅限无保留价值的 L1 |
reset:cluster | PostgreSQL 集群配置、成员和服务 | 本章不修改集群级状态,因此应为 no-op | 后续章节按变更提供;不能用“重装集群”替代诊断 |
reset:host | 整台实验主机 | 回到第 0 章重建 L1 | R2;仅当主机基线已不可相信 |
执行 reset:sql 前必须同时满足:
- 当前是明确标识的可销毁 L1;
pg36_shop中没有需要保留的数据;- 三个
pg36_角色没有被其他数据库使用; PG36_BOOTSTRAP_URL指向预期集群的主库管理服务;- 已阅读
reset.sql,确认它没有被本地修改。
然后使用完整确认口令:
psql -X "$PG36_BOOTSTRAP_URL" \
-v ON_ERROR_STOP=1 \
-v confirm_reset=RESET_PG36_SHOP \
-f reset.sql脚本会先终止连接到 pg36_shop 的会话,再删除数据库与角色。这不是可回滚事务。未提供精确口令时脚本会拒绝执行。复位后重新运行 setup.sql 与 verify.sql,应得到新的数据库 OID;OID 改变正好说明它不能作为业务稳定标识。
1.7.4 验收:从 SQL 与 Pigsty 两侧指认同一对象
最后把所有名字放回一张表。下面是结构,不是要求实际值与示例相同:
| 层次 | 示例 | 证据来源 |
|---|---|---|
| 客户端入口 | <L1_HOST>:5436 | PG36_SHOP_ADMIN_URL、\conninfo |
| 平台服务 | pg-meta-default | Pigsty 服务定义、HAProxy 后端 |
| Pigsty 集群 | pg-meta | inventory、pg list |
| Pigsty 实例 | pg-meta-1 | inventory、pg list |
| 节点 | <IP 或主机名> | inventory、主机事实 |
| PostgreSQL 后端 | 某个 pid、地址、5432 | pg_backend_pid()、inet_server_*() |
| PostgreSQL 数据库 | pg36_shop | current_database()、pg_database |
| 模式 | shop | pg_namespace、\dn+ |
| 角色 | pg36_owner、pg36_app、pg36_ro | pg_roles、\du+ |
从 PostgreSQL 侧运行最终快照:
SELECT
current_database() AS database_name,
(
SELECT oid
FROM pg_catalog.pg_database
WHERE datname = current_database()
) AS database_oid,
inet_server_addr() AS server_addr,
inet_server_port() AS server_port,
pg_backend_pid() AS backend_pid,
pg_is_in_recovery() AS in_recovery;从 Pigsty 侧用 pg list <cluster> 找到同一 server_addr 对应的实例与角色,再检查 HAProxy 的 default 服务是否把 5436 路由到该实例的 PostgreSQL 5432。这就是“双侧指认”:平台名字最终落到 PostgreSQL 原生事实,SQL 地址也能反查回平台实体。
章级验收清单
只有以下各项全部成立,第 1 章才算完成:
-
verify.sql输出status=ok,并以非零退出码报告任何不满足项; - 能解释客户端访问
5436而服务器接受端口显示5432的原因; - 能区分
pg-meta、pg-meta-1、pg36_shop、shop与pg36_app; -
pg36_owner不可登录,数据库与模式均由它拥有; -
pg36_app与pg36_ro没有超级用户、建库、建角色、复制或绕过 RLS 权限; - 能从 SQL 判断当前实例是否处于恢复状态,并从
pg list找到对应平台角色; -
connection.json、objects.txt、platform.txt、versions.txt和verify.txt已生成; - 证据文件不包含密码、SCRAM verifier、令牌或不必要的完整 inventory;
- 知道三档 reset 的影响范围,但没有为了“练习”执行无关的集群或主机重建;
- 复位演练后可以重新运行 setup → verify,且知道数据库 OID 会改变。
交付给 ch02
下一章将接收本章的五样东西:两个管理员连接 URI、三类角色、pg36_shop.shop 命名约定、证据目录和可运行的 setup/verify/reset 脚本。ch02 会给应用角色配置安全凭据与服务入口,把这些手工命令改造成可审查、可重跑的任务。
本章刻意没有创建业务表。下一步不是凭直觉开始堆 DDL,而是先把工具链和失败语义固定下来。
参考资料
- PostgreSQL 18:CREATE ROLE
- PostgreSQL 18:CREATE DATABASE
- PostgreSQL 18:默认权限
- Pigsty v4.4:用户与角色配置
- Pigsty v4.4:数据库配置
上一节:最小 psql 生存卡 · 返回本章目录 · 下一章:手到擒来:psql 与可复现工作流 · 查看全书目录 · 查看索引中心