Skip to content

Guide: Resource Identity#

This guide covers adding Resource Identity to a new or existing resource. For more information on Resource Identity, see Resources - Identity.

[!IMPORTANT] Resource Identity is mandatory for all new resources. It is also a prerequisite for List Resources, which are equally required. If your resource cannot support Resource Identity (see caveats below), please explain why in the PR description.

The provider's Resource Identity generator does not yet support all identity types. commonids.CompositeResourceID and any custom resource IDs (i.e. not one provided by commonids or go-azure-sdk/resource-manager) are not supported.

Adding Resource Identity#

Typed Resources#

To add Resource Identity to a typed resource, we will need to implement the sdk.ResourceWithIdentity interface and modify the Read() function.

  1. Define a variable of type sdk.ResourceWithIdentity and assign it a value of the resource type struct.

    package example
    
    import "github.com/hashicorp/terraform-provider-azurerm/internal/sdk"
    
    type ExampleResource struct{}
    
    var _ sdk.ResourceWithIdentity = ExampleResource{}
    
  2. Add the Identity() method, this method should return a pointer to the correct resource ID, if you are unsure, you can reference the IDValidationFunc method, the ID that is being validated here is the one you'll want to use.

    package example
    
    import "github.com/hashicorp/terraform-provider-azurerm/internal/sdk"
    import "github.com/hashicorp/go-azure-helpers/resourceids"
    
    type ExampleResource struct{}
    
    var _ sdk.ResourceWithIdentity = ExampleResource{}
    
    func (r ExampleResource) Identity() resourceids.ResourceId {
        return &examplepackage.ExampleResourceId{}
    }
    
  3. Update the Create() function to include a step setting the Resource Identity data into state, this should be done right after we set the id attribute. Resource Identity data does not have to be set manually, we can make use of the pluginsdk.SetResourceIdentityData helper function.

    func (r ExampleResource) Create() sdk.ResourceFunc {
        return sdk.ResourceFunc{
            Timeout: 30 * time.Minute,
            Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
                client := metadata.Client.Service.ExampleClient
    
                id := examplepackage.NewExampleResourceID(metadata.Client.Account.SubscriptionId, model.ResourceGroupName, model.Name)
    
                ...
    
                // If the resource uses a `CallbackThenPoll` method, ensure the callback function is updated to `SetIDAndIdentityCallBack`.
                if err := client.CreateOrUpdateCallbackThenPoll(ctx, id, param, metadata.SetIDAndIdentityCallback(&id)); err != nil {
                    return fmt.Errorf("creating %s: %+v", &id, err)
                }
    
                metadata.SetID(id)
                return pluginsdk.SetResourceIdentityData(metadata.ResourceData, id)
            },
        }
    }
    

Note: While this may seem redundant given Read() gets called after Create(), this is done to prevent Missing Resource Identity After Create errors, in the event something errors after setting the id attribute.

  1. Update the Read() function to include a step setting the Resource Identity data into state.

    func (r ExampleResource) Read() sdk.ResourceFunc {
        return sdk.ResourceFunc{
            Timeout: 5 * time.Minute,
            Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
                client := metadata.Client.Service.ExampleClient
                id, err := examplepackage.ParseExampleResourceID(metadata.ResourceData.Id())
                if err != nil {
                    return err
                }
    
                ...
    
                if err := pluginsdk.SetResourceIdentityData(metadata.ResourceData, id); err != nil {
                    return err
                }
    
                return metadata.Encode(&model)
            },
        }
    }
    
  2. Add an acceptance test to ensure the identity data is accurately set into state, please reference Resource Identity Tests.

Untyped Resources#

