首页
/ Terraform pg 后端测试实战:Docker 搭建 Postgres 环境并运行状态后端验收测试

Terraform pg 后端测试实战:Docker 搭建 Postgres 环境并运行状态后端验收测试

2026-09-05 11:25:26作者:乔或婵

本文为 Terraform 内置 pg 远程状态后端(remote state backend)的测试指南。pg 后端把 Terraform 的状态文件存储到 PostgreSQL 数据库中,而 internal/backend/remote-state/pg/README.md 正是该模块自带的测试手册:它给出了一键启动带 SSL 的 Postgres 容器、初始化测试数据库、配置连接环境变量并运行验收测试的完整步骤。读完本文,你将掌握如何在本地从零复现 pg 后端的验收测试环境,理解 DATABASE_URL 等环境变量的设计动机,并能结合 backend.goclient.go 的源码理解该后端的配置项、建表逻辑与基于 Postgres 咨询锁(advisory lock)的状态锁机制。

一、pg 后端在 Terraform 中的位置

Terraform 内置了一批远程状态后端,统一在 internal/backend/init/init.go 中注册。其中 pg 是 14 个内置后端之一:

// internal/backend/init/init.go
backends = map[string]backend.InitFn{
    "local":  func() backend.Backend { return backendLocal.New() },
    "remote": func() backend.Backend { return backendRemote.New(services) },

    // Remote State backends.
    "azurerm":    func() backend.Backend { return backendAzure.New() },
    "consul":     func() backend.Backend { return backendConsul.New() },
    // ...
    "pg":         func() backend.Backend { return backendPg.New() },
    "s3":         func() backend.Backend { return backendS3.New() },
    // ...
}

从源码结构看,pg 目录是一个独立的 Go module(自带 go.mod),被其他后端模块的 go.mod 以 replace 方式引用,而真正对外暴露的能力由 New() 构造的 Backend 提供。理解这一点后,下面的测试指南本质就是:为这个模块的验收测试准备一个它期望的 Postgres 服务,然后让 go test 连上去。

二、搭建测试环境:启动带 SSL 的 Postgres 容器

pg 后端的测试要求目标实例开启 SSL(连接串中会显式指定 sslmode=require),因此 README 的第一步是用 Docker 启动一个显式启用 SSL 的 postgres 镜像。注意使用了镜像自带的 snakeoil 自签名证书(/etc/ssl/certs/ssl-cert-snakeoil.pem/etc/ssl/private/ssl-cert-snakeoil.key),这是 Debian 系 postgres 镜像的默认测试证书:

docker run \
  --name pg_backend_testing \
  --rm \
  -e POSTGRES_USER=postgres \
  -e POSTGRES_PASSWORD=password \
  -p 5432:5432 \
  postgres:latest \
  -c ssl=on \
  -c ssl_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem \
  -c ssl_key_file=/etc/ssl/private/ssl-cert-snakeoil.key

参数说明:

  • --name pg_backend_testing:固定容器名,后续步骤通过它定位容器;
  • --rm:容器退出后自动清理,测试环境用完即弃;
  • POSTGRES_USER=postgres / POSTGRES_PASSWORD=password:初始化超级用户。README 特别强调:测试使用用户 postgres,且该值会在后续命令中复用(即第三步的 createdb -U postgres 与环境变量 PGUSER=postgres);
  • -p 5432:5432:映射到宿主机本地端口,使连接串 localhost:5432 生效;
  • 镜像名之后的 -c ... 参数:透传给 postgres 服务进程的启动参数,用于强制开启 SSL 并指定证书/密钥路径。

三、创建测试专用数据库

