拓冰建站拓冰建站
首页 / 资讯中心 / 正文

ZITADEL 数据库初始化机制详解:cmd/initialise/sql 引导 SQL 文件与 zitadel init 命令源码解析

ZITADEL 数据库初始化机制详解cmd/initialise/sql 引导 SQL 文件与 zitadel init 命令源码解析【免费下载链接】zitadelZITADEL - Identity infrastructure, simplified for you.项目地址: https://gitcode.com/GitHub_Trending/zi/zitadel本篇聚焦 ZITADEL 仓库中cmd/initialise/sql/目录下的 9 个引导 SQL 文件它们定义了 ZITADEL 启动前数据库侧的全部最小前提——专用用户、专用数据库、三个核心 Schemaeventstore / projections / system、事件存储表eventstore.events2及配套的加密密钥表与唯一约束表。读完本文你将能够理解每个 SQL 文件的具体职责与占位符机制掌握zitadel init含schema/database/user/grant子命令如何以幂等方式消费这些 SQL并能针对托管 PostgreSQL 等无超管权限的环境选择正确的初始化路径。sql 目录的定位ZITADEL 启动前的数据库最小前提ZITADEL 是基于事件溯源Event Sourcing的开源身份基础设施其核心状态全部落在 PostgreSQL亦支持 CockroachDB 方言中。但 ZITADEL 自身并不假设数据库环境已经就绪cmd/initialise/sql/README.md明确了该目录的定位The sql-files in this folder initialize the ZITADEL database and user. These objects need to exist before ZITADEL is able to set and start up.即这些 SQL 文件负责初始化 ZITADEL 使用的数据库与数据库用户这些对象必须在 ZITADEL 执行后续 setup写入初始事件、建立投影并启动之前就存在。目录内的 9 个文件按编号顺序组织文件README 中的职责说明实际内容01_user.sql创建 ZITADEL 连接数据库所用的用户CREATE USER %[1]s02_database.sql创建 ZITADEL 使用的数据库CREATE DATABASE %[1]s03_grant_user.sql将数据库完全权限授予上一步创建的用户GRANT ALL ON DATABASE %[1]s TO %[2]s04_eventstore.sql创建事件溯源所需的 SchemaCREATE SCHEMA IF NOT EXISTS eventstore 表级授权05_projections.sql创建读取数据投影所需的 SchemaCREATE SCHEMA IF NOT EXISTS projections 表级授权06_system.sql创建 ZITADEL 自身系统/配置所需的 SchemaCREATE SCHEMA IF NOT EXISTS system 表级授权07_encryption_keys_table.sql创建加密密钥表用于事件数据加密CREATE TABLE IF NOT EXISTS system.encryption_keys08_events_table.sql创建事件溯源的核心事件表eventstore.events2表、索引、复合类型与函数10_unique_constraints_table.sql创建用于检查事件唯一约束的表eventstore.unique_constraints表值得注意的是 03_grant_user.sql 的授权范围README 指出用户必须拥有数据库的full accessALL 权限因为 ZITADEL 在运行时会执行 DDL/DML——后续的 setup 迁移、投影表创建等都在服务用户身份下动态发生因此不能用最小权限原则做收缩授权。前三个文件用户、数据库与授权的占位符机制01_user.sql、02_database.sql、03_grant_user.sql都是只含占位符的模板语句由 Go 代码在运行时填充实际值。以 01_user.sql 为例文件全部内容为-- replace %[1]s with the name of the user CREATE USER %[1]s%[1]s、%[2]s是 Gofmt包的格式化占位符。填充逻辑在 verify_user.go 中// 先格式化用户名避免密码出现在 fmt 格式串中 stmt : fmt.Sprintf(createUserStmt, username) if password ! { stmt WITH PASSWORD quotePostgresLiteral(password) }这里有两个工程细节值得注意见 verify_user.go 中quotePostgresLiteral及其注释密码不作为 fmt 参数因为密码可能包含%字符会被误解析为格式动词因此用户名先填充密码通过专用的quotePostgresLiteral以单引号字面量追加单引号内部按 PostgreSQL 规则翻倍转义。建用户前先查目录代码先执行SELECT EXISTS(SELECT 1 FROM pg_roles WHERE rolname $1)若角色已存在则直接跳过创建——注释说明这样做是为了避免在重复部署/重复初始化时PostgreSQL 日志里记录出包含明文密码的完整CREATE USER语句。02_database.sqlCREATE DATABASE %[1]s与 03_grant_user.sqlGRANT ALL ON DATABASE %[1]s TO %[2]s第一个占位符填数据库名、第二个填用户名同理均由 verify_database.go、verify_grant.go 通过fmt.Sprintf填充后执行。三个 Schema 文件eventstore、projections 与 system04_eventstore.sql、05_projections.sql、06_system.sql 结构一致以 eventstore 为例CREATE SCHEMA IF NOT EXISTS eventstore; GRANT ALL ON ALL TABLES IN SCHEMA eventstore TO %[1]s;这三个 Schema 构成了 ZITADEL 数据库的逻辑分区eventstore事件溯源层存放不可变的事件流events2表与唯一约束检查表是事实来源source of truthprojections投影层从事件流构建出可高效读取的物化状态README 原文即 creates the schema needed to read the datasystemZITADEL 系统自身元数据README 表述为 creates the schema needed for ZITADEL itself加密密钥表就建在这里。GRANT ALL ON ALL TABLES语句针对的是 Schema 内的全部表含后续创建的表不自动覆盖此处为对当时已存在表的一次性授权 Schema 创建动作与 03 文件中的数据库级GRANT ALL共同保证服务用户能在运行期自由 DDL。08_events_table.sql 深读事件表、复合类型与命令转事件函数08_events_table.sql 是目录中最重的一个文件它定义了事件溯源的地基。核心是一张主表与三个索引CREATE TABLE IF NOT EXISTS eventstore.events2 ( instance_id TEXT NOT NULL , aggregate_type TEXT NOT NULL , aggregate_id TEXT NOT NULL , event_type TEXT NOT NULL , sequence BIGINT NOT NULL , revision SMALLINT NOT NULL , created_at TIMESTAMPTZ NOT NULL , payload JSONB , creator TEXT NOT NULL , owner TEXT NOT NULL , position DECIMAL NOT NULL , in_tx_order INTEGER NOT NULL , PRIMARY KEY (instance_id, aggregate_type, aggregate_id, sequence) ); CREATE INDEX IF NOT EXISTS es_active_instances ON eventstore.events2 (created_at DESC, instance_id); CREATE INDEX IF NOT EXISTS es_wm ON eventstore.events2 (aggregate_id, instance_id, aggregate_type, event_type); CREATE INDEX IF NOT EXISTS es_projection ON eventstore.events2 (instance_id, aggregate_type, event_type, position);从字段设计可以看出事件存储模型的几个要点复合主键(instance_id, aggregate_type, aggregate_id, sequence)事件以实例 聚合类型 聚合 ID 序列号定位sequence即聚合内的事件序号revision 为事件版本号二者语义不同payload JSONB事件负载以 PostgreSQL 原生 JSONB 存储created_at记录事件时间三个索引各服务一类访问模式es_active_instances按创建时间倒序扫描活跃实例es_wmwater mark按聚合定位事件水位es_projection按实例/聚合类型/事件类型 position支撑投影的增量消费position是clock_timestamp()抽取的 epoch 值作为全局排序位点。文件后半部分定义了事件写入的核心机制。先是两个 PostgreSQL 复合类型见 08_events_table.sql 第 24–51 行-- represents an event to be created. CREATE TYPE eventstore.command2 AS ( instance_id TEXT , aggregate_type TEXT , aggregate_id TEXT , command_type TEXT , revision INT2 , payload JSONB , creator TEXT , owner TEXT , enforce_owner BOOLEAN ); CREATE TYPE eventstore.latest_command AS ( instance_id TEXT , aggregate_type TEXT , aggregate_id TEXT , owner TEXT , sequence BIGINT );command2即待落库的事件command_type字段落库后映射为event_typeenforce_owner控制 owner资源属主ZITADEL 的跨租户隔离依据是沿用该聚合历史事件的 owner 还是强制使用命令携带的 owner。关键的 plpgsql 函数eventstore.commands_to_events(commands eventstore.command2[])见 08_events_table.sql 第 53–154 行实现了批量命令 → 事件的原子转换先对输入数组按(instance_id, aggregate_type, aggregate_id)分组通过LEFT JOIN LATERAL查询每个聚合当前events2中最大的sequence_latest_events遍历每条命令若不enforce_owner则继承该聚合已有事件的 owner否则把新 owner 回写到_latest_events使同批后续事件也沿用新 ownerowner 变更传播最后一条RETURN QUERY中用COALESCE(e.sequence, 0) ROW_NUMBER() OVER (PARTITION BY ... ORDER BY c.in_tx_order)为同批命令分配连续序列号statement_timestamp()作为created_at保证同一事务内多条命令写入同一聚合时序列号严格递增。其上的包装函数eventstore.push文件第 156–161 行是 Go 侧实际调用的入口CREATE OR REPLACE FUNCTION eventstore.push(commands eventstore.command2[]) RETURNS SETOF eventstore.events2 VOLATILE AS $$ INSERT INTO eventstore.events2 SELECT * FROM eventstore.commands_to_events(commands) ORDER BY in_tx_order RETURNING * $$ LANGUAGE SQL;也就是说ZITADEL 写入事件不是逐条INSERT而是把一批命令作为复合类型数组一次性交给push由数据库端完成序列号分配与 owner 解析从而把一致性逻辑收敛到单条 SQL 执行计划内。与之配合internal/eventstore/v3/eventstore.go 中的CheckExecutionPlan会在初始化后通过 pgx 的conn.Raw注册这些复合类型RegisterEventstoreTypes因为 pgx 驱动需要显式注册才能正确编解码command2、latest_command这类自定义类型——这就是为什么建表之后还要再跑一次类型注册。07 与 10 文件加密密钥表与唯一约束表07_encryption_keys_table.sql 创建system.encryption_keys表CREATE TABLE IF NOT EXISTS system.encryption_keys ( id TEXT NOT NULL , key TEXT NOT NULL , PRIMARY KEY (id) );这是 README 所述creates the table for encryption keys (for event data)的落地事件 payload 可按密钥 ID 加密存储密钥本身落在该表中由 ZITADEL 的 master key 体系参见 cmd/key 与 cmd/encryption 子命令保护。10_unique_constraints_table.sql 创建CREATE TABLE IF NOT EXISTS eventstore.unique_constraints ( instance_id TEXT, unique_type TEXT, unique_field TEXT, PRIMARY KEY (instance_id, unique_type, unique_field) );README 说明其用途是check unique constraints for events——事件级唯一性约束例如同一实例内某邮箱只能绑定一个用户这类业务不变式通过在这张表中登记实例 约束类型 唯一值的复合主键实现与events2表解耦使约束的增删不必改动核心事件表结构。zitadel init 如何消费这些 SQL嵌入、编排与幂等这些 SQL 文件不是让 DBA 手工逐个执行的脚本而是被 Go 二进制在编译期直接嵌入。init.go 第 16–32 行var ( //go:embed sql/*.sql stmts embed.FS createUserStmt string grantStmt string databaseStmt string createEventstoreStmt string createProjectionsStmt string createSystemStmt string createEncryptionKeysStmt string createEventsStmt string createUniqueConstraints string roleAlreadyExistsCode 42710 dbAlreadyExistsCode 42P04 )ReadStmts()init.go 第 118–165 行依次从嵌入的embed.FS中按文件名读取 9 个 SQL 语句到包级变量中与上文文件编号一一对应。随后由zitadel init命令按步骤执行。InitAllinit.go 第 74–89 行的编排顺序为init (admin 连接) → VerifyUser → VerifyDatabase → VerifyGrant → verifyZitadel (服务用户连接) → system / encryption_keys / projections / eventstore / events / unique_constraints即先用Admin 凭据完成用户、库、授权三件事对应 01–03 文件再切换到服务用户凭据连接到目标库创建三个 Schema 与基础表对应 04–10 文件。VerifyZitadel的内部顺序见 verify_schema.go 第 51–94 行system → encryption keys → projections → eventstore → events 表 → unique constraints。幂等性由两层机制保证目录预检VerifyDatabase先判断当前连接的库是否就是目标库对应Admin.ExistingDatabase配置再查pg_database目录VerifyUser查pg_roles。已存在则记录日志并跳过不执行 DDL错误码白名单所有 DDL 经由 helper.go 中的exec执行它把 PostgreSQL 错误码与白名单比对后吞掉预期内的冲突错误——用户已存在SQLSTATE42710、数据库已存在42P04都不算失败。配合 SQL 中的IF NOT EXISTSzitadel init可以安全地重复执行。zitadel init还提供四个可独立运行的子命令init.go 第 70 行cmd.AddCommand(newSchema(), newDatabase(), newUser(), newGrant())子命令作用使用的 SQLzitadel init默认完整初始化用户、库、授权 全部 Schema/表01–10 全部zitadel init user仅初始化数据库用户01zitadel init database仅初始化数据库02zitadel init grant仅设置 ALL 授权03zitadel init schema别名zitadel以服务用户凭据引导全部 Schema 与基础表无需 admin/超管权限04–10无超管权限场景手动建库建用户 zitadel init schema托管型 PostgreSQLRDS 类服务通常不发放CREATE DATABASE/ 创建任意用户的权限zitadel init完整流程依赖 Admin 凭据做这三件事。init.go 的命令说明给出了官方推荐的替代路径If you dont have admin/superuser credentials (e.g. on a managed PostgreSQL service), you can provision the database user and database manually and then run only zitadel init schema to bootstrap the required schemas and tables using the service user credentials. No admin privileges are needed for that sub-command.即先由平台侧手工创建数据库与用户并确保该用户是目标库的 owner然后只运行zitadel init schema。该子命令verify_schema.go 第 18–49 行直接调用verifyZitadel只依赖服务用户即可把 04–10 文件的对象建齐——这正是 SQL 文件按权限需求分成两段编排的价值01–03 需要 admin04–10 只需要库 owner。createEvents中还有一处与 setup 阶段的衔接细节verify_schema.go 第 125–153 行执行08_events_table.sql前会先查询information_schema.tables中eventstore下是否已存在events%表若已存在则跳过建表——注释说明 if events already exists events2 is created during a setup job即旧版events表向events2的升级由后续 setup 任务负责建表成功后还会调用es_v3.CheckExecutionPlan完成 pgx 复合类型注册。配置与冷启动defaults.yaml 与 start-from-init初始化所需的全部连接参数来自Database.postgres配置块默认值集中在 cmd/defaults.yaml第 248–297 行附近每个字段都有对应的环境变量Database: postgres: DSN: # ZITADEL_DATABASE_POSTGRES_DSN Host: localhost # ZITADEL_DATABASE_POSTGRES_HOST Port: 5432 # ZITADEL_DATABASE_POSTGRES_PORT Database: zitadel # ZITADEL_DATABASE_POSTGRES_DATABASE User: Username: zitadel # ZITADEL_DATABASE_POSTGRES_USER_USERNAME Password: # ZITADEL_DATABASE_POSTGRES_USER_PASSWORD Admin: ExistingDatabase: # ZITADEL_DATABASE_POSTGRES_ADMIN_EXISTINGDATABASE Username: postgres # ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME Password: postgres # ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD配置语义要点Admin段仅供zitadel init家族使用它连接一个已存在的库ExistingDatabase用来执行 01–03 文件的建用户、建库、授权动作User段是 ZITADEL 运行时的服务账号04–10 文件中的%[1]s占位符以及所有GRANT ... TO目标都指向它DSN模式限制当配置完整连接串 DSN 时zitadel init无法再借 Admin 连接去创建独立的目标库/用户——DSN 指向的库和用户必须已存在且权限足够见 cmd/defaults.yaml 中 DSN 字段的注释。配置结构在 cmd/initialise/config.go 中通过 viperUnmarshal反序列化为Config{Database: database.Config, ...}数据库连接抽象来自 internal/database含 postgres / cockroach 两个方言实现因此同一套 SQL 引导流程对不同数据库方言也有一致的入口。最后这些初始化逻辑是冷启动命令的一环。cmd/start/start_from_init.go 定义的zitadel start-from-init按注释描述 First the minimum requirements to start ZITADEL are set up. Second the initial events are created. Last ZITADEL starts.其执行链为initialise.InitAll本文主角SQL 引导→setup.Setup写入初始事件、建立投影此时才用到事件表与加密密钥表→startZitadel。也就是说cmd/initialise/sql中的 9 个文件是 ZITADEL 从空数据库到可服务状态的第一块基石后续的 setup 迁移cmd/setup与运行时 DDL 都以它们建立的对象为前提。小结cmd/initialise/sql/目录以编号文件 占位符模板的形式把 ZITADEL 的数据库前置条件完整固化在仓库内01–03 解决专用用户、专用库、全量授权的身份问题04–06 建立 eventstore/projections/system 三层 Schema 分区07/08/10 则落地事件溯源核心——events2事件表含push/commands_to_events数据库端写入函数、加密密钥表与事件唯一约束表。Go 侧通过go:embed在编译期嵌入这些 SQL由zitadel init及其user/database/grant/schema子命令按权限边界拆分执行并以目录预检 SQLSTATE 白名单实现可重复执行的幂等语义配合 cmd/defaults.yaml 中 Admin/User 双账号配置即可覆盖自建 PostgreSQL 与托管数据库两类部署形态。【免费下载链接】zitadelZITADEL - Identity infrastructure, simplified for you.项目地址: https://gitcode.com/GitHub_Trending/zi/zitadel创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门