Standardize Use of the id
Attribute#
Summary: Define a standard for use of the id
attribute given improvements to provider development and testing libraries have removed its requirement.
Created: 2024-05-24
Author: @jar-b
Historically all resources in the Terraform AWS provider have included a read-only id
attribute, as Terraform Plugin SDK V2 and its associated acceptance testing library requires it. In most cases, this attribute corresponds to a unique identifier generated by AWS during resource creation. However, for some resources the identifier is a value provided by the user and the resulting id
attribute inherently duplicates the value of some other required argument.
With the general availability of Terraform Plugin Framework, the separation of the provider testing functionality into its own standalone library (terraform-plugin-testing
), and some corresponding enhancements made to this library, resources are no longer required to have an id
attribute. This leaves an opportunity for provider developers to define a standard for when to use an id
attribute. Broadly, the two options are:
- Continue to require an
id
attribute in all resources for consistency. - Omit the
id
attribute when it is redundant with some other required argument(s).
This RFC will propose a standard for the Terraform AWS provider. A secondary goal is to gain feedback from other HashiCorp-owned provider teams to determine if a common standard can be shared across the provider ecosystem.
Background#
Prior to general availability of the Terraform Plugin Framework, all providers were built with the Terraform Plugin SDK. As the Terraform Plugin SDK was developed with pre-v1.0 versions of Terraform, an internal core implementation detail requiring the presence of an id
attribute was incorporated into the library design. This requirement was accordingly passed along to provider developers in the form of an implicit requirement that all resources have an id
attribute.
This convention is most noticeable in the method for setting id
attributes (d.SetId(“value”)
, versus d.Set(“field”, “value”)
), the special syntax for removing objects from state during a read operation when deleted outside of Terraform (d.SetId(“”)
), and the acceptance testing helpers for import
, which rely on a populated id
attribute to complete the import command and verify the results.
The corresponding method for removing Terraform Plugin Framework-based resources from state during read (State.RemoveResource
) includes no mention of an ID. In version 1.5.0
of terraform-plugin-testing
, additional fields were added to the TestStep
struct to enable testing imports for resources that do not implement an id
attribute. With these advancements, providers using Terraform Plugin Framework, either exclusively or via a muxed provider configuration, and terraform-plugin-testing
in place of the legacy testing packages embedded in Terraform Plugin SDK V2 are no longer bound to the requirement of including an id
attribute in every resource.
From a Terraform core perspective, releases after 0.12 no longer treat id
as a special attribute. Terraform 0.11 and earlier implicitly required it. Due to the linkage between Terraform 0.12 and protocol version 5, and the fact that supported provider SDKs today only speak protocol version 5 (and 6), the primary Hashicorp-owned providers, like hashicorp/aws
, inherently only support Terraform 0.12 and later. Therefore it is safe to remove the id
attribute since there is no concern for a Terraform version that requires it.
id
Attributes in the AWS Provider#
In the AWS provider, the id
attribute most commonly corresponds to an identifier generated by AWS during creation of the resource. The Create API typically returns this value as a field named Id
or, in some cases, Id
with a prefixed with a resource name (eg. InstanceId
). When a resource can be referenced by a unique, remotely generated identifier, storing this value in a computed id
is a straightforward choice and the meaning of the attribute is easily understood by the practitioner.
However, there are cases where AWS does not generate a unique identifier during resource creation, and instead one or more of the arguments supplied to the Create API functions as the identifier. For single value identifiers this is commonly a field like name
. For “relationship” resources (ie. resources creating a link between two other resources, such as an IAM policy attachment), the unique identifier may be a “multi-part key” made up of identifiers from both resources being linked. Historically, multi-part keys have not used a consistent delimiter, further complicating the user experience for practitioners.
In these situations, the AWS provider copied the identifying value (or a delimited string of values for multi-part keys) to the id
attribute. This approach leaves ambiguity for practitioners regarding which attribute should be used if the exported value is referenced somewhere else in the configuration.
Proposal#
Going forward, all net-new resources for which an id
attribute would be redundant with an existing argument(s) will omit this attribute. In all other cases the id
attribute should continue to be used as it has been historically. When an id
is omitted and the unique identifier is a combination of arguments, these should always be delimited with a comma (,
), and rely on the internal ExpandResourceID
function to handle splitting values within the import method.
The added clarity of removing redundant attribute values will directly benefit practitioners, especially for resources with complex multi-part keys.
The trade-offs being made with this decision are:
- A departure from a historical precedent where all resources include an
id
attribute, which some practitioners may have come to expect and rely on. - A minor UI difference in how resources without an ID are rendered during
plan
andapply
operations. Specifically, when noid
attribute is present the logged line for this resource will omit this extra piece of information.
Team discussion around this topic consistently landed on a consensus that the clarity gained by having only a single attribute containing the identifier value outweighs the departure from historical conventions.
It should also be noted that this standard is enabled by an existing policy requiring all net-new resources be implemented with Terraform Plugin Framework, and that the AWS provider has already migrated to the standalone terraform-plugin-testing
library.
Import Methods and Testing#
For resources omitting an id
argument, minor changes are required to customize the import method and acceptance test the import operation. For testing specifically, the import verification TestStep
will now require the ImportStateVerifyIdentifierAttribute
and one of ImportStateID
or ImportStateIdFunc
be configured. Examples for single non-id identifier and a multi-part key are included below.
Single Value#
ImportState
method:
func (r *resourceEndpointPrivateDNS) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("vpc_endpoint_id"), req, resp)
}
TestStep
:
{
ResourceName: resourceName,
ImportState: true,
ImportStateIdFunc: testAccVPCEndpointPrivateDNSImportStateIdFunc(resourceName),
ImportStateVerify: true,
ImportStateVerifyIdentifierAttribute: "vpc_endpoint_id",
},
ImportStateIdFunc
:
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
}
}
Multi-part Value#
ImportState
Method:
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])...)
}
TestStep
:
{
ResourceName: resourceName,
ImportState: true,
ImportStateIdFunc: testAccRuntimeManagementConfigImportStateIdFunc(resourceName),
ImportStateVerify: true,
ImportStateVerifyIdentifierAttribute: "function_name",
},
ImportStateIdFunc
:
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
}
}
Abandoned Ideas#
The alternative to omitting id
attributes when possible is to continue requiring all resources to include an id
attribute. While this approach provides consistency with the historical design of the provider, the drawbacks of redundant attribute values and complex (and often inconsistently delimited) multi-part identifiers remain. Consensus among the team has been that the benefits of clarity outweigh the departure from precedent in this case.
References#
Terraform Plugin Framework
Terraform Plugin Testing
- https://github.com/hashicorp/terraform-plugin-testing/issues/84
- https://github.com/hashicorp/terraform-plugin-testing/pull/164
Terraform AWS Provider
- Prototype resources implemented without an
id
attributeaws_vpc_endpoint_private_dns
- The identifier is a single value,
vpc_endpoint_id
- PR
- Registry documentation
- The identifier is a single value,
aws_lambda_runtime_management_config
- The identifier is a multi-part key, made up of the required
function_name
and optionalqualifier
arguments - PR
- Registry documentation
- The identifier is a multi-part key, made up of the required