To add Resource Identity to an untyped resource, follow the steps below.

  1. Add the Identity schema. Here, we make use of the pluginsdk.GenerateIdentitySchema function, which takes in a pointer to a resourceids.ResourceId. The ID provided here should be the same as the ID that is being parsed in the Importer field.

    package example
    
    import (
        "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk"
    )
    
    func resourceExample() *pluginsdk.Resource {
        return &pluginsdk.Resource{
            Create: resourceExampleCreate,
            Read: resourceExampleRead,
            Update: resourceExampleUpdate,
            Delete: resourceExampleDelete,
    
            Importer: pluginsdk.ImporterValidatingResourceId(func(id string) error {
                _, err := examplepackage.ParseExampleID(id)
                return err
            }),
    
            // We will be including the new `Identity` field
            Identity: &schema.ResourceIdentity{
                SchemaFunc: pluginsdk.GenerateIdentitySchema(&examplepackage.ExampleId{}),
            },
    
            ...
        }
    }
    
  2. Update the Importer field, we'll want to use the pluginsdk.ImporterValidatingIdentity function and provide it with the same resource ID as the pluginsdk.GenerateIdentitySchema function.

        package example
    
        import (
            "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk"
        )
    
        func resourceExample() *pluginsdk.Resource {
            return &pluginsdk.Resource{
                Create: resourceExampleCreate,
                Read: resourceExampleRead,
                Update: resourceExampleUpdate,
                Delete: resourceExampleDelete,
    
                Importer: pluginsdk.ImporterValidatingIdentity(&examplepackage.ExampleId{}),
    
                // We will be including the new `Identity` field
                Identity: &schema.ResourceIdentity{
                    SchemaFunc: pluginsdk.GenerateIdentitySchema(&examplepackage.ExampleId{}),
                },
    
                ...
            }
        }
    
    3. Update the resourceExampleCreate() function to include a step setting the Resource Identity data into state, this should be done right after we set the id attribute. Resource Identity data does not have to be set manually, we can make use of the pluginsdk.SetResourceIdentityData helper function.

    func resourceExampleCreate(d *pluginsdk.ResourceData, meta interface{}) error {
        Timeout: 30 * time.Minute,
        Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
            client := meta.(*clients.Client).Compute.DedicatedHostsClient
            ctx, cancel := timeouts.ForCreate(meta.(*clients.Client).StopContext, d)
            defer cancel()
    
            id := examplepackage.NewExampleResourceID(metadata.Client.Account.SubscriptionId, model.ResourceGroupName, model.Name)
    
            ...
    
            // If the resource uses a `CallbackThenPoll` method, ensure the callback function is updated to `SetIDAndIdentityCallBack`.
            if err := client.CreateOrUpdateCallbackThenPoll(ctx, id, param, sdk.SetIDAndIdentityCallback(meta, &id, d)); err != nil {
               return fmt.Errorf("creating %s: %+v", &id, err)
            }
    
            d.SetId(id.ID())
            if err := pluginsdk.SetResourceIdentityData(d, &id); err != nil {
                return err
            }
    
            return resourceExampleRead(d, meta)
        }
    }
    

Note: While this may seem redundant given resourceExampleRead() gets called after resourceExampleCreate(), this is done to prevent Missing Resource Identity After Create errors, in the event a function call errors after setting the id attribute.

  1. Update the resourceExampleRead function to include a step setting the Resource Identity data into state. Resource Identity data does not have to be set manually, we can make use of the pluginsdk.SetResourceIdentityData helper function.

        func resourceExampleRead(d *pluginsdk.ResourceData, meta interface{}) error {
            client := meta.(*clients.Client).Service.ExampleClient
            ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d)
            defer cancel()
    
            id, err := examplepackage.ParseExampleResourceID(d.Id())
            if err != nil {
                return err
            }
    
            ...
    
            // Usually we can simply replace the final `return nil` line with the return below.
            return pluginsdk.SetResourceIdentityData(d, id)
        }
    
  2. Add an acceptance test to ensure the identity data is accurately set into state, please reference Resource Identity Tests.

Resource Identity Tests#

Just like the schema, Resource Identity tests are entirely generated. This is done by adding a go:generate comment. Both untyped and typed resources use the same format. To make this easy to find and modify, place it underneath the imports.

The schema is generated for us by taking different parts of the ID and converting them to snake_case. By default, if the last segment ends in Name, it will not be converted to snake case in the schema but rather set to name.

For the vast majority of resources, the generator-tests tool uses Abstract Syntax Tree (AST) inference to automatically inspect the Go file, locate the ID struct, and infer the correct property mappings. This means you can simply provide the base command with zero flags:

//go:generate go run ../../tools/generator-tests resourceidentity

If your resource uses a Virtual Identity (a sub-resource that inherits its ID entirely from a parent resource), you will need to specify the -parent-id flag with the name of the parent ID property in the schema (e.g. -parent-id workspace_id). The tool will automatically expand this to map all ID fields to that parent ID.

//go:generate go run ../../tools/generator-tests resourceidentity -parent-id parent_resource_id

Note: If a resource ID doesn't include the subscription_id segment, omit it from the tests by using -no-subscription-id.

How the Generator Works (AST Inference)#

