# 用声明式清单交付两个服务单元

LLMS 索引： [llms.txt](/llms.txt)

---

声明式交付的核心不是“YAML 很先进”，而是让目标、作用域、版本、差异与
执行证据可以评审。inventory 是意图，不是已经发生的事实。

## 19.6.1 inventory、参数模板与主机分组 {#item-19-6-1}

### inventory 同时承载图和参数

Pigsty inventory 的结构大致是：

```yaml
all:
  children:
    infra:
      hosts: ...
    etcd:
      hosts: ...
    pg-meta:
      hosts: ...
      vars:
        pg_cluster: pg-meta
    pg-test:
      hosts: ...
      vars:
        pg_cluster: pg-test
  vars:
    version: v4.5.0
    pg_version: 18
```

它表达两类信息：

```text
membership graph
  哪些 host 属于哪些 module/cluster

desired parameters
  version, paths, packages, tuning, users, DBs, services, access
```

混在一个文件里不表示它们的生命周期相同。host identity 可能多年稳定，
password 要轮换，service definition 会迭代，初始化参数只在新 cluster 生效。

### identity 参数必须明确

Pigsty 的参数层次包含 global、group/cluster、host/instance 等作用域。对
PostgreSQL member，核心 identity 至少有：

```yaml
pg_cluster: pg-test
pg_seq: 1
pg_role: primary
```