启动容器后,README 要求用 exec 进入容器 shell,再创建名为 terraform_backend_pg_test 的数据库——这个库名是测试硬编码期望的(backend_test.gotestACC 的默认回退连接串也印证了这一点:postgres://localhost/terraform_backend_pg_test?sslmode=disable):

% docker exec -it $(docker ps -aqf "name=^pg_backend_testing$") bash

这里 docker ps -aqf "name=^pg_backend_testing$" 用正则精确锚定容器名,避免误取同名前缀的其他容器。

进入容器后执行:

root@<container-id>:/# createdb -U postgres terraform_backend_pg_test
root@<container-id>:/# exit

创建完成后 exit 回到宿主机,测试环境就绪。

四、配置测试所需的环境变量

在宿主机上导出以下三个环境变量:

DATABASE_URL=postgresql://localhost:5432/terraform_backend_pg_test?sslmode=require
PGUSER=postgres
PGPASSWORD=password

README 在这里给出一条重要设计说明,值得展开理解:

DATABASE_URL 的值是一个连接串,不应包含用户名和密码。用户名和密码必须通过独立的环境变量提供,以便部分测试可以覆盖这些值。

这一点可以从测试源码中得到印证。backend_test.goTestBackendConfig 中专门设计了几个依赖此特性的用例:

  • setting-credentials-using-env-vars:设置 PGUSER=baduserPGPASSWORD=badpassword,期望连接失败并返回 password authentication failed for user "baduser"——即测试通过覆盖这两个变量来验证错误凭据的处理路径;
  • host-in-env-vars:设置 PGHOST=hostthatdoesnotexist,期望报 no such host
  • missing-conn_str-defaults-to-localhost:不提供 conn_str 时,仅靠 PGDATABASE + 默认的 localhost 主机也能建连;
  • boolean-env-vars / wrong-boolean-env-vars:验证 PG_SKIP_SCHEMA_CREATION 等布尔型环境变量解析,非法值(如 foo)会触发 invalid value for "skip_schema_creation" 配置错误。

也就是说,lib/pq 驱动本身支持从 PGUSERPGPASSWORDPGHOSTPGDATABASE 等环境变量补全连接信息;测试正是利用"连接串不含凭据"这一约定,才能在不改写 URL 的前提下注入错误凭据来验证失败分支。

五、运行测试

环境就绪后,按照 backend_test.goclient_test.go 文件头的注释,运行验收测试:

TF_ACC=1 GO111MODULE=on go test -v -timeout=2m -parallel=4 github.com/hashicorp/terraform/backend/remote-state/pg

关键约束有两条:

  1. 必须设置 TF_ACC=1。两个测试文件都通过 testACC(t) 守卫:未设置 TF_ACC 时所有测试直接 t.Skip()。这是 Terraform 验收测试的通用约定(依赖真实外部服务的测试才在验收模式下运行);
  2. 每次运行前都要重建环境。README 明确指出:每次运行测试都需要重新启动容器并重新创建 terraform_backend_pg_test 数据库。这是因为测试会创建/丢弃大量临时 schema(如 terraform_TestBackendConfigtest with spaces: TestBackendStates 这类带测试名的 schema),且各用例使用 DROP SCHEMA IF EXISTS ... CASCADE 做清理,而容器本身是 --rm 即用即弃的,因此环境按次重建是最简单可靠的隔离方式。

这套测试覆盖了哪些行为?结合测试源码可以确认验收范围:

  • backend_test.goTestBackendConfig:配置解析、环境变量回退、错误凭据/错误主机的连接失败路径,以及 TestBackendStates 状态读写基本用例;
  • TestBackendConfigSkipOptions:验证 skip_schema_creationskip_table_creationskip_index_creation 三个选项下,已预先建好前置对象的库能正常通过,并通过 pg_indexes 校验 states_by_name 索引是否存在;还验证了同一 schema 内插入两个同名 workspace 会被唯一约束拒绝;
  • TestBackendStateLocks / TestBackendConcurrentLock:验证状态锁的正确性——两个后端实例先后锁定各自的 workspace,确认"先创建 workspace 再并发加锁"的完整流程;
  • client_test.goTestRemoteClient / TestRemoteLocks:调用共享的 remote.TestClient / remote.TestRemoteLocks(来自 internal/states/remote 包),对所有 remote-state 后端统一执行 Get/Put/Delete 数据完整性与锁互斥检查。

六、源码视角:pg 后端测试验证的核心机制

理解了测试在"测什么",再回到源码看它"凭什么通过",测试的价值会更清晰。

6.1 配置项与自动建表

backend.goNew() 定义了后端配置模式,共 5 个属性,全部可选,并各自绑定环境变量与默认值:

配置项 环境变量 默认值 说明
conn_str PG_CONN_STR —(缺省时由驱动回退到 PG 环境变量) Postgres 连接串,postgres:// URL
schema_name PG_SCHEMA_NAME terraform_remote_state 自动管理的 Postgres schema 名
skip_schema_creation PG_SKIP_SCHEMA_CREATION false true 时不创建 schema
skip_table_creation PG_SKIP_TABLE_CREATION false true 时不创建表
skip_index_creation PG_SKIP_INDEX_CREATION false true 时不创建索引

Configure()backend.go#L91-L151)在建立连接后会按需执行三段初始化 DDL:

  1. schema:先查 information_schema.schemata 判断是否存在,不存在才执行 CREATE SCHEMA。源码注释解释了不用 CREATE SCHEMA IF NOT EXISTS 一步到位的原因:若用户未被授予 CREATE SCHEMA 权限,直接执行会报错,而先探测再创建可以把"已存在"与"无权限"两种情形区分开;
  2. :创建序列 public.global_states_id_seqstates 表(常量 statesTableName = "states"),结构为 id bigint(由序列生成主键), name text UNIQUE, data text
  3. 索引CREATE UNIQUE INDEX IF NOT EXISTS states_by_name ON <schema>.states (name)

测试中的 TestBackendConfigSkipOptions 正是逐一反向验证这三步:当某一步被 skip 时,要求前置对象已由测试自行预建(见该用例的 Setup 函数),缺任何前置都会导致 Configure 失败。

另外注意 b.schemaName = pq.QuoteIdentifier(...)——schema 名在 Configure 阶段就做标识符引用转义。TestBackendStates 中刻意用 test with spaces: TestBackendStates 这种含空格、含冒号的 schema 名做用例,就是为了覆盖这条转义路径。

6.2 状态读写:Get / Put / Delete

client.goRemoteClient 把每个 workspace 的状态存为 states 表中 name 唯一的一行:

  • Get()SELECT data FROM <schema>.states WHERE name = $1,无行时返回空状态(sql.ErrNoRows 分支),有行时附带 data 的 MD5 摘要作为 remote.Payload.MD5
  • Put()INSERT ... ON CONFLICT (name) DO UPDATE SET data = $2,即 upsert,保证同一 workspace 行唯一;
  • Delete():按 name 直接删除行。

backend_state.go 在其上实现了 workspace 管理:Workspaces() 查询 name != 'default' 的所有行并始终把 default 排在首位;StateMgr() 发现目标 workspace 不存在时会先加锁,写入一份空状态再持久化——源码注释称这是"sentinel value"(哨兵值),否则空状态不会在表中留下行,Workspaces() 就无法枚举出它。

6.3 状态锁:advisory lock 与"创建锁"

pg 后端的并发安全建立在 Postgres 的咨询锁(advisory lock)之上,client.go#L72-L133Lock() 逻辑是全模块最精巧的部分,也解释了 TestBackendConcurrentLock 中"必须先创建 workspace 才能并发加锁"的测试写法:

  • 行锁:对已存在的状态行,执行 SELECT id, pg_try_advisory_lock(id), pg_try_advisory_lock(-1) FROM states WHERE name = $1,即同时尝试获取"该行 id 对应的锁"和"创建锁";
  • 创建锁(-1):当 workspace 行还不存在时(sql.ErrNoRows),单独尝试 pg_try_advisory_lock(-1)。这把全局的 -1 锁保证同一时刻只有一个客户端可以"创建"某个 workspace,拿到后把 info.Path 置为 "-1"
  • 冲突处理:若行锁拿到但创建锁被占(pg_try_advisory_lock(-1) 返回 false),说明有另一进程正在创建该 workspace,此时会先释放刚拿到的行锁(lockUnlock)再报 Cannot lock workspace; already locked for workspace creation——源码注释说明原因是"此时触碰该行可能不安全";
  • 解锁Unlock() 只释放 info.Path 记录的那一把锁(pg_advisory_unlock),因此"只拿到了行锁"的会话不会误释放创建锁,-1 创建锁则会在拿到行锁的分支或解锁路径中被显式释放。

咨询锁的粒度是"会话级 + 数字 key",天然避免了为每个 workspace 建锁表的额外迁移,且失败是原子的(pg_try_advisory_lock 非阻塞、立即返回 true/false)。TestRemoteLocks 通过共享的 remote.TestRemoteLocks 对两个 RemoteClient 做互斥验证,TestBackendStateLocks 则走完整的 Backend → StateMgr → remote.State 链路,两层互为补充。

七、环境要求与常见注意点

  • 适用前提:本地已安装 Docker;运行测试的机器需要能访问 localhost:5432(若 Postgres 不在本机,需相应修改 DATABASE_URL 的主机与端口,并保持 sslmode=require 与容器 SSL 配置一致);
  • 凭据必须与容器变量对齐PGUSER/PGPASSWORD 必须与 docker run 时的 POSTGRES_USER/POSTGRES_PASSWORD 一致,且与 createdb -U 使用的用户一致,否则会出现 README 隐含的"连得上库、建不了库"类故障;
  • 不要跳过数据库创建:容器初始化只保证默认库存在,terraform_backend_pg_test 必须手动 createdb,这是每次重跑测试前最容易遗漏的一步;
  • 测试是自清理的:各用例以 defer db.Query("DROP SCHEMA IF EXISTS ... CASCADE") 清理自己创建的 schema,但不删除数据库本身,因此 README 才强调数据库需要每次重建——旧库里残留的 schema 不会干扰测试,但重建环境可以彻底消除变量。

小结

围绕 internal/backend/remote-state/pg/README.md 的三步流程(起 SSL 容器、建库、设环境变量)可以直接复现 pg 后端的验收测试环境;而测试真正压测的是 backend.go 的自动建表/跳过选项、client.go 的 upsert 状态读写,以及基于 pg_try_advisory_lock 的双锁(行锁 + -1 创建锁)并发控制。掌握这套指南后,无论是为 pg 后端补充用例,还是排查 terraform init 时连接 Postgres 状态的失败,都有了可对照的测试基线与源码坐标。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
983
503
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384