规范 Terraform AWS Provider 中 id 属性的使用:从"强制必备"到"按需省略"
【免费下载链接】terraform-provider-awsThe AWS Provider enables Terraform to manage AWS resources.项目地址: https://gitcode.com/GitHub_Trending/te/terraform-provider-aws
本篇基于 AWS Provider 的设计决策文档《Standardize Use of theidAttribute》展开。随着 Terraform Plugin Framework 的正式可用与terraform-plugin-testing测试库的独立演进,资源不再被强制要求携带id属性。文章将完整讲解这一 RFC 的背景、决策内容与落地方式,并结合仓库中aws_vpc_endpoint_private_dns、aws_lambda_runtime_management_config两个原型资源的源码实现,演示无id资源的 Import 方法编写与验收测试写法,帮助 Provider 开发者掌握"冗余即省略"的标准及其工程实现细节。
背景:id 属性为何曾是"隐性强需求"
历史上,Terraform AWS Provider 中的所有资源都包含一个只读的id属性。这一约定源于 Terraform Plugin SDK V2(下文简称 SDKv2)及其配套验收测试库的设计:该库在 Terraform 0.11 及更早版本时期开发,其内部核心实现要求资源状态中必须存在id字段,这一要求以"隐式约定"的形式传递给了所有 Provider 开发者。
这一约定体现在三个典型 API 形态上:
- 设置
id使用专门的d.SetId("value"),而不是普通的d.Set("field", "value"); - 在 Read 操作中检测到资源已在 Terraform 之外被删除时,用特殊语法
d.SetId("")将对象从 state 中移除; - 验收测试库中针对
import的辅助方法依赖id属性已有值,才能完成 import 命令并校验结果。
而在新栈上,这些约束已被移除:
- Terraform Plugin Framework 在 Read 时把资源从 state 中移除的对应方法是
State.RemoveResource,其 API 中完全不涉及 ID 概念; terraform-plugin-testing的TestStep结构体新增了支持"无id属性资源"的导入测试字段(包括ImportStateVerifyIdentifierAttribute、ImportStateID、ImportStateIdFunc等);- 从 Terraform 核心视角看,0.12 之后的版本不再把
id当作特殊属性;而当前受支持的 Provider SDK 只讲 protocol version 5 和 6,与 Terraform 0.12+ 绑定,因此主流 HashiCorp 系 Provider(包括hashicorp/aws)天然只支持 Terraform 0.12 及以上版本,移除id不存在向后兼容风险。
从源码结构看,AWS Provider 当前已通过 mux(多路复用)方式混合运行 Plugin Framework 与 SDKv2 资源,并已整体迁移到独立的terraform-plugin-testing库,这为下述标准提供了落地前提。
id 在 AWS Provider 中的现状
在 AWS Provider 中,id最常见的形态是对应 AWS 在创建资源时生成的唯一标识符。Create API 通常将该值返回在名为Id的字段中,或带资源名前缀的Id(例如InstanceId)。当资源可以这样一个远程生成的唯一标识符被引用时,把它存入一个 computed 的id属性是顺理成章的选择,用户也容易理解其含义。
但存在另一类情况:AWS 并不在创建时生成唯一标识符,而是把用户提交给 Create API 的一个或多个参数本身作为标识符。
- 单值标识:通常是
name这类字段; - "关系型"资源(在两个资源之间建立关联的资源,例如 IAM 策略附加):唯一标识可能是一个由两侧资源标识符拼成的"多段键(multi-part key)"。历史上,这类多段键的分隔符并不统一,进一步恶化了用户的使用体验。
在这些场景中,AWS Provider 的做法是把标识值(或多段键用某个分隔符拼接后的字符串)复制到id属性。这给使用者留下一个歧义:当这个导出的值要在配置的其他地方被引用时,究竟该用哪个属性——是id,还是原始参数本身?
提案:冗余即省略,多段键统一用逗号分隔
RFC 给出的核心标准是:
今后,所有新增(net-new)资源,若其
id属性会与某个已存在的参数(argument)重复,则一律省略该属性。其他场景下,id属性继续按历史惯例使用。当省略id且唯一标识符是多个参数的组合时,这些参数必须使用逗号(,)分隔,并在 Import 方法中依赖内部的ExpandResourceId函数来切分各段值。
逗号分隔与 ExpandResourceId 工具函数
这一标准依托的底层工具位于 internal/flex/flex.go。该文件定义了统一的分隔符常量与切分函数:
// internal/flex/flex.go ResourceIdSeparator = "," // L26 // 接收以 ResourceIdSeparator 分隔的资源属性字符串、预期的 Id 段数、 // 是否允许空段;返回用于构造唯一 Id 的属性字符串列表, // 若 id 无法正确解析则返回错误信息 func ExpandResourceId(id string, partCount int, allowEmptyPart bool) ([]string, error) { idParts := strings.Split(id, ResourceIdSeparator) if len(idParts) <= 1 { return nil, smarterr.Errorf("unexpected format for ID (%v), expected more than one part", idParts) } if len(idParts) != partCount { return nil, smarterr.Errorf("unexpected format for ID (%s), expected (%d) parts separated by (%s)", id, partCount, ResourceIdSeparator) } if !allowEmptyPart { // 逐段检查空值,若不允许空段则报告具体的空段下标 ... } return idParts, nil }(参见 internal/flex/flex.go#L255-L281)
从实现可以看到ExpandResourceId的三重校验:段数超过 1、段数与预期partCount精确相等、(在allowEmptyPart为 false 时)逐段拒绝空字符串并精确报告空段下标。与之配对的FlattenResourceId则负责把多段属性拼接回逗号分隔的字符串,二者构成"多段键"资源导入/构造 ID 的标准工具对。
说明:原 RFC 引用的是 v5.51.1 版本中的
ExpandResourceID(camelCase 写法略有差异),当前主干代码中该函数名为ExpandResourceId。
权衡与共识
省略冗余id带来的直接收益是:去掉重复的属性值后,"哪个属性才是标识符"不再有歧义,对多段键这类复杂标识符的资源收益尤其明显。
为这一决策付出的代价有两点:
- 偏离历史惯例——过去所有资源都有
id属性,部分使用者可能已形成依赖该属性的心智模型; plan/apply输出上的细微 UI 差异——没有id属性的资源,其日志行会缺少这段额外的标识信息。
团队讨论后的结论是:只保留一个承载标识符值的属性所带来的清晰度,超过了打破历史惯例的代价。
需要指出,该标准依赖两条既有策略:其一,AWS Provider 已规定所有新增资源必须使用 Terraform Plugin Framework 实现(存量 SDKv2 资源不受影响);其二,Provider 已迁移到独立的terraform-plugin-testing库。
无 id 资源的 Import 方法与验收测试
对于省略id的资源,Import 方法和导入验收测试需要少量定制。就测试而言,import 校验用的TestStep现在需要配置ImportStateVerifyIdentifierAttribute,以及ImportStateID或ImportStateIdFunc之一。下面分别给出单值标识与多段键两类示例。
单值标识:aws_vpc_endpoint_private_dns
aws_vpc_endpoint_private_dns是唯一标识为单值vpc_endpoint_id的原型资源。其 ImportState 方法直接把请求 ID 透传到该属性:
// internal/service/ec2/vpc_endpoint_private_dns.go func (r *resourceEndpointPrivateDNS) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { resource.ImportStatePassthroughID(ctx, path.Root("vpc_endpoint_id"), req, resp) }对应仓库实现见 internal/service/ec2/vpc_endpoint_private_dns.go#L131-L133。值得注意的是,该资源的 Schema 中根本不存在id属性——只有必填的vpc_endpoint_id(带RequiresReplace计划修改器)与private_dns_enabled,且嵌入了framework.WithNoOpDelete,因为"删除"该资源本质上只是把端点的私有 DNS 设置回退,并无真正的远端删除动作。
对应的TestStep:
{ ResourceName: resourceName, ImportState: true, ImportStateIdFunc: testAccVPCEndpointPrivateDNSImportStateIdFunc(resourceName), ImportStateVerify: true, ImportStateVerifyIdentifierAttribute: "vpc_endpoint_id", },ImportStateIdFunc负责从既有 state 中读出真正的标识属性值,构造 import 命令使用的 ID:
func testAccVPCEndpointPrivateDNSImportStateIdFunc(resourceName string) resource.ImportStateIdFunc { return func(s *terraform.State) (string, error) { rs, ok := s.RootModule().Resources[resourceName] if !ok { return "", fmt.Errorf("Not found: %s", resourceName) } return rs.Primary.Attributes["vpc_endpoint_id"], nil } }在实际测试 internal/service/ec2/vpc_endpoint_private_dns_test.go 中,单值场景采用了仓库提供的现成辅助函数,避免手写闭包:
// internal/service/ec2/vpc_endpoint_private_dns_test.go(TestStep 摘录) { ResourceName: resourceName, ImportState: true, ImportStateIdFunc: acctest.AttrImportStateIdFunc(resourceName, names.AttrVPCEndpointID), ImportStateVerify: true, ImportStateVerifyIdentifierAttribute: names.AttrVPCEndpointID, },该辅助函数定义在 internal/acctest/state_id.go:AttrImportStateIdFunc直接返回指定属性的值,AttrsImportStateIdFunc则把多个属性按给定分隔符拼接后返回——后者正是多段键场景的标准取 ID 方式。
多段键:aws_lambda_runtime_management_config
aws_lambda_runtime_management_config的标识是"必选参数function_name+ 可选参数qualifier"组成的两段键。其 ImportState 方法按 RFC 规定用ExpandResourceId切分逗号分隔的导入 ID(第三参true表示允许空段,因为qualifier可缺省):
// internal/service/lambda/runtime_management_config.go func (r *resourceRuntimeManagementConfig) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { parts, err := intflex.ExpandResourceId(req.ID, runtimeManagementConfigIDParts, true) if err != nil { resp.Diagnostics.AddError( "Unexpected Import Identifier", fmt.Sprintf("Expected import identifier with format: function_name,qualifier. Got: %q", req.ID), ) return } resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("function_name"), parts[0])...) resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("qualifier"), parts[1])...) }仓库中的对应实现见 internal/service/lambda/runtime_management_config.go#L201-L213,其中段数常量声明为runtimeManagementConfigIDParts = 2(internal/service/lambda/runtime_management_config.go#L38-L41)。
对应的TestStep:
{ ResourceName: resourceName, ImportState: true, ImportStateIdFunc: testAccRuntimeManagementConfigImportStateIdFunc(resourceName), ImportStateVerify: true, ImportStateVerifyIdentifierAttribute: "function_name", },ImportStateIdFunc把两个属性用逗号拼回导入 ID——分隔符与ExpandResourceId使用的ResourceIdSeparator严格一致,保证"导入时怎么拼、解析时就怎么切":
func testAccRuntimeManagementConfigImportStateIdFunc(resourceName string) resource.ImportStateIdFunc { return func(s *terraform.State) (string, error) { rs, ok := s.RootModule().Resources[resourceName] if !ok { return "", fmt.Errorf("Not found: %s", resourceName) } return fmt.Sprintf("%s,%s", rs.Primary.Attributes["function_name"], rs.Primary.Attributes["qualifier"]), nil } }实际测试文件 internal/service/lambda/runtime_management_config_test.go#L53-L62 还展示了一个工程细节:由于qualifier是可选参数,导入后 state 中该字段与导入前可能不一致(导入 ID 中空段被规范化),因此测试显式配置了ImportStateVerifyIgnore: []string{"qualifier"}来排除该字段的比对——这是多段键 + 可选段场景下容易踩坑的一处,值得开发者留意。
两个原型资源的小结
| 维度 | aws_vpc_endpoint_private_dns | aws_lambda_runtime_management_config |
|---|---|---|
| 标识形态 | 单值:vpc_endpoint_id | 两段键:function_name+ 可选qualifier |
| ImportState 实现 | ImportStatePassthroughID透传到标识属性 | intflex.ExpandResourceId按,切分后逐段写回属性 |
| 测试取 ID | acctest.AttrImportStateIdFunc(现成辅助) | 自定义闭包,用fmt.Sprintf("%s,%s", ...)拼接 |
| 关键 TestStep 字段 | ImportStateVerifyIdentifierAttribute: "vpc_endpoint_id" | 同左,指向function_name,并附加ImportStateVerifyIgnore |
| 源码位置 | internal/service/ec2/vpc_endpoint_private_dns.go | internal/service/lambda/runtime_management_config.go |
被放弃的方案:要求所有资源继续保留 id
与"省略冗余id"的替代方案是:延续历史设计,要求所有资源都包含id属性。该方案的好处是与 Provider 的历史形态保持一致;但重复属性值的歧义、以及复杂(且历史上分隔符不统一)的多段键问题依然原样存在。团队共识认为,在这一点上清晰度的收益超过了打破惯例的代价,故该方案被放弃。
结论
这一设计决策为 Terraform AWS Provider 确立了一条清晰的边界:标识符只在一处出现。当 AWS 在创建时生成了唯一 ID,id属性继续作为它的载体;当标识本身就是用户提交的参数(单值或多段键),新增资源不再复制一份到id,多段键统一以逗号分隔并借助 internal/flex/flex.go 中的ExpandResourceId/FlattenResourceId完成解析与构造。配套的验收测试通过ImportStateVerifyIdentifierAttribute与ImportStateIdFunc(或acctest包中的AttrImportStateIdFunc/AttrsImportStateIdFunc辅助)完成无id场景下的导入校验。对于正在为 Provider 编写新资源的开发者而言,这套标准连同aws_vpc_endpoint_private_dns、aws_lambda_runtime_management_config两个可直接对照的原型实现,就是一份可复制的完整范本。
【免费下载链接】terraform-provider-awsThe AWS Provider enables Terraform to manage AWS resources.项目地址: https://gitcode.com/GitHub_Trending/te/terraform-provider-aws
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考