The generator reduces boilerplate by using Abstract Syntax Tree (AST) inference to automatically map the properties of an ID struct to the resource schema. - Typed SDK Wrappers: It scans the .go file for the Identity() method to locate the identity struct, and the IdentityType() method to determine if it is a Virtual Identity. - Legacy (Untyped) Resources: It scans for the pluginsdk.GenerateIdentitySchema(&struct{}, ...) function call in the schema definition, extracting the identity struct from the first argument and checking if the second argument is pluginsdk.ResourceTypeForIdentityVirtual (or scanning for the older VirtualIdentity() method). - By parsing the returned commonids or resourceids struct from either pattern, it inherently knows the fields required (e.g., SubscriptionId, ResourceGroupName, StorageAccountName). - It converts these properties to snake_case (e.g., resource_group_name). - By convention, the final identifier segment (e.g., StorageAccountName) is converted to name unless the resource is identified as a Virtual Identity.

Self-Correcting Generator Tags#

The generator-tests tool has "self-correcting" capabilities. When you run make generate, the tool will automatically clean up your //go:generate tag if it contains redundant flags:

What the self-correcting logic CAN do: - Flag Stripping: If you provide explicit -compare-values, -known-values, or -resource-name flags that perfectly match what the AST infers, the generator will automatically strip those flags from your .go file to keep the tag clean and concise (Zero-Flags). - Auto-Formatting: When the generator rewrites the tag, it automatically runs gofumpt on the file so that no whitespace formatting issues are introduced. - Virtual Identity Resolution: If a resource is detected as Virtual (via IdentityType() or VirtualIdentity()) and you supply -parent-id "xyz", the generator automatically expands the mapping for all the struct's fields (including subscription_id) to that -parent-id.

What the self-correcting logic CANNOT do: - Guess a Virtual Identity's Parent ID without an anchor: While the AST detects that a resource is Virtual, it cannot magically guess which property in the schema acts as the parent ID. Therefore, you must supply the -parent-id "xyz" flag explicitly for Virtual Identities. (If the resource uses the legacy explicit -compare-values for every field pointing to the exact same parent ID, the generator can infer and upgrade it to -parent-id, but for new resources, -parent-id is required). - Remove non-standard configurations: If you explicitly provide -properties or -compare-values mappings that deviate intentionally from the standard struct field names, the generator will leave those flags intact. - Resolve arbitrary types: The AST inference relies on locating the standard identity structs in the commonids or resourceids packages, or inside the provider namespace. If the identity struct is deeply aliased or nested in a way it cannot trace, it will fall back and require explicit flags.

There are edge cases where the AST inference cannot automatically map the properties. In these rare cases, you can fallback to explicitly defining them using -properties and -compare-values. All fields in the ID struct must be mapped to one of these options if explicit mapping is used.

  • -properties: This flag specifies the 1:1 relationship between the Resource Schema and the Resource Identity Schema fields (i.e name, resource_group_name, etc), this would be specified as name,resource_group_name. If the schema property name does not match the Resource Identity schema name these should be mapped accordingly. This would be specified as {id_field_name}:{schema_field_name}, e.g. api_management_id:api_management_name.

  • -compare-values: This flag allows for comparing values that are exposed in the resource schema through another resource ID. This comes up when we use a parent resource ID in the schema but the Resource Identity Schema uses the individual parts of that parent ID. This would be specified as {id_field_name}:{schema_field_id_name}, e.g. subscription_id:virtual_network_id,virtual_network_name:virtual_network_id.

Please reference the Resource Identity Test Generator for additional options that are used less frequently.

```go package example

import ( "time"

"github.com/hashicorp/terraform-provider-azurerm/internal/sdk"

)

// A basic example where the AST parser infers everything (Zero-Flags) //go:generate go run ../../tools/generator-tests resourceidentity

// An example where the resource is a sub-resource utilizing a Virtual Identity //go:generate go run ../../tools/generator-tests resourceidentity -parent-id parent_resource_id

// An example of a resource ID that doesn't contain the subscription_id segment, e.g. management groups //go:generate go run ../../tools/generator-tests resourceidentity -no-subscription-id

type ExampleResource struct{}

var _ sdk.ResourceWithIdentity = ExampleResource{}

func (r ExampleResource) Identity() resourceids.ResourceId { return &examplepackage.ExampleResourceId{} }

func (r ExampleResource) Read() sdk.ResourceFunc { return sdk.ResourceFunc{ Timeout: 5 * time.Minute, Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { client := metadata.Client.Service.ExampleClient id, err := examplepackage.ParseExampleResourceID(metadata.ResourceData.Id()) if err != nil { return err }

        ...

        if err := pluginsdk.SetResourceIdentityData(metadata.ResourceData, id); err != nil {
            return err
        }

        return metadata.Encode(&model)
    },
}

} ```