Why Importing Matters in Infrastructure-as-Code
When adopting Terraform for existing cloud environments, you'll inevitably face a critical challenge: How to bring existing infrastructure under Terraform management? This is where resource importing becomes essential. Without proper imports, Terraform attempts to recreate resources that already exist, leading to errors and potential configuration conflicts.
The New Import Block: Introduced in Terraform 1.5+
Terraform 1.5 brought a game-changing feature to the table with its new import block, revolutionizing how we handle existing resources. This declarative approach lets you seamlessly import resources into your state file, ditching the need for complex CLI commands. Here's how it supercharges your workflow.
I will start with below basic terraform provider block of azurerm.
terraform { required_providers { azurerm = { source = "hashicorp/azurerm" version = ">=4.31.0" } } } provider "azurerm" { features {} }
Just to verify I am using correct version I checked it first and next initialized provider.
Check my earlier email on same topic:
Importing existing resources into Terraform - Step by Step
Importing already created module Infrastructure into Terraform and update state file
Let's start with a simple example. In my Azure portal, manually I've already created a resource group named 'test' and added a single resource - a Storage Account. In the next step, I'll demonstrate how to import these resources into a Terraform configuration file using the new import block, eliminating the need for complex terraform import command-line interactions.
If you try to create a resource using Terraform that already exists in the Azure portal, the plan phase will complete successfully. However, when you attempt to apply the configuration, you'll encounter an error. This is because Terraform needs to import the existing resources into its state file before managing them.
# Added new configuration in tf file resource "azurerm_resource_group" "rg" { name = "test" location = "East US" } -------------------------------------------------------------- Error: A resource with the ID "subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/test" already exists - to be managed via Terraform this resource needs to be imported into the State. Please see the resource documenations for "azurerm_resource_group" for more information terraform apply --auto-approve Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: # azurerm_resource_group.rg will be created + resource "azurerm_resource_group" "rg" { + id = (known after apply) + location = "eastus" + name = "test" } Plan: 1 to add, 0 to change, 0 to destroy. azurerm_resource_group.rg: Creating... ╷ │ Error: A resource with the ID "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/test" already exists - to be managed via Terraform this resource needs to be imported into the State. Please see the resource documentation for "azurerm_resource_group" for more information. │ │ with azurerm_resource_group.rg, │ on main.tf line 14, in resource "azurerm_resource_group" "rg": │ 14: resource "azurerm_resource_group" "rg" {
To resolve this issue, you'll need to import the existing resources into your Terraform configuration using an import block. The import block requires two arguments: id and to.
To obtain the resource ID, you can use either the Azure CLI command: az resource list --resource-group <resourcegroupname> --query "[].id" --out tsv or the powershell command Get-AzResource -ResourceGroupName <resourcegroupname> | Select-Object -ExpandProperty ResourceId.
In the to parameter, specify the corresponding resource name, which will be used as the resource block name in your Terraform configuration.
import { id = "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/test" #ResourceID to = azurerm_resource_group.rg } import { id = "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/test/providers/Microsoft.Storage/storageAccounts/vcloudlabcom" #ResourceID to = azurerm_storage_account.sa } ------------------------------------------------------------------------------------------ $rg = 'test' az resource list --resource-group $rg --query "[].id" --out tsv Get-AzResource -ResourceGroupName $rg | Select-Object -ExpandProperty ResourceId
In the screenshot below, I'm verifying my Terraform configuration files before running terraform plan. Currently, I have a single configuration file, main.tf, which contains:
A provider block for Azure Resource Manager (azurerm)
- Two import blocks for the resource group and storage account
- To import and generate configurations for these existing resources, I'll run the command:
terraform plan --generate-config-out=existingresources.tf
This generates a new file, existingresources.tf, containing the configuration for the resource group and storage account. You can see the updated file list after running the ls or dir command.
Note that this configuration generation feature is currently experimental.
terraform plan --generate-config-out=existingresources.tf azurerm_resource_group.rg: Preparing import... [id=/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/test] azurerm_storage_account.sa: Preparing import... [id=/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/test/providers/Microsoft.Storage/storageAccounts/vcloudlabcom] azurerm_resource_group.rg: Refreshing state... [id=/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/test] azurerm_storage_account.sa: Refreshing state... [id=/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/test/providers/Microsoft.Storage/storageAccounts/vcloudlabcom] Terraform planned the following actions, but then encountered a problem: # azurerm_resource_group.rg will be imported # (config will be generated) resource "azurerm_resource_group" "rg" { id = "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/test" location = "eastus" managed_by = null name = "test" tags = {} } Plan: 1 to import, 0 to add, 0 to change, 0 to destroy. ╷ │ Warning: Config generation is experimental │ │ Generating configuration during import is currently experimental, and the generated configuration format may change in future versions. ╵ ╷ │ Error: expected blob_properties.0.change_feed_retention_in_days to be in the range (1 - 146000), got 0 │ │ with azurerm_storage_account.sa, │ on existingresources.tf line 29: │ (source code not available)
Although an error occurred during generation, I can resolve it by manually enabling blob change feed on the Storage Account. To do this, I'll configure the change feed logs to be deleted after a specified number of days, setting the retention period to a value between 1 and 146000 days.
Upon opening the newly generated existingresources.tf file, you'll find resource blocks for azurerm_resource_group and azurerm_storage_account. To tidy up the code, you can remove unnecessary empty or null parameters. I'll also remove non-mandatory arguments and parameters, as per Terraform documentation.
Currently, the values are hardcoded in the generated configuration. To improve flexibility, you can create variables for these values. As shown in the screenshot, I've already fixed the change_feed_retention_days value and highlighted the arguments that can be removed as they're not required or changed from the configuration as per requirement.
In this final step, I'll apply the new Terraform configuration, which will import and add the existing resources to the Terraform state file. With this, my Terraform configuration code is now production-ready and accurately reflects the current state of my infrastructure.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
terraform apply --auto-approve azurerm_resource_group.rg: Preparing import... [id=/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/test] azurerm_storage_account.sa: Preparing import... [id=/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/test/providers/Microsoft.Storage/storageAccounts/vcloudlabcom] azurerm_resource_group.rg: Refreshing state... [id=/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/test] azurerm_storage_account.sa: Refreshing state... [id=/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/test/providers/Microsoft.Storage/storageAccounts/vcloudlabcom] Terraform will perform the following actions: # azurerm_resource_group.rg will be imported resource "azurerm_resource_group" "rg" { id = "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/test" location = "eastus" managed_by = null name = "test" tags = {} } # azurerm_storage_account.sa will be imported resource "azurerm_storage_account" "sa" { access_tier = "Hot" account_kind = "StorageV2" account_replication_type = "LRS" account_tier = "Standard" allow_nested_items_to_be_public = false allowed_copy_scope = null cross_tenant_replication_enabled = false default_to_oauth_authentication = false dns_endpoint_type = "Standard" edge_zone = null https_traffic_only_enabled = true id = "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/test/providers/Microsoft.Storage/storageAccounts/vcloudlabcom" infrastructure_encryption_enabled = false is_hns_enabled = false large_file_share_enabled = true local_user_enabled = true location = "eastus" min_tls_version = "TLS1_2" name = "vcloudlabcom" nfsv3_enabled = false primary_access_key = (sensitive value) primary_blob_connection_string = (sensitive value) primary_blob_endpoint = "https://vcloudlabcom.blob.core.windows.net/" primary_blob_host = "vcloudlabcom.blob.core.windows.net" primary_blob_internet_endpoint = null primary_blob_internet_host = null primary_blob_microsoft_endpoint = null primary_blob_microsoft_host = null primary_connection_string = (sensitive value) primary_dfs_endpoint = "https://vcloudlabcom.dfs.core.windows.net/" primary_dfs_host = "vcloudlabcom.dfs.core.windows.net" primary_dfs_internet_endpoint = null primary_dfs_internet_host = null primary_dfs_microsoft_endpoint = null primary_dfs_microsoft_host = null primary_file_endpoint = "https://vcloudlabcom.file.core.windows.net/" primary_file_host = "vcloudlabcom.file.core.windows.net" primary_file_internet_endpoint = null primary_file_internet_host = null primary_file_microsoft_endpoint = null primary_file_microsoft_host = null primary_location = "eastus" primary_queue_endpoint = "https://vcloudlabcom.queue.core.windows.net/" primary_queue_host = "vcloudlabcom.queue.core.windows.net" primary_queue_microsoft_endpoint = null primary_queue_microsoft_host = null primary_table_endpoint = "https://vcloudlabcom.table.core.windows.net/" primary_table_host = "vcloudlabcom.table.core.windows.net" primary_table_microsoft_endpoint = null primary_table_microsoft_host = null primary_web_endpoint = "https://vcloudlabcom.z13.web.core.windows.net/" primary_web_host = "vcloudlabcom.z13.web.core.windows.net" primary_web_internet_endpoint = null primary_web_internet_host = null primary_web_microsoft_endpoint = null primary_web_microsoft_host = null public_network_access_enabled = true queue_encryption_key_type = "Service" resource_group_name = "test" secondary_access_key = (sensitive value) secondary_blob_connection_string = (sensitive value) secondary_blob_endpoint = null secondary_blob_host = null secondary_blob_internet_endpoint = null secondary_blob_internet_host = null secondary_blob_microsoft_endpoint = null secondary_blob_microsoft_host = null secondary_connection_string = (sensitive value) secondary_dfs_endpoint = null secondary_dfs_host = null secondary_dfs_internet_endpoint = null secondary_dfs_internet_host = null secondary_dfs_microsoft_endpoint = null secondary_dfs_microsoft_host = null secondary_file_endpoint = null secondary_file_host = null secondary_file_internet_endpoint = null secondary_file_internet_host = null secondary_file_microsoft_endpoint = null secondary_file_microsoft_host = null secondary_location = null secondary_queue_endpoint = null secondary_queue_host = null secondary_queue_microsoft_endpoint = null secondary_queue_microsoft_host = null secondary_table_endpoint = null secondary_table_host = null secondary_table_microsoft_endpoint = null secondary_table_microsoft_host = null secondary_web_endpoint = null secondary_web_host = null secondary_web_internet_endpoint = null secondary_web_internet_host = null secondary_web_microsoft_endpoint = null secondary_web_microsoft_host = null sftp_enabled = false shared_access_key_enabled = true table_encryption_key_type = "Service" tags = {} blob_properties { change_feed_enabled = true change_feed_retention_in_days = 7 default_service_version = null last_access_time_enabled = false versioning_enabled = false container_delete_retention_policy { days = 7 } delete_retention_policy { days = 7 permanent_delete_enabled = false } } queue_properties { hour_metrics { enabled = false include_apis = false retention_policy_days = 0 version = "1.0" } logging { delete = false read = false retention_policy_days = 0 version = "1.0" write = false } minute_metrics { enabled = false include_apis = false retention_policy_days = 0 version = "1.0" } } share_properties { retention_policy { days = 7 } } } Plan: 2 to import, 0 to add, 0 to change, 0 to destroy. azurerm_resource_group.rg: Importing... [id=/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/test] azurerm_resource_group.rg: Import complete [id=/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/test] azurerm_storage_account.sa: Importing... [id=/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/test/providers/Microsoft.Storage/storageAccounts/vcloudlabcom] azurerm_storage_account.sa: Import complete [id=/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/test/providers/Microsoft.Storage/storageAccounts/vcloudlabcom] Apply complete! Resources: 2 imported, 0 added, 0 changed, 0 destroyed. |
Conclusion: Why This Matters
The new import block fundamentally changes how we adopt Terraform in existing environments:
Declarative approach - Manage imports within configuration files
Auditability - Track imports via version control
Safety - Prevent accidental resource recreation
Team collaboration - Simplify onboarding to existing infrastructure
While the configuration generation is still experimental, this workflow dramatically reduces the manual effort required to bring existing Azure resources under Terraform management. By following this process, you'll achieve true infrastructure-as-code nirvana - where every resource is defined, versioned, and managed through Terraform.
Pro Tip: Always test imports in a non-production environment first!
Useful Articles
Create an Azure virtual machine scale set and load balancer using Terraform
Azure Terraform fixed Availibility Zones on Virtual Machine Scale Set
Writing and Using Terraform modules
Terraform Using one module variable in another module
Hashicorp Terraform dynamic block with example
Terraform for_each loop on map example
Terraform for_each loop on resource example
Terraform manage similar resources with for_each loop inside modules
Importing existing resources into Terraform - Step by Step
Importing already created module Infrastructure into Terraform and update state file
Conditionally create resources in Terraform