官方
[Pigsty parameter model](https://pigsty.io/docs/concept/iac/parameter/)
将 `pg_cluster`、`pg_seq`、`pg_role` 等视为无默认值的 identity 参数。

没有 identity，不应让自动化猜：

- 这是哪个 HA group；
- member 名是什么；
- bootstrap primary 是谁；
- service/monitor/backup 如何命名。

### 参数优先级要可解释

同一参数可能来自：

```text
role default
global vars
module/group vars
cluster vars
host vars
extra vars
generated template
runtime dynamic config
```

最终值要能回答：

```text
value
source
scope
owner
change context
rendered destination
live observed value
```

“我在 YAML 里搜不到”不等于它使用 PostgreSQL 默认；可能来自 role default
或 template。

本章 live inventory 没有重复声明每一个 role default。验收从 SQL 读取
checksum、locale、timezone 等结果，避免把“省略”误作“不确定”或
“一定是 PostgreSQL 默认”。

### 参数模板是起点，不是服务等级

`pg_conf: oltp.yml` 可提供合理 OLTP 起点，`node_tune: oltp` 可收敛主机
baseline。它们不包含业务 workload evidence。

模板不能自动知道：

```text
peak TPS and query mix
working set
connection fan-out
storage latency
WAL generation
RPO/RTO
maintenance workload
tenant contention
```

第 26、27 章才通过压力与瓶颈证据调参。

### 从 exact release 生成配置

本章没有直接运行当前 dirty checkout，而是：

```bash
git archive v4.5.0
```

解到私有临时目录，并记录 tag commit：

```text
2d5a45f759274048de0c197829228a71d0182e5c
```

然后：

```bash
./configure -c ha/full -s -n -g -v 18 -r default
```

参数含义：

```text
-c ha/full   使用四节点功能演示模板
-s           跳过 IP 探测/替换
-n           非交互
-g           生成随机密码
-v 18        PostgreSQL major 18
-r default   默认上游仓库区域
```

`ha/full` 官方定位主要是演示与测试，不是生产拓扑模板；它把 infra、单 etcd、
MinIO 和 `pg-meta` 放在第一节点，`pg-test` 放在后面三节点。这正是本章
将其标为 sandbox 的原因。

### generator output 必须 review

wizard 给出初稿后，逐项 review：

```text
source release
target addresses and SSH user
OS/architecture
module membership
PostgreSQL major/locale/checksum
storage paths
cluster/member identity
service/offline placement
repo/mirror/proxy
secrets
safeguards
backup target
monitoring retention
firewall/access
```

`configure` 成功只表示生成文件，不表示目标可部署。

### live inventory 是 secret-bearing artifact

随机密码比模板默认密码安全，但文件仍含 secret。处理：

```text
mode 0600
private temporary/secret-managed path
never paste full file into issue/chat/log
never commit
rotate if exposed
production uses secret authority and lifecycle
```

本次初始化过程中，首次生成的凭据曾出现在工具输出边界；因此立即重新生成，
正式 live inventory 使用另一组未打印的值。这个事件提醒我们：

> redaction 不是最后一步；command output、debug、diff 和 CI log 都是泄露面。

### 只导出 allowlist projection

[`inventory_projection.py`](/labs/ch19/inventory_projection.py) 读取 live
inventory，只保留：

```text
Pigsty/PostgreSQL/locale non-secret globals
host set and group membership
cluster/member role/offline flags
source mode and a withheld source-fingerprint marker
safe projection checksum in the capture manifest
redacted secret-field count
secret_values_exported=0
```

它不使用“把已知 password 字段替换为星号后整份输出”的 denylist 模式。
denylist 容易漏掉新字段；allowlist 默认拒绝未知内容。

执行：

```bash
export PG36_CH19_INVENTORY=/absolute/private/pg36.yml
export PG36_EVIDENCE_DIR=/absolute/evidence/ch19
static/labs/ch19/task.sh project
```

投影不是可用于部署的 inventory，故意不可逆。

### sanitized example 只表示形状

[`inventory.example.yml`](/labs/ch19/inventory.example.yml) 使用 sentinel
secret，帮助理解结构。它不应原样部署。

审阅 example 时也要防止：

```text
真实 address/owner accidentally copied
有效 token/password
private key
internal repository credential
production backup endpoint
```

### inventory 是 code，但不等于把一切都放 Git

适合版本化：

```text
schema and groups
non-secret desired parameters
service definitions
role/database declarations without secret values
policy IDs
change history
```

需要受控 secret store：

```text
password
private key
recovery secret
API token
CA signing key
KMS credential
```

需要外部事实系统：

```text
asset/failure-domain inventory
IPAM/DNS authority
owner/on-call
certificate issuance
package artifact promotion
```

声明式不是单文件崇拜，而是让 authority 可追踪。

## 19.6.2 生产服务与隔离验证服务 {#item-19-6-2}

### 先澄清本节标题的边界

生产设计应该区分：

```text
application service unit
platform/control/validation unit
```

本章用 `pg-test` 和 `pg-meta` 演练这种分工，但两者都位于 disposable
sandbox：

```text
production_data_permitted=false
production_traffic_permitted=false
```

所以 `pg-test` 是 production-shaped teaching service，不是生产服务；
`pg-meta` 也不是合格的生产 control plane。

### 服务单元一：`pg-test`

声明：

```yaml
pg-test:
  hosts:
    10.10.10.11:
      pg_seq: 1
      pg_role: primary
    10.10.10.12:
      pg_seq: 2
      pg_role: replica
    10.10.10.13:
      pg_seq: 3
      pg_role: replica
      pg_offline_query: true
  vars:
    pg_cluster: pg-test
```

目的：

```text
three-member topology
primary/replica service rehearsal
offline placement rehearsal
chapter 20 fault target
chapter 21 recovery target
chapter 22 endpoint target
```

当前 bootstrap role：

```text
.11 primary
.12 replica
.13 replica/offline
```

future failover 后 runtime role 可能变化；inventory declaration 与 observed
role 的解释要跟着 chapter 20 contract 更新。

### 服务单元二：`pg-meta`

声明：

```yaml
pg-meta:
  hosts:
    10.10.10.10:
      pg_seq: 1
      pg_role: primary
  vars:
    pg_cluster: pg-meta
```

同一 node 还承载：

```text
Pigsty infra
single etcd member
MinIO
monitoring/logging
admin source
```

它可用于控制与验证，但这个共置形成大 blast radius。不要把“组件齐全”解释为
“组件高可用”。

### 两个 service unit 必须有不同 system identifier

部署时如果错误 clone 同一 cluster，再改名字，表面可能出现两个 group。
因此验收要求：

```text
system_id(pg-meta) != system_id(pg-test)
```

同时：

```text
system_id(pg-test-1)
= system_id(pg-test-2)
= system_id(pg-test-3)
```

这是 declaration 和 physical lineage 的交叉检查。

### control data 与 application data 不要无意混合

真实平台需要决定：

```text
Pigsty metadata DB 是否与业务 cluster 分离
monitoring outage 是否影响 DB availability
backup repository failure 是否影响 primary
DCS failure 是否影响 existing traffic/new failover
control credentials 是否能访问 business data
control-plane maintenance blast radius
```

本沙箱共置是资源选择，不是推荐生产答案。

### offline 服务不等于隔离环境

`.13` 是 replica + offline query 标记。它仍：

```text
接收同一 WAL
占同一 laptop CPU/storage
依赖同一 DCS
可能被 direct access
可能因慢查询产生 recovery conflict
```

要实现更强隔离，可能需要：

```text
dedicated host/failure domain
cgroup/resource limits
separate service/HBA/role
query timeout
replication/freshness SLO
dedicated cluster or analytical system
```

### production topology 要替换六个例外

从本章沙箱走向生产，不是删除 `sandbox` 字样。至少解决：

```text
independent failure domains
etcd quorum
backup target/DR independence
qualified storage
secret authority/rotation
measured resource capacity
```

并完成后续章节 gate。

### service unit review 表

| 问题 | `pg-meta` | `pg-test` |
|---|---|---|
| authority | control/validation | teaching application service |
| members | 1 | 3 |
| current primary | `.10` | `.11` |
| replicas | 0 | `.12`, `.13` |
| offline placement | no | `.13` |
| system ID | own | shared within three members |
| production SLO | none | none |
| later fault target | limited | chapter 20 |

这张表应进入 design review，而不是从 Ansible recap 猜。

## 19.6.3 幂等部署、差异检查与失败重跑 {#item-19-6-3}

### 幂等的正确含义

理想的 idempotent task：

```text
run desired convergence once -> target state
run again without input/drift -> no material change
```

但整套部署包含：

```text
package repositories
generated secrets/certificates
database initialization
backup creation
monitoring registration
external APIs
service restarts
time-dependent facts
```

所以不能用“Ansible 是幂等的”替代每个 action 的语义。

Pigsty 官方
[Playbooks](https://pigsty.io/docs/ref/playbook/)
说明大多数 playbook 可重复运行，同时指出清理参数和 `*-rm.yml` 等有重要
caveat。

### 一次完整部署的受控顺序

本章实际流程：

```text
1. resolve/extract exact release
2. generate and review private inventory
3. direct SSH ping four identities
4. copy exact release + private inventory to admin node
5. bootstrap admin prerequisites
6. run ./deploy.yml -i pg36.yml
7. retain private log and return code
8. capture only safe recap
9. read-only L2 evidence capture
10. positive + negative validation and review
```

Pigsty `deploy.yml` 是 core chain 的 one-pass deployment。它完成很多工作，
不意味着每个业务 policy 自动满足。

### recap 是执行证据，不是服务验收

本次安全 recap：

| target | ok | changed | unreachable | failed |
|---|---:|---:|---:|---:|
| `10.10.10.10` | 326 | 248 | 0 | 0 |
| `10.10.10.11` | 174 | 134 | 0 | 0 |
| `10.10.10.12` | 159 | 120 | 0 | 0 |
| `10.10.10.13` | 159 | 120 | 0 | 0 |
| `localhost` | 6 | 4 | 0 | 0 |

它证明 playbook 没报告 failed/unreachable。它不证明：

```text
correct target identity
correct locale/checksum
one leader
replication healthy
service routing
production capacity
backup restore
HA behavior
```

这些另行验收。

### `changed=0` 也不是唯一幂等标准

一些 task 合理地每次：

```text
refresh facts
check service
render timestamped artifact
probe endpoint
rotate ephemeral state
```

也有 task 误报 changed。应关注：

```text
unplanned service restart
config checksum drift
package version change
database reinitialization
role/permission mutation
secret rotation
endpoint outage
```

二次执行前先查 playbook/tag 的语义，不以追求漂亮 recap 为目标。

### 失败重跑前先分类

失败类型：

| 类型 | 例子 | 下一步 |
|---|---|---|
| transient | repo timeout、短暂 DNS | 保存证据后 bounded retry |
| declaration | wrong group/var | 修 inventory，review diff |
| prerequisite | sudo/clock/disk | 修前提并重新 preflight |
| partial init | primary created, replica failed | 查 cluster state，按 role runbook |
| incompatible | package/extension/OS | 停止，修矩阵 |
| destructive drift | wrong target/data exists | 停止并升级决策 |
| secret exposure | log printed credential | 先 rotate/contain |

不要在不知道已执行到哪里时直接：

```text
rm -rf PGDATA
pgsql-rm
wipe/reconfigure all
```

失败现场是证据。

### 限制 scope

Pigsty playbook 支持 Ansible `-l` 与 tags。例：

```bash
./pgsql.yml -i private.yml -l pg-test
./pgsql.yml -i private.yml -l 10.10.10.13
./pgsql.yml -i private.yml -l pg-test -t pg_service
```

使用前必须确认：

```text
inventory exact path
limit resolves to expected hosts
tag dependencies
check/diff support and limitations
serial/batch behavior
current member role
change authority
```

`-l pg-test` 是作用域控制，不是安全沙箱。

### diff 不能泄密

安全 diff 分层：

```text
secret-free projected inventory diff
rendered config hash/semantic diff
live pg_settings diff excluding secrets
package/version diff
service membership diff
policy exception diff
```

不要把 live inventory `git diff`、Ansible `-vvv`、template variables 或
`.pgpass` 直接上传。

### automatic reboot 被刻意拒绝

bootstrap 观察到 meta 节点已安装 kernel 与当前 running kernel 有差异。这
可能要求 reboot 才完成 host baseline，但本章没有自动重启：

- reboot 会改变运行状态；
- control/infra/DB 共置；
- 尚未建立第 20 章 HA 行为证据；
- maintenance authority 未授予；
- 本章 deployment 成功不要求偷偷消除 warning。

正确做法是登记 exception/change，计划可观察的 reboot，而不是为了让
preflight 变绿直接执行。

### normal lab 没有 deploy action

[`task.sh`](/labs/ch19/task.sh) 只提供：

```text
project
capture
verify
review
all
reset:cluster
```

其中 `all` 只读。没有 `deploy` 是有意设计：

> source-controlled 验收脚本不应在读者以为“检查环境”时顺便收敛 package、
> 重启服务或重建数据库。

部署需要独立变更窗口和 runbook。

### removal playbook 是 destructive action

Pigsty `pgsql-rm.yml` 用于移除 cluster/instance。官方文档说明 production
应显式启用 `pg_safeguard`，并对 override 格外谨慎。

本章只提供多重 guard 的
[`reset-cluster.sh`](/labs/ch19/reset-cluster.sh)，没有运行它。它要求：

```text
exact sandbox target
exact reset token
no production data assertion
clients drained assertion
exact release/inventory
prior machine-ID allowlist
fresh passing evidence
interactive second ACK
```

这是演示 destructive contract，不是授权。

### 交付完成定义

声明式交付完成需同时满足：

```text
source pinned
inventory reviewed and secret-safe
target identity proven
playbook rc/recap retained
live host facts pass
PostgreSQL init contract pass
Patroni/SQL/inventory topology agree
endpoint identity observable
exceptions accepted by owners
rollback/reset boundary explicit
later gates remain named
```

本章环境满足 sandbox L2，带六个 exception；production gate 仍 pending。

---

[上一节：拓扑、命名与故障域](../05/) · [返回本章目录](../) · [下一节：实战：L2 部署验收](../07/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
