In my last post, I looked at Terraform Actions. Terraform Actions are a new feature that allows you to trigger actions on resources, such as powering on/off a VM. As we discussed in the article, actions were one of multiple announcements at HashiConf 2025, and I’m determined to find the time to explore each one and document as I go!
In this post, we will explore Terraform Search at an introductory level together - errors and all!
Overview
Reading the HashiConf announcement post HashiCorp stated that the new search functionality would…
Discover and import resources in bulk more efficiently and accurately.
Sounds interesting! Importing has always been a little bit of a pain, though it has gotten better over the years, moving away from the old terraform import command to the newer import block approach. But when importing resources, you still need to poke about portals or APIs trying to identify the IDs of the resources you want to import one at a time, or use third-party tools such as aztfexport.
Terraform search (currently in Beta and available from Terraform 1.14) now brings an end-to-end workflow directly into Terraform to make this easier. Time to hit the docs…
The Basics
Reading over the documentation, it became apparent that this new functionality is dependant on a new resource type having been written into the provider. The high-level workflow is as follows:
- Define Queries - Use one or more
listblocks in your code to define the resources you want to import. - Execute the Search - Run a search using the
terraform queryto see what it finds. - Review the Search Results - Review the matching results and ensure you are happy with what you’ll be importing.
- Generate Code - Generate the Terraform HCL code and import blocks required to import the identified resources.
- Apply the Configuration - Apply the changes to import the resources into state as normal, allowing Terraform to manage them moving forward
Finding a Resource Type
As mentioned, the search workflow is dependant on a new resource type having been defined in the provider. So, let’s pop over to the AzureRM GitHub page and do a quick search to see if there is a new resource type we can use…
Bingo! Looks like both virtual network and storage accounts are supported. Let’s go check the registry documentation…
Time to give it a go.
Working Through an Example
Best way to hopefully understand this, is to work through an example and see where we get.
Prerequisites
We need to be running at least Terraform 1.14.0, so let’s download that and unzip it…
curl -LO https://releases.hashicorp.com/terraform/1.14.0-beta2/terraform_1.14.0-beta2_darwin_amd64.zip && unzip terraform_1.14.0-beta2_darwin_amd64.zip
I’m also going to need an existing resource that is not managed through Terraform. I’m going to use a storage account for this example. In the words of Blue Peter… “Here’s one I made earlier…”
Define the Queries
Next, I need to define the queries using the new list block. Let’s plonk some code into main.tf to search based on a specific resource group…
# main.tf
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "4.49.0"
}
}
}
provider "azurerm" {
features {}
subscription_id = "4595d981-87d1-4772-bd3a-1f5471da6c24"
}
list "azurerm_storage_account" "example" {
provider = azurerm
config {
resource_group_name = "general-storage"
}
}
And give terraform init a whirl…
Oh ☹️ - not what I was expecting to see, but I guess that’s what happens when you don’t read the documentation properly! Poking about a bit more, I realised the new list block needs to go in a .tfquery.hcl file as documented here.
So, let’s move that and try an init again…
# main.tf
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "4.49.0"
}
}
}
provider "azurerm" {
features {}
subscription_id = "4595d981-87d1-4772-bd3a-1f5471da6c24"
}
# test.tfquery.hcl
list "azurerm_storage_account" "example" {
provider = azurerm
config {
resource_group_name = "general-storage"
}
}
A terraform init now works as expected…
What about a terraform plan? I’ve nothing defined, so presumably nothing is going to be flagged here…
As I expected! Good. Moving on.
Running a Query
Let’s now try the new terraform query command and see what happens.
Success! Not the most helpful of outputs just yet, but on the plus side,I have gotten the name, resource group and subscription ID without having to poke about the portal. Now, what was all this about config generation?!
Generating Configuration
Using the -generate-config-out=<filename> flag, we should be able to have Terraform generate the HCL code and import blocks for the resources matching our query. Let’s give it a try:
terraform query -generate-config-out=test.tf
Well, there are no errors so that’s a good start, and I can see a new test.tf file. Let’s take a look at the contents (no judging me on poor security settings - it’s a demo environment after all 😉)…
# test.tf
# __generated__ by Terraform
# Please review these resources and move them into your main configuration files.
# __generated__ by Terraform
resource "azurerm_storage_account" "example_0" {
provider = azurerm
access_tier = "Hot"
account_kind = "StorageV2"
account_replication_type = "LRS"
account_tier = "Standard"
allow_nested_items_to_be_public = true
allowed_copy_scope = ""
cross_tenant_replication_enabled = true
default_to_oauth_authentication = true
dns_endpoint_type = "Standard"
edge_zone = ""
https_traffic_only_enabled = false
id = "/subscriptions/4595d981-87d1-4772-bd3a-1f5471da6c24/resourceGroups/general-storage/providers/Microsoft.Storage/storageAccounts/mikeguy"
infrastructure_encryption_enabled = true
is_hns_enabled = false
large_file_share_enabled = false
local_user_enabled = true
location = "uksouth"
min_tls_version = "TLS1_0"
name = "mikeguy"
nfsv3_enabled = false
primary_access_key = null # sensitive
primary_blob_connection_string = null # sensitive
primary_blob_endpoint = "https://mikeguy.blob.core.windows.net/"
primary_blob_host = "mikeguy.blob.core.windows.net"
primary_blob_internet_endpoint = ""
primary_blob_internet_host = ""
primary_blob_microsoft_endpoint = ""
primary_blob_microsoft_host = ""
primary_connection_string = null # sensitive
primary_dfs_endpoint = "https://mikeguy.dfs.core.windows.net/"
primary_dfs_host = "mikeguy.dfs.core.windows.net"
primary_dfs_internet_endpoint = ""
primary_dfs_internet_host = ""
primary_dfs_microsoft_endpoint = ""
primary_dfs_microsoft_host = ""
primary_file_endpoint = "https://mikeguy.file.core.windows.net/"
primary_file_host = "mikeguy.file.core.windows.net"
primary_file_internet_endpoint = ""
primary_file_internet_host = ""
primary_file_microsoft_endpoint = ""
primary_file_microsoft_host = ""
primary_location = "uksouth"
primary_queue_endpoint = "https://mikeguy.queue.core.windows.net/"
primary_queue_host = "mikeguy.queue.core.windows.net"
primary_queue_microsoft_endpoint = ""
primary_queue_microsoft_host = ""
primary_table_endpoint = "https://mikeguy.table.core.windows.net/"
primary_table_host = "mikeguy.table.core.windows.net"
primary_table_microsoft_endpoint = ""
primary_table_microsoft_host = ""
primary_web_endpoint = "https://mikeguy.z33.web.core.windows.net/"
primary_web_host = "mikeguy.z33.web.core.windows.net"
primary_web_internet_endpoint = ""
primary_web_internet_host = ""
primary_web_microsoft_endpoint = ""
primary_web_microsoft_host = ""
provisioned_billing_model_version = ""
public_network_access_enabled = true
queue_encryption_key_type = "Service"
resource_group_name = "general-storage"
secondary_access_key = null # sensitive
secondary_blob_connection_string = null # sensitive
secondary_blob_endpoint = ""
secondary_blob_host = ""
secondary_blob_internet_endpoint = ""
secondary_blob_internet_host = ""
secondary_blob_microsoft_endpoint = ""
secondary_blob_microsoft_host = ""
secondary_connection_string = null # sensitive
secondary_dfs_endpoint = ""
secondary_dfs_host = ""
secondary_dfs_internet_endpoint = ""
secondary_dfs_internet_host = ""
secondary_dfs_microsoft_endpoint = ""
secondary_dfs_microsoft_host = ""
secondary_file_endpoint = ""
secondary_file_host = ""
secondary_file_internet_endpoint = ""
secondary_file_internet_host = ""
secondary_file_microsoft_endpoint = ""
secondary_file_microsoft_host = ""
secondary_location = ""
secondary_queue_endpoint = ""
secondary_queue_host = ""
secondary_queue_microsoft_endpoint = ""
secondary_queue_microsoft_host = ""
secondary_table_endpoint = ""
secondary_table_host = ""
secondary_table_microsoft_endpoint = ""
secondary_table_microsoft_host = ""
secondary_web_endpoint = ""
secondary_web_host = ""
secondary_web_internet_endpoint = ""
secondary_web_internet_host = ""
secondary_web_microsoft_endpoint = ""
secondary_web_microsoft_host = ""
sftp_enabled = false
shared_access_key_enabled = true
table_encryption_key_type = "Service"
tags = {}
blob_properties {
change_feed_enabled = false
change_feed_retention_in_days = 0
default_service_version = ""
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 = true
include_apis = true
retention_policy_days = 7
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
}
}
timeouts {
create = null
delete = null
read = null
update = null
}
}
import {
to = azurerm_storage_account.example_0
provider = azurerm
identity = {
name = "mikeguy"
resource_group_name = "general-storage"
subscription_id = "4595d981-87d1-4772-bd3a-1f5471da6c24"
}
}
Whilst pretty verbose (these import tools always tend to be), it largely looks ok. There are clearly a load of defaults we could clean up, and some attributes that are going to cause problems (as they aren’t configuration items - e.g. id), but let’s give the plan/apply a whirl anyway…
Not quite what we’d hoped, but as I expected!
A tonne of different errors relating to empty strings, deprecated fields, state-only fields, and so on. Not quite perfect yet, and like many of the other tools, some tidy up work is needed.
Let’s give test.tf a bit of TLC and remove the state only fields, and loads of the empty strings/defaults…
# test.tf
resource "azurerm_storage_account" "example_0" {
access_tier = "Hot"
account_kind = "StorageV2"
account_replication_type = "LRS"
account_tier = "Standard"
allow_nested_items_to_be_public = true
cross_tenant_replication_enabled = true
default_to_oauth_authentication = true
dns_endpoint_type = "Standard"
https_traffic_only_enabled = false
infrastructure_encryption_enabled = true
is_hns_enabled = false
large_file_share_enabled = false
local_user_enabled = true
location = "uksouth"
min_tls_version = "TLS1_0"
name = "mikeguy"
nfsv3_enabled = false
public_network_access_enabled = true
queue_encryption_key_type = "Service"
resource_group_name = "general-storage"
sftp_enabled = false
shared_access_key_enabled = true
}
import {
to = azurerm_storage_account.example_0
provider = azurerm
identity = {
name = "mikeguy"
resource_group_name = "general-storage"
subscription_id = "4595d981-87d1-4772-bd3a-1f5471da6c24"
}
}
Trying the plan again…
Doesn’t seem to like the fact I’ve removed the provider argument, so let’s just add that back in and try again…
Looks better 😀. Let’s carry on with an apply…
And there we have it. We’ve successfully imported the storage account through the new workflow, and it can now be managed as normal through Terraform.
Conclusion
Whilst we’ve only looked at a single resource type example in this post, you can see how if you had a load of different things to import and generate config for, that this will be useful. At least it will, once the support trickles into the providers which is the main downside of this approach.
One to keep your eye on as time progresses!












