news 2026/9/21 2:18:57

Teleport Terraform Provider 资源开发指南:从新增资源到 legacy 迁移的完整实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Teleport Terraform Provider 资源开发指南:从新增资源到 legacy 迁移的完整实践
  • 网络安全
  • 认证鉴权
  • 运维
  • 后端

【免费下载链接】teleport

The easiest, and most secure way to access and protect all of your infrastructure.

项目地址:https://gitcode.com/gh_mirrors/tel/teleport
点击查看免费下载

本指南以 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(通用驱动实现)。GetResourcesGetDataSources先取 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.xmlmake test-ent则会先由go generate testlib/plugin_test.go生成企业版测试文件再跑完整套件。

新增一个通用驱动资源(六步全流程)

第一步:生成或更新 Terraform schema 代码

资源的 Terraform schema 与 copy(转换)函数统一存放在integrations/terraform/tfschema。如果目标资源的GenSchemaXXX/CopyXXXToTerraform/CopyXXXFromTerraform已经存在,直接复用即可。

若不存在,则按以下步骤补齐:

  1. 新增或更新一个protoc-gen-terraform-*.yaml配置文件(仓库根目录下已有一批现成示例,如 protoc-gen-terraform-accesslist.yaml、protoc-gen-terraform-loginrule.yaml 等);
  2. 在 Makefile 的gen-tfschema目标中新增对应的protoc调用与mv步骤;
  3. 执行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 形态
NameIdentifierPolicymetadata.Name唯一定位的大多数资源纯名称,如my-role
ScopeQualifiedNameIdentifierPolicy(name, scope)定位的 scoped 资源scope 限定的限定名(通过lib/scopesQualifiedName解析并做强校验)
CompositeIdentifierPolicy双段 ID,如 Access List Member 需要"列表名 + 成员名"prefix/name两段式
SingletonIdentifierPolicy集群级单例资源固定名称,import 时 ID 必须与固定名完全一致

此外还有ScopeQualifiedCompositeIdentifierPolicy(prefix 与 name 各自都可能带 scope)与"可能不带 scope"的变体。每种策略都内置了FromStateFromResourceFromImportID三套提取逻辑,并负责把标识符渲染成 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 中完成注册。把资源加入GetResourcesgenericResourceTypes

"teleport_foo": resources.NewFooResourceType(),

如果同时有数据源,加入GetDataSourcesgenericDataSourceTypes

"teleport_foo": resources.NewFooDataSourceType(),

从当前源码看,genericResourceTypes已覆盖teleport_access_listteleport_roleteleport_databaseteleport_userteleport_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.tfapp_1_update.tfclassifier_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>.goprovider/internal/resources/<resource>.goschema 与 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-*.yamlmake 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 rmterraform 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 状态解析与填充ImportStateIdentifier.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-tfschemamake 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.

项目地址:https://gitcode.com/gh_mirrors/tel/teleport
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/21 2:18:45

AI率过高怎么办?从检测原理到手改技巧的完整降AI率指南

我前段时间帮一个做自媒体的朋友改稿子&#xff0c;他拿着一份检测报告跑过来&#xff0c;满脸困惑地问&#xff1a;"这段明明是我亲手写的&#xff0c;怎么就标成60%的AI率了&#xff1f;"他说自己从没用AI写过这篇内容&#xff0c;只是习惯性地把句子写得很规矩&am…

作者头像 李华
网站建设 2026/9/21 2:17:40

IEC 60068-2-14温度变化试验全解:标准解读与实操指南

简介&#xff1a;本资源为IEC 60068-2-14:2023国际标准官方PDF文档&#xff0c;主题为环境试验中的温度变化试验&#xff08;Test N: Change of temperature&#xff09;。标准面向电子产品与设备制造商、第三方测试机构及研发人员&#xff0c;用于规范产品在快速或缓慢温度变化…

作者头像 李华
网站建设 2026/9/21 2:09:59

大数据中心建设方案:从需求分析到落地实战的完整拆解

简介&#xff1a;这份大数据中心建设方案PPT共46页&#xff0c;面向政务、城市治理与行业数据平台规划人员&#xff0c;围绕大数据供给侧改革、数据业务智慧三大中台及数字孪生能力展开&#xff0c;可支撑县级政务大数据资源中心、城市数据运营与事件管理等场景的汇报与落地。资…

作者头像 李华