- 网络安全
- 认证鉴权
- 运维
- 后端
【免费下载链接】teleport
The easiest, and most secure way to access and protect all of your infrastructure.
本指南以 integrations/terraform/CONTRIBUTING.md 为主线,系统讲解如何在 Teleport 的 Terraform Provider 中新增一个资源(resource)或数据源(data source),以及如何把遗留(legacy)生成式资源平滑迁移到通用驱动(generic driver)架构。读者将掌握完整的六步新增流程、五步迁移路径、标识符策略选型与评审清单,并了解底层 tfdriver 驱动的生命周期行为,可直接用于向当前仓库提交新资源支持。
背景:为什么需要"通用驱动"
Teleport Terraform Provider 构建在 HashiCorp Terraform Plugin Framework 之上(入口见 provider/provider.go)。早期资源由代码生成器批量产出,每个资源都携带大量重复的 Terraform 生命周期样板代码;后续演进出的通用驱动(generic driver)把"Terraform 生命周期机制"与"资源专属的 Teleport API 调用"彻底分离,从而让新增一个资源只需要编写很少的手写代码。
两者的架构关系见 ARCHITECTURE.md:provider 的注册表被拆成两组——provider/internal/legacy(遗留生成式实现)与provider/internal/resources(通用驱动实现)。GetResources与GetDataSources先取 legacy 注册表,再把通用驱动资源插入其中(见 provider.go 的 GetResources/GetDataSources),这样迁移后的资源可以无缝替换同名注册,对外暴露的 Terraform 资源名完全不变。通用驱动是新增资源的唯一推荐方式,legacy 生成器已被标记为废弃,将在所有存量资源迁移完成后移除。
开始前的准备
按文档要求,开始动手前需确认以下几点:
- 除非特别说明,所有命令都在
integrations/terraform目录下执行; - 先查阅对应的 Teleport 资源 API;涉及 RFD 153 的资源,遵循 rfd/0153-resource-guidelines.md;
- 迁移存量资源时,必须保留公开的 Terraform 名称、字段路径、import ID 与 state 行为;
- 保持生成代码的"生成物"属性:除了文档命令产生的生成输出外,不要手改生成文件。
常用命令速查:
# 重新生成 Terraform schema/copy 代码与 legacy 生成文件 make gen-tfschema # 构建/安装 provider,并在 testlib 中运行 Terraform 验收风格测试 make test # 从测试套件中运行一个聚焦用例 make test TEST_ARGS='-run TestTerraformOSS/TestApp' # 重新生成面向用户的 Terraform Provider 参考文档 make docs关于make test有一点需要留意:测试目标依赖本机安装 Terraform v1.4+(Makefile 中会检测terraform -version,不满足直接报错退出),并通过gotestsum把结果输出到test-logs/unit-tests-terraform.xml;make test-ent则会先由go generate testlib/plugin_test.go生成企业版测试文件再跑完整套件。
新增一个通用驱动资源(六步全流程)
第一步:生成或更新 Terraform schema 代码
资源的 Terraform schema 与 copy(转换)函数统一存放在integrations/terraform/tfschema。如果目标资源的GenSchemaXXX/CopyXXXToTerraform/CopyXXXFromTerraform已经存在,直接复用即可。
若不存在,则按以下步骤补齐:
- 新增或更新一个
protoc-gen-terraform-*.yaml配置文件(仓库根目录下已有一批现成示例,如 protoc-gen-terraform-accesslist.yaml、protoc-gen-terraform-loginrule.yaml 等); - 在 Makefile 的
gen-tfschema目标中新增对应的protoc调用与mv步骤; - 执行
make gen-tfschema。
从 Makefile 的 gen-tfschema 目标可以看到该目标的真实工作方式:它通过go list -m定位 go mod cache 中的 gogo/protobuf 路径,对每个 proto 文件执行一次protoc --terraform_out=config=protoc-gen-terraform-XXX.yaml:./tfschema,再把产物从tfschema/github.com/gravitational/teleport/...逐目录mv到扁平结构,最后调用go run ./gen/main.go重新生成 legacy 代码。凡是 proto 变更影响到 Terraform 层,都必须在integrations/terraform下运行该目标——这也呼应了"不要手改生成文件"的纪律。
第二步:编写 Teleport API 适配层
创建provider/internal/teleport/<resource>.go。适配层应当包装*client.Client,对托管资源实现tfdriver.ResourceClient[T, I](Get/Create/Upsert/Delete),对数据源实现tfdriver.DataSourceClient[T, I](只需Get)。该层只负责 Teleport API 调用,不得依赖 Terraform 的 schema、plan、state 或 diagnostics。
文档给出的骨架:
package teleport import ( "context" "github.com/gravitational/trace" "github.com/gravitational/teleport/api/client" apitypes "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/integrations/terraform/provider/internal/tfdriver" ) func NewFooClient(c *client.Client) FooClient { return FooClient{client: c} } type FooClient struct { client *client.Client } func (c FooClient) Get(ctx context.Context, id tfdriver.NameIdentifier) (*apitypes.FooV1, error) { foo, err := c.client.GetFoo(ctx, id.Name) if err != nil { return nil, trace.Wrap(err) } return foo, nil } func (c FooClient) Create(ctx context.Context, foo *apitypes.FooV1) error { return trace.Wrap(c.client.CreateFoo(ctx, foo)) } func (c FooClient) Upsert(ctx context.Context, foo *apitypes.FooV1) error { return trace.Wrap(c.client.UpsertFoo(ctx, foo)) } func (c FooClient) Delete(ctx context.Context, id tfdriver.NameIdentifier) error { return trace.Wrap(c.client.DeleteFoo(ctx, id.Name)) }两点进阶要求:
- API 返回接口类型时,在适配层做类型断言,遇到意外类型返回可读的错误;
- 更新需要保留服务端字段时,让适配层实现
tfdriver.UpdatePreparer[T]。真实例子见 provider/internal/teleport/access_list.go:PrepareUpdate会把旧资源的Spec.Audit.NextAuditDate(服务端计算字段)拷贝到新值上,避免更新时被重置;驱动在 resource.go 的 Update 流程中会自动检测该接口并调用。
第三步:添加资源与数据源描述符
创建provider/internal/resources/<resource>.go。描述符(descriptor)负责把 API 适配层、生成的 schema/copy 函数、标识符策略、normalizer 与 revision 提取逻辑"接"在一起:
package resources import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/tfsdk" apitypes "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/integrations/terraform/provider/internal/teleport" "github.com/gravitational/teleport/integrations/terraform/provider/internal/tfdriver" "github.com/gravitational/teleport/integrations/terraform/tfschema" ) func NewFooDataSourceType() tfdriver.DataSourceType[apitypes.FooV1, tfdriver.NameIdentifier] { return tfdriver.DataSourceType[apitypes.FooV1, tfdriver.NameIdentifier]{ NewDataSourceClient: func(p tfsdk.Provider) tfdriver.DataSourceClient[apitypes.FooV1, tfdriver.NameIdentifier] { return teleport.NewFooClient(clientFromProvider(p)) }, Kind: apitypes.KindFoo, Codec: tfdriver.DataSourceCodecFuncs[apitypes.FooV1]{ SchemaFunc: tfschema.GenSchemaFooV1, ToStateFunc: tfschema.CopyFooV1ToTerraform, }, Identifier: tfdriver.NameIdentifierFromPath(path.Root("metadata").AtName("name")), } } func NewFooResourceType() tfdriver.ResourceType[apitypes.FooV1, tfdriver.NameIdentifier] { return tfdriver.ResourceType[apitypes.FooV1, tfdriver.NameIdentifier]{ NewResourceClient: func(p tfsdk.Provider) tfdriver.ResourceClient[apitypes.FooV1, tfdriver.NameIdentifier] { return teleport.NewFooClient(clientFromProvider(p)) }, Kind: apitypes.KindFoo, Codec: tfdriver.ResourceCodecFuncs[apitypes.FooV1]{ SchemaFunc: tfschema.GenSchemaFooV1, FromPlanFunc: tfschema.CopyFooV1FromTerraform, ToStateFunc: tfschema.CopyFooV1ToTerraform, }, Normalizer: tfdriver.CheckAndSetDefaults[apitypes.FooV1](), Identifier: tfdriver.NameIdentifierPolicy( path.Root("metadata").AtName("name"), func(foo *apitypes.FooV1) string { return foo.GetMetadata().Name }, ), ResourceRevision: func(foo *apitypes.FooV1) string { return foo.GetMetadata().Revision }, } }描述符中值得深入理解的两个概念:
标识符策略(IdentifierPolicy):标识符负责把 Terraform 侧的对象映射到 Teleport 集群中唯一的资源,同时决定了 import ID 的解析格式。按 identifier.go 的实现,常用策略有四种:
| 策略 | 适用场景 | import ID 形态 |
|---|---|---|
NameIdentifierPolicy | 以metadata.Name唯一定位的大多数资源 | 纯名称,如my-role |
ScopeQualifiedNameIdentifierPolicy | 以(name, scope)定位的 scoped 资源 | scope 限定的限定名(通过lib/scopes的QualifiedName解析并做强校验) |
CompositeIdentifierPolicy | 双段 ID,如 Access List Member 需要"列表名 + 成员名" | prefix/name两段式 |
SingletonIdentifierPolicy | 集群级单例资源 | 固定名称,import 时 ID 必须与固定名完全一致 |
此外还有ScopeQualifiedCompositeIdentifierPolicy(prefix 与 name 各自都可能带 scope)与"可能不带 scope"的变体。每种策略都内置了FromState、FromResource、FromImportID三套提取逻辑,并负责把标识符渲染成 Terraform import ID。
Normalizer(规范化器):在调用 Teleport API 之前强制资源不变量。内置实现见 normalize.go:
tfdriver.CheckAndSetDefaults[T]():调用资源类型的CheckAndSetDefaults()方法补齐默认值;tfdriver.ForceKindT:当 API 类型需要设置 kind、但 Terraform 不应要求用户配置时,通过SetKind强制写入;tfdriver.ResourceNormalizers[T]:按序组合多个 normalizer。
架构文档的忠告是:优先用 normalizer 表达默认值逻辑,而不是把默认值逻辑重复塞进 API 适配层。
第四步:注册资源
在 provider/provider.go 中完成注册。把资源加入GetResources的genericResourceTypes:
"teleport_foo": resources.NewFooResourceType(),如果同时有数据源,加入GetDataSources的genericDataSourceTypes:
"teleport_foo": resources.NewFooDataSourceType(),从当前源码看,genericResourceTypes已覆盖teleport_access_list、teleport_role、teleport_database、teleport_user、teleport_workload_identity等三十个资源,genericDataSourceTypes与之基本一一对应;这些 map 会通过maps.Insert合并进 legacy 注册表,同名资源由通用驱动实现覆盖 legacy 实现。注意:一个资源只能注册在一个活跃位置(generic map 或 legacy registry),绝不允许两边同时注册同名资源。
第五步:编写测试与 fixture
在testlib/fixtures下添加 Terraform fixture,通常包括:
<resource>_0_create.tf(创建)<resource>_1_update.tf(更新)<resource>_data_source.tf(数据源,如适用)
当前仓库的 fixture 目录(testlib/fixtures)展示了完整命名惯例:例如app_0_create.tf、app_1_update.tf、classifier_data_source.tf,以及针对 cache 行为的app_0_create_with_cache.tf、针对默认值的access_list_defaults.tf等。
在testlib/<resource>_test.go中覆盖以下场景:
- create / read / update / delete 全生命周期;
- create 与 update 后的 plan 稳定性(plan-only 检查);
- import 状态;
- 数据源行为(若存在);
- 如果该资源已知会与缓存读交互,覆盖 cache 启用时的行为。
对含密钥或 write-only 字段的资源,还需断言敏感值不会泄漏进 state,除非 schema 有意存储。
第六步:更新文档
如果资源改变了 Provider 的公开面,运行make docs(该目标依赖gen-tfschema、本地安装 provider 与terraform fmt,最终通过./gen/docs.sh渲染)。参考文档的生成机制见 integrations/terraform/DOCS.md:默认所有资源共用模板 templates/resources.md.tmpl,并自动引用examples/resources/teleport_<resource-name>/resource.tf;如需自定义说明或多种示例,可把默认模板复制为资源专属模板templates/resources/<resource_name>.md.tmpl,再用{{tffile "./examples/resources/..."}}函数嵌入代码示例。
把 legacy 资源迁移到通用驱动(五步路径)
迁移路径与新增资源类似,但兼容性是第一优先级。
第一步:先摸清当前行为
动手改代码前,检查provider/internal/legacy下的现有实现与testlib中既有测试,逐一记录:
- Terraform 资源名与数据源名;
- schema 路径及各字段 required/optional/computed/sensitive 标志;
- import ID 格式;
- 写入 Terraform state 的 ID;
- create/update 使用的方法(Create 还是 Upsert);
- 默认值、强制 kind/version 行为;
- update 时从旧 state 或远端 state 拷贝的字段;
- write-only/密钥字段的特殊处理;
- 重试与轮询行为;
- 数据源的怪癖(quirks)。
通用实现必须复刻这些行为,除非是有意为之且有文档记录的变更。
第二步:补齐通用实现
按"新增资源"的第二、三步,添加provider/internal/teleport/<resource>.go与provider/internal/resources/<resource>.go。schema 与 copy 函数尽量复用 legacy 资源已经在用的tfschema生成代码——这是保持 Terraform 字段兼容稳定的关键。
第三步:切换注册
把资源与数据源从provider/internal/legacy/registry.go移除,并在provider/provider.go的 generic map 中加入同名条目。例如迁移teleport_foo:
// provider/provider.go "teleport_foo": resources.NewFooResourceType(),如有数据源:
"teleport_foo": resources.NewFooDataSourceType(),迁移过程中不得改变公开的 Terraform 类型名——这正是GetResources先加载 legacy 再插入 generic 的合并设计所保证的(见 provider.go)。
第四步:处置 legacy 生成文件
部分 legacy 文件由gen/main.go依据文件内嵌的 payload 生成(make gen-tfschema末尾会先 grep 掉带Code generated by _gen/main.go DO NOT EDIT标记的旧文件再重新生成,见 Makefile)。被转换的资源一旦不再注册,旧生成文件可能仍能编译但已无人使用。安全的情况下,优先删除 legacy 生成器 payload 与对应生成文件;若生成的 schema/copy 函数仍需使用,保留protoc-gen-terraform-*.yaml与make gen-tfschema条目——它们与 legacy provider 生成是相互独立的两条链路。
第五步:围绕兼容性强化测试
先原封不动地运行既有资源测试,再针对迁移敏感行为增补用例:
- 用旧的 import ID 格式导入一个已存在的 Teleport 资源;
- 先 apply 旧 fixture,再 apply 新 fixture,验证不会触发无谓的资源替换;
- create/update 后的 plan-only 检查;
- 用户已在依赖的最小配置下的数据源读取;
- 敏感/write-only 字段的 state 表现;
- Teleport 返回 not found 时的行为。
迭代阶段用make test TEST_ARGS='-run ...'聚焦运行,提交前务必跑完整 provider 测试目标。
驱动层如何实现这些生命周期约定
理解驱动层的通用行为(resource.go),能帮你写出与既有资源行为一致的适配层:
- Create 前检活:
Create先按标识符Get一次,若资源已存在于 Teleport(非单例),直接报错并提示tctl rm或terraform import两条出路,防止误创建重复资源; - Create/Update 前规范化:调用 normalizer 的
NormalizeCreate/NormalizeUpdate; - 最终一致性的重试:创建/更新后进入重试循环,按
retry_base_duration(默认 1s)、retry_cap_duration(默认 5s)、retry_max_tries(默认 10)配置的指数退避(带 HalfJitter)反复Get,直到读到数据;超限后给出"state outdated, please import resource"的诊断——这些重试参数正是在 provider.go 的 Configure 中解析并存入RetryConfig的; - Update 收敛判断:若描述符提供了
ResourceRevision,Update 会一直轮询到远端metadata.Revision真正变化,才认为更新生效(见 resource.go 的 Update 循环)——这就是文档中"Update 应等待真实远端 revision 变化"的实现; - Read 行为:远端资源不存在时,驱动会把资源从 state 中移除(
resp.State.RemoveResource); - Import 状态解析与填充:
ImportState用Identifier.FromImportID解析导入 ID,Get后经 Codec 写入 state,并回填id属性; - 统一诊断封装:所有错误经
internal/tfdiag包装成一致的 Terraform diagnostics。
提交前的评审清单(Review Checklist)
文档给出了明确的 PR 验收标准,逐条核对:
- 资源恰好注册在一个活跃位置:generic map 或 legacy registry,二选一;
- 迁移场景下公开的 Terraform 名称与 import ID 保持不变;
- 资源与数据源的
id字段被一致地填充; - 资源的 kind/version/defaults 由 Teleport 默认值或显式 normalizer 设置;
- 适用时,Update 会等待远端真实 revision 变化;
- 密钥与 write-only 字段的处理是有意为之的;
- 测试覆盖 create/update/delete/import,数据源场景如适用也覆盖;
- schema 或公开文档有变时,已运行
make gen-tfschema与make docs。
参考实现速览
- 架构总览:integrations/terraform/ARCHITECTURE.md
- Provider 入口与注册表:integrations/terraform/provider/provider.go
- 通用驱动实现:integrations/terraform/provider/internal/tfdriver/resource.go、identifier.go、normalize.go
- API 适配层示例:integrations/terraform/provider/internal/teleport/access_list.go
- 描述符目录:integrations/terraform/provider/internal/resources
- 生成规则与 YAML 配置:integrations/terraform/Makefile、protoc-gen-terraform-accesslist.yaml
- 验收测试与 fixture:integrations/terraform/testlib、testlib/fixtures
- 文档生成机制:integrations/terraform/DOCS.md
- 资源指南规范:rfd/0153-resource-guidelines.md
- 网络安全
- 认证鉴权
- 运维
- 后端
【免费下载链接】teleport
The easiest, and most secure way to access and protect all of your infrastructure.
相关推荐
terraform-provider-aws 新增 Ephemeral Resource(临时资源)完整开发指南
terraform provider aws 新增 Ephemeral Resource(临时资源)完整开发指南 导读 本指南基于 terraform prov
IaC云原生基础设施Cua Fleets Terraform Provider:从 Cyclops 迁移到 Fleets 的完整迁移指南
Cua Fleets Terraform Provider:从 Cyclops 迁移到 Fleets 的完整迁移指南 本篇指南基于 Cua 仓库中 MIGRAT
人工智能AI AgentGUI 自动化Agent 评测强化学习Agent 沙箱计算机视觉MCP 服务Ice:macOS 菜单栏管理工具,一键隐藏图标,10 分钟理好菜单栏
Ice:macOS 菜单栏管理工具,一键隐藏图标,10 分钟理好菜单栏 你的 Mac 菜单栏被各种应用图标挤满,Wi Fi 和电池信息被推到角落。Ice 是一款
桌面应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考